/*! DataTables Editor v1.9.2 * * ©2012-2019 SpryMedia Ltd, all rights reserved. * License: editor.datatables.net/license */ /** * @summary DataTables Editor * @description Table editing library for DataTables * @version 1.9.2 * @file dataTables.editor.js * @author SpryMedia Ltd * @contact www.datatables.net/contact */ /*jslint evil: true, undef: true, browser: true */ /*globals jQuery,alert,console */ (function( factory ){ if ( typeof define === 'function' && define.amd ) { // AMD // @jscrambler disable * define( ['jquery', 'datatables.net'], function ( $ ) { return factory( $, window, document ); } ); } else if ( typeof exports === 'object' ) { // CommonJS // @jscrambler disable * module.exports = function (root, $) { if ( ! root ) { root = window; } if ( ! $ || ! $.fn.dataTable ) { $ = require('datatables.net')(root, $).$; } return factory( $, root, root.document ); }; } else { // Browser factory( jQuery, window, document ); } }(function( $, window, document, undefined ) { 'use strict'; var DataTable = $.fn.dataTable; if ( ! DataTable || ! DataTable.versionCheck || ! DataTable.versionCheck('1.10.7') ) { throw new Error('Editor requires DataTables 1.10.7 or newer'); } /** * Editor is a plug-in for DataTables which * provides an interface for creating, reading, editing and deleting and entries * (a CRUD interface) in a DataTable. The documentation presented here is * primarily focused on presenting the API for Editor. For a full list of * features, examples and the server interface protocol, please refer to the Editor web-site. * * Note that in this documentation, for brevity, the `DataTable` refers to the * jQuery parameter `jQuery.fn.dataTable` through which it may be accessed. * Therefore, when creating a new Editor instance, use `jQuery.fn.Editor` as * shown in the examples below. * * @class * @param {object} [oInit={}] Configuration object for Editor. Options * are defined by {@link Editor.defaults}. * @requires jQuery 1.7+ * @requires DataTables 1.10+ */ var Editor = function ( opts ) { if ( ! (this instanceof Editor) ) { alert( "DataTables Editor must be initialised as a 'new' instance'" ); } this._constructor( opts ); }; // Export Editor as a DataTables property DataTable.Editor = Editor; $.fn.DataTable.Editor = Editor; // Internal methods /** * Get an Editor node based on the data-dte-e (element) attribute and return it * as a jQuery object. * @param {string} dis The data-dte-e attribute name to match for the element * @param {node} [ctx=document] The context for the search - recommended this * parameter is included for performance. * @returns {jQuery} jQuery object of found node(s). * @private */ var _editor_el = function ( dis, ctx ) { if ( ctx === undefined ) { ctx = document; } return $('*[data-dte-e="'+dis+'"]', ctx); }; /** @internal Counter for unique event namespaces in the inline control */ var __inlineCounter = 0; var _pluck = function ( a, prop ) { var out = []; $.each( a, function ( idx, el ) { out.push( el[ prop ] ); } ); return out; }; // The file and file methods are common on both the DataTables and Editor APIs // so rather than writing the same methods twice, they are defined once here and // assigned as required. var _api_file = function ( name, id ) { var table = this.files( name ); // can throw. `this` will be Editor or var file = table[ id ]; // DataTables.Api context. Both work. if ( ! file ) { throw 'Unknown file id '+ id +' in table '+ name; } return table[ id ]; }; var _api_files = function ( name ) { if ( ! name ) { return Editor.files; } var table = Editor.files[ name ]; if ( ! table ) { throw 'Unknown file table name: '+ name; } return table; }; /** * Get the keys of an object / array * * @param {object} o Object to get the keys of * @return {array} Keys */ var _objectKeys = function ( o ) { var out = []; for ( var key in o ) { if ( o.hasOwnProperty( key ) ) { out.push( key ); } } return out; }; /** * Compare parameters for difference - diving into arrays and objects if * needed, allowing the object reference to be different, but the contents to * match. * * Please note that LOOSE type checking is used * * @param {*} o1 Object to compare * @param {*} o2 Object to compare * @return {boolean} `true` if matching, `false` otherwise */ var _deepCompare = function (o1, o2) { if ( typeof o1 !== 'object' || typeof o2 !== 'object' ) { return o1 == o2; } var o1Props = _objectKeys( o1 ); var o2Props = _objectKeys( o2 ); if (o1Props.length !== o2Props.length) { return false; } for ( var i=0, ien=o1Props.length ; i'+ ''+ '
'+ // Field specific HTML is added here if there is any '
'+ '
'+ multiI18n.title+ ''+ multiI18n.info+ ''+ '
'+ '
'+ multiI18n.restore+ '
'+ '
'+ '
'+opts.message+'
'+ '
'+opts.fieldInfo+'
'+ '
'+ '
'+ '
'); 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), processing: _editor_el('field-processing', template) } ); // On click - set a common value for the field this.dom.multi.on( 'click', function () { if ( that.s.opts.multiEditable && ! template.hasClass( classes.disabled ) && opts.type !== 'readonly' ) { that.val(''); that.focus(); } } ); this.dom.multiReturn.on( 'click', function () { that.multiRestore(); } ); // 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 typeof def === 'function' ? def() : def; } opts.def = set; return this; }, disable: function () { this.dom.container.addClass( this.s.classes.disabled ); 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.dom.container.removeClass( this.s.classes.disabled ); this._typeFn( 'enable' ); return this; }, enabled: function () { return this.dom.container.hasClass( this.s.classes.disabled ) === false; }, 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 ); } this._typeFn( 'errorMessage', msg ); return this._msg( this.dom.fieldError, msg, fn ); }, fieldInfo: function ( msg ) { return this._msg( this.dom.fieldInfo, msg ); }, isMultiValue: function () { return this.s.multiValue && this.s.multiIds.length !== 1; }, 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 && $.fn.slideUp ) { el.slideUp(); } else { el.css( 'display', 'none' ); } return this; }, label: function ( str ) { var label = this.dom.label; var labelInfo = this.dom.labelInfo.detach(); if ( str === undefined ) { return label.html(); } label.html( str ); label.append( labelInfo ); return this; }, labelInfo: function ( msg ) { return this._msg( this.dom.labelInfo, msg ); }, 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; var isMultiValue = this.isMultiValue(); if ( id === undefined ) { var fieldVal = this.val(); // 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, '\'') .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 && ! _deepCompare( val, last ) ) { different = true; break; } last = val; } } if ( (different && isMultiValue) || (!isMultiEditable && this.isMultiValue()) ) { // Different values or same values, but not multiple editable 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 ( isMultiValue && ! different ) { this.set( last, false ); } } this.dom.multiReturn.css( { display: ids && ids.length > 1 && different && ! isMultiValue ? 'block' : 'none' } ); // Update information label var i18n = this.s.host.i18n.multi; this.dom.multiInfo.html( isMultiEditable ? i18n.info : i18n.noMulti); this.dom.multi.toggleClass( this.s.classes.multiNoEdit, ! isMultiEditable ); 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", /** * Information message for the field - expected to be dynamic * @type string * @default Empty string */ "message": "", /** * Allow a field to be editable when multiple rows are selected * @type boolean * @default true */ "multiEditable": true, /** * Indicate if the field's value can be submitted * @type boolean * @default true */ "submit": true }; /** * * @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 or not and what type of display * @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, /** * Unique instance counter to be able to remove events */ "unique": 0 }; /** * 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', /** * Action to take when a field error is detected in the returned JSON - can * be `focus` or `none` * * @type string */ onFieldError: 'focus', /** * 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, /** * Editing scope. Can be `row` or `cell`. * * @type string */ scope: 'row' }; /* * 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() { 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(); self._dte._animate( dom.wrapper, { opacity: 1, top: 0 }, callback ); self._dte._animate( dom.background, { opacity: 1 } ); // Terrible Chrome workaround. Since m53 the footer would be incorrectly // offset. This triggers a rerender. See thread 38145 setTimeout( function () { $('div.DTE_Footer').css( 'text-indent', -1 ); }, 10 ); // 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 ); self._dte._animate( dom.wrapper, { opacity: 0, top: self.conf.offsetAni }, function () { $(this).detach(); callback(); } ); self._dte._animate( dom.background, { 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 }; }()); (function() { 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]; // 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 () {}; } document.body.appendChild( self._dom.background ); document.body.appendChild( self._dom.wrapper ); // 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', function () { $(this).remove(); 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 = new $.fn.dataTable.Api(self._dte.s.table); // 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 }; }()); /* * 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 ) ) { // Do it in reverse to allow fields to appear in the same order given, otherwise, // the would appear in reverse if given an `after` if ( after !== undefined ) { cfg.reverse(); } for ( var i=0 ; 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( that.dom.formError ); table.prepend( that.dom.form ); if ( opts.message ) { liner.prepend( that.dom.formInfo ); } if ( opts.title ) { liner.prepend( that.dom.header ); } if ( opts.buttons ) { table.append( that.dom.buttons ); } var pair = $().add( container ).add( background ); that._closeReg( function ( submitComplete ) { that._animate( pair, { opacity: 0 }, function () { if (this === container[0]) { pair.detach(); $(window).off( 'resize.'+namespace ); // Clear error messages "offline" that._clearDynamicInfo(); that._event( 'closed', ['bubble'] ); } } ); } ); // Close event handlers background.click( function () { that.blur(); } ); close.click( function () { that._close(); } ); that.bubblePosition(); that._animate( pair, { opacity: 1 } ); that._focus( that.s.includeFields, opts.focus ); that._postopen( 'bubble', true ); } ); 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(); node = $(node).get(0); 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.text 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.action] 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 = [ { text: this.i18n[ this.s.action ].submit, action: 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 = { text: btn, action: function () { this.submit(); } }; } var text = btn.text || btn.label; var action = btn.action || btn.fn; $( ''+ '
'+ '
'+ ''+ '
'+ '
'+ ''+ ''+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
' ); this.dom = { container: structure, date: structure.find( '.'+classPrefix+'-date' ), title: structure.find( '.'+classPrefix+'-title' ), calendar: structure.find( '.'+classPrefix+'-calendar' ), time: structure.find( '.'+classPrefix+'-time' ), error: structure.find( '.'+classPrefix+'-error' ), input: $(input) }; this.s = { /** @type {Date} Date value that the picker has currently selected */ d: null, /** @type {Date} Date of the calendar - might not match the value */ display: null, /** @type {number} Used to select minutes in a range where the range base is itself unavailable */ minutesRange: null, /** @type {number} Used to select minutes in a range where the range base is itself unavailable */ secondsRange: 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]|L(?!T)|l/ ) !== null, time: this.c.format.match( /[Hhm]|LT|LTS/ ) !== 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 ) .append( this.dom.error ); 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'); }, errorMsg: function ( msg ) { var error = this.dom.error; if ( msg ) { error.html( msg ); } else { error.empty(); } }, hide: function () { this._hide(); }, 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 ( set === '--now' ) { this.s.d = new Date(); } 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() ); // Set the day of the month to be 1 so changing between months doesn't // run into issues when going from day 31 to 28 (for example) this.s.display.setUTCDate( 1 ); // 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 onChange = function () { that.c.onChange.call( that, that.dom.input.val(), that.s.d, that.dom.input ); }; 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.'+classPrefix+'-seconds').remove(); this.dom.time.children('span').eq(1).remove(); } // Render the options this._optionsTitle(); // Trigger the display of the widget when clicking or focusing on the // input element this.dom.input .attr('autocomplete', 'off') .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 calendar'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 ); onChange(); } else if ( select.hasClass(classPrefix+'-minutes') ) { // Minutes select that.s.d.setUTCMinutes( val ); that._setTime(); that._writeOutput( true ); onChange(); } else if ( select.hasClass(classPrefix+'-seconds') ) { // Seconds select that.s.d.setSeconds( val ); that._setTime(); that._writeOutput( true ); onChange(); } that.dom.input.focus(); that._position(); } ) .on( 'click', function (e) { var d = that.s.d; var nodeName = e.target.nodeName.toLowerCase(); var target = nodeName === 'span' ? e.target.parentNode : e.target; nodeName = target.nodeName.toLowerCase(); if ( nodeName === 'select' ) { return; } e.stopPropagation(); if ( nodeName === 'button' ) { var button = $(target); var parent = button.parent(); if ( parent.hasClass('disabled') && ! parent.hasClass('range') ) { button.blur(); 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 ( button.parents('.'+classPrefix+'-time').length ) { var val = button.data('value'); var unit = button.data('unit'); if ( unit === 'minutes' ) { if ( parent.hasClass('disabled') && parent.hasClass('range') ) { that.s.minutesRange = val; that._setTime(); return; } else { that.s.minutesRange = null; } } if ( unit === 'seconds' ) { if ( parent.hasClass('disabled') && parent.hasClass('range') ) { that.s.secondsRange = val; that._setTime(); return; } else { that.s.secondsRange = null; } } // Specific to hours for 12h clock if ( val === 'am' ) { if ( d.getUTCHours() >= 12 ) { val = d.getUTCHours() - 12; } else { return; } } else if ( val === 'pm' ) { if ( d.getUTCHours() < 12 ) { val = d.getUTCHours() + 12; } else { return; } } var set = unit === 'hours' ? 'setUTCHours' : unit === 'minutes' ? 'setUTCMinutes' : 'setSeconds'; d[set]( val ); that._setTime(); that._writeOutput( true ); onChange(); } else { // Calendar click if ( ! d ) { d = that._dateToUtc( new Date() ); } // Can't be certain that the current day will exist in // the new month, and likewise don't know that the // new day will exist in the old month, But 1 always // does, so we can change the month without worry of a // recalculation being done automatically by `Date` d.setUTCDate( 1 ); d.setUTCFullYear( button.data('year') ); d.setUTCMonth( button.data('month') ); d.setUTCDate( button.data('day') ); that._writeOutput( true ); // Don't hide if there is a time picker, since we want to // be able to select a time as well. if ( ! that.s.parts.time ) { // 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 // calendar opening immediately setTimeout( function () { that._hide(); }, 10 ); } else { that._setCalander(); } onChange(); } } 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.dataTables_scrollBody').off( 'scroll.'+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 calendar - 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 ''; } var classes = [ 'selectable' ]; var classPrefix = this.c.classPrefix; if ( day.disabled ) { classes.push( 'disabled' ); } if ( day.today ) { classes.push( 'now' ); } if ( day.selected ) { classes.push( 'selected' ); } return '' + '' + ''; }, /** * Create the HTML for a month to be displayed in the calendar table. * * Based upon the logic used in Pikaday - MIT licensed * Copyright (c) 2014 David Bushell * https://github.com/dbushell/Pikaday * * @param {integer} year Year * @param {integer} month Month (starting at 0) * @return {string} Calendar month HTML * @private */ _htmlMonth: function ( year, month ) { var now = this._dateToUtc( new Date() ), days = this._daysInMonth( year, month ), before = new Date( Date.UTC(year, month, 1) ).getUTCDay(), data = [], row = []; if ( this.c.firstDay > 0 ) { before -= this.c.firstDay; if (before < 0) { before += 7; } } var cells = days + before, after = cells; while ( after > 7 ) { after -= 7; } cells += 7 - after; var minDate = this.c.minDate; var maxDate = this.c.maxDate; if ( minDate ) { minDate.setUTCHours(0); minDate.setUTCMinutes(0); minDate.setSeconds(0); } if ( maxDate ) { maxDate.setUTCHours(23); maxDate.setUTCMinutes(59); maxDate.setSeconds(59); } for ( var i=0, r=0 ; i= (days + before), disabled = (minDate && day < minDate) || (maxDate && day > maxDate); var disableDays = this.c.disableDays; if ( $.isArray( disableDays ) && $.inArray( day.getUTCDay(), disableDays ) !== -1 ) { disabled = true; } else if ( typeof disableDays === 'function' && disableDays( day ) === true ) { disabled = true; } var dayConfig = { day: 1 + (i - before), month: month, year: year, selected: selected, today: today, disabled: disabled, empty: empty }; row.push( this._htmlDay(dayConfig) ); if ( ++r === 7 ) { if ( this.c.showWeekNumber ) { row.unshift( this._htmlWeekOfYear(i - before, month, year) ); } data.push( ''+row.join('')+'' ); row = []; r = 0; } } var classPrefix = this.c.classPrefix; var className = classPrefix+'-table'; if ( this.c.showWeekNumber ) { className += ' weekNumber'; } // Show / hide month icons based on min/max if ( minDate ) { var underMin = minDate >= new Date( Date.UTC(year, month, 1, 0, 0, 0 ) ); this.dom.title.find('div.'+classPrefix+'-iconLeft') .css( 'display', underMin ? 'none' : 'block' ); } if ( maxDate ) { var overMax = maxDate < new Date( Date.UTC(year, month+1, 1, 0, 0, 0 ) ); this.dom.title.find('div.'+classPrefix+'-iconRight') .css( 'display', overMax ? 'none' : 'block' ); } return '' + ''+ this._htmlMonthHead() + ''+ ''+ data.join('') + ''+ '
'; }, /** * Create the calendar 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 - ISO8601 * * Based on https://stackoverflow.com/questions/6117814/ and * http://techblog.procurios.nl/k/n618/news/view/33796/14863/ * * @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 date = new Date( y, m, d, 0, 0, 0, 0 ); // First week of the year always has 4th January in it date.setDate( date.getDate() + 4 - (date.getDay() || 7) ); var oneJan = new Date( y, 0, 1 ); var weekNum = Math.ceil( ( ( (date - oneJan) / 86400000) + 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 options list. * * @param {string} unit Time unit - hours, minutes or seconds * @param {integer} count Count range - 12, 24 or 60 * @param {integer} val Existing value for this unit * @param {integer[]} allowed Values allow for selection * @param {integer} range Override range * @private */ _optionsTime: function ( unit, count, val, allowed, range ) { var classPrefix = this.c.classPrefix; var container = this.dom.container.find('div.'+classPrefix+'-'+unit); var i, j; var render = count === 12 ? function (i) { return i; } : this._pad; var classPrefix = this.c.classPrefix; var className = classPrefix+'-table'; var i18n = this.c.i18n; if ( ! container.length ) { return; } var a = ''; var span = 10; var button = function (value, label, className) { // Shift the value for PM if ( count === 12 && val >= 12 && typeof value === 'number' ) { value += 12; } var selected = val === value || (value === 'am' && val < 12) || (value === 'pm' && val >= 12) ? 'selected' : ''; if (allowed && $.inArray(value, allowed) === -1) { selected += ' disabled'; } if ( className ) { selected += ' '+className; } return '' + '' + ''; } if ( count === 12 ) { // Hours with AM/PM a += ''; for ( i=1 ; i<=6 ; i++ ) { a += button(i, render(i)); } a += button('am', i18n.amPm[0]); a += ''; a += ''; for ( i=7 ; i<=12 ; i++ ) { a += button(i, render(i)); } a += button('pm', i18n.amPm[1]); a += ''; span = 7; } else if ( count === 24 ) { // Hours - 24 var c = 0; for (j=0 ; j<4 ; j++ ) { a += ''; for ( i=0 ; i<6 ; i++ ) { a += button(c, render(c)); c++; } a += ''; } span = 6; } else { // Minutes and seconds a += ''; for (j=0 ; j<60 ; j+=10 ) { a += button(j, render(j), 'range'); } a += ''; // Slight hack to allow for the different number of columns a += ''; var start = range !== null ? range : Math.floor( val / 10 )*10; a += ''; for (j=start+1 ; j'+ ''+ ''+ a+ ''+ '
'+ i18n[unit] + '
' ); }, /** * Create the options for the month and year * * @param {integer} year Year * @param {integer} month Month (starting at 0) * @private */ _optionsTitle: function () { 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 calendar 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(); if ( this.s.parts.date && this.s.parts.time && $(window).width() > 550 ) { container.addClass('horizontal'); } else { container.removeClass('horizontal'); } container .css( { top: offset.top + inputHeight, left: offset.left } ) .appendTo( 'body' ); var calHeight = container.outerHeight(); var calWidth = container.outerWidth(); var scrollTop = $(window).scrollTop(); // Correct to the bottom if ( offset.top + inputHeight + calHeight - scrollTop > $(window).height() ) { var newTop = offset.top - calHeight; container.css( 'top', newTop < 0 ? 0 : newTop ); } // Correct to the right if ( calWidth + offset.left > $(window).width() ) { var newLeft = $(window).width() - calWidth; container.css( 'left', newLeft < 0 ? 0 : newLeft ); } }, /** * Create a simple array with a range of values * * @param {integer} start Start value (inclusive) * @param {integer} end End value (inclusive) * @param {integer} [inc=1] Increment value * @return {array} Created array * @private */ _range: function ( start, end, inc ) { var a = []; if ( ! inc ) { inc = 1; } for ( var i=start ; i<=end ; i+=inc ) { a.push( i ); } return a; }, /** * Redraw the calendar based on the display date - this is a destructive * operation * * @private */ _setCalander: function () { if ( this.s.display ) { this.dom.calendar .empty() .append( this._htmlMonth( this.s.display.getUTCFullYear(), this.s.display.getUTCMonth() ) ); } }, /** * Set the month and year for the calendar 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 that = this; var d = this.s.d; var hours = d ? d.getUTCHours() : 0; var allowed = function ( prop ) { // Backwards compt with `Increment` option return that.c[prop+'Available'] ? that.c[prop+'Available'] : that._range( 0, 59, that.c[prop+'Increment'] ); } this._optionsTime( 'hours', this.s.parts.hours12 ? 12 : 24, hours, this.c.hoursAvailable ) this._optionsTime( 'minutes', 60, d ? d.getUTCMinutes() : 0, allowed('minutes'), this.s.minutesRange ); this._optionsTime( 'seconds', 60, d ? d.getSeconds() : 0, allowed('seconds'), this.s.secondsRange ); }, /** * 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._hide(); } ); $('div.DTE_Body_Content').on( 'scroll.'+namespace, function () { that._hide(); } ); $('div.dataTables_scrollBody').on( 'scroll.'+namespace, function () { that._hide(); } ); // 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', hoursAvailable: null, // Not documented as i18n is done by the Editor.defaults.i18n obj i18n: Editor.defaults.i18n.datetime, maxDate: null, minDate: null, minutesAvailable: null, minutesIncrement: 1, // deprecated momentStrict: true, momentLocale: 'en', onChange: function () {}, secondsAvailable: null, secondsIncrement: 1, // deprecated // 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, multiple ) { var btnClass = editor.classes.form.buttonInternal; var container = $( '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
' ); conf._input = container; conf._enabled = true; if ( conf.id ) { container.find('input[type=file]').attr('id', Editor.safeId( conf.id )); } if ( conf.attr ) { container.find('input[type=file]').attr(conf.attr); } _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 (e) { e.preventDefault(); if ( conf._enabled ) { 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 ); }, canReturnSubmit: function ( conf, node ) { return 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 = $('