winter/modules/system/assets/js/framework.js
Luke Towers 37a735c8ed Process JS API requests through the document
Process JS API requests ($.request(handler, options)) through the document object instead of a non-existent form element. The form element was originally added because the framework only supported requests made from within a form
```
[22:08:12] spunky:	The $triggerEl was implemented because previously it was just $form
[22:08:24] spunky: For data-requests without a form, these events were being ignored
[22:08:31] spunky:	So as a workaround I implemented $triggerEl
```
As this is no longer the case, any JS API requests will now be made through the document element instead to support listening to events (such as ajaxSetup) even when there is no real attached element to trigger them on.
2017-02-06 10:36:57 -06:00

494 lines
18 KiB
JavaScript

/* ========================================================================
* OctoberCMS: front-end JavaScript framework
* http://octobercms.com
* ========================================================================
* Copyright 2017 Alexey Bobkov, Samuel Georges
* ======================================================================== */
if (window.jQuery === undefined) {
throw new Error('The jQuery library is not loaded. The OctoberCMS framework cannot be initialized.');
}
if (window.jQuery.request !== undefined) {
throw new Error('The OctoberCMS framework is already loaded.');
}
+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".')
}
/*
* Prepare the options and execute the request
*/
var $form = $el.closest('form'),
$triggerEl = !!$form.length ? $form : $el,
context = { handler: handler, options: options }
$el.trigger('ajaxSetup', [context])
var _event = jQuery.Event('oc.beforeRequest')
$triggerEl.trigger(_event, context)
if (_event.isDefaultPrevented()) return
var data = [$form.serialize()],
loading = options.loading !== undefined ? options.loading : null,
isRedirect = options.redirect !== undefined && options.redirect.length,
useFlash = options.flash !== undefined
$.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))
}
if ($.type(loading) == 'string') {
loading = $(loading)
}
var requestHeaders = {
'X-OCTOBER-REQUEST-HANDLER': handler,
'X-OCTOBER-REQUEST-PARTIALS': this.extractPartials(options.update)
}
if (useFlash) {
requestHeaders['X-OCTOBER-REQUEST-FLASH'] = 1
}
var requestOptions = {
url: window.location.href,
context: context,
headers: requestHeaders,
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
if (useFlash && data['X_OCTOBER_FLASH_MESSAGES']) {
$.each(data['X_OCTOBER_FLASH_MESSAGES'], function(type, message) {
requestOptions.handleFlashMessage(message, type)
})
}
/*
* 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, errorMsg, textStatus, jqXHR])
if (_event.isDefaultPrevented()) return
/*
* Halt here if the data-request-error attribute returns false
*/
if (options.evalError && eval('(function($el, context, errorMsg, textStatus, jqXHR) {'+options.evalError+'}.call($el.get(0), $el, context, errorMsg, 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, requests confirmation from the user
*/
handleConfirmMessage: function(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)
},
/*
* 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, focus fields with errors
*/
handleValidationMessage: function(message, fields) {
$triggerEl.trigger('ajaxValidation', [context, message, fields])
var isFirstInvalidField = true
$.each(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
}
}
})
},
/*
* Custom function, display a flash message to the user
*/
handleFlashMessage: function(message, type) {},
/*
* Custom function, redirect the browser to another location
*/
handleRedirectResponse: function(url) {
window.location.href = url
},
/*
* 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 ($.type(selector) == 'string' && selector.charAt(0) == '@') {
$(selector.substring(1)).append(data[partial]).trigger('ajaxUpdate', [context, data, textStatus, jqXHR])
}
else if ($.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) {
requestOptions.handleRedirectResponse(options.redirect)
}
/*
* Handle validation
*/
if (data['X_OCTOBER_ERROR_FIELDS']) {
requestOptions.handleValidationMessage(data['X_OCTOBER_ERROR_MESSAGE'], data['X_OCTOBER_ERROR_FIELDS'])
}
/*
* 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('&')
/*
* Initiate request
*/
if (options.confirm && !requestOptions.handleConfirmMessage(options.confirm)) {
return
}
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'),
flash: $this.data('request-flash'),
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 $(document).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(e) {
e.preventDefault()
$(this).request()
if ($(this).is('[type=submit]'))
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) {
var
$el = $(this),
lastValue = $el.data('oc.lastvalue')
if (!$el.is('[type=email],[type=number],[type=password],[type=search],[type=text]'))
return
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);