');
+
+ var input = this._typeFn( 'create', opts );
+ if ( input !== null ) {
+ _editor_el('input-control', template).prepend( input );
+ }
+ else {
+ template.css('display', "none");
+ }
+
+ this.dom = $.extend( true, {}, Editor.Field.models.dom, {
+ container: template,
+ inputControl: _editor_el('input-control', template),
+ label: _editor_el('label', template),
+ fieldInfo: _editor_el('msg-info', template),
+ labelInfo: _editor_el('msg-label', template),
+ fieldError: _editor_el('msg-error', template),
+ fieldMessage: _editor_el('msg-message', template),
+ multi: _editor_el('multi-value', template),
+ multiReturn: _editor_el('msg-multi', template),
+ multiInfo: _editor_el('multi-info', template)
+ } );
+
+ // On click - set a common value for the field
+ this.dom.multi.on( 'click', function () {
+ that.val('');
+ } );
+
+ this.dom.multiReturn.on( 'click', function () {
+ that.s.multiValue = true;
+ that._multiValueCheck();
+ } );
+
+ // Field type extension methods - add a method to the field for the public
+ // methods that each field type defines beyond the default ones that already
+ // exist as part of this instance
+ $.each( this.s.type, function ( name, fn ) {
+ if ( typeof fn === 'function' && that[name] === undefined ) {
+ that[ name ] = function () {
+ var args = Array.prototype.slice.call( arguments );
+
+ args.unshift( name );
+ var ret = that._typeFn.apply( that, args );
+
+ // Return the given value if there is one, or the field instance
+ // for chaining if there is no value
+ return ret === undefined ?
+ that :
+ ret;
+ };
+ }
+ } );
+};
+
+
+Editor.Field.prototype = {
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Public
+ */
+ def: function ( set ) {
+ var opts = this.s.opts;
+
+ if ( set === undefined ) {
+ // Backwards compat
+ var def = opts['default'] !== undefined ?
+ opts['default'] :
+ opts.def;
+
+ return $.isFunction( def ) ?
+ def() :
+ def;
+ }
+
+ opts.def = set;
+ return this;
+ },
+
+ disable: function () {
+ this._typeFn( 'disable' );
+ return this;
+ },
+
+ displayed: function () {
+ var container = this.dom.container;
+
+ return container.parents('body').length && container.css('display') != 'none' ?
+ true :
+ false;
+ },
+
+ enable: function () {
+ this._typeFn( 'enable' );
+ return this;
+ },
+
+ error: function ( msg, fn ) {
+ var classes = this.s.classes;
+
+ // Add or remove the error class
+ if ( msg ) {
+ this.dom.container.addClass( classes.error );
+ }
+ else {
+ this.dom.container.removeClass( classes.error );
+ }
+
+ return this._msg( this.dom.fieldError, msg, fn );
+ },
+
+ isMultiValue: function () {
+ return this.s.multiValue;
+ },
+
+ inError: function () {
+ return this.dom.container.hasClass( this.s.classes.error );
+ },
+
+ input: function () {
+ return this.s.type.input ?
+ this._typeFn( 'input' ) :
+ $('input, select, textarea', this.dom.container);
+ },
+
+ focus: function () {
+ if ( this.s.type.focus ) {
+ this._typeFn( 'focus' );
+ }
+ else {
+ $('input, select, textarea', this.dom.container).focus();
+ }
+
+ return this;
+ },
+
+ get: function () {
+ // When multi-value a single get is undefined
+ if ( this.isMultiValue() ) {
+ return undefined;
+ }
+
+ var val = this._typeFn( 'get' );
+ return val !== undefined ?
+ val :
+ this.def();
+ },
+
+ hide: function ( animate ) {
+ var el = this.dom.container;
+
+ if ( animate === undefined ) {
+ animate = true;
+ }
+
+ if ( this.s.host.display() && animate ) {
+ el.slideUp();
+ }
+ else {
+ el.css( 'display', 'none' );
+ }
+ return this;
+ },
+
+ label: function ( str ) {
+ var label = this.dom.label;
+
+ if ( str === undefined ) {
+ return label.html();
+ }
+
+ label.html( str );
+ return this;
+ },
+
+ message: function ( msg, fn ) {
+ return this._msg( this.dom.fieldMessage, msg, fn );
+ },
+
+ // There is no `multiVal()` as its arguments could be ambiguous
+ // id is an idSrc value _only_
+ multiGet: function ( id ) {
+ var value;
+ var multiValues = this.s.multiValues;
+ var multiIds = this.s.multiIds;
+
+ if ( id === undefined ) {
+ // Get an object with the values for each item being edited
+ value = {};
+
+ for ( var i=0 ; i')
+ .replace(/</g, '<')
+ .replace(/&/g, '&')
+ .replace(/"/g, '"')
+ .replace(/'/g, '\'')
+ .replace(/
/g, '\n');
+ };
+
+ this.s.multiValue = false;
+
+ var decode = this.s.opts.entityDecode;
+ if ( decode === undefined || decode === true ) {
+ if ( $.isArray( val ) ) {
+ for ( var i=0, ien=val.length ; i 0 && val !== last ) {
+ different = true;
+ break;
+ }
+
+ last = val;
+ }
+ }
+
+ if ( different && this.s.multiValue ) {
+ // Different values
+ this.dom.inputControl.css( { display: 'none' } );
+ this.dom.multi.css( { display: 'block' } );
+ }
+ else {
+ // All the same value
+ this.dom.inputControl.css( { display: 'block' } );
+ this.dom.multi.css( { display: 'none' } );
+
+ if ( this.s.multiValue ) {
+ this.val( last );
+ }
+ }
+
+ this.dom.multiReturn.css( {
+ display: ids && ids.length > 1 && different && ! this.s.multiValue ?
+ 'block' :
+ 'none'
+ } );
+
+ this.s.host._multiInfo();
+
+ return true;
+ },
+
+ _typeFn: function ( name /*, ... */ ) {
+ // Remove the name from the arguments list, so the rest can be passed
+ // straight into the field type
+ var args = Array.prototype.slice.call( arguments );
+ args.shift();
+
+ // Insert the options as the first parameter - all field type methods
+ // take the field's configuration object as the first parameter
+ args.unshift( this.s.opts );
+
+ var fn = this.s.type[ name ];
+ if ( fn ) {
+ return fn.apply( this.s.host, args );
+ }
+ }
+};
+
+
+Editor.Field.models = {};
+
+
+/**
+ * Initialisation options that can be given to Editor.Field at initialisation
+ * time.
+ * @namespace
+ */
+Editor.Field.defaults = {
+ /**
+ * Class name to assign to the field's container element (in addition to the other
+ * classes that Editor assigns by default).
+ * @type string
+ * @default Empty string
+ */
+ "className": "",
+
+ /**
+ * The data property (`mData` in DataTables terminology) that is used to
+ * read from and write to the table. If not given then it will take the same
+ * value as the `name` that is given in the field object. Note that `data`
+ * can be given as null, which will result in Editor not using a DataTables
+ * row property for the value of the field for either getting or setting
+ * data.
+ *
+ * In previous versions of Editor (1.2-) this was called `dataProp`. The old
+ * name can still be used for backwards compatibility, but the new form is
+ * preferred.
+ * @type string
+ * @default Empty string
+ */
+ "data": "",
+
+ /**
+ * The default value for the field. Used when creating new rows (editing will
+ * use the currently set value). If given as a function the function will be
+ * executed and the returned value used as the default
+ *
+ * In Editor 1.2 and earlier this field was called `default` - however
+ * `default` is a reserved word in Javascript, so it couldn't be used
+ * unquoted. `default` will still work with Editor 1.3, but the new property
+ * name of `def` is preferred.
+ * @type string|function
+ * @default Empty string
+ */
+ "def": "",
+
+ /**
+ * Helpful information text about the field that is shown below the input control.
+ * @type string
+ * @default Empty string
+ */
+ "fieldInfo": "",
+
+ /**
+ * The ID of the field. This is used by the `label` HTML tag as the "for" attribute
+ * improved accessibility. Although this using this parameter is not mandatory,
+ * it is a good idea to assign the ID to the DOM element that is the input for the
+ * field (if this is applicable).
+ * @type string
+ * @default Calculated
+ */
+ "id": "",
+
+ /**
+ * The label to display for the field input (i.e. the name that is visually
+ * assigned to the field).
+ * @type string
+ * @default Empty string
+ */
+ "label": "",
+
+ /**
+ * Helpful information text about the field that is shown below the field label.
+ * @type string
+ * @default Empty string
+ */
+ "labelInfo": "",
+
+ /**
+ * The name for the field that is submitted to the server. This is the only
+ * mandatory parameter in the field description object.
+ * @type string
+ * @default null
+ */
+ "name": null,
+
+ /**
+ * The input control that is presented to the end user. The options available
+ * are defined by {@link Editor.fieldTypes} and any extensions made
+ * to that object.
+ * @type string
+ * @default text
+ */
+ "type": "text"
+};
+
+
+
+/**
+ *
+ * @namespace
+ */
+Editor.Field.models.settings = {
+ type: null,
+ name: null,
+ classes: null,
+ opts: null,
+ host: null
+};
+
+
+
+/**
+ *
+ * @namespace
+ */
+Editor.Field.models.dom = {
+ container: null,
+ label: null,
+ labelInfo: null,
+ fieldInfo: null,
+ fieldError: null,
+ fieldMessage: null
+};
+
+
+/*
+ * Models
+ */
+
+/**
+ * Object models container, for the various models that DataTables has available
+ * to it. These models define the objects that are used to hold the active state
+ * and configuration of the table.
+ * @namespace
+ */
+Editor.models = {};
+
+
+/**
+ * Editor makes very few assumptions about how its form will actually be
+ * displayed to the end user (where in the DOM, interaction etc), instead
+ * focusing on providing form interaction controls only. To actually display
+ * a form in the browser we need to use a display controller, and then select
+ * which one we want to use at initialisation time using the `display`
+ * option. For example a display controller could display the form in a
+ * lightbox (as the default display controller does), it could completely
+ * empty the document and put only the form in place, ir could work with
+ * DataTables to use `fnOpen` / `fnClose` to show the form in a "details" row
+ * and so on.
+ *
+ * Editor has two built-in display controllers ('lightbox' and 'envelope'),
+ * but others can readily be created and installed for use as plug-ins. When
+ * creating a display controller plug-in you **must** implement the methods
+ * in this control. Additionally when closing the display internally you
+ * **must** trigger a `requestClose` event which Editor will listen
+ * for and act upon (this allows Editor to ask the user if they are sure
+ * they want to close the form, for example).
+ * @namespace
+ */
+Editor.models.displayController = {
+ /**
+ * Initialisation method, called by Editor when itself, initialises.
+ * @param {object} dte The DataTables Editor instance that has requested
+ * the action - this allows access to the Editor API if required.
+ * @returns {object} The object that Editor will use to run the 'open'
+ * and 'close' methods against. If static methods are used then
+ * just return the object that holds the init, open and close methods,
+ * however, this allows the display to be created with a 'new'
+ * instance of an object is the display controller calls for that.
+ * @type function
+ */
+ "init": function ( dte ) {},
+
+ /**
+ * Display the form (add it to the visual display in the document)
+ * @param {object} dte The DataTables Editor instance that has requested
+ * the action - this allows access to the Editor API if required.
+ * @param {element} append The DOM node that contains the form to be
+ * displayed
+ * @param {function} [fn] Callback function that is to be executed when
+ * the form has been displayed. Note that this parameter is optional.
+ */
+ "open": function ( dte, append, fn ) {},
+
+ /**
+ * Hide the form (remove it form the visual display in the document)
+ * @param {object} dte The DataTables Editor instance that has requested
+ * the action - this allows access to the Editor API if required.
+ * @param {function} [fn] Callback function that is to be executed when
+ * the form has been hidden. Note that this parameter is optional.
+ */
+ "close": function ( dte, fn ) {}
+};
+
+
+
+/**
+ * Model object for input types which are available to fields (assigned to
+ * {@link Editor.fieldTypes}). Any plug-ins which add additional
+ * input types to Editor **must** implement the methods in this object
+ * (dummy functions are given in the model so they can be used as defaults
+ * if extending this object).
+ *
+ * All functions in the model are executed in the Editor's instance scope,
+ * so you have full access to the settings object and the API methods if
+ * required.
+ * @namespace
+ * @example
+ * // Add a simple text input (the 'text' type that is built into Editor
+ * // does this, so you wouldn't implement this exactly as show, but it
+ * // it is a good example.
+ *
+ * var Editor = $.fn.Editor;
+ *
+ * Editor.fieldTypes.myInput = $.extend( true, {}, Editor.models.type, {
+ * "create": function ( conf ) {
+ * // We store the 'input' element in the configuration object so
+ * // we can easily access it again in future.
+ * conf._input = document.createElement('input');
+ * conf._input.id = conf.id;
+ * return conf._input;
+ * },
+ *
+ * "get": function ( conf ) {
+ * return conf._input.value;
+ * },
+ *
+ * "set": function ( conf, val ) {
+ * conf._input.value = val;
+ * },
+ *
+ * "enable": function ( conf ) {
+ * conf._input.disabled = false;
+ * },
+ *
+ * "disable": function ( conf ) {
+ * conf._input.disabled = true;
+ * }
+ * } );
+ */
+Editor.models.fieldType = {
+ /**
+ * Create the field - this is called when the field is added to the form.
+ * Note that this is called at initialisation time, or when the
+ * {@link Editor#add} API method is called, not when the form is displayed.
+ * If you need to know when the form is shown, you can use the API to listen
+ * for the `open` event.
+ * @param {object} conf The configuration object for the field in question:
+ * {@link Editor.models.field}.
+ * @returns {element|null} The input element (or a wrapping element if a more
+ * complex input is required) or null if nothing is to be added to the
+ * DOM for this input type.
+ * @type function
+ */
+ "create": function ( conf ) {},
+
+ /**
+ * Get the value from the field
+ * @param {object} conf The configuration object for the field in question:
+ * {@link Editor.models.field}.
+ * @returns {*} The value from the field - the exact value will depend on the
+ * formatting required by the input type control.
+ * @type function
+ */
+ "get": function ( conf ) {},
+
+ /**
+ * Set the value for a field
+ * @param {object} conf The configuration object for the field in question:
+ * {@link Editor.models.field}.
+ * @param {*} val The value to set the field to - the exact value will
+ * depend on the formatting required by the input type control.
+ * @type function
+ */
+ "set": function ( conf, val ) {},
+
+ /**
+ * Enable the field - i.e. allow user interface
+ * @param {object} conf The configuration object for the field in question:
+ * {@link Editor.models.field}.
+ * @type function
+ */
+ "enable": function ( conf ) {},
+
+ /**
+ * Disable the field - i.e. disallow user interface
+ * @param {object} conf The configuration object for the field in question:
+ * {@link Editor.models.field}.
+ * @type function
+ */
+ "disable": function ( conf ) {}
+};
+
+
+
+/**
+ * Settings object for Editor - this provides the state for each instance of
+ * Editor and can be accessed through the instance's `s` property. Note that the
+ * settings object is considered to be "private" and thus is liable to change
+ * between versions. As such if you do read any of the setting parameters,
+ * please keep this in mind when upgrading!
+ * @namespace
+ */
+Editor.models.settings = {
+ /**
+ * URL to submit Ajax data to.
+ * This is directly set by the initialisation parameter / default of the same name.
+ * @type string
+ * @default null
+ */
+ "ajaxUrl": null,
+
+ /**
+ * Ajax submit function.
+ * This is directly set by the initialisation parameter / default of the same name.
+ * @type function
+ * @default null
+ */
+ "ajax": null,
+
+ /**
+ * Data source for get and set data actions. This allows Editor to perform
+ * as an Editor for virtually any data source simply by defining additional
+ * data sources.
+ * @type object
+ * @default null
+ */
+ "dataSource": null,
+
+ /**
+ * DataTable selector, can be anything that the Api supports
+ * This is directly set by the initialisation parameter / default of the same name.
+ * @type string
+ * @default null
+ */
+ "domTable": null,
+
+ /**
+ * The initialisation object that was given by the user - stored for future reference.
+ * This is directly set by the initialisation parameter / default of the same name.
+ * @type string
+ * @default null
+ */
+ "opts": null,
+
+ /**
+ * The display controller object for the Form.
+ * This is directly set by the initialisation parameter / default of the same name.
+ * @type string
+ * @default null
+ */
+ "displayController": null,
+
+ /**
+ * The form fields - see {@link Editor.models.field} for details of the
+ * objects held in this array.
+ * @type object
+ * @default null
+ */
+ "fields": {},
+
+ /**
+ * Field order - order that the fields will appear in on the form. Array of strings,
+ * the names of the fields.
+ * @type array
+ * @default null
+ */
+ "order": [],
+
+ /**
+ * The ID of the row being edited (set to -1 on create and remove actions)
+ * @type string
+ * @default null
+ */
+ "id": -1,
+
+ /**
+ * Flag to indicate if the form is currently displayed (true) or not (false)
+ * @type string
+ * @default null
+ */
+ "displayed": false,
+
+ /**
+ * Flag to indicate if the form is current in a processing state (true) or not (false)
+ * @type string
+ * @default null
+ */
+ "processing": false,
+
+ /**
+ * Developer provided identifier for the elements to be edited (i.e. at
+ * `dt-type row-selector` to select rows to edit or delete.
+ * @type array
+ * @default null
+ */
+ "modifier": null,
+
+ /**
+ * The current form action - 'create', 'edit' or 'remove'. If no current action then
+ * it is set to null.
+ * @type string
+ * @default null
+ */
+ "action": null,
+
+ /**
+ * JSON property from which to read / write the row's ID property.
+ * @type string
+ * @default null
+ */
+ "idSrc": null
+};
+
+
+
+/**
+ * Model of the buttons that can be used with the {@link Editor#buttons}
+ * method for creating and displaying buttons (also the {@link Editor#button}
+ * argument option for the {@link Editor#create}, {@link Editor#edit} and
+ * {@link Editor#remove} methods). Although you don't need to extend this object,
+ * it is available for reference to show the options available.
+ * @namespace
+ */
+Editor.models.button = {
+ /**
+ * The text to put into the button. This can be any HTML string you wish as
+ * it will be rendered as HTML (allowing images etc to be shown inside the
+ * button).
+ * @type string
+ * @default null
+ */
+ "label": null,
+
+ /**
+ * Callback function which the button is activated. For example for a 'submit'
+ * button you would call the {@link Editor#submit} API method, while for a cancel button
+ * you would call the {@link Editor#close} API method. Note that the function is executed
+ * in the scope of the Editor instance, so you can call the Editor's API methods
+ * using the `this` keyword.
+ * @type function
+ * @default null
+ */
+ "fn": null,
+
+ /**
+ * The CSS class(es) to apply to the button which can be useful for styling buttons
+ * which preform different functions each with a distinctive visual appearance.
+ * @type string
+ * @default null
+ */
+ "className": null
+};
+
+
+
+/**
+ * This is really an internal namespace
+ *
+ * @namespace
+ */
+Editor.models.formOptions = {
+ /**
+ * Action to take when the return key is pressed when focused in a form
+ * element. Cam be `submit` or `none`. Could also be `blur` or `close`, but
+ * why would you ever want that. Replaces `submitOnReturn` from 1.4.
+ *
+ * @type string
+ */
+ onReturn: 'submit',
+
+ /**
+ * Action to take on blur. Can be `close`, `submit` or `none`. Replaces
+ * `submitOnBlur` from 1.4
+ *
+ * @type string
+ */
+ onBlur: 'close',
+
+ /**
+ * Action to take when the lightbox background is clicked - can be `close`,
+ * `submit`, `blur` or `none`. Replaces `blurOnBackground` from 1.4
+ *
+ * @type string
+ */
+ onBackground: 'blur',
+
+ /**
+ * Close for at the end of the Ajax request. Can be `close` or `none`.
+ * Replaces `closeOnComplete` from 1.4.
+ *
+ * @type string
+ */
+ onComplete: 'close',
+
+ /**
+ * Action to take when the `esc` key is pressed when focused in the form -
+ * can be `close`, `submit`, `blur` or `none`
+ *
+ * @type string
+ */
+ onEsc: 'close',
+
+ /**
+ * Data to submit to the server when submitting a form. If an option is
+ * selected that results in no data being submitted, the Ajax request will
+ * not be made Can be `all`, `changed` or `allIfChanged`. This effects the
+ * edit action only.
+ *
+ * @type string
+ */
+ submit: 'all',
+
+ /**
+ * Field identifier to focus on
+ *
+ * @type null|integer|string
+ */
+ focus: 0,
+
+ /**
+ * Buttons to show in the form
+ *
+ * @type string|boolean|array|object
+ */
+ buttons: true,
+
+ /**
+ * Form title
+ *
+ * @type string|boolean
+ */
+ title: true,
+
+ /**
+ * Form message
+ *
+ * @type string|boolean
+ */
+ message: true,
+
+ /**
+ * DataTables redraw option
+ *
+ * @type string|boolean
+ */
+ drawType: false
+};
+
+
+/*
+ * Display controllers
+ */
+
+/**
+ * Display controllers. See {@link Editor.models.displayController} for
+ * full information about the display controller options for Editor. The display
+ * controllers given in this object can be utilised by specifying the
+ * {@link Editor.defaults.display} option.
+ * @namespace
+ */
+Editor.display = {};
+
+
+(function(window, document, $, DataTable) {
+
+
+var self;
+
+Editor.display.lightbox = $.extend( true, {}, Editor.models.displayController, {
+ /*
+ * API methods
+ */
+ "init": function ( dte ) {
+ self._init();
+ return self;
+ },
+
+ "open": function ( dte, append, callback ) {
+ if ( self._shown ) {
+ if ( callback ) {
+ callback();
+ }
+ return;
+ }
+
+ self._dte = dte;
+
+ var content = self._dom.content;
+ content.children().detach();
+ content
+ .append( append )
+ .append( self._dom.close );
+
+ self._shown = true;
+ self._show( callback );
+ },
+
+ "close": function ( dte, callback ) {
+ if ( !self._shown ) {
+ if ( callback ) {
+ callback();
+ }
+ return;
+ }
+
+ self._dte = dte;
+ self._hide( callback );
+
+ self._shown = false;
+ },
+
+ node: function ( dte ) {
+ return self._dom.wrapper[0];
+ },
+
+
+ /*
+ * Private methods
+ */
+ "_init": function () {
+ if ( self._ready ) {
+ return;
+ }
+
+ var dom = self._dom;
+ dom.content = $('div.DTED_Lightbox_Content', self._dom.wrapper);
+
+ dom.wrapper.css( 'opacity', 0 );
+ dom.background.css( 'opacity', 0 );
+ },
+
+
+ "_show": function ( callback ) {
+ var that = this;
+ var dom = self._dom;
+
+ // Mobiles have very poor position fixed abilities, so we need to know
+ // when using mobile A media query isn't good enough
+ if ( window.orientation !== undefined ) {
+ $('body').addClass( 'DTED_Lightbox_Mobile' );
+ }
+
+ // Adjust size for the content
+ dom.content.css( 'height', 'auto' );
+ dom.wrapper.css( {
+ top: -self.conf.offsetAni
+ } );
+
+ $('body')
+ .append( self._dom.background )
+ .append( self._dom.wrapper );
+
+ self._heightCalc();
+
+ dom.wrapper
+ .stop()
+ .animate( {
+ opacity: 1,
+ top: 0
+ }, callback );
+
+ dom.background
+ .stop()
+ .animate( {
+ opacity: 1
+ } );
+
+ // Event handlers - assign on show (and unbind on hide) rather than init
+ // since we might need to refer to different editor instances - 12563
+ dom.close.bind( 'click.DTED_Lightbox', function (e) {
+ self._dte.close();
+ } );
+
+ dom.background.bind( 'click.DTED_Lightbox', function (e) {
+ self._dte.background();
+ } );
+
+ $('div.DTED_Lightbox_Content_Wrapper', dom.wrapper).bind( 'click.DTED_Lightbox', function (e) {
+ if ( $(e.target).hasClass('DTED_Lightbox_Content_Wrapper') ) {
+ self._dte.background();
+ }
+ } );
+
+ $(window).bind( 'resize.DTED_Lightbox', function () {
+ self._heightCalc();
+ } );
+
+ self._scrollTop = $('body').scrollTop();
+
+ // For smaller screens we need to hide the other elements in the
+ // document since iOS and Android both mess up display:fixed when
+ // the virtual keyboard is shown
+ if ( window.orientation !== undefined ) {
+ var kids = $('body').children().not( dom.background ).not( dom.wrapper );
+ $('body').append( '' );
+ $('div.DTED_Lightbox_Shown').append( kids );
+ }
+ },
+
+
+ "_heightCalc": function () {
+ // Set the max-height for the form content
+ var dom = self._dom;
+ var maxHeight = $(window).height() - (self.conf.windowPadding*2) -
+ $('div.DTE_Header', dom.wrapper).outerHeight() -
+ $('div.DTE_Footer', dom.wrapper).outerHeight();
+
+ $('div.DTE_Body_Content', dom.wrapper).css(
+ 'maxHeight',
+ maxHeight
+ );
+ },
+
+
+ "_hide": function ( callback ) {
+ var dom = self._dom;
+
+ if ( !callback ) {
+ callback = function () {};
+ }
+
+ if ( window.orientation !== undefined ) {
+ var show = $('div.DTED_Lightbox_Shown');
+ show.children().appendTo('body');
+ show.remove();
+ }
+
+ // Restore scroll state
+ $('body')
+ .removeClass( 'DTED_Lightbox_Mobile' )
+ .scrollTop( self._scrollTop );
+
+ dom.wrapper
+ .stop()
+ .animate( {
+ opacity: 0,
+ top: self.conf.offsetAni
+ }, function () {
+ $(this).detach();
+ callback();
+ } );
+
+ dom.background
+ .stop()
+ .animate( {
+ opacity: 0
+ }, function () {
+ $(this).detach();
+ } );
+
+ // Event handlers
+ dom.close.unbind( 'click.DTED_Lightbox' );
+ dom.background.unbind( 'click.DTED_Lightbox' );
+ $('div.DTED_Lightbox_Content_Wrapper', dom.wrapper).unbind( 'click.DTED_Lightbox' );
+ $(window).unbind( 'resize.DTED_Lightbox' );
+ },
+
+
+ /*
+ * Private properties
+ */
+ "_dte": null,
+ "_ready": false,
+ "_shown": false,
+ "_dom": {
+ "wrapper": $(
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'
+ ),
+
+ "background": $(
+ '
'
+ ),
+
+ "close": $(
+ ''
+ ),
+
+ "content": null
+ }
+} );
+
+self = Editor.display.lightbox;
+
+self.conf = {
+ "offsetAni": 25,
+ "windowPadding": 25
+};
+
+
+}(window, document, jQuery, jQuery.fn.dataTable));
+
+
+
+(function(window, document, $, DataTable) {
+
+
+var self;
+
+Editor.display.envelope = $.extend( true, {}, Editor.models.displayController, {
+ /*
+ * API methods
+ */
+ "init": function ( dte ) {
+ self._dte = dte;
+ self._init();
+ return self;
+ },
+
+
+ "open": function ( dte, append, callback ) {
+ self._dte = dte;
+ $(self._dom.content).children().detach();
+ self._dom.content.appendChild( append );
+ self._dom.content.appendChild( self._dom.close );
+
+ self._show( callback );
+ },
+
+
+ "close": function ( dte, callback ) {
+ self._dte = dte;
+ self._hide( callback );
+ },
+
+ node: function ( dte ) {
+ return self._dom.wrapper[0];
+ },
+
+
+ /*
+ * Private methods
+ */
+ "_init": function () {
+ if ( self._ready ) {
+ return;
+ }
+
+ self._dom.content = $('div.DTED_Envelope_Container', self._dom.wrapper)[0];
+
+ document.body.appendChild( self._dom.background );
+ document.body.appendChild( self._dom.wrapper );
+
+ // For IE6-8 we need to make it a block element to read the opacity...
+ self._dom.background.style.visbility = 'hidden';
+ self._dom.background.style.display = 'block';
+ self._cssBackgroundOpacity = $(self._dom.background).css('opacity');
+ self._dom.background.style.display = 'none';
+ self._dom.background.style.visbility = 'visible';
+ },
+
+
+ "_show": function ( callback ) {
+ var that = this;
+ var formHeight;
+
+ if ( !callback ) {
+ callback = function () {};
+ }
+
+ // Adjust size for the content
+ self._dom.content.style.height = 'auto';
+
+ var style = self._dom.wrapper.style;
+ style.opacity = 0;
+ style.display = 'block';
+
+ var targetRow = self._findAttachRow();
+ var height = self._heightCalc();
+ var width = targetRow.offsetWidth;
+
+ style.display = 'none';
+ style.opacity = 1;
+
+ // Prep the display
+ self._dom.wrapper.style.width = width+"px";
+ self._dom.wrapper.style.marginLeft = -(width/2)+"px";
+ self._dom.wrapper.style.top = ($(targetRow).offset().top + targetRow.offsetHeight)+"px";
+ self._dom.content.style.top = ((-1 * height) - 20)+"px";
+
+ // Start animating in the background
+ self._dom.background.style.opacity = 0;
+ self._dom.background.style.display = 'block';
+ $(self._dom.background).animate( {
+ 'opacity': self._cssBackgroundOpacity
+ }, 'normal' );
+
+ // Animate in the display
+ $(self._dom.wrapper).fadeIn();
+
+ // Slide the slider down to 'open' the view
+ if ( self.conf.windowScroll ) {
+ // Scroll the window so we can see the editor first
+ $('html,body').animate( {
+ "scrollTop": $(targetRow).offset().top + targetRow.offsetHeight - self.conf.windowPadding
+ }, function () {
+ // Now open the editor
+ $(self._dom.content).animate( {
+ "top": 0
+ }, 600, callback );
+ } );
+ }
+ else {
+ // Just open the editor without moving the document position
+ $(self._dom.content).animate( {
+ "top": 0
+ }, 600, callback );
+ }
+
+ // Event handlers
+ $(self._dom.close).bind( 'click.DTED_Envelope', function (e) {
+ self._dte.close();
+ } );
+
+ $(self._dom.background).bind( 'click.DTED_Envelope', function (e) {
+ self._dte.background();
+ } );
+
+ $('div.DTED_Lightbox_Content_Wrapper', self._dom.wrapper).bind( 'click.DTED_Envelope', function (e) {
+ if ( $(e.target).hasClass('DTED_Envelope_Content_Wrapper') ) {
+ self._dte.background();
+ }
+ } );
+
+ $(window).bind( 'resize.DTED_Envelope', function () {
+ self._heightCalc();
+ } );
+ },
+
+
+ "_heightCalc": function () {
+ var formHeight;
+
+ formHeight = self.conf.heightCalc ?
+ self.conf.heightCalc( self._dom.wrapper ) :
+ $(self._dom.content).children().height();
+
+ // Set the max-height for the form content
+ var maxHeight = $(window).height() - (self.conf.windowPadding*2) -
+ $('div.DTE_Header', self._dom.wrapper).outerHeight() -
+ $('div.DTE_Footer', self._dom.wrapper).outerHeight();
+
+ $('div.DTE_Body_Content', self._dom.wrapper).css('maxHeight', maxHeight);
+
+ return $(self._dte.dom.wrapper).outerHeight();
+ },
+
+
+ "_hide": function ( callback ) {
+ if ( !callback ) {
+ callback = function () {};
+ }
+
+ $(self._dom.content).animate( {
+ "top": -(self._dom.content.offsetHeight+50)
+ }, 600, function () {
+ $([self._dom.wrapper, self._dom.background]).fadeOut( 'normal', callback );
+ } );
+
+ // Event handlers
+ $(self._dom.close).unbind( 'click.DTED_Lightbox' );
+ $(self._dom.background).unbind( 'click.DTED_Lightbox' );
+ $('div.DTED_Lightbox_Content_Wrapper', self._dom.wrapper).unbind( 'click.DTED_Lightbox' );
+ $(window).unbind( 'resize.DTED_Lightbox' );
+ },
+
+
+ "_findAttachRow": function () {
+ var dt = $(self._dte.s.table).DataTable();
+
+ // Figure out where we want to put the form display
+ if ( self.conf.attach === 'head' ) {
+ return dt.table().header();
+ }
+ else if ( self._dte.s.action === 'create' ) {
+ return dt.table().header();
+ }
+ else {
+ return dt.row( self._dte.s.modifier ).node();
+ }
+ },
+
+
+ /*
+ * Private properties
+ */
+ "_dte": null,
+ "_ready": false,
+ "_cssBackgroundOpacity": 1, // read from the CSS dynamically, but stored for future reference
+
+
+ "_dom": {
+ "wrapper": $(
+ '
'+
+ ''+
+ ''+
+ ''+
+ '
'
+ )[0],
+
+ "background": $(
+ '
'
+ )[0],
+
+ "close": $(
+ '
×
'
+ )[0],
+
+ "content": null
+ }
+} );
+
+
+// Assign to 'self' for easy referencing of our own object!
+self = Editor.display.envelope;
+
+
+// Configuration object - can be accessed globally using
+// $.fn.Editor.display.envelope.conf (!)
+self.conf = {
+ "windowPadding": 50,
+ "heightCalc": null,
+ "attach": "row",
+ "windowScroll": true
+};
+
+
+}(window, document, jQuery, jQuery.fn.dataTable));
+
+
+/*
+ * Prototype includes
+ */
+
+
+/**
+ * Add a new field to the from. This is the method that is called automatically when
+ * fields are given in the initialisation objects as {@link Editor.defaults.fields}.
+ * @memberOf Editor
+ * @param {object|array} field The object that describes the field (the full
+ * object is described by {@link Editor.model.field}. Note that multiple
+ * fields can be given by passing in an array of field definitions.
+ * @param {string} [after] Existing field to insert the new field after. This
+ * can be `undefined` (insert at end), `null` (insert at start) or `string`
+ * the field name to insert after.
+ */
+Editor.prototype.add = function ( cfg, after )
+{
+ // Allow multiple fields to be added at the same time
+ if ( $.isArray( cfg ) ) {
+ for ( var i=0, iLen=cfg.length ; i
' );
+ var container = $(
+ '
'+
+ '
'+
+ '
'+
+ ''+
+ '
'+
+ '
'+
+ ''+
+ '
'
+ );
+
+ if ( show ) {
+ container.appendTo( 'body' );
+ background.appendTo( 'body' );
+ }
+
+ var liner = container.children().eq(0);
+ var table = liner.children();
+ var close = table.children();
+ liner.append( this.dom.formError );
+ table.prepend( this.dom.form );
+
+ if ( opts.message ) {
+ liner.prepend( this.dom.formInfo );
+ }
+
+ if ( opts.title ) {
+ liner.prepend( this.dom.header );
+ }
+
+ if ( opts.buttons ) {
+ table.append( this.dom.buttons );
+ }
+
+ var pair = $().add( container ).add( background );
+ this._closeReg( function ( submitComplete ) {
+ pair.animate(
+ { opacity: 0 },
+ function () {
+ pair.detach();
+
+ $(window).off( 'resize.'+namespace );
+
+ // Clear error messages "offline"
+ that._clearDynamicInfo();
+ }
+ );
+ } );
+
+ // Close event handlers
+ background.click( function () {
+ that.blur();
+ } );
+
+ close.click( function () {
+ that._close();
+ } );
+
+ this.bubblePosition();
+
+ pair.animate( { opacity: 1 } );
+
+ this._focus( this.s.includeFields, opts.focus );
+ this._postopen( 'bubble' );
+
+ return this;
+};
+
+
+/**
+ * Reposition the editing bubble (`bubble()`) when it is visible. This can be
+ * used to update the bubble position if other elements on the page change
+ * position. Editor will automatically call this method on window resize.
+ *
+ * @return {Editor} Editor instance, for chaining
+ */
+Editor.prototype.bubblePosition = function ()
+{
+ var
+ wrapper = $('div.DTE_Bubble'),
+ liner = $('div.DTE_Bubble_Liner'),
+ nodes = this.s.bubbleNodes;
+
+ // Average the node positions to insert the container
+ var position = { top: 0, left: 0, right: 0, bottom: 0 };
+
+ $.each( nodes, function (i, node) {
+ var pos = $(node).offset();
+
+ position.top += pos.top;
+ position.left += pos.left;
+ position.right += pos.left + node.offsetWidth;
+ position.bottom += pos.top + node.offsetHeight;
+ } );
+
+ position.top /= nodes.length;
+ position.left /= nodes.length;
+ position.right /= nodes.length;
+ position.bottom /= nodes.length;
+
+ var
+ top = position.top,
+ left = (position.left + position.right) / 2,
+ width = liner.outerWidth(),
+ visLeft = left - (width / 2),
+ visRight = visLeft + width,
+ docWidth = $(window).width(),
+ padding = 15,
+ classes = this.classes.bubble;
+
+ wrapper.css( {
+ top: top,
+ left: left
+ } );
+
+ // Correct for overflow from the top of the document by positioning below
+ // the field if needed
+ if ( liner.length && liner.offset().top < 0 ) {
+ wrapper
+ .css( 'top', position.bottom )
+ .addClass( 'below' );
+ }
+ else {
+ wrapper.removeClass( 'below' );
+ }
+
+ // Attempt to correct for overflow to the right of the document
+ if ( visRight+padding > docWidth ) {
+ var diff = visRight - docWidth;
+
+ // If left overflowing, that takes priority
+ liner.css( 'left', visLeft < padding ?
+ -(visLeft-padding) :
+ -(diff+padding)
+ );
+ }
+ else {
+ // Correct overflow to the left
+ liner.css( 'left', visLeft < padding ? -(visLeft-padding) : 0 );
+ }
+
+ return this;
+};
+
+
+/**
+ * Setup the buttons that will be shown in the footer of the form - calling this
+ * method will replace any buttons which are currently shown in the form.
+ * @param {array|object} buttons A single button definition to add to the form or
+ * an array of objects with the button definitions to add more than one button.
+ * The options for the button definitions are fully defined by the
+ * {@link Editor.models.button} object.
+ * @param {string} buttons.label The text to put into the button. This can be any
+ * HTML string you wish as it will be rendered as HTML (allowing images etc to
+ * be shown inside the button).
+ * @param {function} [buttons.fn] Callback function which the button is activated.
+ * For example for a 'submit' button you would call the {@link Editor#submit} method,
+ * while for a cancel button you would call the {@link Editor#close} method. Note that
+ * the function is executed in the scope of the Editor instance, so you can call
+ * the Editor's API methods using the `this` keyword.
+ * @param {string} [buttons.className] The CSS class(es) to apply to the button
+ * which can be useful for styling buttons which preform different functions
+ * each with a distinctive visual appearance.
+ * @return {Editor} Editor instance, for chaining
+ */
+Editor.prototype.buttons = function ( buttons )
+{
+ var that = this;
+
+ if ( buttons === '_basic' ) {
+ // Special string to create a basic button - undocumented
+ buttons = [ {
+ label: this.i18n[ this.s.action ].submit,
+ fn: function () { this.submit(); }
+ } ];
+ }
+ else if ( ! $.isArray( buttons ) ) {
+ // Allow a single button to be passed in as an object with an array
+ buttons = [ buttons ];
+ }
+
+ $(this.dom.buttons).empty();
+
+ $.each( buttons, function ( i, btn ) {
+ if ( typeof btn === 'string' ) {
+ btn = {
+ label: btn,
+ fn: function () { this.submit(); }
+ };
+ }
+
+ $( '', {
+ 'class': that.classes.form.button+(btn.className ? ' '+btn.className : '')
+ } )
+ .html( typeof btn.label === 'function' ?
+ btn.label( that ) :
+ btn.label || ''
+ )
+ .attr( 'tabindex', 0 )
+ .on( 'keyup', function (e) {
+ if ( e.keyCode === 13 && btn.fn ) {
+ btn.fn.call( that );
+ }
+ } )
+ .on( 'keypress', function (e) {
+ // Stop the browser activating the click event - if we don't
+ // have this and the Ajax return is fast, the keyup in
+ // `_formOptions()` might trigger another submit
+ if ( e.keyCode === 13 ) {
+ e.preventDefault();
+ }
+ } )
+ .on( 'click', function (e) {
+ e.preventDefault();
+
+ if ( btn.fn ) {
+ btn.fn.call( that );
+ }
+ } )
+ .appendTo( that.dom.buttons );
+ } );
+
+ return this;
+};
+
+
+/**
+ * Remove fields from the form (fields are those that have been added using the
+ * {@link Editor#add} method or the `fields` initialisation option). A single,
+ * multiple or all fields can be removed at a time based on the passed parameter.
+ * Fields are identified by the `name` property that was given to each field
+ * when added to the form.
+ * @param {string|array} [fieldName] Field or fields to remove from the form. If
+ * not given then all fields are removed from the form. If given as a string
+ * then the single matching field will be removed. If given as an array of
+ * strings, then all matching fields will be removed.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Clear the form of current fields and then add a new field
+ * // before displaying a 'create' display
+ * editor.clear();
+ * editor.add( {
+ * "label": "User name",
+ * "name": "username"
+ * } );
+ * editor.create( "Create user" );
+ *
+ * @example
+ * // Remove an individual field
+ * editor.clear( "username" );
+ *
+ * @example
+ * // Remove multiple fields
+ * editor.clear( [ "first_name", "last_name" ] );
+ */
+Editor.prototype.clear = function ( fieldName )
+{
+ var that = this;
+ var fields = this.s.fields;
+
+ if ( typeof fieldName === 'string' ) {
+ // Remove an individual form element
+ fields[ fieldName ].destroy();
+ delete fields[ fieldName ];
+
+ var orderIdx = $.inArray( fieldName, this.s.order );
+ this.s.order.splice( orderIdx, 1 );
+ }
+ else {
+ $.each( this._fieldNames( fieldName ), function (i, name) {
+ that.clear( name );
+ } );
+ }
+
+ return this;
+};
+
+
+/**
+ * Close the form display.
+ *
+ * Note that `close()` will close any of the three Editor form types (main,
+ * bubble and inline).
+ *
+ * @return {Editor} Editor instance, for chaining
+ */
+Editor.prototype.close = function ()
+{
+ this._close( false );
+
+ return this;
+};
+
+
+/**
+ * Create a new record - show the form that allows the user to enter information
+ * for a new row and then subsequently submit that data.
+ * @param {boolean} [show=true] Show the form or not.
+ *
+ * @example
+ * // Show the create form with a submit button
+ * editor
+ * .title( 'Add new record' )
+ * .buttons( {
+ * "label": "Save",
+ * "fn": function () {
+ * this.submit();
+ * }
+ * } )
+ * .create();
+ *
+ * @example
+ * // Don't show the form and automatically submit it after programatically
+ * // setting the values of fields (and using the field defaults)
+ * editor
+ * create()
+ * set( 'name', 'Test user' )
+ * set( 'access', 'Read only' )
+ * submit();
+ */
+Editor.prototype.create = function ( arg1, arg2, arg3, arg4 )
+{
+ var that = this;
+ var fields = this.s.fields;
+ var count = 1;
+
+ // Some other field in inline edit mode?
+ if ( this._tidy( function () { that.create( arg1, arg2, arg3, arg4 ); } ) ) {
+ return this;
+ }
+
+ // Multi-row creation support (only supported by the 1.3+ style of calling
+ // this method, so a max of three arguments
+ if ( typeof arg1 === 'number' ) {
+ count = arg1;
+ arg1 = arg2;
+ arg2 = arg3;
+ }
+
+ // Set up the edit fields for submission
+ this.s.editFields = {};
+ for ( var i=0 ; i 0 ) {
+ throw 'Cannot edit more than one row inline at a time';
+ }
+
+ node = $(editField.attach[0]);
+
+ // Only a single item in that row
+ countInner = 0;
+ $.each( editField.displayFields, function ( j, f ) {
+ if ( countInner > 0 ) {
+ throw 'Cannot edit more than one field inline at a time';
+ }
+
+ field = f;
+ countInner++;
+ } );
+
+ countOuter++;
+
+ // If only changed values are to be submitted, then only allow the
+ // individual field that we are editing to be edited.
+ // This is currently disabled, as I'm not convinced that it is actually
+ // useful!
+ // if ( opts.submit === 'changed' ) {
+ // editField.fields = editField.displayFields;
+ // }
+ } );
+
+ // Already in edit mode for this cell?
+ if ( $('div.DTE_Field', node).length ) {
+ return this;
+ }
+
+ // Some other field in inline edit mode?
+ if ( this._tidy( function () { that.inline( cell, fieldName, opts ); } ) ) {
+ return this;
+ }
+
+ // Start a full row edit, but don't display - we will be showing the field
+ this._edit( cell, editFields, 'inline' );
+ var namespace = this._formOptions( opts );
+
+ var ret = this._preopen( 'inline' );
+ if ( ! ret ) {
+ return this;
+ }
+
+ // Remove from DOM, keeping event handlers, and include text nodes in remove
+ var children = node.contents().detach();
+
+ node.append( $(
+ '
'+
+ ''+
+ ''+
+ '
'
+ ) );
+
+ node.find('div.DTE_Inline_Field').append( field.node() );
+
+ if ( opts.buttons ) {
+ // Use prepend for the CSS, so we can float the buttons right
+ node.find('div.DTE_Inline_Buttons').append( this.dom.buttons );
+ }
+
+ this._closeReg( function ( submitComplete ) {
+ // Mark that this specific inline edit has closed
+ closed = true;
+
+ $(document).off( 'click'+namespace );
+
+ // If there was no submit, we need to put the DOM back as it was. If
+ // there was a submit, the write of the new value will set the DOM to
+ // how it should be
+ if ( ! submitComplete ) {
+ node.contents().detach();
+ node.append( children );
+ }
+
+ // Clear error messages "offline"
+ that._clearDynamicInfo();
+ } );
+
+ // Submit and blur actions
+ setTimeout( function () {
+ // If already closed, possibly due to some other aspect of the event
+ // that triggered the inline call, don't add the event listener - it
+ // isn't needed (and is dangerous)
+ if ( closed ) {
+ return;
+ }
+
+ $(document).on( 'click'+namespace, function ( e ) {
+ // Was the click inside or owned by the editing node? If not, then
+ // come out of editing mode.
+
+ // andSelf is deprecated in jQ1.8, but we want 1.7 compat
+ var back = $.fn.addBack ? 'addBack' : 'andSelf';
+
+ if ( ! field._typeFn( 'owns', e.target ) &&
+ $.inArray( node[0], $(e.target).parents()[ back ]() ) === -1 )
+ {
+ that.blur();
+ }
+ } );
+ }, 0 );
+
+ this._focus( [ field ], opts.focus );
+ this._postopen( 'inline' );
+
+ return this;
+};
+
+
+/**
+ * Show an information message for the form as a whole, or for an individual
+ * field. This can be used to provide helpful information to a user about an
+ * individual field, or more typically the form (for example when deleting
+ * a record and asking for confirmation).
+ * @param {string} [name] The name of the field to show the message for. If not
+ * given then a global message is shown for the form
+ * @param {string|function} msg The message to show
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Show a global message for a 'create' form
+ * editor.message( 'Add a new user to the database by completing the fields below' );
+ * editor.create( 'Add new user', {
+ * "label": "Submit",
+ * "fn": function () { this.submit(); }
+ * } );
+ *
+ * @example
+ * // Show a message for an individual field when a 'help' icon is clicked on
+ * $('#user_help').click( function () {
+ * editor.message( 'user', 'The user name is what the system user will login with' );
+ * } );
+ */
+Editor.prototype.message = function ( name, msg )
+{
+ if ( msg === undefined ) {
+ // Global message
+ this._message( this.dom.formInfo, name );
+ }
+ else {
+ // Field message
+ this.s.fields[ name ].message( msg );
+ }
+
+ return this;
+};
+
+
+/**
+ * Get which mode of operation the Editor form is in
+ * @return {string} `create`, `edit`, `remove` or `null` if no active state.
+ */
+Editor.prototype.mode = function ()
+{
+ return this.s.action;
+};
+
+
+/**
+ * Get the modifier that was used to trigger the edit or delete action.
+ * @return {*} The identifier that was used for the editing / remove method
+ * called.
+ */
+Editor.prototype.modifier = function ()
+{
+ return this.s.modifier;
+};
+
+
+/**
+ * Get the values from one or more fields, taking into account multiple data
+ * points being edited at the same time.
+ *
+ * @param {string|array} fieldNames A single field name or an array of field
+ * names.
+ * @return {object} If a string is given as the first parameter an object that
+ * contains the value for each row being edited is returned. If an array is
+ * given, then the object has the field names as the parameter name and the
+ * value is the value object with values for each row being edited.
+ */
+Editor.prototype.multiGet = function ( fieldNames )
+{
+ var fields = this.s.fields;
+
+ if ( fieldNames === undefined ) {
+ fieldNames = this.fields();
+ }
+
+ if ( $.isArray( fieldNames ) ) {
+ var out = {};
+
+ $.each( fieldNames, function ( i, name ) {
+ out[ name ] = fields[ name ].multiGet();
+ } );
+
+ return out;
+ }
+
+ return fields[ fieldNames ].multiGet();
+};
+
+
+/**
+ * Set the values for one or more fields, taking into account multiple data
+ * points being edited at the same time.
+ *
+ * @param {object|string} fieldNames The name of the field to set, or an object
+ * with the field names as the parameters that contains the value object to
+ * set for each field.
+ * @param {*} [val] Value to set if first parameter is given as a string.
+ * Otherwise it is ignored.
+ * @return {Editor} Editor instance, for chaining
+ */
+Editor.prototype.multiSet = function ( fieldNames, val )
+{
+ var fields = this.s.fields;
+
+ if ( $.isPlainObject( fieldNames ) && val === undefined ) {
+ $.each( fieldNames, function ( name, value ) {
+ fields[ name ].multiSet( value );
+ } );
+ }
+ else {
+ fields[ fieldNames ].multiSet( val );
+ }
+
+ return this;
+};
+
+
+/**
+ * Get the container node for an individual field.
+ * @param {string|array} name The field name (from the `name` parameter given
+ * when originally setting up the field) to get the DOM node for.
+ * @return {node|array} Field container node
+ *
+ * @example
+ * // Dynamically add a class to a field's container
+ * $(editor.node( 'account_type' )).addClass( 'account' );
+ */
+Editor.prototype.node = function ( name )
+{
+ var fields = this.s.fields;
+
+ if ( ! name ) {
+ name = this.order();
+ }
+
+ return $.isArray( name ) ?
+ $.map( name, function (n) {
+ return fields[ n ].node();
+ } ) :
+ fields[ name ].node();
+};
+
+
+/**
+ * Remove a bound event listener to the editor instance. This method provides a
+ * shorthand way of binding jQuery events that would be the same as writing
+ * `$(editor).off(...)` for convenience.
+ * @param {string} name Event name to remove the listeners for - event names are
+ * defined by {@link Editor}.
+ * @param {function} [fn] The function to remove. If not given, all functions which
+ * are assigned to the given event name will be removed.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Add an event to alert when the form is shown and then remove the listener
+ * // so it will only fire once
+ * editor.on( 'open', function () {
+ * alert('Form displayed!');
+ * editor.off( 'open' );
+ * } );
+ */
+Editor.prototype.off = function ( name, fn )
+{
+ $(this).off( this._eventName( name ), fn );
+
+ return this;
+};
+
+
+/**
+ * Listen for an event which is fired off by Editor when it performs certain
+ * actions. This method provides a shorthand way of binding jQuery events that
+ * would be the same as writing `$(editor).on(...)` for convenience.
+ * @param {string} name Event name to add the listener for - event names are
+ * defined by {@link Editor}.
+ * @param {function} fn The function to run when the event is triggered.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Log events on the console when they occur
+ * editor.on( 'open', function () { console.log( 'Form opened' ); } );
+ * editor.on( 'close', function () { console.log( 'Form closed' ); } );
+ * editor.on( 'submit', function () { console.log( 'Form submitted' ); } );
+ */
+Editor.prototype.on = function ( name, fn )
+{
+ $(this).on( this._eventName( name ), fn );
+
+ return this;
+};
+
+
+/**
+ * Listen for a single event event which is fired off by Editor when it performs
+ * certain actions. This method provides a shorthand way of binding jQuery
+ * events that would be the same as writing `$(editor).one(...)` for
+ * convenience.
+ * @param {string} name Event name to add the listener for - event names are
+ * defined by {@link Editor}.
+ * @param {function} fn The function to run when the event is triggered.
+ * @return {Editor} Editor instance, for chaining
+ */
+Editor.prototype.one = function ( name, fn )
+{
+ $(this).one( this._eventName( name ), fn );
+
+ return this;
+};
+
+
+/**
+ * Display the main form editor to the end user in the web-browser.
+ *
+ * Note that the `close()` method will close any of the three Editor form types
+ * (main, bubble and inline), but this method will open only the main type.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Build a 'create' form, but don't display it until some values have
+ * // been set. When done, then display the form.
+ * editor.create( 'Create user', {
+ * "label": "Submit",
+ * "fn": function () { this.submit(); }
+ * }, false );
+ * editor.set( 'name', 'Test user' );
+ * editor.set( 'access', 'Read only' );
+ * editor.open();
+ */
+Editor.prototype.open = function ()
+{
+ var that = this;
+
+ // Insert the display elements in order
+ this._displayReorder();
+
+ // Define how to do a close
+ this._closeReg( function ( submitComplete ) {
+ that.s.displayController.close( that, function () {
+ that._clearDynamicInfo();
+ } );
+ } );
+
+ // Run the standard open with common events
+ var ret = this._preopen( 'main' );
+ if ( ! ret ) {
+ return this;
+ }
+
+ this.s.displayController.open( this, this.dom.wrapper );
+ this._focus(
+ $.map( this.s.order, function (name) {
+ return that.s.fields[ name ];
+ } ),
+ this.s.editOpts.focus
+ );
+ this._postopen( 'main' );
+
+ return this;
+};
+
+
+/**
+ * Get or set the ordering of fields, as they are displayed in the form. When used as
+ * a getter, the field names are returned in an array, in their current order, and when
+ * used as a setting you can alter the field ordering by passing in an array with all
+ * field names in their new order.
+ *
+ * Note that all fields *must* be included when reordering, and no additional fields can
+ * be added here (use {@link Editor#add} to add more fields). Finally, for setting the
+ * order, you can pass an array of the field names, or give the field names as individual
+ * parameters (see examples below).
+ * @param {array|string} [set] Field order to set.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Get field ordering
+ * var order = editor.order();
+ *
+ * @example
+ * // Set the field order
+ * var order = editor.order();
+ * order.unshift( order.pop() ); // move the last field into the first position
+ * editor.order( order );
+ *
+ * @example
+ * // Set the field order as arguments
+ * editor.order( "pupil", "grade", "dept", "exam-board" );
+ *
+ */
+Editor.prototype.order = function ( set /*, ... */ )
+{
+ if ( !set ) {
+ return this.s.order;
+ }
+
+ // Allow new layout to be passed in as arguments
+ if ( arguments.length && ! $.isArray( set ) ) {
+ set = Array.prototype.slice.call(arguments);
+ }
+
+ // Sanity check - array must exactly match the fields we have available
+ if ( this.s.order.slice().sort().join('-') !== set.slice().sort().join('-') ) {
+ throw "All fields, and no additional fields, must be provided for ordering.";
+ }
+
+ // Copy the new array into the order (so the reference is maintained)
+ $.extend( this.s.order, set );
+
+ this._displayReorder();
+
+ return this;
+};
+
+
+/**
+ * Remove (delete) entries from the table. The rows to remove are given as
+ * either a single DOM node or an array of DOM nodes (including a jQuery
+ * object).
+ * @param {node|array} items The row, or array of nodes, to delete
+ * @param {boolean} [show=true] Show the form or not.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Delete a given row with a message to let the user know exactly what is
+ * // happening
+ * editor.message( "Are you sure you want to remove this row?" );
+ * editor.remove( row_to_delete, 'Delete row', {
+ * "label": "Confirm",
+ * "fn": function () { this.submit(); }
+ * } );
+ *
+ * @example
+ * // Delete the first row in a table without asking the user for confirmation
+ * editor.remove( '', $('#example tbody tr:eq(0)')[0], null, false );
+ * editor.submit();
+ *
+ * @example
+ * // Delete all rows in a table with a submit button
+ * editor.remove( $('#example tbody tr'), 'Delete all rows', {
+ * "label": "Delete all",
+ * "fn": function () { this.submit(); }
+ * } );
+ */
+Editor.prototype.remove = function ( items, arg1, arg2, arg3, arg4 )
+{
+ var that = this;
+
+ // Some other field in inline edit mode?
+ if ( this._tidy( function () { that.remove( items, arg1, arg2, arg3, arg4 ); } ) ) {
+ return this;
+ }
+
+ // Allow a single row node to be passed in to remove, Can't use $.isArray
+ // as we also allow array like objects to be passed in (API, jQuery)
+ if ( items.length === undefined ) {
+ items = [ items ];
+ }
+
+ var argOpts = this._crudArgs( arg1, arg2, arg3, arg4 );
+ var editFields = this._dataSource( 'fields', items );
+
+ this.s.action = "remove";
+ this.s.modifier = items;
+ this.s.editFields = editFields;
+ this.dom.form.style.display = 'none';
+
+ this._actionClass();
+
+ this._event( 'initRemove', [
+ _pluck( editFields, 'node' ),
+ _pluck( editFields, 'data' ),
+ items
+ ] );
+
+ this._event( 'initMultiRemove', [
+ editFields,
+ items
+ ] );
+
+ this._assembleMain();
+ this._formOptions( argOpts.opts );
+
+ argOpts.maybeOpen();
+
+ var opts = this.s.editOpts;
+ if ( opts.focus !== null ) {
+ $('button', this.dom.buttons).eq( opts.focus ).focus();
+ }
+
+ return this;
+};
+
+
+/**
+ * Set the value of a field
+ * @param {string|object} name The field name (from the `name` parameter given
+ * when originally setting up the field) to set the value of. If given as an
+ * object the object parameter name will be the value of the field to set and
+ * the value the value to set for the field.
+ * @param {*} [val] The value to set the field to. The format of the value will
+ * depend upon the field type. Not required if the first parameter is given
+ * as an object.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Set the values of a few fields before then automatically submitting the form
+ * editor.create( null, null, false );
+ * editor.set( 'name', 'Test user' );
+ * editor.set( 'access', 'Read only' );
+ * editor.submit();
+ */
+Editor.prototype.set = function ( set, val )
+{
+ var fields = this.s.fields;
+
+ if ( ! $.isPlainObject( set ) ) {
+ var o = {};
+ o[ set ] = val;
+ set = o;
+ }
+
+ $.each( set, function (n, v) {
+ fields[ n ].set( v );
+ } );
+
+ return this;
+};
+
+
+/**
+ * Show a field in the display that was previously hidden.
+ * @param {string|array} [names] The field name (from the `name` parameter
+ * given when originally setting up the field) to make visible, or an array of
+ * field names to make visible. If not given all fields are shown.
+ * @param {boolean} [animate=true] Animate if visible
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Shuffle the fields that are visible, hiding one field and making two
+ * // others visible before then showing the {@link Editor#create} record form.
+ * editor.hide( 'username' );
+ * editor.show( 'account_type' );
+ * editor.show( 'access_level' );
+ * editor.create( 'Add new user', {
+ * "label": "Save",
+ * "fn": function () { this.submit(); }
+ * } );
+ *
+ * @example
+ * // Show all fields
+ * editor.show();
+ */
+Editor.prototype.show = function ( names, animate )
+{
+ var fields = this.s.fields;
+
+ $.each( this._fieldNames( names ), function (i, n) {
+ fields[ n ].show( animate );
+ } );
+
+ return this;
+};
+
+
+/**
+ * Submit a form to the server for processing. The exact action performed will depend
+ * on which of the methods {@link Editor#create}, {@link Editor#edit} or
+ * {@link Editor#remove} were called to prepare the form - regardless of which one is
+ * used, you call this method to submit data.
+ * @param {function} [successCallback] Callback function that is executed once the
+ * form has been successfully submitted to the server and no errors occurred.
+ * @param {function} [errorCallback] Callback function that is executed if the
+ * server reports an error due to the submission (this includes a JSON formatting
+ * error should the error return invalid JSON).
+ * @param {function} [formatdata] Callback function that is passed in the data
+ * that will be submitted to the server, allowing pre-formatting of the data,
+ * removal of data or adding of extra fields.
+ * @param {boolean} [hide=true] When the form is successfully submitted, by default
+ * the form display will be hidden - this option allows that to be overridden.
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Submit data from a form button
+ * editor.create( 'Add new record', {
+ * "label": "Save",
+ * "fn": function () {
+ * this.submit();
+ * }
+ * } );
+ *
+ * @example
+ * // Submit without showing the user the form
+ * editor.create( null, null, false );
+ * editor.submit();
+ *
+ * @example
+ * // Provide success and error callback methods
+ * editor.create( 'Add new record', {
+ * "label": "Save",
+ * "fn": function () {
+ * this.submit( function () {
+ * alert( 'Form successfully submitted!' );
+ * }, function () {
+ * alert( 'Form encountered an error :-(' );
+ * }
+ * );
+ * }
+ * } );
+ *
+ * @example
+ * // Add an extra field to the data
+ * editor.create( 'Add new record', {
+ * "label": "Save",
+ * "fn": function () {
+ * this.submit( null, null, function (data) {
+ * data.extra = "Extra information";
+ * } );
+ * }
+ * } );
+ *
+ * @example
+ * // Don't hide the form immediately - change the title and then close the form
+ * // after a small amount of time
+ * editor.create( 'Add new record', {
+ * "label": "Save",
+ * "fn": function () {
+ * this.submit(
+ * function () {
+ * var that = this;
+ * this.title( 'Data successfully added!' );
+ * setTimeout( function () {
+ * that.close();
+ * }, 1000 );
+ * },
+ * null,
+ * null,
+ * false
+ * );
+ * }
+ * } );
+ *
+ */
+Editor.prototype.submit = function ( successCallback, errorCallback, formatdata, hide )
+{
+ var
+ that = this,
+ fields = this.s.fields,
+ errorFields = [],
+ errorReady = 0,
+ sent = false;
+
+ if ( this.s.processing || ! this.s.action ) {
+ return this;
+ }
+ this._processing( true );
+
+ // If there are fields in error, we want to wait for the error notification
+ // to be cleared before the form is submitted - errorFields tracks the
+ // fields which are in the error state, while errorReady tracks those which
+ // are ready to submit
+ var send = function () {
+ if ( errorFields.length !== errorReady || sent ) {
+ return;
+ }
+
+ sent = true;
+ that._submit( successCallback, errorCallback, formatdata, hide );
+ };
+
+ // Remove the global error (don't know if the form is still in an error
+ // state!)
+ this.error();
+
+ // Count how many fields are in error
+ $.each( fields, function ( name, field ) {
+ if ( field.inError() ) {
+ errorFields.push( name );
+ }
+ } );
+
+ // Remove the error display
+ $.each( errorFields, function ( i, name ) {
+ fields[ name ].error('', function () {
+ errorReady++;
+ send();
+ } );
+ } );
+
+ send();
+
+ return this;
+};
+
+
+/**
+ * Set the title of the form
+ * @param {string|function} title The title to give to the form
+ * @return {Editor} Editor instance, for chaining
+ *
+ * @example
+ * // Create an edit display used the title, buttons and edit methods (note that
+ * // this is just an example, typically you would use the parameters of the edit
+ * // method to achieve this.
+ * editor.title( 'Edit record' );
+ * editor.buttons( {
+ * "label": "Update",
+ * "fn": function () { this.submit(); }
+ * } );
+ * editor.edit( TR_to_edit );
+ *
+ * @example
+ * // Show a create form, with a timer for the duration that the form is open
+ * editor.create( 'Add new record - time on form: 0s', {
+ * "label": "Save",
+ * "fn": function () { this.submit(); }
+ * } );
+ *
+ * // Add an event to the editor to stop the timer when the display is removed
+ * var runTimer = true;
+ * var timer = 0;
+ * editor.on( 'close', function () {
+ * runTimer = false;
+ * editor.off( 'close' );
+ * } );
+ * // Start the timer running
+ * updateTitle();
+ *
+ * // Local function to update the title once per second
+ * function updateTitle() {
+ * editor.title( 'Add new record - time on form: '+timer+'s' );
+ * timer++;
+ * if ( runTimer ) {
+ * setTimeout( function() {
+ * updateTitle();
+ * }, 1000 );
+ * }
+ * }
+ */
+Editor.prototype.title = function ( title )
+{
+ var header = $(this.dom.header).children( 'div.'+this.classes.header.content );
+
+ if ( title === undefined ) {
+ return header.html();
+ }
+
+ if ( typeof title === 'function' ) {
+ title = title( this, new DataTable.Api(this.s.table) );
+ }
+
+ header.html( title );
+
+ return this;
+};
+
+
+/**
+ * Get or set the value of a specific field, or get the value of all fields in
+ * the form.
+ *
+ * @param {string|array} [names] The field name(s) to get or set the value of.
+ * If not given, then the value of all fields will be obtained.
+ * @param {*} [value] Value to set
+ * @return {Editor|object|*} Editor instance, for chaining if used as a setter,
+ * an object containing the values of the requested fields if used as a
+ * getter with multiple fields requested, or the value of the requested field
+ * if a single field is requested.
+ */
+Editor.prototype.val = function ( field, value )
+{
+ if ( value === undefined ) {
+ return this.get( field ); // field can be undefined to get all
+ }
+
+ return this.set( field, value );
+};
+
+
+/*
+ * DataTables 1.10 API integration. Provides the ability to control basic Editor
+ * aspects from the DataTables API. Full control does of course require use of
+ * the Editor API though.
+ */
+var apiRegister = DataTable.Api.register;
+
+
+function __getInst( api ) {
+ var ctx = api.context[0];
+ return ctx.oInit.editor || ctx._editor;
+}
+
+// Set sensible defaults for the editing options
+function __setBasic( inst, opts, type, plural ) {
+ if ( ! opts ) {
+ opts = {};
+ }
+
+ if ( opts.buttons === undefined ) {
+ opts.buttons = '_basic';
+ }
+
+ if ( opts.title === undefined ) {
+ opts.title = inst.i18n[ type ].title;
+ }
+
+ if ( opts.message === undefined ) {
+ if ( type === 'remove' ) {
+ var confirm = inst.i18n[ type ].confirm;
+ opts.message = plural!==1 ? confirm._.replace(/%d/, plural) : confirm['1'];
+ }
+ else {
+ opts.message = '';
+ }
+ }
+
+ return opts;
+}
+
+
+apiRegister( 'editor()', function () {
+ return __getInst( this );
+} );
+
+// Row editing
+apiRegister( 'row.create()', function ( opts ) {
+ // main
+ var inst = __getInst( this );
+ inst.create( __setBasic( inst, opts, 'create' ) );
+ return this;
+} );
+
+apiRegister( 'row().edit()', function ( opts ) {
+ // main
+ var inst = __getInst( this );
+ inst.edit( this[0][0], __setBasic( inst, opts, 'edit' ) );
+ return this;
+} );
+
+apiRegister( 'rows().edit()', function ( opts ) {
+ // main
+ var inst = __getInst( this );
+ inst.edit( this[0], __setBasic( inst, opts, 'edit' ) );
+ return this;
+} );
+
+apiRegister( 'row().delete()', function ( opts ) {
+ // main
+ var inst = __getInst( this );
+ inst.remove( this[0][0], __setBasic( inst, opts, 'remove', 1 ) );
+ return this;
+} );
+
+apiRegister( 'rows().delete()', function ( opts ) {
+ // main
+ var inst = __getInst( this );
+ inst.remove( this[0], __setBasic( inst, opts, 'remove', this[0].length ) );
+ return this;
+} );
+
+apiRegister( 'cell().edit()', function ( type, opts ) {
+ // inline or bubble
+ if ( ! type ) {
+ type = 'inline';
+ }
+ else if ( $.isPlainObject( type ) ) {
+ opts = type;
+ type = 'inline';
+ }
+
+ __getInst( this )[ type ]( this[0][0], opts );
+ return this;
+} );
+
+apiRegister( 'cells().edit()', function ( opts ) {
+ // bubble only at the moment
+ __getInst( this ).bubble( this[0], opts );
+ return this;
+} );
+
+apiRegister( 'file()', function ( name, id ) {
+ return Editor.files[ name ][ id ];
+} );
+
+apiRegister( 'files()', function ( name, value ) {
+ if ( ! name ) {
+ return Editor.files;
+ }
+
+ if ( ! value ) {
+ return Editor.files[ name ];
+ }
+
+ // The setter option of this method is not publicly documented
+ Editor.files[ name ] = value;
+
+ return this;
+} );
+
+// Global listener for file information updates via DataTables' Ajax JSON
+$(document).on( 'xhr.dt', function (e, ctx, json) {
+ if ( e.namespace !== 'dt' ) {
+ return;
+ }
+
+ if ( json && json.files ) {
+ $.each( json.files, function ( name, files ) {
+ Editor.files[ name ] = files;
+ } );
+ }
+} );
+
+
+/**
+ * Common error message emitter. This method is not (yet) publicly documented on
+ * the Editor site. It might be in future.
+ *
+ * @param {string} msg Error message
+ * @param {int} tn Tech note link
+ */
+Editor.error = function ( msg, tn )
+{
+ throw tn ?
+ msg +' For more information, please refer to https://datatables.net/tn/'+tn :
+ msg;
+};
+
+
+/**
+ * Obtain label / value pairs of data from a data source, be it an array or
+ * object, for use in an input that requires label / value pairs such as
+ * `select`, `radio` and `checkbox` inputs.
+ *
+ * A callback function is triggered for each label / value pair found, so the
+ * caller can add it to the input as required.
+ *
+ * @static
+ * @param {object|array} An object or array of data to iterate over getting the
+ * label / value pairs.
+ * @param {object} props When an array of objects is passed in as the data
+ * source by default the label will be read from the `label` property and
+ * the value from the `value` property of the object. This option can alter
+ * that behaviour.
+ * @param {function} fn Callback function. Takes three parameters: the label,
+ * the value and the iterator index.
+ */
+Editor.pairs = function ( data, props, fn )
+{
+ var i, ien, dataPoint;
+
+ // Define default properties to read the data from if using an object.
+ // The passed in `props` object and override.
+ props = $.extend( {
+ label: 'label',
+ value: 'value'
+ }, props );
+
+ if ( $.isArray( data ) ) {
+ // As an array, we iterate each item which can be an object or value
+ for ( i=0, ien=data.length ; iUploading file" );
+
+ reader.onload = function ( e ) {
+ var data = new FormData();
+ var ajax;
+
+ data.append( 'action', 'upload' );
+ data.append( 'uploadField', conf.name );
+ data.append( 'upload', files[ counter ] );
+
+ if ( conf.ajaxData ) {
+ conf.ajaxData( data );
+ }
+
+ if ( conf.ajax ) {
+ ajax = conf.ajax;
+ }
+ else if ( typeof editor.s.ajax === 'string' || $.isPlainObject( editor.s.ajax ) ) {
+ ajax = editor.s.ajax;
+ }
+
+ if ( ! ajax ) {
+ throw 'No Ajax option specified for upload plug-in';
+ }
+
+ if ( typeof ajax === 'string' ) {
+ ajax = { url: ajax };
+ }
+
+ // Use preSubmit to stop form submission during an upload, since the
+ // value won't be known until that point.
+ var submit = false;
+ editor
+ .on( 'preSubmit.DTE_Upload', function () {
+ submit = true;
+ return false;
+ } );
+
+ $.ajax( $.extend( {}, ajax, {
+ type: 'post',
+ data: data,
+ dataType: 'json',
+ contentType: false,
+ processData: false,
+ xhr: function () {
+ var xhr = $.ajaxSettings.xhr();
+
+ if ( xhr.upload ) {
+ xhr.upload.onprogress = function ( e ) {
+ if ( e.lengthComputable ) {
+ var percent = (e.loaded/e.total*100).toFixed(0)+"%";
+
+ progressCallback( conf, files.length === 1 ?
+ percent :
+ counter+':'+files.length+' '+percent
+ );
+ }
+ };
+ xhr.upload.onloadend = function ( e ) {
+ progressCallback( conf );
+ };
+ }
+
+ return xhr;
+ },
+ success: function ( json ) {
+ editor.off( 'preSubmit.DTE_Upload' );
+
+ if ( json.fieldErrors && json.fieldErrors.length ) {
+ var errors = json.fieldErrors;
+
+ for ( var i=0, ien=errors.length ; i'+
+ ''+
+ '
')[0],
+ "buttons": $('')[0]
+ };
+
+ // Customise the TableTools buttons with the i18n settings - worth noting that
+ // this could easily be done outside the Editor instance, but this makes things
+ // a bit easier to understand and more cohesive. Also worth noting that when
+ // there are two or more Editor instances, the init sequence should be
+ // Editor / DataTables, Editor / DataTables etc, since the value of these button
+ // instances matter when you create the TableTools buttons for the DataTable.
+ if ( $.fn.dataTable.TableTools ) {
+ var ttButtons = $.fn.dataTable.TableTools.BUTTONS;
+ var i18n = this.i18n;
+
+ $.each(['create', 'edit', 'remove'], function (i, val) {
+ ttButtons['editor_'+val].sButtonText = i18n[val].button;
+ } );
+ }
+
+ // Bind callback methods
+ $.each( init.events, function (evt, fn) {
+ that.on( evt, function () {
+ // When giving events in the constructor the event argument was not
+ // given in 1.2-, so we remove it here. This is solely for
+ // backwards compatibility as the events in the initialisation are
+ // not documented in 1.3+.
+ var args = Array.prototype.slice.call(arguments);
+ args.shift();
+ fn.apply( that, args );
+ } );
+ } );
+
+ // Cache the DOM nodes
+ var dom = this.dom;
+ var wrapper = dom.wrapper;
+ dom.formContent = _editor_el('form_content', dom.form)[0];
+ dom.footer = _editor_el('foot', wrapper)[0];
+ dom.body = _editor_el('body', wrapper)[0];
+ dom.bodyContent = _editor_el('body_content', wrapper)[0];
+ dom.processing = _editor_el('processing', wrapper)[0];
+
+ // Add any fields which are given on initialisation
+ if ( init.fields ) {
+ this.add( init.fields );
+ }
+
+ $(document)
+ .on( 'init.dt.dte', function (e, settings, json) {
+ // Attempt to attach to a DataTable automatically when the table is
+ // initialised
+ if ( that.s.table && settings.nTable === $(that.s.table).get(0) ) {
+ settings._editor = that;
+ }
+ } )
+ .on( 'xhr.dt', function (e, settings, json) {
+ // Automatically update fields which have a field name defined in
+ // the returned json - saves an `initComplete` for the user
+ if ( json && that.s.table && settings.nTable === $(that.s.table).get(0) ) {
+ that._optionsUpdate( json );
+ }
+ } );
+
+ // Prep the display controller
+ this.s.displayController = Editor.display[init.display].init( this );
+
+ this._event( 'initComplete', [] );
+};
+
+/*global __inlineCounter*/
+
+/**
+ * Set the class on the form to relate to the action that is being performed.
+ * This allows styling to be applied to the form to reflect the state that
+ * it is in.
+ *
+ * @private
+ */
+Editor.prototype._actionClass = function ()
+{
+ var classesActions = this.classes.actions;
+ var action = this.s.action;
+ var wrapper = $(this.dom.wrapper);
+
+ wrapper.removeClass( [classesActions.create, classesActions.edit, classesActions.remove].join(' ') );
+
+ if ( action === "create" ) {
+ wrapper.addClass( classesActions.create );
+ }
+ else if ( action === "edit" ) {
+ wrapper.addClass( classesActions.edit );
+ }
+ else if ( action === "remove" ) {
+ wrapper.addClass( classesActions.remove );
+ }
+};
+
+
+/**
+ * Create an Ajax request in the same style as DataTables 1.10, with full
+ * backwards compatibility for Editor 1.2.
+ *
+ * @param {object} data Data to submit
+ * @param {function} success Success callback
+ * @param {function} error Error callback
+ * @private
+ */
+Editor.prototype._ajax = function ( data, success, error )
+{
+ var opts = {
+ type: 'POST',
+ dataType: 'json',
+ data: null,
+ error: error,
+ success: function ( json, status, xhr ) {
+ if ( xhr.status === 204 ) {
+ json = {};
+ }
+ success( json );
+ }
+ };
+ var a;
+ var action = this.s.action;
+ var ajaxSrc = this.s.ajax || this.s.ajaxUrl;
+ var id = action === 'edit' || action === 'remove' ?
+ _pluck( this.s.editFields, 'idSrc' ) :
+ null;
+
+ if ( $.isArray( id ) ) {
+ id = id.join(',');
+ }
+
+ // Get the correct object for rest style
+ if ( $.isPlainObject( ajaxSrc ) && ajaxSrc[ action ] ) {
+ ajaxSrc = ajaxSrc[ action ];
+ }
+
+ if ( $.isFunction( ajaxSrc ) ) {
+ // As a function, execute it, passing in the required parameters
+ var uri = null;
+ var method = null;
+
+ // If the old style ajaxSrc is given, we need to process it for
+ // backwards compatibility with 1.2-. Unfortunate otherwise this would
+ // be a very simply function!
+ if ( this.s.ajaxUrl ) {
+ var url = this.s.ajaxUrl;
+
+ if ( url.create ) {
+ uri = url[ action ];
+ }
+
+ if ( uri.indexOf(' ') !== -1 ) {
+ a = uri.split(' ');
+ method = a[0];
+ uri = a[1];
+ }
+
+ uri = uri.replace( /_id_/, id );
+ }
+
+ ajaxSrc( method, uri, data, success, error );
+ return;
+ }
+ else if ( typeof ajaxSrc === 'string' ) {
+ // As a string it gives the URL. For backwards compatibility it can also
+ // give the method.
+ if ( ajaxSrc.indexOf(' ') !== -1 ) {
+ a = ajaxSrc.split(' ');
+ opts.type = a[0];
+ opts.url = a[1];
+ }
+ else {
+ opts.url = ajaxSrc;
+ }
+ }
+ else {
+ // As an object, we extend the defaults
+ opts = $.extend( {}, opts, ajaxSrc || {} );
+ }
+
+ // URL macros
+ opts.url = opts.url.replace( /_id_/, id );
+
+ // Data processing option like in DataTables
+ if ( opts.data ) {
+ var newData = $.isFunction( opts.data ) ?
+ opts.data( data ) : // fn can manipulate data or return an object
+ opts.data; // object or array to merge
+
+ // If the function returned something, use that alone
+ data = $.isFunction( opts.data ) && newData ?
+ newData :
+ $.extend( true, data, newData );
+ }
+
+ opts.data = data;
+
+ // If a DELETE method is used there are a number of servers which will
+ // reject the request if it has a body. So we need to append to the URL.
+ //
+ // http://stackoverflow.com/questions/15088955
+ // http://bugs.jquery.com/ticket/11586
+ if ( opts.type === 'DELETE' ) {
+ var params = $.param( opts.data );
+
+ opts.url += opts.url.indexOf('?') === -1 ?
+ '?'+params :
+ '&'+params;
+
+ delete opts.data;
+ }
+
+ // Finally, make the ajax call
+ $.ajax( opts );
+};
+
+
+/**
+ * Create the DOM structure from the source elements for the main form.
+ * This is required since the elements can be moved around for other form types
+ * (bubble).
+ *
+ * @private
+ */
+Editor.prototype._assembleMain = function ()
+{
+ var dom = this.dom;
+
+ $(dom.wrapper)
+ .prepend( dom.header );
+
+ $(dom.footer)
+ .append( dom.formError )
+ .append( dom.buttons );
+
+ $(dom.bodyContent)
+ .append( dom.formInfo )
+ .append( dom.form );
+};
+
+
+/**
+ * Blur the editing window. A blur is different from a close in that it might
+ * cause either a close or the form to be submitted. A typical example of a
+ * blur would be clicking on the background of the bubble or main editing forms
+ * - i.e. it might be a close, or it might submit depending upon the
+ * configuration, while a click on the close box is a very definite close.
+ *
+ * @private
+ */
+Editor.prototype._blur = function ()
+{
+ var opts = this.s.editOpts;
+
+ if ( this._event( 'preBlur' ) === false ) {
+ return;
+ }
+
+ if ( opts.onBlur === 'submit' ) {
+ this.submit();
+ }
+ else if ( opts.onBlur === 'close' ) {
+ this._close();
+ }
+};
+
+
+/**
+ * Clear all of the information that might have been dynamically set while
+ * the form was visible - specifically errors and dynamic messages
+ *
+ * @private
+ */
+Editor.prototype._clearDynamicInfo = function ()
+{
+ var errorClass = this.classes.field.error;
+ var fields = this.s.fields;
+
+ $('div.'+errorClass, this.dom.wrapper).removeClass( errorClass );
+
+ $.each( fields, function (name, field) {
+ field
+ .error('')
+ .message('');
+ } );
+
+ this
+ .error('')
+ .message('');
+};
+
+
+/**
+ * Close an editing display, firing callbacks and events as needed
+ *
+ * @param {function} submitComplete Function to call after the preClose event
+ * @private
+ */
+Editor.prototype._close = function ( submitComplete )
+{
+ // Allow preClose event to cancel the opening of the display
+ if ( this._event( 'preClose' ) === false ) {
+ return;
+ }
+
+ if ( this.s.closeCb ) {
+ this.s.closeCb( submitComplete );
+ this.s.closeCb = null;
+ }
+
+ if ( this.s.closeIcb ) {
+ this.s.closeIcb();
+ this.s.closeIcb = null;
+ }
+
+ // Remove focus control
+ $('body').off( 'focus.editor-focus' );
+
+ this.s.displayed = false;
+ this._event( 'close' );
+};
+
+
+/**
+ * Register a function to be called when the editing display is closed. This is
+ * used by function that create the editing display to tidy up the display on
+ * close - for example removing event handlers to prevent memory leaks.
+ *
+ * @param {function} fn Function to call on close
+ * @private
+ */
+Editor.prototype._closeReg = function ( fn )
+{
+ this.s.closeCb = fn;
+};
+
+
+/**
+ * Argument shifting for the create(), edit() and remove() methods. In Editor
+ * 1.3 the preferred form of calling those three methods is with just two
+ * parameters (one in the case of create() - the id and the show flag), while in
+ * previous versions four / three parameters could be passed in, including the
+ * buttons and title options. In 1.3 the chaining API is preferred, but we want
+ * to support the old form as well, so this function is provided to perform
+ * that argument shifting, common to all three.
+ *
+ * @private
+ */
+Editor.prototype._crudArgs = function ( arg1, arg2, arg3, arg4 )
+{
+ var that = this;
+ var title;
+ var buttons;
+ var show;
+ var opts;
+
+ if ( $.isPlainObject( arg1 ) ) {
+ // Form options passed in as the first option
+ opts = arg1;
+ }
+ else if ( typeof arg1 === 'boolean' ) {
+ // Show / hide passed in as the first option - form options second
+ show = arg1;
+ opts = arg2; // can be undefined
+ }
+ else {
+ // Old style arguments
+ title = arg1; // can be undefined
+ buttons = arg2; // can be undefined
+ show = arg3; // can be undefined
+ opts = arg4; // can be undefined
+ }
+
+ // If all undefined, then fall into here
+ if ( show === undefined ) {
+ show = true;
+ }
+
+ if ( title ) {
+ that.title( title );
+ }
+
+ if ( buttons ) {
+ that.buttons( buttons );
+ }
+
+ return {
+ opts: $.extend( {}, this.s.formOptions.main, opts ),
+ maybeOpen: function () {
+ if ( show ) {
+ that.open();
+ }
+ }
+ };
+};
+
+
+/**
+ * Execute the data source abstraction layer functions. This is simply a case
+ * of executing the function with the Editor scope, passing in the remaining
+ * parameters.
+ *
+ * @param {string) name Function name to execute
+ * @private
+ */
+Editor.prototype._dataSource = function ( name /*, ... */ )
+{
+ // Remove the name from the arguments list, so the rest can be passed
+ // straight into the field type
+ var args = Array.prototype.slice.call( arguments );
+ args.shift();
+
+ var fn = this.s.dataSource[ name ];
+ if ( fn ) {
+ return fn.apply( this, args );
+ }
+};
+
+
+/**
+ * Insert the fields into the DOM, in the correct order
+ *
+ * @private
+ */
+Editor.prototype._displayReorder = function ( includeFields )
+{
+ var formContent = $(this.dom.formContent);
+ var fields = this.s.fields;
+ var order = this.s.order;
+
+ if ( includeFields ) {
+ this.s.includeFields = includeFields;
+ }
+ else {
+ includeFields = this.s.includeFields;
+ }
+
+ // Empty before adding in the required fields
+ formContent.children().detach();
+
+ $.each( order, function (i, fieldOrName) {
+ var name = fieldOrName instanceof Editor.Field ?
+ fieldOrName.name() :
+ fieldOrName;
+
+ if ( $.inArray( name, includeFields ) !== -1 ) {
+ formContent.append( fields[ name ].node() );
+ }
+ } );
+
+ this._event( 'displayOrder', [
+ this.s.displayed,
+ this.s.action,
+ formContent
+ ] );
+};
+
+
+/**
+ * Generic editing handler. This can be called by the three editing modes (main,
+ * bubble and inline) to configure Editor for a row edit, and fire the required
+ * events to ensure that the editing interfaces all provide a common API.
+ *
+ * @param {*} rows Identifier for the item(s) to be edited
+ * @param {string} type Editing type - for the initEdit event
+ * @private
+ */
+Editor.prototype._edit = function ( items, editFields, type )
+{
+ var that = this;
+ var fields = this.s.fields;
+ var usedFields = [];
+ var includeInOrder;
+
+ this.s.editFields = editFields;
+ this.s.modifier = items;
+ this.s.action = "edit";
+ this.dom.form.style.display = 'block';
+
+ this._actionClass();
+
+ // Setup the field values for editing
+ $.each( fields, function ( name, field ) {
+ field.multiReset();
+ includeInOrder = true;
+
+ $.each( editFields, function ( idSrc, edit ) {
+ if ( edit.fields[ name ] ) {
+ var val = field.valFromData( edit.data );
+
+ field.multiSet( idSrc, val !== undefined ?
+ val :
+ field.def()
+ );
+
+ // If there is an displayFields object, we need to know if this
+ // field is present in it or not. If not, then the field isn't
+ // displayed
+ if ( edit.displayFields && ! edit.displayFields[ name ] ) {
+ includeInOrder = false;
+ }
+ }
+ } );
+
+ // If the field is used, then add it to the fields to be shown
+ if ( field.multiIds().length !== 0 && includeInOrder ) {
+ usedFields.push( name );
+ }
+ } );
+
+ // Remove the fields that are not required from the display
+ var currOrder = this.order().slice();
+
+ for ( var i=currOrder.length ; i >= 0 ; i-- ) {
+ if ( $.inArray( currOrder[i], usedFields ) === -1 ) {
+ currOrder.splice( i, 1 );
+ }
+ }
+
+ this._displayReorder( currOrder );
+
+ // Save the set data values so we can decided in submit if data has changed
+ this.s.editData = $.extend( true, {}, this.multiGet() );
+
+ // Events
+ this._event( 'initEdit', [
+ _pluck( editFields, 'node' )[0],
+ _pluck( editFields, 'data' )[0],
+ items,
+ type
+ ] );
+
+ this._event( 'initMultiEdit', [
+ editFields,
+ items,
+ type
+ ] );
+};
+
+
+/**
+ * Fire callback functions and trigger events.
+ *
+ * @param {string|array} trigger Name(s) of the jQuery custom event to trigger
+ * @param {array) args Array of arguments to pass to the triggered event
+ * @return {*} Return from the event
+ * @private
+ */
+Editor.prototype._event = function ( trigger, args )
+{
+ if ( ! args ) {
+ args = [];
+ }
+
+ // Allow an array to be passed in for the trigger to fire multiple events
+ if ( $.isArray( trigger ) ) {
+ for ( var i=0, ien=trigger.length ; iEmpty string
+ *
+ * @example
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": "php/index.php",
+ * "table": "#example"
+ * } );
+ * } );
+ */
+ "table": null,
+
+ /**
+ * The URL, or collection of URLs when using a REST interface, which will accept
+ * the data for the create, edit and remove functions. The target script / program
+ * must accept data in the format defined by Editor and return the expected JSON as
+ * required by Editor. When given as an object, the `create`, `edit` and `remove`
+ * properties should be defined, each being the URL to send the data to for that
+ * action. When used as an object, the string `_id_` will be replaced for the edit
+ * and remove actions, allowing a URL to be dynamically created for those actions.
+ * @type string|object
+ * @default Empty string
+ * @deprecated This option has been deprecated in favour of the `ajax` option.
+ * It can still be used, but it is recommended that you use the `ajax` option
+ * which provides all of the abilities of this old option and more.
+ */
+ "ajaxUrl": null,
+
+ /**
+ * Fields to initialise the form with - see {@link Editor.models.field} for
+ * a full list of the options available to each field. Note that if fields are not
+ * added to the form at initialisation time using this option, they can be added using
+ * the {@link Editor#add} API method.
+ * @type array
+ * @default []
+ *
+ * @example
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": "php/index.php",
+ * "table": "#example",
+ * "fields": [ {
+ * "label": "User name:",
+ * "name": "username"
+ * }
+ * // More fields would typically be added here!
+ * } ]
+ * } );
+ * } );
+ */
+ "fields": [],
+
+ /**
+ * The display controller for the form. The form itself is just a collection of
+ * DOM elements which require a display container. This display controller allows
+ * the visual appearance of the form to be significantly altered without major
+ * alterations to the Editor code. There are two display controllers built into
+ * Editor *lightbox* and *envelope*. The value of this property will
+ * be used to access the display controller defined in {@link Editor.display}
+ * for the given name. Additional display controllers can be added by adding objects
+ * to that object, through extending the displayController model:
+ * {@link Editor.models.displayController}.
+ * @type string
+ * @default lightbox
+ *
+ * @example
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": "php/index.php",
+ * "table": "#example",
+ * "display": 'envelope'
+ * } );
+ * } );
+ */
+ "display": 'lightbox',
+
+ /**
+ * Control how the Ajax call to update data on the server.
+ *
+ * This option matches the `dt-init ajax` option in that is can be provided
+ * in one of three different ways:
+ *
+ * * string - As a string, the value given is used as the url to target
+ * the Ajax request to, using the default Editor Ajax options. Note that
+ * for backwards compatibility you can use the form "METHOD URL" - for
+ * example: `"PUT api/users"`, although it is recommended you use the
+ * object form described below.
+ * * object - As an object, the `ajax` property has two forms:
+ * * Used to extend and override the default Ajax options that Editor
+ * uses. This can be very useful for adding extra data for example, or
+ * changing the HTTP request type.
+ * * With `create`, `edit` and `remove` properties, Editor will use the
+ * option for the action that it is taking, which can be useful for
+ * REST style interfaces. The value of each property can be a string,
+ * object or function, using exactly the same options as the main `ajax`
+ * option. All three options must be defined if this form is to be used.
+ * * function - As a function this gives complete control over the method
+ * used to update the server (if indeed a server is being used!). For
+ * example, you could use a different data store such as localStorage,
+ * Firebase or route the data through a web-socket.
+ *
+ * @example
+ * // As a string - all actions are submitted to this URI as POST requests
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": 'php/index.php',
+ * "table": "#example"
+ * } );
+ * } );
+ *
+ * @example
+ * // As an object - using GET rather than POST
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": {
+ * "type": 'GET',
+ * "url": 'php/index.php
+ * },
+ * "table": "#example"
+ * } );
+ * } );
+ *
+ * @example
+ * // As an object - each action is submitted to a different URI as POST requests
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": {
+ * "create": "/rest/user/create",
+ * "edit": "/rest/user/_id_/edit",
+ * "remove": "/rest/user/_id_/delete"
+ * },
+ * "table": "#example"
+ * } );
+ * } );
+ *
+ * @example
+ * // As an object - with different HTTP methods for each action
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": {
+ * "create": {
+ * type: 'POST',
+ * url: '/rest/user/create'
+ * },
+ * "edit": {
+ * type: 'PUT',
+ * url: '/rest/user/edit/_id_'
+ * },
+ * "remove": {
+ * type: 'DELETE',
+ * url: '/rest/user/delete'
+ * }
+ * },
+ * "table": "#example"
+ * } );
+ * } );
+ *
+ * // As a function - Making a custom `$.ajax` call
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": "php/index.php",
+ * "table": "#example",
+ * "ajax": function ( method, url, data, successCallback, errorCallback ) {
+ * $.ajax( {
+ * "type": method,
+ * "url": url,
+ * "data": data,
+ * "dataType": "json",
+ * "success": function (json) {
+ * successCallback( json );
+ * },
+ * "error": function (xhr, error, thrown) {
+ * errorCallback( xhr, error, thrown );
+ * }
+ * } );
+ * }
+ * } );
+ * } );
+ */
+ "ajax": null,
+
+ /**
+ * JSON property from which to read / write the row's ID property (i.e. its
+ * unique column index that identifies the row to the database). By default
+ * Editor will use the `DT_RowId` property from the data source object
+ * (DataTable's magic property to set the DOM id for the row).
+ *
+ * If you want to read a parameter from the data source object instead of
+ * using `DT_RowId`, set this option to the property name to use.
+ *
+ * Like other data source options the `srcId` option can be given in dotted
+ * object notation to read nested objects.
+ * @type null|string
+ * @default DT_RowId
+ *
+ * @example
+ * // Using a data source such as:
+ * // { "id":12, "browser":"Chrome", ... }
+ * $(document).ready(function() {
+ * var editor = new $.fn.Editor( {
+ * "ajax": "php/index.php",
+ * "table": "#example",
+ * "idSrc": "id"
+ * } );
+ * } );
+ */
+ "idSrc": 'DT_RowId',
+
+ /**
+ * Events / callbacks - event handlers can be assigned as an individual function
+ * during initialisation using the parameters in this name space. The names, and
+ * the parameters passed to each callback match their event equivalent in the
+ * {@link Editor} object.
+ * @namespace
+ * @deprecated Since 1.3. Use the `on()` API method instead. Note that events
+ * passed in do still operate as they did in 1.2- but are no longer
+ * individually documented.
+ */
+ "events": {},
+
+ /**
+ * Internationalisation options for Editor. All client-side strings that the
+ * end user can see in the interface presented by Editor can be modified here.
+ *
+ * You may also wish to refer to the
+ * DataTables internationalisation options to provide a fully language
+ * customised table interface.
+ * @namespace
+ *
+ * @example
+ * // Set the 'create' button text. All other strings used are the
+ * // default values.
+ * var editor = new $.fn.Editor( {
+ * "ajax": "data/source",
+ * "table": "#example",
+ * "i18n": {
+ * "create": {
+ * "button": "New user"
+ * }
+ * }
+ * } );
+ *
+ * @example
+ * // Set the submit text for all three actions
+ * var editor = new $.fn.Editor( {
+ * "ajax": "data/source",
+ * "table": "#example",
+ * "i18n": {
+ * "create": {
+ * "submit": "Create new user"
+ * },
+ * "edit": {
+ * "submit": "Update user"
+ * },
+ * "remove": {
+ * "submit": "Remove user"
+ * }
+ * }
+ * } );
+ */
+ "i18n": {
+ /**
+ * Strings used when working with the Editor 'create' action (creating new
+ * records).
+ * @namespace
+ */
+ "create": {
+ /**
+ * TableTools button text
+ * @type string
+ * @default New
+ */
+ "button": "New",
+
+ /**
+ * Display container title (when showing the editor display)
+ * @type string
+ * @default Create new entry
+ */
+ "title": "Create new entry",
+
+ /**
+ * Submit button text
+ * @type string
+ * @default Create
+ */
+ "submit": "Create"
+ },
+
+ /**
+ * Strings used when working with the Editor 'edit' action (editing existing
+ * records).
+ * @namespace
+ */
+ "edit": {
+ /**
+ * TableTools button text
+ * @type string
+ * @default Edit
+ */
+ "button": "Edit",
+
+ /**
+ * Display container title (when showing the editor display)
+ * @type string
+ * @default Edit entry
+ */
+ "title": "Edit entry",
+
+ /**
+ * Submit button text
+ * @type string
+ * @default Update
+ */
+ "submit": "Update"
+ },
+
+ /**
+ * Strings used when working with the Editor 'delete' action (deleting
+ * existing records).
+ * @namespace
+ */
+ "remove": {
+ /**
+ * TableTools button text
+ * @type string
+ * @default Delete
+ */
+ "button": "Delete",
+
+ /**
+ * Display container title (when showing the editor display)
+ * @type string
+ * @default Delete
+ */
+ "title": "Delete",
+
+ /**
+ * Submit button text
+ * @type string
+ * @default Delete
+ */
+ "submit": "Delete",
+
+ /**
+ * Deletion confirmation message.
+ *
+ * As Editor has the ability to delete either a single or multiple rows
+ * at a time, this option can be given as either a string (which will be
+ * used regardless of how many records are selected) or as an object
+ * where the property "_" will be used (with %d substituted for the number
+ * of records to be deleted) as the delete message, unless there is a
+ * key with the number of records to be deleted. This allows Editor
+ * to consider the different pluralisation characteristics of different
+ * languages.
+ * @type object|string
+ * @default Are you sure you wish to delete %d rows?
+ *
+ * @example
+ * // String - no plural consideration
+ * var editor = new $.fn.Editor( {
+ * "ajax": "data/source",
+ * "table": "#example",
+ * "i18n": {
+ * "remove": {
+ * "confirm": "Are you sure you wish to delete %d record(s)?"
+ * }
+ * }
+ * } );
+ *
+ * @example
+ * // Basic 1 (singular) or _ (plural)
+ * var editor = new $.fn.Editor( {
+ * "ajax": "data/source",
+ * "table": "#example",
+ * "i18n": {
+ * "remove": {
+ * "confirm": {
+ * "_": "Confirm deletion of %d records.",
+ * "1": "Confirm deletion of record."
+ * }
+ * }
+ * } );
+ *
+ * @example
+ * // Singular, dual and plural
+ * var editor = new $.fn.Editor( {
+ * "ajax": "data/source",
+ * "table": "#example",
+ * "i18n": {
+ * "remove": {
+ * "confirm": {
+ * "_": "Confirm deletion of %d records.",
+ * "1": "Confirm deletion of record.",
+ * "2": "Confirm deletion of both record."
+ * }
+ * }
+ * } );
+ *
+ */
+ "confirm": {
+ "_": "Are you sure you wish to delete %d rows?",
+ "1": "Are you sure you wish to delete 1 row?"
+ }
+ },
+
+ /**
+ * Strings used for error conditions.
+ * @namespace
+ */
+ "error": {
+ /**
+ * Generic server error message
+ * @type string
+ * @default A system error has occurred (More information)
+ */
+ "system": "A system error has occurred (More information)."
+ },
+
+ /**
+ * Strings used for multi-value editing
+ * @namespace
+ */
+ "multi": {
+ /**
+ * Shown in place of the field value when a field has multiple values
+ */
+ "title": "Multiple values",
+
+ /**
+ * Shown below the multi title text, although only the first
+ * instance of this text is shown in the form to reduce redundancy
+ */
+ "info": "The selected items contain different values for this input. To edit and set all items for this input to the same value, click or tap here, otherwise they will retain their individual values.",
+
+ /**
+ * Shown below the field input when group editing a value to allow
+ * the user to return to the original multiple values
+ */
+ "restore": "Undo changes"
+ },
+
+ "datetime": {
+ previous: 'Previous',
+ next: 'Next',
+ months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
+ weekdays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
+ amPm: [ 'am', 'pm' ],
+ unknown: '-'
+ }
+ },
+
+ formOptions: {
+ bubble: $.extend( {}, Editor.models.formOptions, {
+ title: false,
+ message: false,
+ buttons: '_basic',
+ submit: 'changed'
+ } ),
+
+ inline: $.extend( {}, Editor.models.formOptions, {
+ buttons: false,
+ submit: 'changed'
+ } ),
+
+ main: $.extend( {}, Editor.models.formOptions )
+ },
+
+ /**
+ * Submit data to the server in the 1.4- data format (`true`) or in the 1.5+
+ * data format (`false` - default).
+ *
+ * @type Boolean
+ */
+ legacyAjax: false
+};
+
+
+/*
+ * Extensions
+ */
+
+(function(){
+
+
+var __dataSources = Editor.dataSources = {};
+
+
+/* - - - - - - - - - -
+ * DataTables editor interface
+ */
+
+var __dtIsSsp = function ( dt, editor ) {
+ // If the draw type is `none`, then we still need to use the DT API to
+ // update the display with the new data
+ return dt.settings()[0].oFeatures.bServerSide &&
+ editor.s.editOpts.drawType !== 'none';
+};
+
+var __dtApi = function ( table ) {
+ return $(table).DataTable();
+};
+
+var __dtHighlight = function ( node ) {
+ // Highlight a row using CSS transitions. The timeouts need to match the
+ // transition duration from the CSS
+ node = $(node);
+
+ setTimeout( function () {
+ node.addClass( 'highlight' );
+
+ setTimeout( function () {
+ node
+ .addClass( 'noHighlight' )
+ .removeClass( 'highlight' );
+
+ setTimeout( function () {
+ node.removeClass( 'noHighlight' );
+ }, 550 );
+ }, 500 );
+ }, 20 );
+};
+
+var __dtRowSelector = function ( out, dt, identifier, fields, idFn )
+{
+ dt.rows( identifier ).indexes().each( function ( idx ) {
+ var row = dt.row( idx );
+ var data = row.data();
+ var idSrc = idFn( data );
+
+ if ( idSrc === undefined ) {
+ Editor.error( 'Unable to find row identifier', 14 );
+ }
+
+ out[ idSrc ] = {
+ idSrc: idSrc,
+ data: data,
+ node: row.node(),
+ fields: fields,
+ type: 'row'
+ };
+ } );
+};
+
+var __dtColumnSelector = function ( out, dt, identifier, fields, idFn )
+{
+ dt.cells( null, identifier ).indexes().each( function ( idx ) {
+ __dtCellSelector( out, dt, idx, fields, idFn );
+ } );
+};
+
+var __dtCellSelector = function ( out, dt, identifier, allFields, idFn, forceFields )
+{
+ dt.cells( identifier ).indexes().each( function ( idx ) {
+ var cell = dt.cell( idx );
+ var row = dt.row( idx.row );
+ var data = row.data();
+ var idSrc = idFn( data );
+ var fields = forceFields || __dtFieldsFromIdx( dt, allFields, idx.column );
+
+ // Use the row selector to get the row information
+ __dtRowSelector(out, dt, idx.row, allFields, idFn);
+
+ out[ idSrc ].attach = typeof identifier === 'object' && identifier.nodeName ?
+ [ identifier ] :
+ [ cell.node() ];
+ out[ idSrc ].displayFields = fields; // all fields are edited, but only
+ // these are actually displayed
+ } );
+};
+
+var __dtFieldsFromIdx = function ( dt, fields, idx )
+{
+ var field;
+ var col = dt.settings()[0].aoColumns[ idx ];
+ var dataSrc = col.editField !== undefined ?
+ col.editField :
+ col.mData;
+ var resolvedFields = {};
+ var run = function ( field, dataSrc ) {
+ if ( field.dataSrc() === dataSrc ) {
+ resolvedFields[ field.name() ] = field;
+ }
+ };
+
+ $.each( fields, function ( name, fieldInst ) {
+ if ( $.isArray( dataSrc ) ) {
+ for ( var i=0 ; i tag
+ */
+ "tag": "",
+
+ /**
+ * Global form information
+ */
+ "info": "DTE_Form_Info",
+
+ /**
+ * Global error imformation
+ */
+ "error": "DTE_Form_Error",
+
+ /**
+ * Buttons container
+ */
+ "buttons": "DTE_Form_Buttons",
+
+ /**
+ * Buttons container
+ */
+ "button": "btn"
+ },
+
+ /**
+ * Field classes
+ * @namespace
+ */
+ "field": {
+ /**
+ * Container for each field
+ */
+ "wrapper": "DTE_Field",
+
+ /**
+ * Class prefix for the field type - field type is added to the end allowing
+ * styling based on field type.
+ */
+ "typePrefix": "DTE_Field_Type_",
+
+ /**
+ * Class prefix for the field name - field name is added to the end allowing
+ * styling based on field name.
+ */
+ "namePrefix": "DTE_Field_Name_",
+
+ /**
+ * Field label
+ */
+ "label": "DTE_Label",
+
+ /**
+ * Field input container
+ */
+ "input": "DTE_Field_Input",
+
+ /**
+ * Input elements wrapper
+ */
+ "inputControl": "DTE_Field_InputControl",
+
+ /**
+ * Field error state (added to the field.wrapper element when in error state
+ */
+ "error": "DTE_Field_StateError",
+
+ /**
+ * Label information text
+ */
+ "msg-label": "DTE_Label_Info",
+
+ /**
+ * Error information text
+ */
+ "msg-error": "DTE_Field_Error",
+
+ /**
+ * Live messaging (API) information text
+ */
+ "msg-message": "DTE_Field_Message",
+
+ /**
+ * General information text
+ */
+ "msg-info": "DTE_Field_Info",
+
+ /**
+ * Multi-value information display wrapper
+ */
+ "multiValue": "multi-value",
+
+ /**
+ * Multi-value information descriptive text
+ */
+ "multiInfo": "multi-info",
+
+ /**
+ * Multi-value information display
+ */
+ "multiRestore": "multi-restore"
+ },
+
+ /**
+ * Action classes - these are added to the Editor base element ("wrapper")
+ * and allows styling based on the type of form view that is being employed.
+ * @namespace
+ */
+ "actions": {
+ /**
+ * Editor is in 'create' state
+ */
+ "create": "DTE_Action_Create",
+
+ /**
+ * Editor is in 'edit' state
+ */
+ "edit": "DTE_Action_Edit",
+
+ /**
+ * Editor is in 'remove' state
+ */
+ "remove": "DTE_Action_Remove"
+ },
+
+ /**
+ * Bubble editing classes - these are used to display the bubble editor
+ * @namespace
+ */
+ "bubble": {
+ /**
+ * Bubble container element
+ */
+ "wrapper": "DTE DTE_Bubble",
+
+ /**
+ * Bubble content liner
+ */
+ "liner": "DTE_Bubble_Liner",
+
+ /**
+ * Bubble table display wrapper, so the buttons and form can be shown
+ * as table cells (via css)
+ */
+ "table": "DTE_Bubble_Table",
+
+ /**
+ * Close button
+ */
+ "close": "DTE_Bubble_Close",
+
+ /**
+ * Pointer shown which node is being edited
+ */
+ "pointer": "DTE_Bubble_Triangle",
+
+ /**
+ * Fixed background
+ */
+ "bg": "DTE_Bubble_Background"
+ }
+};
+
+
+
+/*
+ * Add helpful TableTool buttons to make life easier
+ *
+ * Note that the values that require a string to make any sense (the button text
+ * for example) are set by Editor when Editor is initialised through the i18n
+ * options.
+ */
+if ( DataTable.TableTools ) {
+ var ttButtons = DataTable.TableTools.BUTTONS;
+ var ttButtonBase = {
+ sButtonText: null,
+ editor: null,
+ formTitle: null
+ };
+
+ ttButtons.editor_create = $.extend( true, ttButtons.text, ttButtonBase, {
+ formButtons: [ {
+ label: null,
+ fn: function (e) { this.submit(); }
+ } ],
+
+ fnClick: function( button, config ) {
+ var editor = config.editor;
+ var i18nCreate = editor.i18n.create;
+ var buttons = config.formButtons;
+
+ if ( ! buttons[0].label ) {
+ buttons[0].label = i18nCreate.submit;
+ }
+
+ editor.create( {
+ title: i18nCreate.title,
+ buttons: buttons
+ } );
+ }
+ } );
+
+
+ ttButtons.editor_edit = $.extend( true, ttButtons.select_single, ttButtonBase, {
+ formButtons: [ {
+ label: null,
+ fn: function (e) { this.submit(); }
+ } ],
+
+ fnClick: function( button, config ) {
+ var selected = this.fnGetSelectedIndexes();
+ if ( selected.length !== 1 ) {
+ return;
+ }
+
+ var editor = config.editor;
+ var i18nEdit = editor.i18n.edit;
+ var buttons = config.formButtons;
+
+ if ( ! buttons[0].label ) {
+ buttons[0].label = i18nEdit.submit;
+ }
+
+ editor.edit( selected[0], {
+ title: i18nEdit.title,
+ buttons: buttons
+ } );
+ }
+ } );
+
+
+ ttButtons.editor_remove = $.extend( true, ttButtons.select, ttButtonBase, {
+ question: null,
+
+ formButtons: [
+ {
+ label: null,
+ fn: function (e) {
+ // Executed in the Form instance's scope
+ var that = this;
+ this.submit( function ( json ) {
+ var tt = $.fn.dataTable.TableTools.fnGetInstance(
+ $(that.s.table).DataTable().table().node()
+ );
+ tt.fnSelectNone();
+ } );
+ }
+ }
+ ],
+
+ fnClick: function( button, config ) {
+ var rows = this.fnGetSelectedIndexes();
+ if ( rows.length === 0 ) {
+ return;
+ }
+
+ var editor = config.editor;
+ var i18nRemove = editor.i18n.remove;
+ var buttons = config.formButtons;
+ var question = typeof i18nRemove.confirm === 'string' ?
+ i18nRemove.confirm :
+ i18nRemove.confirm[rows.length] ?
+ i18nRemove.confirm[rows.length] : i18nRemove.confirm._;
+
+ if ( ! buttons[0].label ) {
+ buttons[0].label = i18nRemove.submit;
+ }
+
+ editor.remove( rows, {
+ message: question.replace( /%d/g, rows.length ),
+ title: i18nRemove.title,
+ buttons: buttons
+ } );
+ }
+ } );
+}
+
+
+$.extend( DataTable.ext.buttons, {
+ create: {
+ text: function ( dt, node, config ) {
+ return dt.i18n( 'buttons.create', config.editor.i18n.create.button );
+ },
+ className: 'buttons-create',
+ editor: null,
+ formButtons: {
+ label: function ( editor ) {
+ return editor.i18n.create.submit;
+ },
+ fn: function (e) {
+ this.submit();
+ }
+ },
+ formMessage: null,
+ formTitle: null,
+ action: function( e, dt, node, config ) {
+ var editor = config.editor;
+ var buttons = config.formButtons;
+
+ editor.create( {
+ buttons: config.formButtons,
+ message: config.formMessage,
+ title: config.formTitle || editor.i18n.create.title
+ } );
+ }
+ },
+
+ edit: {
+ extend: 'selected',
+ text: function ( dt, node, config ) {
+ return dt.i18n( 'buttons.edit', config.editor.i18n.edit.button );
+ },
+ className: 'buttons-edit',
+ editor: null,
+ formButtons: {
+ label: function ( editor ) {
+ return editor.i18n.edit.submit;
+ },
+ fn: function (e) {
+ this.submit();
+ }
+ },
+ formMessage: null,
+ formTitle: null,
+ action: function( e, dt, node, config ) {
+ var editor = config.editor;
+ var rows = dt.rows( { selected: true } ).indexes();
+ var columns = dt.columns( { selected: true } ).indexes();
+ var cells = dt.cells( { selected: true } ).indexes();
+
+ var items = columns.length || cells.length ?
+ {
+ rows: rows,
+ columns: columns,
+ cells: cells
+ } :
+ rows;
+
+ editor.edit( items, {
+ message: config.formMessage,
+ buttons: config.formButtons,
+ title: config.formTitle || editor.i18n.edit.title
+ } );
+ }
+ },
+
+ remove: {
+ extend: 'selected',
+ text: function ( dt, node, config ) {
+ return dt.i18n( 'buttons.remove', config.editor.i18n.remove.button );
+ },
+ className: 'buttons-remove',
+ editor: null,
+ formButtons: {
+ label: function ( editor ) {
+ return editor.i18n.remove.submit;
+ },
+ fn: function (e) {
+ this.submit();
+ }
+ },
+ formMessage: function ( editor, dt ) {
+ var rows = dt.rows( { selected: true } ).indexes();
+ var i18n = editor.i18n.remove;
+ var question = typeof i18n.confirm === 'string' ?
+ i18n.confirm :
+ i18n.confirm[rows.length] ?
+ i18n.confirm[rows.length] : i18n.confirm._;
+
+ return question.replace( /%d/g, rows.length );
+ },
+ formTitle: null,
+ action: function( e, dt, node, config ) {
+ var editor = config.editor;
+
+ editor.remove( dt.rows( { selected: true } ).indexes(), {
+ buttons: config.formButtons,
+ message: config.formMessage,
+ title: config.formTitle || editor.i18n.remove.title
+ } );
+ }
+ }
+} );
+
+/**
+ * Field types array - this can be used to add field types or modify the pre-
+ * defined options. See the online Editor documentation for information about
+ * the built in field types.
+ *
+ * Note that we use a DataTables ext object to allow plug-ins to be loaded
+ * before Editor itself. This is useful for the online DataTables download
+ * builder.
+ *
+ * @namespace
+ */
+Editor.fieldTypes = {};
+/*
+ * This file provides a DateTime GUI picker (calender and time input). Only the
+ * format YYYY-MM-DD is supported without additional software, but the end user
+ * experience can be greatly enhanced by including the momentjs library which
+ * provides date / time parsing and formatting options.
+ *
+ * This functionality is required because the HTML5 date and datetime input
+ * types are not widely supported in desktop browsers.
+ *
+ * Constructed by using:
+ *
+ * new Editor.DateTime( input, opts )
+ *
+ * where `input` is the HTML input element to use and `opts` is an object of
+ * options based on the `Editor.DateTime.defaults` object.
+ */
+
+Editor.DateTime = function ( input, opts ) {
+ this.c = $.extend( true, {}, Editor.DateTime.defaults, opts );
+ var classPrefix = this.c.classPrefix;
+ var i18n = this.c.i18n;
+
+ // Only IS8601 dates are supported without moment
+ if ( ! window.moment && this.c.format !== 'YYYY-MM-DD' ) {
+ throw "Editor datetime: Without momentjs only the format 'YYYY-MM-DD' can be used";
+ }
+
+ var timeBlock = function ( type ) {
+ return '
'+
+ '
'+
+ ''+
+ '
'+
+ '
'+
+ ''+
+ ''+
+ '
'+
+ '
'+
+ ''+
+ '
'+
+ '
';
+ };
+
+ var gap = function () {
+ return ':';
+ };
+
+ // DOM structure
+ var structure = $(
+ '
'
+ );
+
+ this.dom = {
+ container: structure,
+ date: structure.find( '.'+classPrefix+'-date' ),
+ title: structure.find( '.'+classPrefix+'-title' ),
+ calendar: structure.find( '.'+classPrefix+'-calendar' ),
+ time: structure.find( '.'+classPrefix+'-time' ),
+ input: $(input)
+ };
+
+ this.s = {
+ /** @type {Date} Date value that the picker has currently selected */
+ d: null,
+
+ /** @type {Date} Date of the calender - might not match the value */
+ display: null,
+
+ /** @type {String} Unique namespace string for this instance */
+ namespace: 'editor-dateime-'+(Editor.DateTime._instance++),
+
+ /** @type {Object} Parts of the picker that should be shown */
+ parts: {
+ date: this.c.format.match( /[YMD]/ ) !== null,
+ time: this.c.format.match( /[Hhm]/ ) !== null,
+ seconds: this.c.format.indexOf( 's' ) !== -1,
+ hours12: this.c.format.match( /[haA]/ ) !== null
+ }
+ };
+
+ this.dom.container
+ .append( this.dom.date )
+ .append( this.dom.time );
+
+ this.dom.date
+ .append( this.dom.title )
+ .append( this.dom.calendar );
+
+ this._constructor();
+};
+
+$.extend( Editor.DateTime.prototype, {
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Public
+ */
+
+ /**
+ * Destroy the control
+ */
+ destroy: function () {
+ this._hide();
+ this.dom.container().off('').empty();
+ this.dom.input.off('.editor-datetime');
+ },
+
+ max: function ( date ) {
+ this.c.maxDate = date;
+
+ this._optionsTitle();
+ this._setCalander();
+ },
+
+ min: function ( date ) {
+ this.c.minDate = date;
+
+ this._optionsTitle();
+ this._setCalander();
+ },
+
+ /**
+ * Check if an element belongs to this control
+ *
+ * @param {node} node Element to check
+ * @return {boolean} true if owned by this control, false otherwise
+ */
+ owns: function ( node ) {
+ return $(node).parents().filter( this.dom.container ).length > 0;
+ },
+
+ /**
+ * Get / set the value
+ *
+ * @param {string|Date} set Value to set
+ * @param {boolean} [write=true] Flag to indicate if the formatted value
+ * should be written into the input element
+ */
+ val: function ( set, write ) {
+ if ( set === undefined ) {
+ return this.s.d;
+ }
+
+ if ( set instanceof Date ) {
+ this.s.d = this._dateToUtc( set );
+ }
+ else if ( set === null || set === '' ) {
+ this.s.d = null;
+ }
+ else if ( typeof set === 'string' ) {
+ if ( window.moment ) {
+ // Use moment if possible (even for ISO8601 strings, since it
+ // will correctly handle 0000-00-00 and the like)
+ var m = window.moment.utc( set, this.c.format, this.c.momentLocale, this.c.momentStrict );
+ this.s.d = m.isValid() ? m.toDate() : null;
+ }
+ else {
+ // Else must be using ISO8601 without moment (constructor would
+ // have thrown an error otherwise)
+ var match = set.match(/(\d{4})\-(\d{2})\-(\d{2})/ );
+ this.s.d = match ?
+ new Date( Date.UTC(match[1], match[2]-1, match[3]) ) :
+ null;
+ }
+ }
+
+ if ( write || write === undefined ) {
+ if ( this.s.d ) {
+ this._writeOutput();
+ }
+ else {
+ // The input value was not valid...
+ this.dom.input.val( set );
+ }
+ }
+
+ // We need a date to be able to display the calendar at all
+ if ( ! this.s.d ) {
+ this.s.d = this._dateToUtc( new Date() );
+ }
+
+ this.s.display = new Date( this.s.d.toString() );
+
+ // Update the display elements for the new value
+ this._setTitle();
+ this._setCalander();
+ this._setTime();
+ },
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Constructor
+ */
+
+ /**
+ * Build the control and assign initial event handlers
+ *
+ * @private
+ */
+ _constructor: function () {
+ var that = this;
+ var classPrefix = this.c.classPrefix;
+ var container = this.dom.container;
+ var i18n = this.c.i18n;
+
+ if ( ! this.s.parts.date ) {
+ this.dom.date.css( 'display', 'none' );
+ }
+
+ if ( ! this.s.parts.time ) {
+ this.dom.time.css( 'display', 'none' );
+ }
+
+ if ( ! this.s.parts.seconds ) {
+ this.dom.time.children('div.editor-datetime-timeblock').eq(2).remove();
+ this.dom.time.children('span').eq(1).remove();
+ }
+
+ if ( ! this.s.parts.hours12 ) {
+ this.dom.time.children('div.editor-datetime-timeblock').last().remove();
+ }
+
+ // Render the options
+ this._optionsTitle();
+ this._optionsTime( 'hours', this.s.parts.hours12 ? 12 : 24, 1 );
+ this._optionsTime( 'minutes', 60, this.c.minutesIncrement );
+ this._optionsTime( 'seconds', 60, this.c.secondsIncrement );
+ this._options( 'ampm', [ 'am', 'pm' ], i18n.amPm );
+
+ // Trigger the display of the widget when clicking or focusing on the
+ // input element
+ this.dom.input
+ .on('focus.editor-datetime click.editor-datetime', function () {
+ // If already visible - don't do anything
+ if ( that.dom.container.is(':visible') || that.dom.input.is(':disabled') ) {
+ return;
+ }
+
+ // In case the value has changed by text
+ that.val( that.dom.input.val(), false );
+
+ that._show();
+ } )
+ .on('keyup.editor-datetime', function () {
+ // Update the calender's displayed value as the user types
+ if ( that.dom.container.is(':visible') ) {
+ that.val( that.dom.input.val(), false );
+ }
+ } );
+
+ // Main event handlers for input in the widget
+ this.dom.container
+ .on( 'change', 'select', function () {
+ var select = $(this);
+ var val = select.val();
+
+ if ( select.hasClass(classPrefix+'-month') ) {
+ // Month select
+ that._correctMonth( that.s.display, val );
+ that._setTitle();
+ that._setCalander();
+ }
+ else if ( select.hasClass(classPrefix+'-year') ) {
+ // Year select
+ that.s.display.setUTCFullYear( val );
+ that._setTitle();
+ that._setCalander();
+ }
+ else if ( select.hasClass(classPrefix+'-hours') || select.hasClass(classPrefix+'-ampm') ) {
+ // Hours - need to take account of AM/PM input if present
+ if ( that.s.parts.hours12 ) {
+ var hours = $(that.dom.container).find('.'+classPrefix+'-hours').val() * 1;
+ var pm = $(that.dom.container).find('.'+classPrefix+'-ampm').val() === 'pm';
+
+ that.s.d.setUTCHours( hours === 12 && !pm ?
+ 0 :
+ pm && hours !== 12 ?
+ hours + 12 :
+ hours
+ );
+ }
+ else {
+ that.s.d.setUTCHours( val );
+ }
+
+ that._setTime();
+ that._writeOutput( true );
+ }
+ else if ( select.hasClass(classPrefix+'-minutes') ) {
+ // Minutes select
+ that.s.d.setUTCMinutes( val );
+ that._setTime();
+ that._writeOutput( true );
+ }
+ else if ( select.hasClass(classPrefix+'-seconds') ) {
+ // Seconds select
+ that.s.d.setSeconds( val );
+ that._setTime();
+ that._writeOutput( true );
+ }
+
+ that.dom.input.focus();
+ that._position();
+ } )
+ .on( 'click', function (e) {
+ var nodeName = e.target.nodeName.toLowerCase();
+
+ if ( nodeName === 'select' ) {
+ return;
+ }
+
+ e.stopPropagation();
+
+ if ( nodeName === 'button' ) {
+ var button = $(e.target);
+ var parent = button.parent();
+ var select;
+
+ if ( parent.hasClass('disabled') ) {
+ return;
+ }
+
+ if ( parent.hasClass(classPrefix+'-iconLeft') ) {
+ // Previous month
+ that.s.display.setUTCMonth( that.s.display.getUTCMonth()-1 );
+ that._setTitle();
+ that._setCalander();
+
+ that.dom.input.focus();
+ }
+ else if ( parent.hasClass(classPrefix+'-iconRight') ) {
+ // Next month
+ that._correctMonth( that.s.display, that.s.display.getUTCMonth()+1 );
+ that._setTitle();
+ that._setCalander();
+
+ that.dom.input.focus();
+ }
+ else if ( parent.hasClass(classPrefix+'-iconUp') ) {
+ // Value increase - common to all time selects
+ select = parent.parent().find('select')[0];
+ select.selectedIndex = select.selectedIndex !== select.options.length - 1 ?
+ select.selectedIndex+1 :
+ 0;
+ $(select).change();
+ }
+ else if ( parent.hasClass(classPrefix+'-iconDown') ) {
+ // Value decrease - common to all time selects
+ select = parent.parent().find('select')[0];
+ select.selectedIndex = select.selectedIndex === 0 ?
+ select.options.length - 1 :
+ select.selectedIndex-1;
+ $(select).change();
+ }
+ else {
+ // Calender click
+ if ( ! that.s.d ) {
+ that.s.d = that._dateToUtc( new Date() );
+ }
+
+ that.s.d.setUTCFullYear( button.data('year') );
+ that.s.d.setUTCMonth( button.data('month') );
+ that.s.d.setUTCDate( button.data('day') );
+
+ that._writeOutput( true );
+
+ // This is annoying but IE has some kind of async
+ // behaviour with focus and the focus from the above
+ // write would occur after this hide - resulting in the
+ // calender opening immediately
+ setTimeout( function () {
+ that._hide();
+ }, 10 );
+ }
+ }
+ else {
+ // Click anywhere else in the widget - return focus to the
+ // input element
+ that.dom.input.focus();
+ }
+ } );
+ },
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Private
+ */
+
+ /**
+ * Compare the date part only of two dates - this is made super easy by the
+ * toDateString method!
+ *
+ * @param {Date} a Date 1
+ * @param {Date} b Date 2
+ * @private
+ */
+ _compareDates: function( a, b ) {
+ // Can't use toDateString as that converts to local time
+ return this._dateToUtcString(a) === this._dateToUtcString(b);
+ },
+
+ /**
+ * When changing month, take account of the fact that some months don't have
+ * the same number of days. For example going from January to February you
+ * can have the 31st of Jan selected and just add a month since the date
+ * would still be 31, and thus drop you into March.
+ *
+ * @param {Date} date Date - will be modified
+ * @param {integer} month Month to set
+ * @private
+ */
+ _correctMonth: function ( date, month ) {
+ var days = this._daysInMonth( date.getUTCFullYear(), month );
+ var correctDays = date.getUTCDate() > days;
+
+ date.setUTCMonth( month );
+
+ if ( correctDays ) {
+ date.setUTCDate( days );
+ date.setUTCMonth( month );
+ }
+ },
+
+ /**
+ * Get the number of days in a method. Based on
+ * http://stackoverflow.com/a/4881951 by Matti Virkkunen
+ *
+ * @param {integer} year Year
+ * @param {integer} month Month (starting at 0)
+ * @private
+ */
+ _daysInMonth: function ( year, month ) {
+ //
+ var isLeap = ((year % 4) === 0 && ((year % 100) !== 0 || (year % 400) === 0));
+ var months = [31, (isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+ return months[month];
+ },
+
+ /**
+ * Create a new date object which has the UTC values set to the local time.
+ * This allows the local time to be used directly for the library which
+ * always bases its calculations and display on UTC.
+ *
+ * @param {Date} s Date to "convert"
+ * @return {Date} Shifted date
+ */
+ _dateToUtc: function ( s ) {
+ return new Date( Date.UTC(
+ s.getFullYear(), s.getMonth(), s.getDate(),
+ s.getHours(), s.getMinutes(), s.getSeconds()
+ ) );
+ },
+
+ /**
+ * Create a UTC ISO8601 date part from a date object
+ *
+ * @param {Date} d Date to "convert"
+ * @return {string} ISO formatted date
+ */
+ _dateToUtcString: function ( d ) {
+ return d.getUTCFullYear()+'-'+
+ this._pad(d.getUTCMonth()+1)+'-'+
+ this._pad(d.getUTCDate());
+ },
+
+ /**
+ * Hide the control and remove events related to its display
+ *
+ * @private
+ */
+ _hide: function () {
+ var namespace = this.s.namespace;
+
+ this.dom.container.detach();
+
+ $(window).off( '.'+namespace );
+ $(document).off( 'keydown.'+namespace );
+ $('div.DTE_Body_Content').off( 'scroll.'+namespace );
+ $('body').off( 'click.'+namespace );
+ },
+
+ /**
+ * Convert a 24 hour value to a 12 hour value
+ *
+ * @param {integer} val 24 hour value
+ * @return {integer} 12 hour value
+ * @private
+ */
+ _hours24To12: function ( val ) {
+ return val === 0 ?
+ 12 :
+ val > 12 ?
+ val - 12 :
+ val;
+ },
+
+ /**
+ * Generate the HTML for a single day in the calender - this is basically
+ * and HTML cell with a button that has data attributes so we know what was
+ * clicked on (if it is clicked on) and a bunch of classes for styling.
+ *
+ * @param {object} day Day object from the `_htmlMonth` method
+ * @return {string} HTML cell
+ */
+ _htmlDay: function( day )
+ {
+ if ( day.empty ) {
+ return '
';
+ },
+
+ /**
+ * Create the calender table's header (week days)
+ *
+ * @return {string} HTML cells for the row
+ * @private
+ */
+ _htmlMonthHead: function () {
+ var a = [];
+ var firstDay = this.c.firstDay;
+ var i18n = this.c.i18n;
+
+ // Take account of the first day shift
+ var dayName = function ( day ) {
+ day += firstDay;
+
+ while (day >= 7) {
+ day -= 7;
+ }
+
+ return i18n.weekdays[day];
+ };
+
+ // Empty cell in the header
+ if ( this.c.showWeekNumber ) {
+ a.push( '
' );
+ }
+
+ for ( var i=0 ; i<7 ; i++ ) {
+ a.push( '
'+dayName( i )+'
' );
+ }
+
+ return a.join('');
+ },
+
+ /**
+ * Create a cell that contains week of the year - ISO style
+ *
+ * Based on http://javascript.about.com/library/blweekyear.htm
+ *
+ * @param {integer} d Day of month
+ * @param {integer} m Month of year (zero index)
+ * @param {integer} y Year
+ * @return {string}
+ * @private
+ */
+ _htmlWeekOfYear: function ( d, m, y ) {
+ var onejan = new Date(y, 0, 1),
+ weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getUTCDay()+1)/7);
+
+ return '
' + weekNum + '
';
+ },
+
+ /**
+ * Create option elements from a range in an array
+ *
+ * @param {string} selector Class name unique to the select element to use
+ * @param {array} values Array of values
+ * @param {array} [labels] Array of labels. If given must be the same
+ * length as the values parameter.
+ * @private
+ */
+ _options: function ( selector, values, labels ) {
+ if ( ! labels ) {
+ labels = values;
+ }
+
+ var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
+ select.empty();
+
+ for ( var i=0, ien=values.length ; i'+labels[i]+'' );
+ }
+ },
+
+ /**
+ * Set an option and update the option's span pair (since the select element
+ * has opacity 0 for styling)
+ *
+ * @param {string} selector Class name unique to the select element to use
+ * @param {*} val Value to set
+ * @private
+ */
+ _optionSet: function ( selector, val ) {
+ var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
+ var span = select.parent().children('span');
+
+ select.val( val );
+
+ var selected = select.find('option:selected');
+ span.html( selected.length !== 0 ?
+ selected.text() :
+ this.c.i18n.unknown
+ );
+ },
+
+ /**
+ * Create time option list. Can't just use `_options` for the time as the
+ * hours can be a little complex and we want to be able to control the
+ * increment option for the minutes and seconds.
+ *
+ * @param {jQuery} select Select element to operate on
+ * @param {integer} count Value to loop to
+ * @param {integer} inc Increment value
+ * @private
+ */
+ _optionsTime: function ( select, count, inc ) {
+ var classPrefix = this.c.classPrefix;
+ var sel = this.dom.container.find('select.'+classPrefix+'-'+select);
+ var start=0, end=count;
+ var render = count === 12 ?
+ function (i) { return i; } :
+ this._pad;
+
+ if ( count === 12 ) {
+ start = 1;
+ end = 13;
+ }
+
+ for ( var i=start ; i'+render(i)+'' );
+ }
+ },
+
+ /**
+ * Create the options for the month and year
+ *
+ * @param {integer} year Year
+ * @param {integer} month Month (starting at 0)
+ * @private
+ */
+ _optionsTitle: function ( year, month ) {
+ var classPrefix = this.c.classPrefix;
+ var i18n = this.c.i18n;
+ var min = this.c.minDate;
+ var max = this.c.maxDate;
+ var minYear = min ? min.getFullYear() : null;
+ var maxYear = max ? max.getFullYear() : null;
+
+ var i = minYear !== null ? minYear : new Date().getFullYear() - this.c.yearRange;
+ var j = maxYear !== null ? maxYear : new Date().getFullYear() + this.c.yearRange;
+
+ this._options( 'month', this._range( 0, 11 ), i18n.months );
+ this._options( 'year', this._range( i, j ) );
+ },
+
+ /**
+ * Simple two digit pad
+ *
+ * @param {integer} i Value that might need padding
+ * @return {string|integer} Padded value
+ * @private
+ */
+ _pad: function ( i ) {
+ return i<10 ? '0'+i : i;
+ },
+
+ /**
+ * Position the calender to look attached to the input element
+ * @private
+ */
+ _position: function () {
+ var offset = this.dom.input.offset();
+ var container = this.dom.container;
+ var inputHeight = this.dom.input.outerHeight();
+
+ container
+ .css( {
+ top: offset.top + inputHeight,
+ left: offset.left
+ } )
+ .appendTo( 'body' );
+
+ var calHeight = container.outerHeight();
+ var scrollTop = $('body').scrollTop();
+
+ if ( offset.top + inputHeight + calHeight - scrollTop > $(window).height() ) {
+ var newTop = offset.top - calHeight;
+
+ container.css( 'top', newTop < 0 ? 0 : newTop );
+ }
+ },
+
+ /**
+ * Create a simple array with a range of values
+ *
+ * @param {integer} start Start value (inclusive)
+ * @param {integer} end End value (inclusive)
+ * @return {array} Created array
+ * @private
+ */
+ _range: function ( start, end ) {
+ var a = [];
+
+ for ( var i=start ; i<=end ; i++ ) {
+ a.push( i );
+ }
+
+ return a;
+ },
+
+ /**
+ * Redraw the calender based on the display date - this is a destructive
+ * operation
+ *
+ * @private
+ */
+ _setCalander: function () {
+ this.dom.calendar
+ .empty()
+ .append( this._htmlMonth(
+ this.s.display.getUTCFullYear(),
+ this.s.display.getUTCMonth()
+ ) );
+ },
+
+ /**
+ * Set the month and year for the calender based on the current display date
+ *
+ * @private
+ */
+ _setTitle: function () {
+ this._optionSet( 'month', this.s.display.getUTCMonth() );
+ this._optionSet( 'year', this.s.display.getUTCFullYear() );
+ },
+
+ /**
+ * Set the time based on the current value of the widget
+ *
+ * @private
+ */
+ _setTime: function () {
+ var d = this.s.d;
+ var hours = d ? d.getUTCHours() : 0;
+
+ if ( this.s.parts.hours12 ) {
+ this._optionSet( 'hours', this._hours24To12( hours ) );
+ this._optionSet( 'ampm', hours < 12 ? 'am' : 'pm' );
+ }
+ else {
+ this._optionSet( 'hours', hours );
+ }
+
+ this._optionSet( 'minutes', d ? d.getUTCMinutes() : 0 );
+ this._optionSet( 'seconds', d ? d.getSeconds() : 0 );
+ },
+
+ /**
+ * Show the widget and add events to the document required only while it
+ * is displayed
+ *
+ * @private
+ */
+ _show: function () {
+ var that = this;
+ var namespace = this.s.namespace;
+
+ this._position();
+
+ // Need to reposition on scroll
+ $(window).on( 'scroll.'+namespace+' resize.'+namespace, function () {
+ that._position();
+ } );
+
+ $('div.DTE_Body_Content').on( 'scroll.'+namespace, function () {
+ that._position();
+ } );
+
+ // On tab focus will move to a different field (no keyboard navigation
+ // in the date picker - this might need to be changed).
+ // On esc the Editor might close. Even if it doesn't the date picker
+ // should
+ $(document).on( 'keydown.'+namespace, function (e) {
+ if (
+ e.keyCode === 9 || // tab
+ e.keyCode === 27 || // esc
+ e.keyCode === 13 // return
+ ) {
+ that._hide();
+ }
+ } );
+
+ // Hide if clicking outside of the widget - but in a different click
+ // event from the one that was used to trigger the show (bubble and
+ // inline)
+ setTimeout( function () {
+ $('body').on( 'click.'+namespace, function (e) {
+ var parents = $(e.target).parents();
+
+ if ( ! parents.filter( that.dom.container ).length && e.target !== that.dom.input[0] ) {
+ that._hide();
+ }
+ } );
+ }, 10 );
+ },
+
+ /**
+ * Write the formatted string to the input element this control is attached
+ * to
+ *
+ * @private
+ */
+ _writeOutput: function ( focus ) {
+ var date = this.s.d;
+
+ // Use moment if possible - otherwise it must be ISO8601 (or the
+ // constructor would have thrown an error)
+ var out = window.moment ?
+ window.moment.utc( date, undefined, this.c.momentLocale, this.c.momentStrict ).format( this.c.format ) :
+ date.getUTCFullYear() +'-'+
+ this._pad(date.getUTCMonth() + 1) +'-'+
+ this._pad(date.getUTCDate());
+
+ this.dom.input.val( out );
+
+ if ( focus ) {
+ this.dom.input.focus();
+ }
+ }
+} );
+
+
+/**
+ * For generating unique namespaces
+ *
+ * @type {Number}
+ * @private
+ */
+Editor.DateTime._instance = 0;
+
+/**
+ * Defaults for the date time picker
+ *
+ * @type {Object}
+ */
+Editor.DateTime.defaults = {
+ // Not documented - could be an internal property
+ classPrefix: 'editor-datetime',
+
+ // function or array of ints
+ disableDays: null,
+
+ // first day of the week (0: Sunday, 1: Monday, etc)
+ firstDay: 1,
+
+ format: 'YYYY-MM-DD',
+
+ // Not documented as i18n is done by the Editor.defaults.i18n obj
+ i18n: Editor.defaults.i18n.datetime,
+
+ maxDate: null,
+
+ minDate: null,
+
+ minutesIncrement: 1,
+
+ momentStrict: true,
+
+ momentLocale: 'en',
+
+ secondsIncrement: 1,
+
+ // show the ISO week number at the head of the row
+ showWeekNumber: false,
+
+ // overruled by max / min date
+ yearRange: 10
+};
+
+
+
+(function() {
+
+var fieldTypes = Editor.fieldTypes;
+
+// Upload private helper method
+function _buttonText ( conf, text )
+{
+ if ( text === null || text === undefined ) {
+ text = conf.uploadText || "Choose file...";
+ }
+
+ conf._input.find('div.upload button').html( text );
+}
+
+function _commonUpload ( editor, conf, dropCallback )
+{
+ var btnClass = editor.classes.form.button;
+ var container = $(
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ ''+
+ ''+
+ '
'+
+ '
'+
+ ''+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ '
'+
+ ''+
+ '
'+
+ '
'+
+ '
'+
+ '
'
+ );
+
+ conf._input = container;
+ conf._enabled = true;
+
+ _buttonText( conf );
+
+ if ( window.FileReader && conf.dragDrop !== false ) {
+ container.find('div.drop span').text(
+ conf.dragDropText || "Drag and drop a file here to upload"
+ );
+
+ var dragDrop = container.find('div.drop');
+ dragDrop
+ .on( 'drop', function (e) {
+ if ( conf._enabled ) {
+ Editor.upload( editor, conf, e.originalEvent.dataTransfer.files, _buttonText, dropCallback );
+ dragDrop.removeClass('over');
+ }
+ return false;
+ } )
+ .on( 'dragleave dragexit', function (e) {
+ if ( conf._enabled ) {
+ dragDrop.removeClass('over');
+ }
+ return false;
+ } )
+ .on( 'dragover', function (e) {
+ if ( conf._enabled ) {
+ dragDrop.addClass('over');
+ }
+ return false;
+ } );
+
+ // When an Editor is open with a file upload input there is a
+ // reasonable chance that the user will miss the drop point when
+ // dragging and dropping. Rather than loading the file in the browser,
+ // we want nothing to happen, otherwise the form will be lost.
+ editor
+ .on( 'open', function () {
+ $('body').on( 'dragover.DTE_Upload drop.DTE_Upload', function (e) {
+ return false;
+ } );
+ } )
+ .on( 'close', function () {
+ $('body').off( 'dragover.DTE_Upload drop.DTE_Upload' );
+ } );
+ }
+ else {
+ container.addClass( 'noDrop' );
+ container.append( container.find('div.rendered') );
+ }
+
+ container.find('div.clearValue button').on( 'click', function () {
+ Editor.fieldTypes.upload.set.call( editor, conf, '' );
+ } );
+
+ container.find('input[type=file]').on('change', function () {
+ Editor.upload( editor, conf, this.files, _buttonText, function (ids) {
+ dropCallback.call( editor, ids );
+
+ // Clear the value so change will happen on the next file select,
+ // even if it is the same file
+ container.find('input[type=file]').val('');
+ } );
+ } );
+
+ return container;
+}
+
+// Typically a change event caused by the end user will be added to a queue that
+// the browser will handle when no other script is running. However, using
+// `$().trigger()` will cause it to happen immediately, so in order to simulate
+// the standard browser behaviour we use setTimeout. This also means that
+// `dependent()` and other change event listeners will trigger when the field
+// values have all been set, rather than as they are being set - 31594
+function _triggerChange ( input ) {
+ setTimeout( function () {
+ input.trigger( 'change', {editor: true, editorSet: true} ); // editorSet legacy
+ }, 0 );
+}
+
+
+// A number of the fields in this file use the same get, set, enable and disable
+// methods (specifically the text based controls), so in order to reduce the code
+// size, we just define them once here in our own local base model for the field
+// types.
+var baseFieldType = $.extend( true, {}, Editor.models.fieldType, {
+ get: function ( conf ) {
+ return conf._input.val();
+ },
+
+ set: function ( conf, val ) {
+ conf._input.val( val );
+ _triggerChange( conf._input );
+ },
+
+ enable: function ( conf ) {
+ conf._input.prop( 'disabled', false );
+ },
+
+ disable: function ( conf ) {
+ conf._input.prop( 'disabled', true );
+ }
+} );
+
+
+
+fieldTypes.hidden = {
+ create: function ( conf ) {
+ conf._val = conf.value;
+ return null;
+ },
+
+ get: function ( conf ) {
+ return conf._val;
+ },
+
+ set: function ( conf, val ) {
+ conf._val = val;
+ }
+};
+
+
+fieldTypes.readonly = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( {
+ id: Editor.safeId( conf.id ),
+ type: 'text',
+ readonly: 'readonly'
+ }, conf.attr || {} ) );
+
+ return conf._input[0];
+ }
+} );
+
+
+fieldTypes.text = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( {
+ id: Editor.safeId( conf.id ),
+ type: 'text'
+ }, conf.attr || {} ) );
+
+ return conf._input[0];
+ }
+} );
+
+
+fieldTypes.password = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( {
+ id: Editor.safeId( conf.id ),
+ type: 'password'
+ }, conf.attr || {} ) );
+
+ return conf._input[0];
+ }
+} );
+
+fieldTypes.textarea = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( {
+ id: Editor.safeId( conf.id )
+ }, conf.attr || {} ) );
+ return conf._input[0];
+ }
+} );
+
+
+fieldTypes.select = $.extend( true, {}, baseFieldType, {
+ // Locally "private" function that can be reused for the create and update methods
+ _addOptions: function ( conf, opts ) {
+ var elOpts = conf._input[0].options;
+ var countOffset = 0;
+
+ elOpts.length = 0;
+
+ if ( conf.placeholder !== undefined ) {
+ countOffset += 1;
+ elOpts[0] = new Option( conf.placeholder, conf.placeholderValue !== undefined ?
+ conf.placeholderValue :
+ ''
+ );
+
+ var disabled = conf.placeholderDisabled !== undefined ?
+ conf.placeholderDisabled :
+ true;
+
+ elOpts[0].hidden = disabled; // can't be hidden if not disabled!
+ elOpts[0].disabled = disabled;
+ }
+
+ if ( opts ) {
+ Editor.pairs( opts, conf.optionsPair, function ( val, label, i ) {
+ elOpts[ i+countOffset ] = new Option( label, val );
+ elOpts[ i+countOffset ]._editor_val = val;
+ } );
+ }
+ },
+
+ create: function ( conf ) {
+ conf._input = $('')
+ .attr( $.extend( {
+ id: Editor.safeId( conf.id ),
+ multiple: conf.multiple === true
+ }, conf.attr || {} ) )
+ .on( 'change.dte', function (e, d) {
+ // On change, get the user selected value and store it as the
+ // last set, so `update` can reflect it. This way `_lastSet`
+ // always gives the intended value, be it set via the API or by
+ // the end user.
+ if ( ! d || ! d.editor ) {
+ conf._lastSet = fieldTypes.select.get( conf );
+ }
+ } );
+
+ fieldTypes.select._addOptions( conf, conf.options || conf.ipOpts );
+
+ return conf._input[0];
+ },
+
+ update: function ( conf, options ) {
+ fieldTypes.select._addOptions( conf, options );
+
+ // Attempt to set the last selected value (set by the API or the end
+ // user, they get equal priority)
+ var lastSet = conf._lastSet;
+
+ if ( lastSet !== undefined ) {
+ fieldTypes.select.set( conf, lastSet, true );
+ }
+
+ _triggerChange( conf._input );
+ },
+
+ get: function ( conf ) {
+ var val = conf._input.find('option:selected').map( function () {
+ return this._editor_val;
+ } ).toArray();
+
+ if ( conf.multiple ) {
+ return conf.separator ?
+ val.join( conf.separator ) :
+ val;
+ }
+
+ return val.length ? val[0] : null;
+ },
+
+ set: function ( conf, val, localUpdate ) {
+ if ( ! localUpdate ) {
+ conf._lastSet = val;
+ }
+
+ // Can't just use `$().val()` because it won't work with strong types
+ if ( conf.multiple && conf.separator && ! $.isArray( val ) ) {
+ val = val.split( conf.separator );
+ }
+ else if ( ! $.isArray( val ) ) {
+ val = [ val ];
+ }
+
+ var i, len=val.length, found, allFound = false;
+ var options = conf._input.find('option');
+
+ conf._input.find('option').each( function () {
+ found = false;
+
+ for ( i=0 ; i'+
+ ''+
+ ''+
+ ''
+ );
+ $('input:last', jqInput).attr('value', val)[0]._editor_val = val;
+ } );
+ }
+ },
+
+
+ create: function ( conf ) {
+ conf._input = $('');
+ fieldTypes.checkbox._addOptions( conf, conf.options || conf.ipOpts );
+
+ return conf._input[0];
+ },
+
+ get: function ( conf ) {
+ var out = [];
+ conf._input.find('input:checked').each( function () {
+ out.push( this._editor_val );
+ } );
+ return ! conf.separator ?
+ out :
+ out.length === 1 ?
+ out[0] :
+ out.join(conf.separator);
+ },
+
+ set: function ( conf, val ) {
+ var jqInputs = conf._input.find('input');
+ if ( ! $.isArray(val) && typeof val === 'string' ) {
+ val = val.split( conf.separator || '|' );
+ }
+ else if ( ! $.isArray(val) ) {
+ val = [ val ];
+ }
+
+ var i, len=val.length, found;
+
+ jqInputs.each( function () {
+ found = false;
+
+ for ( i=0 ; i'+
+ ''+
+ ''+
+ ''
+ );
+ $('input:last', jqInput).attr('value', val)[0]._editor_val = val;
+ } );
+ }
+ },
+
+
+ create: function ( conf ) {
+ conf._input = $('');
+ fieldTypes.radio._addOptions( conf, conf.options || conf.ipOpts );
+
+ // this is ugly, but IE6/7 has a problem with radio elements that are created
+ // and checked before being added to the DOM! Basically it doesn't check them. As
+ // such we use the _preChecked property to set cache the checked button and then
+ // check it again when the display is shown. This has no effect on other browsers
+ // other than to cook a few clock cycles.
+ this.on('open', function () {
+ conf._input.find('input').each( function () {
+ if ( this._preChecked ) {
+ this.checked = true;
+ }
+ } );
+ } );
+
+ return conf._input[0];
+ },
+
+ get: function ( conf ) {
+ var el = conf._input.find('input:checked');
+ return el.length ? el[0]._editor_val : undefined;
+ },
+
+ set: function ( conf, val ) {
+ var that = this;
+
+ conf._input.find('input').each( function () {
+ this._preChecked = false;
+
+ if ( this._editor_val == val ) {
+ this.checked = true;
+ this._preChecked = true;
+ }
+ else {
+ // In a detached DOM tree, there is no relationship between the
+ // input elements, so we need to uncheck any element that does
+ // not match the value
+ this.checked = false;
+ this._preChecked = false;
+ }
+ } );
+
+ _triggerChange( conf._input.find('input:checked') );
+ },
+
+ enable: function ( conf ) {
+ conf._input.find('input').prop('disabled', false);
+ },
+
+ disable: function ( conf ) {
+ conf._input.find('input').prop('disabled', true);
+ },
+
+ update: function ( conf, options ) {
+ var radio = fieldTypes.radio;
+ var currVal = radio.get( conf );
+
+ radio._addOptions( conf, options );
+
+ // Select the current value if it exists in the new data set, otherwise
+ // select the first radio input so there is always a value selected
+ var inputs = conf._input.find('input');
+ radio.set( conf, inputs.filter('[value="'+currVal+'"]').length ?
+ currVal :
+ inputs.eq(0).attr('value')
+ );
+ }
+} );
+
+
+fieldTypes.date = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( {
+ id: Editor.safeId( conf.id ),
+ type: 'text'
+ }, conf.attr ) );
+
+ if ( $.datepicker ) {
+ // jQuery UI date picker
+ conf._input.addClass( 'jqueryui' );
+
+ if ( ! conf.dateFormat ) {
+ conf.dateFormat = $.datepicker.RFC_2822;
+ }
+
+ if ( conf.dateImage === undefined ) {
+ conf.dateImage = "../../images/calender.png";
+ }
+
+ // Allow the element to be attached to the DOM
+ setTimeout( function () {
+ $( conf._input ).datepicker( $.extend( {
+ showOn: "both",
+ dateFormat: conf.dateFormat,
+ buttonImage: conf.dateImage,
+ buttonImageOnly: true
+ }, conf.opts ) );
+
+ $('#ui-datepicker-div').css('display','none');
+ }, 10 );
+ }
+ else {
+ // HTML5 (only Chrome and Edge on the desktop support this atm)
+ conf._input.attr( 'type', 'date' );
+ }
+
+ return conf._input[0];
+ },
+
+ // use default get method as will work for all
+
+ set: function ( conf, val ) {
+ if ( $.datepicker && conf._input.hasClass('hasDatepicker') ) {
+ // Due to the async init of the control it is possible that we might
+ // try to set a value before it has been initialised!
+ conf._input.datepicker( "setDate" , val ).change();
+ }
+ else {
+ $(conf._input).val( val );
+ }
+ },
+
+ enable: function ( conf ) {
+ $.datepicker ?
+ conf._input.datepicker( "enable" ) :
+ $(conf._input).prop( 'disabled', false );
+ },
+
+ disable: function ( conf ) {
+ $.datepicker ?
+ conf._input.datepicker( "disable" ) :
+ $(conf._input).prop( 'disabled', true );
+ },
+
+ owns: function ( conf, node ) {
+ return $(node).parents('div.ui-datepicker').length || $(node).parents('div.ui-datepicker-header').length ?
+ true :
+ false;
+ }
+} );
+
+
+fieldTypes.datetime = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ conf._input = $('').attr( $.extend( true, {
+ id: Editor.safeId( conf.id ),
+ type: 'text'
+ }, conf.attr ) );
+
+ conf._picker = new Editor.DateTime( conf._input, $.extend( {
+ format: conf.format, // can be undefined
+ i18n: this.i18n.datetime
+ }, conf.opts ) );
+
+ return conf._input[0];
+ },
+
+ // default get, disable and enable options are okay
+
+ set: function ( conf, val ) {
+ conf._picker.val( val );
+
+ _triggerChange( conf._input );
+ },
+
+ owns: function ( conf, node ) {
+ return conf._picker.owns( node );
+ },
+
+ destroy: function ( conf ) {
+ conf._picker.destroy();
+ },
+
+ minDate: function ( conf, min ) {
+ conf._picker.min( min );
+ },
+
+ maxDate: function ( conf, max ) {
+ conf._picker.max( max );
+ }
+} );
+
+
+fieldTypes.upload = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ var editor = this;
+ var container = _commonUpload( editor, conf, function ( val ) {
+ Editor.fieldTypes.upload.set.call( editor, conf, val[0] );
+ } );
+
+ return container;
+ },
+
+ get: function ( conf ) {
+ return conf._val;
+ },
+
+ set: function ( conf, val ) {
+ conf._val = val;
+
+ var container = conf._input;
+
+ if ( conf.display ) {
+ var rendered = container.find('div.rendered');
+
+ if ( conf._val ) {
+ rendered.html( conf.display( conf._val ) );
+ }
+ else {
+ rendered
+ .empty()
+ .append( ''+( conf.noFileText || 'No file' )+'' );
+ }
+ }
+
+ var button = container.find('div.clearValue button');
+ if ( val && conf.clearText ) {
+ button.html( conf.clearText );
+ container.removeClass( 'noClear' );
+ }
+ else {
+ container.addClass( 'noClear' );
+ }
+
+ conf._input.find('input').triggerHandler( 'upload.editor', [ conf._val ] );
+ },
+
+ enable: function ( conf ) {
+ conf._input.find('input').prop('disabled', false);
+ conf._enabled = true;
+ },
+
+ disable: function ( conf ) {
+ conf._input.find('input').prop('disabled', true);
+ conf._enabled = false;
+ }
+} );
+
+
+fieldTypes.uploadMany = $.extend( true, {}, baseFieldType, {
+ create: function ( conf ) {
+ var editor = this;
+ var container = _commonUpload( editor, conf, function ( val ) {
+ conf._val = conf._val.concat( val );
+ Editor.fieldTypes.uploadMany.set.call( editor, conf, conf._val );
+ } );
+
+ container
+ .addClass( 'multi' )
+ .on( 'click', 'button.remove', function (e) {
+ e.stopPropagation();
+
+ var idx = $(this).data('idx');
+
+ conf._val.splice( idx, 1 );
+ Editor.fieldTypes.uploadMany.set.call( editor, conf, conf._val );
+ } );
+
+ return container;
+ },
+
+ get: function ( conf ) {
+ return conf._val;
+ },
+
+ set: function ( conf, val ) {
+ // Default value for fields is an empty string, whereas we want []
+ if ( ! val ) {
+ val = [];
+ }
+
+ if ( ! $.isArray( val ) ) {
+ throw 'Upload collections must have an array as a value';
+ }
+
+ conf._val = val;
+
+ var that = this;
+ var container = conf._input;
+
+ if ( conf.display ) {
+ var rendered = container.find('div.rendered').empty();
+
+ if ( val.length ) {
+ var list = $('
').appendTo( rendered );
+
+ $.each( val, function ( i, file ) {
+ list.append(
+ '
');c&&(i.appendTo("body"),k.appendTo("body"));var c=i.children().eq(0),g=c.children(),u=g.children();c.append(this.dom.formError);g.prepend(this.dom.form);
+e.message&&c.prepend(this.dom.formInfo);e.title&&c.prepend(this.dom.header);e.buttons&&g.append(this.dom.buttons);var z=d().add(i).add(k);this._closeReg(function(){z.animate({opacity:0},function(){z.detach();d(j).off("resize."+f);l._clearDynamicInfo()})});k.click(function(){l.blur()});u.click(function(){l._close()});this.bubblePosition();z.animate({opacity:1});this._focus(this.s.includeFields,e.focus);this._postopen("bubble");return this};f.prototype.bubblePosition=function(){var a=d("div.DTE_Bubble"),
+b=d("div.DTE_Bubble_Liner"),c=this.s.bubbleNodes,e=0,l=0,k=0,f=0;d.each(c,function(a,b){var c=d(b).offset();e+=c.top;l+=c.left;k+=c.left+b.offsetWidth;f+=c.top+b.offsetHeight});var e=e/c.length,l=l/c.length,k=k/c.length,f=f/c.length,c=e,i=(l+k)/2,g=b.outerWidth(),u=i-g/2,g=u+g,h=d(j).width();a.css({top:c,left:i});b.length&&0>b.offset().top?a.css("top",f).addClass("below"):a.removeClass("below");g+15>h?b.css("left",15>u?-(u-15):-(g-h+15)):b.css("left",15>u?-(u-15):0);return this};f.prototype.buttons=
+function(a){var b=this;"_basic"===a?a=[{label:this.i18n[this.s.action].submit,fn:function(){this.submit()}}]:d.isArray(a)||(a=[a]);d(this.dom.buttons).empty();d.each(a,function(a,e){"string"===typeof e&&(e={label:e,fn:function(){this.submit()}});d("",{"class":b.classes.form.button+(e.className?" "+e.className:"")}).html("function"===typeof e.label?e.label(b):e.label||"").attr("tabindex",0).on("keyup",function(a){13===a.keyCode&&e.fn&&e.fn.call(b)}).on("keypress",function(a){13===a.keyCode&&
+a.preventDefault()}).on("click",function(a){a.preventDefault();e.fn&&e.fn.call(b)}).appendTo(b.dom.buttons)});return this};f.prototype.clear=function(a){var b=this,c=this.s.fields;"string"===typeof a?(c[a].destroy(),delete c[a],a=d.inArray(a,this.s.order),this.s.order.splice(a,1)):d.each(this._fieldNames(a),function(a,c){b.clear(c)});return this};f.prototype.close=function(){this._close(!1);return this};f.prototype.create=function(a,b,c,e){var l=this,k=this.s.fields,f=1;if(this._tidy(function(){l.create(a,
+b,c,e)}))return this;"number"===typeof a&&(f=a,a=b,b=c);this.s.editFields={};for(var i=0;i0)throw"Cannot edit more than one row inline at a time";k=d(b.attach[0]);g=0;d.each(b.displayFields,function(a,b){if(g>0)throw"Cannot edit more than one field inline at a time";f=b;g++});i++});if(d("div.DTE_Field",k).length||this._tidy(function(){e.inline(a,b,c)}))return this;this._edit(a,l,"inline");var z=this._formOptions(c);if(!this._preopen("inline"))return this;var O=k.contents().detach();k.append(d('
')[0],buttons:d('')[0]};if(d.fn.dataTable.TableTools){var e=d.fn.dataTable.TableTools.BUTTONS,l=this.i18n;d.each(["create","edit","remove"],function(a,b){e["editor_"+
+b].sButtonText=l[b].button})}d.each(a.events,function(a,c){b.on(a,function(){var a=Array.prototype.slice.call(arguments);a.shift();c.apply(b,a)})});var c=this.dom,k=c.wrapper;c.formContent=t("form_content",c.form)[0];c.footer=t("foot",k)[0];c.body=t("body",k)[0];c.bodyContent=t("body_content",k)[0];c.processing=t("processing",k)[0];a.fields&&this.add(a.fields);d(q).on("init.dt.dte",function(a,c){b.s.table&&c.nTable===d(b.s.table).get(0)&&(c._editor=b)}).on("xhr.dt",function(a,c,e){e&&(b.s.table&&
+c.nTable===d(b.s.table).get(0))&&b._optionsUpdate(e)});this.s.displayController=f.display[a.display].init(this);this._event("initComplete",[])};f.prototype._actionClass=function(){var a=this.classes.actions,b=this.s.action,c=d(this.dom.wrapper);c.removeClass([a.create,a.edit,a.remove].join(" "));"create"===b?c.addClass(a.create):"edit"===b?c.addClass(a.edit):"remove"===b&&c.addClass(a.remove)};f.prototype._ajax=function(a,b,c){var e={type:"POST",dataType:"json",data:null,error:c,success:function(a,
+c,e){204===e.status&&(a={});b(a)}},l;l=this.s.action;var f=this.s.ajax||this.s.ajaxUrl,g="edit"===l||"remove"===l?y(this.s.editFields,"idSrc"):null;d.isArray(g)&&(g=g.join(","));d.isPlainObject(f)&&f[l]&&(f=f[l]);if(d.isFunction(f)){var h=null,e=null;if(this.s.ajaxUrl){var J=this.s.ajaxUrl;J.create&&(h=J[l]);-1!==h.indexOf(" ")&&(l=h.split(" "),e=l[0],h=l[1]);h=h.replace(/_id_/,g)}f(e,h,a,b,c)}else"string"===typeof f?-1!==f.indexOf(" ")?(l=f.split(" "),e.type=l[0],e.url=l[1]):e.url=f:e=d.extend({},
+e,f||{}),e.url=e.url.replace(/_id_/,g),e.data&&(c=d.isFunction(e.data)?e.data(a):e.data,a=d.isFunction(e.data)&&c?c:d.extend(!0,a,c)),e.data=a,"DELETE"===e.type&&(a=d.param(e.data),e.url+=-1===e.url.indexOf("?")?"?"+a:"&"+a,delete e.data),d.ajax(e)};f.prototype._assembleMain=function(){var a=this.dom;d(a.wrapper).prepend(a.header);d(a.footer).append(a.formError).append(a.buttons);d(a.bodyContent).append(a.formInfo).append(a.form)};f.prototype._blur=function(){var a=this.s.editOpts;!1!==this._event("preBlur")&&
+("submit"===a.onBlur?this.submit():"close"===a.onBlur&&this._close())};f.prototype._clearDynamicInfo=function(){var a=this.classes.field.error,b=this.s.fields;d("div."+a,this.dom.wrapper).removeClass(a);d.each(b,function(a,b){b.error("").message("")});this.error("").message("")};f.prototype._close=function(a){!1!==this._event("preClose")&&(this.s.closeCb&&(this.s.closeCb(a),this.s.closeCb=null),this.s.closeIcb&&(this.s.closeIcb(),this.s.closeIcb=null),d("body").off("focus.editor-focus"),this.s.displayed=
+!1,this._event("close"))};f.prototype._closeReg=function(a){this.s.closeCb=a};f.prototype._crudArgs=function(a,b,c,e){var l=this,f,g,i;d.isPlainObject(a)||("boolean"===typeof a?(i=a,a=b):(f=a,g=b,i=c,a=e));i===h&&(i=!0);f&&l.title(f);g&&l.buttons(g);return{opts:d.extend({},this.s.formOptions.main,a),maybeOpen:function(){i&&l.open()}}};f.prototype._dataSource=function(a){var b=Array.prototype.slice.call(arguments);b.shift();var c=this.s.dataSource[a];if(c)return c.apply(this,b)};f.prototype._displayReorder=
+function(a){var b=d(this.dom.formContent),c=this.s.fields,e=this.s.order;a?this.s.includeFields=a:a=this.s.includeFields;b.children().detach();d.each(e,function(e,k){var g=k instanceof f.Field?k.name():k;-1!==d.inArray(g,a)&&b.append(c[g].node())});this._event("displayOrder",[this.s.displayed,this.s.action,b])};f.prototype._edit=function(a,b,c){var e=this.s.fields,l=[],f;this.s.editFields=b;this.s.modifier=a;this.s.action="edit";this.dom.form.style.display="block";this._actionClass();d.each(e,function(a,
+c){c.multiReset();f=!0;d.each(b,function(b,e){if(e.fields[a]){var d=c.valFromData(e.data);c.multiSet(b,d!==h?d:c.def());e.displayFields&&!e.displayFields[a]&&(f=!1)}});0!==c.multiIds().length&&f&&l.push(a)});for(var e=this.order().slice(),g=e.length;0<=g;g--)-1===d.inArray(e[g],l)&&e.splice(g,1);this._displayReorder(e);this.s.editData=d.extend(!0,{},this.multiGet());this._event("initEdit",[y(b,"node")[0],y(b,"data")[0],a,c]);this._event("initMultiEdit",[b,a,c])};f.prototype._event=function(a,b){b||
+(b=[]);if(d.isArray(a))for(var c=0,e=a.length;cMore information).'},
+multi:{title:"Multiple values",info:"The selected items contain different values for this input. To edit and set all items for this input to the same value, click or tap here, otherwise they will retain their individual values.",restore:"Undo changes"},datetime:{previous:"Previous",next:"Next",months:"January February March April May June July August September October November December".split(" "),weekdays:"Sun Mon Tue Wed Thu Fri Sat".split(" "),amPm:["am","pm"],unknown:"-"}},formOptions:{bubble:d.extend({},
+f.models.formOptions,{title:!1,message:!1,buttons:"_basic",submit:"changed"}),inline:d.extend({},f.models.formOptions,{buttons:!1,submit:"changed"}),main:d.extend({},f.models.formOptions)},legacyAjax:!1};var K=function(a,b,c){d.each(b,function(b,d){var f=d.valFromData(c);f!==h&&C(a,d.dataSrc()).each(function(){for(;this.childNodes.length;)this.removeChild(this.firstChild)}).html(f)})},C=function(a,b){var c="keyless"===a?q:d('[data-editor-id="'+a+'"]');return d('[data-editor-field="'+b+'"]',c)},D=
+f.dataSources={},E=function(a,b){return a.settings()[0].oFeatures.bServerSide&&"none"!==b.s.editOpts.drawType},L=function(a){a=d(a);setTimeout(function(){a.addClass("highlight");setTimeout(function(){a.addClass("noHighlight").removeClass("highlight");setTimeout(function(){a.removeClass("noHighlight")},550)},500)},20)},F=function(a,b,c,e,d){b.rows(c).indexes().each(function(c){var c=b.row(c),g=c.data(),i=d(g);i===h&&f.error("Unable to find row identifier",14);a[i]={idSrc:i,data:g,node:c.node(),fields:e,
+type:"row"}})},G=function(a,b,c,e,l,g){b.cells(c).indexes().each(function(w){var i=b.cell(w),j=b.row(w.row).data(),j=l(j),u;if(!(u=g)){u=w.column;u=b.settings()[0].aoColumns[u];var m=u.editField!==h?u.editField:u.mData,n={};d.each(e,function(a,b){if(d.isArray(m))for(var c=0;c