PHP Classes

File: includes/js/jinplace.js

Recommend this page to a friend!
  Classes of Mark Richards   CliqonV4   includes/js/jinplace.js   Download  
File: includes/js/jinplace.js
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: CliqonV4
Framework with modules to build Web applications
Author: By
Last change:
Date: 5 years ago
Size: 20,123 bytes
 

Contents

Class file image Download
(function ($, window, document, undefined) { 'use strict'; var pluginName = "jinplace"; /** * @typedef {object} Options * @class Options * @property {!string} type - The type of field. Defaults to 'input' * @property {string} url - The url to submit to. Defaults to same page * @property {string} method - The HTTP method to use when submitting. Defaults to POST * @property {string} data - Text or JSON data as initial editing text * @property {string} loadurl - The URL to load content for editing * @property {string} elementId - The ID of the element * @property {string} object - A name to pass back on submit * @property {string} attribute - Another name to pass back on submit * @property {string} okButton - Create a submit button with this name * @property {string} cancelButton - Create a cancel button with this name * @property {string} inputClass - A css class that is added to the input field * @property {string} okButtonClass - A css class that is added to the ok button * @property {string} cancelButtonClass - A css class that is added to the cancel button * @property {jQuery|string} activator - Object (or css selector) for object to activate editing. Defaults to the element itself. * @property {boolean} textOnly - When true (the default) text returned from server is displayed literally and not as html. * @property {string} placeholder - Text to display in empty elements. * @property {submitFunction} submitFunction - Function that is called to submit the new value. * @property {loadFunction} loadFunction - Function that is called to load the editing data */ var option_list = ['type', 'url', 'method', 'data', 'loadurl', 'elementId', 'object', 'attribute', 'okButton', 'cancelButton', 'inputClass', 'activator', 'textOnly', 'placeholder', 'submitFunction', 'okButtonClass', 'cancelButtonClass' ]; /** * The actual constructor of the JinPlace object. * * @class jinplace * @memberOf jQuery.fn * @constructor * * @property {jQuery} element - The element containing plain text to be edited. * @property {Options} opts - The final set of options. */ function JinPlace(element, options) { var $el = this.element = $(element); // The editable element (often a span or div). var elementOptions = this.elementOptions($el); var act = elementOptions.activator || element; elementOptions.activator = $(act); // So we have 1) options defined in defaults, 2) passed into the plugin, 3) set // on the element. Combine all these together. var opts = $.extend({}, $.fn[pluginName].defaults, options, elementOptions); this.opts = opts; this.bindElement(opts); } JinPlace.prototype = { /** * Get the options that are set on the editable element with the data-* attributes. * * @param {jQuery} $el The element that is being made editable. */ elementOptions: function ($el) { var opts = {}; function upperToHyphenLower(match) { return '-' + match.toLowerCase(); } function make_attr_name(value) { return "data-" + value.replace(/[A-Z]/g, upperToHyphenLower); } $.each(option_list, function(index, value) { opts[value] = $el.attr(make_attr_name(value)); }); opts.elementId = $el.attr('id'); if (opts.textOnly) opts.textOnly = opts.textOnly !== 'false'; return opts; }, /** * Prepare the activator element to receive click events. * * This involves setting placeholder text if the element is empty. * * @param {Options} opts - The editor options. */ bindElement: function(opts) { // Remove any existing handler we set and bind to the activation click handler. opts.activator .off('click.jip') .on('click.jip', $.proxy(this.clickHandler, this)); // If there is no content, then we replace it with the empty indicator. var $el = this.element; if ($.trim($el.html()) == "") { $el.html(opts.placeholder); // In IE<9 the html is made uppercase which means it no longer matches what think the text is. // So we retrieve the html. opts.placeholder = $el.html(); } }, /** * Handle a click that is activating the element. This click can be on any element * so is not directly useful. Things are always set up so that 'this' is this object * and not the element that the click occurred on. * * @this {JinPlace} * @param ev The event. */ clickHandler: function(ev) { ev.preventDefault(); ev.stopPropagation(); // Turn off the activation handler, and disable any effect in case the activator // was a button that might submit. $(ev.currentTarget) .off('click.jip') .on('click.jip', function(ev) { ev.preventDefault(); }); var self = this, opts = self.opts; /** A new editor is created for every activation. So it is OK to keep instance * data on it. * @type {editorBase} */ var editor = $.extend({}, editorBase, $.fn[pluginName].editors[opts.type]); // Save original for use when cancelling. self.origValue = self.element.html(); self.fetchData(opts).done(function(data) { var field = editor.makeField(self.element, data); if (!editor.inputField) editor.inputField = field; field.addClass(opts.inputClass); var form = createForm(opts, field, editor.buttonsAllowed); // Add the form to the element to be edited self.element.html(form); // Now we can setup handlers and focus or otherwise activate the field. form .on("jip:submit submit", function(ev) { self.submit(editor, opts); return false; }) .on("jip:cancel", function(ev) { self.cancel(editor); return false; }) .on("keyup", function(ev) { if (ev.keyCode == 27) { self.cancel(editor); } }); editor.activate(form, field); // The action to take on blur can be set on the editor. If not, and there // are automatically added buttons, then the blur action is set according to // which ones exist. By default nothing happens on blur. var act = editor.blurAction || ( (!opts.okButton)? 'submit': (!opts.cancelButton)? 'jip:cancel': undefined); editor.blurEvent(field, form, act); }); }, /** * Fetch the data that will be placed into the editing control. The data is * obtained from the following sources in this order: * 1. data-data (or options.data) * 2. data-loadurl (or options.loadurl) a request is made to the given url and the * resulting data is used. * 3. The existing contents of 'element'. * * @param {Options} opts */ fetchData: function(opts) { var data; if (opts.data) { data = opts.data; } else if (opts.loadurl) { data = opts.loadFunction(opts); } else if (opts.textOnly) { data = $.trim(this.element.text()); } else { data = $.trim(this.element.html().replace(/&amp;/gi, '&')); } var placeholderFilter = function (data) { if (data == opts.placeholder) return ''; return data; }; var when = $.when(data); if (when.pipe) { return when.pipe(placeholderFilter); } else { return when.then(placeholderFilter); } }, /** * Throw away any edits and return the element to its original text. * * @param {editorBase} editor The element editor. * @return {void} */ cancel: function(editor) { var self = this; self.element.html(self.origValue); editor.finish(); // Rebind the element for the next time self.bindElement(self.opts); }, /** * Called to submit the changed data to the server. * * This method is always called with 'this' set to this object. * * @this {JinPlace} * @param {editorBase} editor * @param {Options} opts */ submit: function (editor, opts) { var self = this; var rval; var rejected = $.Deferred().reject(); // Since the function is user defined protect against exceptions and // returning nothing. Either problem causes the edit to be cancelled. // Of course it is possible that some action has been taken depending // on why the exception was thrown, but there is no way to know that. try { rval = opts.submitFunction.call(undefined, opts, editor.value()); if (rval === undefined) rval = rejected; } catch (e) { rval = rejected; } $.when(rval) .done(function(data, textStatus, jqxhr) { // If you have your own submitFunction, the arguments may have different meanings. self.element.trigger('jinplace:done', [data, textStatus, jqxhr]); self.onUpdate(editor, opts, data); }) .fail(function(jqxhr, textStatus, errorThrown) { // If you have your own submitFunction, the arguments may have different meanings. self.element.trigger('jinplace:fail', [jqxhr, textStatus, errorThrown]); self.cancel(editor); }) .always(function(a, textStatus, c) { // The meaning of the arguments depends on whether this is success or failure. self.element.trigger('jinplace:always', [a, textStatus, c]); }); }, /** * The server has received our data and replied successfully and the new data to * be displayed is available. * * @param {editorBase} editor The element editor. * @param {Options} opts The element options. * @param {string} data The data to display from the server. */ onUpdate: function(editor, opts, data) { var self = this; self.setContent(data); editor.finish(); self.bindElement(opts); }, /** * Set the content of the element. Called to update the value from the value * returned by the server. * * @param data The data to be displayed, it has been converted to the display format. */ setContent: function(data) { var element = this.element; if (!data) { element.html(this.opts.placeholder); return; } if (this.opts.textOnly) { element.text(data); } else { element.html(data); } } }; /** * Get the parameters that will be sent in the ajax call to the server. * Called for both the url and loadurl cases. * * @param {Options} opts The options from the element and config settings. * @param {*=} [value] The value of the control as returned by editor.value(). * @returns {object} */ var requestParams = function (opts, value) { var params = { "id": opts.elementId, "object": opts.object, attribute: opts.attribute }; if ($.isPlainObject(value)) { $.extend(params, value); } else if (value !== undefined) { params.value = value; } return params; }; // A really lightweight plugin wrapper around the constructor, // preventing against multiple instantiations $.fn[pluginName] = function (options) { return this.each(function () { if (!$.data(this, "plugin_" + pluginName)) { $.data(this, "plugin_" + pluginName, new JinPlace(this, options)); } }); }; /** These are the plugin defaults. You can override these if required. * @type {Options} */ $.fn[pluginName].defaults = { url: document.location.pathname, method: "post", type: "input", textOnly: true, placeholder: '[ --- ]', /** * @name Options.submitFunction * * The function to call when an editor form is submitted. This can be supplied as an * option to completely change the default action. * * @callback submitFunction * @param {Options} opts The options for this element. * @param {string} value The value that was submitted. * @returns {string|object} Returns a string which will be used to populate the element text or * a promise that will resolve to a string. */ submitFunction: function(opts, value) { return $.ajax(opts.url, { type: opts.method, data: requestParams(opts, value), dataType: 'text', // iOS 6 has a dreadful bug where POST requests are not sent to the // server if they are in the cache. headers: {'Cache-Control': 'no-cache'} // Apple! }); }, /** * @name Options.loadFunction * * @callback loadFunction * @param {Options} opts * @returns {string} */ loadFunction: function(opts) { return $.ajax(opts.loadurl, { data: requestParams(opts) }); } }; /** * Create a form for the editing area. The input element is added and if buttons * are required then they are added. Event handlers are set up. * * @param {Options} opts The options for this editor. * @param {jQuery} inputField The newly created input field. * @param {boolean} [buttons] True if buttons can be added. Whether buttons really are added * depends on the options and data-* attributes. * @returns {jQuery} The newly created form element. */ var createForm = function (opts, inputField, buttons) { var form = $("<form>") .attr("style", "display: inline;") .attr("action", "javascript:void(0);") .append(inputField); if (buttons) addButtons(form, opts); return form; }; /** * Add any requested buttons to the output. * * @param {jQuery} form The form that is being created. * @param {Options} opts The options set for this editor. */ var addButtons = function (form, opts) { var setHandler = function (button, action) { form.append(button); button.one('click', function(ev) { ev.stopPropagation(); form.trigger(action); }); }; var ok = opts.okButton; if (ok) { var $button = $("<input>").attr("type", "button").attr("value", ok); if (opts.okButtonClass) { $button.addClass(opts.okButtonClass); } else { $button.addClass('jip-button jip-ok-button'); } setHandler($button, 'submit'); } var cancel = opts.cancelButton; if (cancel) { $button = $("<input>").attr("type", "button").attr("value", cancel); if (opts.cancelButtonClass) { $button.addClass(opts.cancelButtonClass); } else { $button.addClass('jip-button jip-cancel-button'); } setHandler($button, 'jip:cancel'); } }; //noinspection UnnecessaryLocalVariableJS /** * This is the interface of an editor function. Plugins need only redefine the methods * or data that are appropriate. * @class */ var editorBase = { /** * Are we allowed to automatically add buttons to the form. Set this to * true for a text input where it might make sense. They are only added * if the user asks for them in any case. * * @name editorBase.buttonsAllowed, * @type {boolean} */ /** * The input field returned by makeField() will be saved as this.inputField unless * it is set within the makeField() method itself. * * @name editorBase.inputField * @type {jQuery} */ /** * Set up default blur handlers to cause the given action of 'submit' or 'cancel'. * If the default mechanism is not appropriate, then define this with the value 'ignore' * and no default processing will be provided and you must set it up yourself if you * want any action on blur. * * @name editorBase.blurAction * @type {string} */ /** * Make the editing field that will be added to the form. Editing field is * a general term; it could be a complex control or just a plain <input>. * * You may set this.inputField within the body of this method, if you do * not then it will be set to the value you return. * * @param {jQuery} element The original element that we are going to edit. * @param {string|Object} data The initial data that should be used to initialise the * field. For text inputs this will be just text, but for other types of * input it may be an object specific to that field. * @returns {jQuery} The new field wrapped in a jquery object. */ makeField: function (element, data) { // This is an implementation for <input type="text">. You would almost // always need to override this. return $("<input>") .attr("type", "text") .val(data); }, /** * Activate the field. It is now part of the document. * * Set up events as required. You should ensure that the events 'jip:submit' or * 'jip:cancel' are triggered on the form to submit the field or to cancel the * edit as appropriate. * * You can use 'submit' instead of 'jip:submit' to take advantage of standard * form processing. * * The default implementation is only useful for straight-forward text inputs. * * @param {jQuery} form The form your editor is contained in. If you want to avoid * events bubbling up, you can stop them here. * @param {jQuery} field The editing field. Passed as a convenience so we don't have * to save it. */ activate: function (form, field) { field.focus(); }, /** * The value of the editor. This is the value returned by the input field * or component that should be sent to the server. * * The default implementation just calls .val() on the inputField. * * @returns {string} The value that should be submitted to the server for this editor. */ value: function () { return this.inputField.val(); }, /** * This is not a method to be overridden. Used to set up blur event handlers * when you want the blur to be cancelled if there is a click on the control * or any of its components as will usually be the case. * * @param {jQuery} blurElement This is the element to set the blur handler on. * @param {jQuery} cancelElement These elements will cancel the blur action when clicked. * @param {string} action The action to take on blur. This will be 'submit' or 'jip:cancel'. * Can be set to 'ignore' to ensure that it is ignored and default values do not * get used. */ blurEvent: function (blurElement, cancelElement, action) { if (!action || action == 'ignore') return; var onBlur = function (ev) { var t = setTimeout(function () { blurElement.trigger(action); }, 300); // If a click occurs on these elements, then the blur is cancelled. cancelElement.on('click', function () { clearTimeout(t); }); }; // Set the handler to our wrapper. blurElement.on('blur', onBlur); }, /** * This is guaranteed to be called after editing is complete and before the element * is rebound. * * @type {function} * @return {void} */ finish: function() {} }; // The base implementation that can be extended. This is normally handled automatically. $.fn[pluginName].editorBase = editorBase; /** The field editors can be overridden or added to * * @type {Object.<string, editorBase>} */ $.fn[pluginName].editors = { /** * A regular text input field. All methods inherit from the base 'class'. */ input: { buttonsAllowed: true }, /* * A multi-line text area field. */ textarea: { buttonsAllowed: true, makeField: function (element, data) { return $("<textarea>") .css({ 'min-width': element.width(), 'min-height': element.height() }) .val(data); }, activate: function(form, field) { field.focus(); if (field.elastic) field.elastic(); } }, /* * A selection. This is slightly more complex as we have to pass in the possible * values so that one can be selected. */ select: { makeField: function (element, data) { var field = $("<select>"), choices = $.parseJSON(data); var selected = false; var elementChoice = null; $.each(choices, function(index, value) { var opt = $("<option>").val(value[0]).html(value[1]); if (value[2]) { opt.attr("selected", "1"); selected = true; } if (value[1] == element.text()) elementChoice = opt; field.append(opt); }); // If we didn't get any indication of the selected element from the // given data, then use the match we found with the element text. if (!selected && elementChoice) elementChoice.attr("selected", "1"); return field; }, activate: function(form, field) { field.focus(); field.on('change', function() { field.trigger('jip:submit'); }); } } }; })(jQuery, window, document);