comparison js/Editor-1.5.6/js/dataTables.editor.js @ 2:a64ece32a01a draft default tip

"planemo upload for repository https://github.com/ImmPortDB/immport-galaxy-tools/tree/master/flowtools/cs_overview commit a46097db0b6056e1125237393eb6974cfd51eb41"
author azomics
date Tue, 28 Jul 2020 08:32:36 -0400
parents
children
comparison
equal deleted inserted replaced
1:bca68066a957 2:a64ece32a01a
1 /*! DataTables Editor v1.5.6
2 *
3 * ©2012-2016 SpryMedia Ltd, all rights reserved.
4 * License: editor.datatables.net/license
5 */
6
7 /**
8 * @summary DataTables Editor
9 * @description Table editing library for DataTables
10 * @version 1.5.6
11 * @file dataTables.editor.js
12 * @author SpryMedia Ltd
13 * @contact www.datatables.net/contact
14 */
15
16 /*jslint evil: true, undef: true, browser: true */
17 /*globals jQuery,alert,console */
18
19 (function( factory ){
20 if ( typeof define === 'function' && define.amd ) {
21 // AMD
22 define( ['jquery', 'datatables.net'], function ( $ ) {
23 return factory( $, window, document );
24 } );
25 }
26 else if ( typeof exports === 'object' ) {
27 // CommonJS
28 module.exports = function (root, $) {
29 if ( ! root ) {
30 root = window;
31 }
32
33 if ( ! $ || ! $.fn.dataTable ) {
34 $ = require('datatables.net')(root, $).$;
35 }
36
37 return factory( $, root, root.document );
38 };
39 }
40 else {
41 // Browser
42 factory( jQuery, window, document );
43 }
44 }(function( $, window, document, undefined ) {
45 'use strict';
46 var DataTable = $.fn.dataTable;
47
48
49 if ( ! DataTable || ! DataTable.versionCheck || ! DataTable.versionCheck('1.10.7') ) {
50 throw 'Editor requires DataTables 1.10.7 or newer';
51 }
52
53 /**
54 * Editor is a plug-in for <a href="http://datatables.net">DataTables</a> which
55 * provides an interface for creating, reading, editing and deleting and entries
56 * (a CRUD interface) in a DataTable. The documentation presented here is
57 * primarily focused on presenting the API for Editor. For a full list of
58 * features, examples and the server interface protocol, please refer to the <a
59 * href="http://editor.datatables.net">Editor web-site</a>.
60 *
61 * Note that in this documentation, for brevity, the `DataTable` refers to the
62 * jQuery parameter `jQuery.fn.dataTable` through which it may be accessed.
63 * Therefore, when creating a new Editor instance, use `jQuery.fn.Editor` as
64 * shown in the examples below.
65 *
66 * @class
67 * @param {object} [oInit={}] Configuration object for Editor. Options
68 * are defined by {@link Editor.defaults}.
69 * @requires jQuery 1.7+
70 * @requires DataTables 1.10+
71 */
72 var Editor = function ( opts )
73 {
74 if ( ! this instanceof Editor ) {
75 alert( "DataTables Editor must be initialised as a 'new' instance'" );
76 }
77
78 this._constructor( opts );
79 };
80
81 // Export Editor as a DataTables property
82 DataTable.Editor = Editor;
83 $.fn.DataTable.Editor = Editor;
84
85 // Internal methods
86
87
88 /**
89 * Get an Editor node based on the data-dte-e (element) attribute and return it
90 * as a jQuery object.
91 * @param {string} dis The data-dte-e attribute name to match for the element
92 * @param {node} [ctx=document] The context for the search - recommended this
93 * parameter is included for performance.
94 * @returns {jQuery} jQuery object of found node(s).
95 * @private
96 */
97 var _editor_el = function ( dis, ctx )
98 {
99 if ( ctx === undefined ) {
100 ctx = document;
101 }
102
103 return $('*[data-dte-e="'+dis+'"]', ctx);
104 };
105
106
107 /** @internal Counter for unique event namespaces in the inline control */
108 var __inlineCounter = 0;
109
110 var _pluck = function ( a, prop )
111 {
112 var out = [];
113
114 $.each( a, function ( idx, el ) {
115 out.push( el[ prop ] );
116 } );
117
118 return out;
119 };
120
121 // Field class
122
123
124 Editor.Field = function ( opts, classes, host ) {
125 var that = this;
126 var multiI18n = host.i18n.multi;
127
128 opts = $.extend( true, {}, Editor.Field.defaults, opts );
129
130 if ( ! Editor.fieldTypes[ opts.type ] ) {
131 throw "Error adding field - unknown field type "+opts.type;
132 }
133
134 this.s = $.extend( {}, Editor.Field.settings, { // has to be a shallow copy!
135 type: Editor.fieldTypes[ opts.type ],
136 name: opts.name,
137 classes: classes,
138 host: host,
139 opts: opts,
140 multiValue: false
141 } );
142
143 // No id, so assign one to have the label reference work
144 if ( ! opts.id ) {
145 opts.id = 'DTE_Field_'+opts.name;
146 }
147
148 // Backwards compatibility
149 if ( opts.dataProp ) {
150 opts.data = opts.dataProp;
151 }
152
153 // If no `data` option is given, then we use the name from the field as the
154 // data prop to read data for the field from DataTables
155 if ( opts.data === '' ) {
156 opts.data = opts.name;
157 }
158
159 // Get and set functions in the data object for the record
160 var dtPrivateApi = DataTable.ext.oApi;
161 this.valFromData = function ( d ) { // get val from data
162 // wrapper to automatically pass `editor` as the type
163 return dtPrivateApi._fnGetObjectDataFn( opts.data )( d, 'editor' );
164 };
165 this.valToData = dtPrivateApi._fnSetObjectDataFn( opts.data ); // set val to data
166
167 // Field HTML structure
168 var template = $(
169 '<div class="'+classes.wrapper+' '+classes.typePrefix+opts.type+' '+classes.namePrefix+opts.name+' '+opts.className+'">'+
170 '<label data-dte-e="label" class="'+classes.label+'" for="'+opts.id+'">'+
171 opts.label+
172 '<div data-dte-e="msg-label" class="'+classes['msg-label']+'">'+opts.labelInfo+'</div>'+
173 '</label>'+
174 '<div data-dte-e="input" class="'+classes.input+'">'+
175 // Field specific HTML is added here if there is any
176 '<div data-dte-e="input-control" class="'+classes.inputControl+'"/>'+
177 '<div data-dte-e="multi-value" class="'+classes.multiValue+'">'+
178 multiI18n.title+
179 '<span data-dte-e="multi-info" class="'+classes.multiInfo+'">'+
180 multiI18n.info+
181 '</span>'+
182 '</div>'+
183 '<div data-dte-e="msg-multi" class="'+classes.multiRestore+'">'+
184 multiI18n.restore+
185 '</div>'+
186 '<div data-dte-e="msg-error" class="'+classes['msg-error']+'"></div>'+
187 '<div data-dte-e="msg-message" class="'+classes['msg-message']+'"></div>'+
188 '<div data-dte-e="msg-info" class="'+classes['msg-info']+'">'+opts.fieldInfo+'</div>'+
189 '</div>'+
190 '</div>');
191
192 var input = this._typeFn( 'create', opts );
193 if ( input !== null ) {
194 _editor_el('input-control', template).prepend( input );
195 }
196 else {
197 template.css('display', "none");
198 }
199
200 this.dom = $.extend( true, {}, Editor.Field.models.dom, {
201 container: template,
202 inputControl: _editor_el('input-control', template),
203 label: _editor_el('label', template),
204 fieldInfo: _editor_el('msg-info', template),
205 labelInfo: _editor_el('msg-label', template),
206 fieldError: _editor_el('msg-error', template),
207 fieldMessage: _editor_el('msg-message', template),
208 multi: _editor_el('multi-value', template),
209 multiReturn: _editor_el('msg-multi', template),
210 multiInfo: _editor_el('multi-info', template)
211 } );
212
213 // On click - set a common value for the field
214 this.dom.multi.on( 'click', function () {
215 that.val('');
216 } );
217
218 this.dom.multiReturn.on( 'click', function () {
219 that.s.multiValue = true;
220 that._multiValueCheck();
221 } );
222
223 // Field type extension methods - add a method to the field for the public
224 // methods that each field type defines beyond the default ones that already
225 // exist as part of this instance
226 $.each( this.s.type, function ( name, fn ) {
227 if ( typeof fn === 'function' && that[name] === undefined ) {
228 that[ name ] = function () {
229 var args = Array.prototype.slice.call( arguments );
230
231 args.unshift( name );
232 var ret = that._typeFn.apply( that, args );
233
234 // Return the given value if there is one, or the field instance
235 // for chaining if there is no value
236 return ret === undefined ?
237 that :
238 ret;
239 };
240 }
241 } );
242 };
243
244
245 Editor.Field.prototype = {
246 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
247 * Public
248 */
249 def: function ( set ) {
250 var opts = this.s.opts;
251
252 if ( set === undefined ) {
253 // Backwards compat
254 var def = opts['default'] !== undefined ?
255 opts['default'] :
256 opts.def;
257
258 return $.isFunction( def ) ?
259 def() :
260 def;
261 }
262
263 opts.def = set;
264 return this;
265 },
266
267 disable: function () {
268 this._typeFn( 'disable' );
269 return this;
270 },
271
272 displayed: function () {
273 var container = this.dom.container;
274
275 return container.parents('body').length && container.css('display') != 'none' ?
276 true :
277 false;
278 },
279
280 enable: function () {
281 this._typeFn( 'enable' );
282 return this;
283 },
284
285 error: function ( msg, fn ) {
286 var classes = this.s.classes;
287
288 // Add or remove the error class
289 if ( msg ) {
290 this.dom.container.addClass( classes.error );
291 }
292 else {
293 this.dom.container.removeClass( classes.error );
294 }
295
296 return this._msg( this.dom.fieldError, msg, fn );
297 },
298
299 isMultiValue: function () {
300 return this.s.multiValue;
301 },
302
303 inError: function () {
304 return this.dom.container.hasClass( this.s.classes.error );
305 },
306
307 input: function () {
308 return this.s.type.input ?
309 this._typeFn( 'input' ) :
310 $('input, select, textarea', this.dom.container);
311 },
312
313 focus: function () {
314 if ( this.s.type.focus ) {
315 this._typeFn( 'focus' );
316 }
317 else {
318 $('input, select, textarea', this.dom.container).focus();
319 }
320
321 return this;
322 },
323
324 get: function () {
325 // When multi-value a single get is undefined
326 if ( this.isMultiValue() ) {
327 return undefined;
328 }
329
330 var val = this._typeFn( 'get' );
331 return val !== undefined ?
332 val :
333 this.def();
334 },
335
336 hide: function ( animate ) {
337 var el = this.dom.container;
338
339 if ( animate === undefined ) {
340 animate = true;
341 }
342
343 if ( this.s.host.display() && animate ) {
344 el.slideUp();
345 }
346 else {
347 el.css( 'display', 'none' );
348 }
349 return this;
350 },
351
352 label: function ( str ) {
353 var label = this.dom.label;
354
355 if ( str === undefined ) {
356 return label.html();
357 }
358
359 label.html( str );
360 return this;
361 },
362
363 message: function ( msg, fn ) {
364 return this._msg( this.dom.fieldMessage, msg, fn );
365 },
366
367 // There is no `multiVal()` as its arguments could be ambiguous
368 // id is an idSrc value _only_
369 multiGet: function ( id ) {
370 var value;
371 var multiValues = this.s.multiValues;
372 var multiIds = this.s.multiIds;
373
374 if ( id === undefined ) {
375 // Get an object with the values for each item being edited
376 value = {};
377
378 for ( var i=0 ; i<multiIds.length ; i++ ) {
379 value[ multiIds[i] ] = this.isMultiValue() ?
380 multiValues[ multiIds[i] ] :
381 this.val();
382 }
383 }
384 else if ( this.isMultiValue() ) {
385 // Individual value
386 value = multiValues[ id ];
387 }
388 else {
389 // Common value
390 value = this.val();
391 }
392
393 return value;
394 },
395
396 multiSet: function ( id, val )
397 {
398 var multiValues = this.s.multiValues;
399 var multiIds = this.s.multiIds;
400
401 if ( val === undefined ) {
402 val = id;
403 id = undefined;
404 }
405
406 // Set
407 var set = function ( idSrc, val ) {
408 // Get an individual item's value - add the id to the edit ids if
409 // it isn't already in the set.
410 if ( $.inArray( multiIds ) === -1 ) {
411 multiIds.push( idSrc );
412 }
413
414 multiValues[ idSrc ] = val;
415 };
416
417 if ( $.isPlainObject( val ) && id === undefined ) {
418 // idSrc / value pairs passed in
419 $.each( val, function ( idSrc, innerVal ) {
420 set( idSrc, innerVal );
421 } );
422 }
423 else if ( id === undefined ) {
424 // Set same value for all existing ids
425 $.each( multiIds, function ( i, idSrc ) {
426 set( idSrc, val );
427 } );
428 }
429 else {
430 // Setting an individual property
431 set( id, val );
432 }
433
434 this.s.multiValue = true;
435 this._multiValueCheck();
436
437 return this;
438 },
439
440 name: function () {
441 return this.s.opts.name;
442 },
443
444 node: function () {
445 return this.dom.container[0];
446 },
447
448 set: function ( val ) {
449 var decodeFn = function ( d ) {
450 return typeof d !== 'string' ?
451 d :
452 d
453 .replace(/&gt;/g, '>')
454 .replace(/&lt;/g, '<')
455 .replace(/&amp;/g, '&')
456 .replace(/&quot;/g, '"')
457 .replace(/&#39;/g, '\'')
458 .replace(/&#10;/g, '\n');
459 };
460
461 this.s.multiValue = false;
462
463 var decode = this.s.opts.entityDecode;
464 if ( decode === undefined || decode === true ) {
465 if ( $.isArray( val ) ) {
466 for ( var i=0, ien=val.length ; i<ien ; i++ ) {
467 val[i] = decodeFn( val[i] );
468 }
469 }
470 else {
471 val = decodeFn( val );
472 }
473 }
474
475 this._typeFn( 'set', val );
476
477 this._multiValueCheck();
478
479 return this;
480 },
481
482 show: function ( animate ) {
483 var el = this.dom.container;
484
485 if ( animate === undefined ) {
486 animate = true;
487 }
488
489 if ( this.s.host.display() && animate ) {
490 el.slideDown();
491 }
492 else {
493 el.css( 'display', 'block' );
494 }
495 return this;
496 },
497
498 val: function ( val ) {
499 return val === undefined ?
500 this.get() :
501 this.set( val );
502 },
503
504
505 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
506 * Internal - Called from Editor only and are not publicly documented -
507 * these APIs can change!
508 */
509 dataSrc: function () {
510 return this.s.opts.data;
511 },
512
513 destroy: function () {
514 // remove element
515 this.dom.container.remove();
516
517 // field's own destroy method if there is one
518 this._typeFn( 'destroy' );
519 return this;
520 },
521
522 multiIds: function () {
523 return this.s.multiIds;
524 },
525
526 multiInfoShown: function ( show ) {
527 this.dom.multiInfo.css( { display: show ? 'block' : 'none' } );
528 },
529
530 multiReset: function () {
531 this.s.multiIds = [];
532 this.s.multiValues = {};
533 },
534
535 valFromData: null,
536
537 valToData: null,
538
539
540 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
541 * Private
542 */
543 _errorNode: function () {
544 return this.dom.fieldError;
545 },
546
547 _msg: function ( el, msg, fn ) {
548 if ( typeof msg === 'function' ) {
549 var editor = this.s.host;
550 msg = msg( editor, new DataTable.Api( editor.s.table ) );
551 }
552
553 if ( el.parent().is(":visible") ) {
554 el.html( msg );
555
556 if ( msg ) {
557 el.slideDown( fn ); // fn can be undefined - so jQuery won't execute it
558 }
559 else {
560 el.slideUp( fn );
561 }
562 }
563 else {
564 // Not visible, so immediately set, or blank out the element
565 el
566 .html( msg || '' )
567 .css( 'display', msg ? 'block' : 'none' );
568
569 if ( fn ) {
570 fn();
571 }
572 }
573
574 return this;
575 },
576
577 _multiValueCheck: function () {
578 var last;
579 var ids = this.s.multiIds;
580 var values = this.s.multiValues;
581 var val;
582 var different = false;
583
584 if ( ids ) {
585 for ( var i=0 ; i<ids.length ; i++ ) {
586 val = values[ ids[i] ];
587
588 if ( i > 0 && val !== last ) {
589 different = true;
590 break;
591 }
592
593 last = val;
594 }
595 }
596
597 if ( different && this.s.multiValue ) {
598 // Different values
599 this.dom.inputControl.css( { display: 'none' } );
600 this.dom.multi.css( { display: 'block' } );
601 }
602 else {
603 // All the same value
604 this.dom.inputControl.css( { display: 'block' } );
605 this.dom.multi.css( { display: 'none' } );
606
607 if ( this.s.multiValue ) {
608 this.val( last );
609 }
610 }
611
612 this.dom.multiReturn.css( {
613 display: ids && ids.length > 1 && different && ! this.s.multiValue ?
614 'block' :
615 'none'
616 } );
617
618 this.s.host._multiInfo();
619
620 return true;
621 },
622
623 _typeFn: function ( name /*, ... */ ) {
624 // Remove the name from the arguments list, so the rest can be passed
625 // straight into the field type
626 var args = Array.prototype.slice.call( arguments );
627 args.shift();
628
629 // Insert the options as the first parameter - all field type methods
630 // take the field's configuration object as the first parameter
631 args.unshift( this.s.opts );
632
633 var fn = this.s.type[ name ];
634 if ( fn ) {
635 return fn.apply( this.s.host, args );
636 }
637 }
638 };
639
640
641 Editor.Field.models = {};
642
643
644 /**
645 * Initialisation options that can be given to Editor.Field at initialisation
646 * time.
647 * @namespace
648 */
649 Editor.Field.defaults = {
650 /**
651 * Class name to assign to the field's container element (in addition to the other
652 * classes that Editor assigns by default).
653 * @type string
654 * @default <i>Empty string</i>
655 */
656 "className": "",
657
658 /**
659 * The data property (`mData` in DataTables terminology) that is used to
660 * read from and write to the table. If not given then it will take the same
661 * value as the `name` that is given in the field object. Note that `data`
662 * can be given as null, which will result in Editor not using a DataTables
663 * row property for the value of the field for either getting or setting
664 * data.
665 *
666 * In previous versions of Editor (1.2-) this was called `dataProp`. The old
667 * name can still be used for backwards compatibility, but the new form is
668 * preferred.
669 * @type string
670 * @default <i>Empty string</i>
671 */
672 "data": "",
673
674 /**
675 * The default value for the field. Used when creating new rows (editing will
676 * use the currently set value). If given as a function the function will be
677 * executed and the returned value used as the default
678 *
679 * In Editor 1.2 and earlier this field was called `default` - however
680 * `default` is a reserved word in Javascript, so it couldn't be used
681 * unquoted. `default` will still work with Editor 1.3, but the new property
682 * name of `def` is preferred.
683 * @type string|function
684 * @default <i>Empty string</i>
685 */
686 "def": "",
687
688 /**
689 * Helpful information text about the field that is shown below the input control.
690 * @type string
691 * @default <i>Empty string</i>
692 */
693 "fieldInfo": "",
694
695 /**
696 * The ID of the field. This is used by the `label` HTML tag as the "for" attribute
697 * improved accessibility. Although this using this parameter is not mandatory,
698 * it is a good idea to assign the ID to the DOM element that is the input for the
699 * field (if this is applicable).
700 * @type string
701 * @default <i>Calculated</i>
702 */
703 "id": "",
704
705 /**
706 * The label to display for the field input (i.e. the name that is visually
707 * assigned to the field).
708 * @type string
709 * @default <i>Empty string</i>
710 */
711 "label": "",
712
713 /**
714 * Helpful information text about the field that is shown below the field label.
715 * @type string
716 * @default <i>Empty string</i>
717 */
718 "labelInfo": "",
719
720 /**
721 * The name for the field that is submitted to the server. This is the only
722 * mandatory parameter in the field description object.
723 * @type string
724 * @default <i>null</i>
725 */
726 "name": null,
727
728 /**
729 * The input control that is presented to the end user. The options available
730 * are defined by {@link Editor.fieldTypes} and any extensions made
731 * to that object.
732 * @type string
733 * @default text
734 */
735 "type": "text"
736 };
737
738
739
740 /**
741 *
742 * @namespace
743 */
744 Editor.Field.models.settings = {
745 type: null,
746 name: null,
747 classes: null,
748 opts: null,
749 host: null
750 };
751
752
753
754 /**
755 *
756 * @namespace
757 */
758 Editor.Field.models.dom = {
759 container: null,
760 label: null,
761 labelInfo: null,
762 fieldInfo: null,
763 fieldError: null,
764 fieldMessage: null
765 };
766
767
768 /*
769 * Models
770 */
771
772 /**
773 * Object models container, for the various models that DataTables has available
774 * to it. These models define the objects that are used to hold the active state
775 * and configuration of the table.
776 * @namespace
777 */
778 Editor.models = {};
779
780
781 /**
782 * Editor makes very few assumptions about how its form will actually be
783 * displayed to the end user (where in the DOM, interaction etc), instead
784 * focusing on providing form interaction controls only. To actually display
785 * a form in the browser we need to use a display controller, and then select
786 * which one we want to use at initialisation time using the `display`
787 * option. For example a display controller could display the form in a
788 * lightbox (as the default display controller does), it could completely
789 * empty the document and put only the form in place, ir could work with
790 * DataTables to use `fnOpen` / `fnClose` to show the form in a "details" row
791 * and so on.
792 *
793 * Editor has two built-in display controllers ('lightbox' and 'envelope'),
794 * but others can readily be created and installed for use as plug-ins. When
795 * creating a display controller plug-in you **must** implement the methods
796 * in this control. Additionally when closing the display internally you
797 * **must** trigger a `requestClose` event which Editor will listen
798 * for and act upon (this allows Editor to ask the user if they are sure
799 * they want to close the form, for example).
800 * @namespace
801 */
802 Editor.models.displayController = {
803 /**
804 * Initialisation method, called by Editor when itself, initialises.
805 * @param {object} dte The DataTables Editor instance that has requested
806 * the action - this allows access to the Editor API if required.
807 * @returns {object} The object that Editor will use to run the 'open'
808 * and 'close' methods against. If static methods are used then
809 * just return the object that holds the init, open and close methods,
810 * however, this allows the display to be created with a 'new'
811 * instance of an object is the display controller calls for that.
812 * @type function
813 */
814 "init": function ( dte ) {},
815
816 /**
817 * Display the form (add it to the visual display in the document)
818 * @param {object} dte The DataTables Editor instance that has requested
819 * the action - this allows access to the Editor API if required.
820 * @param {element} append The DOM node that contains the form to be
821 * displayed
822 * @param {function} [fn] Callback function that is to be executed when
823 * the form has been displayed. Note that this parameter is optional.
824 */
825 "open": function ( dte, append, fn ) {},
826
827 /**
828 * Hide the form (remove it form the visual display in the document)
829 * @param {object} dte The DataTables Editor instance that has requested
830 * the action - this allows access to the Editor API if required.
831 * @param {function} [fn] Callback function that is to be executed when
832 * the form has been hidden. Note that this parameter is optional.
833 */
834 "close": function ( dte, fn ) {}
835 };
836
837
838
839 /**
840 * Model object for input types which are available to fields (assigned to
841 * {@link Editor.fieldTypes}). Any plug-ins which add additional
842 * input types to Editor **must** implement the methods in this object
843 * (dummy functions are given in the model so they can be used as defaults
844 * if extending this object).
845 *
846 * All functions in the model are executed in the Editor's instance scope,
847 * so you have full access to the settings object and the API methods if
848 * required.
849 * @namespace
850 * @example
851 * // Add a simple text input (the 'text' type that is built into Editor
852 * // does this, so you wouldn't implement this exactly as show, but it
853 * // it is a good example.
854 *
855 * var Editor = $.fn.Editor;
856 *
857 * Editor.fieldTypes.myInput = $.extend( true, {}, Editor.models.type, {
858 * "create": function ( conf ) {
859 * // We store the 'input' element in the configuration object so
860 * // we can easily access it again in future.
861 * conf._input = document.createElement('input');
862 * conf._input.id = conf.id;
863 * return conf._input;
864 * },
865 *
866 * "get": function ( conf ) {
867 * return conf._input.value;
868 * },
869 *
870 * "set": function ( conf, val ) {
871 * conf._input.value = val;
872 * },
873 *
874 * "enable": function ( conf ) {
875 * conf._input.disabled = false;
876 * },
877 *
878 * "disable": function ( conf ) {
879 * conf._input.disabled = true;
880 * }
881 * } );
882 */
883 Editor.models.fieldType = {
884 /**
885 * Create the field - this is called when the field is added to the form.
886 * Note that this is called at initialisation time, or when the
887 * {@link Editor#add} API method is called, not when the form is displayed.
888 * If you need to know when the form is shown, you can use the API to listen
889 * for the `open` event.
890 * @param {object} conf The configuration object for the field in question:
891 * {@link Editor.models.field}.
892 * @returns {element|null} The input element (or a wrapping element if a more
893 * complex input is required) or null if nothing is to be added to the
894 * DOM for this input type.
895 * @type function
896 */
897 "create": function ( conf ) {},
898
899 /**
900 * Get the value from the field
901 * @param {object} conf The configuration object for the field in question:
902 * {@link Editor.models.field}.
903 * @returns {*} The value from the field - the exact value will depend on the
904 * formatting required by the input type control.
905 * @type function
906 */
907 "get": function ( conf ) {},
908
909 /**
910 * Set the value for a field
911 * @param {object} conf The configuration object for the field in question:
912 * {@link Editor.models.field}.
913 * @param {*} val The value to set the field to - the exact value will
914 * depend on the formatting required by the input type control.
915 * @type function
916 */
917 "set": function ( conf, val ) {},
918
919 /**
920 * Enable the field - i.e. allow user interface
921 * @param {object} conf The configuration object for the field in question:
922 * {@link Editor.models.field}.
923 * @type function
924 */
925 "enable": function ( conf ) {},
926
927 /**
928 * Disable the field - i.e. disallow user interface
929 * @param {object} conf The configuration object for the field in question:
930 * {@link Editor.models.field}.
931 * @type function
932 */
933 "disable": function ( conf ) {}
934 };
935
936
937
938 /**
939 * Settings object for Editor - this provides the state for each instance of
940 * Editor and can be accessed through the instance's `s` property. Note that the
941 * settings object is considered to be "private" and thus is liable to change
942 * between versions. As such if you do read any of the setting parameters,
943 * please keep this in mind when upgrading!
944 * @namespace
945 */
946 Editor.models.settings = {
947 /**
948 * URL to submit Ajax data to.
949 * This is directly set by the initialisation parameter / default of the same name.
950 * @type string
951 * @default null
952 */
953 "ajaxUrl": null,
954
955 /**
956 * Ajax submit function.
957 * This is directly set by the initialisation parameter / default of the same name.
958 * @type function
959 * @default null
960 */
961 "ajax": null,
962
963 /**
964 * Data source for get and set data actions. This allows Editor to perform
965 * as an Editor for virtually any data source simply by defining additional
966 * data sources.
967 * @type object
968 * @default null
969 */
970 "dataSource": null,
971
972 /**
973 * DataTable selector, can be anything that the Api supports
974 * This is directly set by the initialisation parameter / default of the same name.
975 * @type string
976 * @default null
977 */
978 "domTable": null,
979
980 /**
981 * The initialisation object that was given by the user - stored for future reference.
982 * This is directly set by the initialisation parameter / default of the same name.
983 * @type string
984 * @default null
985 */
986 "opts": null,
987
988 /**
989 * The display controller object for the Form.
990 * This is directly set by the initialisation parameter / default of the same name.
991 * @type string
992 * @default null
993 */
994 "displayController": null,
995
996 /**
997 * The form fields - see {@link Editor.models.field} for details of the
998 * objects held in this array.
999 * @type object
1000 * @default null
1001 */
1002 "fields": {},
1003
1004 /**
1005 * Field order - order that the fields will appear in on the form. Array of strings,
1006 * the names of the fields.
1007 * @type array
1008 * @default null
1009 */
1010 "order": [],
1011
1012 /**
1013 * The ID of the row being edited (set to -1 on create and remove actions)
1014 * @type string
1015 * @default null
1016 */
1017 "id": -1,
1018
1019 /**
1020 * Flag to indicate if the form is currently displayed (true) or not (false)
1021 * @type string
1022 * @default null
1023 */
1024 "displayed": false,
1025
1026 /**
1027 * Flag to indicate if the form is current in a processing state (true) or not (false)
1028 * @type string
1029 * @default null
1030 */
1031 "processing": false,
1032
1033 /**
1034 * Developer provided identifier for the elements to be edited (i.e. at
1035 * `dt-type row-selector` to select rows to edit or delete.
1036 * @type array
1037 * @default null
1038 */
1039 "modifier": null,
1040
1041 /**
1042 * The current form action - 'create', 'edit' or 'remove'. If no current action then
1043 * it is set to null.
1044 * @type string
1045 * @default null
1046 */
1047 "action": null,
1048
1049 /**
1050 * JSON property from which to read / write the row's ID property.
1051 * @type string
1052 * @default null
1053 */
1054 "idSrc": null
1055 };
1056
1057
1058
1059 /**
1060 * Model of the buttons that can be used with the {@link Editor#buttons}
1061 * method for creating and displaying buttons (also the {@link Editor#button}
1062 * argument option for the {@link Editor#create}, {@link Editor#edit} and
1063 * {@link Editor#remove} methods). Although you don't need to extend this object,
1064 * it is available for reference to show the options available.
1065 * @namespace
1066 */
1067 Editor.models.button = {
1068 /**
1069 * The text to put into the button. This can be any HTML string you wish as
1070 * it will be rendered as HTML (allowing images etc to be shown inside the
1071 * button).
1072 * @type string
1073 * @default null
1074 */
1075 "label": null,
1076
1077 /**
1078 * Callback function which the button is activated. For example for a 'submit'
1079 * button you would call the {@link Editor#submit} API method, while for a cancel button
1080 * you would call the {@link Editor#close} API method. Note that the function is executed
1081 * in the scope of the Editor instance, so you can call the Editor's API methods
1082 * using the `this` keyword.
1083 * @type function
1084 * @default null
1085 */
1086 "fn": null,
1087
1088 /**
1089 * The CSS class(es) to apply to the button which can be useful for styling buttons
1090 * which preform different functions each with a distinctive visual appearance.
1091 * @type string
1092 * @default null
1093 */
1094 "className": null
1095 };
1096
1097
1098
1099 /**
1100 * This is really an internal namespace
1101 *
1102 * @namespace
1103 */
1104 Editor.models.formOptions = {
1105 /**
1106 * Action to take when the return key is pressed when focused in a form
1107 * element. Cam be `submit` or `none`. Could also be `blur` or `close`, but
1108 * why would you ever want that. Replaces `submitOnReturn` from 1.4.
1109 *
1110 * @type string
1111 */
1112 onReturn: 'submit',
1113
1114 /**
1115 * Action to take on blur. Can be `close`, `submit` or `none`. Replaces
1116 * `submitOnBlur` from 1.4
1117 *
1118 * @type string
1119 */
1120 onBlur: 'close',
1121
1122 /**
1123 * Action to take when the lightbox background is clicked - can be `close`,
1124 * `submit`, `blur` or `none`. Replaces `blurOnBackground` from 1.4
1125 *
1126 * @type string
1127 */
1128 onBackground: 'blur',
1129
1130 /**
1131 * Close for at the end of the Ajax request. Can be `close` or `none`.
1132 * Replaces `closeOnComplete` from 1.4.
1133 *
1134 * @type string
1135 */
1136 onComplete: 'close',
1137
1138 /**
1139 * Action to take when the `esc` key is pressed when focused in the form -
1140 * can be `close`, `submit`, `blur` or `none`
1141 *
1142 * @type string
1143 */
1144 onEsc: 'close',
1145
1146 /**
1147 * Data to submit to the server when submitting a form. If an option is
1148 * selected that results in no data being submitted, the Ajax request will
1149 * not be made Can be `all`, `changed` or `allIfChanged`. This effects the
1150 * edit action only.
1151 *
1152 * @type string
1153 */
1154 submit: 'all',
1155
1156 /**
1157 * Field identifier to focus on
1158 *
1159 * @type null|integer|string
1160 */
1161 focus: 0,
1162
1163 /**
1164 * Buttons to show in the form
1165 *
1166 * @type string|boolean|array|object
1167 */
1168 buttons: true,
1169
1170 /**
1171 * Form title
1172 *
1173 * @type string|boolean
1174 */
1175 title: true,
1176
1177 /**
1178 * Form message
1179 *
1180 * @type string|boolean
1181 */
1182 message: true,
1183
1184 /**
1185 * DataTables redraw option
1186 *
1187 * @type string|boolean
1188 */
1189 drawType: false
1190 };
1191
1192
1193 /*
1194 * Display controllers
1195 */
1196
1197 /**
1198 * Display controllers. See {@link Editor.models.displayController} for
1199 * full information about the display controller options for Editor. The display
1200 * controllers given in this object can be utilised by specifying the
1201 * {@link Editor.defaults.display} option.
1202 * @namespace
1203 */
1204 Editor.display = {};
1205
1206
1207 (function(window, document, $, DataTable) {
1208
1209
1210 var self;
1211
1212 Editor.display.lightbox = $.extend( true, {}, Editor.models.displayController, {
1213 /*
1214 * API methods
1215 */
1216 "init": function ( dte ) {
1217 self._init();
1218 return self;
1219 },
1220
1221 "open": function ( dte, append, callback ) {
1222 if ( self._shown ) {
1223 if ( callback ) {
1224 callback();
1225 }
1226 return;
1227 }
1228
1229 self._dte = dte;
1230
1231 var content = self._dom.content;
1232 content.children().detach();
1233 content
1234 .append( append )
1235 .append( self._dom.close );
1236
1237 self._shown = true;
1238 self._show( callback );
1239 },
1240
1241 "close": function ( dte, callback ) {
1242 if ( !self._shown ) {
1243 if ( callback ) {
1244 callback();
1245 }
1246 return;
1247 }
1248
1249 self._dte = dte;
1250 self._hide( callback );
1251
1252 self._shown = false;
1253 },
1254
1255 node: function ( dte ) {
1256 return self._dom.wrapper[0];
1257 },
1258
1259
1260 /*
1261 * Private methods
1262 */
1263 "_init": function () {
1264 if ( self._ready ) {
1265 return;
1266 }
1267
1268 var dom = self._dom;
1269 dom.content = $('div.DTED_Lightbox_Content', self._dom.wrapper);
1270
1271 dom.wrapper.css( 'opacity', 0 );
1272 dom.background.css( 'opacity', 0 );
1273 },
1274
1275
1276 "_show": function ( callback ) {
1277 var that = this;
1278 var dom = self._dom;
1279
1280 // Mobiles have very poor position fixed abilities, so we need to know
1281 // when using mobile A media query isn't good enough
1282 if ( window.orientation !== undefined ) {
1283 $('body').addClass( 'DTED_Lightbox_Mobile' );
1284 }
1285
1286 // Adjust size for the content
1287 dom.content.css( 'height', 'auto' );
1288 dom.wrapper.css( {
1289 top: -self.conf.offsetAni
1290 } );
1291
1292 $('body')
1293 .append( self._dom.background )
1294 .append( self._dom.wrapper );
1295
1296 self._heightCalc();
1297
1298 dom.wrapper
1299 .stop()
1300 .animate( {
1301 opacity: 1,
1302 top: 0
1303 }, callback );
1304
1305 dom.background
1306 .stop()
1307 .animate( {
1308 opacity: 1
1309 } );
1310
1311 // Event handlers - assign on show (and unbind on hide) rather than init
1312 // since we might need to refer to different editor instances - 12563
1313 dom.close.bind( 'click.DTED_Lightbox', function (e) {
1314 self._dte.close();
1315 } );
1316
1317 dom.background.bind( 'click.DTED_Lightbox', function (e) {
1318 self._dte.background();
1319 } );
1320
1321 $('div.DTED_Lightbox_Content_Wrapper', dom.wrapper).bind( 'click.DTED_Lightbox', function (e) {
1322 if ( $(e.target).hasClass('DTED_Lightbox_Content_Wrapper') ) {
1323 self._dte.background();
1324 }
1325 } );
1326
1327 $(window).bind( 'resize.DTED_Lightbox', function () {
1328 self._heightCalc();
1329 } );
1330
1331 self._scrollTop = $('body').scrollTop();
1332
1333 // For smaller screens we need to hide the other elements in the
1334 // document since iOS and Android both mess up display:fixed when
1335 // the virtual keyboard is shown
1336 if ( window.orientation !== undefined ) {
1337 var kids = $('body').children().not( dom.background ).not( dom.wrapper );
1338 $('body').append( '<div class="DTED_Lightbox_Shown"/>' );
1339 $('div.DTED_Lightbox_Shown').append( kids );
1340 }
1341 },
1342
1343
1344 "_heightCalc": function () {
1345 // Set the max-height for the form content
1346 var dom = self._dom;
1347 var maxHeight = $(window).height() - (self.conf.windowPadding*2) -
1348 $('div.DTE_Header', dom.wrapper).outerHeight() -
1349 $('div.DTE_Footer', dom.wrapper).outerHeight();
1350
1351 $('div.DTE_Body_Content', dom.wrapper).css(
1352 'maxHeight',
1353 maxHeight
1354 );
1355 },
1356
1357
1358 "_hide": function ( callback ) {
1359 var dom = self._dom;
1360
1361 if ( !callback ) {
1362 callback = function () {};
1363 }
1364
1365 if ( window.orientation !== undefined ) {
1366 var show = $('div.DTED_Lightbox_Shown');
1367 show.children().appendTo('body');
1368 show.remove();
1369 }
1370
1371 // Restore scroll state
1372 $('body')
1373 .removeClass( 'DTED_Lightbox_Mobile' )
1374 .scrollTop( self._scrollTop );
1375
1376 dom.wrapper
1377 .stop()
1378 .animate( {
1379 opacity: 0,
1380 top: self.conf.offsetAni
1381 }, function () {
1382 $(this).detach();
1383 callback();
1384 } );
1385
1386 dom.background
1387 .stop()
1388 .animate( {
1389 opacity: 0
1390 }, function () {
1391 $(this).detach();
1392 } );
1393
1394 // Event handlers
1395 dom.close.unbind( 'click.DTED_Lightbox' );
1396 dom.background.unbind( 'click.DTED_Lightbox' );
1397 $('div.DTED_Lightbox_Content_Wrapper', dom.wrapper).unbind( 'click.DTED_Lightbox' );
1398 $(window).unbind( 'resize.DTED_Lightbox' );
1399 },
1400
1401
1402 /*
1403 * Private properties
1404 */
1405 "_dte": null,
1406 "_ready": false,
1407 "_shown": false,
1408 "_dom": {
1409 "wrapper": $(
1410 '<div class="DTED DTED_Lightbox_Wrapper">'+
1411 '<div class="DTED_Lightbox_Container">'+
1412 '<div class="DTED_Lightbox_Content_Wrapper">'+
1413 '<div class="DTED_Lightbox_Content">'+
1414 '</div>'+
1415 '</div>'+
1416 '</div>'+
1417 '</div>'
1418 ),
1419
1420 "background": $(
1421 '<div class="DTED_Lightbox_Background"><div/></div>'
1422 ),
1423
1424 "close": $(
1425 '<div class="DTED_Lightbox_Close"></div>'
1426 ),
1427
1428 "content": null
1429 }
1430 } );
1431
1432 self = Editor.display.lightbox;
1433
1434 self.conf = {
1435 "offsetAni": 25,
1436 "windowPadding": 25
1437 };
1438
1439
1440 }(window, document, jQuery, jQuery.fn.dataTable));
1441
1442
1443
1444 (function(window, document, $, DataTable) {
1445
1446
1447 var self;
1448
1449 Editor.display.envelope = $.extend( true, {}, Editor.models.displayController, {
1450 /*
1451 * API methods
1452 */
1453 "init": function ( dte ) {
1454 self._dte = dte;
1455 self._init();
1456 return self;
1457 },
1458
1459
1460 "open": function ( dte, append, callback ) {
1461 self._dte = dte;
1462 $(self._dom.content).children().detach();
1463 self._dom.content.appendChild( append );
1464 self._dom.content.appendChild( self._dom.close );
1465
1466 self._show( callback );
1467 },
1468
1469
1470 "close": function ( dte, callback ) {
1471 self._dte = dte;
1472 self._hide( callback );
1473 },
1474
1475 node: function ( dte ) {
1476 return self._dom.wrapper[0];
1477 },
1478
1479
1480 /*
1481 * Private methods
1482 */
1483 "_init": function () {
1484 if ( self._ready ) {
1485 return;
1486 }
1487
1488 self._dom.content = $('div.DTED_Envelope_Container', self._dom.wrapper)[0];
1489
1490 document.body.appendChild( self._dom.background );
1491 document.body.appendChild( self._dom.wrapper );
1492
1493 // For IE6-8 we need to make it a block element to read the opacity...
1494 self._dom.background.style.visbility = 'hidden';
1495 self._dom.background.style.display = 'block';
1496 self._cssBackgroundOpacity = $(self._dom.background).css('opacity');
1497 self._dom.background.style.display = 'none';
1498 self._dom.background.style.visbility = 'visible';
1499 },
1500
1501
1502 "_show": function ( callback ) {
1503 var that = this;
1504 var formHeight;
1505
1506 if ( !callback ) {
1507 callback = function () {};
1508 }
1509
1510 // Adjust size for the content
1511 self._dom.content.style.height = 'auto';
1512
1513 var style = self._dom.wrapper.style;
1514 style.opacity = 0;
1515 style.display = 'block';
1516
1517 var targetRow = self._findAttachRow();
1518 var height = self._heightCalc();
1519 var width = targetRow.offsetWidth;
1520
1521 style.display = 'none';
1522 style.opacity = 1;
1523
1524 // Prep the display
1525 self._dom.wrapper.style.width = width+"px";
1526 self._dom.wrapper.style.marginLeft = -(width/2)+"px";
1527 self._dom.wrapper.style.top = ($(targetRow).offset().top + targetRow.offsetHeight)+"px";
1528 self._dom.content.style.top = ((-1 * height) - 20)+"px";
1529
1530 // Start animating in the background
1531 self._dom.background.style.opacity = 0;
1532 self._dom.background.style.display = 'block';
1533 $(self._dom.background).animate( {
1534 'opacity': self._cssBackgroundOpacity
1535 }, 'normal' );
1536
1537 // Animate in the display
1538 $(self._dom.wrapper).fadeIn();
1539
1540 // Slide the slider down to 'open' the view
1541 if ( self.conf.windowScroll ) {
1542 // Scroll the window so we can see the editor first
1543 $('html,body').animate( {
1544 "scrollTop": $(targetRow).offset().top + targetRow.offsetHeight - self.conf.windowPadding
1545 }, function () {
1546 // Now open the editor
1547 $(self._dom.content).animate( {
1548 "top": 0
1549 }, 600, callback );
1550 } );
1551 }
1552 else {
1553 // Just open the editor without moving the document position
1554 $(self._dom.content).animate( {
1555 "top": 0
1556 }, 600, callback );
1557 }
1558
1559 // Event handlers
1560 $(self._dom.close).bind( 'click.DTED_Envelope', function (e) {
1561 self._dte.close();
1562 } );
1563
1564 $(self._dom.background).bind( 'click.DTED_Envelope', function (e) {
1565 self._dte.background();
1566 } );
1567
1568 $('div.DTED_Lightbox_Content_Wrapper', self._dom.wrapper).bind( 'click.DTED_Envelope', function (e) {
1569 if ( $(e.target).hasClass('DTED_Envelope_Content_Wrapper') ) {
1570 self._dte.background();
1571 }
1572 } );
1573
1574 $(window).bind( 'resize.DTED_Envelope', function () {
1575 self._heightCalc();
1576 } );
1577 },
1578
1579
1580 "_heightCalc": function () {
1581 var formHeight;
1582
1583 formHeight = self.conf.heightCalc ?
1584 self.conf.heightCalc( self._dom.wrapper ) :
1585 $(self._dom.content).children().height();
1586
1587 // Set the max-height for the form content
1588 var maxHeight = $(window).height() - (self.conf.windowPadding*2) -
1589 $('div.DTE_Header', self._dom.wrapper).outerHeight() -
1590 $('div.DTE_Footer', self._dom.wrapper).outerHeight();
1591
1592 $('div.DTE_Body_Content', self._dom.wrapper).css('maxHeight', maxHeight);
1593
1594 return $(self._dte.dom.wrapper).outerHeight();
1595 },
1596
1597
1598 "_hide": function ( callback ) {
1599 if ( !callback ) {
1600 callback = function () {};
1601 }
1602
1603 $(self._dom.content).animate( {
1604 "top": -(self._dom.content.offsetHeight+50)
1605 }, 600, function () {
1606 $([self._dom.wrapper, self._dom.background]).fadeOut( 'normal', callback );
1607 } );
1608
1609 // Event handlers
1610 $(self._dom.close).unbind( 'click.DTED_Lightbox' );
1611 $(self._dom.background).unbind( 'click.DTED_Lightbox' );
1612 $('div.DTED_Lightbox_Content_Wrapper', self._dom.wrapper).unbind( 'click.DTED_Lightbox' );
1613 $(window).unbind( 'resize.DTED_Lightbox' );
1614 },
1615
1616
1617 "_findAttachRow": function () {
1618 var dt = $(self._dte.s.table).DataTable();
1619
1620 // Figure out where we want to put the form display
1621 if ( self.conf.attach === 'head' ) {
1622 return dt.table().header();
1623 }
1624 else if ( self._dte.s.action === 'create' ) {
1625 return dt.table().header();
1626 }
1627 else {
1628 return dt.row( self._dte.s.modifier ).node();
1629 }
1630 },
1631
1632
1633 /*
1634 * Private properties
1635 */
1636 "_dte": null,
1637 "_ready": false,
1638 "_cssBackgroundOpacity": 1, // read from the CSS dynamically, but stored for future reference
1639
1640
1641 "_dom": {
1642 "wrapper": $(
1643 '<div class="DTED DTED_Envelope_Wrapper">'+
1644 '<div class="DTED_Envelope_ShadowLeft"></div>'+
1645 '<div class="DTED_Envelope_ShadowRight"></div>'+
1646 '<div class="DTED_Envelope_Container"></div>'+
1647 '</div>'
1648 )[0],
1649
1650 "background": $(
1651 '<div class="DTED_Envelope_Background"><div/></div>'
1652 )[0],
1653
1654 "close": $(
1655 '<div class="DTED_Envelope_Close">&times;</div>'
1656 )[0],
1657
1658 "content": null
1659 }
1660 } );
1661
1662
1663 // Assign to 'self' for easy referencing of our own object!
1664 self = Editor.display.envelope;
1665
1666
1667 // Configuration object - can be accessed globally using
1668 // $.fn.Editor.display.envelope.conf (!)
1669 self.conf = {
1670 "windowPadding": 50,
1671 "heightCalc": null,
1672 "attach": "row",
1673 "windowScroll": true
1674 };
1675
1676
1677 }(window, document, jQuery, jQuery.fn.dataTable));
1678
1679
1680 /*
1681 * Prototype includes
1682 */
1683
1684
1685 /**
1686 * Add a new field to the from. This is the method that is called automatically when
1687 * fields are given in the initialisation objects as {@link Editor.defaults.fields}.
1688 * @memberOf Editor
1689 * @param {object|array} field The object that describes the field (the full
1690 * object is described by {@link Editor.model.field}. Note that multiple
1691 * fields can be given by passing in an array of field definitions.
1692 * @param {string} [after] Existing field to insert the new field after. This
1693 * can be `undefined` (insert at end), `null` (insert at start) or `string`
1694 * the field name to insert after.
1695 */
1696 Editor.prototype.add = function ( cfg, after )
1697 {
1698 // Allow multiple fields to be added at the same time
1699 if ( $.isArray( cfg ) ) {
1700 for ( var i=0, iLen=cfg.length ; i<iLen ; i++ ) {
1701 this.add( cfg[i] );
1702 }
1703 }
1704 else {
1705 var name = cfg.name;
1706
1707 if ( name === undefined ) {
1708 throw "Error adding field. The field requires a `name` option";
1709 }
1710
1711 if ( this.s.fields[ name ] ) {
1712 throw "Error adding field '"+name+"'. A field already exists with this name";
1713 }
1714
1715 // Allow the data source to add / modify the field properties
1716 // Dev: would this be better as an event `preAddField`? And have the
1717 // data sources init only once, but can listen for such events? More
1718 // complexity, but probably more flexible...
1719 this._dataSource( 'initField', cfg );
1720
1721 this.s.fields[ name ] = new Editor.Field( cfg, this.classes.field, this );
1722
1723 if ( after === undefined ) {
1724 this.s.order.push( name );
1725 }
1726 else if ( after === null ) {
1727 this.s.order.unshift( name );
1728 }
1729 else {
1730 var idx = $.inArray( after, this.s.order );
1731 this.s.order.splice( idx+1, 0, name );
1732 }
1733
1734 }
1735
1736 this._displayReorder( this.order() );
1737
1738 return this;
1739 };
1740
1741
1742 /**
1743 * Perform background activation tasks.
1744 *
1745 * This is NOT publicly documented on the Editor web-site, but rather can be
1746 * used by display controller plug-ins to perform the required task on
1747 * background activation.
1748 *
1749 * @return {Editor} Editor instance, for chaining
1750 */
1751 Editor.prototype.background = function ()
1752 {
1753 var onBackground = this.s.editOpts.onBackground;
1754
1755 if ( onBackground === 'blur' ) {
1756 this.blur();
1757 }
1758 else if ( onBackground === 'close' ) {
1759 this.close();
1760 }
1761 else if ( onBackground === 'submit' ) {
1762 this.submit();
1763 }
1764
1765 return this;
1766 };
1767
1768
1769 /**
1770 * Blur the currently displayed editor.
1771 *
1772 * A blur is different from a `close()` in that it might cause either a close or
1773 * the form to be submitted. A typical example of a blur would be clicking on
1774 * the background of the bubble or main editing forms - i.e. it might be a
1775 * close, or it might submit depending upon the configuration, while a click on
1776 * the close box is a very definite close.
1777 *
1778 * @return {Editor} Editor instance, for chaining
1779 */
1780 Editor.prototype.blur = function ()
1781 {
1782 this._blur();
1783
1784 return this;
1785 };
1786
1787
1788
1789 Editor.prototype.bubble = function ( cells, fieldNames, show, opts )
1790 {
1791 var that = this;
1792
1793 // Some other field in inline edit mode?
1794 if ( this._tidy( function () { that.bubble( cells, fieldNames, opts ); } ) ) {
1795 return this;
1796 }
1797
1798 // Argument shifting
1799 if ( $.isPlainObject( fieldNames ) ) {
1800 opts = fieldNames;
1801 fieldNames = undefined;
1802 show = true;
1803 }
1804 else if ( typeof fieldNames === 'boolean' ) {
1805 show = fieldNames;
1806 fieldNames = undefined;
1807 opts = undefined;
1808 }
1809
1810 if ( $.isPlainObject( show ) ) {
1811 opts = show;
1812 show = true;
1813 }
1814
1815 if ( show === undefined ) {
1816 show = true;
1817 }
1818
1819 opts = $.extend( {}, this.s.formOptions.bubble, opts );
1820
1821 var editFields = this._dataSource( 'individual', cells, fieldNames );
1822
1823 this._edit( cells, editFields, 'bubble' );
1824
1825 var ret = this._preopen( 'bubble' );
1826 if ( ! ret ) {
1827 return this;
1828 }
1829
1830 // Keep the bubble in position on resize
1831 var namespace = this._formOptions( opts );
1832 $(window).on( 'resize.'+namespace, function () {
1833 that.bubblePosition();
1834 } );
1835
1836 // Store the nodes that are being used so the bubble can be positioned
1837 var nodes = [];
1838 this.s.bubbleNodes = nodes.concat.apply( nodes, _pluck( editFields, 'attach' ) );
1839
1840 // Create container display
1841 var classes = this.classes.bubble;
1842 var background = $( '<div class="'+classes.bg+'"><div/></div>' );
1843 var container = $(
1844 '<div class="'+classes.wrapper+'">'+
1845 '<div class="'+classes.liner+'">'+
1846 '<div class="'+classes.table+'">'+
1847 '<div class="'+classes.close+'" />'+
1848 '</div>'+
1849 '</div>'+
1850 '<div class="'+classes.pointer+'" />'+
1851 '</div>'
1852 );
1853
1854 if ( show ) {
1855 container.appendTo( 'body' );
1856 background.appendTo( 'body' );
1857 }
1858
1859 var liner = container.children().eq(0);
1860 var table = liner.children();
1861 var close = table.children();
1862 liner.append( this.dom.formError );
1863 table.prepend( this.dom.form );
1864
1865 if ( opts.message ) {
1866 liner.prepend( this.dom.formInfo );
1867 }
1868
1869 if ( opts.title ) {
1870 liner.prepend( this.dom.header );
1871 }
1872
1873 if ( opts.buttons ) {
1874 table.append( this.dom.buttons );
1875 }
1876
1877 var pair = $().add( container ).add( background );
1878 this._closeReg( function ( submitComplete ) {
1879 pair.animate(
1880 { opacity: 0 },
1881 function () {
1882 pair.detach();
1883
1884 $(window).off( 'resize.'+namespace );
1885
1886 // Clear error messages "offline"
1887 that._clearDynamicInfo();
1888 }
1889 );
1890 } );
1891
1892 // Close event handlers
1893 background.click( function () {
1894 that.blur();
1895 } );
1896
1897 close.click( function () {
1898 that._close();
1899 } );
1900
1901 this.bubblePosition();
1902
1903 pair.animate( { opacity: 1 } );
1904
1905 this._focus( this.s.includeFields, opts.focus );
1906 this._postopen( 'bubble' );
1907
1908 return this;
1909 };
1910
1911
1912 /**
1913 * Reposition the editing bubble (`bubble()`) when it is visible. This can be
1914 * used to update the bubble position if other elements on the page change
1915 * position. Editor will automatically call this method on window resize.
1916 *
1917 * @return {Editor} Editor instance, for chaining
1918 */
1919 Editor.prototype.bubblePosition = function ()
1920 {
1921 var
1922 wrapper = $('div.DTE_Bubble'),
1923 liner = $('div.DTE_Bubble_Liner'),
1924 nodes = this.s.bubbleNodes;
1925
1926 // Average the node positions to insert the container
1927 var position = { top: 0, left: 0, right: 0, bottom: 0 };
1928
1929 $.each( nodes, function (i, node) {
1930 var pos = $(node).offset();
1931
1932 position.top += pos.top;
1933 position.left += pos.left;
1934 position.right += pos.left + node.offsetWidth;
1935 position.bottom += pos.top + node.offsetHeight;
1936 } );
1937
1938 position.top /= nodes.length;
1939 position.left /= nodes.length;
1940 position.right /= nodes.length;
1941 position.bottom /= nodes.length;
1942
1943 var
1944 top = position.top,
1945 left = (position.left + position.right) / 2,
1946 width = liner.outerWidth(),
1947 visLeft = left - (width / 2),
1948 visRight = visLeft + width,
1949 docWidth = $(window).width(),
1950 padding = 15,
1951 classes = this.classes.bubble;
1952
1953 wrapper.css( {
1954 top: top,
1955 left: left
1956 } );
1957
1958 // Correct for overflow from the top of the document by positioning below
1959 // the field if needed
1960 if ( liner.length && liner.offset().top < 0 ) {
1961 wrapper
1962 .css( 'top', position.bottom )
1963 .addClass( 'below' );
1964 }
1965 else {
1966 wrapper.removeClass( 'below' );
1967 }
1968
1969 // Attempt to correct for overflow to the right of the document
1970 if ( visRight+padding > docWidth ) {
1971 var diff = visRight - docWidth;
1972
1973 // If left overflowing, that takes priority
1974 liner.css( 'left', visLeft < padding ?
1975 -(visLeft-padding) :
1976 -(diff+padding)
1977 );
1978 }
1979 else {
1980 // Correct overflow to the left
1981 liner.css( 'left', visLeft < padding ? -(visLeft-padding) : 0 );
1982 }
1983
1984 return this;
1985 };
1986
1987
1988 /**
1989 * Setup the buttons that will be shown in the footer of the form - calling this
1990 * method will replace any buttons which are currently shown in the form.
1991 * @param {array|object} buttons A single button definition to add to the form or
1992 * an array of objects with the button definitions to add more than one button.
1993 * The options for the button definitions are fully defined by the
1994 * {@link Editor.models.button} object.
1995 * @param {string} buttons.label The text to put into the button. This can be any
1996 * HTML string you wish as it will be rendered as HTML (allowing images etc to
1997 * be shown inside the button).
1998 * @param {function} [buttons.fn] Callback function which the button is activated.
1999 * For example for a 'submit' button you would call the {@link Editor#submit} method,
2000 * while for a cancel button you would call the {@link Editor#close} method. Note that
2001 * the function is executed in the scope of the Editor instance, so you can call
2002 * the Editor's API methods using the `this` keyword.
2003 * @param {string} [buttons.className] The CSS class(es) to apply to the button
2004 * which can be useful for styling buttons which preform different functions
2005 * each with a distinctive visual appearance.
2006 * @return {Editor} Editor instance, for chaining
2007 */
2008 Editor.prototype.buttons = function ( buttons )
2009 {
2010 var that = this;
2011
2012 if ( buttons === '_basic' ) {
2013 // Special string to create a basic button - undocumented
2014 buttons = [ {
2015 label: this.i18n[ this.s.action ].submit,
2016 fn: function () { this.submit(); }
2017 } ];
2018 }
2019 else if ( ! $.isArray( buttons ) ) {
2020 // Allow a single button to be passed in as an object with an array
2021 buttons = [ buttons ];
2022 }
2023
2024 $(this.dom.buttons).empty();
2025
2026 $.each( buttons, function ( i, btn ) {
2027 if ( typeof btn === 'string' ) {
2028 btn = {
2029 label: btn,
2030 fn: function () { this.submit(); }
2031 };
2032 }
2033
2034 $( '<button/>', {
2035 'class': that.classes.form.button+(btn.className ? ' '+btn.className : '')
2036 } )
2037 .html( typeof btn.label === 'function' ?
2038 btn.label( that ) :
2039 btn.label || ''
2040 )
2041 .attr( 'tabindex', 0 )
2042 .on( 'keyup', function (e) {
2043 if ( e.keyCode === 13 && btn.fn ) {
2044 btn.fn.call( that );
2045 }
2046 } )
2047 .on( 'keypress', function (e) {
2048 // Stop the browser activating the click event - if we don't
2049 // have this and the Ajax return is fast, the keyup in
2050 // `_formOptions()` might trigger another submit
2051 if ( e.keyCode === 13 ) {
2052 e.preventDefault();
2053 }
2054 } )
2055 .on( 'click', function (e) {
2056 e.preventDefault();
2057
2058 if ( btn.fn ) {
2059 btn.fn.call( that );
2060 }
2061 } )
2062 .appendTo( that.dom.buttons );
2063 } );
2064
2065 return this;
2066 };
2067
2068
2069 /**
2070 * Remove fields from the form (fields are those that have been added using the
2071 * {@link Editor#add} method or the `fields` initialisation option). A single,
2072 * multiple or all fields can be removed at a time based on the passed parameter.
2073 * Fields are identified by the `name` property that was given to each field
2074 * when added to the form.
2075 * @param {string|array} [fieldName] Field or fields to remove from the form. If
2076 * not given then all fields are removed from the form. If given as a string
2077 * then the single matching field will be removed. If given as an array of
2078 * strings, then all matching fields will be removed.
2079 * @return {Editor} Editor instance, for chaining
2080 *
2081 * @example
2082 * // Clear the form of current fields and then add a new field
2083 * // before displaying a 'create' display
2084 * editor.clear();
2085 * editor.add( {
2086 * "label": "User name",
2087 * "name": "username"
2088 * } );
2089 * editor.create( "Create user" );
2090 *
2091 * @example
2092 * // Remove an individual field
2093 * editor.clear( "username" );
2094 *
2095 * @example
2096 * // Remove multiple fields
2097 * editor.clear( [ "first_name", "last_name" ] );
2098 */
2099 Editor.prototype.clear = function ( fieldName )
2100 {
2101 var that = this;
2102 var fields = this.s.fields;
2103
2104 if ( typeof fieldName === 'string' ) {
2105 // Remove an individual form element
2106 fields[ fieldName ].destroy();
2107 delete fields[ fieldName ];
2108
2109 var orderIdx = $.inArray( fieldName, this.s.order );
2110 this.s.order.splice( orderIdx, 1 );
2111 }
2112 else {
2113 $.each( this._fieldNames( fieldName ), function (i, name) {
2114 that.clear( name );
2115 } );
2116 }
2117
2118 return this;
2119 };
2120
2121
2122 /**
2123 * Close the form display.
2124 *
2125 * Note that `close()` will close any of the three Editor form types (main,
2126 * bubble and inline).
2127 *
2128 * @return {Editor} Editor instance, for chaining
2129 */
2130 Editor.prototype.close = function ()
2131 {
2132 this._close( false );
2133
2134 return this;
2135 };
2136
2137
2138 /**
2139 * Create a new record - show the form that allows the user to enter information
2140 * for a new row and then subsequently submit that data.
2141 * @param {boolean} [show=true] Show the form or not.
2142 *
2143 * @example
2144 * // Show the create form with a submit button
2145 * editor
2146 * .title( 'Add new record' )
2147 * .buttons( {
2148 * "label": "Save",
2149 * "fn": function () {
2150 * this.submit();
2151 * }
2152 * } )
2153 * .create();
2154 *
2155 * @example
2156 * // Don't show the form and automatically submit it after programatically
2157 * // setting the values of fields (and using the field defaults)
2158 * editor
2159 * create()
2160 * set( 'name', 'Test user' )
2161 * set( 'access', 'Read only' )
2162 * submit();
2163 */
2164 Editor.prototype.create = function ( arg1, arg2, arg3, arg4 )
2165 {
2166 var that = this;
2167 var fields = this.s.fields;
2168 var count = 1;
2169
2170 // Some other field in inline edit mode?
2171 if ( this._tidy( function () { that.create( arg1, arg2, arg3, arg4 ); } ) ) {
2172 return this;
2173 }
2174
2175 // Multi-row creation support (only supported by the 1.3+ style of calling
2176 // this method, so a max of three arguments
2177 if ( typeof arg1 === 'number' ) {
2178 count = arg1;
2179 arg1 = arg2;
2180 arg2 = arg3;
2181 }
2182
2183 // Set up the edit fields for submission
2184 this.s.editFields = {};
2185 for ( var i=0 ; i<count ; i++ ) {
2186 this.s.editFields[ i ] = {
2187 fields: this.s.fields
2188 };
2189 }
2190
2191 var argOpts = this._crudArgs( arg1, arg2, arg3, arg4 );
2192
2193 this.s.action = "create";
2194 this.s.modifier = null;
2195 this.dom.form.style.display = 'block';
2196
2197 this._actionClass();
2198
2199 // Allow all fields to be displayed for the create form
2200 this._displayReorder( this.fields() );
2201
2202 // Set the default for the fields
2203 $.each( fields, function ( name, field ) {
2204 field.multiReset();
2205 field.set( field.def() );
2206 } );
2207
2208 this._event( 'initCreate' );
2209 this._assembleMain();
2210 this._formOptions( argOpts.opts );
2211
2212 argOpts.maybeOpen();
2213
2214 return this;
2215 };
2216
2217 /**
2218 * Create a dependent link between two or more fields. This method is used to
2219 * listen for a change in a field's value which will trigger updating of the
2220 * form. This update can consist of updating an options list, changing values
2221 * or making fields hidden / visible.
2222 *
2223 * @param {string} parent The name of the field to listen to changes from
2224 * @param {string|object|function} url Callback definition. This can be:
2225 * * A string, which will be used as a URL to submit the request for update to
2226 * * An object, which is used to extend an Ajax object for the request. The
2227 * `url` parameter must be specified.
2228 * * A function, which is used as a callback, allowing non-ajax updates.
2229 * @return {Editor} Editor instance, for chaining
2230 */
2231 Editor.prototype.dependent = function ( parent, url, opts ) {
2232 if ( $.isArray( parent ) ) {
2233 for ( var i=0, ien=parent.length ; i<ien ; i++ ) {
2234 this.dependent( parent[i], url, opts );
2235 }
2236
2237 return this;
2238 }
2239
2240 var that = this;
2241 var field = this.field( parent );
2242 var ajaxOpts = {
2243 type: 'POST',
2244 dataType: 'json'
2245 };
2246
2247 opts = $.extend( {
2248 event: 'change',
2249 data: null,
2250 preUpdate: null,
2251 postUpdate: null
2252 }, opts );
2253
2254 var update = function ( json ) {
2255 if ( opts.preUpdate ) {
2256 opts.preUpdate( json );
2257 }
2258
2259 // Field specific
2260 $.each( {
2261 labels: 'label',
2262 options: 'update',
2263 values: 'val',
2264 messages: 'message',
2265 errors: 'error'
2266 }, function ( jsonProp, fieldFn ) {
2267 if ( json[ jsonProp ] ) {
2268 $.each( json[ jsonProp ], function ( field, val ) {
2269 that.field( field )[ fieldFn ]( val );
2270 } );
2271 }
2272 } );
2273
2274 // Form level
2275 $.each( [ 'hide', 'show', 'enable', 'disable' ], function ( i, key ) {
2276 if ( json[ key ] ) {
2277 that[ key ]( json[ key ] );
2278 }
2279 } );
2280
2281 if ( opts.postUpdate ) {
2282 opts.postUpdate( json );
2283 }
2284 };
2285
2286 // Use a delegate handler to account for field elements which are added and
2287 // removed after `depenedent` has been called
2288 $(field.node()).on( opts.event, function (e) {
2289 // Make sure that it was one of the input elements that triggered the ev
2290 if ( $.inArray( e.target, field.input().toArray() ) === -1 ) {
2291 return;
2292 }
2293
2294 var data = {};
2295 data.rows = that.s.editFields ?
2296 _pluck( that.s.editFields, 'data' ) :
2297 null;
2298 data.row = data.rows ?
2299 data.rows[0] :
2300 null;
2301 data.values = that.val();
2302
2303 if ( opts.data ) {
2304 var ret = opts.data( data );
2305
2306 if ( ret ) {
2307 opts.data = ret;
2308 }
2309 }
2310
2311 if ( typeof url === 'function' ) {
2312 var o = url( field.val(), data, update );
2313
2314 if ( o ) {
2315 update( o );
2316 }
2317 }
2318 else {
2319 if ( $.isPlainObject( url ) ) {
2320 $.extend( ajaxOpts, url );
2321 }
2322 else {
2323 ajaxOpts.url = url;
2324 }
2325
2326 $.ajax( $.extend( ajaxOpts, {
2327 url: url,
2328 data: data,
2329 success: update
2330 } ) );
2331 }
2332 } );
2333
2334 return this;
2335 };
2336
2337
2338 /**
2339 * Disable one or more field inputs, disallowing subsequent user interaction with the
2340 * fields until they are re-enabled.
2341 * @param {string|array} name The field name (from the `name` parameter given when
2342 * originally setting up the field) to disable, or an array of field names to disable
2343 * multiple fields with a single call.
2344 * @return {Editor} Editor instance, for chaining
2345 *
2346 * @example
2347 * // Show a 'create' record form, but with a field disabled
2348 * editor.disable( 'account_type' );
2349 * editor.create( 'Add new user', {
2350 * "label": "Save",
2351 * "fn": function () { this.submit(); }
2352 * } );
2353 *
2354 * @example
2355 * // Disable multiple fields by using an array of field names
2356 * editor.disable( ['account_type', 'access_level'] );
2357 */
2358 Editor.prototype.disable = function ( name )
2359 {
2360 var fields = this.s.fields;
2361
2362 $.each( this._fieldNames( name ), function ( i, n ) {
2363 fields[ n ].disable();
2364 } );
2365
2366 return this;
2367 };
2368
2369
2370 /**
2371 * Display, or remove the editing form from the display
2372 * @param {boolean} show Show (`true`) or hide (`false`)
2373 * @return {Editor} Editor instance, for chaining
2374 */
2375 Editor.prototype.display = function ( show )
2376 {
2377 if ( show === undefined ) {
2378 return this.s.displayed;
2379 }
2380 return this[ show ? 'open' : 'close' ]();
2381 };
2382
2383
2384 /**
2385 * Fields which are currently displayed
2386 * @return {string[]} Field names that are shown
2387 */
2388 Editor.prototype.displayed = function ()
2389 {
2390 return $.map( this.s.fields, function ( field, name ) {
2391 return field.displayed() ? name : null;
2392 } );
2393 };
2394
2395
2396 /**
2397 * Get display controller node
2398 *
2399 * @return {node} Display controller host element
2400 */
2401 Editor.prototype.displayNode = function ()
2402 {
2403 return this.s.displayController.node( this );
2404 };
2405
2406
2407 /**
2408 * Edit a record - show the form, pre-populated with the data that is in the given
2409 * DataTables row, that allows the user to enter information for the row to be modified
2410 * and then subsequently submit that data.
2411 * @param {node} items The TR element from the DataTable that is to be edited
2412 * @param {boolean} [show=true] Show the form or not.
2413 * @return {Editor} Editor instance, for chaining
2414 *
2415 * @example
2416 * // Show the edit form for the first row in the DataTable with a submit button
2417 * editor.edit( $('#example tbody tr:eq(0)')[0], 'Edit record', {
2418 * "label": "Update",
2419 * "fn": function () { this.submit(); }
2420 * } );
2421 *
2422 * @example
2423 * // Use the title and buttons API methods to show an edit form (this provides
2424 * // the same result as example above, but is a different way of achieving it
2425 * editor.title( 'Edit record' );
2426 * editor.buttons( {
2427 * "label": "Update",
2428 * "fn": function () { this.submit(); }
2429 * } );
2430 * editor.edit( $('#example tbody tr:eq(0)')[0] );
2431 *
2432 * @example
2433 * // Automatically submit an edit without showing the user the form
2434 * editor.edit( TRnode, null, null, false );
2435 * editor.set( 'name', 'Updated name' );
2436 * editor.set( 'access', 'Read only' );
2437 * editor.submit();
2438 */
2439 Editor.prototype.edit = function ( items, arg1, arg2, arg3, arg4 )
2440 {
2441 var that = this;
2442
2443 // Some other field in inline edit mode?
2444 if ( this._tidy( function () { that.edit( items, arg1, arg2, arg3, arg4 ); } ) ) {
2445 return this;
2446 }
2447
2448 var fields = this.s.fields;
2449 var argOpts = this._crudArgs( arg1, arg2, arg3, arg4 );
2450
2451 this._edit( items, this._dataSource( 'fields', items ), 'main' );
2452 this._assembleMain();
2453 this._formOptions( argOpts.opts );
2454
2455 argOpts.maybeOpen();
2456
2457 return this;
2458 };
2459
2460
2461 /**
2462 * Enable one or more field inputs, restoring user interaction with the fields.
2463 * @param {string|array} name The field name (from the `name` parameter given when
2464 * originally setting up the field) to enable, or an array of field names to enable
2465 * multiple fields with a single call.
2466 * @return {Editor} Editor instance, for chaining
2467 *
2468 * @example
2469 * // Show a 'create' form with buttons which will enable and disable certain fields
2470 * editor.create( 'Add new user', [
2471 * {
2472 * "label": "User name only",
2473 * "fn": function () {
2474 * this.enable('username');
2475 * this.disable( ['first_name', 'last_name'] );
2476 * }
2477 * }, {
2478 * "label": "Name based",
2479 * "fn": function () {
2480 * this.disable('username');
2481 * this.enable( ['first_name', 'last_name'] );
2482 * }
2483 * }, {
2484 * "label": "Submit",
2485 * "fn": function () { this.submit(); }
2486 * }
2487 * );
2488 */
2489 Editor.prototype.enable = function ( name )
2490 {
2491 var fields = this.s.fields;
2492
2493 $.each( this._fieldNames( name ), function ( i, n ) {
2494 fields[ n ].enable();
2495 } );
2496
2497 return this;
2498 };
2499
2500
2501 /**
2502 * Show that a field, or the form globally, is in an error state. Note that
2503 * errors are cleared on each submission of the form.
2504 * @param {string} [name] The name of the field that is in error. If not
2505 * given then the global form error display is used.
2506 * @param {string} msg The error message to show
2507 * @return {Editor} Editor instance, for chaining
2508 *
2509 * @example
2510 * // Show an error if the field is required
2511 * editor.create( 'Add new user', {
2512 * "label": "Submit",
2513 * "fn": function () {
2514 * if ( this.get('username') === '' ) {
2515 * this.error( 'username', 'A user name is required' );
2516 * return;
2517 * }
2518 * this.submit();
2519 * }
2520 * } );
2521 *
2522 * @example
2523 * // Show a field and a global error for a required field
2524 * editor.create( 'Add new user', {
2525 * "label": "Submit",
2526 * "fn": function () {
2527 * if ( this.get('username') === '' ) {
2528 * this.error( 'username', 'A user name is required' );
2529 * this.error( 'The data could not be saved because it is incomplete' );
2530 * return;
2531 * }
2532 * this.submit();
2533 * }
2534 * } );
2535 */
2536 Editor.prototype.error = function ( name, msg )
2537 {
2538 if ( msg === undefined ) {
2539 // Global error
2540 this._message( this.dom.formError, name );
2541 }
2542 else {
2543 // Field error
2544 this.s.fields[ name ].error( msg );
2545 }
2546
2547 return this;
2548 };
2549
2550
2551 /**
2552 * Get a field object, configured for a named field, which can then be
2553 * manipulated through its API. This function effectively acts as a
2554 * proxy to the field extensions, allowing easy access to the methods
2555 * for a named field. The methods that are available depend upon the field
2556 * type plug-in for Editor.
2557 *
2558 * @param {string} name Field name to be obtained
2559 * @return {Editor.Field} Field instance
2560 *
2561 * @example
2562 * // Update the values available in a select list
2563 * editor.field('island').update( [
2564 * 'Lewis and Harris',
2565 * 'South Uist',
2566 * 'North Uist',
2567 * 'Benbecula',
2568 * 'Barra'
2569 * ] );
2570 *
2571 * @example
2572 * // Equivalent calls
2573 * editor.field('name').set('John Smith');
2574 *
2575 * // results in the same action as:
2576 * editor.set('John Smith');
2577 */
2578 Editor.prototype.field = function ( name )
2579 {
2580 return this.s.fields[ name ];
2581 };
2582
2583
2584 /**
2585 * Get a list of the fields that are used by the Editor instance.
2586 * @returns {string[]} Array of field names
2587 *
2588 * @example
2589 * // Get current fields and move first item to the end
2590 * var fields = editor.fields();
2591 * var first = fields.shift();
2592 * fields.push( first );
2593 * editor.order( fields );
2594 */
2595 Editor.prototype.fields = function ()
2596 {
2597 return $.map( this.s.fields, function ( field, name ) {
2598 return name;
2599 } );
2600 };
2601
2602
2603 /**
2604 * Get the value of a field
2605 * @param {string|array} [name] The field name (from the `name` parameter given
2606 * when originally setting up the field) to disable. If not given, then an
2607 * object of fields is returned, with the value of each field from the
2608 * instance represented in the array (the object properties are the field
2609 * names). Also an array of field names can be given to get a collection of
2610 * data from the form.
2611 * @returns {*|object} Value from the named field
2612 *
2613 * @example
2614 * // Client-side validation - check that a field has been given a value
2615 * // before submitting the form
2616 * editor.create( 'Add new user', {
2617 * "label": "Submit",
2618 * "fn": function () {
2619 * if ( this.get('username') === '' ) {
2620 * this.error( 'username', 'A user name is required' );
2621 * return;
2622 * }
2623 * this.submit();
2624 * }
2625 * } );
2626 */
2627 Editor.prototype.get = function ( name )
2628 {
2629 var fields = this.s.fields;
2630
2631 if ( ! name ) {
2632 name = this.fields();
2633 }
2634
2635 if ( $.isArray( name ) ) {
2636 var out = {};
2637
2638 $.each( name, function (i, n) {
2639 out[n] = fields[n].get();
2640 } );
2641
2642 return out;
2643 }
2644
2645 return fields[ name ].get();
2646 };
2647
2648
2649 /**
2650 * Remove a field from the form display. Note that the field will still be submitted
2651 * with the other fields in the form, but it simply won't be visible to the user.
2652 * @param {string|array} [name] The field name (from the `name` parameter given when
2653 * originally setting up the field) to hide or an array of names. If not given then all
2654 * fields are hidden.
2655 * @param {boolean} [animate=true] Animate if visible
2656 * @return {Editor} Editor instance, for chaining
2657 *
2658 * @example
2659 * // Show a 'create' record form, but with some fields hidden
2660 * editor.hide( 'account_type' );
2661 * editor.hide( 'access_level' );
2662 * editor.create( 'Add new user', {
2663 * "label": "Save",
2664 * "fn": function () { this.submit(); }
2665 * } );
2666 *
2667 * @example
2668 * // Show a single field by hiding all and then showing one
2669 * editor.hide();
2670 * editor.show('access_type');
2671 */
2672 Editor.prototype.hide = function ( names, animate )
2673 {
2674 var fields = this.s.fields;
2675
2676 $.each( this._fieldNames( names ), function (i, n) {
2677 fields[ n ].hide( animate );
2678 } );
2679
2680 return this;
2681 };
2682
2683
2684 /**
2685 * Determine if there is an error state in the form, either the form's global
2686 * error message, or one or more fields.
2687 *
2688 * @param {string|array|undefined} [inNames] The field names to check. All
2689 * fields checked if undefined.
2690 * @return {boolean} `true` if there is an error in the form
2691 */
2692 Editor.prototype.inError = function ( inNames )
2693 {
2694 // Is there a global error?
2695 if ( $(this.dom.formError).is(':visible') ) {
2696 return true;
2697 }
2698
2699 // Field specific
2700 var fields = this.s.fields;
2701 var names = this._fieldNames( inNames );
2702
2703 for ( var i=0, ien=names.length ; i<ien ; i++ ) {
2704 if ( fields[ names[i] ].inError() ) {
2705 return true;
2706 }
2707 }
2708
2709 return false;
2710 };
2711
2712
2713 /**
2714 * Inline editing for a single field. This method provides a method to allow
2715 * end users to very quickly edit fields in place. For example, a user could
2716 * simply click on a cell in a table, the contents of which would be replaced
2717 * with the editing input field for that cell.
2718 *
2719 * @param {string|node|DataTables.Api|cell-selector} cell The cell or field to
2720 * be edited (note that for table editing this must be a cell - for standalone
2721 * editing it can also be the field name to edit).
2722 * @param {string} [fieldName] The field name to be edited. This parameter is
2723 * optional. If not provided, Editor will attempt to resolve the correct field
2724 * from the cell / element given as the first parameter. If it is unable to do
2725 * so, it will throw an error.
2726 * @param {object} [opts] Inline editing options - see the `form-options` type
2727 * @return {Editor} Editor instance, for chaining
2728 */
2729 Editor.prototype.inline = function ( cell, fieldName, opts )
2730 {
2731 var that = this;
2732
2733 // Argument shifting
2734 if ( $.isPlainObject( fieldName ) ) {
2735 opts = fieldName;
2736 fieldName = undefined;
2737 }
2738
2739 opts = $.extend( {}, this.s.formOptions.inline, opts );
2740
2741 var editFields = this._dataSource( 'individual', cell, fieldName );
2742 var node, field;
2743 var countOuter=0, countInner;
2744 var closed=false;
2745
2746 // Read the individual cell information from the editFields object
2747 $.each( editFields, function ( i, editField ) {
2748 // Only a single row
2749 if ( countOuter > 0 ) {
2750 throw 'Cannot edit more than one row inline at a time';
2751 }
2752
2753 node = $(editField.attach[0]);
2754
2755 // Only a single item in that row
2756 countInner = 0;
2757 $.each( editField.displayFields, function ( j, f ) {
2758 if ( countInner > 0 ) {
2759 throw 'Cannot edit more than one field inline at a time';
2760 }
2761
2762 field = f;
2763 countInner++;
2764 } );
2765
2766 countOuter++;
2767
2768 // If only changed values are to be submitted, then only allow the
2769 // individual field that we are editing to be edited.
2770 // This is currently disabled, as I'm not convinced that it is actually
2771 // useful!
2772 // if ( opts.submit === 'changed' ) {
2773 // editField.fields = editField.displayFields;
2774 // }
2775 } );
2776
2777 // Already in edit mode for this cell?
2778 if ( $('div.DTE_Field', node).length ) {
2779 return this;
2780 }
2781
2782 // Some other field in inline edit mode?
2783 if ( this._tidy( function () { that.inline( cell, fieldName, opts ); } ) ) {
2784 return this;
2785 }
2786
2787 // Start a full row edit, but don't display - we will be showing the field
2788 this._edit( cell, editFields, 'inline' );
2789 var namespace = this._formOptions( opts );
2790
2791 var ret = this._preopen( 'inline' );
2792 if ( ! ret ) {
2793 return this;
2794 }
2795
2796 // Remove from DOM, keeping event handlers, and include text nodes in remove
2797 var children = node.contents().detach();
2798
2799 node.append( $(
2800 '<div class="DTE DTE_Inline">'+
2801 '<div class="DTE_Inline_Field"/>'+
2802 '<div class="DTE_Inline_Buttons"/>'+
2803 '</div>'
2804 ) );
2805
2806 node.find('div.DTE_Inline_Field').append( field.node() );
2807
2808 if ( opts.buttons ) {
2809 // Use prepend for the CSS, so we can float the buttons right
2810 node.find('div.DTE_Inline_Buttons').append( this.dom.buttons );
2811 }
2812
2813 this._closeReg( function ( submitComplete ) {
2814 // Mark that this specific inline edit has closed
2815 closed = true;
2816
2817 $(document).off( 'click'+namespace );
2818
2819 // If there was no submit, we need to put the DOM back as it was. If
2820 // there was a submit, the write of the new value will set the DOM to
2821 // how it should be
2822 if ( ! submitComplete ) {
2823 node.contents().detach();
2824 node.append( children );
2825 }
2826
2827 // Clear error messages "offline"
2828 that._clearDynamicInfo();
2829 } );
2830
2831 // Submit and blur actions
2832 setTimeout( function () {
2833 // If already closed, possibly due to some other aspect of the event
2834 // that triggered the inline call, don't add the event listener - it
2835 // isn't needed (and is dangerous)
2836 if ( closed ) {
2837 return;
2838 }
2839
2840 $(document).on( 'click'+namespace, function ( e ) {
2841 // Was the click inside or owned by the editing node? If not, then
2842 // come out of editing mode.
2843
2844 // andSelf is deprecated in jQ1.8, but we want 1.7 compat
2845 var back = $.fn.addBack ? 'addBack' : 'andSelf';
2846
2847 if ( ! field._typeFn( 'owns', e.target ) &&
2848 $.inArray( node[0], $(e.target).parents()[ back ]() ) === -1 )
2849 {
2850 that.blur();
2851 }
2852 } );
2853 }, 0 );
2854
2855 this._focus( [ field ], opts.focus );
2856 this._postopen( 'inline' );
2857
2858 return this;
2859 };
2860
2861
2862 /**
2863 * Show an information message for the form as a whole, or for an individual
2864 * field. This can be used to provide helpful information to a user about an
2865 * individual field, or more typically the form (for example when deleting
2866 * a record and asking for confirmation).
2867 * @param {string} [name] The name of the field to show the message for. If not
2868 * given then a global message is shown for the form
2869 * @param {string|function} msg The message to show
2870 * @return {Editor} Editor instance, for chaining
2871 *
2872 * @example
2873 * // Show a global message for a 'create' form
2874 * editor.message( 'Add a new user to the database by completing the fields below' );
2875 * editor.create( 'Add new user', {
2876 * "label": "Submit",
2877 * "fn": function () { this.submit(); }
2878 * } );
2879 *
2880 * @example
2881 * // Show a message for an individual field when a 'help' icon is clicked on
2882 * $('#user_help').click( function () {
2883 * editor.message( 'user', 'The user name is what the system user will login with' );
2884 * } );
2885 */
2886 Editor.prototype.message = function ( name, msg )
2887 {
2888 if ( msg === undefined ) {
2889 // Global message
2890 this._message( this.dom.formInfo, name );
2891 }
2892 else {
2893 // Field message
2894 this.s.fields[ name ].message( msg );
2895 }
2896
2897 return this;
2898 };
2899
2900
2901 /**
2902 * Get which mode of operation the Editor form is in
2903 * @return {string} `create`, `edit`, `remove` or `null` if no active state.
2904 */
2905 Editor.prototype.mode = function ()
2906 {
2907 return this.s.action;
2908 };
2909
2910
2911 /**
2912 * Get the modifier that was used to trigger the edit or delete action.
2913 * @return {*} The identifier that was used for the editing / remove method
2914 * called.
2915 */
2916 Editor.prototype.modifier = function ()
2917 {
2918 return this.s.modifier;
2919 };
2920
2921
2922 /**
2923 * Get the values from one or more fields, taking into account multiple data
2924 * points being edited at the same time.
2925 *
2926 * @param {string|array} fieldNames A single field name or an array of field
2927 * names.
2928 * @return {object} If a string is given as the first parameter an object that
2929 * contains the value for each row being edited is returned. If an array is
2930 * given, then the object has the field names as the parameter name and the
2931 * value is the value object with values for each row being edited.
2932 */
2933 Editor.prototype.multiGet = function ( fieldNames )
2934 {
2935 var fields = this.s.fields;
2936
2937 if ( fieldNames === undefined ) {
2938 fieldNames = this.fields();
2939 }
2940
2941 if ( $.isArray( fieldNames ) ) {
2942 var out = {};
2943
2944 $.each( fieldNames, function ( i, name ) {
2945 out[ name ] = fields[ name ].multiGet();
2946 } );
2947
2948 return out;
2949 }
2950
2951 return fields[ fieldNames ].multiGet();
2952 };
2953
2954
2955 /**
2956 * Set the values for one or more fields, taking into account multiple data
2957 * points being edited at the same time.
2958 *
2959 * @param {object|string} fieldNames The name of the field to set, or an object
2960 * with the field names as the parameters that contains the value object to
2961 * set for each field.
2962 * @param {*} [val] Value to set if first parameter is given as a string.
2963 * Otherwise it is ignored.
2964 * @return {Editor} Editor instance, for chaining
2965 */
2966 Editor.prototype.multiSet = function ( fieldNames, val )
2967 {
2968 var fields = this.s.fields;
2969
2970 if ( $.isPlainObject( fieldNames ) && val === undefined ) {
2971 $.each( fieldNames, function ( name, value ) {
2972 fields[ name ].multiSet( value );
2973 } );
2974 }
2975 else {
2976 fields[ fieldNames ].multiSet( val );
2977 }
2978
2979 return this;
2980 };
2981
2982
2983 /**
2984 * Get the container node for an individual field.
2985 * @param {string|array} name The field name (from the `name` parameter given
2986 * when originally setting up the field) to get the DOM node for.
2987 * @return {node|array} Field container node
2988 *
2989 * @example
2990 * // Dynamically add a class to a field's container
2991 * $(editor.node( 'account_type' )).addClass( 'account' );
2992 */
2993 Editor.prototype.node = function ( name )
2994 {
2995 var fields = this.s.fields;
2996
2997 if ( ! name ) {
2998 name = this.order();
2999 }
3000
3001 return $.isArray( name ) ?
3002 $.map( name, function (n) {
3003 return fields[ n ].node();
3004 } ) :
3005 fields[ name ].node();
3006 };
3007
3008
3009 /**
3010 * Remove a bound event listener to the editor instance. This method provides a
3011 * shorthand way of binding jQuery events that would be the same as writing
3012 * `$(editor).off(...)` for convenience.
3013 * @param {string} name Event name to remove the listeners for - event names are
3014 * defined by {@link Editor}.
3015 * @param {function} [fn] The function to remove. If not given, all functions which
3016 * are assigned to the given event name will be removed.
3017 * @return {Editor} Editor instance, for chaining
3018 *
3019 * @example
3020 * // Add an event to alert when the form is shown and then remove the listener
3021 * // so it will only fire once
3022 * editor.on( 'open', function () {
3023 * alert('Form displayed!');
3024 * editor.off( 'open' );
3025 * } );
3026 */
3027 Editor.prototype.off = function ( name, fn )
3028 {
3029 $(this).off( this._eventName( name ), fn );
3030
3031 return this;
3032 };
3033
3034
3035 /**
3036 * Listen for an event which is fired off by Editor when it performs certain
3037 * actions. This method provides a shorthand way of binding jQuery events that
3038 * would be the same as writing `$(editor).on(...)` for convenience.
3039 * @param {string} name Event name to add the listener for - event names are
3040 * defined by {@link Editor}.
3041 * @param {function} fn The function to run when the event is triggered.
3042 * @return {Editor} Editor instance, for chaining
3043 *
3044 * @example
3045 * // Log events on the console when they occur
3046 * editor.on( 'open', function () { console.log( 'Form opened' ); } );
3047 * editor.on( 'close', function () { console.log( 'Form closed' ); } );
3048 * editor.on( 'submit', function () { console.log( 'Form submitted' ); } );
3049 */
3050 Editor.prototype.on = function ( name, fn )
3051 {
3052 $(this).on( this._eventName( name ), fn );
3053
3054 return this;
3055 };
3056
3057
3058 /**
3059 * Listen for a single event event which is fired off by Editor when it performs
3060 * certain actions. This method provides a shorthand way of binding jQuery
3061 * events that would be the same as writing `$(editor).one(...)` for
3062 * convenience.
3063 * @param {string} name Event name to add the listener for - event names are
3064 * defined by {@link Editor}.
3065 * @param {function} fn The function to run when the event is triggered.
3066 * @return {Editor} Editor instance, for chaining
3067 */
3068 Editor.prototype.one = function ( name, fn )
3069 {
3070 $(this).one( this._eventName( name ), fn );
3071
3072 return this;
3073 };
3074
3075
3076 /**
3077 * Display the main form editor to the end user in the web-browser.
3078 *
3079 * Note that the `close()` method will close any of the three Editor form types
3080 * (main, bubble and inline), but this method will open only the main type.
3081 * @return {Editor} Editor instance, for chaining
3082 *
3083 * @example
3084 * // Build a 'create' form, but don't display it until some values have
3085 * // been set. When done, then display the form.
3086 * editor.create( 'Create user', {
3087 * "label": "Submit",
3088 * "fn": function () { this.submit(); }
3089 * }, false );
3090 * editor.set( 'name', 'Test user' );
3091 * editor.set( 'access', 'Read only' );
3092 * editor.open();
3093 */
3094 Editor.prototype.open = function ()
3095 {
3096 var that = this;
3097
3098 // Insert the display elements in order
3099 this._displayReorder();
3100
3101 // Define how to do a close
3102 this._closeReg( function ( submitComplete ) {
3103 that.s.displayController.close( that, function () {
3104 that._clearDynamicInfo();
3105 } );
3106 } );
3107
3108 // Run the standard open with common events
3109 var ret = this._preopen( 'main' );
3110 if ( ! ret ) {
3111 return this;
3112 }
3113
3114 this.s.displayController.open( this, this.dom.wrapper );
3115 this._focus(
3116 $.map( this.s.order, function (name) {
3117 return that.s.fields[ name ];
3118 } ),
3119 this.s.editOpts.focus
3120 );
3121 this._postopen( 'main' );
3122
3123 return this;
3124 };
3125
3126
3127 /**
3128 * Get or set the ordering of fields, as they are displayed in the form. When used as
3129 * a getter, the field names are returned in an array, in their current order, and when
3130 * used as a setting you can alter the field ordering by passing in an array with all
3131 * field names in their new order.
3132 *
3133 * Note that all fields *must* be included when reordering, and no additional fields can
3134 * be added here (use {@link Editor#add} to add more fields). Finally, for setting the
3135 * order, you can pass an array of the field names, or give the field names as individual
3136 * parameters (see examples below).
3137 * @param {array|string} [set] Field order to set.
3138 * @return {Editor} Editor instance, for chaining
3139 *
3140 * @example
3141 * // Get field ordering
3142 * var order = editor.order();
3143 *
3144 * @example
3145 * // Set the field order
3146 * var order = editor.order();
3147 * order.unshift( order.pop() ); // move the last field into the first position
3148 * editor.order( order );
3149 *
3150 * @example
3151 * // Set the field order as arguments
3152 * editor.order( "pupil", "grade", "dept", "exam-board" );
3153 *
3154 */
3155 Editor.prototype.order = function ( set /*, ... */ )
3156 {
3157 if ( !set ) {
3158 return this.s.order;
3159 }
3160
3161 // Allow new layout to be passed in as arguments
3162 if ( arguments.length && ! $.isArray( set ) ) {
3163 set = Array.prototype.slice.call(arguments);
3164 }
3165
3166 // Sanity check - array must exactly match the fields we have available
3167 if ( this.s.order.slice().sort().join('-') !== set.slice().sort().join('-') ) {
3168 throw "All fields, and no additional fields, must be provided for ordering.";
3169 }
3170
3171 // Copy the new array into the order (so the reference is maintained)
3172 $.extend( this.s.order, set );
3173
3174 this._displayReorder();
3175
3176 return this;
3177 };
3178
3179
3180 /**
3181 * Remove (delete) entries from the table. The rows to remove are given as
3182 * either a single DOM node or an array of DOM nodes (including a jQuery
3183 * object).
3184 * @param {node|array} items The row, or array of nodes, to delete
3185 * @param {boolean} [show=true] Show the form or not.
3186 * @return {Editor} Editor instance, for chaining
3187 *
3188 * @example
3189 * // Delete a given row with a message to let the user know exactly what is
3190 * // happening
3191 * editor.message( "Are you sure you want to remove this row?" );
3192 * editor.remove( row_to_delete, 'Delete row', {
3193 * "label": "Confirm",
3194 * "fn": function () { this.submit(); }
3195 * } );
3196 *
3197 * @example
3198 * // Delete the first row in a table without asking the user for confirmation
3199 * editor.remove( '', $('#example tbody tr:eq(0)')[0], null, false );
3200 * editor.submit();
3201 *
3202 * @example
3203 * // Delete all rows in a table with a submit button
3204 * editor.remove( $('#example tbody tr'), 'Delete all rows', {
3205 * "label": "Delete all",
3206 * "fn": function () { this.submit(); }
3207 * } );
3208 */
3209 Editor.prototype.remove = function ( items, arg1, arg2, arg3, arg4 )
3210 {
3211 var that = this;
3212
3213 // Some other field in inline edit mode?
3214 if ( this._tidy( function () { that.remove( items, arg1, arg2, arg3, arg4 ); } ) ) {
3215 return this;
3216 }
3217
3218 // Allow a single row node to be passed in to remove, Can't use $.isArray
3219 // as we also allow array like objects to be passed in (API, jQuery)
3220 if ( items.length === undefined ) {
3221 items = [ items ];
3222 }
3223
3224 var argOpts = this._crudArgs( arg1, arg2, arg3, arg4 );
3225 var editFields = this._dataSource( 'fields', items );
3226
3227 this.s.action = "remove";
3228 this.s.modifier = items;
3229 this.s.editFields = editFields;
3230 this.dom.form.style.display = 'none';
3231
3232 this._actionClass();
3233
3234 this._event( 'initRemove', [
3235 _pluck( editFields, 'node' ),
3236 _pluck( editFields, 'data' ),
3237 items
3238 ] );
3239
3240 this._event( 'initMultiRemove', [
3241 editFields,
3242 items
3243 ] );
3244
3245 this._assembleMain();
3246 this._formOptions( argOpts.opts );
3247
3248 argOpts.maybeOpen();
3249
3250 var opts = this.s.editOpts;
3251 if ( opts.focus !== null ) {
3252 $('button', this.dom.buttons).eq( opts.focus ).focus();
3253 }
3254
3255 return this;
3256 };
3257
3258
3259 /**
3260 * Set the value of a field
3261 * @param {string|object} name The field name (from the `name` parameter given
3262 * when originally setting up the field) to set the value of. If given as an
3263 * object the object parameter name will be the value of the field to set and
3264 * the value the value to set for the field.
3265 * @param {*} [val] The value to set the field to. The format of the value will
3266 * depend upon the field type. Not required if the first parameter is given
3267 * as an object.
3268 * @return {Editor} Editor instance, for chaining
3269 *
3270 * @example
3271 * // Set the values of a few fields before then automatically submitting the form
3272 * editor.create( null, null, false );
3273 * editor.set( 'name', 'Test user' );
3274 * editor.set( 'access', 'Read only' );
3275 * editor.submit();
3276 */
3277 Editor.prototype.set = function ( set, val )
3278 {
3279 var fields = this.s.fields;
3280
3281 if ( ! $.isPlainObject( set ) ) {
3282 var o = {};
3283 o[ set ] = val;
3284 set = o;
3285 }
3286
3287 $.each( set, function (n, v) {
3288 fields[ n ].set( v );
3289 } );
3290
3291 return this;
3292 };
3293
3294
3295 /**
3296 * Show a field in the display that was previously hidden.
3297 * @param {string|array} [names] The field name (from the `name` parameter
3298 * given when originally setting up the field) to make visible, or an array of
3299 * field names to make visible. If not given all fields are shown.
3300 * @param {boolean} [animate=true] Animate if visible
3301 * @return {Editor} Editor instance, for chaining
3302 *
3303 * @example
3304 * // Shuffle the fields that are visible, hiding one field and making two
3305 * // others visible before then showing the {@link Editor#create} record form.
3306 * editor.hide( 'username' );
3307 * editor.show( 'account_type' );
3308 * editor.show( 'access_level' );
3309 * editor.create( 'Add new user', {
3310 * "label": "Save",
3311 * "fn": function () { this.submit(); }
3312 * } );
3313 *
3314 * @example
3315 * // Show all fields
3316 * editor.show();
3317 */
3318 Editor.prototype.show = function ( names, animate )
3319 {
3320 var fields = this.s.fields;
3321
3322 $.each( this._fieldNames( names ), function (i, n) {
3323 fields[ n ].show( animate );
3324 } );
3325
3326 return this;
3327 };
3328
3329
3330 /**
3331 * Submit a form to the server for processing. The exact action performed will depend
3332 * on which of the methods {@link Editor#create}, {@link Editor#edit} or
3333 * {@link Editor#remove} were called to prepare the form - regardless of which one is
3334 * used, you call this method to submit data.
3335 * @param {function} [successCallback] Callback function that is executed once the
3336 * form has been successfully submitted to the server and no errors occurred.
3337 * @param {function} [errorCallback] Callback function that is executed if the
3338 * server reports an error due to the submission (this includes a JSON formatting
3339 * error should the error return invalid JSON).
3340 * @param {function} [formatdata] Callback function that is passed in the data
3341 * that will be submitted to the server, allowing pre-formatting of the data,
3342 * removal of data or adding of extra fields.
3343 * @param {boolean} [hide=true] When the form is successfully submitted, by default
3344 * the form display will be hidden - this option allows that to be overridden.
3345 * @return {Editor} Editor instance, for chaining
3346 *
3347 * @example
3348 * // Submit data from a form button
3349 * editor.create( 'Add new record', {
3350 * "label": "Save",
3351 * "fn": function () {
3352 * this.submit();
3353 * }
3354 * } );
3355 *
3356 * @example
3357 * // Submit without showing the user the form
3358 * editor.create( null, null, false );
3359 * editor.submit();
3360 *
3361 * @example
3362 * // Provide success and error callback methods
3363 * editor.create( 'Add new record', {
3364 * "label": "Save",
3365 * "fn": function () {
3366 * this.submit( function () {
3367 * alert( 'Form successfully submitted!' );
3368 * }, function () {
3369 * alert( 'Form encountered an error :-(' );
3370 * }
3371 * );
3372 * }
3373 * } );
3374 *
3375 * @example
3376 * // Add an extra field to the data
3377 * editor.create( 'Add new record', {
3378 * "label": "Save",
3379 * "fn": function () {
3380 * this.submit( null, null, function (data) {
3381 * data.extra = "Extra information";
3382 * } );
3383 * }
3384 * } );
3385 *
3386 * @example
3387 * // Don't hide the form immediately - change the title and then close the form
3388 * // after a small amount of time
3389 * editor.create( 'Add new record', {
3390 * "label": "Save",
3391 * "fn": function () {
3392 * this.submit(
3393 * function () {
3394 * var that = this;
3395 * this.title( 'Data successfully added!' );
3396 * setTimeout( function () {
3397 * that.close();
3398 * }, 1000 );
3399 * },
3400 * null,
3401 * null,
3402 * false
3403 * );
3404 * }
3405 * } );
3406 *
3407 */
3408 Editor.prototype.submit = function ( successCallback, errorCallback, formatdata, hide )
3409 {
3410 var
3411 that = this,
3412 fields = this.s.fields,
3413 errorFields = [],
3414 errorReady = 0,
3415 sent = false;
3416
3417 if ( this.s.processing || ! this.s.action ) {
3418 return this;
3419 }
3420 this._processing( true );
3421
3422 // If there are fields in error, we want to wait for the error notification
3423 // to be cleared before the form is submitted - errorFields tracks the
3424 // fields which are in the error state, while errorReady tracks those which
3425 // are ready to submit
3426 var send = function () {
3427 if ( errorFields.length !== errorReady || sent ) {
3428 return;
3429 }
3430
3431 sent = true;
3432 that._submit( successCallback, errorCallback, formatdata, hide );
3433 };
3434
3435 // Remove the global error (don't know if the form is still in an error
3436 // state!)
3437 this.error();
3438
3439 // Count how many fields are in error
3440 $.each( fields, function ( name, field ) {
3441 if ( field.inError() ) {
3442 errorFields.push( name );
3443 }
3444 } );
3445
3446 // Remove the error display
3447 $.each( errorFields, function ( i, name ) {
3448 fields[ name ].error('', function () {
3449 errorReady++;
3450 send();
3451 } );
3452 } );
3453
3454 send();
3455
3456 return this;
3457 };
3458
3459
3460 /**
3461 * Set the title of the form
3462 * @param {string|function} title The title to give to the form
3463 * @return {Editor} Editor instance, for chaining
3464 *
3465 * @example
3466 * // Create an edit display used the title, buttons and edit methods (note that
3467 * // this is just an example, typically you would use the parameters of the edit
3468 * // method to achieve this.
3469 * editor.title( 'Edit record' );
3470 * editor.buttons( {
3471 * "label": "Update",
3472 * "fn": function () { this.submit(); }
3473 * } );
3474 * editor.edit( TR_to_edit );
3475 *
3476 * @example
3477 * // Show a create form, with a timer for the duration that the form is open
3478 * editor.create( 'Add new record - time on form: 0s', {
3479 * "label": "Save",
3480 * "fn": function () { this.submit(); }
3481 * } );
3482 *
3483 * // Add an event to the editor to stop the timer when the display is removed
3484 * var runTimer = true;
3485 * var timer = 0;
3486 * editor.on( 'close', function () {
3487 * runTimer = false;
3488 * editor.off( 'close' );
3489 * } );
3490 * // Start the timer running
3491 * updateTitle();
3492 *
3493 * // Local function to update the title once per second
3494 * function updateTitle() {
3495 * editor.title( 'Add new record - time on form: '+timer+'s' );
3496 * timer++;
3497 * if ( runTimer ) {
3498 * setTimeout( function() {
3499 * updateTitle();
3500 * }, 1000 );
3501 * }
3502 * }
3503 */
3504 Editor.prototype.title = function ( title )
3505 {
3506 var header = $(this.dom.header).children( 'div.'+this.classes.header.content );
3507
3508 if ( title === undefined ) {
3509 return header.html();
3510 }
3511
3512 if ( typeof title === 'function' ) {
3513 title = title( this, new DataTable.Api(this.s.table) );
3514 }
3515
3516 header.html( title );
3517
3518 return this;
3519 };
3520
3521
3522 /**
3523 * Get or set the value of a specific field, or get the value of all fields in
3524 * the form.
3525 *
3526 * @param {string|array} [names] The field name(s) to get or set the value of.
3527 * If not given, then the value of all fields will be obtained.
3528 * @param {*} [value] Value to set
3529 * @return {Editor|object|*} Editor instance, for chaining if used as a setter,
3530 * an object containing the values of the requested fields if used as a
3531 * getter with multiple fields requested, or the value of the requested field
3532 * if a single field is requested.
3533 */
3534 Editor.prototype.val = function ( field, value )
3535 {
3536 if ( value === undefined ) {
3537 return this.get( field ); // field can be undefined to get all
3538 }
3539
3540 return this.set( field, value );
3541 };
3542
3543
3544 /*
3545 * DataTables 1.10 API integration. Provides the ability to control basic Editor
3546 * aspects from the DataTables API. Full control does of course require use of
3547 * the Editor API though.
3548 */
3549 var apiRegister = DataTable.Api.register;
3550
3551
3552 function __getInst( api ) {
3553 var ctx = api.context[0];
3554 return ctx.oInit.editor || ctx._editor;
3555 }
3556
3557 // Set sensible defaults for the editing options
3558 function __setBasic( inst, opts, type, plural ) {
3559 if ( ! opts ) {
3560 opts = {};
3561 }
3562
3563 if ( opts.buttons === undefined ) {
3564 opts.buttons = '_basic';
3565 }
3566
3567 if ( opts.title === undefined ) {
3568 opts.title = inst.i18n[ type ].title;
3569 }
3570
3571 if ( opts.message === undefined ) {
3572 if ( type === 'remove' ) {
3573 var confirm = inst.i18n[ type ].confirm;
3574 opts.message = plural!==1 ? confirm._.replace(/%d/, plural) : confirm['1'];
3575 }
3576 else {
3577 opts.message = '';
3578 }
3579 }
3580
3581 return opts;
3582 }
3583
3584
3585 apiRegister( 'editor()', function () {
3586 return __getInst( this );
3587 } );
3588
3589 // Row editing
3590 apiRegister( 'row.create()', function ( opts ) {
3591 // main
3592 var inst = __getInst( this );
3593 inst.create( __setBasic( inst, opts, 'create' ) );
3594 return this;
3595 } );
3596
3597 apiRegister( 'row().edit()', function ( opts ) {
3598 // main
3599 var inst = __getInst( this );
3600 inst.edit( this[0][0], __setBasic( inst, opts, 'edit' ) );
3601 return this;
3602 } );
3603
3604 apiRegister( 'rows().edit()', function ( opts ) {
3605 // main
3606 var inst = __getInst( this );
3607 inst.edit( this[0], __setBasic( inst, opts, 'edit' ) );
3608 return this;
3609 } );
3610
3611 apiRegister( 'row().delete()', function ( opts ) {
3612 // main
3613 var inst = __getInst( this );
3614 inst.remove( this[0][0], __setBasic( inst, opts, 'remove', 1 ) );
3615 return this;
3616 } );
3617
3618 apiRegister( 'rows().delete()', function ( opts ) {
3619 // main
3620 var inst = __getInst( this );
3621 inst.remove( this[0], __setBasic( inst, opts, 'remove', this[0].length ) );
3622 return this;
3623 } );
3624
3625 apiRegister( 'cell().edit()', function ( type, opts ) {
3626 // inline or bubble
3627 if ( ! type ) {
3628 type = 'inline';
3629 }
3630 else if ( $.isPlainObject( type ) ) {
3631 opts = type;
3632 type = 'inline';
3633 }
3634
3635 __getInst( this )[ type ]( this[0][0], opts );
3636 return this;
3637 } );
3638
3639 apiRegister( 'cells().edit()', function ( opts ) {
3640 // bubble only at the moment
3641 __getInst( this ).bubble( this[0], opts );
3642 return this;
3643 } );
3644
3645 apiRegister( 'file()', function ( name, id ) {
3646 return Editor.files[ name ][ id ];
3647 } );
3648
3649 apiRegister( 'files()', function ( name, value ) {
3650 if ( ! name ) {
3651 return Editor.files;
3652 }
3653
3654 if ( ! value ) {
3655 return Editor.files[ name ];
3656 }
3657
3658 // The setter option of this method is not publicly documented
3659 Editor.files[ name ] = value;
3660
3661 return this;
3662 } );
3663
3664 // Global listener for file information updates via DataTables' Ajax JSON
3665 $(document).on( 'xhr.dt', function (e, ctx, json) {
3666 if ( e.namespace !== 'dt' ) {
3667 return;
3668 }
3669
3670 if ( json && json.files ) {
3671 $.each( json.files, function ( name, files ) {
3672 Editor.files[ name ] = files;
3673 } );
3674 }
3675 } );
3676
3677
3678 /**
3679 * Common error message emitter. This method is not (yet) publicly documented on
3680 * the Editor site. It might be in future.
3681 *
3682 * @param {string} msg Error message
3683 * @param {int} tn Tech note link
3684 */
3685 Editor.error = function ( msg, tn )
3686 {
3687 throw tn ?
3688 msg +' For more information, please refer to https://datatables.net/tn/'+tn :
3689 msg;
3690 };
3691
3692
3693 /**
3694 * Obtain label / value pairs of data from a data source, be it an array or
3695 * object, for use in an input that requires label / value pairs such as
3696 * `select`, `radio` and `checkbox` inputs.
3697 *
3698 * A callback function is triggered for each label / value pair found, so the
3699 * caller can add it to the input as required.
3700 *
3701 * @static
3702 * @param {object|array} An object or array of data to iterate over getting the
3703 * label / value pairs.
3704 * @param {object} props When an array of objects is passed in as the data
3705 * source by default the label will be read from the `label` property and
3706 * the value from the `value` property of the object. This option can alter
3707 * that behaviour.
3708 * @param {function} fn Callback function. Takes three parameters: the label,
3709 * the value and the iterator index.
3710 */
3711 Editor.pairs = function ( data, props, fn )
3712 {
3713 var i, ien, dataPoint;
3714
3715 // Define default properties to read the data from if using an object.
3716 // The passed in `props` object and override.
3717 props = $.extend( {
3718 label: 'label',
3719 value: 'value'
3720 }, props );
3721
3722 if ( $.isArray( data ) ) {
3723 // As an array, we iterate each item which can be an object or value
3724 for ( i=0, ien=data.length ; i<ien ; i++ ) {
3725 dataPoint = data[i];
3726
3727 if ( $.isPlainObject( dataPoint ) ) {
3728 fn(
3729 dataPoint[ props.value ] === undefined ?
3730 dataPoint[ props.label ] :
3731 dataPoint[ props.value ],
3732 dataPoint[ props.label ],
3733 i
3734 );
3735 }
3736 else {
3737 fn( dataPoint, dataPoint, i );
3738 }
3739 }
3740 }
3741 else {
3742 // As an object the key is the label and the value is the value
3743 i = 0;
3744
3745 $.each( data, function ( key, val ) {
3746 fn( val, key, i );
3747 i++;
3748 } );
3749 }
3750 };
3751
3752
3753 /**
3754 * Make a string safe to use as a DOM ID. This is primarily for use by field
3755 * plug-in authors.
3756 *
3757 * @static
3758 * @param {string} String to make safe
3759 * @param {string} Safe string
3760 */
3761 Editor.safeId = function ( id )
3762 {
3763 return id.replace(/\./g, '-');
3764 };
3765
3766
3767 /**
3768 * Field specific upload method. This can be used to upload a file to the Editor
3769 * libraries. This method is not (yet) publicly documented on the Editor site.
3770 * It might be in future.
3771 *
3772 * @static
3773 * @param {Editor} editor The Editor instance operating on
3774 * @param {object} conf Field configuration object
3775 * @param {Files} files The file(s) to upload
3776 * @param {function} progressCallback Upload progress callback
3777 * @param {function} completeCallback Callback function for once the file has
3778 * been uploaded
3779 */
3780 Editor.upload = function ( editor, conf, files, progressCallback, completeCallback )
3781 {
3782 var reader = new FileReader();
3783 var counter = 0;
3784 var ids = [];
3785 var generalError = 'A server error occurred while uploading the file';
3786
3787 // Clear any existing errors, as the new upload might not be in error
3788 editor.error( conf.name, '' );
3789
3790 progressCallback( conf, conf.fileReadText || "<i>Uploading file</i>" );
3791
3792 reader.onload = function ( e ) {
3793 var data = new FormData();
3794 var ajax;
3795
3796 data.append( 'action', 'upload' );
3797 data.append( 'uploadField', conf.name );
3798 data.append( 'upload', files[ counter ] );
3799
3800 if ( conf.ajaxData ) {
3801 conf.ajaxData( data );
3802 }
3803
3804 if ( conf.ajax ) {
3805 ajax = conf.ajax;
3806 }
3807 else if ( typeof editor.s.ajax === 'string' || $.isPlainObject( editor.s.ajax ) ) {
3808 ajax = editor.s.ajax;
3809 }
3810
3811 if ( ! ajax ) {
3812 throw 'No Ajax option specified for upload plug-in';
3813 }
3814
3815 if ( typeof ajax === 'string' ) {
3816 ajax = { url: ajax };
3817 }
3818
3819 // Use preSubmit to stop form submission during an upload, since the
3820 // value won't be known until that point.
3821 var submit = false;
3822 editor
3823 .on( 'preSubmit.DTE_Upload', function () {
3824 submit = true;
3825 return false;
3826 } );
3827
3828 $.ajax( $.extend( {}, ajax, {
3829 type: 'post',
3830 data: data,
3831 dataType: 'json',
3832 contentType: false,
3833 processData: false,
3834 xhr: function () {
3835 var xhr = $.ajaxSettings.xhr();
3836
3837 if ( xhr.upload ) {
3838 xhr.upload.onprogress = function ( e ) {
3839 if ( e.lengthComputable ) {
3840 var percent = (e.loaded/e.total*100).toFixed(0)+"%";
3841
3842 progressCallback( conf, files.length === 1 ?
3843 percent :
3844 counter+':'+files.length+' '+percent
3845 );
3846 }
3847 };
3848 xhr.upload.onloadend = function ( e ) {
3849 progressCallback( conf );
3850 };
3851 }
3852
3853 return xhr;
3854 },
3855 success: function ( json ) {
3856 editor.off( 'preSubmit.DTE_Upload' );
3857
3858 if ( json.fieldErrors && json.fieldErrors.length ) {
3859 var errors = json.fieldErrors;
3860
3861 for ( var i=0, ien=errors.length ; i<ien ; i++ ) {
3862 editor.error( errors[i].name, errors[i].status );
3863 }
3864 }
3865 else if ( json.error ) {
3866 editor.error( json.error );
3867 }
3868 else if ( ! json.upload || ! json.upload.id ) {
3869 editor.error( conf.name, generalError );
3870 }
3871 else {
3872 if ( json.files ) {
3873 $.each( json.files, function ( name, value ) {
3874 Editor.files[ name ] = value;
3875 } );
3876 }
3877
3878 ids.push( json.upload.id );
3879
3880 if ( counter < files.length-1 ) {
3881 counter++;
3882 reader.readAsDataURL( files[counter] );
3883 }
3884 else {
3885 completeCallback.call( editor, ids );
3886
3887 if ( submit ) {
3888 editor.submit();
3889 }
3890 }
3891 }
3892 },
3893 error: function () {
3894 editor.error( conf.name, generalError );
3895 }
3896 } ) );
3897 };
3898
3899 reader.readAsDataURL( files[0] );
3900 };
3901
3902
3903 /**
3904 * Editor constructor - take the developer configuration and apply it to the instance.
3905 * @param {object} init The initialisation options provided by the developer - see
3906 * {@link Editor.defaults} for a full list of options.
3907 * @private
3908 */
3909 Editor.prototype._constructor = function ( init )
3910 {
3911 init = $.extend( true, {}, Editor.defaults, init );
3912 this.s = $.extend( true, {}, Editor.models.settings, {
3913 table: init.domTable || init.table,
3914 dbTable: init.dbTable || null, // legacy
3915 ajaxUrl: init.ajaxUrl,
3916 ajax: init.ajax,
3917 idSrc: init.idSrc,
3918 dataSource: init.domTable || init.table ?
3919 Editor.dataSources.dataTable :
3920 Editor.dataSources.html,
3921 formOptions: init.formOptions,
3922 legacyAjax: init.legacyAjax
3923 } );
3924 this.classes = $.extend( true, {}, Editor.classes );
3925 this.i18n = init.i18n;
3926
3927 var that = this;
3928 var classes = this.classes;
3929
3930 this.dom = {
3931 "wrapper": $(
3932 '<div class="'+classes.wrapper+'">'+
3933 '<div data-dte-e="processing" class="'+classes.processing.indicator+'"></div>'+
3934 '<div data-dte-e="body" class="'+classes.body.wrapper+'">'+
3935 '<div data-dte-e="body_content" class="'+classes.body.content+'"/>'+
3936 '</div>'+
3937 '<div data-dte-e="foot" class="'+classes.footer.wrapper+'">'+
3938 '<div class="'+classes.footer.content+'"/>'+
3939 '</div>'+
3940 '</div>'
3941 )[0],
3942 "form": $(
3943 '<form data-dte-e="form" class="'+classes.form.tag+'">'+
3944 '<div data-dte-e="form_content" class="'+classes.form.content+'"/>'+
3945 '</form>'
3946 )[0],
3947 "formError": $('<div data-dte-e="form_error" class="'+classes.form.error+'"/>')[0],
3948 "formInfo": $('<div data-dte-e="form_info" class="'+classes.form.info+'"/>')[0],
3949 "header": $('<div data-dte-e="head" class="'+classes.header.wrapper+'"><div class="'+classes.header.content+'"/></div>')[0],
3950 "buttons": $('<div data-dte-e="form_buttons" class="'+classes.form.buttons+'"/>')[0]
3951 };
3952
3953 // Customise the TableTools buttons with the i18n settings - worth noting that
3954 // this could easily be done outside the Editor instance, but this makes things
3955 // a bit easier to understand and more cohesive. Also worth noting that when
3956 // there are two or more Editor instances, the init sequence should be
3957 // Editor / DataTables, Editor / DataTables etc, since the value of these button
3958 // instances matter when you create the TableTools buttons for the DataTable.
3959 if ( $.fn.dataTable.TableTools ) {
3960 var ttButtons = $.fn.dataTable.TableTools.BUTTONS;
3961 var i18n = this.i18n;
3962
3963 $.each(['create', 'edit', 'remove'], function (i, val) {
3964 ttButtons['editor_'+val].sButtonText = i18n[val].button;
3965 } );
3966 }
3967
3968 // Bind callback methods
3969 $.each( init.events, function (evt, fn) {
3970 that.on( evt, function () {
3971 // When giving events in the constructor the event argument was not
3972 // given in 1.2-, so we remove it here. This is solely for
3973 // backwards compatibility as the events in the initialisation are
3974 // not documented in 1.3+.
3975 var args = Array.prototype.slice.call(arguments);
3976 args.shift();
3977 fn.apply( that, args );
3978 } );
3979 } );
3980
3981 // Cache the DOM nodes
3982 var dom = this.dom;
3983 var wrapper = dom.wrapper;
3984 dom.formContent = _editor_el('form_content', dom.form)[0];
3985 dom.footer = _editor_el('foot', wrapper)[0];
3986 dom.body = _editor_el('body', wrapper)[0];
3987 dom.bodyContent = _editor_el('body_content', wrapper)[0];
3988 dom.processing = _editor_el('processing', wrapper)[0];
3989
3990 // Add any fields which are given on initialisation
3991 if ( init.fields ) {
3992 this.add( init.fields );
3993 }
3994
3995 $(document)
3996 .on( 'init.dt.dte', function (e, settings, json) {
3997 // Attempt to attach to a DataTable automatically when the table is
3998 // initialised
3999 if ( that.s.table && settings.nTable === $(that.s.table).get(0) ) {
4000 settings._editor = that;
4001 }
4002 } )
4003 .on( 'xhr.dt', function (e, settings, json) {
4004 // Automatically update fields which have a field name defined in
4005 // the returned json - saves an `initComplete` for the user
4006 if ( json && that.s.table && settings.nTable === $(that.s.table).get(0) ) {
4007 that._optionsUpdate( json );
4008 }
4009 } );
4010
4011 // Prep the display controller
4012 this.s.displayController = Editor.display[init.display].init( this );
4013
4014 this._event( 'initComplete', [] );
4015 };
4016
4017 /*global __inlineCounter*/
4018
4019 /**
4020 * Set the class on the form to relate to the action that is being performed.
4021 * This allows styling to be applied to the form to reflect the state that
4022 * it is in.
4023 *
4024 * @private
4025 */
4026 Editor.prototype._actionClass = function ()
4027 {
4028 var classesActions = this.classes.actions;
4029 var action = this.s.action;
4030 var wrapper = $(this.dom.wrapper);
4031
4032 wrapper.removeClass( [classesActions.create, classesActions.edit, classesActions.remove].join(' ') );
4033
4034 if ( action === "create" ) {
4035 wrapper.addClass( classesActions.create );
4036 }
4037 else if ( action === "edit" ) {
4038 wrapper.addClass( classesActions.edit );
4039 }
4040 else if ( action === "remove" ) {
4041 wrapper.addClass( classesActions.remove );
4042 }
4043 };
4044
4045
4046 /**
4047 * Create an Ajax request in the same style as DataTables 1.10, with full
4048 * backwards compatibility for Editor 1.2.
4049 *
4050 * @param {object} data Data to submit
4051 * @param {function} success Success callback
4052 * @param {function} error Error callback
4053 * @private
4054 */
4055 Editor.prototype._ajax = function ( data, success, error )
4056 {
4057 var opts = {
4058 type: 'POST',
4059 dataType: 'json',
4060 data: null,
4061 error: error,
4062 success: function ( json, status, xhr ) {
4063 if ( xhr.status === 204 ) {
4064 json = {};
4065 }
4066 success( json );
4067 }
4068 };
4069 var a;
4070 var action = this.s.action;
4071 var ajaxSrc = this.s.ajax || this.s.ajaxUrl;
4072 var id = action === 'edit' || action === 'remove' ?
4073 _pluck( this.s.editFields, 'idSrc' ) :
4074 null;
4075
4076 if ( $.isArray( id ) ) {
4077 id = id.join(',');
4078 }
4079
4080 // Get the correct object for rest style
4081 if ( $.isPlainObject( ajaxSrc ) && ajaxSrc[ action ] ) {
4082 ajaxSrc = ajaxSrc[ action ];
4083 }
4084
4085 if ( $.isFunction( ajaxSrc ) ) {
4086 // As a function, execute it, passing in the required parameters
4087 var uri = null;
4088 var method = null;
4089
4090 // If the old style ajaxSrc is given, we need to process it for
4091 // backwards compatibility with 1.2-. Unfortunate otherwise this would
4092 // be a very simply function!
4093 if ( this.s.ajaxUrl ) {
4094 var url = this.s.ajaxUrl;
4095
4096 if ( url.create ) {
4097 uri = url[ action ];
4098 }
4099
4100 if ( uri.indexOf(' ') !== -1 ) {
4101 a = uri.split(' ');
4102 method = a[0];
4103 uri = a[1];
4104 }
4105
4106 uri = uri.replace( /_id_/, id );
4107 }
4108
4109 ajaxSrc( method, uri, data, success, error );
4110 return;
4111 }
4112 else if ( typeof ajaxSrc === 'string' ) {
4113 // As a string it gives the URL. For backwards compatibility it can also
4114 // give the method.
4115 if ( ajaxSrc.indexOf(' ') !== -1 ) {
4116 a = ajaxSrc.split(' ');
4117 opts.type = a[0];
4118 opts.url = a[1];
4119 }
4120 else {
4121 opts.url = ajaxSrc;
4122 }
4123 }
4124 else {
4125 // As an object, we extend the defaults
4126 opts = $.extend( {}, opts, ajaxSrc || {} );
4127 }
4128
4129 // URL macros
4130 opts.url = opts.url.replace( /_id_/, id );
4131
4132 // Data processing option like in DataTables
4133 if ( opts.data ) {
4134 var newData = $.isFunction( opts.data ) ?
4135 opts.data( data ) : // fn can manipulate data or return an object
4136 opts.data; // object or array to merge
4137
4138 // If the function returned something, use that alone
4139 data = $.isFunction( opts.data ) && newData ?
4140 newData :
4141 $.extend( true, data, newData );
4142 }
4143
4144 opts.data = data;
4145
4146 // If a DELETE method is used there are a number of servers which will
4147 // reject the request if it has a body. So we need to append to the URL.
4148 //
4149 // http://stackoverflow.com/questions/15088955
4150 // http://bugs.jquery.com/ticket/11586
4151 if ( opts.type === 'DELETE' ) {
4152 var params = $.param( opts.data );
4153
4154 opts.url += opts.url.indexOf('?') === -1 ?
4155 '?'+params :
4156 '&'+params;
4157
4158 delete opts.data;
4159 }
4160
4161 // Finally, make the ajax call
4162 $.ajax( opts );
4163 };
4164
4165
4166 /**
4167 * Create the DOM structure from the source elements for the main form.
4168 * This is required since the elements can be moved around for other form types
4169 * (bubble).
4170 *
4171 * @private
4172 */
4173 Editor.prototype._assembleMain = function ()
4174 {
4175 var dom = this.dom;
4176
4177 $(dom.wrapper)
4178 .prepend( dom.header );
4179
4180 $(dom.footer)
4181 .append( dom.formError )
4182 .append( dom.buttons );
4183
4184 $(dom.bodyContent)
4185 .append( dom.formInfo )
4186 .append( dom.form );
4187 };
4188
4189
4190 /**
4191 * Blur the editing window. A blur is different from a close in that it might
4192 * cause either a close or the form to be submitted. A typical example of a
4193 * blur would be clicking on the background of the bubble or main editing forms
4194 * - i.e. it might be a close, or it might submit depending upon the
4195 * configuration, while a click on the close box is a very definite close.
4196 *
4197 * @private
4198 */
4199 Editor.prototype._blur = function ()
4200 {
4201 var opts = this.s.editOpts;
4202
4203 if ( this._event( 'preBlur' ) === false ) {
4204 return;
4205 }
4206
4207 if ( opts.onBlur === 'submit' ) {
4208 this.submit();
4209 }
4210 else if ( opts.onBlur === 'close' ) {
4211 this._close();
4212 }
4213 };
4214
4215
4216 /**
4217 * Clear all of the information that might have been dynamically set while
4218 * the form was visible - specifically errors and dynamic messages
4219 *
4220 * @private
4221 */
4222 Editor.prototype._clearDynamicInfo = function ()
4223 {
4224 var errorClass = this.classes.field.error;
4225 var fields = this.s.fields;
4226
4227 $('div.'+errorClass, this.dom.wrapper).removeClass( errorClass );
4228
4229 $.each( fields, function (name, field) {
4230 field
4231 .error('')
4232 .message('');
4233 } );
4234
4235 this
4236 .error('')
4237 .message('');
4238 };
4239
4240
4241 /**
4242 * Close an editing display, firing callbacks and events as needed
4243 *
4244 * @param {function} submitComplete Function to call after the preClose event
4245 * @private
4246 */
4247 Editor.prototype._close = function ( submitComplete )
4248 {
4249 // Allow preClose event to cancel the opening of the display
4250 if ( this._event( 'preClose' ) === false ) {
4251 return;
4252 }
4253
4254 if ( this.s.closeCb ) {
4255 this.s.closeCb( submitComplete );
4256 this.s.closeCb = null;
4257 }
4258
4259 if ( this.s.closeIcb ) {
4260 this.s.closeIcb();
4261 this.s.closeIcb = null;
4262 }
4263
4264 // Remove focus control
4265 $('body').off( 'focus.editor-focus' );
4266
4267 this.s.displayed = false;
4268 this._event( 'close' );
4269 };
4270
4271
4272 /**
4273 * Register a function to be called when the editing display is closed. This is
4274 * used by function that create the editing display to tidy up the display on
4275 * close - for example removing event handlers to prevent memory leaks.
4276 *
4277 * @param {function} fn Function to call on close
4278 * @private
4279 */
4280 Editor.prototype._closeReg = function ( fn )
4281 {
4282 this.s.closeCb = fn;
4283 };
4284
4285
4286 /**
4287 * Argument shifting for the create(), edit() and remove() methods. In Editor
4288 * 1.3 the preferred form of calling those three methods is with just two
4289 * parameters (one in the case of create() - the id and the show flag), while in
4290 * previous versions four / three parameters could be passed in, including the
4291 * buttons and title options. In 1.3 the chaining API is preferred, but we want
4292 * to support the old form as well, so this function is provided to perform
4293 * that argument shifting, common to all three.
4294 *
4295 * @private
4296 */
4297 Editor.prototype._crudArgs = function ( arg1, arg2, arg3, arg4 )
4298 {
4299 var that = this;
4300 var title;
4301 var buttons;
4302 var show;
4303 var opts;
4304
4305 if ( $.isPlainObject( arg1 ) ) {
4306 // Form options passed in as the first option
4307 opts = arg1;
4308 }
4309 else if ( typeof arg1 === 'boolean' ) {
4310 // Show / hide passed in as the first option - form options second
4311 show = arg1;
4312 opts = arg2; // can be undefined
4313 }
4314 else {
4315 // Old style arguments
4316 title = arg1; // can be undefined
4317 buttons = arg2; // can be undefined
4318 show = arg3; // can be undefined
4319 opts = arg4; // can be undefined
4320 }
4321
4322 // If all undefined, then fall into here
4323 if ( show === undefined ) {
4324 show = true;
4325 }
4326
4327 if ( title ) {
4328 that.title( title );
4329 }
4330
4331 if ( buttons ) {
4332 that.buttons( buttons );
4333 }
4334
4335 return {
4336 opts: $.extend( {}, this.s.formOptions.main, opts ),
4337 maybeOpen: function () {
4338 if ( show ) {
4339 that.open();
4340 }
4341 }
4342 };
4343 };
4344
4345
4346 /**
4347 * Execute the data source abstraction layer functions. This is simply a case
4348 * of executing the function with the Editor scope, passing in the remaining
4349 * parameters.
4350 *
4351 * @param {string) name Function name to execute
4352 * @private
4353 */
4354 Editor.prototype._dataSource = function ( name /*, ... */ )
4355 {
4356 // Remove the name from the arguments list, so the rest can be passed
4357 // straight into the field type
4358 var args = Array.prototype.slice.call( arguments );
4359 args.shift();
4360
4361 var fn = this.s.dataSource[ name ];
4362 if ( fn ) {
4363 return fn.apply( this, args );
4364 }
4365 };
4366
4367
4368 /**
4369 * Insert the fields into the DOM, in the correct order
4370 *
4371 * @private
4372 */
4373 Editor.prototype._displayReorder = function ( includeFields )
4374 {
4375 var formContent = $(this.dom.formContent);
4376 var fields = this.s.fields;
4377 var order = this.s.order;
4378
4379 if ( includeFields ) {
4380 this.s.includeFields = includeFields;
4381 }
4382 else {
4383 includeFields = this.s.includeFields;
4384 }
4385
4386 // Empty before adding in the required fields
4387 formContent.children().detach();
4388
4389 $.each( order, function (i, fieldOrName) {
4390 var name = fieldOrName instanceof Editor.Field ?
4391 fieldOrName.name() :
4392 fieldOrName;
4393
4394 if ( $.inArray( name, includeFields ) !== -1 ) {
4395 formContent.append( fields[ name ].node() );
4396 }
4397 } );
4398
4399 this._event( 'displayOrder', [
4400 this.s.displayed,
4401 this.s.action,
4402 formContent
4403 ] );
4404 };
4405
4406
4407 /**
4408 * Generic editing handler. This can be called by the three editing modes (main,
4409 * bubble and inline) to configure Editor for a row edit, and fire the required
4410 * events to ensure that the editing interfaces all provide a common API.
4411 *
4412 * @param {*} rows Identifier for the item(s) to be edited
4413 * @param {string} type Editing type - for the initEdit event
4414 * @private
4415 */
4416 Editor.prototype._edit = function ( items, editFields, type )
4417 {
4418 var that = this;
4419 var fields = this.s.fields;
4420 var usedFields = [];
4421 var includeInOrder;
4422
4423 this.s.editFields = editFields;
4424 this.s.modifier = items;
4425 this.s.action = "edit";
4426 this.dom.form.style.display = 'block';
4427
4428 this._actionClass();
4429
4430 // Setup the field values for editing
4431 $.each( fields, function ( name, field ) {
4432 field.multiReset();
4433 includeInOrder = true;
4434
4435 $.each( editFields, function ( idSrc, edit ) {
4436 if ( edit.fields[ name ] ) {
4437 var val = field.valFromData( edit.data );
4438
4439 field.multiSet( idSrc, val !== undefined ?
4440 val :
4441 field.def()
4442 );
4443
4444 // If there is an displayFields object, we need to know if this
4445 // field is present in it or not. If not, then the field isn't
4446 // displayed
4447 if ( edit.displayFields && ! edit.displayFields[ name ] ) {
4448 includeInOrder = false;
4449 }
4450 }
4451 } );
4452
4453 // If the field is used, then add it to the fields to be shown
4454 if ( field.multiIds().length !== 0 && includeInOrder ) {
4455 usedFields.push( name );
4456 }
4457 } );
4458
4459 // Remove the fields that are not required from the display
4460 var currOrder = this.order().slice();
4461
4462 for ( var i=currOrder.length ; i >= 0 ; i-- ) {
4463 if ( $.inArray( currOrder[i], usedFields ) === -1 ) {
4464 currOrder.splice( i, 1 );
4465 }
4466 }
4467
4468 this._displayReorder( currOrder );
4469
4470 // Save the set data values so we can decided in submit if data has changed
4471 this.s.editData = $.extend( true, {}, this.multiGet() );
4472
4473 // Events
4474 this._event( 'initEdit', [
4475 _pluck( editFields, 'node' )[0],
4476 _pluck( editFields, 'data' )[0],
4477 items,
4478 type
4479 ] );
4480
4481 this._event( 'initMultiEdit', [
4482 editFields,
4483 items,
4484 type
4485 ] );
4486 };
4487
4488
4489 /**
4490 * Fire callback functions and trigger events.
4491 *
4492 * @param {string|array} trigger Name(s) of the jQuery custom event to trigger
4493 * @param {array) args Array of arguments to pass to the triggered event
4494 * @return {*} Return from the event
4495 * @private
4496 */
4497 Editor.prototype._event = function ( trigger, args )
4498 {
4499 if ( ! args ) {
4500 args = [];
4501 }
4502
4503 // Allow an array to be passed in for the trigger to fire multiple events
4504 if ( $.isArray( trigger ) ) {
4505 for ( var i=0, ien=trigger.length ; i<ien ; i++ ) {
4506 this._event( trigger[i], args );
4507 }
4508 }
4509 else {
4510 var e = $.Event( trigger );
4511
4512 $(this).triggerHandler( e, args );
4513
4514 return e.result;
4515 }
4516 };
4517
4518
4519 /**
4520 * 'Modernise' event names, from the old style `on[A-Z]` names to camelCase.
4521 * This is done to provide backwards compatibility with Editor 1.2- event names.
4522 * The names themselves were updated for consistency with DataTables.
4523 *
4524 * @param {string} Event name to modernise
4525 * @return {string} String with new event name structure
4526 * @private
4527 */
4528 Editor.prototype._eventName = function ( input )
4529 {
4530 var name;
4531 var names = input.split( ' ' );
4532
4533 for ( var i=0, ien=names.length ; i<ien ; i++ ) {
4534 name = names[i];
4535
4536 // Strip the 'on' part and lowercase the first character
4537 var onStyle = name.match(/^on([A-Z])/);
4538 if ( onStyle ) {
4539 name = onStyle[1].toLowerCase() + name.substring( 3 );
4540 }
4541
4542 names[i] = name;
4543 }
4544
4545 return names.join( ' ' );
4546 };
4547
4548
4549 /**
4550 * Convert a field name input parameter to an array of field names.
4551 *
4552 * Many of the API methods provide the ability to pass `undefined` a string or
4553 * array of strings to identify fields. This method harmonises that.
4554 *
4555 * @param {array|string} [fieldNames] Field names to get
4556 * @return {array} Field names
4557 * @private
4558 */
4559 Editor.prototype._fieldNames = function ( fieldNames )
4560 {
4561 if ( fieldNames === undefined ) {
4562 return this.fields();
4563 }
4564 else if ( ! $.isArray( fieldNames ) ) {
4565 return [ fieldNames ];
4566 }
4567
4568 return fieldNames;
4569 };
4570
4571
4572 /**
4573 * Focus on a field. Providing the logic to allow complex focus expressions
4574 *
4575 * @param {array} fields Array of Field instances or field names for the fields
4576 * that are shown
4577 * @param {null|string|integer} focus Field identifier to focus on
4578 * @private
4579 */
4580 Editor.prototype._focus = function ( fieldsIn, focus )
4581 {
4582 var that = this;
4583 var field;
4584 var fields = $.map( fieldsIn, function ( fieldOrName ) {
4585 return typeof fieldOrName === 'string' ?
4586 that.s.fields[ fieldOrName ] :
4587 fieldOrName;
4588 } );
4589
4590 if ( typeof focus === 'number' ) {
4591 field = fields[ focus ];
4592 }
4593 else if ( focus ) {
4594 if ( focus.indexOf( 'jq:' ) === 0 ) {
4595 field = $('div.DTE '+focus.replace(/^jq:/, ''));
4596 }
4597 else {
4598 field = this.s.fields[ focus ];
4599 }
4600 }
4601
4602 this.s.setFocus = field;
4603
4604 if ( field ) {
4605 field.focus();
4606 }
4607 };
4608
4609
4610 /**
4611 * Form options - common function so all editing methods can provide the same
4612 * basic options, DRY.
4613 *
4614 * @param {object} opts Editing options. See model.formOptions
4615 * @private
4616 */
4617 Editor.prototype._formOptions = function ( opts )
4618 {
4619 var that = this;
4620 var inlineCount = __inlineCounter++;
4621 var namespace = '.dteInline'+inlineCount;
4622
4623 // Backwards compatibility with 1.4
4624 if ( opts.closeOnComplete !== undefined ) {
4625 opts.onComplete = opts.closeOnComplete ? 'close' : 'none';
4626 }
4627
4628 if ( opts.submitOnBlur !== undefined ) {
4629 opts.onBlur = opts.submitOnBlur ? 'submit' : 'close';
4630 }
4631
4632 if ( opts.submitOnReturn !== undefined ) {
4633 opts.onReturn = opts.submitOnReturn ? 'submit' : 'none';
4634 }
4635
4636 if ( opts.blurOnBackground !== undefined ) {
4637 opts.onBackground = opts.blurOnBackground ? 'blur' : 'none';
4638 }
4639
4640 this.s.editOpts = opts;
4641
4642 // When submitting by Ajax we don't want to close a form that has been
4643 // opened during the ajax request, so we keep a count of the form opening
4644 this.s.editCount = inlineCount;
4645
4646 if ( typeof opts.title === 'string' || typeof opts.title === 'function' ) {
4647 this.title( opts.title );
4648 opts.title = true;
4649 }
4650
4651 if ( typeof opts.message === 'string' || typeof opts.message === 'function' ) {
4652 this.message( opts.message );
4653 opts.message = true;
4654 }
4655
4656 if ( typeof opts.buttons !== 'boolean' ) {
4657 this.buttons( opts.buttons );
4658 opts.buttons = true;
4659 }
4660
4661 $(document).on( 'keydown'+namespace, function ( e ) {
4662 var el = $(document.activeElement);
4663 var name = el.length ? el[0].nodeName.toLowerCase() : null;
4664 var type = $(el).attr('type');
4665 var returnFriendlyNode = name === 'input';
4666
4667 if ( that.s.displayed && opts.onReturn === 'submit' && e.keyCode === 13 && returnFriendlyNode ) { // return
4668 e.preventDefault();
4669 that.submit();
4670 }
4671 else if ( e.keyCode === 27 ) { // esc
4672 e.preventDefault();
4673
4674 switch( opts.onEsc ) {
4675 case 'blur':
4676 that.blur();
4677 break;
4678
4679 case 'close':
4680 that.close();
4681 break;
4682
4683 case 'submit':
4684 that.submit();
4685 break;
4686
4687 default: // 'none' - no action
4688 break;
4689 }
4690 }
4691 else if ( el.parents('.DTE_Form_Buttons').length ) {
4692 if ( e.keyCode === 37 ) { // left
4693 el.prev( 'button' ).focus();
4694 }
4695 else if ( e.keyCode === 39 ) { // right
4696 el.next( 'button' ).focus();
4697 }
4698 }
4699 } );
4700
4701 this.s.closeIcb = function () {
4702 $(document).off( 'keydown'+namespace );
4703 };
4704
4705 return namespace;
4706 };
4707
4708
4709 /**
4710 * Convert from the 1.5+ data interchange format to the 1.4- format if suitable.
4711 *
4712 * @param {string} direction 'send' or 'receive'
4713 * @param {string} action CRUD action
4714 * @param {object} data Data object to transform
4715 * @private
4716 */
4717 Editor.prototype._legacyAjax = function ( direction, action, data )
4718 {
4719 if ( ! this.s.legacyAjax ) {
4720 return;
4721 }
4722
4723 if ( direction === 'send' ) {
4724 if ( action === 'create' || action === 'edit' ) {
4725 var id;
4726
4727 $.each( data.data, function ( rowId, values ) {
4728 if ( id !== undefined ) {
4729 throw 'Editor: Multi-row editing is not supported by the legacy Ajax data format';
4730 }
4731
4732 id = rowId;
4733 } );
4734
4735 data.data = data.data[ id ];
4736
4737 if ( action === 'edit' ) {
4738 data.id = id;
4739 }
4740 }
4741 else {
4742 data.id = $.map( data.data, function ( values, id ) {
4743 return id;
4744 } );
4745
4746 delete data.data;
4747 }
4748 }
4749 else {
4750 if ( ! data.data && data.row ) {
4751 // 1.4 libraries retuned data in the `row` property
4752 data.data = [ data.row ];
4753 }
4754 else {
4755 // 1.4- allowed data not to be returned - 1.5 requires it
4756 data.data = [];
4757 }
4758 }
4759 };
4760
4761
4762 /**
4763 * Update the field options from a JSON data source
4764 *
4765 * @param {object} json JSON object from the server
4766 * @private
4767 */
4768 Editor.prototype._optionsUpdate = function ( json )
4769 {
4770 var that = this;
4771
4772 if ( json.options ) {
4773 $.each( this.s.fields, function (name, field) {
4774 if ( json.options[ name ] !== undefined ) {
4775 var fieldInst = that.field( name );
4776
4777 if ( fieldInst && fieldInst.update ) {
4778 fieldInst.update( json.options[ name ] );
4779 }
4780 }
4781 } );
4782 }
4783 };
4784
4785
4786 /**
4787 * Show a message in the form. This can be used for error messages or dynamic
4788 * messages (information display) as the structure for each is basically the
4789 * same. This method will take into account if the form is visible or not - if
4790 * so then the message is shown with an effect for the end user, otherwise
4791 * it is just set immediately.
4792 *
4793 * @param {element} el The field display node to use
4794 * @param {string|function} msg The message to show
4795 * @private
4796 */
4797 Editor.prototype._message = function ( el, msg )
4798 {
4799 if ( typeof msg === 'function' ) {
4800 msg = msg( this, new DataTable.Api(this.s.table) );
4801 }
4802
4803 el = $(el);
4804
4805 if ( ! msg && this.s.displayed ) {
4806 // Clear the message with visual effect since the form is visible
4807 el
4808 .stop()
4809 .fadeOut( function () {
4810 el.html( '' );
4811 } );
4812 }
4813 else if ( ! msg ) {
4814 // Clear the message without visual effect
4815 el
4816 .html( '' )
4817 .css('display', 'none');
4818 }
4819 else if ( this.s.displayed ) {
4820 // Show the message with visual effect
4821 el
4822 .stop()
4823 .html( msg )
4824 .fadeIn();
4825 }
4826 else {
4827 // Show the message without visual effect
4828 el
4829 .html( msg )
4830 .css('display', 'block');
4831 }
4832 };
4833
4834
4835 /**
4836 * Update the multi-value information display to not show redundant information
4837 *
4838 * @private
4839 */
4840 Editor.prototype._multiInfo = function ()
4841 {
4842 var fields = this.s.fields;
4843 var include = this.s.includeFields;
4844 var show = true;
4845
4846 if ( ! include ) {
4847 return;
4848 }
4849
4850 for ( var i=0, ien=include.length ; i<ien ; i++ ) {
4851 var field = fields[ include[i] ];
4852
4853 if ( field.isMultiValue() && show ) {
4854 fields[ include[i] ].multiInfoShown( show );
4855 show = false;
4856 }
4857 else {
4858 fields[ include[i] ].multiInfoShown( false );
4859 }
4860 }
4861 };
4862
4863
4864 /**
4865 * Common display editing form method called by all editing methods after the
4866 * form has been configured and displayed. This is to ensure all fire the same
4867 * events.
4868 *
4869 * @param {string} Editing type
4870 * @return {boolean} `true`
4871 * @private
4872 */
4873 Editor.prototype._postopen = function ( type )
4874 {
4875 var that = this;
4876 var focusCapture = this.s.displayController.captureFocus;
4877 if ( focusCapture === undefined ) {
4878 focusCapture = true;
4879 }
4880
4881 $(this.dom.form)
4882 .off( 'submit.editor-internal' )
4883 .on( 'submit.editor-internal', function (e) {
4884 e.preventDefault();
4885 } );
4886
4887 // Focus capture - when the Editor form is shown we capture the browser's
4888 // focus action. Without doing this is would result in the user being able
4889 // to control items under the Editor display - triggering actions that
4890 // shouldn't be possible while the editing is shown.
4891 if ( focusCapture && (type === 'main' || type === 'bubble') ) {
4892 $('body').on( 'focus.editor-focus', function () {
4893 if ( $(document.activeElement).parents('.DTE').length === 0 &&
4894 $(document.activeElement).parents('.DTED').length === 0
4895 ) {
4896 if ( that.s.setFocus ) {
4897 that.s.setFocus.focus();
4898 }
4899 }
4900 } );
4901 }
4902
4903 this._multiInfo();
4904
4905 this._event( 'open', [type, this.s.action] );
4906
4907 return true;
4908 };
4909
4910
4911 /**
4912 * Common display editing form method called by all editing methods before the
4913 * form has been configured and displayed. This is to ensure all fire the same
4914 * events.
4915 *
4916 * @param {string} Editing type
4917 * @return {boolean} `false` if the open is cancelled by the preOpen event,
4918 * otherwise `true`
4919 * @private
4920 */
4921 Editor.prototype._preopen = function ( type )
4922 {
4923 // Allow preOpen event to cancel the opening of the display
4924 if ( this._event( 'preOpen', [type, this.s.action] ) === false ) {
4925 // Tidy- this would normally be done on close, but we never get that far
4926 this._clearDynamicInfo();
4927
4928 return false;
4929 }
4930
4931 this.s.displayed = type;
4932
4933 return true;
4934 };
4935
4936
4937 /**
4938 * Set the form into processing mode or take it out of processing mode. In
4939 * processing mode a processing indicator is shown and user interaction with the
4940 * form buttons is blocked
4941 *
4942 * @param {boolean} processing true if to go into processing mode and false if
4943 * to come out of processing mode
4944 * @private
4945 */
4946 Editor.prototype._processing = function ( processing )
4947 {
4948 var wrapper = $(this.dom.wrapper);
4949 var procStyle = this.dom.processing.style;
4950 var procClass = this.classes.processing.active;
4951
4952 if ( processing ) {
4953 procStyle.display = 'block';
4954 wrapper.addClass( procClass );
4955 $('div.DTE').addClass( procClass );
4956 }
4957 else {
4958 procStyle.display = 'none';
4959 wrapper.removeClass( procClass );
4960 $('div.DTE').removeClass( procClass );
4961 }
4962
4963 this.s.processing = processing;
4964
4965 this._event( 'processing', [processing] );
4966 };
4967
4968
4969 /**
4970 * Submit a form to the server for processing. This is the private method that is used
4971 * by the 'submit' API method, which should always be called in preference to calling
4972 * this method directly.
4973 *
4974 * @param {function} [successCallback] Callback function that is executed once the
4975 * form has been successfully submitted to the server and no errors occurred.
4976 * @param {function} [errorCallback] Callback function that is executed if the
4977 * server reports an error due to the submission (this includes a JSON formatting
4978 * error should the error return invalid JSON).
4979 * @param {function} [formatdata] Callback function that is passed in the data
4980 * that will be submitted to the server, allowing pre-formatting of the data,
4981 * removal of data or adding of extra fields.
4982 * @param {boolean} [hide=true] When the form is successfully submitted, by default
4983 * the form display will be hidden - this option allows that to be overridden.
4984 * @private
4985 */
4986 Editor.prototype._submit = function ( successCallback, errorCallback, formatdata, hide )
4987 {
4988 var that = this;
4989 var i, iLen, eventRet, errorNodes;
4990 var changed = false, allData = {}, changedData = {};
4991 var setBuilder = DataTable.ext.oApi._fnSetObjectDataFn;
4992 var dataSource = this.s.dataSource;
4993 var fields = this.s.fields;
4994 var action = this.s.action;
4995 var editCount = this.s.editCount;
4996 var modifier = this.s.modifier;
4997 var editFields = this.s.editFields;
4998 var editData = this.s.editData;
4999 var opts = this.s.editOpts;
5000 var changedSubmit = opts.submit;
5001 var submitParams = {
5002 "action": this.s.action,
5003 "data": {}
5004 };
5005 var submitParamsLocal;
5006
5007 // For backwards compatibility
5008 if ( this.s.dbTable ) {
5009 submitParams.table = this.s.dbTable;
5010 }
5011
5012 // Gather the data that is to be submitted
5013 if ( action === "create" || action === "edit" ) {
5014 $.each( editFields, function ( idSrc, edit ) {
5015 var allRowData = {};
5016 var changedRowData = {};
5017
5018 $.each( fields, function (name, field) {
5019 if ( edit.fields[ name ] ) {
5020 var value = field.multiGet( idSrc );
5021 var builder = setBuilder( name );
5022 var manyBuilder = $.isArray( value ) && name.indexOf('[]') !== -1 ?
5023 setBuilder( name.replace(/\[.*$/,'')+'-many-count' ) :
5024 null;
5025
5026 builder( allRowData, value );
5027
5028 // We need to tell the server-side if an array submission
5029 // actually has no elements so it knows if the array was
5030 // being submitted or not (since otherwise it doesn't know
5031 // if the array was empty, or just not being submitted)
5032 if ( manyBuilder ) {
5033 manyBuilder( allRowData, value.length );
5034 }
5035
5036 // Build a changed object for if that is the selected data
5037 // type
5038 if ( action === 'edit' && value !== editData[ name ][ idSrc ] ) {
5039 builder( changedRowData, value );
5040 changed = true;
5041
5042 if ( manyBuilder ) {
5043 manyBuilder( changedRowData, value.length );
5044 }
5045 }
5046 }
5047 } );
5048
5049 if ( ! $.isEmptyObject( allRowData ) ) {
5050 allData[ idSrc ] = allRowData;
5051 }
5052
5053 if ( ! $.isEmptyObject( changedRowData ) ) {
5054 changedData[ idSrc ] = changedRowData;
5055 }
5056 } );
5057
5058 // Decide what data to submit to the server for edit (create is all, always)
5059 if ( action === 'create' || changedSubmit === 'all' || (changedSubmit === 'allIfChanged' && changed) ) {
5060 submitParams.data = allData;
5061 }
5062 else if ( changedSubmit === 'changed' && changed ) {
5063 submitParams.data = changedData;
5064 }
5065 else {
5066 // Nothing to submit
5067 this.s.action = null;
5068
5069 if ( opts.onComplete === 'close' && (hide === undefined || hide) ) {
5070 this._close( false );
5071 }
5072
5073 if ( successCallback ) {
5074 successCallback.call( this );
5075 }
5076
5077 this._processing( false );
5078 this._event( 'submitComplete' );
5079 return;
5080 }
5081 }
5082 else if ( action === "remove" ) {
5083 $.each( editFields, function ( idSrc, edit ) {
5084 submitParams.data[ idSrc ] = edit.data;
5085 } );
5086 }
5087
5088 this._legacyAjax( 'send', action, submitParams );
5089
5090 // Local copy of the submit parameters, needed for the data lib prep since
5091 // the preSubmit can modify the format and we need to know what the format is
5092 submitParamsLocal = $.extend( true, {}, submitParams );
5093
5094 // Allow the data to be submitted to the server to be preprocessed by callback
5095 // and event functions
5096 if ( formatdata ) {
5097 formatdata( submitParams );
5098 }
5099 if ( this._event( 'preSubmit', [submitParams, action] ) === false ) {
5100 this._processing( false );
5101 return;
5102 }
5103
5104 // Submit to the server (or whatever method is defined in the settings)
5105 this._ajax(
5106 submitParams,
5107 function (json) {
5108 var setData;
5109 that._legacyAjax( 'receive', action, json );
5110 that._event( 'postSubmit', [json, submitParams, action] );
5111
5112 if ( !json.error ) {
5113 json.error = "";
5114 }
5115 if ( !json.fieldErrors ) {
5116 json.fieldErrors = [];
5117 }
5118
5119 if ( json.error || json.fieldErrors.length ) {
5120 // Global form error
5121 that.error( json.error );
5122
5123 // Field specific errors
5124 $.each( json.fieldErrors, function (i, err) {
5125 var field = fields[ err.name ];
5126
5127 field.error( err.status || "Error" );
5128
5129 if ( i === 0 ) {
5130 // Scroll the display to the first error and focus
5131 $(that.dom.bodyContent, that.s.wrapper).animate( {
5132 "scrollTop": $(field.node()).position().top
5133 }, 500 );
5134
5135 field.focus();
5136 }
5137 } );
5138
5139 if ( errorCallback ) {
5140 errorCallback.call( that, json );
5141 }
5142 }
5143 else {
5144 // Create a data store that the data source can use, which is
5145 // unique to this action
5146 var store = {};
5147 that._dataSource( 'prep', action, modifier, submitParamsLocal, json.data, store );
5148
5149 if ( action === "create" || action === "edit" ) {
5150 for ( i=0 ; i<json.data.length ; i++ ) {
5151 setData = json.data[ i ];
5152 that._event( 'setData', [json, setData, action] );
5153
5154 if ( action === "create" ) {
5155 // New row was created to add it to the DT
5156 that._event( 'preCreate', [json, setData] );
5157 that._dataSource( 'create', fields, setData, store );
5158 that._event( ['create', 'postCreate'], [json, setData] );
5159 }
5160 else if ( action === "edit" ) {
5161 // Row was updated, so tell the DT
5162 that._event( 'preEdit', [json, setData] );
5163 that._dataSource( 'edit', modifier, fields, setData, store );
5164 that._event( ['edit', 'postEdit'], [json, setData] );
5165 }
5166 }
5167 }
5168 else if ( action === "remove" ) {
5169 // Remove the rows given and then redraw the table
5170 that._event( 'preRemove', [json] );
5171 that._dataSource( 'remove', modifier, fields, store );
5172 that._event( ['remove', 'postRemove'], [json] );
5173 }
5174
5175 that._dataSource( 'commit', action, modifier, json.data, store );
5176
5177 // Submission complete
5178 if ( editCount === that.s.editCount ) {
5179 that.s.action = null;
5180
5181 if ( opts.onComplete === 'close' && (hide === undefined || hide) ) {
5182 that._close( true );
5183 }
5184 }
5185
5186 // All done - fire off the callbacks and events
5187 if ( successCallback ) {
5188 successCallback.call( that, json );
5189 }
5190 that._event( 'submitSuccess', [json, setData] );
5191 }
5192
5193 that._processing( false );
5194 that._event( 'submitComplete', [json, setData] );
5195 },
5196 function (xhr, err, thrown) {
5197 that._event( 'postSubmit', [xhr, err, thrown, submitParams] );
5198
5199 that.error( that.i18n.error.system );
5200 that._processing( false );
5201
5202 if ( errorCallback ) {
5203 errorCallback.call( that, xhr, err, thrown );
5204 }
5205
5206 that._event( ['submitError', 'submitComplete'], [xhr, err, thrown, submitParams] );
5207 }
5208 ); // /ajax submit
5209 };
5210
5211
5212 /**
5213 * Check to see if the form needs to be tidied before a new action can be performed.
5214 * This includes if the from is currently processing an old action and if it
5215 * is inline editing.
5216 *
5217 * @param {function} fn Callback function
5218 * @returns {boolean} `true` if was in inline mode, `false` otherwise
5219 * @private
5220 */
5221 Editor.prototype._tidy = function ( fn )
5222 {
5223 var that = this;
5224 var dt = this.s.table ?
5225 new $.fn.dataTable.Api( this.s.table ) :
5226 null;
5227
5228 var ssp = false;
5229 if ( dt ) {
5230 ssp = dt.settings()[0].oFeatures.bServerSide;
5231 }
5232
5233 if ( this.s.processing ) {
5234 // If currently processing, wait until the action is complete
5235 this.one( 'submitComplete', function () {
5236 // If server-side processing is being used in DataTables, first
5237 // check that we are still processing (might not be if nothing was
5238 // submitted) and then wait for the draw to finished
5239 if ( ssp ) {
5240 dt.one( 'draw', fn );
5241 }
5242 else {
5243 setTimeout( function () {
5244 fn();
5245 }, 10 );
5246 }
5247 } );
5248
5249 return true;
5250 }
5251 else if ( this.display() === 'inline' || this.display() === 'bubble' ) {
5252 // If there is an inline edit box, it needs to be tidied
5253 this
5254 .one( 'close', function () {
5255 // On close if processing then we need to wait for the submit to
5256 // complete before running the callback as onBlur was set to
5257 // submit
5258 if ( ! that.s.processing ) {
5259 // IE needs a small timeout, otherwise it may not focus on a
5260 // field if one already has focus
5261 setTimeout( function () {
5262 fn();
5263 }, 10 );
5264 }
5265 else {
5266 // Need to wait for the submit to finish
5267 that.one( 'submitComplete', function ( e, json ) {
5268 // If SSP then need to wait for the draw
5269 if ( ssp && json ) {
5270 dt.one( 'draw', fn );
5271 }
5272 else {
5273 setTimeout( function () {
5274 fn();
5275 }, 10 );
5276 }
5277 } );
5278 }
5279 } )
5280 .blur();
5281
5282 return true;
5283 }
5284
5285 return false;
5286 };
5287
5288
5289 /*
5290 * Defaults
5291 */
5292
5293
5294 // Dev node - although this file is held in the models directory (because it
5295 // really is a model, it is assigned to Editor.defaults for easy
5296 // and sensible access to set the defaults for Editor.
5297
5298 /**
5299 * Initialisation options that can be given to Editor at initialisation time.
5300 * @namespace
5301 */
5302 Editor.defaults = {
5303 /**
5304 * jQuery selector that can be used to identify the table you wish to apply
5305 * this editor instance to.
5306 *
5307 * In previous versions of Editor (1.2 and earlier), this parameter was
5308 * called `table`. The name has been altered in 1.3+ to simplify the
5309 * initialisation. This is a backwards compatible change - if you pass in
5310 * a `table` option it will be used.
5311 * @type string
5312 * @default <i>Empty string</i>
5313 *
5314 * @example
5315 * $(document).ready(function() {
5316 * var editor = new $.fn.Editor( {
5317 * "ajax": "php/index.php",
5318 * "table": "#example"
5319 * } );
5320 * } );
5321 */
5322 "table": null,
5323
5324 /**
5325 * The URL, or collection of URLs when using a REST interface, which will accept
5326 * the data for the create, edit and remove functions. The target script / program
5327 * must accept data in the format defined by Editor and return the expected JSON as
5328 * required by Editor. When given as an object, the `create`, `edit` and `remove`
5329 * properties should be defined, each being the URL to send the data to for that
5330 * action. When used as an object, the string `_id_` will be replaced for the edit
5331 * and remove actions, allowing a URL to be dynamically created for those actions.
5332 * @type string|object
5333 * @default <i>Empty string</i>
5334 * @deprecated This option has been deprecated in favour of the `ajax` option.
5335 * It can still be used, but it is recommended that you use the `ajax` option
5336 * which provides all of the abilities of this old option and more.
5337 */
5338 "ajaxUrl": null,
5339
5340 /**
5341 * Fields to initialise the form with - see {@link Editor.models.field} for
5342 * a full list of the options available to each field. Note that if fields are not
5343 * added to the form at initialisation time using this option, they can be added using
5344 * the {@link Editor#add} API method.
5345 * @type array
5346 * @default []
5347 *
5348 * @example
5349 * $(document).ready(function() {
5350 * var editor = new $.fn.Editor( {
5351 * "ajax": "php/index.php",
5352 * "table": "#example",
5353 * "fields": [ {
5354 * "label": "User name:",
5355 * "name": "username"
5356 * }
5357 * // More fields would typically be added here!
5358 * } ]
5359 * } );
5360 * } );
5361 */
5362 "fields": [],
5363
5364 /**
5365 * The display controller for the form. The form itself is just a collection of
5366 * DOM elements which require a display container. This display controller allows
5367 * the visual appearance of the form to be significantly altered without major
5368 * alterations to the Editor code. There are two display controllers built into
5369 * Editor *lightbox* and *envelope*. The value of this property will
5370 * be used to access the display controller defined in {@link Editor.display}
5371 * for the given name. Additional display controllers can be added by adding objects
5372 * to that object, through extending the displayController model:
5373 * {@link Editor.models.displayController}.
5374 * @type string
5375 * @default lightbox
5376 *
5377 * @example
5378 * $(document).ready(function() {
5379 * var editor = new $.fn.Editor( {
5380 * "ajax": "php/index.php",
5381 * "table": "#example",
5382 * "display": 'envelope'
5383 * } );
5384 * } );
5385 */
5386 "display": 'lightbox',
5387
5388 /**
5389 * Control how the Ajax call to update data on the server.
5390 *
5391 * This option matches the `dt-init ajax` option in that is can be provided
5392 * in one of three different ways:
5393 *
5394 * * string - As a string, the value given is used as the url to target
5395 * the Ajax request to, using the default Editor Ajax options. Note that
5396 * for backwards compatibility you can use the form "METHOD URL" - for
5397 * example: `"PUT api/users"`, although it is recommended you use the
5398 * object form described below.
5399 * * object - As an object, the `ajax` property has two forms:
5400 * * Used to extend and override the default Ajax options that Editor
5401 * uses. This can be very useful for adding extra data for example, or
5402 * changing the HTTP request type.
5403 * * With `create`, `edit` and `remove` properties, Editor will use the
5404 * option for the action that it is taking, which can be useful for
5405 * REST style interfaces. The value of each property can be a string,
5406 * object or function, using exactly the same options as the main `ajax`
5407 * option. All three options must be defined if this form is to be used.
5408 * * function - As a function this gives complete control over the method
5409 * used to update the server (if indeed a server is being used!). For
5410 * example, you could use a different data store such as localStorage,
5411 * Firebase or route the data through a web-socket.
5412 *
5413 * @example
5414 * // As a string - all actions are submitted to this URI as POST requests
5415 * $(document).ready(function() {
5416 * var editor = new $.fn.Editor( {
5417 * "ajax": 'php/index.php',
5418 * "table": "#example"
5419 * } );
5420 * } );
5421 *
5422 * @example
5423 * // As an object - using GET rather than POST
5424 * $(document).ready(function() {
5425 * var editor = new $.fn.Editor( {
5426 * "ajax": {
5427 * "type": 'GET',
5428 * "url": 'php/index.php
5429 * },
5430 * "table": "#example"
5431 * } );
5432 * } );
5433 *
5434 * @example
5435 * // As an object - each action is submitted to a different URI as POST requests
5436 * $(document).ready(function() {
5437 * var editor = new $.fn.Editor( {
5438 * "ajax": {
5439 * "create": "/rest/user/create",
5440 * "edit": "/rest/user/_id_/edit",
5441 * "remove": "/rest/user/_id_/delete"
5442 * },
5443 * "table": "#example"
5444 * } );
5445 * } );
5446 *
5447 * @example
5448 * // As an object - with different HTTP methods for each action
5449 * $(document).ready(function() {
5450 * var editor = new $.fn.Editor( {
5451 * "ajax": {
5452 * "create": {
5453 * type: 'POST',
5454 * url: '/rest/user/create'
5455 * },
5456 * "edit": {
5457 * type: 'PUT',
5458 * url: '/rest/user/edit/_id_'
5459 * },
5460 * "remove": {
5461 * type: 'DELETE',
5462 * url: '/rest/user/delete'
5463 * }
5464 * },
5465 * "table": "#example"
5466 * } );
5467 * } );
5468 *
5469 * // As a function - Making a custom `$.ajax` call
5470 * $(document).ready(function() {
5471 * var editor = new $.fn.Editor( {
5472 * "ajax": "php/index.php",
5473 * "table": "#example",
5474 * "ajax": function ( method, url, data, successCallback, errorCallback ) {
5475 * $.ajax( {
5476 * "type": method,
5477 * "url": url,
5478 * "data": data,
5479 * "dataType": "json",
5480 * "success": function (json) {
5481 * successCallback( json );
5482 * },
5483 * "error": function (xhr, error, thrown) {
5484 * errorCallback( xhr, error, thrown );
5485 * }
5486 * } );
5487 * }
5488 * } );
5489 * } );
5490 */
5491 "ajax": null,
5492
5493 /**
5494 * JSON property from which to read / write the row's ID property (i.e. its
5495 * unique column index that identifies the row to the database). By default
5496 * Editor will use the `DT_RowId` property from the data source object
5497 * (DataTable's magic property to set the DOM id for the row).
5498 *
5499 * If you want to read a parameter from the data source object instead of
5500 * using `DT_RowId`, set this option to the property name to use.
5501 *
5502 * Like other data source options the `srcId` option can be given in dotted
5503 * object notation to read nested objects.
5504 * @type null|string
5505 * @default DT_RowId
5506 *
5507 * @example
5508 * // Using a data source such as:
5509 * // { "id":12, "browser":"Chrome", ... }
5510 * $(document).ready(function() {
5511 * var editor = new $.fn.Editor( {
5512 * "ajax": "php/index.php",
5513 * "table": "#example",
5514 * "idSrc": "id"
5515 * } );
5516 * } );
5517 */
5518 "idSrc": 'DT_RowId',
5519
5520 /**
5521 * Events / callbacks - event handlers can be assigned as an individual function
5522 * during initialisation using the parameters in this name space. The names, and
5523 * the parameters passed to each callback match their event equivalent in the
5524 * {@link Editor} object.
5525 * @namespace
5526 * @deprecated Since 1.3. Use the `on()` API method instead. Note that events
5527 * passed in do still operate as they did in 1.2- but are no longer
5528 * individually documented.
5529 */
5530 "events": {},
5531
5532 /**
5533 * Internationalisation options for Editor. All client-side strings that the
5534 * end user can see in the interface presented by Editor can be modified here.
5535 *
5536 * You may also wish to refer to the <a href="http://datatables.net/usage/i18n">
5537 * DataTables internationalisation options</a> to provide a fully language
5538 * customised table interface.
5539 * @namespace
5540 *
5541 * @example
5542 * // Set the 'create' button text. All other strings used are the
5543 * // default values.
5544 * var editor = new $.fn.Editor( {
5545 * "ajax": "data/source",
5546 * "table": "#example",
5547 * "i18n": {
5548 * "create": {
5549 * "button": "New user"
5550 * }
5551 * }
5552 * } );
5553 *
5554 * @example
5555 * // Set the submit text for all three actions
5556 * var editor = new $.fn.Editor( {
5557 * "ajax": "data/source",
5558 * "table": "#example",
5559 * "i18n": {
5560 * "create": {
5561 * "submit": "Create new user"
5562 * },
5563 * "edit": {
5564 * "submit": "Update user"
5565 * },
5566 * "remove": {
5567 * "submit": "Remove user"
5568 * }
5569 * }
5570 * } );
5571 */
5572 "i18n": {
5573 /**
5574 * Strings used when working with the Editor 'create' action (creating new
5575 * records).
5576 * @namespace
5577 */
5578 "create": {
5579 /**
5580 * TableTools button text
5581 * @type string
5582 * @default New
5583 */
5584 "button": "New",
5585
5586 /**
5587 * Display container title (when showing the editor display)
5588 * @type string
5589 * @default Create new entry
5590 */
5591 "title": "Create new entry",
5592
5593 /**
5594 * Submit button text
5595 * @type string
5596 * @default Create
5597 */
5598 "submit": "Create"
5599 },
5600
5601 /**
5602 * Strings used when working with the Editor 'edit' action (editing existing
5603 * records).
5604 * @namespace
5605 */
5606 "edit": {
5607 /**
5608 * TableTools button text
5609 * @type string
5610 * @default Edit
5611 */
5612 "button": "Edit",
5613
5614 /**
5615 * Display container title (when showing the editor display)
5616 * @type string
5617 * @default Edit entry
5618 */
5619 "title": "Edit entry",
5620
5621 /**
5622 * Submit button text
5623 * @type string
5624 * @default Update
5625 */
5626 "submit": "Update"
5627 },
5628
5629 /**
5630 * Strings used when working with the Editor 'delete' action (deleting
5631 * existing records).
5632 * @namespace
5633 */
5634 "remove": {
5635 /**
5636 * TableTools button text
5637 * @type string
5638 * @default Delete
5639 */
5640 "button": "Delete",
5641
5642 /**
5643 * Display container title (when showing the editor display)
5644 * @type string
5645 * @default Delete
5646 */
5647 "title": "Delete",
5648
5649 /**
5650 * Submit button text
5651 * @type string
5652 * @default Delete
5653 */
5654 "submit": "Delete",
5655
5656 /**
5657 * Deletion confirmation message.
5658 *
5659 * As Editor has the ability to delete either a single or multiple rows
5660 * at a time, this option can be given as either a string (which will be
5661 * used regardless of how many records are selected) or as an object
5662 * where the property "_" will be used (with %d substituted for the number
5663 * of records to be deleted) as the delete message, unless there is a
5664 * key with the number of records to be deleted. This allows Editor
5665 * to consider the different pluralisation characteristics of different
5666 * languages.
5667 * @type object|string
5668 * @default Are you sure you wish to delete %d rows?
5669 *
5670 * @example
5671 * // String - no plural consideration
5672 * var editor = new $.fn.Editor( {
5673 * "ajax": "data/source",
5674 * "table": "#example",
5675 * "i18n": {
5676 * "remove": {
5677 * "confirm": "Are you sure you wish to delete %d record(s)?"
5678 * }
5679 * }
5680 * } );
5681 *
5682 * @example
5683 * // Basic 1 (singular) or _ (plural)
5684 * var editor = new $.fn.Editor( {
5685 * "ajax": "data/source",
5686 * "table": "#example",
5687 * "i18n": {
5688 * "remove": {
5689 * "confirm": {
5690 * "_": "Confirm deletion of %d records.",
5691 * "1": "Confirm deletion of record."
5692 * }
5693 * }
5694 * } );
5695 *
5696 * @example
5697 * // Singular, dual and plural
5698 * var editor = new $.fn.Editor( {
5699 * "ajax": "data/source",
5700 * "table": "#example",
5701 * "i18n": {
5702 * "remove": {
5703 * "confirm": {
5704 * "_": "Confirm deletion of %d records.",
5705 * "1": "Confirm deletion of record.",
5706 * "2": "Confirm deletion of both record."
5707 * }
5708 * }
5709 * } );
5710 *
5711 */
5712 "confirm": {
5713 "_": "Are you sure you wish to delete %d rows?",
5714 "1": "Are you sure you wish to delete 1 row?"
5715 }
5716 },
5717
5718 /**
5719 * Strings used for error conditions.
5720 * @namespace
5721 */
5722 "error": {
5723 /**
5724 * Generic server error message
5725 * @type string
5726 * @default A system error has occurred (<a target=\"_blank\" href=\"//datatables.net/tn/12\">More information</a>)
5727 */
5728 "system": "A system error has occurred (<a target=\"_blank\" href=\"//datatables.net/tn/12\">More information</a>)."
5729 },
5730
5731 /**
5732 * Strings used for multi-value editing
5733 * @namespace
5734 */
5735 "multi": {
5736 /**
5737 * Shown in place of the field value when a field has multiple values
5738 */
5739 "title": "Multiple values",
5740
5741 /**
5742 * Shown below the multi title text, although only the first
5743 * instance of this text is shown in the form to reduce redundancy
5744 */
5745 "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.",
5746
5747 /**
5748 * Shown below the field input when group editing a value to allow
5749 * the user to return to the original multiple values
5750 */
5751 "restore": "Undo changes"
5752 },
5753
5754 "datetime": {
5755 previous: 'Previous',
5756 next: 'Next',
5757 months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
5758 weekdays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
5759 amPm: [ 'am', 'pm' ],
5760 unknown: '-'
5761 }
5762 },
5763
5764 formOptions: {
5765 bubble: $.extend( {}, Editor.models.formOptions, {
5766 title: false,
5767 message: false,
5768 buttons: '_basic',
5769 submit: 'changed'
5770 } ),
5771
5772 inline: $.extend( {}, Editor.models.formOptions, {
5773 buttons: false,
5774 submit: 'changed'
5775 } ),
5776
5777 main: $.extend( {}, Editor.models.formOptions )
5778 },
5779
5780 /**
5781 * Submit data to the server in the 1.4- data format (`true`) or in the 1.5+
5782 * data format (`false` - default).
5783 *
5784 * @type Boolean
5785 */
5786 legacyAjax: false
5787 };
5788
5789
5790 /*
5791 * Extensions
5792 */
5793
5794 (function(){
5795
5796
5797 var __dataSources = Editor.dataSources = {};
5798
5799
5800 /* - - - - - - - - - -
5801 * DataTables editor interface
5802 */
5803
5804 var __dtIsSsp = function ( dt, editor ) {
5805 // If the draw type is `none`, then we still need to use the DT API to
5806 // update the display with the new data
5807 return dt.settings()[0].oFeatures.bServerSide &&
5808 editor.s.editOpts.drawType !== 'none';
5809 };
5810
5811 var __dtApi = function ( table ) {
5812 return $(table).DataTable();
5813 };
5814
5815 var __dtHighlight = function ( node ) {
5816 // Highlight a row using CSS transitions. The timeouts need to match the
5817 // transition duration from the CSS
5818 node = $(node);
5819
5820 setTimeout( function () {
5821 node.addClass( 'highlight' );
5822
5823 setTimeout( function () {
5824 node
5825 .addClass( 'noHighlight' )
5826 .removeClass( 'highlight' );
5827
5828 setTimeout( function () {
5829 node.removeClass( 'noHighlight' );
5830 }, 550 );
5831 }, 500 );
5832 }, 20 );
5833 };
5834
5835 var __dtRowSelector = function ( out, dt, identifier, fields, idFn )
5836 {
5837 dt.rows( identifier ).indexes().each( function ( idx ) {
5838 var row = dt.row( idx );
5839 var data = row.data();
5840 var idSrc = idFn( data );
5841
5842 if ( idSrc === undefined ) {
5843 Editor.error( 'Unable to find row identifier', 14 );
5844 }
5845
5846 out[ idSrc ] = {
5847 idSrc: idSrc,
5848 data: data,
5849 node: row.node(),
5850 fields: fields,
5851 type: 'row'
5852 };
5853 } );
5854 };
5855
5856 var __dtColumnSelector = function ( out, dt, identifier, fields, idFn )
5857 {
5858 dt.cells( null, identifier ).indexes().each( function ( idx ) {
5859 __dtCellSelector( out, dt, idx, fields, idFn );
5860 } );
5861 };
5862
5863 var __dtCellSelector = function ( out, dt, identifier, allFields, idFn, forceFields )
5864 {
5865 dt.cells( identifier ).indexes().each( function ( idx ) {
5866 var cell = dt.cell( idx );
5867 var row = dt.row( idx.row );
5868 var data = row.data();
5869 var idSrc = idFn( data );
5870 var fields = forceFields || __dtFieldsFromIdx( dt, allFields, idx.column );
5871
5872 // Use the row selector to get the row information
5873 __dtRowSelector(out, dt, idx.row, allFields, idFn);
5874
5875 out[ idSrc ].attach = typeof identifier === 'object' && identifier.nodeName ?
5876 [ identifier ] :
5877 [ cell.node() ];
5878 out[ idSrc ].displayFields = fields; // all fields are edited, but only
5879 // these are actually displayed
5880 } );
5881 };
5882
5883 var __dtFieldsFromIdx = function ( dt, fields, idx )
5884 {
5885 var field;
5886 var col = dt.settings()[0].aoColumns[ idx ];
5887 var dataSrc = col.editField !== undefined ?
5888 col.editField :
5889 col.mData;
5890 var resolvedFields = {};
5891 var run = function ( field, dataSrc ) {
5892 if ( field.dataSrc() === dataSrc ) {
5893 resolvedFields[ field.name() ] = field;
5894 }
5895 };
5896
5897 $.each( fields, function ( name, fieldInst ) {
5898 if ( $.isArray( dataSrc ) ) {
5899 for ( var i=0 ; i<dataSrc.length ; i++ ) {
5900 run( fieldInst, dataSrc[i] );
5901 }
5902 }
5903 else {
5904 run( fieldInst, dataSrc );
5905 }
5906 } );
5907
5908 if ( $.isEmptyObject( resolvedFields ) ) {
5909 Editor.error('Unable to automatically determine field from source. Please specify the field name.', 11);
5910 }
5911
5912 return resolvedFields;
5913 };
5914
5915
5916
5917 __dataSources.dataTable = {
5918 individual: function ( identifier, fieldNames ) {
5919 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
5920 var dt = __dtApi( this.s.table );
5921 var fields = this.s.fields;
5922 var out = {};
5923 var forceFields;
5924 var responsiveNode;
5925
5926 // Responsive field - this isn't really needed as of DT 1.10.11. To be
5927 // removed in Editor 1.6
5928 if ( identifier.nodeName && $(identifier).hasClass( 'dtr-data' ) ) {
5929 responsiveNode = identifier;
5930
5931 // Overwrite the selector so that DataTables will select based on
5932 // the cell that Responsive is showing data for
5933 identifier = dt.responsive.index( $(identifier).closest('li') );
5934 }
5935
5936 if ( fieldNames ) {
5937 if ( ! $.isArray( fieldNames ) ) {
5938 fieldNames = [ fieldNames ];
5939 }
5940
5941 forceFields = {};
5942
5943 $.each( fieldNames, function ( i, name ) {
5944 forceFields[ name ] = fields[ name ];
5945 } );
5946 }
5947
5948 __dtCellSelector( out, dt, identifier, fields, idFn, forceFields );
5949
5950 // If Responsive, we want it to attach the editor to the element that
5951 // was clicked on
5952 if ( responsiveNode ) {
5953 $.each( out, function ( i, val ) {
5954 val.attach = [ responsiveNode ];
5955 } );
5956 }
5957
5958 return out;
5959 },
5960
5961 // get idSrc, fields to edit, data and node for each item
5962 fields: function ( identifier )
5963 {
5964 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
5965 var dt = __dtApi( this.s.table );
5966 var fields = this.s.fields;
5967 var out = {};
5968
5969 if ( $.isPlainObject( identifier ) && ( identifier.rows !== undefined || identifier.columns !== undefined || identifier.cells !== undefined ) ) {
5970 // Multi-item type selector
5971 if ( identifier.rows !== undefined ) {
5972 __dtRowSelector( out, dt, identifier.rows, fields, idFn );
5973 }
5974
5975 if ( identifier.columns !== undefined ) {
5976 __dtColumnSelector( out, dt, identifier.columns, fields, idFn );
5977 }
5978
5979 if ( identifier.cells !== undefined ) {
5980 __dtCellSelector( out, dt, identifier.cells, fields, idFn );
5981 }
5982 }
5983 else {
5984 // Just a rows selector
5985 __dtRowSelector( out, dt, identifier, fields, idFn );
5986 }
5987
5988 return out;
5989 },
5990
5991 create: function ( fields, data ) {
5992 var dt = __dtApi( this.s.table );
5993
5994 if ( ! __dtIsSsp( dt, this ) ) {
5995 var row = dt.row.add( data );
5996 __dtHighlight( row.node() );
5997 }
5998 },
5999
6000 edit: function ( identifier, fields, data, store ) {
6001 var dt = __dtApi( this.s.table );
6002
6003 // No point in doing anything when server-side processing - the commit
6004 // will redraw the table
6005 if ( ! __dtIsSsp( dt, this ) ) {
6006 // The identifier can select one or more rows, but the data will
6007 // refer to just a single row. We need to determine which row from
6008 // the set is the one to operator on.
6009 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
6010 var rowId = idFn( data );
6011 var row;
6012
6013 // Find the row to edit - attempt to do an id look up first for speed
6014 row = dt.row( '#'+rowId );
6015
6016 // If not found, then we need to do it the slow way
6017 if ( ! row.any() ) {
6018 row = dt.row( function ( rowIdx, rowData, rowNode ) {
6019 return rowId == idFn( rowData );
6020 } );
6021 }
6022
6023 if ( row.any() ) {
6024 row.data( data );
6025
6026 // Remove the item from the list of indexes now that is has been
6027 // updated
6028 var idx = $.inArray( rowId, store.rowIds );
6029 store.rowIds.splice( idx, 1 );
6030 }
6031 else {
6032 // If not found, then its a new row (change in pkey possibly)
6033 row = dt.row.add( data );
6034 }
6035
6036 __dtHighlight( row.node() );
6037 }
6038 },
6039
6040 remove: function ( identifier, fields ) {
6041 // No confirmation from the server
6042 var dt = __dtApi( this.s.table );
6043
6044 if ( ! __dtIsSsp( dt, this ) ) {
6045 dt.rows( identifier ).remove();
6046 }
6047 },
6048
6049 prep: function ( action, identifier, submit, data, store ) {
6050 // On edit we store the ids of the rows that are being edited
6051 if ( action === 'edit' ) {
6052 store.rowIds = $.map( submit.data, function ( val, key ) {
6053 if ( ! $.isEmptyObject( submit.data[ key ] ) ) {
6054 return key;
6055 }
6056 } );
6057 }
6058 },
6059
6060 commit: function ( action, identifier, data, store ) {
6061 // Updates complete - redraw
6062 var dt = __dtApi( this.s.table );
6063
6064 // On edit, if there are any rows left in the `store.rowIds`, then they
6065 // were not returned by the server and should be removed (they might not
6066 // meet filtering requirements any more for example)
6067 if ( action === 'edit' && store.rowIds.length ) {
6068 var ids = store.rowIds;
6069 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
6070 var row;
6071
6072 for ( var i=0, ien=ids.length ; i<ien ; i++ ) {
6073 // Find the row to edit - attempt to do an id look up first for speed
6074 row = dt.row( '#'+ids[i] );
6075
6076 // If not found, then we need to do it the slow way
6077 if ( ! row.any() ) {
6078 row = dt.row( function ( rowIdx, rowData, rowNode ) {
6079 return ids[i] === idFn( rowData );
6080 } );
6081 }
6082
6083 if ( row.any() ) {
6084 row.remove();
6085 }
6086 }
6087 }
6088
6089 var drawType = this.s.editOpts.drawType;
6090 if ( drawType !== 'none' ) {
6091 dt.draw( drawType );
6092 }
6093 }
6094 };
6095
6096
6097
6098 /* - - - - - - - -
6099 * HTML editor interface
6100 */
6101
6102 function __html_set( identifier, fields, data ) {
6103 $.each( fields, function ( name, field ) {
6104 var val = field.valFromData( data );
6105
6106 if ( val !== undefined ) {
6107 __html_el( identifier, field.dataSrc() )
6108 .each( function () {
6109 // This is very frustrating, but in IE if you just write directly
6110 // to innerHTML, and elements that are overwritten are GC'ed,
6111 // even if there is a reference to them elsewhere
6112 while ( this.childNodes.length ) {
6113 this.removeChild( this.firstChild );
6114 }
6115 } )
6116 .html( val );
6117 }
6118 } );
6119 }
6120
6121 function __html_els ( identifier, names ) {
6122 var out = $();
6123
6124 for ( var i=0, ien=names.length ; i<ien ; i++ ) {
6125 out = out.add( __html_el( identifier, names[i] ) );
6126 }
6127
6128 return out;
6129 }
6130
6131 function __html_el ( identifier, name ) {
6132 var context = identifier === 'keyless' ?
6133 document :
6134 $('[data-editor-id="'+identifier+'"]');
6135
6136 return $('[data-editor-field="'+name+'"]', context);
6137 }
6138
6139
6140
6141 __dataSources.html = {
6142 initField: function ( cfg ) {
6143 // This is before the field has been initialised so can't use it API
6144 var label = $('[data-editor-label="'+(cfg.data || cfg.name)+'"]');
6145 if ( ! cfg.label && label.length ) {
6146 cfg.label = label.html();
6147 }
6148 },
6149
6150 individual: function ( identifier, fieldNames ) {
6151 // Auto detection of the field name and id
6152 if ( identifier instanceof $ || identifier.nodeName ) {
6153 if ( ! fieldNames ) {
6154 fieldNames = [ $( identifier ).attr('data-editor-field') ];
6155 }
6156
6157 identifier = $( identifier ).parents('[data-editor-id]').data('editor-id');
6158 }
6159
6160 // no id given and none found
6161 if ( ! identifier ) {
6162 identifier = 'keyless';
6163 }
6164
6165 // no field name - cannot continue
6166 if ( fieldNames && ! $.isArray( fieldNames ) ) {
6167 fieldNames = [ fieldNames ];
6168 }
6169
6170 if ( ! fieldNames || fieldNames.length === 0 ) {
6171 throw 'Cannot automatically determine field name from data source';
6172 }
6173
6174 var out = __dataSources.html.fields.call( this, identifier );
6175 var fields = this.s.fields;
6176 var forceFields = {};
6177
6178 $.each( fieldNames, function ( i, name ) {
6179 forceFields[ name ] = fields[ name ];
6180 } );
6181
6182 $.each( out, function ( id, set ) {
6183 set.type = 'cell';
6184 set.attach = __html_els( identifier, fieldNames ).toArray();
6185 set.fields = fields;
6186 set.displayFields = forceFields;
6187 } );
6188
6189 return out;
6190 },
6191
6192 // get idSrc, fields to edit, data and node for each item
6193 fields: function ( identifier )
6194 {
6195 var out = {};
6196 var data = {};
6197 var fields = this.s.fields;
6198
6199 if ( ! identifier ) {
6200 identifier = 'keyless';
6201 }
6202
6203 $.each( fields, function ( name, field ) {
6204 var val = __html_el( identifier, field.dataSrc() ).html();
6205
6206 // If no HTML element is present, jQuery returns null. We want undefined
6207 field.valToData( data, val === null ? undefined : val );
6208 } );
6209
6210 out[ identifier ] = {
6211 idSrc: identifier,
6212 data: data,
6213 node: document,
6214 fields: fields,
6215 type: 'row'
6216 };
6217
6218 return out;
6219 },
6220
6221 create: function ( fields, data ) {
6222 // If there is an element with the id that has been created, then use it
6223 // to assign the values
6224 if ( data ) {
6225 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
6226 var id = idFn( data );
6227
6228 if ( $('[data-editor-id="'+id+'"]').length ) {
6229 __html_set( id, fields, data );
6230 }
6231 }
6232 },
6233
6234 edit: function ( identifier, fields, data ) {
6235 // Get the ids from the returned data or `keyless` if not found
6236 var idFn = DataTable.ext.oApi._fnGetObjectDataFn( this.s.idSrc );
6237 var id = idFn( data ) || 'keyless';
6238
6239 __html_set( id, fields, data );
6240 },
6241
6242 remove: function ( identifier, fields ) {
6243 // If there is an element with an ID property matching the identifier,
6244 // remove it
6245 $('[data-editor-id="'+identifier+'"]').remove();
6246 }
6247 };
6248
6249
6250 }());
6251
6252
6253
6254 /**
6255 * Class names that are used by Editor for its various display components.
6256 * A copy of this object is taken when an Editor instance is initialised, thus
6257 * allowing different classes to be used in different instances if required.
6258 * Class name changes can be useful for easy integration with CSS frameworks,
6259 * for example Twitter Bootstrap.
6260 * @namespace
6261 */
6262 Editor.classes = {
6263 /**
6264 * Applied to the base DIV element that contains all other Editor elements
6265 */
6266 "wrapper": "DTE",
6267
6268 /**
6269 * Processing classes
6270 * @namespace
6271 */
6272 "processing": {
6273 /**
6274 * Processing indicator element
6275 */
6276 "indicator": "DTE_Processing_Indicator",
6277
6278 /**
6279 * Added to the base element ("wrapper") when the form is "processing"
6280 */
6281 "active": "DTE_Processing"
6282 },
6283
6284 /**
6285 * Display header classes
6286 * @namespace
6287 */
6288 "header": {
6289 /**
6290 * Container for the header elements
6291 */
6292 "wrapper": "DTE_Header",
6293
6294 /**
6295 * Liner for the header content
6296 */
6297 "content": "DTE_Header_Content"
6298 },
6299
6300 /**
6301 * Display body classes
6302 * @namespace
6303 */
6304 "body": {
6305 /**
6306 * Container for the body elements
6307 */
6308 "wrapper": "DTE_Body",
6309
6310 /**
6311 * Liner for the body content
6312 */
6313 "content": "DTE_Body_Content"
6314 },
6315
6316 /**
6317 * Display footer classes
6318 * @namespace
6319 */
6320 "footer": {
6321 /**
6322 * Container for the footer elements
6323 */
6324 "wrapper": "DTE_Footer",
6325
6326 /**
6327 * Liner for the footer content
6328 */
6329 "content": "DTE_Footer_Content"
6330 },
6331
6332 /**
6333 * Form classes
6334 * @namespace
6335 */
6336 "form": {
6337 /**
6338 * Container for the form elements
6339 */
6340 "wrapper": "DTE_Form",
6341
6342 /**
6343 * Liner for the form content
6344 */
6345 "content": "DTE_Form_Content",
6346
6347 /**
6348 * Applied to the <form> tag
6349 */
6350 "tag": "",
6351
6352 /**
6353 * Global form information
6354 */
6355 "info": "DTE_Form_Info",
6356
6357 /**
6358 * Global error imformation
6359 */
6360 "error": "DTE_Form_Error",
6361
6362 /**
6363 * Buttons container
6364 */
6365 "buttons": "DTE_Form_Buttons",
6366
6367 /**
6368 * Buttons container
6369 */
6370 "button": "btn"
6371 },
6372
6373 /**
6374 * Field classes
6375 * @namespace
6376 */
6377 "field": {
6378 /**
6379 * Container for each field
6380 */
6381 "wrapper": "DTE_Field",
6382
6383 /**
6384 * Class prefix for the field type - field type is added to the end allowing
6385 * styling based on field type.
6386 */
6387 "typePrefix": "DTE_Field_Type_",
6388
6389 /**
6390 * Class prefix for the field name - field name is added to the end allowing
6391 * styling based on field name.
6392 */
6393 "namePrefix": "DTE_Field_Name_",
6394
6395 /**
6396 * Field label
6397 */
6398 "label": "DTE_Label",
6399
6400 /**
6401 * Field input container
6402 */
6403 "input": "DTE_Field_Input",
6404
6405 /**
6406 * Input elements wrapper
6407 */
6408 "inputControl": "DTE_Field_InputControl",
6409
6410 /**
6411 * Field error state (added to the field.wrapper element when in error state
6412 */
6413 "error": "DTE_Field_StateError",
6414
6415 /**
6416 * Label information text
6417 */
6418 "msg-label": "DTE_Label_Info",
6419
6420 /**
6421 * Error information text
6422 */
6423 "msg-error": "DTE_Field_Error",
6424
6425 /**
6426 * Live messaging (API) information text
6427 */
6428 "msg-message": "DTE_Field_Message",
6429
6430 /**
6431 * General information text
6432 */
6433 "msg-info": "DTE_Field_Info",
6434
6435 /**
6436 * Multi-value information display wrapper
6437 */
6438 "multiValue": "multi-value",
6439
6440 /**
6441 * Multi-value information descriptive text
6442 */
6443 "multiInfo": "multi-info",
6444
6445 /**
6446 * Multi-value information display
6447 */
6448 "multiRestore": "multi-restore"
6449 },
6450
6451 /**
6452 * Action classes - these are added to the Editor base element ("wrapper")
6453 * and allows styling based on the type of form view that is being employed.
6454 * @namespace
6455 */
6456 "actions": {
6457 /**
6458 * Editor is in 'create' state
6459 */
6460 "create": "DTE_Action_Create",
6461
6462 /**
6463 * Editor is in 'edit' state
6464 */
6465 "edit": "DTE_Action_Edit",
6466
6467 /**
6468 * Editor is in 'remove' state
6469 */
6470 "remove": "DTE_Action_Remove"
6471 },
6472
6473 /**
6474 * Bubble editing classes - these are used to display the bubble editor
6475 * @namespace
6476 */
6477 "bubble": {
6478 /**
6479 * Bubble container element
6480 */
6481 "wrapper": "DTE DTE_Bubble",
6482
6483 /**
6484 * Bubble content liner
6485 */
6486 "liner": "DTE_Bubble_Liner",
6487
6488 /**
6489 * Bubble table display wrapper, so the buttons and form can be shown
6490 * as table cells (via css)
6491 */
6492 "table": "DTE_Bubble_Table",
6493
6494 /**
6495 * Close button
6496 */
6497 "close": "DTE_Bubble_Close",
6498
6499 /**
6500 * Pointer shown which node is being edited
6501 */
6502 "pointer": "DTE_Bubble_Triangle",
6503
6504 /**
6505 * Fixed background
6506 */
6507 "bg": "DTE_Bubble_Background"
6508 }
6509 };
6510
6511
6512
6513 /*
6514 * Add helpful TableTool buttons to make life easier
6515 *
6516 * Note that the values that require a string to make any sense (the button text
6517 * for example) are set by Editor when Editor is initialised through the i18n
6518 * options.
6519 */
6520 if ( DataTable.TableTools ) {
6521 var ttButtons = DataTable.TableTools.BUTTONS;
6522 var ttButtonBase = {
6523 sButtonText: null,
6524 editor: null,
6525 formTitle: null
6526 };
6527
6528 ttButtons.editor_create = $.extend( true, ttButtons.text, ttButtonBase, {
6529 formButtons: [ {
6530 label: null,
6531 fn: function (e) { this.submit(); }
6532 } ],
6533
6534 fnClick: function( button, config ) {
6535 var editor = config.editor;
6536 var i18nCreate = editor.i18n.create;
6537 var buttons = config.formButtons;
6538
6539 if ( ! buttons[0].label ) {
6540 buttons[0].label = i18nCreate.submit;
6541 }
6542
6543 editor.create( {
6544 title: i18nCreate.title,
6545 buttons: buttons
6546 } );
6547 }
6548 } );
6549
6550
6551 ttButtons.editor_edit = $.extend( true, ttButtons.select_single, ttButtonBase, {
6552 formButtons: [ {
6553 label: null,
6554 fn: function (e) { this.submit(); }
6555 } ],
6556
6557 fnClick: function( button, config ) {
6558 var selected = this.fnGetSelectedIndexes();
6559 if ( selected.length !== 1 ) {
6560 return;
6561 }
6562
6563 var editor = config.editor;
6564 var i18nEdit = editor.i18n.edit;
6565 var buttons = config.formButtons;
6566
6567 if ( ! buttons[0].label ) {
6568 buttons[0].label = i18nEdit.submit;
6569 }
6570
6571 editor.edit( selected[0], {
6572 title: i18nEdit.title,
6573 buttons: buttons
6574 } );
6575 }
6576 } );
6577
6578
6579 ttButtons.editor_remove = $.extend( true, ttButtons.select, ttButtonBase, {
6580 question: null,
6581
6582 formButtons: [
6583 {
6584 label: null,
6585 fn: function (e) {
6586 // Executed in the Form instance's scope
6587 var that = this;
6588 this.submit( function ( json ) {
6589 var tt = $.fn.dataTable.TableTools.fnGetInstance(
6590 $(that.s.table).DataTable().table().node()
6591 );
6592 tt.fnSelectNone();
6593 } );
6594 }
6595 }
6596 ],
6597
6598 fnClick: function( button, config ) {
6599 var rows = this.fnGetSelectedIndexes();
6600 if ( rows.length === 0 ) {
6601 return;
6602 }
6603
6604 var editor = config.editor;
6605 var i18nRemove = editor.i18n.remove;
6606 var buttons = config.formButtons;
6607 var question = typeof i18nRemove.confirm === 'string' ?
6608 i18nRemove.confirm :
6609 i18nRemove.confirm[rows.length] ?
6610 i18nRemove.confirm[rows.length] : i18nRemove.confirm._;
6611
6612 if ( ! buttons[0].label ) {
6613 buttons[0].label = i18nRemove.submit;
6614 }
6615
6616 editor.remove( rows, {
6617 message: question.replace( /%d/g, rows.length ),
6618 title: i18nRemove.title,
6619 buttons: buttons
6620 } );
6621 }
6622 } );
6623 }
6624
6625
6626 $.extend( DataTable.ext.buttons, {
6627 create: {
6628 text: function ( dt, node, config ) {
6629 return dt.i18n( 'buttons.create', config.editor.i18n.create.button );
6630 },
6631 className: 'buttons-create',
6632 editor: null,
6633 formButtons: {
6634 label: function ( editor ) {
6635 return editor.i18n.create.submit;
6636 },
6637 fn: function (e) {
6638 this.submit();
6639 }
6640 },
6641 formMessage: null,
6642 formTitle: null,
6643 action: function( e, dt, node, config ) {
6644 var editor = config.editor;
6645 var buttons = config.formButtons;
6646
6647 editor.create( {
6648 buttons: config.formButtons,
6649 message: config.formMessage,
6650 title: config.formTitle || editor.i18n.create.title
6651 } );
6652 }
6653 },
6654
6655 edit: {
6656 extend: 'selected',
6657 text: function ( dt, node, config ) {
6658 return dt.i18n( 'buttons.edit', config.editor.i18n.edit.button );
6659 },
6660 className: 'buttons-edit',
6661 editor: null,
6662 formButtons: {
6663 label: function ( editor ) {
6664 return editor.i18n.edit.submit;
6665 },
6666 fn: function (e) {
6667 this.submit();
6668 }
6669 },
6670 formMessage: null,
6671 formTitle: null,
6672 action: function( e, dt, node, config ) {
6673 var editor = config.editor;
6674 var rows = dt.rows( { selected: true } ).indexes();
6675 var columns = dt.columns( { selected: true } ).indexes();
6676 var cells = dt.cells( { selected: true } ).indexes();
6677
6678 var items = columns.length || cells.length ?
6679 {
6680 rows: rows,
6681 columns: columns,
6682 cells: cells
6683 } :
6684 rows;
6685
6686 editor.edit( items, {
6687 message: config.formMessage,
6688 buttons: config.formButtons,
6689 title: config.formTitle || editor.i18n.edit.title
6690 } );
6691 }
6692 },
6693
6694 remove: {
6695 extend: 'selected',
6696 text: function ( dt, node, config ) {
6697 return dt.i18n( 'buttons.remove', config.editor.i18n.remove.button );
6698 },
6699 className: 'buttons-remove',
6700 editor: null,
6701 formButtons: {
6702 label: function ( editor ) {
6703 return editor.i18n.remove.submit;
6704 },
6705 fn: function (e) {
6706 this.submit();
6707 }
6708 },
6709 formMessage: function ( editor, dt ) {
6710 var rows = dt.rows( { selected: true } ).indexes();
6711 var i18n = editor.i18n.remove;
6712 var question = typeof i18n.confirm === 'string' ?
6713 i18n.confirm :
6714 i18n.confirm[rows.length] ?
6715 i18n.confirm[rows.length] : i18n.confirm._;
6716
6717 return question.replace( /%d/g, rows.length );
6718 },
6719 formTitle: null,
6720 action: function( e, dt, node, config ) {
6721 var editor = config.editor;
6722
6723 editor.remove( dt.rows( { selected: true } ).indexes(), {
6724 buttons: config.formButtons,
6725 message: config.formMessage,
6726 title: config.formTitle || editor.i18n.remove.title
6727 } );
6728 }
6729 }
6730 } );
6731
6732 /**
6733 * Field types array - this can be used to add field types or modify the pre-
6734 * defined options. See the online Editor documentation for information about
6735 * the built in field types.
6736 *
6737 * Note that we use a DataTables ext object to allow plug-ins to be loaded
6738 * before Editor itself. This is useful for the online DataTables download
6739 * builder.
6740 *
6741 * @namespace
6742 */
6743 Editor.fieldTypes = {};
6744 /*
6745 * This file provides a DateTime GUI picker (calender and time input). Only the
6746 * format YYYY-MM-DD is supported without additional software, but the end user
6747 * experience can be greatly enhanced by including the momentjs library which
6748 * provides date / time parsing and formatting options.
6749 *
6750 * This functionality is required because the HTML5 date and datetime input
6751 * types are not widely supported in desktop browsers.
6752 *
6753 * Constructed by using:
6754 *
6755 * new Editor.DateTime( input, opts )
6756 *
6757 * where `input` is the HTML input element to use and `opts` is an object of
6758 * options based on the `Editor.DateTime.defaults` object.
6759 */
6760
6761 Editor.DateTime = function ( input, opts ) {
6762 this.c = $.extend( true, {}, Editor.DateTime.defaults, opts );
6763 var classPrefix = this.c.classPrefix;
6764 var i18n = this.c.i18n;
6765
6766 // Only IS8601 dates are supported without moment
6767 if ( ! window.moment && this.c.format !== 'YYYY-MM-DD' ) {
6768 throw "Editor datetime: Without momentjs only the format 'YYYY-MM-DD' can be used";
6769 }
6770
6771 var timeBlock = function ( type ) {
6772 return '<div class="'+classPrefix+'-timeblock">'+
6773 '<div class="'+classPrefix+'-iconUp">'+
6774 '<button>'+i18n.previous+'</button>'+
6775 '</div>'+
6776 '<div class="'+classPrefix+'-label">'+
6777 '<span/>'+
6778 '<select class="'+classPrefix+'-'+type+'"/>'+
6779 '</div>'+
6780 '<div class="'+classPrefix+'-iconDown">'+
6781 '<button>'+i18n.next+'</button>'+
6782 '</div>'+
6783 '</div>';
6784 };
6785
6786 var gap = function () {
6787 return '<span>:</span>';
6788 };
6789
6790 // DOM structure
6791 var structure = $(
6792 '<div class="'+classPrefix+'">'+
6793 '<div class="'+classPrefix+'-date">'+
6794 '<div class="'+classPrefix+'-title">'+
6795 '<div class="'+classPrefix+'-iconLeft">'+
6796 '<button>'+i18n.previous+'</button>'+
6797 '</div>'+
6798 '<div class="'+classPrefix+'-iconRight">'+
6799 '<button>'+i18n.next+'</button>'+
6800 '</div>'+
6801 '<div class="'+classPrefix+'-label">'+
6802 '<span/>'+
6803 '<select class="'+classPrefix+'-month"/>'+
6804 '</div>'+
6805 '<div class="'+classPrefix+'-label">'+
6806 '<span/>'+
6807 '<select class="'+classPrefix+'-year"/>'+
6808 '</div>'+
6809 '</div>'+
6810 '<div class="'+classPrefix+'-calendar"/>'+
6811 '</div>'+
6812 '<div class="'+classPrefix+'-time">'+
6813 timeBlock( 'hours' )+
6814 gap()+
6815 timeBlock( 'minutes' )+
6816 gap()+
6817 timeBlock( 'seconds' )+
6818 timeBlock( 'ampm' )+
6819 '</div>'+
6820 '</div>'
6821 );
6822
6823 this.dom = {
6824 container: structure,
6825 date: structure.find( '.'+classPrefix+'-date' ),
6826 title: structure.find( '.'+classPrefix+'-title' ),
6827 calendar: structure.find( '.'+classPrefix+'-calendar' ),
6828 time: structure.find( '.'+classPrefix+'-time' ),
6829 input: $(input)
6830 };
6831
6832 this.s = {
6833 /** @type {Date} Date value that the picker has currently selected */
6834 d: null,
6835
6836 /** @type {Date} Date of the calender - might not match the value */
6837 display: null,
6838
6839 /** @type {String} Unique namespace string for this instance */
6840 namespace: 'editor-dateime-'+(Editor.DateTime._instance++),
6841
6842 /** @type {Object} Parts of the picker that should be shown */
6843 parts: {
6844 date: this.c.format.match( /[YMD]/ ) !== null,
6845 time: this.c.format.match( /[Hhm]/ ) !== null,
6846 seconds: this.c.format.indexOf( 's' ) !== -1,
6847 hours12: this.c.format.match( /[haA]/ ) !== null
6848 }
6849 };
6850
6851 this.dom.container
6852 .append( this.dom.date )
6853 .append( this.dom.time );
6854
6855 this.dom.date
6856 .append( this.dom.title )
6857 .append( this.dom.calendar );
6858
6859 this._constructor();
6860 };
6861
6862 $.extend( Editor.DateTime.prototype, {
6863 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
6864 * Public
6865 */
6866
6867 /**
6868 * Destroy the control
6869 */
6870 destroy: function () {
6871 this._hide();
6872 this.dom.container().off('').empty();
6873 this.dom.input.off('.editor-datetime');
6874 },
6875
6876 max: function ( date ) {
6877 this.c.maxDate = date;
6878
6879 this._optionsTitle();
6880 this._setCalander();
6881 },
6882
6883 min: function ( date ) {
6884 this.c.minDate = date;
6885
6886 this._optionsTitle();
6887 this._setCalander();
6888 },
6889
6890 /**
6891 * Check if an element belongs to this control
6892 *
6893 * @param {node} node Element to check
6894 * @return {boolean} true if owned by this control, false otherwise
6895 */
6896 owns: function ( node ) {
6897 return $(node).parents().filter( this.dom.container ).length > 0;
6898 },
6899
6900 /**
6901 * Get / set the value
6902 *
6903 * @param {string|Date} set Value to set
6904 * @param {boolean} [write=true] Flag to indicate if the formatted value
6905 * should be written into the input element
6906 */
6907 val: function ( set, write ) {
6908 if ( set === undefined ) {
6909 return this.s.d;
6910 }
6911
6912 if ( set instanceof Date ) {
6913 this.s.d = this._dateToUtc( set );
6914 }
6915 else if ( set === null || set === '' ) {
6916 this.s.d = null;
6917 }
6918 else if ( typeof set === 'string' ) {
6919 if ( window.moment ) {
6920 // Use moment if possible (even for ISO8601 strings, since it
6921 // will correctly handle 0000-00-00 and the like)
6922 var m = window.moment.utc( set, this.c.format, this.c.momentLocale, this.c.momentStrict );
6923 this.s.d = m.isValid() ? m.toDate() : null;
6924 }
6925 else {
6926 // Else must be using ISO8601 without moment (constructor would
6927 // have thrown an error otherwise)
6928 var match = set.match(/(\d{4})\-(\d{2})\-(\d{2})/ );
6929 this.s.d = match ?
6930 new Date( Date.UTC(match[1], match[2]-1, match[3]) ) :
6931 null;
6932 }
6933 }
6934
6935 if ( write || write === undefined ) {
6936 if ( this.s.d ) {
6937 this._writeOutput();
6938 }
6939 else {
6940 // The input value was not valid...
6941 this.dom.input.val( set );
6942 }
6943 }
6944
6945 // We need a date to be able to display the calendar at all
6946 if ( ! this.s.d ) {
6947 this.s.d = this._dateToUtc( new Date() );
6948 }
6949
6950 this.s.display = new Date( this.s.d.toString() );
6951
6952 // Update the display elements for the new value
6953 this._setTitle();
6954 this._setCalander();
6955 this._setTime();
6956 },
6957
6958
6959 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
6960 * Constructor
6961 */
6962
6963 /**
6964 * Build the control and assign initial event handlers
6965 *
6966 * @private
6967 */
6968 _constructor: function () {
6969 var that = this;
6970 var classPrefix = this.c.classPrefix;
6971 var container = this.dom.container;
6972 var i18n = this.c.i18n;
6973
6974 if ( ! this.s.parts.date ) {
6975 this.dom.date.css( 'display', 'none' );
6976 }
6977
6978 if ( ! this.s.parts.time ) {
6979 this.dom.time.css( 'display', 'none' );
6980 }
6981
6982 if ( ! this.s.parts.seconds ) {
6983 this.dom.time.children('div.editor-datetime-timeblock').eq(2).remove();
6984 this.dom.time.children('span').eq(1).remove();
6985 }
6986
6987 if ( ! this.s.parts.hours12 ) {
6988 this.dom.time.children('div.editor-datetime-timeblock').last().remove();
6989 }
6990
6991 // Render the options
6992 this._optionsTitle();
6993 this._optionsTime( 'hours', this.s.parts.hours12 ? 12 : 24, 1 );
6994 this._optionsTime( 'minutes', 60, this.c.minutesIncrement );
6995 this._optionsTime( 'seconds', 60, this.c.secondsIncrement );
6996 this._options( 'ampm', [ 'am', 'pm' ], i18n.amPm );
6997
6998 // Trigger the display of the widget when clicking or focusing on the
6999 // input element
7000 this.dom.input
7001 .on('focus.editor-datetime click.editor-datetime', function () {
7002 // If already visible - don't do anything
7003 if ( that.dom.container.is(':visible') || that.dom.input.is(':disabled') ) {
7004 return;
7005 }
7006
7007 // In case the value has changed by text
7008 that.val( that.dom.input.val(), false );
7009
7010 that._show();
7011 } )
7012 .on('keyup.editor-datetime', function () {
7013 // Update the calender's displayed value as the user types
7014 if ( that.dom.container.is(':visible') ) {
7015 that.val( that.dom.input.val(), false );
7016 }
7017 } );
7018
7019 // Main event handlers for input in the widget
7020 this.dom.container
7021 .on( 'change', 'select', function () {
7022 var select = $(this);
7023 var val = select.val();
7024
7025 if ( select.hasClass(classPrefix+'-month') ) {
7026 // Month select
7027 that._correctMonth( that.s.display, val );
7028 that._setTitle();
7029 that._setCalander();
7030 }
7031 else if ( select.hasClass(classPrefix+'-year') ) {
7032 // Year select
7033 that.s.display.setUTCFullYear( val );
7034 that._setTitle();
7035 that._setCalander();
7036 }
7037 else if ( select.hasClass(classPrefix+'-hours') || select.hasClass(classPrefix+'-ampm') ) {
7038 // Hours - need to take account of AM/PM input if present
7039 if ( that.s.parts.hours12 ) {
7040 var hours = $(that.dom.container).find('.'+classPrefix+'-hours').val() * 1;
7041 var pm = $(that.dom.container).find('.'+classPrefix+'-ampm').val() === 'pm';
7042
7043 that.s.d.setUTCHours( hours === 12 && !pm ?
7044 0 :
7045 pm && hours !== 12 ?
7046 hours + 12 :
7047 hours
7048 );
7049 }
7050 else {
7051 that.s.d.setUTCHours( val );
7052 }
7053
7054 that._setTime();
7055 that._writeOutput( true );
7056 }
7057 else if ( select.hasClass(classPrefix+'-minutes') ) {
7058 // Minutes select
7059 that.s.d.setUTCMinutes( val );
7060 that._setTime();
7061 that._writeOutput( true );
7062 }
7063 else if ( select.hasClass(classPrefix+'-seconds') ) {
7064 // Seconds select
7065 that.s.d.setSeconds( val );
7066 that._setTime();
7067 that._writeOutput( true );
7068 }
7069
7070 that.dom.input.focus();
7071 that._position();
7072 } )
7073 .on( 'click', function (e) {
7074 var nodeName = e.target.nodeName.toLowerCase();
7075
7076 if ( nodeName === 'select' ) {
7077 return;
7078 }
7079
7080 e.stopPropagation();
7081
7082 if ( nodeName === 'button' ) {
7083 var button = $(e.target);
7084 var parent = button.parent();
7085 var select;
7086
7087 if ( parent.hasClass('disabled') ) {
7088 return;
7089 }
7090
7091 if ( parent.hasClass(classPrefix+'-iconLeft') ) {
7092 // Previous month
7093 that.s.display.setUTCMonth( that.s.display.getUTCMonth()-1 );
7094 that._setTitle();
7095 that._setCalander();
7096
7097 that.dom.input.focus();
7098 }
7099 else if ( parent.hasClass(classPrefix+'-iconRight') ) {
7100 // Next month
7101 that._correctMonth( that.s.display, that.s.display.getUTCMonth()+1 );
7102 that._setTitle();
7103 that._setCalander();
7104
7105 that.dom.input.focus();
7106 }
7107 else if ( parent.hasClass(classPrefix+'-iconUp') ) {
7108 // Value increase - common to all time selects
7109 select = parent.parent().find('select')[0];
7110 select.selectedIndex = select.selectedIndex !== select.options.length - 1 ?
7111 select.selectedIndex+1 :
7112 0;
7113 $(select).change();
7114 }
7115 else if ( parent.hasClass(classPrefix+'-iconDown') ) {
7116 // Value decrease - common to all time selects
7117 select = parent.parent().find('select')[0];
7118 select.selectedIndex = select.selectedIndex === 0 ?
7119 select.options.length - 1 :
7120 select.selectedIndex-1;
7121 $(select).change();
7122 }
7123 else {
7124 // Calender click
7125 if ( ! that.s.d ) {
7126 that.s.d = that._dateToUtc( new Date() );
7127 }
7128
7129 that.s.d.setUTCFullYear( button.data('year') );
7130 that.s.d.setUTCMonth( button.data('month') );
7131 that.s.d.setUTCDate( button.data('day') );
7132
7133 that._writeOutput( true );
7134
7135 // This is annoying but IE has some kind of async
7136 // behaviour with focus and the focus from the above
7137 // write would occur after this hide - resulting in the
7138 // calender opening immediately
7139 setTimeout( function () {
7140 that._hide();
7141 }, 10 );
7142 }
7143 }
7144 else {
7145 // Click anywhere else in the widget - return focus to the
7146 // input element
7147 that.dom.input.focus();
7148 }
7149 } );
7150 },
7151
7152
7153 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
7154 * Private
7155 */
7156
7157 /**
7158 * Compare the date part only of two dates - this is made super easy by the
7159 * toDateString method!
7160 *
7161 * @param {Date} a Date 1
7162 * @param {Date} b Date 2
7163 * @private
7164 */
7165 _compareDates: function( a, b ) {
7166 // Can't use toDateString as that converts to local time
7167 return this._dateToUtcString(a) === this._dateToUtcString(b);
7168 },
7169
7170 /**
7171 * When changing month, take account of the fact that some months don't have
7172 * the same number of days. For example going from January to February you
7173 * can have the 31st of Jan selected and just add a month since the date
7174 * would still be 31, and thus drop you into March.
7175 *
7176 * @param {Date} date Date - will be modified
7177 * @param {integer} month Month to set
7178 * @private
7179 */
7180 _correctMonth: function ( date, month ) {
7181 var days = this._daysInMonth( date.getUTCFullYear(), month );
7182 var correctDays = date.getUTCDate() > days;
7183
7184 date.setUTCMonth( month );
7185
7186 if ( correctDays ) {
7187 date.setUTCDate( days );
7188 date.setUTCMonth( month );
7189 }
7190 },
7191
7192 /**
7193 * Get the number of days in a method. Based on
7194 * http://stackoverflow.com/a/4881951 by Matti Virkkunen
7195 *
7196 * @param {integer} year Year
7197 * @param {integer} month Month (starting at 0)
7198 * @private
7199 */
7200 _daysInMonth: function ( year, month ) {
7201 //
7202 var isLeap = ((year % 4) === 0 && ((year % 100) !== 0 || (year % 400) === 0));
7203 var months = [31, (isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
7204
7205 return months[month];
7206 },
7207
7208 /**
7209 * Create a new date object which has the UTC values set to the local time.
7210 * This allows the local time to be used directly for the library which
7211 * always bases its calculations and display on UTC.
7212 *
7213 * @param {Date} s Date to "convert"
7214 * @return {Date} Shifted date
7215 */
7216 _dateToUtc: function ( s ) {
7217 return new Date( Date.UTC(
7218 s.getFullYear(), s.getMonth(), s.getDate(),
7219 s.getHours(), s.getMinutes(), s.getSeconds()
7220 ) );
7221 },
7222
7223 /**
7224 * Create a UTC ISO8601 date part from a date object
7225 *
7226 * @param {Date} d Date to "convert"
7227 * @return {string} ISO formatted date
7228 */
7229 _dateToUtcString: function ( d ) {
7230 return d.getUTCFullYear()+'-'+
7231 this._pad(d.getUTCMonth()+1)+'-'+
7232 this._pad(d.getUTCDate());
7233 },
7234
7235 /**
7236 * Hide the control and remove events related to its display
7237 *
7238 * @private
7239 */
7240 _hide: function () {
7241 var namespace = this.s.namespace;
7242
7243 this.dom.container.detach();
7244
7245 $(window).off( '.'+namespace );
7246 $(document).off( 'keydown.'+namespace );
7247 $('div.DTE_Body_Content').off( 'scroll.'+namespace );
7248 $('body').off( 'click.'+namespace );
7249 },
7250
7251 /**
7252 * Convert a 24 hour value to a 12 hour value
7253 *
7254 * @param {integer} val 24 hour value
7255 * @return {integer} 12 hour value
7256 * @private
7257 */
7258 _hours24To12: function ( val ) {
7259 return val === 0 ?
7260 12 :
7261 val > 12 ?
7262 val - 12 :
7263 val;
7264 },
7265
7266 /**
7267 * Generate the HTML for a single day in the calender - this is basically
7268 * and HTML cell with a button that has data attributes so we know what was
7269 * clicked on (if it is clicked on) and a bunch of classes for styling.
7270 *
7271 * @param {object} day Day object from the `_htmlMonth` method
7272 * @return {string} HTML cell
7273 */
7274 _htmlDay: function( day )
7275 {
7276 if ( day.empty ) {
7277 return '<td class="empty"></td>';
7278 }
7279
7280 var classes = [ 'day' ];
7281 var classPrefix = this.c.classPrefix;
7282
7283 if ( day.disabled ) {
7284 classes.push( 'disabled' );
7285 }
7286
7287 if ( day.today ) {
7288 classes.push( 'today' );
7289 }
7290
7291 if ( day.selected ) {
7292 classes.push( 'selected' );
7293 }
7294
7295 return '<td data-day="' + day.day + '" class="' + classes.join(' ') + '">' +
7296 '<button class="'+classPrefix+'-button '+classPrefix+'-day" type="button" ' +'data-year="' + day.year + '" data-month="' + day.month + '" data-day="' + day.day + '">' +
7297 day.day +
7298 '</button>' +
7299 '</td>';
7300 },
7301
7302
7303 /**
7304 * Create the HTML for a month to be displayed in the calender table.
7305 *
7306 * Based upon the logic used in Pikaday - MIT licensed
7307 * Copyright (c) 2014 David Bushell
7308 * https://github.com/dbushell/Pikaday
7309 *
7310 * @param {integer} year Year
7311 * @param {integer} month Month (starting at 0)
7312 * @return {string} Calender month HTML
7313 * @private
7314 */
7315 _htmlMonth: function ( year, month ) {
7316 var now = new Date(),
7317 days = this._daysInMonth( year, month ),
7318 before = new Date( Date.UTC(year, month, 1) ).getUTCDay(),
7319 data = [],
7320 row = [];
7321
7322 if ( this.c.firstDay > 0 ) {
7323 before -= this.c.firstDay;
7324
7325 if (before < 0) {
7326 before += 7;
7327 }
7328 }
7329
7330 var cells = days + before,
7331 after = cells;
7332
7333 while ( after > 7 ) {
7334 after -= 7;
7335 }
7336
7337 cells += 7 - after;
7338
7339 var minDate = this.c.minDate;
7340 var maxDate = this.c.maxDate;
7341
7342 if ( minDate ) {
7343 minDate.setUTCHours(0);
7344 minDate.setUTCMinutes(0);
7345 minDate.setSeconds(0);
7346 }
7347
7348 if ( maxDate ) {
7349 maxDate.setUTCHours(23);
7350 maxDate.setUTCMinutes(59);
7351 maxDate.setSeconds(59);
7352 }
7353
7354 for ( var i=0, r=0 ; i<cells ; i++ ) {
7355 var day = new Date( Date.UTC(year, month, 1 + (i - before)) ),
7356 selected = this.s.d ? this._compareDates(day, this.s.d) : false,
7357 today = this._compareDates(day, now),
7358 empty = i < before || i >= (days + before),
7359 disabled = (minDate && day < minDate) ||
7360 (maxDate && day > maxDate);
7361
7362 var disableDays = this.c.disableDays;
7363 if ( $.isArray( disableDays ) && $.inArray( day.getUTCDay(), disableDays ) !== -1 ) {
7364 disabled = true;
7365 }
7366 else if ( typeof disableDays === 'function' && disableDays( day ) === true ) {
7367 disabled = true;
7368 }
7369
7370 var dayConfig = {
7371 day: 1 + (i - before),
7372 month: month,
7373 year: year,
7374 selected: selected,
7375 today: today,
7376 disabled: disabled,
7377 empty: empty
7378 };
7379
7380 row.push( this._htmlDay(dayConfig) );
7381
7382 if ( ++r === 7 ) {
7383 if ( this.c.showWeekNumber ) {
7384 row.unshift( this._htmlWeekOfYear(i - before, month, year) );
7385 }
7386
7387 data.push( '<tr>'+row.join('')+'</tr>' );
7388 row = [];
7389 r = 0;
7390 }
7391 }
7392
7393 var className = this.c.classPrefix+'-table';
7394 if ( this.c.showWeekNumber ) {
7395 className += ' weekNumber';
7396 }
7397
7398 return '<table class="'+className+'">' +
7399 '<thead>'+
7400 this._htmlMonthHead() +
7401 '</thead>'+
7402 '<tbody>'+
7403 data.join('') +
7404 '</tbody>'+
7405 '</table>';
7406 },
7407
7408 /**
7409 * Create the calender table's header (week days)
7410 *
7411 * @return {string} HTML cells for the row
7412 * @private
7413 */
7414 _htmlMonthHead: function () {
7415 var a = [];
7416 var firstDay = this.c.firstDay;
7417 var i18n = this.c.i18n;
7418
7419 // Take account of the first day shift
7420 var dayName = function ( day ) {
7421 day += firstDay;
7422
7423 while (day >= 7) {
7424 day -= 7;
7425 }
7426
7427 return i18n.weekdays[day];
7428 };
7429
7430 // Empty cell in the header
7431 if ( this.c.showWeekNumber ) {
7432 a.push( '<th></th>' );
7433 }
7434
7435 for ( var i=0 ; i<7 ; i++ ) {
7436 a.push( '<th>'+dayName( i )+'</th>' );
7437 }
7438
7439 return a.join('');
7440 },
7441
7442 /**
7443 * Create a cell that contains week of the year - ISO style
7444 *
7445 * Based on http://javascript.about.com/library/blweekyear.htm
7446 *
7447 * @param {integer} d Day of month
7448 * @param {integer} m Month of year (zero index)
7449 * @param {integer} y Year
7450 * @return {string}
7451 * @private
7452 */
7453 _htmlWeekOfYear: function ( d, m, y ) {
7454 var onejan = new Date(y, 0, 1),
7455 weekNum = Math.ceil((((new Date(y, m, d) - onejan) / 86400000) + onejan.getUTCDay()+1)/7);
7456
7457 return '<td class="'+this.c.classPrefix+'-week">' + weekNum + '</td>';
7458 },
7459
7460 /**
7461 * Create option elements from a range in an array
7462 *
7463 * @param {string} selector Class name unique to the select element to use
7464 * @param {array} values Array of values
7465 * @param {array} [labels] Array of labels. If given must be the same
7466 * length as the values parameter.
7467 * @private
7468 */
7469 _options: function ( selector, values, labels ) {
7470 if ( ! labels ) {
7471 labels = values;
7472 }
7473
7474 var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
7475 select.empty();
7476
7477 for ( var i=0, ien=values.length ; i<ien ; i++ ) {
7478 select.append( '<option value="'+values[i]+'">'+labels[i]+'</option>' );
7479 }
7480 },
7481
7482 /**
7483 * Set an option and update the option's span pair (since the select element
7484 * has opacity 0 for styling)
7485 *
7486 * @param {string} selector Class name unique to the select element to use
7487 * @param {*} val Value to set
7488 * @private
7489 */
7490 _optionSet: function ( selector, val ) {
7491 var select = this.dom.container.find('select.'+this.c.classPrefix+'-'+selector);
7492 var span = select.parent().children('span');
7493
7494 select.val( val );
7495
7496 var selected = select.find('option:selected');
7497 span.html( selected.length !== 0 ?
7498 selected.text() :
7499 this.c.i18n.unknown
7500 );
7501 },
7502
7503 /**
7504 * Create time option list. Can't just use `_options` for the time as the
7505 * hours can be a little complex and we want to be able to control the
7506 * increment option for the minutes and seconds.
7507 *
7508 * @param {jQuery} select Select element to operate on
7509 * @param {integer} count Value to loop to
7510 * @param {integer} inc Increment value
7511 * @private
7512 */
7513 _optionsTime: function ( select, count, inc ) {
7514 var classPrefix = this.c.classPrefix;
7515 var sel = this.dom.container.find('select.'+classPrefix+'-'+select);
7516 var start=0, end=count;
7517 var render = count === 12 ?
7518 function (i) { return i; } :
7519 this._pad;
7520
7521 if ( count === 12 ) {
7522 start = 1;
7523 end = 13;
7524 }
7525
7526 for ( var i=start ; i<end ; i+=inc ) {
7527 sel.append( '<option value="'+i+'">'+render(i)+'</option>' );
7528 }
7529 },
7530
7531 /**
7532 * Create the options for the month and year
7533 *
7534 * @param {integer} year Year
7535 * @param {integer} month Month (starting at 0)
7536 * @private
7537 */
7538 _optionsTitle: function ( year, month ) {
7539 var classPrefix = this.c.classPrefix;
7540 var i18n = this.c.i18n;
7541 var min = this.c.minDate;
7542 var max = this.c.maxDate;
7543 var minYear = min ? min.getFullYear() : null;
7544 var maxYear = max ? max.getFullYear() : null;
7545
7546 var i = minYear !== null ? minYear : new Date().getFullYear() - this.c.yearRange;
7547 var j = maxYear !== null ? maxYear : new Date().getFullYear() + this.c.yearRange;
7548
7549 this._options( 'month', this._range( 0, 11 ), i18n.months );
7550 this._options( 'year', this._range( i, j ) );
7551 },
7552
7553 /**
7554 * Simple two digit pad
7555 *
7556 * @param {integer} i Value that might need padding
7557 * @return {string|integer} Padded value
7558 * @private
7559 */
7560 _pad: function ( i ) {
7561 return i<10 ? '0'+i : i;
7562 },
7563
7564 /**
7565 * Position the calender to look attached to the input element
7566 * @private
7567 */
7568 _position: function () {
7569 var offset = this.dom.input.offset();
7570 var container = this.dom.container;
7571 var inputHeight = this.dom.input.outerHeight();
7572
7573 container
7574 .css( {
7575 top: offset.top + inputHeight,
7576 left: offset.left
7577 } )
7578 .appendTo( 'body' );
7579
7580 var calHeight = container.outerHeight();
7581 var scrollTop = $('body').scrollTop();
7582
7583 if ( offset.top + inputHeight + calHeight - scrollTop > $(window).height() ) {
7584 var newTop = offset.top - calHeight;
7585
7586 container.css( 'top', newTop < 0 ? 0 : newTop );
7587 }
7588 },
7589
7590 /**
7591 * Create a simple array with a range of values
7592 *
7593 * @param {integer} start Start value (inclusive)
7594 * @param {integer} end End value (inclusive)
7595 * @return {array} Created array
7596 * @private
7597 */
7598 _range: function ( start, end ) {
7599 var a = [];
7600
7601 for ( var i=start ; i<=end ; i++ ) {
7602 a.push( i );
7603 }
7604
7605 return a;
7606 },
7607
7608 /**
7609 * Redraw the calender based on the display date - this is a destructive
7610 * operation
7611 *
7612 * @private
7613 */
7614 _setCalander: function () {
7615 this.dom.calendar
7616 .empty()
7617 .append( this._htmlMonth(
7618 this.s.display.getUTCFullYear(),
7619 this.s.display.getUTCMonth()
7620 ) );
7621 },
7622
7623 /**
7624 * Set the month and year for the calender based on the current display date
7625 *
7626 * @private
7627 */
7628 _setTitle: function () {
7629 this._optionSet( 'month', this.s.display.getUTCMonth() );
7630 this._optionSet( 'year', this.s.display.getUTCFullYear() );
7631 },
7632
7633 /**
7634 * Set the time based on the current value of the widget
7635 *
7636 * @private
7637 */
7638 _setTime: function () {
7639 var d = this.s.d;
7640 var hours = d ? d.getUTCHours() : 0;
7641
7642 if ( this.s.parts.hours12 ) {
7643 this._optionSet( 'hours', this._hours24To12( hours ) );
7644 this._optionSet( 'ampm', hours < 12 ? 'am' : 'pm' );
7645 }
7646 else {
7647 this._optionSet( 'hours', hours );
7648 }
7649
7650 this._optionSet( 'minutes', d ? d.getUTCMinutes() : 0 );
7651 this._optionSet( 'seconds', d ? d.getSeconds() : 0 );
7652 },
7653
7654 /**
7655 * Show the widget and add events to the document required only while it
7656 * is displayed
7657 *
7658 * @private
7659 */
7660 _show: function () {
7661 var that = this;
7662 var namespace = this.s.namespace;
7663
7664 this._position();
7665
7666 // Need to reposition on scroll
7667 $(window).on( 'scroll.'+namespace+' resize.'+namespace, function () {
7668 that._position();
7669 } );
7670
7671 $('div.DTE_Body_Content').on( 'scroll.'+namespace, function () {
7672 that._position();
7673 } );
7674
7675 // On tab focus will move to a different field (no keyboard navigation
7676 // in the date picker - this might need to be changed).
7677 // On esc the Editor might close. Even if it doesn't the date picker
7678 // should
7679 $(document).on( 'keydown.'+namespace, function (e) {
7680 if (
7681 e.keyCode === 9 || // tab
7682 e.keyCode === 27 || // esc
7683 e.keyCode === 13 // return
7684 ) {
7685 that._hide();
7686 }
7687 } );
7688
7689 // Hide if clicking outside of the widget - but in a different click
7690 // event from the one that was used to trigger the show (bubble and
7691 // inline)
7692 setTimeout( function () {
7693 $('body').on( 'click.'+namespace, function (e) {
7694 var parents = $(e.target).parents();
7695
7696 if ( ! parents.filter( that.dom.container ).length && e.target !== that.dom.input[0] ) {
7697 that._hide();
7698 }
7699 } );
7700 }, 10 );
7701 },
7702
7703 /**
7704 * Write the formatted string to the input element this control is attached
7705 * to
7706 *
7707 * @private
7708 */
7709 _writeOutput: function ( focus ) {
7710 var date = this.s.d;
7711
7712 // Use moment if possible - otherwise it must be ISO8601 (or the
7713 // constructor would have thrown an error)
7714 var out = window.moment ?
7715 window.moment.utc( date, undefined, this.c.momentLocale, this.c.momentStrict ).format( this.c.format ) :
7716 date.getUTCFullYear() +'-'+
7717 this._pad(date.getUTCMonth() + 1) +'-'+
7718 this._pad(date.getUTCDate());
7719
7720 this.dom.input.val( out );
7721
7722 if ( focus ) {
7723 this.dom.input.focus();
7724 }
7725 }
7726 } );
7727
7728
7729 /**
7730 * For generating unique namespaces
7731 *
7732 * @type {Number}
7733 * @private
7734 */
7735 Editor.DateTime._instance = 0;
7736
7737 /**
7738 * Defaults for the date time picker
7739 *
7740 * @type {Object}
7741 */
7742 Editor.DateTime.defaults = {
7743 // Not documented - could be an internal property
7744 classPrefix: 'editor-datetime',
7745
7746 // function or array of ints
7747 disableDays: null,
7748
7749 // first day of the week (0: Sunday, 1: Monday, etc)
7750 firstDay: 1,
7751
7752 format: 'YYYY-MM-DD',
7753
7754 // Not documented as i18n is done by the Editor.defaults.i18n obj
7755 i18n: Editor.defaults.i18n.datetime,
7756
7757 maxDate: null,
7758
7759 minDate: null,
7760
7761 minutesIncrement: 1,
7762
7763 momentStrict: true,
7764
7765 momentLocale: 'en',
7766
7767 secondsIncrement: 1,
7768
7769 // show the ISO week number at the head of the row
7770 showWeekNumber: false,
7771
7772 // overruled by max / min date
7773 yearRange: 10
7774 };
7775
7776
7777
7778 (function() {
7779
7780 var fieldTypes = Editor.fieldTypes;
7781
7782 // Upload private helper method
7783 function _buttonText ( conf, text )
7784 {
7785 if ( text === null || text === undefined ) {
7786 text = conf.uploadText || "Choose file...";
7787 }
7788
7789 conf._input.find('div.upload button').html( text );
7790 }
7791
7792 function _commonUpload ( editor, conf, dropCallback )
7793 {
7794 var btnClass = editor.classes.form.button;
7795 var container = $(
7796 '<div class="editor_upload">'+
7797 '<div class="eu_table">'+
7798 '<div class="row">'+
7799 '<div class="cell upload">'+
7800 '<button class="'+btnClass+'" />'+
7801 '<input type="file"/>'+
7802 '</div>'+
7803 '<div class="cell clearValue">'+
7804 '<button class="'+btnClass+'" />'+
7805 '</div>'+
7806 '</div>'+
7807 '<div class="row second">'+
7808 '<div class="cell">'+
7809 '<div class="drop"><span/></div>'+
7810 '</div>'+
7811 '<div class="cell">'+
7812 '<div class="rendered"/>'+
7813 '</div>'+
7814 '</div>'+
7815 '</div>'+
7816 '</div>'
7817 );
7818
7819 conf._input = container;
7820 conf._enabled = true;
7821
7822 _buttonText( conf );
7823
7824 if ( window.FileReader && conf.dragDrop !== false ) {
7825 container.find('div.drop span').text(
7826 conf.dragDropText || "Drag and drop a file here to upload"
7827 );
7828
7829 var dragDrop = container.find('div.drop');
7830 dragDrop
7831 .on( 'drop', function (e) {
7832 if ( conf._enabled ) {
7833 Editor.upload( editor, conf, e.originalEvent.dataTransfer.files, _buttonText, dropCallback );
7834 dragDrop.removeClass('over');
7835 }
7836 return false;
7837 } )
7838 .on( 'dragleave dragexit', function (e) {
7839 if ( conf._enabled ) {
7840 dragDrop.removeClass('over');
7841 }
7842 return false;
7843 } )
7844 .on( 'dragover', function (e) {
7845 if ( conf._enabled ) {
7846 dragDrop.addClass('over');
7847 }
7848 return false;
7849 } );
7850
7851 // When an Editor is open with a file upload input there is a
7852 // reasonable chance that the user will miss the drop point when
7853 // dragging and dropping. Rather than loading the file in the browser,
7854 // we want nothing to happen, otherwise the form will be lost.
7855 editor
7856 .on( 'open', function () {
7857 $('body').on( 'dragover.DTE_Upload drop.DTE_Upload', function (e) {
7858 return false;
7859 } );
7860 } )
7861 .on( 'close', function () {
7862 $('body').off( 'dragover.DTE_Upload drop.DTE_Upload' );
7863 } );
7864 }
7865 else {
7866 container.addClass( 'noDrop' );
7867 container.append( container.find('div.rendered') );
7868 }
7869
7870 container.find('div.clearValue button').on( 'click', function () {
7871 Editor.fieldTypes.upload.set.call( editor, conf, '' );
7872 } );
7873
7874 container.find('input[type=file]').on('change', function () {
7875 Editor.upload( editor, conf, this.files, _buttonText, function (ids) {
7876 dropCallback.call( editor, ids );
7877
7878 // Clear the value so change will happen on the next file select,
7879 // even if it is the same file
7880 container.find('input[type=file]').val('');
7881 } );
7882 } );
7883
7884 return container;
7885 }
7886
7887 // Typically a change event caused by the end user will be added to a queue that
7888 // the browser will handle when no other script is running. However, using
7889 // `$().trigger()` will cause it to happen immediately, so in order to simulate
7890 // the standard browser behaviour we use setTimeout. This also means that
7891 // `dependent()` and other change event listeners will trigger when the field
7892 // values have all been set, rather than as they are being set - 31594
7893 function _triggerChange ( input ) {
7894 setTimeout( function () {
7895 input.trigger( 'change', {editor: true, editorSet: true} ); // editorSet legacy
7896 }, 0 );
7897 }
7898
7899
7900 // A number of the fields in this file use the same get, set, enable and disable
7901 // methods (specifically the text based controls), so in order to reduce the code
7902 // size, we just define them once here in our own local base model for the field
7903 // types.
7904 var baseFieldType = $.extend( true, {}, Editor.models.fieldType, {
7905 get: function ( conf ) {
7906 return conf._input.val();
7907 },
7908
7909 set: function ( conf, val ) {
7910 conf._input.val( val );
7911 _triggerChange( conf._input );
7912 },
7913
7914 enable: function ( conf ) {
7915 conf._input.prop( 'disabled', false );
7916 },
7917
7918 disable: function ( conf ) {
7919 conf._input.prop( 'disabled', true );
7920 }
7921 } );
7922
7923
7924
7925 fieldTypes.hidden = {
7926 create: function ( conf ) {
7927 conf._val = conf.value;
7928 return null;
7929 },
7930
7931 get: function ( conf ) {
7932 return conf._val;
7933 },
7934
7935 set: function ( conf, val ) {
7936 conf._val = val;
7937 }
7938 };
7939
7940
7941 fieldTypes.readonly = $.extend( true, {}, baseFieldType, {
7942 create: function ( conf ) {
7943 conf._input = $('<input/>').attr( $.extend( {
7944 id: Editor.safeId( conf.id ),
7945 type: 'text',
7946 readonly: 'readonly'
7947 }, conf.attr || {} ) );
7948
7949 return conf._input[0];
7950 }
7951 } );
7952
7953
7954 fieldTypes.text = $.extend( true, {}, baseFieldType, {
7955 create: function ( conf ) {
7956 conf._input = $('<input/>').attr( $.extend( {
7957 id: Editor.safeId( conf.id ),
7958 type: 'text'
7959 }, conf.attr || {} ) );
7960
7961 return conf._input[0];
7962 }
7963 } );
7964
7965
7966 fieldTypes.password = $.extend( true, {}, baseFieldType, {
7967 create: function ( conf ) {
7968 conf._input = $('<input/>').attr( $.extend( {
7969 id: Editor.safeId( conf.id ),
7970 type: 'password'
7971 }, conf.attr || {} ) );
7972
7973 return conf._input[0];
7974 }
7975 } );
7976
7977 fieldTypes.textarea = $.extend( true, {}, baseFieldType, {
7978 create: function ( conf ) {
7979 conf._input = $('<textarea/>').attr( $.extend( {
7980 id: Editor.safeId( conf.id )
7981 }, conf.attr || {} ) );
7982 return conf._input[0];
7983 }
7984 } );
7985
7986
7987 fieldTypes.select = $.extend( true, {}, baseFieldType, {
7988 // Locally "private" function that can be reused for the create and update methods
7989 _addOptions: function ( conf, opts ) {
7990 var elOpts = conf._input[0].options;
7991 var countOffset = 0;
7992
7993 elOpts.length = 0;
7994
7995 if ( conf.placeholder !== undefined ) {
7996 countOffset += 1;
7997 elOpts[0] = new Option( conf.placeholder, conf.placeholderValue !== undefined ?
7998 conf.placeholderValue :
7999 ''
8000 );
8001
8002 var disabled = conf.placeholderDisabled !== undefined ?
8003 conf.placeholderDisabled :
8004 true;
8005
8006 elOpts[0].hidden = disabled; // can't be hidden if not disabled!
8007 elOpts[0].disabled = disabled;
8008 }
8009
8010 if ( opts ) {
8011 Editor.pairs( opts, conf.optionsPair, function ( val, label, i ) {
8012 elOpts[ i+countOffset ] = new Option( label, val );
8013 elOpts[ i+countOffset ]._editor_val = val;
8014 } );
8015 }
8016 },
8017
8018 create: function ( conf ) {
8019 conf._input = $('<select/>')
8020 .attr( $.extend( {
8021 id: Editor.safeId( conf.id ),
8022 multiple: conf.multiple === true
8023 }, conf.attr || {} ) )
8024 .on( 'change.dte', function (e, d) {
8025 // On change, get the user selected value and store it as the
8026 // last set, so `update` can reflect it. This way `_lastSet`
8027 // always gives the intended value, be it set via the API or by
8028 // the end user.
8029 if ( ! d || ! d.editor ) {
8030 conf._lastSet = fieldTypes.select.get( conf );
8031 }
8032 } );
8033
8034 fieldTypes.select._addOptions( conf, conf.options || conf.ipOpts );
8035
8036 return conf._input[0];
8037 },
8038
8039 update: function ( conf, options ) {
8040 fieldTypes.select._addOptions( conf, options );
8041
8042 // Attempt to set the last selected value (set by the API or the end
8043 // user, they get equal priority)
8044 var lastSet = conf._lastSet;
8045
8046 if ( lastSet !== undefined ) {
8047 fieldTypes.select.set( conf, lastSet, true );
8048 }
8049
8050 _triggerChange( conf._input );
8051 },
8052
8053 get: function ( conf ) {
8054 var val = conf._input.find('option:selected').map( function () {
8055 return this._editor_val;
8056 } ).toArray();
8057
8058 if ( conf.multiple ) {
8059 return conf.separator ?
8060 val.join( conf.separator ) :
8061 val;
8062 }
8063
8064 return val.length ? val[0] : null;
8065 },
8066
8067 set: function ( conf, val, localUpdate ) {
8068 if ( ! localUpdate ) {
8069 conf._lastSet = val;
8070 }
8071
8072 // Can't just use `$().val()` because it won't work with strong types
8073 if ( conf.multiple && conf.separator && ! $.isArray( val ) ) {
8074 val = val.split( conf.separator );
8075 }
8076 else if ( ! $.isArray( val ) ) {
8077 val = [ val ];
8078 }
8079
8080 var i, len=val.length, found, allFound = false;
8081 var options = conf._input.find('option');
8082
8083 conf._input.find('option').each( function () {
8084 found = false;
8085
8086 for ( i=0 ; i<len ; i++ ) {
8087 // Weak typing
8088 if ( this._editor_val == val[i] ) {
8089 found = true;
8090 allFound = true;
8091 break;
8092 }
8093 }
8094
8095 this.selected = found;
8096 } );
8097
8098 // If there is a placeholder, we might need to select it if nothing else
8099 // was selected. It doesn't make sense to select when multi is enabled
8100 if ( conf.placeholder && ! allFound && ! conf.multiple && options.length ) {
8101 options[0].selected = true;
8102 }
8103
8104 // Update will call change itself, otherwise multiple might be called
8105 if ( ! localUpdate ) {
8106 _triggerChange( conf._input );
8107 }
8108
8109 return allFound;
8110 },
8111
8112 destroy: function ( conf ) {
8113 conf._input.off( 'change.dte' );
8114 }
8115 } );
8116
8117
8118 fieldTypes.checkbox = $.extend( true, {}, baseFieldType, {
8119 // Locally "private" function that can be reused for the create and update methods
8120 _addOptions: function ( conf, opts ) {
8121 var val, label;
8122 var elOpts = conf._input[0].options;
8123 var jqInput = conf._input.empty();
8124
8125 if ( opts ) {
8126 Editor.pairs( opts, conf.optionsPair, function ( val, label, i ) {
8127 jqInput.append(
8128 '<div>'+
8129 '<input id="'+Editor.safeId( conf.id )+'_'+i+'" type="checkbox" />'+
8130 '<label for="'+Editor.safeId( conf.id )+'_'+i+'">'+label+'</label>'+
8131 '</div>'
8132 );
8133 $('input:last', jqInput).attr('value', val)[0]._editor_val = val;
8134 } );
8135 }
8136 },
8137
8138
8139 create: function ( conf ) {
8140 conf._input = $('<div />');
8141 fieldTypes.checkbox._addOptions( conf, conf.options || conf.ipOpts );
8142
8143 return conf._input[0];
8144 },
8145
8146 get: function ( conf ) {
8147 var out = [];
8148 conf._input.find('input:checked').each( function () {
8149 out.push( this._editor_val );
8150 } );
8151 return ! conf.separator ?
8152 out :
8153 out.length === 1 ?
8154 out[0] :
8155 out.join(conf.separator);
8156 },
8157
8158 set: function ( conf, val ) {
8159 var jqInputs = conf._input.find('input');
8160 if ( ! $.isArray(val) && typeof val === 'string' ) {
8161 val = val.split( conf.separator || '|' );
8162 }
8163 else if ( ! $.isArray(val) ) {
8164 val = [ val ];
8165 }
8166
8167 var i, len=val.length, found;
8168
8169 jqInputs.each( function () {
8170 found = false;
8171
8172 for ( i=0 ; i<len ; i++ ) {
8173 if ( this._editor_val == val[i] ) {
8174 found = true;
8175 break;
8176 }
8177 }
8178
8179 this.checked = found;
8180 } );
8181
8182 _triggerChange( jqInputs );
8183 },
8184
8185 enable: function ( conf ) {
8186 conf._input.find('input').prop('disabled', false);
8187 },
8188
8189 disable: function ( conf ) {
8190 conf._input.find('input').prop('disabled', true);
8191 },
8192
8193 update: function ( conf, options ) {
8194 // Get the current value
8195 var checkbox = fieldTypes.checkbox;
8196 var currVal = checkbox.get( conf );
8197
8198 checkbox._addOptions( conf, options );
8199 checkbox.set( conf, currVal );
8200 }
8201 } );
8202
8203
8204 fieldTypes.radio = $.extend( true, {}, baseFieldType, {
8205 // Locally "private" function that can be reused for the create and update methods
8206 _addOptions: function ( conf, opts ) {
8207 var val, label;
8208 var elOpts = conf._input[0].options;
8209 var jqInput = conf._input.empty();
8210
8211 if ( opts ) {
8212 Editor.pairs( opts, conf.optionsPair, function ( val, label, i ) {
8213 jqInput.append(
8214 '<div>'+
8215 '<input id="'+Editor.safeId( conf.id )+'_'+i+'" type="radio" name="'+conf.name+'" />'+
8216 '<label for="'+Editor.safeId( conf.id )+'_'+i+'">'+label+'</label>'+
8217 '</div>'
8218 );
8219 $('input:last', jqInput).attr('value', val)[0]._editor_val = val;
8220 } );
8221 }
8222 },
8223
8224
8225 create: function ( conf ) {
8226 conf._input = $('<div />');
8227 fieldTypes.radio._addOptions( conf, conf.options || conf.ipOpts );
8228
8229 // this is ugly, but IE6/7 has a problem with radio elements that are created
8230 // and checked before being added to the DOM! Basically it doesn't check them. As
8231 // such we use the _preChecked property to set cache the checked button and then
8232 // check it again when the display is shown. This has no effect on other browsers
8233 // other than to cook a few clock cycles.
8234 this.on('open', function () {
8235 conf._input.find('input').each( function () {
8236 if ( this._preChecked ) {
8237 this.checked = true;
8238 }
8239 } );
8240 } );
8241
8242 return conf._input[0];
8243 },
8244
8245 get: function ( conf ) {
8246 var el = conf._input.find('input:checked');
8247 return el.length ? el[0]._editor_val : undefined;
8248 },
8249
8250 set: function ( conf, val ) {
8251 var that = this;
8252
8253 conf._input.find('input').each( function () {
8254 this._preChecked = false;
8255
8256 if ( this._editor_val == val ) {
8257 this.checked = true;
8258 this._preChecked = true;
8259 }
8260 else {
8261 // In a detached DOM tree, there is no relationship between the
8262 // input elements, so we need to uncheck any element that does
8263 // not match the value
8264 this.checked = false;
8265 this._preChecked = false;
8266 }
8267 } );
8268
8269 _triggerChange( conf._input.find('input:checked') );
8270 },
8271
8272 enable: function ( conf ) {
8273 conf._input.find('input').prop('disabled', false);
8274 },
8275
8276 disable: function ( conf ) {
8277 conf._input.find('input').prop('disabled', true);
8278 },
8279
8280 update: function ( conf, options ) {
8281 var radio = fieldTypes.radio;
8282 var currVal = radio.get( conf );
8283
8284 radio._addOptions( conf, options );
8285
8286 // Select the current value if it exists in the new data set, otherwise
8287 // select the first radio input so there is always a value selected
8288 var inputs = conf._input.find('input');
8289 radio.set( conf, inputs.filter('[value="'+currVal+'"]').length ?
8290 currVal :
8291 inputs.eq(0).attr('value')
8292 );
8293 }
8294 } );
8295
8296
8297 fieldTypes.date = $.extend( true, {}, baseFieldType, {
8298 create: function ( conf ) {
8299 conf._input = $('<input />').attr( $.extend( {
8300 id: Editor.safeId( conf.id ),
8301 type: 'text'
8302 }, conf.attr ) );
8303
8304 if ( $.datepicker ) {
8305 // jQuery UI date picker
8306 conf._input.addClass( 'jqueryui' );
8307
8308 if ( ! conf.dateFormat ) {
8309 conf.dateFormat = $.datepicker.RFC_2822;
8310 }
8311
8312 if ( conf.dateImage === undefined ) {
8313 conf.dateImage = "../../images/calender.png";
8314 }
8315
8316 // Allow the element to be attached to the DOM
8317 setTimeout( function () {
8318 $( conf._input ).datepicker( $.extend( {
8319 showOn: "both",
8320 dateFormat: conf.dateFormat,
8321 buttonImage: conf.dateImage,
8322 buttonImageOnly: true
8323 }, conf.opts ) );
8324
8325 $('#ui-datepicker-div').css('display','none');
8326 }, 10 );
8327 }
8328 else {
8329 // HTML5 (only Chrome and Edge on the desktop support this atm)
8330 conf._input.attr( 'type', 'date' );
8331 }
8332
8333 return conf._input[0];
8334 },
8335
8336 // use default get method as will work for all
8337
8338 set: function ( conf, val ) {
8339 if ( $.datepicker && conf._input.hasClass('hasDatepicker') ) {
8340 // Due to the async init of the control it is possible that we might
8341 // try to set a value before it has been initialised!
8342 conf._input.datepicker( "setDate" , val ).change();
8343 }
8344 else {
8345 $(conf._input).val( val );
8346 }
8347 },
8348
8349 enable: function ( conf ) {
8350 $.datepicker ?
8351 conf._input.datepicker( "enable" ) :
8352 $(conf._input).prop( 'disabled', false );
8353 },
8354
8355 disable: function ( conf ) {
8356 $.datepicker ?
8357 conf._input.datepicker( "disable" ) :
8358 $(conf._input).prop( 'disabled', true );
8359 },
8360
8361 owns: function ( conf, node ) {
8362 return $(node).parents('div.ui-datepicker').length || $(node).parents('div.ui-datepicker-header').length ?
8363 true :
8364 false;
8365 }
8366 } );
8367
8368
8369 fieldTypes.datetime = $.extend( true, {}, baseFieldType, {
8370 create: function ( conf ) {
8371 conf._input = $('<input />').attr( $.extend( true, {
8372 id: Editor.safeId( conf.id ),
8373 type: 'text'
8374 }, conf.attr ) );
8375
8376 conf._picker = new Editor.DateTime( conf._input, $.extend( {
8377 format: conf.format, // can be undefined
8378 i18n: this.i18n.datetime
8379 }, conf.opts ) );
8380
8381 return conf._input[0];
8382 },
8383
8384 // default get, disable and enable options are okay
8385
8386 set: function ( conf, val ) {
8387 conf._picker.val( val );
8388
8389 _triggerChange( conf._input );
8390 },
8391
8392 owns: function ( conf, node ) {
8393 return conf._picker.owns( node );
8394 },
8395
8396 destroy: function ( conf ) {
8397 conf._picker.destroy();
8398 },
8399
8400 minDate: function ( conf, min ) {
8401 conf._picker.min( min );
8402 },
8403
8404 maxDate: function ( conf, max ) {
8405 conf._picker.max( max );
8406 }
8407 } );
8408
8409
8410 fieldTypes.upload = $.extend( true, {}, baseFieldType, {
8411 create: function ( conf ) {
8412 var editor = this;
8413 var container = _commonUpload( editor, conf, function ( val ) {
8414 Editor.fieldTypes.upload.set.call( editor, conf, val[0] );
8415 } );
8416
8417 return container;
8418 },
8419
8420 get: function ( conf ) {
8421 return conf._val;
8422 },
8423
8424 set: function ( conf, val ) {
8425 conf._val = val;
8426
8427 var container = conf._input;
8428
8429 if ( conf.display ) {
8430 var rendered = container.find('div.rendered');
8431
8432 if ( conf._val ) {
8433 rendered.html( conf.display( conf._val ) );
8434 }
8435 else {
8436 rendered
8437 .empty()
8438 .append( '<span>'+( conf.noFileText || 'No file' )+'</span>' );
8439 }
8440 }
8441
8442 var button = container.find('div.clearValue button');
8443 if ( val && conf.clearText ) {
8444 button.html( conf.clearText );
8445 container.removeClass( 'noClear' );
8446 }
8447 else {
8448 container.addClass( 'noClear' );
8449 }
8450
8451 conf._input.find('input').triggerHandler( 'upload.editor', [ conf._val ] );
8452 },
8453
8454 enable: function ( conf ) {
8455 conf._input.find('input').prop('disabled', false);
8456 conf._enabled = true;
8457 },
8458
8459 disable: function ( conf ) {
8460 conf._input.find('input').prop('disabled', true);
8461 conf._enabled = false;
8462 }
8463 } );
8464
8465
8466 fieldTypes.uploadMany = $.extend( true, {}, baseFieldType, {
8467 create: function ( conf ) {
8468 var editor = this;
8469 var container = _commonUpload( editor, conf, function ( val ) {
8470 conf._val = conf._val.concat( val );
8471 Editor.fieldTypes.uploadMany.set.call( editor, conf, conf._val );
8472 } );
8473
8474 container
8475 .addClass( 'multi' )
8476 .on( 'click', 'button.remove', function (e) {
8477 e.stopPropagation();
8478
8479 var idx = $(this).data('idx');
8480
8481 conf._val.splice( idx, 1 );
8482 Editor.fieldTypes.uploadMany.set.call( editor, conf, conf._val );
8483 } );
8484
8485 return container;
8486 },
8487
8488 get: function ( conf ) {
8489 return conf._val;
8490 },
8491
8492 set: function ( conf, val ) {
8493 // Default value for fields is an empty string, whereas we want []
8494 if ( ! val ) {
8495 val = [];
8496 }
8497
8498 if ( ! $.isArray( val ) ) {
8499 throw 'Upload collections must have an array as a value';
8500 }
8501
8502 conf._val = val;
8503
8504 var that = this;
8505 var container = conf._input;
8506
8507 if ( conf.display ) {
8508 var rendered = container.find('div.rendered').empty();
8509
8510 if ( val.length ) {
8511 var list = $('<ul/>').appendTo( rendered );
8512
8513 $.each( val, function ( i, file ) {
8514 list.append(
8515 '<li>'+
8516 conf.display( file, i )+
8517 ' <button class="'+that.classes.form.button+' remove" data-idx="'+i+'">&times;</button>'+
8518 '</li>'
8519 );
8520 } );
8521 }
8522 else {
8523 rendered.append( '<span>'+( conf.noFileText || 'No files' )+'</span>' );
8524 }
8525 }
8526
8527 conf._input.find('input').triggerHandler( 'upload.editor', [ conf._val ] );
8528 },
8529
8530 enable: function ( conf ) {
8531 conf._input.find('input').prop('disabled', false);
8532 conf._enabled = true;
8533 },
8534
8535 disable: function ( conf ) {
8536 conf._input.find('input').prop('disabled', true);
8537 conf._enabled = false;
8538 }
8539 } );
8540
8541
8542 }());
8543
8544
8545 // If there are field types available on DataTables we copy them in (after the
8546 // built in ones to allow overrides) and then expose the field types object.
8547 if ( DataTable.ext.editorFields ) {
8548 $.extend( Editor.fieldTypes, DataTable.ext.editorFields );
8549 }
8550
8551 DataTable.ext.editorFields = Editor.fieldTypes;
8552
8553
8554 /**
8555 * File information for uploads
8556 */
8557 Editor.files = {};
8558
8559
8560 /**
8561 * Name of this class
8562 * @constant CLASS
8563 * @type String
8564 * @default Editor
8565 */
8566 Editor.prototype.CLASS = "Editor";
8567
8568
8569 /**
8570 * DataTables Editor version
8571 * @constant Editor.VERSION
8572 * @type String
8573 * @default See code
8574 * @static
8575 */
8576 Editor.version = "1.5.6";
8577
8578
8579 // Event documentation for JSDoc
8580 /**
8581 * Processing event, fired when Editor submits data to the server for processing.
8582 * This can be used to provide your own processing indicator if your UI framework
8583 * already has one.
8584 * @name Editor#processing
8585 * @event
8586 * @param {event} e jQuery event object
8587 * @param {boolean} processing Flag for if the processing is running (true) or
8588 * not (false).
8589 */
8590
8591 /**
8592 * Form displayed event, fired when the form is made available in the DOM. This
8593 * can be useful for fields that require height and width calculations to be
8594 * performed since the element is not available in the document until the
8595 * form is displayed.
8596 * @name Editor#open
8597 * @event
8598 * @param {event} e jQuery event object
8599 * @param {string} type Editing type
8600 */
8601
8602 /**
8603 * Before a form is displayed, this event is fired. It allows the open action to be
8604 * cancelled by returning false from the function.
8605 * @name Editor#preOpen
8606 * @event
8607 * @param {event} e jQuery event object
8608 */
8609
8610 /**
8611 * Form hidden event, fired when the form is removed from the document. The
8612 * of the compliment `open` event.
8613 * @name Editor#close
8614 * @event
8615 * @param {event} e jQuery event object
8616 */
8617
8618 /**
8619 * Before a form is closed, this event is fired. It allows the close action to be
8620 * cancelled by returning false from the function. This can be useful for confirming
8621 * that the user actually wants to close the display (if they have unsaved changes
8622 * for example).
8623 * @name Editor#preClose
8624 * @event
8625 * @param {event} e jQuery event object
8626 * @param {string} trigger Action that caused the close event - can be undefined.
8627 * Typically defined by the display controller.
8628 */
8629
8630 /**
8631 * Emitted before a form blur occurs. A form blur is similar to a close, but
8632 * is triggered by a user, typically, clicking on the background, while a close
8633 * occurs due to a click on the close button. A blur can precede a close.
8634 * @name Editor#preBlur
8635 * @event
8636 * @param {event} e jQuery event object
8637 */
8638
8639 /**
8640 * Pre-submit event for the form, fired just before the data is submitted to
8641 * the server. This event allows you to modify the data that will be submitted
8642 * to the server. Note that this event runs after the 'formatdata' callback
8643 * function of the {@link Editor#submit} API method.
8644 * @name Editor#preSubmit
8645 * @event
8646 * @param {event} e jQuery event object
8647 * @param {object} data The data object that will be submitted to the server
8648 * @param {string} action The action type for this submit - `create`, `edit` or
8649 * `remove`.
8650 */
8651
8652 /**
8653 * Post-submit event for the form, fired immediately after the data has been
8654 * loaded by the Ajax call, allowing modification or any other interception
8655 * of the data returned form the server.
8656 * @name Editor#postSubmit
8657 * @event
8658 * @param {event} e jQuery event object
8659 * @param {object} json The JSON object returned from the server
8660 * @param {object} data The data object that was be submitted to the server
8661 * @param {string} action The action type for this submit - `create`, `edit` or
8662 * `remove`.
8663 */
8664
8665 /**
8666 * Submission complete event, fired when data has been submitted to the server and
8667 * after any of the return handling code has been run (updating the DataTable
8668 * for example). Note that unlike `submitSuccess` and `submitError`, `submitComplete`
8669 * will be fired for both a successful submission and an error. Additionally this
8670 * event will be fired after `submitSuccess` or `submitError`.
8671 * @name Editor#submitComplete
8672 * @event
8673 * @param {event} e jQuery event object
8674 * @param {object} json The JSON object returned from the server
8675 * @param {object} data The data that was used to update the DataTable
8676 */
8677
8678 /**
8679 * Submission complete and successful event, fired when data has been successfully
8680 * submitted to the server and all actions required by the returned data (inserting
8681 * or updating a row) have been completed.
8682 * @name Editor#submitSuccess
8683 * @event
8684 * @param {event} e jQuery event object
8685 * @param {object} json The JSON object returned from the server
8686 * @param {object} data The data that was used to update the DataTable
8687 */
8688
8689 /**
8690 * Submission complete, but in error event, fired when data has been submitted to
8691 * the server but an error occurred on the server (typically a JSON formatting error)
8692 * @name Editor#submitError
8693 * @event
8694 * @param {event} e jQuery event object
8695 * @param {object} xhr The Ajax object
8696 * @param {string} err The error message from jQuery
8697 * @param {object} thrown The exception thrown by jQuery
8698 * @param {object} data The data that was used to update the DataTable
8699 */
8700
8701 /**
8702 * Create method activated event, fired when the create API method has been called,
8703 * just prior to the form being shown. Useful for manipulating the form specifically
8704 * for the create state.
8705 * @name Editor#initCreate
8706 * @event
8707 * @param {event} e jQuery event object
8708 */
8709
8710 /**
8711 * Pre-create new row event, fired just before DataTables calls the fnAddData method
8712 * to add new data to the DataTable, allowing modification of the data that will be
8713 * used to insert into the table.
8714 * @name Editor#preCreate
8715 * @event
8716 * @param {event} e jQuery event object
8717 * @param {object} json The JSON object returned from the server
8718 * @param {object} data The data that will be used to update the DataTable
8719 */
8720
8721 /**
8722 * Create new row event, fired when a new row has been created in the DataTable by
8723 * a form submission. This is called just after the fnAddData call to the DataTable.
8724 * @name Editor#create
8725 * @event
8726 * @param {event} e jQuery event object
8727 * @param {object} json The JSON object returned from the server
8728 * @param {object} data The data that was used to update the DataTable
8729 */
8730
8731 /**
8732 * As per the `create` event - included for naming consistency.
8733 * @name Editor#postCreate
8734 * @event
8735 * @param {event} e jQuery event object
8736 * @param {object} json The JSON object returned from the server
8737 * @param {object} data The data that was used to update the DataTable
8738 */
8739
8740 /**
8741 * Edit method activated event, fired when the edit API method has been called,
8742 * just prior to the form being shown. Useful for manipulating the form specifically
8743 * for the edit state.
8744 * @name Editor#initEdit
8745 * @event
8746 * @param {event} e jQuery event object
8747 * @param {node} tr TR element of the row to be edited
8748 * @param {array|object} data Data source array / object for the row to be
8749 * edited
8750 */
8751
8752 /**
8753 * Pre-edit row event, fired just before DataTables calls the fnUpdate method
8754 * to edit data in a DataTables row, allowing modification of the data that will be
8755 * used to update the table.
8756 * @name Editor#preEdit
8757 * @event
8758 * @param {event} e jQuery event object
8759 * @param {object} json The JSON object returned from the server
8760 * @param {object} data The data that will be used to update the DataTable
8761 */
8762
8763 /**
8764 * Edit row event, fired when a row has been edited in the DataTable by a form
8765 * submission. This is called just after the fnUpdate call to the DataTable.
8766 * @name Editor#edit
8767 * @event
8768 * @param {event} e jQuery event object
8769 * @param {object} json The JSON object returned from the server
8770 * @param {object} data The data that was used to update the DataTable
8771 */
8772
8773 /**
8774 * As per the `edit` event - included for naming consistency.
8775 * @name Editor#postEdit
8776 * @event
8777 * @param {event} e jQuery event object
8778 * @param {object} json The JSON object returned from the server
8779 * @param {object} data The data that was used to update the DataTable
8780 */
8781
8782 /**
8783 * Remove method activated event, fired when the remove API method has been
8784 * called, just prior to the form being shown. Useful for manipulating the form
8785 * specifically for the remove state.
8786 * @name Editor#initRemove
8787 * @event
8788 * @param {event} e jQuery event object
8789 * @param {array} trs Array of the TR elements for the removed to be deleted
8790 * @param {array} data Array of the data source array / objects for the rows to
8791 * be deleted. This is in the same index order as the TR nodes in the second
8792 * parameter.
8793 */
8794
8795 /**
8796 * Pre-remove row event, fired just before DataTables calls the fnDeleteRow method
8797 * to delete a DataTables row.
8798 * @name Editor#preRemove
8799 * @event
8800 * @param {event} e jQuery event object
8801 * @param {object} json The JSON object returned from the server
8802 */
8803
8804 /**
8805 * Row removed event, fired when a row has been removed in the DataTable by a form
8806 * submission. This is called just after the fnDeleteRow call to the DataTable.
8807 * @name Editor#remove
8808 * @event
8809 * @param {event} e jQuery event object
8810 * @param {object} json The JSON object returned from the server
8811 */
8812
8813 /**
8814 * As per the `postRemove` event - included for naming consistency.
8815 * @name Editor#postRemove
8816 * @event
8817 * @param {event} e jQuery event object
8818 * @param {object} json The JSON object returned from the server
8819 */
8820
8821 /**
8822 * Set data event, fired when the data is gathered from the form to be used
8823 * to update the DataTable. This is a "global" version of `preCreate`, `preEdit`
8824 * and `preRemove` and can be used to manipulate the data that will be added
8825 * to the DataTable for all three actions
8826 * @name Editor#setData
8827 * @event
8828 * @param {event} e jQuery event object
8829 * @param {object} json The JSON object returned from the server
8830 * @param {object} data The data that will be used to update the DataTable
8831 * @param {string} action The action being performed by the form - 'create',
8832 * 'edit' or 'remove'.
8833 */
8834
8835 /**
8836 * Initialisation of the Editor instance has been completed.
8837 * @name Editor#initComplete
8838 * @event
8839 * @param {event} e jQuery event object
8840 */
8841
8842
8843 return Editor;
8844 }));