/**
* @namespace preempt
*/
(function ($) {
'use strict';
/**
* @description plugin identifier
* @type {string}
* @private
* @const
*/
var PLUGIN_NAME = 'preempt',
/**
* @description instance_id
* @type {string}
* @private
*/
instance_id = "plugin." + PLUGIN_NAME,
/**
* Script URL prefix. JSHint hates this.
* @type {string}
* @private
* @const
*/
SCRIPT_URL = 'javascript:',
/**
* @description Class representing an instance of the Preempt plugin.
* @param {jQuery} element jQuery element to preempt
* @private
* @constructor
*/
Preempt = function Preempt(element) {
/**
* Name of this plugin
* @type {string}
* @protected
*/
this._name = PLUGIN_NAME;
/**
* jQuery element to preempt
* @type {jQuery}
* @protected
*/
this._element = element;
/**
* Whether or not to prepend "javascript:" to the attr string when restoring
* @type {boolean}
* @protected
*/
this._prefix = false;
};
Preempt.prototype = Object.create({});
/**
* @description Replaces the inlined JS with an event handler which also eval's the inlined JS.
* @param {Object} cfg Configuration object
*/
Preempt.prototype.init =
function init(cfg) {
var el = this._element,
preemptor = function preemptor(evt) {
var exec = function exec() {
return eval('(function(event){ ' + js +
'}).call(el[0], evt);');
},
retval;
if (before.call(el, evt, data.before) === false) {
return false;
}
if (evt.isImmediatePropagationStopped()) {
return;
}
retval = exec();
if (retval === false && !forcePropagation) {
return false;
}
if (evt.isImmediatePropagationStopped() && !forcePropagation) {
return;
}
if (forcePropagation && retval === false) {
after.call(el, evt, data.after);
} else {
retval = after.call(el, evt, data.after);
}
return retval;
},
attr = cfg.attr,
event = cfg.event,
forcePropagation = !!cfg.forcePropagation,
after = cfg.after || $.noop,
before = cfg.before || cfg.callback || $.noop,
data = cfg.data || {},
js = el.attr(attr) || '';
if (js.indexOf(SCRIPT_URL) === 0) {
this._prefix = true;
js = js.substring(11);
}
el.removeAttr(attr)
.off(event)
.on(event, preemptor);
// store it so we can easily restore it
el.data(cfg.js_id, js);
};
/**
* @method
* @param {Object} cfg Configuration object
*/
Preempt.prototype.restore = function restore(cfg) {
var el = this._element,
js_id = cfg.js_id,
attr = cfg.attr,
event = cfg.event,
js = el.data(js_id);
el.attr(attr, this._prefix ? SCRIPT_URL + js : js)
.off(event);
el.removeData(js_id);
};
/**
* @description Takes legacy inline JS (i.e. `onclick` and `href="javascript:..."`) and creates event handler(s) to be run around the inlined code.
* @todo Document more thoroughly.
* @example
*
* // given <button onclick="doSomething()">do something</button>
* // or <a href="javascript:doSomethingElse()">do something else</a>
*
* // Basic usage:
* $('button').preempt({
* attr: 'onclick',
* event: 'click',
* }, function doSomethingBeforeSomething() {
* // do something else
* });
*
* // Restoring the inline JS:
* $('button').preempt({
* attr: 'onclick',
* event: 'click',
* restore: true
* });
*
* // Fancy usage:
* $('button').preempt({
* attr: 'onclick',
* event: 'click',
* before: function executedBeforeInlineJS(event, data) {
* // stuff; return false to halt propagation to inline JS
* },
* after: function exectedAfterInlineJS(event, data) {
* // things; return false to prevent the default action and stop propagation
* },
* // will execute the after() function even if the inlined JS returned false.
* forcePropagation: true,
* data: {
* before: 'some data to be passed to the before() function',
* after: 'some data to be passed to the after() function',
* }
* });
* @this jQuery
* @param {{
* attr: string,
* event: string,
* before: ?function(jQuery.Event, *):?boolean,
* after: ?function(jQuery.Event, *):?boolean,
* forcePropagation: boolean=,
* data: Object<string, *>
* }} options
* - `attr` is the attribute you want to replace
* - `event` is the new event to bind
* - `before` is a function you wish to execute *before* the inline handler. The second parameter, `callback`, will be overridden by this function. If this function returns `false` or halts immediate propagation, the inline function will not be called, and further *immediate* propagation of the event will not occur.
* - `after` is a function you wish to execute *after* the inline handler. If this function returns `false` or halts immediate propagation, further *immediate* propagation of the event will not occur.
* - `forcePropagation` causes your `after` function to be executed *regardless* of whether the inline function stopped immediate propagation or returned `false`. In this case, if the inline function happened to return `false,` the default action for the event (whatever that may be; see {@link http://api.jquery.com/event.preventDefault/}) *will* be prevented and bubbling will not occur. If `after` is not present, this does nothing.
* - `data` is an object with keys `before`, and `after`. The values will be passed, respectively, to the `before` and `after` functions as the *second* parameter (the first will be the Event itself). A third key, `js`, is automatically added to the objects, and its value is the original contents of the `attr` attribute. It will have the `javascript:` prefix stripped, if present. It's worth remembering that the context (the `this`) of an inline function is the DOM element itself.
* @param {Function=} callback Event handler callback function to be executed *before* the inline handler. Alias for `options.before`; if both are present then `options.before` will take precedence.
* @todo support for delegate-style usage
* @returns {jQuery} A jQuery obj
* @memberOf preempt
*/
jQuery.fn.preempt = function preempt(options, callback) {
return $(this).each(function () {
var $this = $(this),
instance = $this.data(instance_id),
cfg,
js_id,
attr,
restore,
new_evt;
if ($.type(options) === 'undefined') {
throw new Error('"options" object is required');
}
attr = options.attr;
restore = !!options.restore;
new_evt = options.event;
if (!(attr && new_evt)) {
throw new Error('jquery.preempt: options.attr and opts.event are required');
}
js_id = instance_id + '.' + attr + '.' + new_evt + '.js';
cfg = $.extend({}, {js_id: js_id, callback: callback}, options);
if (!instance) {
// if we don't have an instance, restore() does nothing.
if (restore) {
return;
}
instance = new Preempt($this);
$this.data(instance_id, instance);
}
if (!$this.data(js_id)) {
instance.init(cfg);
} else if (restore) {
instance.restore(cfg);
}
});
};
// exposed for unit tests, but do what you will.
jQuery.fn.preempt.Preempt = Preempt;
})(jQuery);