Mercurial > repos > immport-devteam > cs_overview
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(/>/g, '>') | |
454 .replace(/</g, '<') | |
455 .replace(/&/g, '&') | |
456 .replace(/"/g, '"') | |
457 .replace(/'/g, '\'') | |
458 .replace(/ /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">×</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+'">×</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 })); |