mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
442 lines
17 KiB
JavaScript
442 lines
17 KiB
JavaScript
/* ========================================================================
|
|
* October CMS: front-end JavaScript framework
|
|
* http://octobercms.com
|
|
* ========================================================================
|
|
* Copyright 2016 Alexey Bobkov, Samuel Georges
|
|
*
|
|
* ======================================================================== */
|
|
|
|
if (window.jQuery === undefined)
|
|
throw new Error('The jQuery library is not loaded. The October CMS framework cannot be initialized.');
|
|
|
|
+function ($) { "use strict";
|
|
|
|
var Request = function (element, handler, options) {
|
|
var $el = this.$el = $(element);
|
|
this.options = options || {};
|
|
/*
|
|
* Validate handler name
|
|
*/
|
|
|
|
if (handler == undefined)
|
|
throw new Error('The request handler name is not specified.')
|
|
|
|
if (!handler.match(/^(?:\w+\:{2})?on*/))
|
|
throw new Error('Invalid handler name. The correct handler name format is: "onEvent".')
|
|
|
|
/*
|
|
* Custom function, requests confirmation from the user
|
|
*/
|
|
|
|
function handleConfirmMessage(message) {
|
|
var _event = jQuery.Event('ajaxConfirmMessage')
|
|
|
|
_event.promise = $.Deferred()
|
|
if ($(window).triggerHandler(_event, [message]) !== undefined) {
|
|
_event.promise.done(function(){
|
|
options.confirm = null
|
|
new Request(element, handler, options)
|
|
})
|
|
return false
|
|
}
|
|
|
|
if (_event.isDefaultPrevented()) return
|
|
if (message) return confirm(message)
|
|
}
|
|
|
|
/*
|
|
* Initiate request
|
|
*/
|
|
|
|
if (options.confirm && !handleConfirmMessage(options.confirm))
|
|
return
|
|
|
|
/*
|
|
* Prepare the options and execute the request
|
|
*/
|
|
|
|
var
|
|
$form = $el.closest('form'),
|
|
$triggerEl = !!$form.length ? $form : $el,
|
|
context = { handler: handler, options: options },
|
|
loading = options.loading !== undefined && options.loading.length ? $(options.loading) : null,
|
|
isRedirect = options.redirect !== undefined && options.redirect.length
|
|
|
|
var _event = jQuery.Event('oc.beforeRequest')
|
|
$triggerEl.trigger(_event, context)
|
|
if (_event.isDefaultPrevented()) return
|
|
|
|
var data = [$form.serialize()]
|
|
|
|
$.each($el.parents('[data-request-data]').toArray().reverse(), function extendRequest(){
|
|
data.push($.param(paramToObj('data-request-data', $(this).data('request-data'))))
|
|
})
|
|
|
|
if ($el.is(':input') && !$form.length) {
|
|
var inputName = $el.attr('name')
|
|
if (inputName !== undefined && options.data[inputName] === undefined)
|
|
options.data[inputName] = $el.val()
|
|
}
|
|
|
|
if (options.data !== undefined && !$.isEmptyObject(options.data))
|
|
data.push($.param(options.data))
|
|
|
|
var requestOptions = {
|
|
context: context,
|
|
headers: {
|
|
'X-OCTOBER-REQUEST-HANDLER': handler,
|
|
'X-OCTOBER-REQUEST-PARTIALS': this.extractPartials(options.update)
|
|
},
|
|
success: function(data, textStatus, jqXHR) {
|
|
/*
|
|
* Halt here if beforeUpdate() or data-request-before-update returns false
|
|
*/
|
|
if (this.options.beforeUpdate.apply(this, [data, textStatus, jqXHR]) === false) return
|
|
if (options.evalBeforeUpdate && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalBeforeUpdate+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))') === false) return
|
|
|
|
/*
|
|
* Trigger 'ajaxBeforeUpdate' on the form, halt if event.preventDefault() is called
|
|
*/
|
|
var _event = jQuery.Event('ajaxBeforeUpdate')
|
|
$triggerEl.trigger(_event, [context, data, textStatus, jqXHR])
|
|
if (_event.isDefaultPrevented()) return
|
|
|
|
/*
|
|
* Proceed with the update process
|
|
*/
|
|
var updatePromise = requestOptions.handleUpdateResponse(data, textStatus, jqXHR)
|
|
|
|
updatePromise.done(function(){
|
|
$triggerEl.trigger('ajaxSuccess', [context, data, textStatus, jqXHR])
|
|
options.evalSuccess && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalSuccess+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))')
|
|
})
|
|
|
|
return updatePromise
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
var errorMsg,
|
|
updatePromise = $.Deferred()
|
|
|
|
if ((window.ocUnloading !== undefined && window.ocUnloading) || errorThrown == 'abort')
|
|
return
|
|
|
|
/*
|
|
* Disable redirects
|
|
*/
|
|
isRedirect = false
|
|
options.redirect = null
|
|
|
|
/*
|
|
* Error 406 is a "smart error" that returns response object that is
|
|
* processed in the same fashion as a successful response.
|
|
*/
|
|
if (jqXHR.status == 406 && jqXHR.responseJSON) {
|
|
errorMsg = jqXHR.responseJSON['X_OCTOBER_ERROR_MESSAGE']
|
|
updatePromise = requestOptions.handleUpdateResponse(jqXHR.responseJSON, textStatus, jqXHR)
|
|
}
|
|
/*
|
|
* Standard error with standard response text
|
|
*/
|
|
else {
|
|
errorMsg = jqXHR.responseText ? jqXHR.responseText : jqXHR.statusText
|
|
updatePromise.resolve()
|
|
}
|
|
|
|
updatePromise.done(function(){
|
|
$el.data('error-message', errorMsg)
|
|
|
|
/*
|
|
* Trigger 'ajaxError' on the form, halt if event.preventDefault() is called
|
|
*/
|
|
var _event = jQuery.Event('ajaxError')
|
|
$triggerEl.trigger(_event, [context, textStatus, jqXHR])
|
|
if (_event.isDefaultPrevented()) return
|
|
|
|
/*
|
|
* Halt here if the data-request-error attribute returns false
|
|
*/
|
|
if (options.evalError && eval('(function($el, context, textStatus, jqXHR) {'+options.evalError+'}.call($el.get(0), $el, context, textStatus, jqXHR))') === false)
|
|
return
|
|
|
|
requestOptions.handleErrorMessage(errorMsg)
|
|
})
|
|
|
|
return updatePromise
|
|
},
|
|
complete: function(data, textStatus, jqXHR) {
|
|
$triggerEl.trigger('ajaxComplete', [context, data, textStatus, jqXHR])
|
|
options.evalComplete && eval('(function($el, context, data, textStatus, jqXHR) {'+options.evalComplete+'}.call($el.get(0), $el, context, data, textStatus, jqXHR))')
|
|
},
|
|
|
|
/*
|
|
* Custom function, display an error message to the user
|
|
*/
|
|
handleErrorMessage: function(message) {
|
|
var _event = jQuery.Event('ajaxErrorMessage')
|
|
$(window).trigger(_event, [message])
|
|
if (_event.isDefaultPrevented()) return
|
|
if (message) alert(message)
|
|
},
|
|
|
|
/*
|
|
* Custom function, handle any application specific response values
|
|
* Using a promisary object here in case injected assets need time to load
|
|
*/
|
|
handleUpdateResponse: function(data, textStatus, jqXHR) {
|
|
|
|
/*
|
|
* Update partials and finish request
|
|
*/
|
|
var updatePromise = $.Deferred().done(function(){
|
|
for (var partial in data) {
|
|
/*
|
|
* If a partial has been supplied on the client side that matches the server supplied key, look up
|
|
* it's selector and use that. If not, we assume it is an explicit selector reference.
|
|
*/
|
|
var selector = (options.update[partial]) ? options.update[partial] : partial
|
|
if (jQuery.type(selector) == 'string' && selector.charAt(0) == '@') {
|
|
$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate', [context, data, textStatus, jqXHR])
|
|
} else if (jQuery.type(selector) == 'string' && selector.charAt(0) == '^') {
|
|
$(selector.substring(1)).prepend(data[partial]).trigger('ajaxUpdate', [context, data, textStatus, jqXHR])
|
|
} else {
|
|
$(selector).trigger('ajaxBeforeReplace')
|
|
$(selector).html(data[partial]).trigger('ajaxUpdate', [context, data, textStatus, jqXHR])
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Wait for .html() method to finish rendering from partial updates
|
|
*/
|
|
setTimeout(function(){
|
|
$(window)
|
|
.trigger('ajaxUpdateComplete', [context, data, textStatus, jqXHR])
|
|
.trigger('resize')
|
|
}, 0)
|
|
})
|
|
|
|
/*
|
|
* Handle redirect
|
|
*/
|
|
if (data['X_OCTOBER_REDIRECT']) {
|
|
options.redirect = data['X_OCTOBER_REDIRECT']
|
|
isRedirect = true
|
|
}
|
|
|
|
if (isRedirect)
|
|
window.location.href = options.redirect
|
|
|
|
/*
|
|
* Focus fields with errors
|
|
*/
|
|
if (data['X_OCTOBER_ERROR_FIELDS']) {
|
|
var isFirstInvalidField = true
|
|
$.each(data['X_OCTOBER_ERROR_FIELDS'], function focusErrorField(fieldName, fieldMessages){
|
|
var fieldElement = $form.find('[name="'+fieldName+'"], [name="'+fieldName+'[]"], [name$="['+fieldName+']"], [name$="['+fieldName+'][]"]').filter(':enabled').first()
|
|
if (fieldElement.length > 0) {
|
|
|
|
var _event = jQuery.Event('ajaxInvalidField')
|
|
$(window).trigger(_event, [fieldElement.get(0), fieldName, fieldMessages, isFirstInvalidField])
|
|
|
|
if (isFirstInvalidField) {
|
|
if (!_event.isDefaultPrevented()) fieldElement.focus()
|
|
isFirstInvalidField = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/*
|
|
* Handle asset injection
|
|
*/
|
|
if (data['X_OCTOBER_ASSETS']) {
|
|
assetManager.load(data['X_OCTOBER_ASSETS'], $.proxy(updatePromise.resolve, updatePromise))
|
|
}
|
|
else
|
|
updatePromise.resolve()
|
|
|
|
return updatePromise
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Allow default business logic to be called from user functions
|
|
*/
|
|
context.success = requestOptions.success
|
|
context.error = requestOptions.error
|
|
context.complete = requestOptions.complete
|
|
requestOptions = $.extend(requestOptions, options)
|
|
|
|
requestOptions.data = data.join('&')
|
|
|
|
if (loading) loading.show()
|
|
|
|
$(window).trigger('ajaxBeforeSend', [context])
|
|
$el.trigger('ajaxPromise', [context])
|
|
return $.ajax(requestOptions)
|
|
.fail(function(jqXHR, textStatus, errorThrown){
|
|
if (!isRedirect) {
|
|
$el.trigger('ajaxFail', [context, textStatus, jqXHR])
|
|
if (loading) loading.hide()
|
|
}
|
|
})
|
|
.done(function(data, textStatus, jqXHR){
|
|
if (!isRedirect) {
|
|
$el.trigger('ajaxDone', [context, data, textStatus, jqXHR])
|
|
if (loading) loading.hide()
|
|
}
|
|
})
|
|
.always(function(dataOrXhr, textStatus, xhrOrError){
|
|
$el.trigger('ajaxAlways', [context, dataOrXhr, textStatus, xhrOrError])
|
|
})
|
|
}
|
|
|
|
Request.DEFAULTS = {
|
|
update: {},
|
|
type : 'POST',
|
|
beforeUpdate: function(data, textStatus, jqXHR) {},
|
|
evalBeforeUpdate: null,
|
|
evalSuccess: null,
|
|
evalError: null,
|
|
evalComplete: null,
|
|
}
|
|
|
|
/*
|
|
* Internal function, build a string of partials and their update elements.
|
|
*/
|
|
Request.prototype.extractPartials = function(update) {
|
|
var result = []
|
|
|
|
for (var partial in update)
|
|
result.push(partial)
|
|
|
|
return result.join('&')
|
|
}
|
|
|
|
// REQUEST PLUGIN DEFINITION
|
|
// ============================
|
|
|
|
var old = $.fn.request
|
|
|
|
$.fn.request = function (handler, option) {
|
|
var args = arguments
|
|
|
|
var $this = $(this).first()
|
|
var data = {
|
|
evalBeforeUpdate: $this.data('request-before-update'),
|
|
evalSuccess: $this.data('request-success'),
|
|
evalError: $this.data('request-error'),
|
|
evalComplete: $this.data('request-complete'),
|
|
confirm: $this.data('request-confirm'),
|
|
redirect: $this.data('request-redirect'),
|
|
loading: $this.data('request-loading'),
|
|
update: paramToObj('data-request-update', $this.data('request-update')),
|
|
data: paramToObj('data-request-data', $this.data('request-data'))
|
|
}
|
|
if (!handler) handler = $this.data('request')
|
|
var options = $.extend(true, {}, Request.DEFAULTS, data, typeof option == 'object' && option)
|
|
return new Request($this, handler, options)
|
|
}
|
|
|
|
$.fn.request.Constructor = Request
|
|
|
|
$.request = function (handler, option) {
|
|
return $('<form />').request(handler, option)
|
|
}
|
|
|
|
// REQUEST NO CONFLICT
|
|
// =================
|
|
|
|
$.fn.request.noConflict = function () {
|
|
$.fn.request = old
|
|
return this
|
|
}
|
|
|
|
// REQUEST DATA-API
|
|
// ==============
|
|
|
|
function paramToObj(name, value) {
|
|
if (value === undefined) value = ''
|
|
if (typeof value == 'object') return value
|
|
|
|
try {
|
|
return JSON.parse(JSON.stringify(eval("({" + value + "})")))
|
|
}
|
|
catch (e) {
|
|
throw new Error('Error parsing the '+name+' attribute value. '+e)
|
|
}
|
|
}
|
|
|
|
$(document).on('change', 'select[data-request], input[type=radio][data-request], input[type=checkbox][data-request]', function documentOnChange(){
|
|
$(this).request()
|
|
})
|
|
|
|
$(document).on('click', 'a[data-request], button[data-request], input[type=button][data-request], input[type=submit][data-request]', function documentOnClick(){
|
|
$(this).request()
|
|
return false
|
|
})
|
|
|
|
$(document).on('keydown', 'input[type=text][data-request], input[type=submit][data-request], input[type=password][data-request]', function documentOnKeydown(e){
|
|
if (e.keyCode == 13) {
|
|
if (this.dataTrackInputTimer !== undefined)
|
|
window.clearTimeout(this.dataTrackInputTimer)
|
|
|
|
$(this).request()
|
|
return false
|
|
}
|
|
})
|
|
|
|
$(document).on('keyup', 'input[data-request][data-track-input]', function documentOnKeyup(e){
|
|
if (!$(this).is('[type=email],[type=number],[type=password],[type=search],[type=text]')) return
|
|
|
|
var
|
|
$el = $(this),
|
|
lastValue = $el.data('oc.lastvalue')
|
|
|
|
if (lastValue !== undefined && lastValue == this.value)
|
|
return
|
|
|
|
$el.data('oc.lastvalue', this.value)
|
|
|
|
if (this.dataTrackInputTimer !== undefined)
|
|
window.clearTimeout(this.dataTrackInputTimer)
|
|
|
|
var interval = $(this).data('track-input')
|
|
if (!interval)
|
|
interval = 300
|
|
|
|
var self = this
|
|
this.dataTrackInputTimer = window.setTimeout(function(){
|
|
$(self).request()
|
|
}, interval)
|
|
})
|
|
|
|
$(document).on('submit', '[data-request]', function documentOnSubmit(){
|
|
$(this).request()
|
|
return false
|
|
})
|
|
|
|
$(window).on('beforeunload', function documentOnBeforeUnload() {
|
|
window.ocUnloading = true
|
|
})
|
|
|
|
/*
|
|
* Invent our own event that unifies document.ready with window.ajaxUpdateComplete
|
|
*
|
|
* $(document).render(function() { })
|
|
* $(document).on('render', function(){ })
|
|
*/
|
|
|
|
$(document).ready(function triggerRenderOnReady(){
|
|
$(document).trigger('render')
|
|
})
|
|
|
|
$(window).on('ajaxUpdateComplete', function triggerRenderOnAjaxUpdateComplete() {
|
|
$(document).trigger('render')
|
|
})
|
|
|
|
$.fn.render = function(callback) {
|
|
$(document).on('render', callback)
|
|
}
|
|
|
|
}(window.jQuery);
|