/* * Field Repeater plugin * * Data attributes: * - data-control="fieldrepeater" - enables the plugin on an element * - data-option="value" - an option with a value * * JavaScript API: * $('a#someElement').fieldRepeater({...}) */ +function ($) { "use strict"; var Base = $.wn.foundation.base, BaseProto = Base.prototype // FIELD REPEATER CLASS DEFINITION // ============================ var Repeater = function(element, options) { this.options = options this.$el = $(element) if (this.options.sortable) { this.$sortable = $(options.sortableContainer, this.$el) } $.wn.foundation.controlUtils.markDisposable(element) Base.call(this) this.init() } Repeater.prototype = Object.create(BaseProto) Repeater.prototype.constructor = Repeater Repeater.DEFAULTS = { sortableHandle: '.repeater-item-handle', sortableContainer: 'ul.field-repeater-items', titleFrom: null, minItems: null, maxItems: null, sortable: false, mode: 'list', style: 'default', } Repeater.prototype.init = function() { if (this.options.sortable) { this.bindSorting() } this.$el.on('ajaxDone', '> .field-repeater-items > .field-repeater-item > .repeater-item-remove > [data-repeater-remove]', this.proxy(this.onRemoveItemSuccess)) this.$el.on('ajaxDone', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add]', this.proxy(this.onAddItemSuccess)) this.$el.on('click', '> ul > li > .repeater-item-collapse .repeater-item-collapse-one', this.proxy(this.toggleCollapse)) this.$el.on('click', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add-group]', this.proxy(this.clickAddGroupButton)) this.$el.find('> ul > li > .repeater-item-collapsed-title') .css({'cursor': 'pointer', 'width': '100%'}) .on('click', this.proxy(this.toggleCollapse)) this.$el.find('> ul > li > .field-repeater-form > .form-group > label') .css({'cursor': 'pointer', 'width': '100%'}) .on('click', this.proxy(this.toggleCollapse)) this.$el.one('dispose-control', this.proxy(this.dispose)) this.togglePrompt() this.applyStyle() } Repeater.prototype.dispose = function() { if (this.options.sortable) { this.$sortable.sortable('destroy') } this.$el.off('ajaxDone', '> .field-repeater-items > .field-repeater-item > .repeater-item-remove > [data-repeater-remove]', this.proxy(this.onRemoveItemSuccess)) this.$el.off('ajaxDone', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add]', this.proxy(this.onAddItemSuccess)) this.$el.off('click', '> ul > li > .repeater-item-collapse .repeater-item-collapse-one', this.proxy(this.toggleCollapse)) this.$el.off('click', '> ul > li > .repeater-item-collapsed-title', this.proxy(this.toggleCollapse)) this.$el.off('click', '> ul > li > .field-repeater-form > .form-group > label', this.proxy(this.toggleCollapse)) this.$el.off('click', '> .field-repeater-items > .field-repeater-add-item > [data-repeater-add-group]', this.proxy(this.clickAddGroupButton)) this.$el.off('dispose-control', this.proxy(this.dispose)) this.$el.removeData('oc.repeater') this.$el = null this.$sortable = null this.options = null BaseProto.dispose.call(this) } // Deprecated Repeater.prototype.unbind = function() { this.dispose() } Repeater.prototype.bindSorting = function() { var sortableOptions = { handle: this.options.sortableHandle, nested: false, vertical: this.options.mode === 'list', } this.$sortable.sortable(sortableOptions) } Repeater.prototype.clickAddGroupButton = function(ev) { var $self = this var templateHtml = $('> [data-group-palette-template]', this.$el).html(), $target = $(ev.target), $form = this.$el.closest('form'), $loadContainer = $target.closest('.loading-indicator-container') $target.ocPopover({ content: templateHtml }) var $container = $target.data('oc.popover').$container // Initialize the scrollpad control in the popup $container.trigger('render') $container .on('click', 'a', function (ev) { setTimeout(function() { $(ev.target).trigger('close.oc.popover') }, 1) }) .on('ajaxPromise', '[data-repeater-add]', function(ev, context) { $loadContainer.loadIndicator() $(window).one('ajaxUpdateComplete', function() { $loadContainer.loadIndicator('hide') $self.togglePrompt() $($self.$el).find('.field-repeater-items > .field-repeater-add-item').each(function () { if ($(this).children().length === 0) { $(this).remove() } }) }) }) $('[data-repeater-add]', $container).data('request-form', $form) // Setup search functionality var $searchInput = $('.repeater-group-search', $container) var $clearButton = $('.repeater-group-search-clear', $container) var $noResults = $('.repeater-group-no-results', $container) var $itemsContainer = $('.repeater-group-items-container', $container) var $listItems = $('.repeater-group-item', $container) // Add hover effects to grid items $listItems.on('mouseenter', 'a', function() { $(this).css({ 'border-color': '#3498db', 'box-shadow': '0 2px 8px rgba(0,0,0,0.1)', 'transform': 'translateY(-2px)' }) }).on('mouseleave', 'a', function() { $(this).css({ 'border-color': '#e0e0e0', 'box-shadow': 'none', 'transform': 'translateY(0)' }) }) $searchInput.on('input', function() { var searchTerm = $(this).val().toLowerCase().trim() // Show/hide clear button $clearButton.toggle(searchTerm.length > 0) if (searchTerm === '') { // Show all items $listItems.show() $noResults.hide() $itemsContainer.show() return } var visibleCount = 0 // Filter items $listItems.each(function() { var $item = $(this) var title = $('.title', $item).text().toLowerCase() var description = $('.description', $item).text().toLowerCase() if (title.indexOf(searchTerm) !== -1 || description.indexOf(searchTerm) !== -1) { $item.show() visibleCount++ } else { $item.hide() } }) // Show/hide no results message and adjust container height if (visibleCount === 0) { $noResults.show() $itemsContainer.hide() } else { $noResults.hide() $itemsContainer.show() } }) // Clear search functionality $clearButton.on('click', function() { $searchInput.val('').trigger('input').focus() }) // Focus search input when popover opens setTimeout(function() { $searchInput.focus() }, 100) } Repeater.prototype.onRemoveItemSuccess = function(ev) { var $target = $(ev.target) // Allow any widgets inside a deleted item to be disposed $target.closest('.field-repeater-item').find('[data-disposable]').each(function () { var $elem = $(this), control = $elem.data('control'), widget = $elem.data('oc.' + control) if (widget && typeof widget['dispose'] === 'function') { widget.dispose() } }) $target.closest('[data-field-name]').trigger('change.oc.formwidget') $target.closest('.field-repeater-item').remove() this.togglePrompt() } Repeater.prototype.onAddItemSuccess = function(ev) { window.requestAnimationFrame(() => { this.togglePrompt() $(ev.target).closest('[data-field-name]').trigger('change.oc.formwidget') $(this.$el).find('.field-repeater-items > .field-repeater-add-item').each(function () { if ($(this).children().length === 0) { $(this).remove() } }) }) } Repeater.prototype.togglePrompt = function () { if (this.options.minItems && this.options.minItems > 0) { var repeatedItems = this.$el.find('> .field-repeater-items > .field-repeater-item').length, $removeItemBtn = this.$el.find('> .field-repeater-items > .field-repeater-item > .repeater-item-remove') $removeItemBtn.toggleClass('disabled', !(repeatedItems > this.options.minItems)) } if (this.options.maxItems && this.options.maxItems > 0) { var repeatedItems = this.$el.find('> .field-repeater-items > .field-repeater-item').length, $addItemBtn = this.$el.find('> .field-repeater-items > .field-repeater-add-item') $addItemBtn.toggle(repeatedItems < this.options.maxItems) } } Repeater.prototype.toggleCollapse = function(ev) { var $item = $(ev.target).closest('.field-repeater-item'), isCollapsed = $item.hasClass('collapsed') ev.preventDefault() if (this.getStyle() === 'accordion') { if (isCollapsed) { this.expand($item) } return } if (ev.ctrlKey || ev.metaKey) { isCollapsed ? this.expandAll() : this.collapseAll() } else { isCollapsed ? this.expand($item) : this.collapse($item) } } Repeater.prototype.collapseAll = function() { var self = this, items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item') $.each(items, function(key, item){ self.collapse($(item)) }) } Repeater.prototype.expandAll = function() { var self = this, items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item') $.each(items, function(key, item){ self.expand($(item)) }) } Repeater.prototype.collapse = function($item) { $item.addClass('collapsed') $('.repeater-item-collapsed-title', $item).text(this.getCollapseTitle($item)) } Repeater.prototype.expand = function($item) { if (this.getStyle() === 'accordion') { this.collapseAll() } $item.removeClass('collapsed') } Repeater.prototype.getCollapseTitle = function($item) { var $target, defaultText = '', explicitText = $item.data('collapse-title'), selector = 'input[type=text], select:first, ul:first' if (explicitText) { return explicitText } if (this.options.titleFrom) { $target = $('[data-field-name="'+this.options.titleFrom+'"]', $item) if ($target.length) { selector = 'input, select:first, ul:first' } } if (!$target || !$target.length) { $target = $item } var $textInput = $(selector, $target).first() if ($textInput.length) { switch($textInput.prop("tagName")) { case 'SELECT': return $textInput.find('option:selected').text() case 'UL': return $textInput.find('li.active').text() default: return $textInput.val() } } else { var $disabledTextInput = $('.form-control:first', $target) if ($disabledTextInput.length) { return $disabledTextInput.text() } } return defaultText } Repeater.prototype.getStyle = function() { var style = 'default' // Validate style if (this.options.style && ['collapsed', 'accordion'].indexOf(this.options.style) !== -1) { style = this.options.style } return style } Repeater.prototype.applyStyle = function() { if (this.options.mode === 'grid') { return } var style = this.getStyle(), self = this, items = $(this.$el).children('.field-repeater-items').children('.field-repeater-item') $.each(items, function(key, item) { switch (style) { case 'collapsed': self.collapse($(item)) break case 'accordion': if (key !== 0) { self.collapse($(item)) } break } }) } // FIELD REPEATER PLUGIN DEFINITION // ============================ var old = $.fn.fieldRepeater $.fn.fieldRepeater = function (option) { var args = Array.prototype.slice.call(arguments, 1), result this.each(function () { var $this = $(this) var data = $this.data('oc.repeater') var options = $.extend({}, Repeater.DEFAULTS, $this.data(), typeof option == 'object' && option) if (!data) $this.data('oc.repeater', (data = new Repeater(this, options))) if (typeof option == 'string') result = data[option].apply(data, args) if (typeof result != 'undefined') return false }) return result ? result : this } $.fn.fieldRepeater.Constructor = Repeater // FIELD REPEATER NO CONFLICT // ================= $.fn.fieldRepeater.noConflict = function () { $.fn.fieldRepeater = old return this } // FIELD REPEATER DATA-API // =============== $(document).render(function() { $('[data-control="fieldrepeater"]').fieldRepeater() }) }(window.jQuery);