Spade

Mini Shell

Directory:~$ /home/lmsyaran/public_html/joomla5/media/com_fabrik/js/
Upload File

[Home] [System Details] [Kill Me]
Current File:~$ /home/lmsyaran/public_html/joomla5/media/com_fabrik/js/form.js

/**
 * Form
 *
 * @copyright: Copyright (C) 2005-2013, fabrikar.com - All rights reserved.
 * @license:   GNU/GPL http://www.gnu.org/copyleft/gpl.html
 */

define(['jquery', 'fab/encoder',
'fab/fabrik',
'lib/debounce/jquery.ba-throttle-debounce'],
    function (jQuery, Encoder, Fabrik, Debounce) {
    var FbForm = new Class({

        Implements: [Options, Events],

        options: {
            'rowid'         : '',
            'admin'         : false,
            'ajax'          : false,
            'primaryKey'    : null,
            'error'         : '',
            'submitOnEnter' : false,
            'updatedMsg'    : 'Form saved',
            'pages'         : [],
            'start_page'    : 0,
            'multipage_save': 0,
            'ajaxValidation': false,
            'showLoader'    : false,
            'customJsAction': '',
            'plugins'       : {},
            'ajaxmethod'    : 'post',
            'inlineMessage' : true,
            'print'         : false,
            'toggleSubmit'  : false,
            'toggleSubmitTip': 'must validate',
            'mustValidate'  : false,
            'lang'          : false,
            'debounceDelay' : 500,
            'images'        : {
                'alert'       : '',
                'action_check': '',
                'ajax_loader' : ''
            }
        },

        initialize: function (id, options) {
            // $$$ hugh - seems options.rowid can be null in certain corner
cases, so defend against that
            if (typeOf(options.rowid) === 'null') {
                options.rowid = '';
            }
            this.id = id;
            //set this to false in window.fireEvents to stop current action
(e.g. stop form submission)
            this.result = true;
            this.setOptions(options);
            this.options.pages = $H(this.options.pages);
            this.subGroups = $H({});
            this.currentPage = this.options.start_page;
            this.formElements = $H({});
            this.hasErrors = $H({});
            this.mustValidateEls = $H({});
            this.toggleSubmitTipAdded = false;
            this.elements = this.formElements;
            this.duplicatedGroups = $H({});
            this.addingOrDeletingGroup = false;
            this.addedGroups = [];
	        this.watchRepeatNumsDone = false;
            this.fx = {};
            this.fx.elements = [];
            this.fx.hidden = [];
            this.fx.validations = {};
            this.setUpAll();
            this._setMozBoxWidths();

            if (this.options.editable) {
                (function () {
                    this.duplicateGroupsToMin();
                }.bind(this)).delay(1000);
            }

            // Delegated element events
            this.events = {};

            this.submitBroker = new FbFormSubmit();
            this.scrollTips();
            Fabrik.fireEvent('fabrik.form.loaded', [this]);
        },

        _setMozBoxWidths: function () {
            if (Browser.firefox && this.getForm()) {
                //as firefox treats display:-moz-box as
display:-moz-box-inline we have to programatically set their widths
                this.getForm().getElements('.fabrikElementContainer
> .displayBox').each(function (b) {
                    var computed = b.getParent().getComputedSize();
                    //remove margins/paddings from width
                    var x = b.getParent().getSize().x -
(computed.computedLeft + computed.computedRight);
                    var w = b.getParent().getSize().x === 0 ? 400 : x;
                    b.setStyle('width', w + 'px');
                    var e = b.getElement('.fabrikElement');
                    if (typeOf(e) !== 'null') {
                        x = 0;
                        b.getChildren().each(function (c) {
                            if (c !== e) {
                                x += c.getSize().x;
                            }
                        });
                        e.setStyle('width', w - x - 10 +
'px');
                    }

                });
            }
        },

        setUpAll: function () {
            this.setUp();

	        // add a wrapper if we're going to be using the tooltip, as
can't do tooltip on disabled elements
	        if (this.options.ajaxValidation &&
this.options.toggleSubmit && this.options.toggleSubmitTip !==
'') {
		        var submit = this._getButton('Submit');
		        if (typeOf(submit) !== 'null') {
			        jQuery(submit).wrap('<div
data-bs-toggle="tooltip" title="' +
Joomla.JText._('COM_FABRIK_MUST_VALIDATE') +
                        '" class="fabrikSubmitWrapper"
style="display: inline-block"></div>div>');
		        }
	        }

            this.winScroller = new Fx.Scroll(window);
            if (this.form) {
                if (this.options.ajax || this.options.submitOnEnter ===
false) {
                    this.stopEnterSubmitting();
                }
                this.watchAddOptions();
            }

            if (this.options.editable) {
                $H(this.options.hiddenGroup).each(function (v, k) {
                    if (v === true &&
typeOf(document.id('group' + k)) !== 'null') {
                        var subGroup = document.id('group' +
k).getElement('.fabrikSubGroup');
                        this.subGroups.set(k, subGroup.cloneWithIds());
                        this.hideLastGroup(k, subGroup);
                    }
                }.bind(this));

                this.setupSortable();
            }

            // get an int from which to start incrementing for each
repeated group id
            // don't ever decrease this value when deleting a group as
it will cause all sorts of
            // reference chaos with cascading dropdowns etc.
            this.repeatGroupMarkers = $H({});
            if (this.form) {
               
this.form.getElements('.fabrikGroup').each(function (group) {
                    var id = group.id.replace('group',
'');
                    var c =
group.getElements('.fabrikSubGroup').length;
                    //if no joined repeating data then c should be 0 and
not 1
                    if (c === 1) {
                        if
(group.getElement('.fabrikSubGroupElements').getStyle('display')
=== 'none') {
                            c = 0;
                        }
                    }
                    this.repeatGroupMarkers.set(id, c);
                }.bind(this));
                this.watchGoBackButton();
            }


            this.watchPrintButton();
            this.watchPdfButton();
            this.watchTabs();
            this.watchRepeatNums();
        },

        watchRepeatNums: function () {
	        Fabrik.addEvent('fabrik.form.elements.added', function
(form) {
	            if (form.id === this.id && !this.watchRepeatNumsDone)
{
		            Object.each(this.options.numRepeatEls, function (name, key) {
			            if (name !== '') {
				            var el = this.formElements.get(name);
				            if (el) {
                                el.addNewEventAux(el.getChangeEvent(),
function(event) {
                                    var v = el.getValue();
                                    this.options.minRepeat[key] =
v.toInt();
	                                this.options.maxRepeat[key] = v.toInt();
	                                this.duplicateGroupsToMin();
                                }.bind(this, el, key));
				            }
			            }
		            }.bind(form));
		            this.watchRepeatNumsDone = true;
	            }
	        }.bind(this));
        },

        /**
         * Print button action - either open up the print preview window -
or print if already opened
         */
        watchPrintButton: function () {
            if (this.form) {
				this.form.getElements('a[data-fabrik-print]').addEvent('click',
function (e) {
                e.stop();
                if (this.options.print) {
                    window.print();
                } else {
                    // Build URL as we could have changed the rowid via
ajax pagination
                    var url = jQuery(e.target).prop('href');
                    url = url.replace(/&rowid=\d+/,
'&rowid=' + this.options.rowid);
                    if (this.options.lang !== false) {
                        if (url.test(/\?/)) {
	                        url += '&lang=' + this.options.lang;
                        }
                        else {
	                        url += '?lang=' + this.options.lang;
                        }
                    }
                    window.open(
                        url,
                        'win2',
                       
'status=no,toolbar=no,scrollbars=yes,titlebar=no,menubar=no,resizable=yes,width=400,height=350,directories=no,location=no;'
                    );
                }
            }.bind(this));
			}
        },

        /**
         * PDF button action.
         */
        watchPdfButton: function () {
            if (this.form) {
			this.form.getElements('*[data-role="open-form-pdf"]').addEvent('click',
function (e) {
                e.stop();
                // Build URL as we could have changed the rowid via ajax
pagination.
                // @FIXME for SEF
                var url = e.event.currentTarget.href.replace(/(rowid=\d*)/,
'rowid=' + this.options.rowid);
                if (this.options.lang !== false) {
	                if (url.test(/\?/)) {
		                url += '&lang=' + this.options.lang;
	                }
	                else {
		                url += '?lang=' + this.options.lang;
	                }                }
                window.location = url;
            }.bind(this));
			}
        },

        /**
         * Go back button in ajax pop up window should close the window
         */
        watchGoBackButton: function () {
            if (this.options.ajax) {
                var goback = this._getButton('Goback');
                if (typeOf(goback) === 'null') {
                    return;
                }
                goback.addEvent('click', function (e) {
                    e.stop();
                    if (Fabrik.Windows[this.options.fabrik_window_id]) {
                       
Fabrik.Windows[this.options.fabrik_window_id].close();
                    }
                    else {
                        // $$$ hugh -
http://fabrikar.com/forums/showthread.php?p=166140#post166140
                        window.history.back();
                    }
                }.bind(this));
            }
        },

        watchAddOptions: function () {
            this.fx.addOptions = [];
           
this.getForm().getElements('.addoption').each(function (d) {
                var a =
d.getParent('.fabrikElementContainer').getElement('.toggle-addoption');
                var mySlider = new Fx.Slide(d, {
                    duration: 500
                });
                mySlider.hide();
                a.addEvent('click', function (e) {
                    e.stop();
                    mySlider.toggle();
                });
            });
        },

        setUp: function () {
            this.form = this.getForm();
            this.watchGroupButtons();
            // Submit can appear in confirmation plugin even when readonly
            this.watchSubmit();
            this.createPages();
            this.watchClearSession();
        },

        getForm: function () {
            if (typeOf(this.form) === 'null') {
                this.form = document.id(this.getBlock());
            }

            return this.form;
        },

        getBlock: function () {
            if (typeOf(this.block) === 'null') {
                this.block = this.options.editable === true ?
'form_' + this.id : 'details_' + this.id;
                if (this.options.rowid !== '') {
                    this.block += '_' + this.options.rowid;
                }
            }

            return this.block;
        },

        /**
         * Attach an effect to an elements
         *
         * @param {string}  id      Element or group to apply the fx TO,
triggered from another element
         * @param {string}  method  JS event which triggers the effect
(click,change etc.)
         *
         * @return {*} false if no element found or element fx
         */
        addElementFX: function (id, method) {
            var c, k, fxdiv;
            id = id.replace('fabrik_trigger_', '');
            // Paul - add sanity checking and error reporting
            if (id.slice(0, 6) === 'group_') {
                id = id.slice(6, id.length);
                k = id;
                c = document.id(id);

                if (!c) {
                    fconsole('Fabrik form::addElementFX: Group
"' + id + '" does not exist.');
                    return false;
                }
            } else if (id.slice(0, 8) === 'element_') {
                id = id.slice(8, id.length);
                k = 'element' + id;
                c = document.id(id);
                if (!c) {
                    fconsole('Fabrik form::addElementFX: Element
"' + id + '" does not exist.');
                    return false;
                }
                c = c.getParent('.fabrikElementContainer');
                if (!c) {
                    fconsole('Fabrik form::addElementFX: Element
"' + id + '.fabrikElementContainer" does not
exist.');
                    return false;
                }
            } else {
                fconsole('Fabrik form::addElementFX: Not an element or
group: ' + id);
                return false;
            }
            if (c) {
                // c will be the <li> element - you can't apply
fx's to this as it makes the
                // DOM squiffy with multi column rows, so get the li's
content and put it
                // inside a div which is injected into c
                // apply fx to div rather than li - damn I'm good
                var tag = (c).get('tag');
                if (tag === 'li' || tag === 'td') {
                    fxdiv = new Element('div',
{'style': 'width:100%'}).adopt(c.getChildren());
                    c.empty();
                    fxdiv.inject(c);
                } else {
                    fxdiv = c;
                }

                var opts = {
                    duration  : 800,
                    transition: Fx.Transitions.Sine.easeInOut
                };
                if (typeOf(this.fx.elements[k]) === 'null') {
                    this.fx.elements[k] = {};
                }

                this.fx.elements[k].css = new Fx.Morph(fxdiv, opts);

                if (typeOf(fxdiv) !== 'null' && (method
=== 'slide in' || method === 'slide out' || method ===
'slide toggle')) {
                    this.fx.elements[k].slide = new Fx.Slide(fxdiv, opts);
                }

                return this.fx.elements[k];
            }
            return false;
        },

        /**
         * An element state has changed, so lets run any associated effects
         *
         * @param   {string}  id            Element id to run the effect on
         * @param   {string}  method        Method to run
         * @param   {object}  elementModel  The element JS object which is
calling the fx, this is used to work ok which
         *                                  repeat group the fx is applied
on
         */

        doElementFX: function (id, method, elementModel) {
            var k, groupfx, fx, fxElement;

            // Could be the source element is in a repeat group but the
target is not.
            var target =
this.formElements.get(id.replace('fabrik_trigger_element_',
'')),
                targetInRepeat = true;
            if (target) {
                targetInRepeat = target.options.inRepeatGroup;
            }

            if (id.slice(0, 21) === 'fabrik_trigger_group_') {
                groupfx = true;
            }
            else {
                groupfx = false;
            }

            // Update the element id that we will apply the fx to to be
that of the calling elementModels group
            // (if in a repeat group)
            if (elementModel && targetInRepeat && !groupfx)
{
                if (elementModel.options.inRepeatGroup) {
                    var bits = id.split('_');
                    bits[bits.length - 1] =
elementModel.options.repeatCounter;
                    id = bits.join('_');
                }
            }

            // Create the fx key
            id = id.replace('fabrik_trigger_', '');
            if (id.slice(0, 6) === 'group_') {
                id = id.slice(6, id.length);
                // weird fix?
                if (id.slice(0, 6) === 'group_') {
                    id = id.slice(6, id.length);
                }
                k = id;
            } else {
                id = id.slice(8, id.length);
                k = 'element' + id;
            }

            // Get the stored fx
            fx = this.fx.elements[k];
            if (!fx) {
                // A group was duplicated but no element FX added, lets try
to add it now
                fx = this.addElementFX('element_' + id, method);

                // If it wasn't added then lets get out of here
                if (!fx) {
                    return;
                }
            }
            // Seems dropdown element fx.css.element is already the
container
            if (groupfx ||
fx.css.element.hasClass('fabrikElementContainer')) {
                fxElement = fx.css.element;
            } else {
                fxElement =
fx.css.element.getParent('.fabrikElementContainer');
            }

            // For repeat groups rendered as tables we cant apply fx on td
so get child
            if (fxElement.get('tag') === 'td') {
                fxElement = fxElement.getChildren()[0];
            }
            switch (method) {
                case 'show':
                   
fxElement.fade('show').removeClass('fabrikHide');
                    if (groupfx) {
                        // strange fix for ie8
                        //
http://fabrik.unfuddle.com/projects/17220/tickets/by_number/703?cycle=true
                       
document.id(id).getElements('.fabrikinput').setStyle('opacity',
'1');
                        this.showGroupTab(id);
                        // if it was hidden by group's "Show
Group" setting ("Yes, but hidden"), need to show()
                        fxElement.show();
                    }
                    break;
                case 'hide':
                   
fxElement.fade('hide').addClass('fabrikHide');
                    if (groupfx) {
                        this.hideGroupTab(id);
                    }
                    break;
                case 'fadein':
                    fxElement.removeClass('fabrikHide');
                    if (fx.css.lastMethod !== 'fadein') {
                        fx.css.element.show();
                        fx.css.start({'opacity': [0, 1]});
                    }
	                if (groupfx) {
		                this.showGroupTab(id);
                        fxElement.show();
	                }
                    break;
                case 'fadeout':
                    if (fx.css.lastMethod !== 'fadeout') {
                        fx.css.start({'opacity': [1,
0]}).chain(function () {
                            fx.css.element.hide();
                            fxElement.addClass('fabrikHide');
                        });
                    }
	                if (groupfx) {
		                this.hideGroupTab(id);
	                }
                    break;
                case 'slide in':
                    fx.slide.slideIn();
                    break;
                case 'slide out':
                    fx.slide.slideOut();
                    fxElement.removeClass('fabrikHide');
                    break;
                case 'slide toggle':
                    fx.slide.toggle();
                    break;
                case 'clear':
                    this.formElements.get(id).clear();
                    break;
                case 'disable':
                    if (!groupfx) {
                        jQuery('#' +
id).prop('disabled', true);
                    }
                    break;
                case 'enable':
                    if (!groupfx) {
                        jQuery('#' +
id).prop('disabled', false);
                    }
                    break;
                case 'readonly':
                    if (!groupfx) {
                        // can't "readonly" a select, so
disable all but selected option instead
                        if (jQuery('#' +
id).prop('tagName') === 'SELECT') {
                            jQuery('#' + id + '
option:not(:selected)').attr('disabled', true);
                        }
                        else {
                            jQuery('#' +
id).prop('readonly', true);
                        }
                    }
                    break;
                case 'notreadonly':
                    if (!groupfx) {
                        if (jQuery('#' +
id).prop('tagName') === 'SELECT') {
                            jQuery('#' + id + '
option').attr('disabled', false);
                        }
                        else {
                            jQuery('#' +
id).prop('readonly', false);
                        }
                    }
                    break;
            }
            fx.lastMethod = method;
            Fabrik.fireEvent('fabrik.form.doelementfx', [this,
method, id, groupfx]);
        },

        /**
         * Get a group's tab, if it exists
         *
         * These tab functions are currently just helpers for user scripts
         *
         * @param  {string}  groupId  group ID
         *
         * @return tab | false
         */
        getGroupTab: function (groupId) {
            if (!groupId.test(/^group/)) {
                groupId = 'group' + groupId;
            }
            var tab_button =
document.getElementById(groupId+'_tab');
            return tab_button == null ? false : tab_button;
        },

        /**
         * Hide a group's tab, if it exists
         *
         * @param  {string}  groupId
         */
        hideGroupTab: function (groupId) {
            var tab = this.getGroupTab(groupId);
            if (tab !== false) {
                jQuery(tab).hide();
                if (tab.hasClass('active')) {
                    if (tab.getPrevious()) {
                       
jQuery(tab.getPrevious().getFirst()).tab('show');
                    }
                    else if (tab.getNext()) {
                       
jQuery(tab.getNext().getFirst()).tab('show');
                    }
                }
            }
        },

        /**
         * Hide a group's tab, if it exists
         *
         * @param  {string}  groupId
         */
        selectGroupTab: function (groupId) {
            var tab = this.getGroupTab(groupId);
            if (tab !== false) {
                if (!tab.hasClass('active')) {
                    jQuery(tab.getFirst()).tab('show');
                }
            }
        },

        /**
         * Hide a group's tab, if it exists
         *
         * @param  {string}  groupId
         */
        showGroupTab: function (groupId) {
            var tab = this.getGroupTab(groupId);
            if (tab !== false) {
                jQuery(tab).show();
            }
        },

        /**
         * Convenience for custom code that needs to fire when a tab is
changed
         */
        watchTabs: function () {
            var self = this;

            jQuery(this.form).on('click',
'*[data-role=fabrik_tab]', function (event) {
                var groupId = event.target.id.match(/group(\d+)_tab/);
                if (groupId.length > 1) {
                    groupId = groupId[1];
                }
                Fabrik.fireEvent('fabrik.form.tab.click', [self,
groupId, event], 500);
            });
        },

        /**
         * If a user has previously started a multi-page form, then we will
have a .clearSession
         * button which resets the form and submits it using the
removeSession task.
         */
        watchClearSession: function () {
	        if (this.options.multipage_save === 0) {
		        return;
	        }

            var self = this,
                form = jQuery(this.form);

            form.find('.clearSession').on('click',
function (e) {
                e.preventDefault();
               
form.find('input[name=task]').val('removeSession');
                self.clearForm();
                self.form.submit();
            });
        },

        createPages: function () {
            var submit, p, firstGroup, tabDiv;
            if (this.isMultiPage()) {

                // Wrap each page in its own div
                this.options.pages.each(function (page, i) {
                    p = jQuery(document.createElement('div'));
                    p.attr({
                        'class': 'page',
                        'id'   : 'page_' + i
                    });
                    firstGroup = jQuery('#group' + page[0]);

                    // Paul - Don't use pages if this is a
bootstrap_tab form
                    tabDiv = firstGroup.closest('div');
                    if (tabDiv.hasClass('tab-pane')) {
                        return;
                    }
                    p.insertBefore(firstGroup);
                    page.each(function (group) {
                        p.append(jQuery('#group' + group));
                    });
                });
                submit = this._getButton('Submit');
                if (submit && this.options.rowid === '')
{
                    submit.disabled = 'disabled';
                    submit.setStyle('opacity', 0.5);
                }
                var self = this;
                jQuery(this.form).on('click',
'.fabrikPagePrevious', function (e) {
                    self._doPageNav(e, -1);
                });
                jQuery(this.form).on('click',
'.fabrikPageNext', function (e) {
                    self._doPageNav(e, 1);
                });
                this.setPageButtons();
                this.hideOtherPages();
            }
        },

        isMultiPage: function () {
            return this.options.pages.getKeys().length > 1;
        },

        /**
         * Move forward/backwards in multi-page form
         *
         * @param   {event}  e
         * @param   {int}    dir  1/-1
         */
        _doPageNav: function (e, dir) {
            var self = this, url, d;
            if (this.options.editable) {
                if
(typeOf(this.form.getElement('.fabrikMainError')) !==
'null') {
                   
this.form.getElement('.fabrikMainError').addClass('fabrikHide');
                }

                // If tip shown at bottom of long page and next page
shorter we need to move the tip to
                // the top of the page to avoid large space appearing at
the bottom of the page.
                jQuery('.tool-tip').css('top', 0);

                // Don't prepend with Fabrik.liveSite, as it can
create cross origin browser errors if
                // you are on www and livesite is not on www.
                url =
'index.php?option=com_fabrik&format=raw&task=form.ajax_validate&form_id='
+ this.id;
                if (this.options.lang !== false) {
                    url += '&lang=' + this.options.lang;
                }

                Fabrik.loader.start(this.getBlock(),
Joomla.JText._('COM_FABRIK_VALIDATING'));
                this.clearErrors();

                d = jQuery.extend({}, this.getFormData(), {
                    task: 'form.ajax_validate',
                    fabrik_ajax: '1',
                    format : 'raw'
                });

                d = this._prepareRepeatsForAjax(d);

                jQuery.ajax({
                    'url' : url,
                    method: this.options.ajaxmethod,
                    data  : d,

                }).done(function (r) {
                    Fabrik.loader.stop(self.getBlock());
                    r = JSON.parse(r);

                    // Don't show validation errors if we are going
back a page
                    if (dir === -1 || self._showGroupError(r, d) === false)
{
                        self.changePage(dir);
                        self.saveGroupsToDb();
                    }
                    jQuery('html, body').animate({
                        scrollTop: jQuery(self.form).offset().top
                    }, 300);
                });
            } else {
                this.changePage(dir);
            }
            e.preventDefault();
        },

        /**
         * On a multi-page form save the group data
         */
        saveGroupsToDb: function () {
            var self = this, orig, origProcess, url, data,
                format =
this.form.querySelector('input[name=format]'),
                task =
this.form.querySelector('input[name=task]'),
                block = this.getBlock();
            if (this.options.multipage_save === 0) {
                return;
            }
            Fabrik.fireEvent('fabrik.form.groups.save.start',
[this]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            orig = format.value;
            origProcess = task.value;
            this.form.querySelector('input[name=format]').value =
'raw';
            this.form.querySelector('input[name=task]').value =
'form.savepage';

            url =
'index.php?option=com_fabrik&format=raw&page=' +
this.currentPage;
            if (this.options.lang !== false) {
                url += '&lang=' + this.options.lang;
            }
            Fabrik.loader.start(block, 'saving page');
            data = this.getFormData();
            data.fabrik_ajax = 1;
            jQuery.ajax({
                url       : url,
                method    : this.options.ajaxmethod,
                data      : data,

            }).done(function (r) {
               
Fabrik.fireEvent('fabrik.form.groups.save.completed', [self]);
                if (self.result === false) {
                    self.result = true;
                    return;
                }
                format.value = orig;
                task.value = origProcess;
                if (self.options.ajax) {
                   
Fabrik.fireEvent('fabrik.form.groups.save.end', [self, r]);
                }
                Fabrik.loader.stop(block);
            });
        },

        changePage: function (dir) {
            this.changePageDir = dir;
            Fabrik.fireEvent('fabrik.form.page.change', [this,
dir]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            this.currentPage = this.currentPage.toInt();
            if (this.currentPage + dir >= 0 && this.currentPage
+ dir < this.options.pages.getKeys().length) {
                this.currentPage = this.currentPage + dir;
                if (!this.pageGroupsVisible()) {
                    this.changePage(dir);
                }
            }

            this.setPageButtons();
            jQuery('#page_' +
this.currentPage).css('display', '');
            this._setMozBoxWidths();
            this.hideOtherPages();
            Fabrik.fireEvent('fabrik.form.page.chage.end', [this,
dir]);
            Fabrik.fireEvent('fabrik.form.page.change.end',
[this, dir]);
            if (this.result === false) {
                this.result = true;
                return;
            }
        },

        pageGroupsVisible: function () {
            var visible = false;
            this.options.pages.get(this.currentPage).each(function (gid) {
                var group = jQuery('#group' + gid);
                if (group.length > 0) {
                    if (group.css('display') !==
'none') {
                        visible = true;
                    }
                }
            });
            return visible;
        },

        /**
         * Hide all groups except those in the active page
         */
        hideOtherPages: function () {
            var page, currentPage = parseInt(this.currentPage, 10);
            this.options.pages.each(function (gids, i) {
                if (parseInt(i, 10) !== currentPage) {
                    page = jQuery('#page_' + i);
                    page.hide();
                }
            });
        },

        setPageButtons: function () {
            var submit = this._getButton('Submit');
            var prevs =
this.form.getElements('.fabrikPagePrevious');
            var nexts = this.form.getElements('.fabrikPageNext');
            nexts.each(function (next) {
                if (this.currentPage ===
this.options.pages.getKeys().length - 1) {
                    if (typeOf(submit) !== 'null') {
                        submit.disabled = '';
                        submit.setStyle('opacity', 1);
                    }
                    next.disabled = 'disabled';
                    next.setStyle('opacity', 0.5);
                } else {
                    if (typeOf(submit) !== 'null' &&
(this.options.rowid === '' ||
                        this.options.rowid.toString() === '0')) {
                        submit.disabled = 'disabled';
                        submit.setStyle('opacity', 0.5);
                    }
                    next.disabled = '';
                    next.setStyle('opacity', 1);
                }
            }.bind(this));
            prevs.each(function (prev) {
                if (this.currentPage === 0) {
                    prev.disabled = 'disabled';
                    prev.setStyle('opacity', 0.5);
                } else {
                    prev.disabled = '';
                    prev.setStyle('opacity', 1);
                }
            }.bind(this));
        },

        destroyElements: function () {
            this.formElements.each(function (el) {
                el.destroy();
            });
        },

        /**
         * Add elements into the form
         *
         * @param  Hash  a  Elements to add.
         */
        addElements: function (a) {
            /*
             * Store the newly added elements so we can call attachedToForm
only on new elements.
             * Avoids issue with cdd in repeat groups resetting themselves
when you add a new group
             */
            var added = [], i = 0;
            a = $H(a);
            a.each(function (elements, gid) {
                elements.each(function (el) {
                    if (typeOf(el) === 'array') {
                        if (typeOf(document.id(el[1])) ===
'null') {
                            /* Some elements may not exist if this is a new
record, specifically the lockrow element */
                            if
(document.getElements('input[name=rowid]')[0].value !=
"" && el[0] != 'FbLockrow') {
                                fconsole('Fabrik form::addElements:
Cannot add element "' + el[1] +
                                    '" because it does not exist
in HTML.');
                            }
                            return;
                        }
                        try {
                            var oEl = new window[el[0]](el[1], el[2]);
                        }
                        catch (err) {
                            fconsole('Fabrik form::addElements: Cannot
add element "' + el[1] +
                                '" of type "' + el[0] +
'" because: ' + err.message);
                            return;
                        }
                        added.push(this.addElement(oEl, el[1], gid));
                    }
                    else if (typeOf(el) === 'object') {
                        if (typeOf(document.id(el.options.element)) ===
'null') {
                            fconsole('Fabrik form::addElements: Cannot
add element "' +
                                el.options.element + '" because
it does not exist in HTML.');
                            return;
                        }
                        added.push(this.addElement(el, el.options.element,
gid));
                    }
                    else if (typeOf(el) !== 'null') {
                        fconsole('Fabrik form::addElements: Cannot add
unknown element: ' + el);
                    }
                    else {
                        fconsole('Fabrik form::addElements: Cannot add
null element.');
                    }
                }.bind(this));
            }.bind(this));
            // $$$ hugh - moved attachedToForm calls out of addElement to
separate loop, to fix forward reference issue,
            // i.e. calc element adding events to other elements which come
after itself, which won't be in formElements
            // yet if we do it in the previous loop ('cos the previous
loop is where elements get added to formElements)
            for (i = 0; i < added.length; i++) {
                if (typeOf(added[i]) !== 'null') {
                    try {
                        added[i].attachedToForm();
                    } catch (err) {
                        fconsole(added[i].options.element + ' attach
to form:' + err);
                    }
                }
            }
            Fabrik.fireEvent('fabrik.form.elements.added',
[this]);
        },

        addElement: function (oEl, elId, gid) {
            elId = oEl.getFormElementsKey(elId);
            elId = elId.replace('[]', '');

            var ro = elId.substring(elId.length - 3, elId.length) ===
'_ro';
            oEl.form = this;
            oEl.groupid = gid;
            this.formElements.set(elId, oEl);
            Fabrik.fireEvent('fabrik.form.element.added', [this,
elId, oEl]);
            if (ro) {
                elId = elId.substr(0, elId.length - 3);
                this.formElements.set(elId, oEl);
            }
            this.submitBroker.addElement(elId, oEl);
            return oEl;
        },

        /**
         * Dispatch an event to an element
         *
         * @param   string  elementType  Deprecated
         * @param   string  elementId    Element key to look up in
this.formElements
         * @param   string  action       Event change/click etc.
         * @param   mixed   js           String or function
         */

        dispatchEvent: function (elementType, elementId, action, js) {
            if (typeOf(js) === 'string') {
                js = Encoder.htmlDecode(js);
            }
            var el = this.formElements.get(elementId);
            if (!el) {
                // E.g. db join rendered as chx
                var els = Object.each(this.formElements, function (e) {
                    if (elementId === e.baseElementId) {
                        el = e;
                    }
                });
            }
            if (!el) {
                fconsole('Fabrik form::dispatchEvent: Cannot find
element to add ' + action + ' event to: ' + elementId);
            }
            else if (js !== '') {
                el.addNewEvent(action, js);
            }
            else if (Fabrik.debug) {
                fconsole('Fabrik form::dispatchEvent: Javascript empty
for ' + action + ' event on: ' + elementId);
            }
        },

        action: function (task, el) {
            var oEl = this.formElements.get(el);
            Browser.exec('oEl.' + task + '()');
        },

        triggerEvents: function (el) {
            this.formElements.get(el).fireEvents(arguments[1]);
        },

        /**
         * If Ajax validations are turned on the watch the elements and
their sub-elements
         *
         * @param {string}  id            Element id to observe
         * @param {string}  triggerEvent  Event type to add
         */
        watchValidation: function (id, triggerEvent) {
            var self = this,
                el = jQuery('#' + id);
            if (this.options.ajaxValidation === false) {
                return;
            }
            if (el.length === 0) {
                fconsole('Fabrik form::watchValidation: Could not add
' + triggerEvent + ' event because element "' +
                    id + '" does not exist.');
                return;
            }
            el = this.formElements.get(id);
            el.addAjaxValidation();
        },

        /**
         * as well as being called from watchValidation can be called from
other
         * element js actions, e.g. date picker closing
         *
         * @param  {event}   e           the event
         * @param  {bool}    subEl       has sub elements
         * @param  {string}  replacetxt  additional text on the value
field, like _time
         */
        doElementValidation: function (e, subEl, replacetxt) {
            var id;
            if (this.options.ajaxValidation === false) {
                return;
            }
            replacetxt = typeOf(replacetxt) === 'null' ?
'_time' : replacetxt;
            if (typeOf(e) === 'event' || typeOf(e) ===
'object' || typeOf(e) === 'domevent') { // type object
in
                id = e.target.id;
                // for elements with subelements e.g. checkboxes
radiobuttons
                if (subEl === true) {
                    id =
document.id(e.target).getParent('.fabrikSubElementContainer').id;
                }
            } else {
                // hack for closing date picker where it seems the event
object isn't
                // available
                id = e;
            }

            if (typeOf(document.id(id)) === 'null') {
                return;
            }
            if (document.id(id).getProperty('readonly') === true
||
                document.id(id).getProperty('readonly') ===
'readonly') {
                // stops date element being validated
                // return;
            }
            var el = this.formElements.get(id);
            if (!el) {
                //silly catch for date elements you cant do the usual
method of setting the id in the
                //fabrikSubElementContainer as its required to be on the
date element for the calendar to work
                id = id.replace(replacetxt, '');
                el = this.formElements.get(id);
                if (!el) {
                    return;
                }
            }

            if (!el.shouldAjaxValidate())
            {
                return;
            }

           
Fabrik.fireEvent('fabrik.form.element.validation.start', [this,
el, e]);
            if (this.result === false) {
                this.result = true;
                return;
            }
           
el.setErrorMessage(Joomla.JText._('COM_FABRIK_VALIDATING'),
'fabrikValidating');

            var d = $H(this.getFormData());
            d.set('task', 'form.ajax_validate');
            d.set('fabrik_ajax', '1');
            d.set('format', 'raw');
            if (this.options.lang !== false) {
                d.set('lang', this.options.lang);
            }
            d = this._prepareRepeatsForAjax(d);

            // $$$ hugh - nasty hack, because validate() in form model will
always use _0 for
            // repeated id's
            var origid = id;
            if (el.origId) {
                origid = el.origId + '_0';
            }
            el.options.repeatCounter = el.options.repeatCounter ?
el.options.repeatCounter : 0;

            var myAjax = new Request({
                url       : '',
                method    : this.options.ajaxmethod,
                data      : d,
                onComplete: function (e) {
                    this._completeValidaton(e, id, origid);
                }.bind(this)
            }).send();
        },

        /**
         * Run once a validation is completed
         * @param {string} r
         * @param {string} id
         * @param {string} origid
         * @private
         */
        _completeValidaton: function (r, id, origid) {
            r = JSON.parse(r);
            if (typeOf(r) === 'null') {
                this._showElementError(['Oups'], id);
                this.result = true;
                return;
            }
            this.formElements.each(function (el, key) {
                el.afterAjaxValidation();
            });
           
Fabrik.fireEvent('fabrik.form.element.validation.complete',
[this, r, id, origid]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            var el = this.formElements.get(id);
            if ((typeOf(r.modified[origid]) !== 'null')) {
                if (el.options.inRepeatGroup) {
                   
el.update(r.modified[origid][el.options.repeatCounter]);
                }
                else {
                    el.update(r.modified[origid]);
                }
            }
            if (typeOf(r.errors[origid]) !== 'null') {
               
this._showElementError(r.errors[origid][el.options.repeatCounter], id);
            } else {
                this._showElementError([], id);
            }

            if (this.options.toggleSubmit) {
                if (this.options.mustValidate) {
                    if (!this.hasErrors.has(id) || !this.hasErrors.get(id))
{
                        this.mustValidateEls[id] = false;
                    }
                    if (!this.mustValidateEls.hasValue(true)) {
                        this.toggleSubmit(true);
                    }
                }
                else {
                    this.toggleSubmit(this.hasErrors.getKeys().length ===
0);
                }
            }
        },

        _prepareRepeatsForAjax: function (d) {
            this.getForm();
            //ensure we are dealing with a simple object
            if (typeOf(d) === 'hash') {
                d = d.getClean();
            }
            //data should be keyed on the data stored in the elements name
between []'s which is the group id
           
this.form.getElements('input[name^=fabrik_repeat_group]').each(
                function (e) {
                    // $$$ hugh - had a client with a table called
fabrik_repeat_group, which was hosing up here,
                    // so added a test to narrow the element name down a
bit!
                    if (e.id.test(/fabrik_repeat_group_\d+_counter/)) {
                        var c = e.name.match(/\[(.*)\]/)[1];
                        d['fabrik_repeat_group[' + c +
']'] = e.get('value');
                    }
                }
            );
            return d;
        },

        _showGroupError: function (r, d) {
            var tmperr;
            var gids =
Array.mfrom(this.options.pages.get(this.currentPage.toInt()));
            var err = false;
            $H(d).each(function (v, k) {
                k = k.replace(/\[(.*)\]/,
'').replace(/%5B(.*)%5D/, '');// for dropdown
validations
                if (this.formElements.has(k)) {
                    var el = this.formElements.get(k);
                    if (gids.contains(el.groupid.toInt())) {

                        if (r.errors[k]) {
                            if (el.options.inRepeatGroup) {
                                r.errors[k].each(function (v2, k2) {
                                    var k3 = k.replace(/_(\d+)$/,
'_' + k2);
                                    var rEl = this.formElements.get(k3);


                                    var msg = '';
                                    if (typeOf(v2) !== 'null') {
                                        msg =
v2.flatten().join('<br />');
                                    }
                                    if (msg !== '') {
                                        tmperr = this._showElementError(v2,
k3);
                                        if (err === false) {
                                            err = tmperr;
                                        }
                                    } else {
                                        rEl.setErrorMessage('',
'');
                                    }
                                }.bind(this));
                            }
                            else {
                                // prepare error so that it only triggers
for real errors and not success
                                // msgs

                                var msg = '';
                                if (typeOf(r.errors[k]) !==
'null') {
                                    msg =
r.errors[k].flatten().join('<br />');
                                }
                                if (msg !== '') {
                                    tmperr =
this._showElementError(r.errors[k], k);
                                    if (err === false) {
                                        err = tmperr;
                                    }
                                } else {
                                    el.setErrorMessage('',
'');
                                }
                            }
                        }
                        if (r.modified[k]) {
                            if (el) {
                                el.update(r.modified[k]);
                            }
                        }
                    }
                }
            }.bind(this));

            return err;
        },

        /**
         * Show element error
         * @param {array} r
         * @param {string} id
         * @returns {boolean}
         * @private
         */
        _showElementError: function (r, id) {
            // r should be the errors for the specific element, down to its
repeat group
            // id.
            var msg = '';
            if (typeOf(r) !== 'null') {
                msg = r.flatten().join('<br />');
            }
            var classname = (msg === '') ?
'fabrikSuccess' : 'fabrikError';
            if (msg === '') {
                delete this.hasErrors[id];
                msg = Joomla.JText._('COM_FABRIK_SUCCESS');
            }
            else {
                this.hasErrors.set(id, true);
            }
            msg = '<span> ' + msg +
'</span>';
            this.formElements.get(id).setErrorMessage(msg, classname);
            return (classname === 'fabrikSuccess') ? false :
true;
        },

        updateMainError: function () {
            var myfx, activeValidations;
            if (typeOf(this.form.getElement('.fabrikMainError'))
!== 'null') {
               
this.form.getElement('.fabrikMainError').set('html',
this.options.error);
            }
            activeValidations =
this.form.getElements('.fabrikError').filter(
                function (e, index) {
                    return !e.hasClass('fabrikMainError');
                });
            if (activeValidations.length > 0 &&
this.form.getElement('.fabrikMainError').hasClass('fabrikHide'))
{
                this.showMainError(this.options.error);
            }
            if (activeValidations.length === 0) {
                this.hideMainError();
            }
        },

        hideMainError: function () {
            if (typeOf(this.form.getElement('.fabrikMainError'))
!== 'null') {
                var mainEr =
this.form.getElement('.fabrikMainError');
                myfx = new Fx.Tween(mainEr, {
                    property  : 'opacity',
                    duration  : 500,
                    onComplete: function () {
                        mainEr.addClass('fabrikHide');
                    }
                }).start(1, 0);
            }
        },

        showMainError: function (msg) {
            // If we are in j3 and ajax validations are on - don't
show main error as it makes the form 'jumpy'
            if (Fabrik.bootstrapped && this.options.ajaxValidation)
{
                return;
            }
            if (typeOf(this.form.getElement('.fabrikMainError'))
!== 'null') {
                var mainEr =
this.form.getElement('.fabrikMainError');
                mainEr.set('html', msg);
                mainEr.removeClass('fabrikHide');
                myfx = new Fx.Tween(mainEr, {
                    property: 'opacity',
                    duration: 500
                }).start(0, 1);
            }
        },

        /** @since 3.0 get a form button name */
        _getButton: function (name) {
            if (!this.getForm()) {
                return;
            }
            var b =
this.form.getElement('input[type=button][name=' + name +
']');
            if (!b) {
                b =
this.form.getElement('input[type=submit][name=' + name +
']');
            }
            if (!b) {
                b =
this.form.getElement('button[type=button][name=' + name +
']');
            }
            if (!b) {
                b =
this.form.getElement('button[type=submit][name=' + name +
']');
            }
            return b;
        },

        watchSubmit: function () {
            var submit = this._getButton('Submit');
            var apply = this._getButton('apply');

            if (!submit && !apply) {
                // look for a button element set to submit
                if (this.form &&
this.form.getElement('button[type=submit]'))
                {
                    submit =
this.form.getElement('button[type=submit]');
                }
                else {
                    return;
                }
            }
            var del = this._getButton('delete'),
                copy = this._getButton('Copy');
            if (del) {
                del.addEvent('click', function (e) {
                    if
(window.confirm(Joomla.JText._('COM_FABRIK_CONFIRM_DELETE_1'))) {
                        var res =
Fabrik.fireEvent('fabrik.form.delete', [this,
this.options.rowid]).eventResults;
                        if (typeOf(res) === 'null' || res.length
=== 0 || !res.contains(false)) {
                            // Task value is the same for front and admin
                           
this.form.getElement('input[name=task]').value =
'form.delete';
                            this.doSubmit(e, del);
                        } else {
                            e.stop();
                            return false;
                        }

                    } else {
                        return false;
                    }
                }.bind(this));
            }
            var submits =
this.form.getElements('button[type=submit]').combine([apply,
submit, copy]);
            submits.each(function (btn) {
                if (typeOf(btn) !== 'null') {
                    btn.addEvent('click', function (e) {
                        this.doSubmit(e, btn);
                    }.bind(this));
                }
            }.bind(this));

            this.form.addEvent('submit', function (e) {
                this.doSubmit(e);
            }.bind(this));
        },

        mockSubmit: function (btnName) {
            btnName = typeof btnName !== 'undefined' ? btnName :
'Submit';
            var btn = this._getButton(btnName);
            if (!btn) {
                btn = new Element('button', {'name':
btnName, 'type': 'submit'});
            }
            this.doSubmit(new Event.Mock(btn, 'click'), btn);
        },

        doSubmit: function (e, btn) {
            if (this.submitBroker.enabled()) {
                e.stop();
                return false;
            }
            this.toggleSubmit(false);
            this.submitBroker.submit(function () {
                if (this.options.showLoader) {
                    Fabrik.loader.start(this.getBlock(),
Joomla.JText._('COM_FABRIK_LOADING'));
                }
                Fabrik.fireEvent('fabrik.form.submit.start',
[this, e, btn]);
                if (this.result === false) {
                    this.result = true;
                    e.stop();
                    Fabrik.loader.stop(this.getBlock());
                    // Update global status error
                    this.updateMainError();
                    this.toggleSubmit(true);

                    // Return otherwise ajax upload may still occur.
                    return;
                }
                // Insert a hidden element so we can reload the last page
if validation fails
                if (this.options.pages.getKeys().length > 1) {
                    this.form.adopt(new Element('input', {
                        'name' : 'currentPage',
                        'value': this.currentPage.toInt(),
                        'type' : 'hidden'
                    }));
                }
                hiddenElements = [];
                // insert hidden element of hidden elements (!) used by
validation code for "skip if hidden" option
                jQuery.each(this.formElements, function (id, el) {
                   if (el.element &&
jQuery(el.element).closest('.fabrikHide').length !== 0) {
                       hiddenElements.push(id);
                   }
                });
                this.form.adopt(new Element('input', {
                    'name' : 'hiddenElements',
                    'value': JSON.stringify(hiddenElements),
                    'type' : 'hidden'
                }));
                if (this.options.ajax) {
                    // Do ajax val only if onSubmit val ok
                    if (this.form) {
                        // if showLoader is enabled (for non AJAX submits)
the loader will already have been shown up there ^^
                        if (!this.options.showLoader) {
                            Fabrik.loader.start(this.getBlock(),
Joomla.JText._('COM_FABRIK_LOADING'));
                        }

                        // Get all values from the form
                        var data = $H(this.getFormData());
                        data = this._prepareRepeatsForAjax(data);
                        data[btn.name] = btn.value;
                        if (btn.name === 'Copy') {
                            data.Copy = 1;
                            e.stop();
                        }
                        data.fabrik_ajax = '1';
                        data.format = 'raw';

                        // if HTML 5, use FormData so we can do uploads
from popups via AJAX
                        // poop, doesn't work in Edge or Safari, punt
till they implement FormData correctly
                        if (false && window.FormData) {
                            fd = new FormData(this.form);

                            jQuery.each(data, function(k, v) {
                                if (fd.has(k)) {
                                    if (typeOf(fd.get(k)) !==
'object') {
                                        fd.set(k, v);
                                    }
                                }
                                else {
                                    fd.set(k, v);
                                }
                            });

                            data = fd;
                            var self = this;

                            jQuery.ajax({
                                'url': this.form.action,
                                'data': data,
                                'method':
this.options.ajaxmethod,
                                'processData': false,
                                'contentType': false
                            })
                                .fail(function (text, error) {
                                    fconsole(text + ': ' +
error);
                                    self.showMainError(error);
                                    Fabrik.loader.stop(self.getBlock(),
'Error in returned JSON');
                                    self.toggleSubmit(true);
                                })
                                .done(function (json, txt) {
                                    json = JSON.parse(json);
                                    self.toggleSubmit(true);
                                    if (typeOf(json) === 'null')
{
                                        // Stop spinner
                                        Fabrik.loader.stop(self.getBlock(),
'Error in returned JSON');
                                        fconsole('error in returned
json', json, txt);
                                        return;
                                    }
                                    // Process errors if there are some
                                   
jQuery(self.form.getElements('[data-role=fabrik_tab]')).removeClass('fabrikErrorGroup')
                                    var errfound = false;
                                    if (json.errors !== undefined) {

                                        // For every element of the form
update error message
                                        $H(json.errors).each(function
(errors, key) {
                                            if (self.formElements.has(key)
&& errors.flatten().length > 0) {
                                                errfound = true;
                                                if
(self.formElements[key].options.inRepeatGroup) {
                                                    for (e = 0; e <
errors.length; e++) {
                                                        if
(errors[e].flatten().length > 0) {
                                                            var this_key =
key.replace(/(_\d+)$/, '_' + e);
                                                           
self._showElementError(errors[e], this_key);
                                                        }
                                                    }
                                                }
                                                else {
                                                   
self._showElementError(errors, key);
                                                }
                                            }
                                        });
                                    }
                                    // Update global status error
                                    self.updateMainError();

                                    if (errfound === false) {
                                        var clear_form = false;
                                        if (self.options.rowid ===
'' && btn.name !== 'apply') {
                                            // We're submitting a new
form - so always clear
                                            clear_form = true;
                                        }
                                       
Fabrik.loader.stop(self.getBlock());
                                        var savedMsg = (typeOf(json.msg)
!== 'null' && json.msg !== undefined && json.msg
!== '') ? json.msg :
Joomla.JText._('COM_FABRIK_FORM_SAVED');
                                        if (json.baseRedirect !== true) {
                                            clear_form = json.reset_form;
                                            if (json.url !== undefined) {
                                                if (json.redirect_how ===
'popup') {
                                                    var width = json.width
? json.width : 400;
                                                    var height =
json.height ? json.height : 400;
                                                    var x_offset =
json.x_offset ? json.x_offset : 0;
                                                    var y_offset =
json.y_offset ? json.y_offset : 0;
                                                    var title = json.title
? json.title : '';
                                                    Fabrik.getWindow({
                                                        'id'     
: 'redirect',
                                                        'type'   
: 'redirect',
                                                        contentURL:
json.url,
                                                        caller    :
self.getBlock(),
                                                        'height' 
: height,
                                                        'width'  
: width,
                                                       
'offset_x': x_offset,
                                                       
'offset_y': y_offset,
                                                        'title'  
: title
                                                    });
                                                }
                                                else {
                                                    if (json.redirect_how
=== 'samepage') {
                                                       
window.open(json.url, '_self');
                                                    }
                                                    else if
(json.redirect_how === 'newpage') {
                                                       
window.open(json.url, '_blank');
                                                    }
                                                }
                                            } else {
                                                if (!json.suppressMsg) {
                                                    alert(savedMsg);
                                                }
                                            }
                                        } else {
                                            clear_form = json.reset_form
!== undefined ? json.reset_form : clear_form;
                                            if (!json.suppressMsg) {
                                                alert(savedMsg);
                                            }
                                        }
                                        // Query the list to get the
updated data
                                       
Fabrik.fireEvent('fabrik.form.submitted', [self, json]);
                                        if (btn.name !== 'apply')
{
                                            if (clear_form) {
                                                self.clearForm();
                                            }
                                            // If the form was loaded in a
Fabrik.Window close the window.
                                            if
(Fabrik.Windows[self.options.fabrik_window_id]) {
                                               
Fabrik.Windows[self.options.fabrik_window_id].close();
                                            }
                                        }
                                    } else {
                                       
Fabrik.fireEvent('fabrik.form.submit.failed', [self, json]);
                                        // Stop spinner
                                        Fabrik.loader.stop(self.getBlock(),
Joomla.JText._('COM_FABRIK_VALIDATION_ERROR'));
                                    }
                                });
                        }
                        else {
                            var myajax = new Request.JSON({
                                'url': this.form.action,
                                'data': data,
                                'method':
this.options.ajaxmethod,
                                onError: function (text, error) {
                                    fconsole(text + ': ' +
error);
                                    this.showMainError(error);
                                    Fabrik.loader.stop(this.getBlock(),
'Error in returned JSON');
                                    this.toggleSubmit(true);
                                }.bind(this),

                                onFailure: function (xhr) {
                                    fconsole(xhr);
                                    Fabrik.loader.stop(this.getBlock(),
'Ajax failure');
                                    this.toggleSubmit(true);
                                }.bind(this),
                                onComplete: function (json, txt) {
                                    this.toggleSubmit(true);
                                    if (typeOf(json) === 'null')
{
                                        // Stop spinner
                                        Fabrik.loader.stop(this.getBlock(),
'Error in returned JSON');
                                        fconsole('error in returned
json', json, txt);
                                        return;
                                    }
                                    // Process errors if there are some
                                   
jQuery(this.form.getElements('[data-role=fabrik_tab]')).removeClass('fabrikErrorGroup')
                                    var errfound = false;

                                    if (json.errors !== undefined) {
                                        // For every element of the form
update error message
                                        $H(json.errors).each(function
(errors, key) {
                                            if (errors.flatten().length
> 0) {
                                                /*
                                                 * might not be an element
error - could be a custom plugin error - so
                                                 * flag an error found,
even if we don't match it to an element.
                                                 */
                                                errfound = true;
                                                if
(this.formElements.has(key)) {
                                                    if
(this.formElements[key].options.inRepeatGroup) {
                                                        for (e = 0; e <
errors.length; e++) {
                                                            if
(errors[e].flatten().length > 0) {
                                                                var
this_key = key.replace(/(_\d+)$/, '_' + e);
                                                               
this._showElementError(errors[e], this_key);
                                                            }
                                                        }
                                                    }
                                                    else {
                                                       
this._showElementError(errors, key);
                                                    }
                                                }
                                            }
                                        }.bind(this));
                                    }
                                    // Update global status error
                                    this.updateMainError();

                                    if (errfound === false) {
                                        var clear_form = false;
                                        if (this.options.rowid ===
'' && btn.name !== 'apply') {
                                            // We're submitting a new
form - so always clear
                                            clear_form = true;
                                        }
                                       
Fabrik.loader.stop(this.getBlock());
                                        var savedMsg = (typeOf(json.msg)
!== 'null' && json.msg !== undefined && json.msg
!== '') ? json.msg :
Joomla.JText._('COM_FABRIK_FORM_SAVED');
                                        if (json.baseRedirect !== true) {
                                            clear_form = json.reset_form;
                                            if (json.url !== undefined) {
                                                if (json.redirect_how ===
'popup') {
                                                    var width = json.width
? json.width : 400;
                                                    var height =
json.height ? json.height : 400;
                                                    var x_offset =
json.x_offset ? json.x_offset : 0;
                                                    var y_offset =
json.y_offset ? json.y_offset : 0;
                                                    var title = json.title
? json.title : '';
                                                    Fabrik.getWindow({
                                                        'id':
'redirect',
                                                        'type':
'redirect',
                                                        contentURL:
json.url,
                                                        caller:
this.getBlock(),
                                                        'height':
height,
                                                        'width':
width,
                                                       
'offset_x': x_offset,
                                                       
'offset_y': y_offset,
                                                        'title':
title
                                                    });
                                                }
                                                else {
                                                    if (json.redirect_how
=== 'samepage') {
                                                       
window.open(json.url, '_self');
                                                    }
                                                    else if
(json.redirect_how === 'newpage') {
                                                       
window.open(json.url, '_blank');
                                                    }
                                                }
                                            } else {
                                                if (!json.suppressMsg) {
                                                    alert(savedMsg);
                                                }
                                            }
                                        } else {
                                            clear_form = json.reset_form
!== undefined ? json.reset_form : clear_form;
                                            if (!json.suppressMsg) {
                                                alert(savedMsg);
                                            }
                                        }
                                        // Query the list to get the
updated data
                                       
Fabrik.fireEvent('fabrik.form.submitted', [this, json]);

                                        if (btn.name !== 'apply')
{
                                            if (clear_form) {
                                                this.clearForm();
                                            }
                                            // If the form was loaded in a
Fabrik.Window close the window.
                                            if
(Fabrik.Windows[this.options.fabrik_window_id]) {
                                               
Fabrik.Windows[this.options.fabrik_window_id].close();
                                            }
                                        }

                                       
Fabrik.fireEvent('fabrik.form.submitted.end', [this, json]);
                                    } else {
                                       
Fabrik.fireEvent('fabrik.form.submit.failed', [this, json]);
                                        // Stop spinner
                                        Fabrik.loader.stop(this.getBlock(),
Joomla.JText._('COM_FABRIK_VALIDATION_ERROR'));
                                    }
                                }.bind(this)
                            }).send();
                        }
                    }
                }
                Fabrik.fireEvent('fabrik.form.submit.end',
[this]);
                if (this.result === false) {
                    this.result = true;
                    e.stop();
                    // Update global status error
                    this.updateMainError();
                } else {
                    // Enables the list to clean up the form and custom
events
                    if (this.options.ajax) {
                        e.stop();
                       
Fabrik.fireEvent('fabrik.form.ajax.submit.end', [this]);
                    } else {
                        // Inject submit button name/value.
                        if (typeOf(btn) !== 'null') {
                            new Element('input', {type:
'hidden', name: btn.name, value: btn.value}).inject(this.form);
                            this.form.submit();
                        } else {
                            // Regular button pressed which seems to be
triggering form.submit() method.
                            e.stop();
                        }
                    }
                }
            }.bind(this));
            e.stop();
        },

        /**
         * Used to get the querystring data and
         * for any element overwrite with its own data definition
         * required for empty select lists which return undefined as their
value if no
         * items available
         *
         * @param  {bool}  submit  Should we run the element onsubmit()
methods - set to false in calc element
         */
        getFormData: function (submit) {
            submit = typeOf(submit) !== 'null' ? submit : true;
            if (submit) {
                this.formElements.each(function (el, key) {
                    el.onsubmit();
                });
            }
            this.getForm();
            var s = this.form.toQueryString();
            var h = {};
            s = s.split('&');
            var arrayCounters = $H({});
            s.each(function (p) {
                p = p.split('=');
                var k = p[0];
                // $$$ rob deal with checkboxes
                // Ensure [] is not encoded
                k = decodeURI(k);
                if (k.substring(k.length - 2) === '[]') {
                    k = k.substring(0, k.length - 2);
                    if (!arrayCounters.has(k)) {
                        // rob for ajax validation on repeat element this
is required to be set to 0
                        arrayCounters.set(k, 0);
                    } else {
                        arrayCounters.set(k, arrayCounters.get(k) + 1);
                    }
                    k = k + '[' + arrayCounters.get(k) +
']';
                }
                h[k] = p[1];
            });

            // toQueryString() doesn't add in empty data - we need to
know that for the
            // validation on multipages
            var elKeys = this.formElements.getKeys();
            this.formElements.each(function (el, key) {
                //fileupload data not included in querystring
                if (el.plugin === 'fabrikfileupload') {
                    h[key] = el.get('value');
                }
                if (typeOf(h[key]) === 'null') {
                    // search for elementname[*] in existing data (search
for * as datetime
                    // elements aren't keyed numerically)
                    var found = false;
                    $H(h).each(function (val, dataKey) {
                        dataKey = unescape(dataKey); // 3.0 ajax submission
[] are escaped
                        dataKey = dataKey.replace(/\[(.*)\]/,
'');
                        if (dataKey === key) {
                            found = true;
                        }
                    }.bind(this));
                    if (!found) {
                        h[key] = '';
                    }
                }
            }.bind(this));
            return h;
        },

        /*
         * Used by things like CDD to populate 'data' for the
AJAX update, so custom 'where' clauses
         * can use {placeholders}. Initially tried to use getFormData for
this, but because
         * it adds ALL the query string args from the page, the AJAX call
from cascade ended
         * up trying to submit the form. So, this func only fetches actual
form element data.
         */
        getFormElementData: function () {
            var h = {};
            this.formElements.each(function (el, key) {
                if (el.element) {
                    h[key] = el.getValue();
                    h[key + '_raw'] = h[key];
                }
            }.bind(this));
            return h;
        },

        watchGroupButtons: function () {

            var self = this;

            jQuery(this.form).on('click',
'.deleteGroup', Debounce(this.options.debounceDelay, true,
function (e, target) {
                e.preventDefault();
                if (!self.addingOrDeletingGroup) {
                    self.addingOrDeletingGroup = true;
                    var group =
e.target.getParent('.fabrikGroup'),
                        subGroup =
e.target.getParent('.fabrikSubGroup');
                    self.deleteGroup(e, group, subGroup);
                    self.addingOrDeletingGroup = false;
                }
            }));

            jQuery(this.form).on('click', '.addGroup',
Debounce(this.options.debounceDelay, true, function (e, target) {
                e.preventDefault();
                if (!self.addingOrDeletingGroup) {
                    self.addingOrDeletingGroup = true;
                    self.duplicateGroup(e, true);
                    self.addingOrDeletingGroup = false;
                }
            }));

            if (this.form) {
				this.form.addEvent('click:relay(.fabrikSubGroup)', function
(e, subGroup) {
                var r =
subGroup.getElement('.fabrikGroupRepeater');
                if (r) {
                    subGroup.addEvent('mouseenter', function (e)
{
                        r.fade(1);
                    });
                    subGroup.addEvent('mouseleave', function (e)
{
                        r.fade(0.2);
                    });
                }
            }.bind(this));
			}
        },

        /**
         * not currently used in our code, provided as a helper function
for custom JS
         *
         * @param groupId
         * @returns {boolean}
         */
        mockDuplicateGroup: function(groupId) {
            var add_btn = this.form.getElement('#group' + groupId
+ ' .addGroup');

            if (typeOf(add_btn) !== 'null') {
                var add_e = new Event.Mock(add_btn, 'click');
                this.duplicateGroup(add_e, false);
                return true;
            }

            return false;
        },

        renumberRepeatGroup: function(el, groupId, newRepeatNum, doDelete)
{
            var input = jQuery(el).find('.fabrikinput');
            if (input) {
                var nameMap = {};
                var newMap = {};
                var elId = input[0].id;
                var element = this.formElements.get(elId);
                if (element) {
                    var repeatNum = elId.split('_').getLast();
                    console.log('renumbering: ' + repeatNum +
' => ' + newRepeatNum);
                    element.update(newRepeatNum + 1);
                    this.formElements.each(function (e, k) {
                        if (e.groupid === groupId &&
k.split('_').getLast() === repeatNum) {
                            nameMap[k] = e.setName(newRepeatNum);
                        }
                    }.bind(this));

                    $H(nameMap).each(function (newKey, oldKey) {
                        if (oldKey !== newKey) {
                            newMap[newKey] = this.formElements[oldKey];

                            if (doDelete === true) {
                                delete this.formElements[oldKey];
                            }
                        }
                    }.bind(this));

                    $H(newMap).each(function (data, newKey) {
                        this.formElements[newKey] = data;
                    }.bind(this));
                }
            }
        },

        renumberSortable: function (groupId) {
            if (typeOf(this.options.group_repeat_sortable[groupId]) ===
'null' || !this.options.group_repeat_sortable[groupId]) {
                return;
            }

            var orderElName =
this.options.group_repeat_order_element[groupId];
            var group = this.form.getElement('#group' + groupId);
            var tbody = jQuery(group).find('tbody');
            var tdEls =
jQuery(tbody).find('.fabrikRepeatGroup___' + orderElName);
            var i = 1;

            tdEls.each(function (k, el) {
                var input = jQuery(el).find('.fabrikinput');

                if (input) {
                    var elId = input[0].id;
                    var element = this.formElements.get(elId);

                    if (element) {
                        element.update(i);
                    }
                }

                i++;
            }.bind(this));
        },

        reorderSortable: function (groupId) {
            if (typeOf(this.options.group_repeat_sortable[groupId]) ===
'null' || !this.options.group_repeat_sortable[groupId]) {
                return;
            }

            var orderElName =
this.options.group_repeat_order_element[groupId];

            var nameMap = {};
            var newMap = {};
            var group = this.form.getElement('#group' + groupId);
            var tbody = jQuery(group).find('tbody');
            var tdEls =
jQuery(tbody).find('.fabrikRepeatGroup___' + orderElName);

            var to = 0;
            var save = false;
            var dir = false;
            var started = false;
            var ended = false;
            var start = false;
            var end = false;
            var lastFrom = -1;

            tdEls.each(function (k, el) {
                if (!ended) {
                    var input = jQuery(el).find('.fabrikinput');
                    if (input) {
                        var elId = input[0].id;
                        var element = this.formElements.get(elId);
                        if (element) {
                            var from =
elId.split('_').getLast().toInt();

                            if (!started) {
                                var gap = (from - lastFrom);

                                if (gap === 2) {
                                    started = true;
                                    dir = 'down';
                                    start = to;
                                } else if (gap > 2) {
                                    started = true;
                                    dir = 'up';
                                    start = to;
                                    end = from;
                                    save = from;
                                    ended = true;
                                }
                            } else {
                                if (dir === 'down') {
                                    if (from === start) {
                                        end = to;
                                        save = to;
                                        ended = true;
                                    }
                                }
                            }

                            lastFrom = from;
                            to++;
                        }
                    }
                }
            }.bind(this))

            if (dir === 'up') {
                var el;

                el = tdEls[end];
                this.renumberRepeatGroup(el, groupId, 9999, false);

                for (var i = end - 1; i >= start; i--) {
                    el = tdEls[i];
                    this.renumberRepeatGroup(el, groupId, i, false);
                }

                el = tdEls[end];
                this.renumberRepeatGroup(el, groupId, end, true);
            }
            else {
                var el;

                el = tdEls[end];
                this.renumberRepeatGroup(el, groupId, 9999, false);

                for (var i = start; i < end; i++) {
                    el = tdEls[i];
                    this.renumberRepeatGroup(el, groupId, i, false);
                }

                el = tdEls[end];
                this.renumberRepeatGroup(el, groupId, end, true);
            }

            $H(nameMap).each(function (newKey, oldKey) {
                if (oldKey !== newKey) {
                    newMap[newKey] = this.formElements[oldKey];
                }
            }.bind(this));

            $H(newMap).each(function (newKey, data) {
                this.formElements[newKey] = data;
            }.bind(this));
        },

        setupSortable: function () {
            if (!this.form) {
                return;
            }

            Object.each(this.options.group_repeats, function (canRepeat,
groupId) {
                if (canRepeat.toInt() !== 1) {
                    return;
                }

                if (typeOf(this.options.group_repeat_sortable[groupId]) !==
'null' && this.options.group_repeat_tablesort[groupId]) {
                    var group = this.form.getElement('#group' +
groupId);

                    if (group) {
                        var cellFilters = [];
                       
group.getElements('th.fabrikElementContainer').each(function (e,
x) {
                            if (e.hasClass('fabrikHide')) {
                                cellFilters.push('fabrikHide');
                            } else {
                                cellFilters.push('');
                            }
                        });
                        jQuery('#group' + groupId + '
table').tablesorter({
                            theme: 'blue',
                            widthFixed: true,
                            widgets: ["filter"],
                            cssInfoBlock: "tablesorter-no-sort",
                            ignoreCase: true,
                            widgetOptions: {
                                filter_ignoreCase: true,
                                filter_matchType: {'input':
'match', 'select': 'match'},
                                filter_saveFilters: true,
                                filter_liveSearch: true,
                                filter_cellFilter: cellFilters
                            }
                        });
                    }
                }

                if (typeOf(this.options.group_repeat_sortable[groupId]) !==
'null' && this.options.group_repeat_sortable[groupId]) {
                    Fabrik.addEvent('fabrik.form.elements.added',
function (form) {
                        this.renumberSortable(groupId);
                    }.bind(this));

                    jQuery('#group' + groupId + ' table
tbody').sortable({
                        scroll: true,
                        scrollSensitivity: 100,
                        stop: function (event, ui) {
                            var group =
ui.item[0].closest('.fabrikGroup');
                            var groupId =
group.id.replace('group', '');
                            this.reorderSortable(groupId);
                        }.bind(this)
                    });
                }
            }.bind(this));
        },

        /**
         * When editing a new form and when min groups set we need to
duplicate each group
         * by the min repeat value.
         */
        duplicateGroupsToMin: function () {
            if (!this.form) {
                return;
            }

            Fabrik.fireEvent('fabrik.form.group.duplicate.min',
[this]);

            Object.each(this.options.group_repeats, function (canRepeat,
groupId) {

                if (typeOf(this.options.minRepeat[groupId]) ===
'null') {
                    return;
                }

                if (canRepeat.toInt() !== 1) {
                    return;
                }

                var repeat_counter =
this.form.getElement('#fabrik_repeat_group_' + groupId +
'_counter'),
                    repeat_added =
this.form.getElement('#fabrik_repeat_group_' + groupId +
'_added').value,
                    repeat_rows, repeat_real, add_btn, deleteButton, i,
repeat_id_0, deleteEvent;

                if (typeOf(repeat_counter) === 'null') {
                    return;
                }

                repeat_rows = repeat_real = repeat_counter.value.toInt();

                // figure out if the first group should be hidden (min
repeat is 0)
                if (repeat_rows === 1) {
                    repeat_id_0 = this.form.getElement('#' +
this.options.group_pk_ids[groupId] + '_0');

                    // repeat_added means they added a first group, and
we've failed validation, so show it
                    if (repeat_added !== '1' &&
typeOf(repeat_id_0) !== 'null' && repeat_id_0.value ===
'') {
                        repeat_real = 0;
                    }
                }

                var min = this.options.minRepeat[groupId].toInt();
                var max = this.options.maxRepeat[groupId].toInt();
                var group = this.form.getElement('#group' +
groupId);
                var subGroup;

                /**
                 * $$$ hugh - added ability to override min count
                 *
http://fabrikar.com/forums/index.php?threads/how-to-initially-show-repeat-group.32911/#post-170147
                 * $$$ hugh - trying out min of 0 for Troester
                 *
http://fabrikar.com/forums/index.php?threads/how-to-start-a-new-record-with-empty-repeat-group.34666/#post-175408
                 * $$$ paul - fixing min of 0 for Jaanus
                 *
http://fabrikar.com/forums/index.php?threads/couple-issues-with-protostar-template.35917/
                 **/
                if (min === 0 && repeat_real === 0) {

                    // Create mock event
                    deleteButton = this.form.getElement('#group'
+ groupId + ' .deleteGroup');
                    deleteEvent = typeOf(deleteButton) !== 'null'
? new Event.Mock(deleteButton, 'click') : false;
                    subGroup =
group.getElement('.fabrikSubGroup');
                    // Remove only group
                    this.deleteGroup(deleteEvent, group, subGroup);

                }
                else if (repeat_rows < min) {
                    // Create mock event
                    add_btn = this.form.getElement('#group' +
groupId + ' .addGroup');
                    if (typeOf(add_btn) !== 'null') {
                        var add_e = new Event.Mock(add_btn,
'click');

                        // Duplicate group
                        for (i = repeat_rows; i < min; i++) {
                            this.duplicateGroup(add_e, false);
                        }
                    }
                }
                else if (max > 0 && repeat_rows > max) {
	                // Delete groups
	                for (i = repeat_rows; i > max; i--) {
		                var b = jQuery(this.form.getElements('#group' +
groupId + ' .deleteGroup')).last()[0];
		                var del_btn =
jQuery(b).find('[data-role=fabrik_delete_group]')[0];
		                subGroup =
jQuery(group.getElements('.fabrikSubGroup')).last()[0];
		                if (typeOf(del_btn) !== 'null') {
		                    var del_e = new Event.Mock(del_btn,
'click');
			                this.deleteGroup(del_e, group, subGroup);
		                }
	                }
                }

                this.setRepeatGroupIntro(group, groupId);
            }.bind(this));
        },

        /**
         * Delete an repeating group
         *
         * @param e
         * @param group
         */
        deleteGroup: function (e, group, subGroup) {
            Fabrik.fireEvent('fabrik.form.group.delete', [this,
e, group]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            if (e) {
                e.preventDefault();
            }

            // Find which repeat group was deleted
            var delIndex = 0;

            // if clicked exactly on icon, e.target will be icon, not
surrounding link, so need find with addBack
            var target =
jQuery(e.target).find('[data-role=fabrik_delete_group]').addBack('[data-role=fabrik_delete_group]')[0];

            group.getElements('.deleteGroup').each(function (b,
x) {
                if
(jQuery(b).find('[data-role=fabrik_delete_group]')[0] === target)
{
                    delIndex = x;
                }
            }.bind(this));
            var i = group.id.replace('group', '');

            var repeats = document.id('fabrik_repeat_group_' + i
+ '_counter').get('value').toInt();
            if (repeats <= this.options.minRepeat[i] &&
this.options.minRepeat[i] !== 0) {
                if (this.options.minMaxErrMsg[i] !== '') {
                    var errorMessage = this.options.minMaxErrMsg[i];
                    errorMessage = errorMessage.replace(/\{min\}/,
this.options.minRepeat[i]);
                    errorMessage = errorMessage.replace(/\{max\}/,
this.options.maxRepeat[i]);
                    alert(errorMessage);
                }
                return;
            }

            delete this.duplicatedGroups.i;
            if (document.id('fabrik_repeat_group_' + i +
'_counter').value === '0') {
                return;
            }
            var subgroups = group.getElements('.fabrikSubGroup');

            this.subGroups.set(i, subGroup.clone());
            if (subgroups.length <= 1) {
                this.hideLastGroup(i, subGroup);
                this.formElements.each(function (e, k) {
                    if (e.groupid === i && typeOf(e.element) !==
'null') {
                        this.removeMustValidate(e);
                    }
                }.bind(this));
                document.id('fabrik_repeat_group_' + i +
'_added').value = '0';
                Fabrik.fireEvent('fabrik.form.group.delete.end',
[this, e, i, delIndex]);
            } else {
                var toel = subGroup.getPrevious();
                /*
                var myFx = new Fx.Tween(subGroup, {
                    'property': 'opacity',
                    duration  : 300,
                    onComplete: function () {
                    */
                        if (subgroups.length > 1) {
                            subGroup.dispose();
                        }

                        this.formElements.each(function (e, k) {
                            if (typeOf(e.element) !== 'null') {
                                if (typeOf(document.id(e.element.id)) ===
'null') {
                                    e.decloned(i);
                                    delete this.formElements[k];
                                }
                            }
                        }.bind(this));

                        // Minus the removed group
                        subgroups =
group.getElements('.fabrikSubGroup');
                        var nameMap = {};
                        this.formElements.each(function (e, k) {
                            if (e.groupid === i) {
                                nameMap[k] = e.decreaseName(delIndex);
                            }
                        }.bind(this));
                        // ensure that formElements' keys are the same
as their object's ids
                        // otherwise delete first group, add 2 groups -
ids/names in last
                        // added group are not updated
                        $H(nameMap).each(function (newKey, oldKey) {
                            if (oldKey !== newKey) {
                                this.formElements[newKey] =
this.formElements[oldKey];
                                delete this.formElements[oldKey];
                            }
                        }.bind(this));
                       
Fabrik.fireEvent('fabrik.form.group.delete.end', [this, e, i,
delIndex]);
                        /*
                    }.bind(this)
                }).start(1, 0);
                */
                if (toel) {
                    // Only scroll the window if the previous element is
not visible
                    var win_scroll = document.id(window).getScroll().y;
                    var obj = toel.getCoordinates();
                    // If the top of the previous repeat goes above the top
of the visible
                    // window,
                    // scroll down just enough to show it.
                    if (obj.top < win_scroll) {
                        var new_win_scroll = obj.top;
                        this.winScroller.start(0, new_win_scroll);
                    }
                }
            }
            // Update the hidden field containing number of repeat groups
            document.id('fabrik_repeat_group_' + i +
'_counter').value =
                document.id('fabrik_repeat_group_' + i +
'_counter').get('value').toInt() - 1;
            // $$$ hugh - no, mustn't decrement this!  See comment in
setupAll
            this.repeatGroupMarkers.set(i, this.repeatGroupMarkers.get(i) -
1);
            this.renumberSortable(i);
            this.setRepeatGroupIntro(group, i);
        },

        hideLastGroup: function (groupId, subGroup) {
            var msg = this.options.noDataMsg[groupId];

            if (msg === '') {
                msg =
Joomla.JText._('COM_FABRIK_NO_REPEAT_GROUP_DATA');
            }

            var sge =
subGroup.getElement('.fabrikSubGroupElements');
            var notice = new Element(
                'div', {'class': 'fabrikNotice
alert'}
            ).appendText(msg);
            if (typeOf(sge) === 'null') {
                sge = subGroup;
                var add = sge.getElement('.addGroup');
                if (typeOf(add) !== 'null') {
                    var lastth =
sge.getParent('table').getElements('*[data-role="fabrik-group-repeaters"]').getLast();
                    if (!lastth) {
                        // for old custom templates that don't have
the data-role, fall back to just grabbing last th
                        lastth =
sge.getParent('table').getElements('thead
th').getLast();
                    }
                    add.inject(lastth);
                }
            }
            sge.setStyle('display', 'none');
            notice.inject(sge, 'after');
        },

        isFirstRepeatSubGroup: function (group) {
            var subgroups = group.getElements('.fabrikSubGroup');
            return subgroups.length === 1 &&
group.getElement('.fabrikNotice');
        },

        getSubGroupToClone: function (groupId) {
            var group = document.id('group' + groupId);
            var subgroup = group.getElement('.fabrikSubGroup');
            if (!subgroup) {
                subgroup = this.subGroups.get(groupId);
            }

            var clone = null;
            var found = false;
            if (this.duplicatedGroups.has(groupId)) {
                found = true;
            }
            if (!found) {
                clone = subgroup.cloneNode(true);
                this.duplicatedGroups.set(groupId, clone);
            } else {
                if (!subgroup) {
                    clone = this.duplicatedGroups.get(groupId);
                } else {
                    clone = subgroup.cloneNode(true);
                }
            }
            return clone;
        },

        repeatGetChecked: function (group) {
            // /stupid fix for radio buttons loosing their checked value
            var tocheck = [];
            group.getElements('.fabrikinput').each(function (i) {
                if (i.type === 'radio' &&
i.getProperty('checked')) {
                    tocheck.push(i);
                }
            });
            return tocheck;
        },

        /**
         * Duplicates the groups sub group and places it at the end of the
group
         *
         * @param   event  e       Click event
         * @param   bool   scroll  Scroll to group if offscreen
         */
        duplicateGroup: function (e, scroll) {
            scroll = typeof scroll !== 'undefined' ? scroll :
true;
            var subElementContainer, container;
            Fabrik.fireEvent('fabrik.form.group.duplicate',
[this, e]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            if (e) {
                e.preventDefault();
            }
            var i =
e.target.getParent('.fabrikGroup').id.replace('group',
'');
            var group_id = i.toInt();
            var group = document.id('group' + i);
            var c = this.repeatGroupMarkers.get(i);
            var repeats = document.id('fabrik_repeat_group_' + i
+ '_counter').get('value').toInt();
            if (repeats >= this.options.maxRepeat[i] &&
this.options.maxRepeat[i] !== 0) {
                if (this.options.minMaxErrMsg[i] !== '') {
                    var errorMessage = this.options.minMaxErrMsg[i];
                    errorMessage = errorMessage.replace(/\{min\}/,
this.options.minRepeat[i]);
                    errorMessage = errorMessage.replace(/\{max\}/,
this.options.maxRepeat[i]);
                    window.alert(errorMessage);
                }
                return;
            }
            document.id('fabrik_repeat_group_' + i +
'_counter').value = repeats + 1;

            if (this.isFirstRepeatSubGroup(group)) {
                var subgroups =
group.getElements('.fabrikSubGroup');
                // user has removed all repeat groups and now wants to add
it back in
                // remove the 'no groups' notice

                var sub =
subgroups[0].getElement('.fabrikSubGroupElements');
                if (typeOf(sub) === 'null') {
                    group.getElement('.fabrikNotice').dispose();
                    sub = subgroups[0];

                    // Table group
                    var add = group.getElement('.addGroup');
                   
add.inject(sub.getElement('div.fabrikGroupRepeater'),'top');
                    sub.setStyle('display', '');
                } else {
                   
subgroups[0].getElement('.fabrikNotice').dispose();
                   
subgroups[0].getElement('.fabrikSubGroupElements').show();
                }

                this.repeatGroupMarkers.set(i,
this.repeatGroupMarkers.get(i) + 1);
                document.id('fabrik_repeat_group_' + i +
'_added').value = '1';

                this.formElements.each(function (e, k) {
                    if (e.groupid === i && typeOf(e.element) !==
'null') {
                        this.addMustValidate(e);
                    }
                }.bind(this));

               
Fabrik.fireEvent('fabrik.form.group.duplicate.end', [this, e, i,
c]);

                return;
            }

            var clone = this.getSubGroupToClone(i);
            var tocheck = this.repeatGetChecked(group);

            // Check for table style group, which may or may not have a
tbody in it
            var groupTable =
group.getElement('table.repeatGroupTable');
            if (groupTable) {
                if (groupTable.getElement('tbody')) {
                    groupTable = groupTable.getElement('tbody');
                }
                groupTable.appendChild(clone);
            } else {
                group.appendChild(clone);
            }

            tocheck.each(function (i) {
                i.setProperty('checked', true);
            });

            this.subelementCounter = 0;
            // Remove values and increment ids
            var newElementControllers = [],
                hasSubElements = false,
                inputs = clone.getElements('.fabrikinput'),
                lastinput = null;
            this.formElements.each(function (el) {
                var formElementFound = false;
                subElementContainer = null;
                var subElementCounter = -1;
                inputs.each(function (input) {

                    hasSubElements = el.hasSubElements();

                    container =
input.getParent('.fabrikSubElementContainer');
                    var testid = (hasSubElements && container) ?
container.id : input.id;
                    var cloneName = el.getCloneName();

                    // Test ===, plus special case for join rendered as
auto-complete
                    if (testid === cloneName || testid === cloneName +
'-auto-complete') {
                        lastinput = input;
                        formElementFound = true;

                        if (hasSubElements) {
                            subElementCounter++;
                            subElementContainer =
input.getParent('.fabrikSubElementContainer');

                            // Clone the first inputs event to all
subelements
                            // $$$ hugh - sanity check in case we have an
element which has no input
                            if
(document.id(testid).getElement('input')) {
                               
input.cloneEvents(document.id(testid).getElement('input'));
                                // Note: Radio's etc. now have their
events delegated from the form - so no need to duplicate them
                                // Update labels for sub elements
                                var l =
subElementContainer.getParent('.fabrikElementContainer').getElement('label');
                                if (l) {
                                    l.setProperty('for',
subElementContainer.id);
                               }
                            }

                        } else {
                            input.cloneEvents(el.element);

                            // Update the element id use el.element.id
rather than input.id as
                            // that may contain _1 at end of id
                            var bits =
Array.mfrom(el.element.id.split('_'));
                            bits.splice(bits.length - 1, 1, c);
                            input.id = bits.join('_');

                            // Update labels for non sub elements
                            var l =
input.getParent('.fabrikElementContainer').getElement('label');
                            if (l) {
                                l.setProperty('for', input.id);
                            }
                        }
                        if (typeOf(input.name) !== 'null') {
                            input.name =
input.name.replace('[0]', '[' + c + ']');
                        }
                    }
                }.bind(this));

                if (formElementFound) {
                    if (hasSubElements &&
typeOf(subElementContainer) !== 'null') {
                        // if we are checking subelements set the container
id after they have all
                        // been processed
                        // otherwise if check only works for first
subelement and no further
                        // events are cloned

                        // $$$ rob fix for date element
                        var bits =
Array.mfrom(el.options.element.split('_'));
                        bits.splice(bits.length - 1, 1, c);
                        subElementContainer.id = bits.join('_');
                    }
                    var origelid = el.options.element;
                    // clone js element controller, set form to be passed
by reference and
                    // not cloned
                    var ignore = el.unclonableProperties();
                    var newEl = new CloneObject(el, true, ignore);

                    newEl.container = null;
                    newEl.options.repeatCounter = c;

                    // This seems to be wrong, as it'll set origId to
the repeat ID with the _X appended.
                    //newEl.origId = origelid;

                    if (hasSubElements &&
typeOf(subElementContainer) !== 'null') {
                        newEl.element = document.id(subElementContainer);
                        newEl.cloneUpdateIds(subElementContainer.id);
                        newEl.options.element = subElementContainer.id;
                        newEl._getSubElements();
                        // update the labels
                        newEl.subElements.each(function (subEl) {
                            l = subEl.nextElementSibling;
                            while (l) {
                                if (l.nodeName == "LABEL") {
                                    let lBits =
l.htmlFor.split('_');
                                    let eBits =
newEl.options.element.split('_');
                                    lBits[6] = eBits[6];
                                    l.htmlFor = lBits.join('_');
                                    break;
                                }
                                l = l.nextElementSibling;
                            }
                        });
                        // Update the id on the fieldset if there is one
                        let fs =
subElementContainer.getElement('fieldset');
                        if (fs) {
                            fs.setAttribute('id',
subElementContainer.id);
                        }
                    } else {
                        newEl.cloneUpdateIds(lastinput.id);
                    }
                    //newEl.reset();
                    newElementControllers.push(newEl);
                }
            }.bind(this));

            newElementControllers.each(function (newEl) {
                newEl.cloned(c);
                // $$$ hugh - moved reset() from end of loop above,
otherwise elements with un-cloneable object
                // like maps end up resetting the wrong map to default
values.  Needs to run after element has done
                // whatever it needs to do with un-cloneable object before
resetting.
                // $$$ hugh - adding new option to allow copying of the
existing element values when copying
                // a group, instead of resetting to default value.  This
means knowing what the group PK element
                // is, do we don't copy that value.  hence new
group_pk_ids[] array, which gives us the PK element
                // name in regular full format, which we need to test
against the join string name.
                //var pk_re = new RegExp('\\[' +
this.options.group_pk_ids[group_id] + '\\]');
                var pk_re = new
RegExp(this.options.group_pk_ids[group_id]);
                if (!this.options.group_copy_element_values[group_id] ||
                    (this.options.group_copy_element_values[group_id]
&&
                    newEl.element.name &&
newEl.element.name.test(pk_re))) {
                    // Call reset method that resets both events and value
back to default.
                    newEl.reset();
                }
                else {
                    // Call reset method that only resets the events, not
the value
                    newEl.resetEvents();
                }
            }.bind(this));
            
            // Update the element container label
            if( container )
            {
               
lastinput.getParent('.fabrikElementContainer').getElement('label').htmlFor
= container.id;
            }
            var o = {};
            o[i] = newElementControllers;
            this.addElements(o);

	        /**
             * Only scroll the window if the new element is not visible and
'scroll' arg true
             * (so for example, we won't scroll if called from
duplicateGroupsToMin)
             */

            if (scroll) {
                var win_size = jQuery(window).height(),
                    win_scroll = document.id(window).getScroll().y,
                    obj = clone.getCoordinates();

	            /**
                 * If the bottom of the new repeat goes below the bottom of
the visible window,
                 * scroll up just enough to show it.
                 */

                if (obj.bottom > (win_scroll + win_size)) {
                    var new_win_scroll = obj.bottom - win_size;
                    this.winScroller.start(0, new_win_scroll);
                }
            }

            var myFx = new Fx.Tween(clone, {
                'property': 'opacity',
                duration  : 500
            }).set(0);

            clone.fade(1);
            Fabrik.fireEvent('fabrik.form.group.duplicate.end',
[this, e, i, c]);
            this.setRepeatGroupIntro(group, i);
            this.renumberSortable(i);
            this.repeatGroupMarkers.set(i, this.repeatGroupMarkers.get(i) +
1);
            this.addedGroups.push('group' + i);
        },

        /**
         * Set the repeat group intro text
         * @param  {string}  group  group container
         * @param  {string}  groupId  group ID
         */
        setRepeatGroupIntro: function (group, groupId) {
            var intro = this.options.group_repeat_intro[groupId],
                tmpIntro = '',
                targets =
group.getElements('*[data-role="group-repeat-intro"]');

            targets.each(function (target, i) {
                tmpIntro = intro.replace('{i}', i + 1);
                // poor man's parseMsgForPlaceholder ... ignore
elements in joined groups.
                this.formElements.each(function (el) {
                    if (!el.options.inRepeatGroup) {
                        var re = new RegExp('\{' + el.element.id
+ '\}');
                        // might should do a match first, to avoid always
calling getValue(), just not sure which is more overhead!
                        tmpIntro = tmpIntro.replace(re, el.getValue());
                    }
                });
                target.set('html', tmpIntro);
            }.bind(this));
        },

        update: function (o) {
            Fabrik.fireEvent('fabrik.form.update', [this,
o.data]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            var leaveEmpties = arguments[1] || false;
            var data = o.data;
            this.getForm();
            if (this.form) { // test for detailed view in module???
                var rowidel =
this.form.getElement('input[name=rowid]');
                if (rowidel && data.rowid) {
                    rowidel.value = data.rowid;
                }
            }
            this.formElements.each(function (el, key) {
                // if updating from a detailed view with prev/next then
data's key is in
                // _ro format
                if (typeOf(data[key]) === 'null') {
                    if (key.substring(key.length - 3, key.length) ===
'_ro') {
                        key = key.substring(0, key.length - 3);
                    }
                }
                // this if stopped the form updating empty fields. Element
update()
                // methods
                // should test for null
                // variables and convert to their correct values
                // if (data[key]) {
                if (typeOf(data[key]) === 'null') {
                    // only update blanks if the form is updating itself
                    // leaveEmpties set to true when this form is called
from updateRows
                    if (o.id === this.id && !leaveEmpties) {
                        el.update('');
                    }
                } else {
                    el.update(data[key]);
                }
            }.bind(this));
        },

        reset: function () {
            this.addedGroups.each(function (subgroup) {
                var group =
document.id(subgroup).findClassUp('fabrikGroup');
                var i = group.id.replace('group', '');
                document.id('fabrik_repeat_group_' + i +
'_counter').value =
                    document.id('fabrik_repeat_group_' + i +
'_counter').get('value').toInt() - 1;
                subgroup.remove();
            });
            this.addedGroups = [];
            Fabrik.fireEvent('fabrik.form.reset', [this]);
            if (this.result === false) {
                this.result = true;
                return;
            }
            this.formElements.each(function (el, key) {
                el.reset();
            }.bind(this));
        },

        showErrors: function (data) {
            var d = null;
            if (data.id === this.id) {
                // show errors
                var errors = new Hash(data.errors);
                if (errors.getKeys().length > 0) {
                    if
(typeOf(this.form.getElement('.fabrikMainError')) !==
'null') {
                       
this.form.getElement('.fabrikMainError').set('html',
this.options.error);
                       
this.form.getElement('.fabrikMainError').removeClass('fabrikHide');
                    }
                    errors.each(function (a, key) {
                        if (typeOf(document.id(key + '_error'))
!== 'null') {
                            var e = document.id(key + '_error');
                            var msg = new Element('span');
                            for (var x = 0; x < a.length; x++) {
                                for (var y = 0; y < a[x].length; y++) {
                                    d = new
Element('div').appendText(a[x][y]).inject(e);
                                }
                            }
                        } else {
                            fconsole(key + '_error' + ' not
found (form show errors)');
                        }
                    });
                }
            }
        },

        /** add additional data to an element - e.g database join elements
*/
        appendInfo: function (data) {
            this.formElements.each(function (el, key) {
                if (el.appendInfo) {
                    el.appendInfo(data, key);
                }
            }.bind(this));
        },

        clearForm: function () {
            this.getForm();
            if (!this.form) {
                return;
            }
            this.formElements.each(function (el, key) {
                if (key === this.options.primaryKey) {
                   
this.form.getElement('input[name=rowid]').value = '';
                }
                el.update('');
            }.bind(this));
            this.form.getElements('.fabrikError').empty();
           
this.form.getElements('.fabrikError').addClass('fabrikHide');
        },

	    /**
	     * Reset errors
         */
        clearErrors: function () {
           
jQuery(this.form).find('.fabrikError').removeClass('fabrikError')
               
.removeClass('error').removeClass('has-error');
            this.hideTips();
        },

        /**
         * Hide tips
         */
        hideTips: function () {
          this.elements.each(function(element) {
              element.removeTipMsg();
          });
        },

        /**
         * If the form is in a modal and the modal scrolls we should update
the
         * elements tips to keep the tip attached to the element.
         */
        scrollTips: function () {
            var self = this, top, left,
                match =
jQuery(self.form).closest('.fabrikWindow'),
                modal = match.find('.itemContent'),
                currentPos;

            var pos = function () {
                var origPos = match.data('origPosition');
                if (origPos === undefined) {
                    origPos = match.position();
                    match.data('origPosition', origPos);
                }

                currentPos = match.position();
                top = origPos.top - currentPos.top + modal.scrollTop();
                left = origPos.left - currentPos.left + modal.scrollLeft();
                self.elements.each(function(element) {
                    element.moveTip(top, left);
                });
            };

            modal.on('scroll', function () {
                pos();
            });

            Fabrik.on('fabrik.window.resized', function (window)
{
                if (match.length > 0 && window === match[0]) {
                    pos();
                }
            });
        },

        stopEnterSubmitting: function () {
            var inputs =
this.form.getElements('input.fabrikinput');
            inputs.each(function (el, i) {
                el.addEvent('keypress', function (e) {
                    if (e.key === 'enter') {
                        e.stop();
                        if (inputs[i + 1]) {
                            inputs[i + 1].focus();
                        }
                        //last one?
                        if (i === inputs.length - 1) {
                            this._getButton('Submit').focus();
                        }
                    }
                }.bind(this));
            }.bind(this));
        },

        getSubGroupCounter: function (group_id) {

        },

        addMustValidate: function (el) {
            if (this.options.ajaxValidation &&
this.options.toggleSubmit) {
	            this.mustValidateEls.set(el.element.id,
el.options.mustValidate);
                if (el.options.mustValidate) {
                    this.options.mustValidate = true;
                    this.toggleSubmit(false);
                }
            }
        },

        removeMustValidate: function (el) {
            if (this.options.ajaxValidation &&
this.options.toggleSubmit) {
                delete this.mustValidateEls[el.element.id];
                if (el.options.mustValidate) {
                    if (!this.mustValidateEls.hasValue(true)) {
                        this.toggleSubmit(true);
                    }
                }
            }
        },

        toggleSubmit: function (on) {
            var submit = this._getButton('Submit');
            if (typeOf(submit) !== 'null') {
                if (on === true) {
                    submit.disabled = '';
                    submit.setStyle('opacity', 1);
	                if (this.options.toggleSubmitTip !== '') {
		               
jQuery(this.form).find('.fabrikSubmitWrapper').tooltip('destroy');
		                this.toggleSubmitTipAdded = false;
	                }
                }
                else {
                    submit.disabled = 'disabled';
                    submit.setStyle('opacity', 0.5);
	                if (this.options.toggleSubmitTip !== '') {
	                    if (!this.toggleSubmitTipAdded) {
		                   
//jQuery(this.form).find('.fabrikSubmitWrapper').data('toggle',
'tooltip');
		                   
//jQuery(this.form).find('.fabrikSubmitWrapper').attr('title',
'Your form cannot be saved until all inputs have been
validated');
		                   
jQuery(this.form).find('.fabrikSubmitWrapper').tooltip();
		                    this.toggleSubmitTipAdded = true;
	                    }
	                }
                }
                Fabrik.fireEvent('fabrik.form.togglesubmit',
[this, on]);
            }
        },

        addPlugins: function (a) {
            var self = this;
            jQuery.each(a, function (k, p) {
                p.form = self;
            });
            this.plugins = a;
        }
    });

        // Deprecated - think its no longer used.
    Fabrik.form = function (ref, id, opts) {
        var form = new FbForm(id, opts);
        Fabrik.addBlock(ref, form);
        return form;
    };
    return FbForm;
});