1 /* |
|
2 * MyAMS |
|
3 * « My Application Management Skin » |
|
4 * |
|
5 * $Tag$ (rev. 1) |
|
6 * A bootstrap based application/administration skin |
|
7 * |
|
8 * Custom administration and application skin tools |
|
9 * Released under Zope Public License ZPL 1.1 |
|
10 * ©2014-2020 Thierry Florac <tflorac@ulthar.net> |
|
11 */ |
|
12 |
|
13 "use strict"; |
|
14 |
|
15 (function ($, globals) { |
|
16 |
|
17 var console = globals.console; |
|
18 |
|
19 /** |
|
20 * String prototype extensions |
|
21 */ |
|
22 String.prototype.startsWith = function (str) { |
|
23 var slen = this.length, |
|
24 dlen = str.length; |
|
25 if (slen < dlen) { |
|
26 return false; |
|
27 } |
|
28 return (this.substr(0, dlen) === str); |
|
29 }; |
|
30 |
|
31 String.prototype.endsWith = function (str) { |
|
32 var slen = this.length, |
|
33 dlen = str.length; |
|
34 if (slen < dlen) { |
|
35 return false; |
|
36 } |
|
37 return (this.substr(slen - dlen) === str); |
|
38 }; |
|
39 |
|
40 String.prototype.unserialize = function (str) { |
|
41 var str = decodeURIComponent(this); |
|
42 var chunks = str.split('&'), |
|
43 obj = {}; |
|
44 for (var c = 0; c < chunks.length; c++) { |
|
45 var split = chunks[c].split('=', 2); |
|
46 obj[split[0]] = split[1]; |
|
47 } |
|
48 return obj; |
|
49 }; |
|
50 |
|
51 /** |
|
52 * Array prototype extensions |
|
53 */ |
|
54 if (!Array.prototype.indexOf) { |
|
55 Array.prototype.indexOf = function (elt, from) { |
|
56 var len = this.length; |
|
57 |
|
58 from = Number(from) || 0; |
|
59 from = (from < 0) ? Math.ceil(from) : Math.floor(from); |
|
60 if (from < 0) { |
|
61 from += len; |
|
62 } |
|
63 |
|
64 for (; from < len; from++) { |
|
65 if (from in this && this[from] === elt) { |
|
66 return from; |
|
67 } |
|
68 } |
|
69 return -1; |
|
70 }; |
|
71 } |
|
72 |
|
73 |
|
74 /** |
|
75 * JQuery 'hasvalue' expression |
|
76 * Filter inputs containing value |
|
77 */ |
|
78 $.expr[":"].hasvalue = function (obj, index, meta /*, stack*/) { |
|
79 return $(obj).val() !== ""; |
|
80 }; |
|
81 |
|
82 |
|
83 /** |
|
84 * JQuery 'econtains' expression |
|
85 * Case insensitive contains expression |
|
86 */ |
|
87 $.expr[":"].econtains = function (obj, index, meta /*, stack*/) { |
|
88 return (obj.textContent || obj.innerText || $(obj).text() || "").toLowerCase() === meta[3].toLowerCase(); |
|
89 }; |
|
90 |
|
91 |
|
92 /** |
|
93 * JQuery 'withtext' expression |
|
94 * Case sensitive exact search expression |
|
95 */ |
|
96 $.expr[":"].withtext = function (obj, index, meta /*, stack*/) { |
|
97 return (obj.textContent || obj.innerText || $(obj).text() || "") === meta[3]; |
|
98 }; |
|
99 |
|
100 |
|
101 /** |
|
102 * JQuery filter on parents class |
|
103 * This filter is often combined with ":not()" to select DOM objects which don't have |
|
104 * parents of a given class. |
|
105 * For example: |
|
106 * |
|
107 * $('.hint:not(:parents(.nohints))', element); |
|
108 * |
|
109 * will select all elements with ".hint" class which don't have a parent with '.nohints' class. |
|
110 */ |
|
111 $.expr[':'].parents = function (obj, index, meta /*, stack*/) { |
|
112 return $(obj).parents(meta[3]).length > 0; |
|
113 }; |
|
114 |
|
115 |
|
116 /** |
|
117 * JQuery 'scrollbarWidth' function |
|
118 * Get width of default vertical scrollbar |
|
119 */ |
|
120 if ($.scrollbarWidth === undefined) { |
|
121 $.scrollbarWidth = function () { |
|
122 var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body'); |
|
123 var child = parent.children(); |
|
124 var width = child.innerWidth() - child.height(99).innerWidth(); |
|
125 parent.remove(); |
|
126 return width; |
|
127 }; |
|
128 } |
|
129 |
|
130 |
|
131 /** |
|
132 * MyAMS JQuery extensions |
|
133 */ |
|
134 $.fn.extend({ |
|
135 |
|
136 /** |
|
137 * Check if current object is empty or not |
|
138 */ |
|
139 exists: function () { |
|
140 return $(this).length > 0; |
|
141 }, |
|
142 |
|
143 /** |
|
144 * Get object if it supports given CSS class, |
|
145 * otherwise look for parents |
|
146 */ |
|
147 objectOrParentWithClass: function (klass) { |
|
148 if (this.hasClass(klass)) { |
|
149 return this; |
|
150 } else { |
|
151 return this.parents('.' + klass); |
|
152 } |
|
153 }, |
|
154 |
|
155 /** |
|
156 * Build an array of attributes of the given selection |
|
157 */ |
|
158 listattr: function (attr) { |
|
159 var result = []; |
|
160 this.each(function () { |
|
161 result.push($(this).attr(attr)); |
|
162 }); |
|
163 return result; |
|
164 }, |
|
165 |
|
166 /** |
|
167 * CSS style function |
|
168 * Code from Aram Kocharyan on stackoverflow.com |
|
169 */ |
|
170 style: function (styleName, value, priority) { |
|
171 // DOM node |
|
172 var node = this.get(0); |
|
173 // Ensure we have a DOM node |
|
174 if (typeof(node) === 'undefined') { |
|
175 return; |
|
176 } |
|
177 // CSSStyleDeclaration |
|
178 var style = this.get(0).style; |
|
179 // Getter/Setter |
|
180 if (typeof(styleName) !== 'undefined') { |
|
181 if (typeof(value) !== 'undefined') { |
|
182 // Set style property |
|
183 priority = typeof(priority) !== 'undefined' ? priority : ''; |
|
184 style.setProperty(styleName, value, priority); |
|
185 return this; |
|
186 } else { |
|
187 // Get style property |
|
188 return style.getPropertyValue(styleName); |
|
189 } |
|
190 } else { |
|
191 // Get CSSStyleDeclaration |
|
192 return style; |
|
193 } |
|
194 }, |
|
195 |
|
196 /** |
|
197 * Remove CSS classes starting with a given prefix |
|
198 */ |
|
199 removeClassPrefix: function (prefix) { |
|
200 this.each(function (i, it) { |
|
201 var classes = it.className.split(" ").map(function (item) { |
|
202 return item.startsWith(prefix) ? "" : item; |
|
203 }); |
|
204 it.className = $.trim(classes.join(" ")); |
|
205 }); |
|
206 return this; |
|
207 } |
|
208 }); |
|
209 |
|
210 |
|
211 /** |
|
212 * MyAMS extensions to JQuery |
|
213 */ |
|
214 if (globals.MyAMS === undefined) { |
|
215 globals.MyAMS = { |
|
216 devmode: true, |
|
217 devext: '', |
|
218 lang: 'en', |
|
219 throttleDelay: 350, |
|
220 menuSpeed: 235, |
|
221 navbarHeight: 49, |
|
222 ajaxNav: true, |
|
223 safeMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'], |
|
224 csrfCookieName: 'csrf_token', |
|
225 csrfHeaderName: 'X-CSRF-Token', |
|
226 enableWidgets: true, |
|
227 enableMobile: false, |
|
228 enableFastclick: false, |
|
229 warnOnFormChange: false, |
|
230 formChangedCallback: null, |
|
231 ismobile: (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase())) |
|
232 }; |
|
233 } |
|
234 var MyAMS = globals.MyAMS; |
|
235 var ams = MyAMS; |
|
236 |
|
237 /** |
|
238 * Get MyAMS base URL |
|
239 * Copyright Andrew Davy: https://forrst.com/posts/Get_the_URL_of_the_current_javascript_file-Dst |
|
240 */ |
|
241 MyAMS.baseURL = (function () { |
|
242 var script = $('script[src*="/myams.js"], script[src*="/myams.min.js"], ' + |
|
243 'script[src*="/myams-core.js"], script[src*="/myams-core.min.js"], ' + |
|
244 'script[src*="/myams-require.js"], script[src*="/myams-require.min.js"]'); |
|
245 var src = script.attr("src"); |
|
246 ams.devmode = src.indexOf('.min.js') < 0; |
|
247 ams.devext = ams.devmode ? '' : '.min'; |
|
248 return src.substring(0, src.lastIndexOf('/') + 1); |
|
249 })(); |
|
250 |
|
251 |
|
252 /** |
|
253 * Basic logging function which log all arguments to console |
|
254 */ |
|
255 MyAMS.log = function () { |
|
256 if (console) { |
|
257 console.debug && console.debug(this, arguments); |
|
258 } |
|
259 }; |
|
260 |
|
261 |
|
262 /** |
|
263 * Extract parameter value from given query string |
|
264 */ |
|
265 MyAMS.getQueryVar = function (src, varName) { |
|
266 // Check src |
|
267 if (src.indexOf('?') < 0) { |
|
268 return false; |
|
269 } |
|
270 if (!src.endsWith('&')) { |
|
271 src += '&'; |
|
272 } |
|
273 // Dynamic replacement RegExp |
|
274 var regex = new RegExp('.*?[&\\?]' + varName + '=(.*?)&.*'); |
|
275 // Apply RegExp to the query string |
|
276 var val = src.replace(regex, "$1"); |
|
277 // If the string is the same, we didn't find a match - return false |
|
278 return val === src ? false : val; |
|
279 }; |
|
280 |
|
281 |
|
282 /** |
|
283 * Color conversion function |
|
284 */ |
|
285 MyAMS.rgb2hex = function (color) { |
|
286 return "#" + $.map(color.match(/\b(\d+)\b/g), function (digit) { |
|
287 return ('0' + parseInt(digit).toString(16)).slice(-2); |
|
288 }).join(''); |
|
289 }; |
|
290 |
|
291 |
|
292 /** |
|
293 * Generate a random ID |
|
294 */ |
|
295 MyAMS.generateId = function () { |
|
296 function s4() { |
|
297 return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); |
|
298 } |
|
299 |
|
300 return s4() + s4() + s4() + s4(); |
|
301 }; |
|
302 |
|
303 |
|
304 /** |
|
305 * Generate a random UUID |
|
306 */ |
|
307 MyAMS.generateUUID = function () { |
|
308 var d = new Date().getTime(); |
|
309 var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
|
310 var r = (d + Math.random() * 16) % 16 | 0; |
|
311 d = Math.floor(d / 16); |
|
312 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); |
|
313 }); |
|
314 return uuid; |
|
315 }; |
|
316 |
|
317 |
|
318 /** |
|
319 * Get an object given by name |
|
320 */ |
|
321 MyAMS.getObject = function (objectName, context) { |
|
322 if (!objectName) { |
|
323 return undefined; |
|
324 } |
|
325 if (typeof(objectName) !== 'string') { |
|
326 return objectName; |
|
327 } |
|
328 var namespaces = objectName.split("."); |
|
329 context = (context === undefined || context === null) ? window : context; |
|
330 for (var i = 0; i < namespaces.length; i++) { |
|
331 try { |
|
332 context = context[namespaces[i]]; |
|
333 } catch (e) { |
|
334 return undefined; |
|
335 } |
|
336 } |
|
337 return context; |
|
338 }; |
|
339 |
|
340 /** |
|
341 * Get and execute a function given by name |
|
342 * Small piece of code by Jason Bunting |
|
343 */ |
|
344 MyAMS.getFunctionByName = function (functionName, context) { |
|
345 if (functionName === undefined) { |
|
346 return undefined; |
|
347 } else if (typeof(functionName) === 'function') { |
|
348 return functionName; |
|
349 } |
|
350 var namespaces = functionName.split("."); |
|
351 var func = namespaces.pop(); |
|
352 context = (context === undefined || context === null) ? window : context; |
|
353 for (var i = 0; i < namespaces.length; i++) { |
|
354 try { |
|
355 context = context[namespaces[i]]; |
|
356 } catch (e) { |
|
357 return undefined; |
|
358 } |
|
359 } |
|
360 try { |
|
361 return context[func]; |
|
362 } catch (e) { |
|
363 return undefined; |
|
364 } |
|
365 }; |
|
366 |
|
367 MyAMS.executeFunctionByName = function (functionName, context /*, args */) { |
|
368 var func = ams.getFunctionByName(functionName, window); |
|
369 if (typeof(func) === 'function') { |
|
370 var args = Array.prototype.slice.call(arguments, 2); |
|
371 return func.apply(context, args); |
|
372 } |
|
373 }; |
|
374 |
|
375 /** |
|
376 * Check to know if given element is still present in DOM |
|
377 */ |
|
378 MyAMS.isInDOM = function (element) { |
|
379 element = $(element); |
|
380 if (!element.exists()) { |
|
381 return false; |
|
382 } |
|
383 return globals.document.body.contains(element[0]); |
|
384 }; |
|
385 |
|
386 /** |
|
387 * Get target URL matching given source |
|
388 * |
|
389 * Given URL can include variable names (with their namespace), given between braces, as in {MyAMS.baseURL} |
|
390 */ |
|
391 MyAMS.getSource = function (url) { |
|
392 return url.replace(/{[^{}]*}/g, function (match) { |
|
393 return ams.getFunctionByName(match.substr(1, match.length - 2)); |
|
394 }); |
|
395 }; |
|
396 |
|
397 /** |
|
398 * Script loader function |
|
399 * |
|
400 * @param url: script URL |
|
401 * @param callback: a callback to be called after script loading |
|
402 * @param options: a set of options to be added to AJAX call |
|
403 * @param onerror: an error callback to be called instead of generic callback |
|
404 */ |
|
405 MyAMS.getScript = function (url, callback, options, onerror) { |
|
406 if (typeof(callback) === 'object') { |
|
407 onerror = options; |
|
408 options = callback; |
|
409 callback = null; |
|
410 } |
|
411 if (options === undefined) { |
|
412 options = {}; |
|
413 } |
|
414 var defaults = { |
|
415 dataType: 'script', |
|
416 url: ams.getSource(url), |
|
417 success: callback, |
|
418 error: onerror || ams.error.show, |
|
419 cache: !ams.devmode, |
|
420 async: options.async === undefined ? typeof(callback) === 'function' : options.async |
|
421 }; |
|
422 var settings = $.extend({}, defaults, options); |
|
423 return $.ajax(settings); |
|
424 }; |
|
425 |
|
426 /** |
|
427 * CSS file loader function |
|
428 * Cross-browser code copied from Stoyan Stefanov blog to be able to |
|
429 * call a callback when CSS is realy loaded. |
|
430 * See: https://www.phpied.com/when-is-a-stylesheet-really-loaded |
|
431 * |
|
432 * @param url: CSS file URL |
|
433 * @param id: a unique ID given to CSS file |
|
434 * @param callback: optional callback function to be called when CSS file is loaded. If set, callback is called |
|
435 * with a 'first_load' boolean argument to indicate is CSS was already loaded (*false* value) or not (*true* |
|
436 * value). |
|
437 * @param options: callback options |
|
438 */ |
|
439 MyAMS.getCSS = function (url, id, callback, options) { |
|
440 if (callback) { |
|
441 callback = ams.getFunctionByName(callback); |
|
442 } |
|
443 var head = $('HEAD'); |
|
444 var style = $('style[data-ams-id="' + id + '"]', head); |
|
445 if (style.length === 0) { |
|
446 style = $('<style>').attr('data-ams-id', id) |
|
447 .text('@import "' + ams.getSource(url) + '";'); |
|
448 if (callback) { |
|
449 var styleInterval = setInterval(function () { |
|
450 try { |
|
451 var _check = style[0].sheet.cssRules; // Is only populated when file is loaded |
|
452 callback.call(window, true, options); |
|
453 clearInterval(styleInterval); |
|
454 } catch (e) { |
|
455 // CSS is not loaded yet... |
|
456 } |
|
457 }, 10); |
|
458 } |
|
459 style.appendTo(head); |
|
460 } else { |
|
461 if (callback) { |
|
462 callback.call(window, false, options); |
|
463 } |
|
464 } |
|
465 }; |
|
466 |
|
467 /** |
|
468 * Initialize main events handlers |
|
469 */ |
|
470 MyAMS.initHandlers = function(element) { |
|
471 |
|
472 // Initialize custom click handlers |
|
473 $(element).on('click', '[data-ams-click-handler]', function(event) { |
|
474 var source = $(this); |
|
475 var handlers = source.data('ams-disabled-handlers'); |
|
476 if ((handlers === true) || (handlers === 'click') || (handlers === 'all')) { |
|
477 return; |
|
478 } |
|
479 var data = source.data(); |
|
480 if (data.amsClickHandler) { |
|
481 if ((data.amsStopPropagation === true) || (data.amsClickStopPropagation === true)) { |
|
482 event.stopPropagation(); |
|
483 } |
|
484 if (data.amsClickKeepDefault !== true) { |
|
485 event.preventDefault(); |
|
486 } |
|
487 var clickHandlers = data.amsClickHandler.split(/\s+/); |
|
488 for (var index=0; index < clickHandlers.length; index++) { |
|
489 var callback = ams.getFunctionByName(clickHandlers[index]); |
|
490 if (callback !== undefined) { |
|
491 callback.call(source, event, data.amsClickHandlerOptions); |
|
492 } |
|
493 } |
|
494 } |
|
495 }); |
|
496 |
|
497 // Initialize custom change handlers |
|
498 $(element).on('change', '[data-ams-change-handler]', function(event) { |
|
499 var source = $(this); |
|
500 // Disable change handlers for readonly inputs |
|
501 // These change handlers are activated by IE!!! |
|
502 if (source.prop('readonly')) { |
|
503 return; |
|
504 } |
|
505 var handlers = source.data('ams-disabled-handlers'); |
|
506 if ((handlers === true) || (handlers === 'change') || (handlers === 'all')) { |
|
507 return; |
|
508 } |
|
509 var data = source.data(); |
|
510 if (data.amsChangeHandler) { |
|
511 if ((data.amsStopPropagation === true) || (data.amsChangeStopPropagation === true)) { |
|
512 event.stopPropagation(); |
|
513 } |
|
514 if (data.amsChangeKeepDefault !== true) { |
|
515 event.preventDefault(); |
|
516 } |
|
517 var changeHandlers = data.amsChangeHandler.split(/\s+/); |
|
518 for (var index=0; index < changeHandlers.length; index++) { |
|
519 var callback = ams.getFunctionByName(changeHandlers[index]); |
|
520 if (callback !== undefined) { |
|
521 callback.call(source, event, data.amsChangeHandlerOptions); |
|
522 } |
|
523 } |
|
524 } |
|
525 }); |
|
526 |
|
527 // Notify reset to update Select2 widgets |
|
528 $(element).on('reset', 'form', function(e) { |
|
529 var form = $(this); |
|
530 setTimeout(function() { |
|
531 $('.alert-danger, SPAN.state-error', form).not('.persistent').remove(); |
|
532 $('LABEL.state-error', form).removeClass('state-error'); |
|
533 $('INPUT.select2[type="hidden"]', form).each(function() { |
|
534 var input = $(this); |
|
535 var select = input.data('select2'); |
|
536 var value = input.data('ams-select2-input-value'); |
|
537 if (value) { |
|
538 input.select2('val', value.split(select.opts.separator)); |
|
539 } |
|
540 }); |
|
541 form.find('.select2').trigger('change'); |
|
542 $('[data-ams-reset-callback]', form).each(function() { |
|
543 var element = $(this); |
|
544 var data = element.data(); |
|
545 var callback = ams.getFunctionByName(data.amsResetCallback); |
|
546 if (callback !== undefined) { |
|
547 callback.call(form, element, data.amsResetCallbackOptions); |
|
548 } |
|
549 }); |
|
550 }, 10); |
|
551 ams.form && ams.form.setFocus(form); |
|
552 }); |
|
553 |
|
554 // Initialize custom reset handlers |
|
555 $(element).on('reset', '[data-ams-reset-handler]', function(e) { |
|
556 var form = $(this); |
|
557 var data = form.data(); |
|
558 if (data.amsResetHandler) { |
|
559 if (data.amsResetKeepDefault !== true) { |
|
560 e.preventDefault(); |
|
561 } |
|
562 var callback = ams.getFunctionByName(data.amsResetHandler); |
|
563 if (callback !== undefined) { |
|
564 callback.call(form, data.amsResetHandlerOptions); |
|
565 } |
|
566 } |
|
567 }); |
|
568 |
|
569 // Initialize custom event on click |
|
570 $(element).on('click', '[data-ams-click-event]', function(e) { |
|
571 var source = $(this); |
|
572 $(e.target).trigger(source.data('ams-click-event'), |
|
573 source.data('ams-click-event-options')); |
|
574 }); |
|
575 |
|
576 // Cancel clicks on readonly checkbox |
|
577 $(element).on('click', 'input[type="checkbox"][readonly]', function() { |
|
578 return false; |
|
579 }); |
|
580 }; |
|
581 |
|
582 })(jQuery, this); |
|