/** @preserve Copyright © 2013, Itinken Limited. * MIT Licence */ /* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * See (http://jquery.com/). * @name jQuery * @class * See the jQuery Library (http://jquery.com/) for full details. This just * documents the function and classes that are added to jQuery by this plug-in. */ /** * See (http://jquery.com/) * @name fn * @class * See the jQuery Library (http://jquery.com/) for full details. This just * documents the function and classes that are added to jQuery by this plug-in. * @memberOf jQuery */ //noinspection JSUnnecessarySemicolon ; //noinspection JSUnusedLocalSymbols (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} 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 {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', 'data', 'loadurl', 'elementId', 'object', 'attribute', 'okButton', 'cancelButton', 'inputClass', 'activator', 'textOnly', 'placeholder', 'submitFunction' ]; /** * 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 { data = $.trim(this.element.html()); } 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) { self.onUpdate(editor, opts, data); }) .fail(function() { self.cancel(editor); }); }, /** * 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.element.trigger('jinplace:done', [data]); 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, type: "input", textOnly: 2, 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: "post", 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 = $("