app/assets/javascripts/fae/form/_ajax.js
/* global Fae, fae_chosen, fileinputer, FCH, Cookies */
/**
* Fae AJAX
* @namespace form.ajax
* @memberof form
*/
Fae.form.ajax = {
init: function() {
this.$addedit_form = $('.js-addedit-form, .js-index-addedit-form');
this.$filter_form = $('.js-filter-form');
this.$nested_form = $('.nested-form');
this.addEditLinks();
this.addEditSubmission();
this.addCancelLinks();
this.imageDeleteLinks();
this.htmlListeners();
this.deleteNoForm();
},
/**
* Click event listener for add and edit links applied to both index and nested forms
*/
addEditLinks: function() {
var _this = this;
this.$addedit_form.on('click', '.js-add-link, .js-edit-link', function(ev) {
ev.preventDefault();
var $this = $(this);
var $parent = $this.hasClass('js-index-add-link') ? $('.js-addedit-form') : $this.closest('.js-addedit-form');
// scroll to the last column of the tbody, where the form will start
FCH.smoothScroll($parent.find('tbody tr:last-child'), 500, 450, -20);
_this._addEditActions($this.attr('href'), $parent.find('.js-addedit-form-wrapper'));
});
},
/**
* Once add or edit is clicked, load remote data, add it to the DOM and initialize listeners on the new create form
* @protected
* @param {String} remote_url - Remote page to load form from
* @param {jQuery} $wrapper - Whole form container
* @see addEditLinks
*/
_addEditActions: function(remote_url, $wrapper) {
$.get(remote_url, function(data){
// check to see if the content is hidden and slide it down if it is.
if ($wrapper.is(':hidden')) {
// replace the content of the form area and initiate the chosen and fileinputer
$wrapper.html(data).find('.select select').fae_chosen({ width: '300px' });
$wrapper.find('.input.file').fileinputer();
$wrapper.slideDown();
} else {
// if it is visible, replace its content by retaining height
$wrapper.height($wrapper.height());
// replace the content of the form area and then remove that height and then chosen and then fileinputer
$wrapper.html(data).css('height', '').find('.select select').fae_chosen();
$wrapper.find('.input.file').fileinputer();
}
this.$nested_form = $('.nested-form');
// Bind validation to nested form fields added by AJAX
Fae.form.validator.bindValidationEvents(this.$nested_form);
// Reinitialize form elements
Fae.form.dates.initDatepicker();
Fae.form.dates.initDateRangePicker();
Fae.form.color.initColorpicker();
Fae.form.slugger.addListener();
Fae.form.validator.length_counter.init();
Fae.form.text.initMarkdown();
Fae.form.text.initHTML();
Fae.form.checkbox.setCheckboxAsActive();
Fae.form.select.init();
Fae.form.formManager.setupAllFields($wrapper.find('form'));
Fae.form.dragDrop.init();
Fae.tables.rowSorting();
// validate nested form fields on submit
Fae.form.validator.formValidate(this.$nested_form);
$wrapper.find('.hint').hinter();
});
},
/**
* Click event listener for cancel links applied to both index and nested forms; clears form to prevent saving errors
*/
addCancelLinks: function() {
this.$addedit_form.on('click', '.js-cancel-nested', function(ev) {
ev.preventDefault();
var $this = $(this);
var $form_wrapper = $this.closest('.js-addedit-form-wrapper');
if ($form_wrapper.length) {
$form_wrapper.slideUp('normal', function(){
$form_wrapper.empty();
});
}
});
},
/**
* Once form is submitted and receives a successful AJAX response, replace form data and initialize listeners on nested elements
* @fires {@link navigation.fadeNotices}
*/
addEditSubmission: function() {
var _this = this;
this.$addedit_form.on('ajax:success', function(evt, data, status, xhr){
var $target = $(evt.target);
// We need to target the form wrapper containing the target form to enable nesting
// multiple forms.
// Relying on $this will end up redrawing the top-most parent form with the returned table
var $theFormWrapper = $target.closest('.js-addedit-form');
// ignore calls not returning html
if (data !== ' ' && $(data)[0]) {
var $this = $(this);
// if its the new or old remotipart, return the html
var $html = $(data).length === 1 ? $(data) : $(data)[2];
// if it returns data inside textarea, strip that out
if ( $($html).is('textarea') ) {
$html = $( $($html).val() );
}
if ($html) {
if($html.hasClass('js-addedit-form') || $html.hasClass( 'js-index-addedit-form' )) {
// we're returning the table, replace everything
_this._addEditReplaceAndReinit($theFormWrapper, $html.html(), $target);
} else if ($html.hasClass('nested-form')) {
// we're returning the form due to an error, just replace the form
$theFormWrapper.find('.nested-form' ).replaceWith($html);
$theFormWrapper.find('.select select').fae_chosen();
$theFormWrapper.find('.input.file').fileinputer();
Fae.form.dates.initDatepicker();
Fae.form.dates.initDateRangePicker();
Fae.form.color.initColorpicker();
Fae.form.validator.length_counter.init();
Fae.form.checkbox.setCheckboxAsActive();
Fae.form.text.initMarkdown();
Fae.form.text.initHTML();
FCH.smoothScroll($this.find('.js-addedit-form-wrapper'), 500, 100, 120);
}
}
if (_this.$filter_form.length) {
_this.filterSubmission();
}
Fae.navigation.fadeNotices();
} else if ($target.hasClass('js-asset-delete')) {
// handle remove asset links on nested forms
var $parent = $target.closest('.asset-actions');
$parent.fadeOut(function(){
$parent.next('.asset-inputs').fadeIn();
$parent.remove();
});
}
Fae.navigation.lockFooter();
});
},
/**
* Replace AJAX'd form and add calls to all new HTML elements
* @protected
* @param $el {jQuery} - Object to be replaced
* @param html {String} - New HTML
* @param $target {jQuery} - Original form wrapper
* @see addEditSubmission
*/
_addEditReplaceAndReinit: function($el, html, $target) {
var $form_wrapper = $el.find('.js-addedit-form-wrapper');
// Private function replaces parent element with HTML and reinits select and sorting
function regenerateHTML() {
// .html() is not replacing it properly
$el.get(0).innerHTML = html;
$el.find('.select select').fae_chosen();
Fae.tables.rowSorting();
Fae.navigation.fadeNotices();
if ($el.find('.js-content-header').length) {
Fae.navigation.stickyHeaders(true);
}
}
// if there's a form wrap, slide it up before replacing content
if ($form_wrapper.length) {
$form_wrapper.slideUp(regenerateHTML);
} else {
regenerateHTML();
}
if (!$target.hasClass('js-delete-link')) {
FCH.smoothScroll($el.parent(), 500, 100, 120);
}
},
/**
* Attach filter listeners
*/
filterSubmission: function() {
var _this = this;
_this.$filter_form
.on('submit', function() {
$('.js-reset-btn').show();
})
// On filter change, update table data
.on('ajax:success', function(evt, data, status, xhr){
$(this).next('table').replaceWith( $(data).find('table').first() );
$('.pagination').replaceWith( $(data).find('.pagination').first() );
Fae.tables.columnSorting();
Fae.tables.rowSorting();
Fae.tables.sortColumnsFromCookies();
Fae.navigation.lockFooter();
})
// Reset filter button
.on('click', '.js-reset-btn', function(ev) {
ev.preventDefault();
var $form = $(this).closest('form');
$form.get(0).reset();
$form.find('select').val('').trigger('chosen:updated');
// reset hashies
window.location.hash = '';
// Spoof form submission
$form.submit();
$(this).hide();
})
.on('click', '.table-filter-keyword-wrapper i', function() {
_this.$filter_form.submit();
})
.on('change', 'select', function() {
_this.$filter_form.submit();
// Update hash when filter dropdowns changed
var key = $(this).attr('id').split('filter_')[1];
var value = $(this).val();
_this.fry.update(key, value);
});
},
/**
* Delete or replace file for non-AJAX'd fields
*/
deleteNoForm: function() {
$('.js-asset-delete').on('ajax:success', function(){
var $this = $(this);
if (!$this.closest('.js-addedit-form-wrapper').length) {
// handle remove asset links
var $parent = $this.closest('.asset-actions');
$parent.fadeOut(function(){
$parent.next('.asset-inputs').fadeIn();
$parent.remove();
});
}
});
},
/**
* Attach delete listener to images uploaded
*/
imageDeleteLinks: function() {
$('.imageDeleteLink').click(function(e) {
e.preventDefault();
var $this = $(this);
if (confirm('Are you sure you want to delete this image?')) {
$.post($this.attr('href'), 'html');
$this.parent().next().show();
$this.parent().hide();
}
});
},
/**
* Attaching click handlers to #js-main-content to allow ajax replacement
* @todo Clean this up, moving listeners into their respective component classes (select, checkbox, etc.)
*/
htmlListeners: function() {
$('#js-main-content, .login-form > form, #simplemodal-data')
/**
* For the delete button on file input
*/
.on('click', '.js-file-clear', function(e) {
e.preventDefault();
var $parent = $(this).parent();
$parent.next().show();
$parent.hide();
})
/**
* For the yes/no slider
*/
.on('click', '.slider-wrapper', function(e){
e.preventDefault();
$(this).toggleClass('slider-yes-selected');
})
/**
* The settings menu for tables and checkboxes
*/
.on('click', '.boolean label, .js-checkbox-wrapper label', function(e){
var $this = $(this);
if(!$this.hasClass('disabled') && !$this.hasClass('readonly')) {
$this.toggleClass('active');
}
})
/**
* Stop event bubbling and running the above toggleClass twice
*/
.on('click', '.boolean :checkbox, .js-checkbox-wrapper :checkbox', function(e){
e.stopPropagation();
})
/**
* Support for a focus state on radio collections
*/
.on('focus blur', '.radio_collection :radio', function(e) {
$(this)
.closest('.input.radio_collection')
.toggleClass('focused', $(this).has(':focus'));
})
/**
* Support for a focus state on checkboxes
*/
.on('focus blur', '.boolean :checkbox, .js-checkbox-wrapper :checkbox', function(e) {
$(this)
.closest('label.boolean, span.checkbox')
.toggleClass('focused', $(this).has(':focus'));
})
/**
* Support spacebar toggling for checkboxes
*/
.on('keydown', '.boolean, .js-checkbox-wrapper :checkbox', function(e) {
if (e.which === 32) {
e.preventDefault();
$(':checkbox:focus')
.closest('label')
.trigger('click');
}
})
/**
* Support for shift+tab off of ms-list element.
* By default, $.multiSelect() plugin captures shift+tab and disgards it
* @todo This entire method feels very brittle. Possible alternative:
* - Create an index of all focusable form elements on page load / DOM mutation
* - Use this index to navigate upwards from .ms-list element
*/
.on('keydown', '.ms-list', function(e) {
if (e.which === 9 && e.shiftKey) {
e.preventDefault();
// Sniff out the previous focusable element
var $prevFocusable = $(this)
.closest('.input')
.prevAll('.input:not(.hidden):first') // Ugh X(
.find('input[type!=hidden][type!=file], select, button, textarea') // Yuck :(
.first();
// Trigger focus
$prevFocusable.focus();
// Check for select instance and further trigger focus via Chosen API
if ($prevFocusable.is('select')) {
$prevFocusable.trigger('chosen:activate');
}
}
});
}
};