1 /* |
1 /* |
2 * MyAMS |
2 * MyAMS |
3 * « My Application Management Skin » |
3 * « My Application Management Skin » |
4 * |
4 * |
5 * $Tag: 0.1.11 $ (rev. 1) |
5 * $Tag$ (rev. 1) |
6 * A bootstrap based application/administration skin |
6 * A bootstrap based application/administration skin |
7 * |
7 * |
8 * Custom administration and application skin tools |
8 * Custom administration and application skin tools |
9 * Released under Zope Public License ZPL 1.1 |
9 * Released under Zope Public License ZPL 1.1 |
10 * ©2014-2016 Thierry Florac <tflorac@ulthar.net> |
10 * ©2014-2016 Thierry Florac <tflorac@ulthar.net> |
88 }; |
88 }; |
89 |
89 |
90 |
90 |
91 /** |
91 /** |
92 * JQuery filter on parents class |
92 * JQuery filter on parents class |
|
93 * This filter is often combined with ":not()" to select DOM objects which don't have |
|
94 * parents of a given class. |
|
95 * For example: |
|
96 * |
|
97 * $('.hint:not(:parents(.nohints))', element); |
|
98 * |
|
99 * will select all elements with ".hint" class which don't have a parent with '.nohints' class. |
93 */ |
100 */ |
94 $.expr[':'].parents = function(obj, index, meta /*, stack*/) { |
101 $.expr[':'].parents = function(obj, index, meta /*, stack*/) { |
95 return $(obj).parents(meta[3]).length > 0; |
102 return $(obj).parents(meta[3]).length > 0; |
96 }; |
103 }; |
97 |
104 |
98 |
105 |
99 /** |
106 /** |
100 * JQuery 'scrollbarWidth' function |
107 * JQuery 'scrollbarWidth' function |
101 * Get width of vertical scrollbar |
108 * Get width of default vertical scrollbar |
102 */ |
109 */ |
103 if ($.scrollbarWidth === undefined) { |
110 if ($.scrollbarWidth === undefined) { |
104 $.scrollbarWidth = function() { |
111 $.scrollbarWidth = function() { |
105 var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body'); |
112 var parent = $('<div style="width: 50px; height: 50px; overflow: auto"><div/></div>').appendTo('body'); |
106 var child = parent.children(); |
113 var child = parent.children(); |
114 /** |
121 /** |
115 * MyAMS JQuery extensions |
122 * MyAMS JQuery extensions |
116 */ |
123 */ |
117 $.fn.extend({ |
124 $.fn.extend({ |
118 |
125 |
119 /* |
126 /** |
120 * Check if current object is empty or not |
127 * Check if current object is empty or not |
121 */ |
128 */ |
122 exists: function() { |
129 exists: function() { |
123 return $(this).length > 0; |
130 return $(this).length > 0; |
124 }, |
131 }, |
125 |
132 |
126 /* |
133 /** |
127 * Get object if it supports given CSS class, |
134 * Get object if it supports given CSS class, |
128 * otherwise looks for parents |
135 * otherwise look for parents |
129 */ |
136 */ |
130 objectOrParentWithClass: function(klass) { |
137 objectOrParentWithClass: function(klass) { |
131 if (this.hasClass(klass)) { |
138 if (this.hasClass(klass)) { |
132 return this; |
139 return this; |
133 } else { |
140 } else { |
134 return this.parents('.' + klass); |
141 return this.parents('.' + klass); |
135 } |
142 } |
136 }, |
143 }, |
137 |
144 |
138 /* |
145 /** |
139 * Build an array of attributes of the given selection |
146 * Build an array of attributes of the given selection |
140 */ |
147 */ |
141 listattr: function(attr) { |
148 listattr: function(attr) { |
142 var result = []; |
149 var result = []; |
143 this.each(function() { |
150 this.each(function() { |
144 result.push($(this).attr(attr)); |
151 result.push($(this).attr(attr)); |
145 }); |
152 }); |
146 return result; |
153 return result; |
147 }, |
154 }, |
148 |
155 |
149 /* |
156 /** |
150 * CSS style function |
157 * CSS style function |
151 * Code from Aram Kocharyan on stackoverflow.com |
158 * Code from Aram Kocharyan on stackoverflow.com |
152 */ |
159 */ |
153 style: function(styleName, value, priority) { |
160 style: function(styleName, value, priority) { |
154 // DOM node |
161 // DOM node |
174 // Get CSSStyleDeclaration |
181 // Get CSSStyleDeclaration |
175 return style; |
182 return style; |
176 } |
183 } |
177 }, |
184 }, |
178 |
185 |
179 /* |
186 /** |
180 * Remove CSS classes starting with a given prefix |
187 * Remove CSS classes starting with a given prefix |
181 */ |
188 */ |
182 removeClassPrefix: function (prefix) { |
189 removeClassPrefix: function (prefix) { |
183 this.each(function (i, it) { |
190 this.each(function (i, it) { |
184 var classes = it.className.split(" ").map(function(item) { |
191 var classes = it.className.split(" ").map(function(item) { |
574 } |
581 } |
575 return globals.document.body.contains(element[0]); |
582 return globals.document.body.contains(element[0]); |
576 }; |
583 }; |
577 |
584 |
578 /** |
585 /** |
579 * Get script or CSS file using browser cache |
586 * Get target URL matching given source |
580 * Script or CSS URLs can include variable names, given between braces, as in |
587 * |
581 * {MyAMS.baseURL} |
588 * Given URL can include variable names (with their namespace), given between braces, as in {MyAMS.baseURL} |
582 */ |
589 */ |
583 MyAMS.getSource = function(url) { |
590 MyAMS.getSource = function(url) { |
584 return url.replace(/{[^{}]*}/g, function(match) { |
591 return url.replace(/{[^{}]*}/g, function(match) { |
585 return ams.getFunctionByName(match.substr(1, match.length-2)); |
592 return ams.getFunctionByName(match.substr(1, match.length-2)); |
586 }); |
593 }); |
587 }; |
594 }; |
588 |
595 |
|
596 /** |
|
597 * Script loader function |
|
598 * |
|
599 * @param url: script URL |
|
600 * @param callback: a callback to be called after script loading |
|
601 * @param options: a set of options to be added to AJAX call |
|
602 */ |
589 MyAMS.getScript = function(url, callback, options) { |
603 MyAMS.getScript = function(url, callback, options) { |
590 if (typeof(callback) === 'object') { |
604 if (typeof(callback) === 'object') { |
591 options = callback; |
605 options = callback; |
592 callback = null; |
606 callback = null; |
593 } |
607 } |
604 }; |
618 }; |
605 var settings = $.extend({}, defaults, options); |
619 var settings = $.extend({}, defaults, options); |
606 return $.ajax(settings); |
620 return $.ajax(settings); |
607 }; |
621 }; |
608 |
622 |
|
623 /** |
|
624 * CSS file loader function |
|
625 * |
|
626 * @param url: CSS file URL |
|
627 * @param id: a unique ID given to CSS file |
|
628 */ |
609 MyAMS.getCSS = function(url, id) { |
629 MyAMS.getCSS = function(url, id) { |
610 var head = $('HEAD'); |
630 var head = $('HEAD'); |
611 var css = $('link[data-ams-id="' + id + '"]', head); |
631 var css = $('link[data-ams-id="' + id + '"]', head); |
612 if (css.length === 0) { |
632 if (css.length === 0) { |
613 var source = ams.getSource(url); |
633 var source = ams.getSource(url); |
648 /** |
671 /** |
649 * Browser testing functions; mostly for IE... |
672 * Browser testing functions; mostly for IE... |
650 */ |
673 */ |
651 MyAMS.browser = { |
674 MyAMS.browser = { |
652 |
675 |
|
676 /** |
|
677 * Get IE version |
|
678 */ |
653 getInternetExplorerVersion: function() { |
679 getInternetExplorerVersion: function() { |
654 var rv = -1; |
680 var rv = -1; |
655 if (navigator.appName === "Microsoft Internet Explorer") { |
681 if (navigator.appName === "Microsoft Internet Explorer") { |
656 var ua = navigator.userAgent; |
682 var ua = navigator.userAgent; |
657 var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})"); |
683 var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})"); |
689 } |
721 } |
690 return msg; |
722 return msg; |
691 }, |
723 }, |
692 |
724 |
693 |
725 |
|
726 /** |
|
727 * Copy selection to clipboard |
|
728 * |
|
729 * If 'text' argument is provided, given text is copied to clipboard. |
|
730 * Otherwise, text ou event's source is copied. |
|
731 * Several methods are tested to do clipboard copy (based on browser features); il copy can't be done, |
|
732 * a prompt is displayed to allow user to make a manual copy. |
|
733 */ |
694 copyToClipboard: function(text) { |
734 copyToClipboard: function(text) { |
695 |
735 |
696 function doCopy(text) { |
736 function doCopy(text) { |
697 var copied = false; |
737 var copied = false; |
698 if (window.clipboardData && window.clipboardData.setData) { |
738 if (window.clipboardData && window.clipboardData.setData) { |
720 { |
760 { |
721 title: text.length > 1 |
761 title: text.length > 1 |
722 ? ams.i18n.CLIPBOARD_TEXT_COPY_OK |
762 ? ams.i18n.CLIPBOARD_TEXT_COPY_OK |
723 : ams.i18n.CLIPBOARD_CHARACTER_COPY_OK, |
763 : ams.i18n.CLIPBOARD_CHARACTER_COPY_OK, |
724 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
764 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
725 timeout: 1000 |
765 timeout: 3000 |
726 }); |
766 }); |
727 } else if (globals.prompt) { |
767 } else if (globals.prompt) { |
728 globals.prompt(MyAMS.i18n.CLIPBOARD_COPY, text); |
768 globals.prompt(MyAMS.i18n.CLIPBOARD_COPY, text); |
729 } |
769 } |
730 } |
770 } |
810 MyAMS.ajax = { |
850 MyAMS.ajax = { |
811 |
851 |
812 /** |
852 /** |
813 * Check for given feature and download script if necessary |
853 * Check for given feature and download script if necessary |
814 * |
854 * |
815 * @checker: pointer to a javascript object which will be downloaded in undefined |
855 * @param checker: pointer to a javascript object which will be downloaded in undefined |
816 * @source: URL of a javascript file containing requested feature |
856 * @param source: URL of a javascript file containing requested feature |
817 * @callback: pointer to a function which will be called after the script is downloaded. The first |
857 * @param callback: pointer to a function which will be called after the script is downloaded. The first |
818 * argument of this callback is a boolean value indicating if the script was just downloaded (true) |
858 * argument of this callback is a boolean value indicating if the script was just downloaded (true) |
819 * or if the requested object was already loaded (false) |
859 * or if the requested object was already loaded (false) |
820 * @options: callback options |
860 * @param options: callback options |
821 */ |
861 */ |
822 check: function(checker, source, callback, options) { |
862 check: function(checker, source, callback, options) { |
823 |
863 |
824 function callCallbacks(firstLoad, options) { |
864 function callCallbacks(firstLoad, options) { |
825 if (callback === undefined) { |
865 if (callback === undefined) { |
1140 var message; |
1180 var message; |
1141 if (result.message) { |
1181 if (result.message) { |
1142 message = result.message; |
1182 message = result.message; |
1143 if (typeof(message) === 'string') { |
1183 if (typeof(message) === 'string') { |
1144 if ((status === 'info') || (status === 'success')) { |
1184 if ((status === 'info') || (status === 'success')) { |
1145 ams.skin.smallBox(status, |
1185 ams.skin.smallBox(status, { |
1146 { |
|
1147 title: message, |
1186 title: message, |
1148 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
1187 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
1149 timeout: 3000 |
1188 timeout: 3000 |
1150 }); |
1189 }); |
1151 } else { |
1190 } else { |
1158 message.body, |
1197 message.body, |
1159 message.subtitle); |
1198 message.subtitle); |
1160 } |
1199 } |
1161 } |
1200 } |
1162 if (result.smallbox) { |
1201 if (result.smallbox) { |
1163 ams.skin.smallBox(result.smallbox_status || status, |
1202 ams.skin.smallBox(result.smallbox_status || status, { |
1164 {title: result.smallbox, |
1203 title: result.smallbox, |
1165 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
1204 icon: 'fa fa-fw fa-info-circle font-xs align-top margin-top-10', |
1166 timeout: 3000}); |
1205 timeout: 3000 |
|
1206 }); |
1167 } |
1207 } |
1168 if (result.messagebox) { |
1208 if (result.messagebox) { |
1169 message = result.messagebox; |
1209 message = result.messagebox; |
1170 if (typeof(message) === 'string') { |
1210 if (typeof(message) === 'string') { |
1171 ams.skin.messageBox('info', |
1211 ams.skin.messageBox('info', { |
1172 { |
|
1173 title: ams.i18n.ERROR_OCCURED, |
1212 title: ams.i18n.ERROR_OCCURED, |
1174 content: message, |
1213 content: message, |
1175 timeout: 10000 |
1214 timeout: 10000 |
1176 }); |
1215 }); |
1177 } else { |
1216 } else { |
1178 var messageStatus = message.status || 'info'; |
1217 var messageStatus = message.status || 'info'; |
1179 if (messageStatus === 'error' && form && target) { |
1218 if (messageStatus === 'error' && form && target) { |
1180 ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target); |
1219 ams.executeFunctionByName(form.data('ams-form-submit-error') || 'MyAMS.form.finalizeSubmitOnError', form, target); |
1181 } |
1220 } |
1182 ams.skin.messageBox(messageStatus, |
1221 ams.skin.messageBox(messageStatus, { |
1183 {title: message.title || ams.i18n.ERROR_OCCURED, |
1222 title: message.title || ams.i18n.ERROR_OCCURED, |
1184 content: message.content, |
1223 content: message.content, |
1185 icon: message.icon, |
1224 icon: message.icon, |
1186 number: message.number, |
1225 number: message.number, |
1187 timeout: message.timeout === null ? undefined : (message.timeout || 10000)}); |
1226 timeout: message.timeout === null ? undefined : (message.timeout || 10000) |
|
1227 }); |
1188 } |
1228 } |
1189 } |
1229 } |
1190 if (result.event) { |
1230 if (result.event) { |
1191 form.trigger(result.event, result.event_options); |
1231 form.trigger(result.event, result.event_options); |
1192 } |
1232 } |
2120 var widget = $('[name="' + widgetData.name + '"]', form); |
2160 var widget = $('[name="' + widgetData.name + '"]', form); |
2121 if (!widget.exists()) { |
2161 if (!widget.exists()) { |
2122 widget = $('[name="' + widgetData.name + ':list"]', form); |
2162 widget = $('[name="' + widgetData.name + ':list"]', form); |
2123 } |
2163 } |
2124 if (widget.exists()) { |
2164 if (widget.exists()) { |
|
2165 // Update widget state |
2125 widget.parents('label:first') |
2166 widget.parents('label:first') |
2126 .removeClassPrefix('state-') |
2167 .removeClassPrefix('state-') |
2127 .addClass('state-error') |
2168 .addClass('state-error') |
2128 .after('<span for="name" class="state-error">' + widgetData.message + '</span>'); |
2169 .after('<span for="name" class="state-error">' + widgetData.message + '</span>'); |
2129 } |
2170 } else { |
2130 // complete form alert message |
2171 // complete form alert message |
2131 if (widgetData.label) { |
2172 if (widgetData.label) { |
2132 message.push(widgetData.label + ' : ' + widgetData.message); |
2173 message.push(widgetData.label + ' : ' + widgetData.message); |
|
2174 } |
2133 } |
2175 } |
2134 // mark parent tab (if any) with error status |
2176 // mark parent tab (if any) with error status |
2135 var tabIndex = widget.parents('.tab-pane').index() + 1; |
2177 var tabIndex = widget.parents('.tab-pane').index() + 1; |
2136 if (tabIndex > 0) { |
2178 if (tabIndex > 0) { |
2137 var navTabs = $('.nav-tabs', $(widget).parents('.tabforms')); |
2179 var navTabs = $('.nav-tabs', $(widget).parents('.tabforms')); |
2593 }, |
2635 }, |
2594 |
2636 |
2595 /** Datetimepicker dialog cleaner callback */ |
2637 /** Datetimepicker dialog cleaner callback */ |
2596 datetimepickerDialogHiddenCallback: function() { |
2638 datetimepickerDialogHiddenCallback: function() { |
2597 $('.datepicker, .timepicker, .datetimepicker', this).datetimepicker('destroy'); |
2639 $('.datepicker, .timepicker, .datetimepicker', this).datetimepicker('destroy'); |
|
2640 }, |
|
2641 |
|
2642 /** Set SEO status */ |
|
2643 setSEOStatus: function() { |
|
2644 var input = $(this); |
|
2645 var progress = input.siblings('.progress').children('.progress-bar'); |
|
2646 var length = Math.min(input.val().length, 100); |
|
2647 var status = 'success'; |
|
2648 if (length < 20 || length > 80) { |
|
2649 status = 'danger'; |
|
2650 } else if (length < 40 || length > 66) { |
|
2651 status = 'warning'; |
|
2652 } |
|
2653 progress.removeClassPrefix('progress-bar') |
|
2654 .addClass('progress-bar') |
|
2655 .addClass('progress-bar-' + status) |
|
2656 .css('width', length + '%'); |
2598 } |
2657 } |
2599 }; |
2658 }; |
2600 |
2659 |
2601 |
2660 |
2602 /** |
2661 /** |
5811 if (button.exists() && button.parent().hasClass('input-file')) { |
5870 if (button.exists() && button.parent().hasClass('input-file')) { |
5812 button.next('input[type="text"]').val(input.val()); |
5871 button.next('input[type="text"]').val(input.val()); |
5813 } |
5872 } |
5814 }); |
5873 }); |
5815 |
5874 |
|
5875 // Always blur readonly inputs |
|
5876 $(document).on('focus', 'input[readonly="readonly"]', function() { |
|
5877 $(this).blur(); |
|
5878 }); |
|
5879 |
5816 // Prevent bootstrap dialog from blocking TinyMCE focus |
5880 // Prevent bootstrap dialog from blocking TinyMCE focus |
5817 $(document).on('focusin', function(e) { |
5881 $(document).on('focusin', function(e) { |
5818 if ($(e.target).closest('.mce-window').length) { |
5882 if ($(e.target).closest('.mce-window').length) { |
5819 e.stopImmediatePropagation(); |
5883 e.stopImmediatePropagation(); |
5820 } |
5884 } |
5821 }); |
5885 }); |
5822 |
5886 |
5823 // Disable clicks on disabled tabs |
5887 // Disable clicks on disabled tabs |
5824 $("a[data-toggle=tab]", ".nav-tabs").on("click", function(e) { |
5888 $(document).on("click", '.nav-tabs a[data-toggle=tab]', function(e) { |
5825 if ($(this).parent('li').hasClass("disabled")) { |
5889 if ($(this).parent('li').hasClass("disabled")) { |
5826 e.preventDefault(); |
5890 e.preventDefault(); |
5827 return false; |
5891 return false; |
5828 } |
5892 } |
|
5893 }); |
|
5894 |
|
5895 // Automatically set orientation of dropdown menus |
|
5896 $(document).on('show.bs.dropdown', '.btn-group', function() { |
|
5897 var menu = $(this); |
|
5898 var ul = menu.children('.dropdown-menu'); |
|
5899 var menuRect = menu.get(0).getBoundingClientRect(); |
|
5900 var position = menuRect.top; |
|
5901 var buttonHeight = menuRect.height; |
|
5902 var menuHeight = ul.outerHeight(); |
|
5903 if (position > menuHeight && $(window).height() - position < buttonHeight + menuHeight) { |
|
5904 menu.addClass("dropup"); |
|
5905 } |
|
5906 }).on('hidden.bs.dropdown', '.btn-group', function() { |
|
5907 // always reset after close |
|
5908 $(this).removeClass('dropup'); |
5829 }); |
5909 }); |
5830 |
5910 |
5831 // Enable tabs dynamic loading |
5911 // Enable tabs dynamic loading |
5832 $(document).on('show.bs.tab', function(e) { |
5912 $(document).on('show.bs.tab', function(e) { |
5833 var link = $(e.target); |
5913 var link = $(e.target); |