PageSourceSearch

https://deval-traiteur.com/admin/inc/js/jdigi.js

js deval-traiteur.com collected 2026-10-04 18:56:34 UTC 118,188 bytes, 3,941 lines download raw bytes

1/* Mini frameWork Js pour Digifactory
2*	quelques raccourcis pratique et fonctions basiques.
3*/
4
5
6var _ = {
7	cfg: {
8		currency:'€',
9		currencyCode:'EUR',
10		workspace:'backoffice',
11		allowFullPageUpload: true,
12	},
13	tmpDatas:{},
14	cache:{
15		lists: {
16			loaders: {},
17			values: {}
18		},
19		indexed: {},
20		uids: {},
21	},
22	flags:{},
23	Datas:{glob:{}}, // Tableau global
24	Tpls:{},
25	quickHelps:{main:[]},
26	dataTpls:{glob:{}},
27	linkTpls:{glob:{}},
28	Users: {0:{nr:0, pic:'/custom/img/user/user_male.png', n:'Non-assigné'}},
29	pageX:0,
30	pageY:0,
31	windowsFocus:true,
32	urlParams: new URLSearchParams(window.location.search),
33	loadedScript: [],
34	charterColors: null,
35	styleSets: null,
36	styleSetsDetailed: null,
37	entities: null,
38	listIndex: [],
39	rootUrl: location.href,
40	dragging:false,
41	dragToMove:false,
42	dragService:null,
43	pageModules:{},
44	delayHandlers:{},
45	momentFormats: ["DD-MM-YYYY HH:mm", "YYYY-MM-DD HH:mm", "DD-MM-YYYY", "YYYY-MM-DD"],
46
47	start: function(config, workspace, toload) {
48		if (typeof digiSuiteVars != "undefined")
49			{
50			 _.cfg = $.extend(_.cfg, digiSuiteVars);
51			}
52
53		if (workspace) _.cfg.workspace = workspace;
54		if (_.cfg.ext) _.cfg.workspace = 'extranet';
55
56		if (!toload) toload = '';
57		switch (_.cfg.workspace)
58			{
59			 case 'frontoffice': toload+= ''; break;
60			 case 'extranet': toload+= ',notif,websocket,upload,chat'; break;
61			 case 'backoffice':
62			 default: toload+= 'kb,cp,tpl,pm,review,help,notif,websocket,upload,chat,alert,header,itemSelector'; break;
63			}
64
65		_.newSession = config.isNew;
66
67		_.digi = new digiRequest({urladmin:_.cfg.urladmin});
68		_.error = new ErrorService({urladmin:_.cfg.urladmin});
69
70		_.startListening();
71
72		var services = toload.split(',');
73		if (_.cfg.workspace!='frontoffice')
74			{
75			 services.push('components');
76			}
77		_.loadServices(services, config);
78
79		_.checkUrlApp();
80
81		_.bringSectionToLife();
82
83		if (_.cfg.workspace=='backoffice') _.startBackOffice();
84	},
85
86	startBackOffice: function() {
87		_.$appBody = $("body");
88		_.$appHead = $("body > header");
89		var lastScrollY = 0;
90		_.MenuOn = true;
91		$('[name="t"]', _.$appHead).change(()=>$('[name="s"]', _.$appHead).focus());
92		$('[name="s"]', _.$appHead).on('blur', ()=>_.keysCtrlF=false);
93		if (_.intval(_.get('nbntor')))
94			{
95			 $('.main-ariane > .home', _.$appHead).append('<span class="toRead small top-corner" title="'+_.get('nbntor')+' news à lire"></span>');
96			}
97
98
99		_.kb.addRules(document, {keys: {
100			'alt+a': function(ev) { ev.preventDefault(); $(".digiAlertBell").click(); },
101			'alt+t': function(ev) { $(".timeInput", _.$appHead).click(); },
102			'alt+f': function(ev) { ev.preventDefault(); $("nav.leftMenu").click(); },
103			'ctrl+f': function(ev) {
104				if (!_.keysCtrlF && !_.keysCtrlFother)
105					{
106					 ev.stopPropagation();
107					 ev.preventDefault();
108
109					 $(".df-search input[name='s']", _.$appHead).focus();
110					 _.$appHead.addClass('on forced-on');
111					 _.$appHead.css('top', 0);
112					 _.MenuOn = true;
113					 _.keysCtrlF = true;
114					}
115					else _.keysCtrlF = false;
116			},
117		}});
118
119
120		setTimeout(function(){
121			_.uptQuickHelp('main', [
122				{kb:'Ctrl + F', name:'Recherche', title:'principale ou TimeLine', detail:'un 2ème Ctrl+F ouvre la recherche du navigateur'},
123				{kb:'Alt + F', name:'Menu', title:'ouvre le menu latéral de gauche', detail:'permet également de refermer le menu'},
124				{kb:'Alt + A', name:'Alertes', title:'afficher le panel de droite', detail:'avec les notifications'},
125				{kb:'Alt + T', name:'Temps', title:'ouvre la saisie rapide de temps'},
126			]);
127		}, 100);
128
129		_.$appHead.on('click', (ev)=>{
130			if (!_.MenuOn)
131				{
132				 _.$appHead.addClass('on forced-on').css('top', 0);
133				 _.MenuOn = true;
134				}
135				else
136				{
137				 if ($(ev.target).hasClass('df-search'))
138				 	{
139					 $(".df-search input[name='s']", _.$appHead).focus();
140					}
141				}
142		});
143
144
145		$(window).scroll(function() {
146			if (!_.MenuLocked)
147				{
148				 if (!$('[name="s"]', _.$appHead).is(':focus'))
149				 	{
150					 var scroll = $(document).scrollTop();
151					 if (scroll<lastScrollY && scroll<500 && _.$appHead.hasClass('forced-on'))
152					 	{
153						 _.$appHead.css('top', 0);
154						 _.MenuOn = true;
155						}
156					 	else
157					 	{
158						 _.$appHead.removeClass('forced-on');
159						 _.$appHead.css('top', Math.max(-scroll, -45));
160						 if (scroll>=50)
161							{
162							 if (_.MenuOn)
163								{
164								 _.MenuOn = false;
165								 _.$appBody.addClass('scrolling');
166								 _.$appHead.removeClass('on');
167								}
168							}
169							else if (scroll<50)
170							{
171							 if (!_.MenuOn)
172								{
173								 _.MenuOn = true;
174								 _.$appBody.removeClass('scrolling');
175								 _.$appHead.addClass('on');
176								}
177							}
178						}
179					 lastScrollY = scroll;
180					}
181				}
182		});
183
184		$('.SidePanel .panel-toggle').click(function() {
185			_.panel.toggle();
186		});
187	},
188
189
190	// A Faire: analyser chaque élément et voir si peut être amélioré - déplacé - transformé en module - ou autre modif intéressante
191	startListening: function() {
192
193		if ($.fn.jAction)
194			{
195			 $(document).on("click", ".jaction,[jsinfos]", function(ev){
196				if (!this.inProcess)
197					{
198					 ev.stopPropagation();
199					 $('.date-Picker').each(function() {if (this.close) this.close();});
200					 $(this).jAction({fromClick:true, event:ev});
201					 $(this).filter(':button').blur();
202					}
203			 });
204
205			 $(document).on("dblclick", ".jactiondbl", function(ev){
206				if (!this.inProcess)
207					{
208					 ev.stopPropagation();
209					 $(this).jAction({fromClick:true, event:ev});
210					 $(this).filter(':button').blur();
211					}
212			 });
213
214			 $(document).on("change", ".jchange,[jschange]", function(ev){
215				if (!this.inProcess)
216					{
217					 ev.stopPropagation();
218					 $(this).jAction({fromChange:true, event:ev});
219					}
220			 });
221
222			 $(document).on("submit", "form:not([action])", function(ev) {
223				ev.preventDefault();
224			 });
225
226			 $(document).on('keypress', ".enter :input", function(ev) {
227				var tn = ev.target.tagName.toLowerCase();
228				if (ev.keyCode == 13 && (tn!="textarea" || (tn=="textarea" && ev.ctrlKey)) ) // Enter
229					{
230					 var panel = $(this).closest('.enter');
231					 var btn = $(".btn.jaction.valid, .btn.valid[jsinfos]", panel);
232					 if (btn.length>0)
233						{
234						 $(btn[0]).jAction();
235						}
236					}
237			 });
238
239			 $(".jActionInit").each(function(){
240				$(this).removeClass('jActionInit'); // pour ne pas répéter l'initialisation en cas d'un second appel
241				$(this).jAction();
242			 });
243
244			 $(window).on("popstate", function(ev) {
245				var state = ev.originalEvent.state;
246				if (state)
247					{
248					 if (state.page || state.lSort || state.filter || state.nbbypage)
249						{
250						 if (_.listIndex[+state.list])
251							{
252							 _.listIndex[+state.list].popState(state);
253							}
254						}
255						else if (state.url)
256						{
257						 var actions = [{url:state.url, state:true}];
258						 if (state.select) actions[0].select = state.select;
259						 $(this).jAction({actions:actions});
260						}
261					}
262			 });
263			}
264
265		$(document).on('change', ':input.calcul', (ev)=>{
266			_.inputCalcul(ev.currentTarget);
267		});
268
269
270		if (_.cfg.workspace=='backoffice')
271			{
272			 var prevZone = _.ce('i', 'i-caret-up', {title:'monter'});
273			 var nextZone = _.ce('i', 'i-caret-down', {title:'descendre'});
274			 $('.sideIco.nav').append(prevZone, nextZone);
275
276			 function getOrderedTarget(reverse=false) {
277			 	var $targets = $('body, nav.tabs, .basket-compose');
278				var orderedTargets = [];
279				$targets.each((i, target)=>{
280					orderedTargets.push({top:$(target).offset().top, target:target});
281				});
282				var way = reverse? -1 : 1;
283				orderedTargets.sort((a, b)=>{
284					if (a.top==b.top) return 0;
285					return a.top<b.top? -1*way : 1*way;
286				});
287				return orderedTargets;
288			 }
289
290
291			 $(prevZone).on('click', ()=>{
292			 	var wscroll = $(window).scrollTop(),
293					orderedTargets = getOrderedTarget(true);
294				for (let item of orderedTargets)
295					{
296					 if (item.top < wscroll - 40)
297					 	{
298						 $('body,html').animate({scrollTop:item.top}, 300); break;
299						}
300					}
301			 });
302
303			 $(nextZone).on('click', ()=>{
304			 	var wscroll = $(window).scrollTop(),
305					orderedTargets = getOrderedTarget();
306				for (let item of orderedTargets)
307					{
308					 if (item.top>wscroll + 40)
309					 	{
310						 $('body,html').animate({scrollTop:item.top}, 300); break;
311						}
312					}
313			 });
314
315
316			 var $mainNav = $("nav.leftMenu");
317
318			 $(".addFavorite").click(function() {
319				var url = document.location.href;
320				if (url.indexOf('?')==-1) url+= "?";
321				url+="&addBkmk=1";
322				document.location.href = url;
323			 });
324
325			 $(".bookmark select").change(function() {
326				document.location.href = $(this).val();
327			 });
328
329			 $("header .burger").click(function() {
330			 	_.isMobileSize = true;
331				if ($mainNav.hasClass('mobile'))
332					{
333					 $mainNav.hide();
334					 $mainNav.removeClass('mobile');
335					 $mainNav[0].close();
336					}
337					else
338					{
339					 $mainNav.show();
340					 $mainNav.addClass('mobile');
341					 $mainNav.click();
342					}
343			 });
344			 $(".df-search input[name='s']").keypress(function(ev) {
345				if (ev.keyCode == 27) // Esc
346					{
347					 ev.preventDefault();
348					 $(this).val("");
349					}
350				if (ev.keyCode == 13) // enter
351					{
352					 ev.preventDefault();
353					 $(this).jAction({actions:[{},{route:'app/search//s'}]});
354					}
355			 });
356
357			 $(".df-search .i-search", _.$appHead).click(function(){
358			 	$(this).jAction({actions:[{},{route:'app/search//s'}]});
359			 });
360
361			 $(document).on('click', ".collapse .collapse-trigger", function() {
362			 	var $section = $(this).closest('.collapse');
363				if ($section.hasClass('closed'))
364					{
365					 $section.removeClass('closed');
366					 $section.find(">div,>article").slideDown(200);
367					}
368					else
369					{
370					 $section.addClass('closed');
371					 $section.find(">div,>article").slideUp(200);
372					}
373			 });
374
375			 $(".timeInput", _.$appHead).dblclick(function() {
376				document.location.href = digiSuiteVars.urladmin+'/pjt/task/times';
377			 }).click(function() {
378				if ($(".timeInputTool").hasClass('open'))
379					{
380					 $(".timeInputTool").removeClass('open');
381					}
382					else
383					{
384					 _.actions({
385						route: 'pjt/task//get-last-time',
386						then: function(data) {
387							$(".timeInputTool [name='startT']").val(data.t);
388							if (data.prev)
389								{
390								 $(".timeInputTool [name='startT']").on('keyup', function(ev) {
391									if (ev.which == 37)
392										{
393										 if (!$(this).val())
394											{
395											 $(this).val(data.prev);
396											}
397										}
398								 });
399								}
400						}
401					 });
402
403					 $(".timeInputTool [name='endT']").val(moment().format('HH:mm'));
404					 $(".timeInputTool [name='comment']").val("");
405					 $(".timeInputTool").show().addClass('open');
406					 $(".timeInputTool input.search").focus();
407					}
408			 });
409
410			 $(".taskBug").on('click', function() {
411				if ($(this).hasClass('inv'))
412					{
413					 $(this).removeClass('inv');
414					 $(this).siblings('input[name="bug"]').val(0);
415					}
416					else
417					{
418					 $(this).addClass('inv');
419					 $(this).siblings('input[name="bug"]').val(1);
420					}
421			 });
422			 $(document).on({
423				mouseenter: function (ev) {
424					if (_.Help)
425						{
426							if ($(ev.target).closest('.suggDropList').length==0)
427								{
428								 var code = $(this).attr('help');
429								 clearTimeout(_.helpOnTimer);
430								 _.helpTargetCode = code;
431								 _.helpTimer = setTimeout(function() {
432									if (_.helpTargetCode && _.helpTargetCode==code)
433										{
434										 _.Help.init(code);
435										}
436								 }, 2000);
437								}
438						}
439				},
440				mouseleave: function (ev) {
441					if (_.Help)
442						{
443							var code = $(this).attr('help');
444							_.helpTargetCode = false;
445							clearTimeout(_.helpTimer);
446							_.helpOnTimer = setTimeout(function() {
447								if (_.helpOnTimer && _.Help.code && _.Help.code==code)
448									{
449									 _.Help.close();
450									}
451							}, 2500);
452						}
453				}
454			 }, '[help]');
455			 $(document).on('mousemove', (ev)=>{
456			 	if ($(ev.target).closest('.suggDropList').length>0)
457					{
458					 if (_.helpOnTimer)
459						{
460						 _.Help.close();
461						}
462					 if (_.tooltipTarget && _.tooltipTarget.closeToolTip) _.tooltipTarget.closeToolTip();
463					}
464			 });
465			//  $(document).on('mouseenter', (ev)=>{
466			// 		if (_.Help && _.helpTargetCode )
467			// 			{
468			// 			 if (_.helpTimer) clearTimeout(_.helpTimer);
469			// 			 _.Help.close();
470			// 			}
471			//  }, '.suggDropList');
472			 $(document).on('scroll', function() {
473				 $('.date-Picker').each(function() {if (this.close) this.close();});
474			 });
475
476			 $(window).focus(function() {
477				_.windowsFocus = true;
478			 });
479			 $(window).blur(function() {
480				_.windowsFocus = false;
481			 });
482
483			 $(document).on('click', '.toggle-next', (ev)=>{
484				var $next = $(ev.currentTarget).next('section.group');
485				if ($(ev.currentTarget).hasClass('closed'))
486					{
487					 $next.slideDown('fast',  ()=>{
488					 	$next.css('overflow', 'visible');
489					 });
490					 $(ev.currentTarget).removeClass('closed');
491					 $('i[class^="i-caret"]', ev.currentTarget).removeClass('i-caret-right').addClass('i-caret-down');
492					}
493					else
494					{
495					 $next.slideUp('fast');
496					 $(ev.currentTarget).addClass('closed');
497					 $('i[class^="i-caret"]', ev.currentTarget).removeClass('i-caret-down').addClass('i-caret-right');
498					}
499
500			 });
501
502			 if (("standalone" in window.navigator) && window.navigator.standalone)
503				{
504				 // For iOS Apps
505				 $(document).on('click', 'a', function(e) {
506
507					e.preventDefault();
508
509					var url = $(this).attr('href');
510					if (url != undefined && url.substr(0, 1) != '#' && url.substr(0, 4) != 'http' && $(this).attr('data-method') == undefined)
511						{
512						 window.location = url;
513						}
514				 });
515				}
516			}
517	},
518
519	uid: function () {
520		let uid = null;
521
522		while (uid === null || uid in this.cache.uids) {
523			uid = (new Date().getTime())+'.'+Math.random().toString(16).slice(2);
524		}
525
526		this.cache.uids[uid] = true;
527
528		return uid;
529	},
530
531	loadServices: function(services, config) {
532		if (typeof services == 'string') services = [services];
533
534		$.each(services, function(i, v) {services[v] = true;});
535
536		if (services.header && !_.PageHeader)	_.PageHeader = new PageHeader();
537		if (services.kb && !_.kb)				_.kb = new KBoard();
538		if (services.cp && !_.cp)				_.cp = new CopyPaste();
539		if (services.tpl && !_.tpl)				_.tpl = new TemplateService();
540		if (services.review && !_.Reviews)		_.Reviews = jReviews(config.reviews, config.isNew? true: false);
541		if (services.help && !_.Help)			_.Help = jHelp({});
542		if (services.websocket && !_.websocket) _.websocket = new WebSocketService(_.cfg.websocketAuthToken, _.cfg.websocketReconnectionInterval);
543		if (services.components && !_.components) _.components = new ComponentService();
544		if (services.notif && !_.Notifs)		_.Notifs = new jNotifs(config.nbNotifs);
545		if (services.upload && !_.Upload)		_.Upload = new digiUpload();
546		if (services.chat && !_.chat)			_.chat = new ChatService();
547		if (services.alert && !_.Alerts)		_.Alerts = new digiAlert({list:config.alerts});
548		if (services.pm && !_.pm)
549			{
550			 import('./mod/ui/ui.js').then((list)=>{
551			 	_.pm = new list['PanelManager']();
552			 });
553			}
554		if (services.itemSelector && !_.itemSelector) _.itemSelector = new itemSelector();
555	},
556
557	bringSectionToLife: function(section, isBloc) {
558		if (!section) section = document;
559
560		if (typeof _ == 'object' && 'components' in _) _.components.check(section);
561
562		if ($.fn.module)
563			{
564			 if (isBloc) $("[module]", section).removeClass('blocTpl').module();
565			 else $("[module]", section).not('.blocTpl').module();
566			}
567
568		$('.to-hide').hide().removeClass('to-hide');
569
570		$('i.log-infos').not('.done').addClass('done').click(function(ev) {
571		 	ev.stopPropagation();
572			var ct = $(this).next('span.JSinfos').html();
573			$(this).jToolTip({cls:'log-infos', title:ct});
574		});
575
576
577		if (isBloc) $(".checkbox", section).removeClass('blocTpl').checkboxField();
578		else $(".checkbox", section).not(".blocTpl").checkboxField();
579
580		$("panel", section).Panel();
581		$(".radio-grp", section).radioGrp();
582		$(".radio-grp-bis", section).radioGrpBis();
583		$(".tags-input", section).tagsInput();
584		$(".multi-select", section).multiSelectField();
585		$("select.dropdown", section).dropDownSelect();
586		$(".multiValueSelect", section).multiValSelect();
587		$(".jUsers", section).userSelector();
588		if (typeof $.fn.uploadHtml5 !== "undefined") $(".appUpload", section).uploadHtml5();
589		if (typeof $.fn.jGED!=='undefined') $(".GedModule", section).jGED();
590		if (typeof $.fn.jGallery!=='undefined') $(".jGallery", section).jGallery();
591		if (typeof $.fn.foldBloc!=='undefined') $(".jFoldBloc", section).foldBloc();
592		if (typeof $.fn.dragNDrop!=='undefined') $(".dragging", section).dragNDrop();
593		if (typeof $.fn.appList!=='undefined') $(".appList", section).appList();
594		if (typeof $.fn.jMoreLess!=='undefined') $(".more-less", section).jMoreLess();
595		if (typeof $.fn.datePicker!=='undefined')
596			{
597			 $(".datepicker", section).datePicker();
598			 $("div.datepicker > input", section).each(function() {$(this).datePicker();});
599			}
600		if (typeof $.fn.jColorPicker!=='undefined') $(".jColorPicker", section).jColorPicker();
601		$(".quantityModifier", section).qttyModifier();
602		if (typeof $.fn.stateFlow!=='undefined') $(".stateFlow", section).stateFlow();
603		if (typeof $.fn.projectFlow!=='undefined') $(".projectFlow", section).projectFlow();
604		if (typeof $.fn.jProgressBar!=='undefined') $(".jProgressBar", section).jProgressBar();
605		if (typeof $.fn.jEmailSplit!=='undefined') $(".jEmailSplit", section).jEmailSplit();
606		if (typeof $.fn.jTxtLength!=='undefined') $(".jTxtLength", section).jTxtLength();
607		if (typeof $.fn.jToolBar!=='undefined') $(".jToolBar", section).jToolBar();
608
609
610		$('.jsonEditor', section).each(function(i, editorBloc) {
611			$(editorBloc).removeClass('jsonEditor').addClass('json-editor');
612			var editorArea = $('textarea', editorBloc)[0];
613			var container = _.ce('div');
614			$(editorArea).hide().addClass('json-editor-area').after(container);
615
616			var job = new SyncJob(function () {
617				// create the editor
618				var code = editorArea.jsonCode? editorArea.jsonCode : $(editorArea).val();
619
620				const options = {
621					mode: 'code',
622					mainMenuBar:false,
623					navigationBar:false,
624					statusBar:false,
625				};
626				editorArea.editor = new JSONEditor(container, options);
627				if (typeof code=="object" || Array.isArray(code))
628					{
629					 editorArea.editor.set(code);
630					}
631					else editorArea.editor.setText(code);
632			});
633			job.addRequiredJob(new ImportScriptJob('/admin/inc/js/jsoneditor/jsoneditor.min.js'));
634			job.addRequiredJob(new ImportStylesheetJob('/admin/inc/js/jsoneditor/jsoneditor.min.css'));
635			job.run();
636		});
637
638		$(".jFilterList", section).jFilterList();
639		if (typeof $.fn.contentPackValues!=='undefined')
640			{
641			 $(".contentView article", section).has('pack').contentPackValues();
642			}
643
644		_.bringJActionInitToLife(section);
645
646		$("fieldset [autofocus]", section).focus();
647
648		_.appModules().then((appModules) => appModules.autoload(section));
649	},
650
651	bringJActionInitToLife: function(section) {
652		if (!section) section = document;
653		$(".jActionInit", section).each(function(){
654			$(this).removeClass('jActionInit'); // pour ne pas répéter l'initialisation en cas d'un second appel
655
656			$(this).jAction();
657		});
658	},
659
660	doubleSessionCatch: function() {
661		$.modalPanel({
662			code:"doubleSession",
663			content:"<nav><h4>Double session</h4></nav><div class='close jaction'><span class='JSinfos'>{act:'close'}</span></div><section><article>Une deuxième session avec cet utilisateur est active. <br>Veuillez fermer la session en trop. <nav class='actions'><br><br><a class='btn danger' href='"+digiSuiteVars.urladmin+"/?quit=1'>Quittez et reconnectez vous</a></nav></article></section>",
664			cssClass:"modal full",
665			callback:function() {
666				setTimeout(function() {$("#doubleSession").hide();}, 4000);
667			}
668		});
669	},
670
671	addQuickHelp: function(type, helps) {
672		this.quickHelps[type] = (this.quickHelps[type] || []).concat(helps);
673		this.displayQuickHelp();
674	},
675	uptQuickHelp: function(type, helps) {
676		this.quickHelps[type] = helps;
677		this.displayQuickHelp();
678	},
679
680	displayQuickHelp: function() {
681		var $QuickHelps = $('.main-quick-helps');
682		if (!$QuickHelps.length)
683			{
684			 $QuickHelps = $('<ul class="main-quick-helps"/>');
685			 if ($('.status-bar').length) $('.status-bar > .quick').append($QuickHelps);
686			 else $('body > .main-container > section:not(.PanelSection):not(.modal), body > .main-container > section > .CentralPanel').first().append($QuickHelps);
687			}
688		$('>*', $QuickHelps).remove();
689		$.each(_.quickHelps, function(type, helps) {
690			$.each(helps, function(i, help) {
691				var li = _.ce('li', 'short-help');
692				$(li).html('<span class="short"></span><span class="name">'+help.name+'<span>');
693				if (help.ico) $('.short', li).append('<i class="i-'+help.ico+'"></i>');
694				if (help.ico && help.kb) $('.short', li).append(' + ');
695				if (help.kb) $('.short', li).append(help.kb);
696				if (help.title) $(li).attr('title', help.title);
697				if (help.detail) $(li).attr('detail', help.detail);
698				$QuickHelps.append(li);
699			});
700
701		});
702	},
703	hideQuickHelp: function() {
704
705	},
706	delay: function (id, callback, timeout) {
707		if (id in _.delayHandlers) {
708			clearTimeout(_.delayHandlers[id]);
709			delete _.delayHandlers[id];
710		}
711		_.delayHandlers[id] = setTimeout(callback, timeout || 500);
712	},
713
714	test: function(cond, data) {
715		if (Array.isArray(cond))
716			{
717			 for (let subCond of cond)
718			 	{
719				 if (subCond.cond)
720				 	{
721				 	 if (_.test(subCond.cond, data))
722					 	{
723						 return subCond;
724						}
725					}
726					else return subCond;
727				}
728			}
729
730		if (typeof cond == "undefined" || cond==="" || cond===null) return true;
731		else if (cond===false) return false;
732
733		var _if = analyserCondition(cond);
734		if (_if===false) return false;
735		return calculCondition(_if, data);
736	},
737
738	makeData: function(code, data) {
739		var parts = code.split('.');
740		var res = {};
741		for (var i=parts.length-1; i>=0; --i)
742			{
743			 if (i==parts.length-1) res = {[parts[i]]:data};
744			 else res = {[parts[i]]: res};
745			}
746		return res;
747	},
748
749	setData: function(code, value, data) {
750		var parts = code.split('.');
751		var target = data;
752		for (var i=0; i < parts.length-1; i++)
753			{
754			 if (target[parts[i]] == null) target[parts[i]] = {};
755			 target = target[parts[i]];
756			}
757		target[parts[parts.length-1]] = value;
758	},
759
760	readData: function(code, data) {
761		if (code.indexOf('.')>0)
762			{
763			 let originCode = code;
764			 var [first, ...code] = code.split('.');
765			 code = code.join('.');
766			 if (!data[first])
767			 	{
768				 if (originCode in data) return data[originCode];
769				 return null;
770			 	}
771			 return _.readData(code, data[first]);
772			}
773			else
774			{
775			 return data[code];
776			}
777	},
778
779	deleteData: function(code, data) {
780		var parts = code.split('.');
781		var target = data;
782		for (var i = 0; i < parts.length - 1; i++)
783			{
784			 if (!target[parts[i]]) return;
785			 target = target[parts[i]];
786			}
787		delete target[parts[parts.length - 1]];
788	},
789
790	extendModule: function(module, config, datas, tree) {
791		module.initStarted = true;
792		if (!datas) datas = {};
793		if (!config) config = {};
794
795		module.initModule = function() {
796			module.tags = {};
797			module.config = config;
798			module.datas = datas;
799		};
800
801		module.initModuleTree = function() {
802
803			 // référentiel global (par type de module)
804			if (!module.initDone && module.moduleCode)
805				{
806				 if (!_.pageModules[module.moduleCode])
807				 	{
808				 	 _.pageModules[module.moduleCode] = [];
809					}
810				 _.pageModules[module.moduleCode].push(module);
811				}
812			if (!tree) tree = {parent:false, children:[], siblings:[]};
813			module.moduleTree = tree;
814			if (tree.parent)
815				{
816				 if (!tree.parent.moduleTree.children) tree.parent.moduleTree.children = [];
817				 if (tree.parent.moduleTree.children.length>0)
818				 	{
819				 	 if (!tree.siblings) tree.siblings = [];
820					 $.each(tree.parent.moduleTree.children, function (i, child) {
821					 	var found = false;
822					 	$.each(tree.siblings, function(j, sibly) {
823					 		if (child==sibly)
824					 			{
825					 			 found = true;
826					 			 return false;
827								}
828						});
829						if (!found) tree.siblings.push(child);
830					 });
831					}
832				 tree.parent.moduleTree.children.push(module);
833				}
834		};
835
836		module.makeChildModuleTree = function() {
837			var tree = {
838				parent: module,
839				siblings: module.moduleTree.children,
840				children: [],
841			};
842			return tree;
843		};
844
845		module.addModuleChild = function(mod) {
846			$.each(module.moduleTree.children, function(j, child) {
847				if (child==mod)
848					{
849					 found = true;
850					 return false;
851					}
852			});
853			if (!found)
854				{
855				 module.moduleTree.children.push(mod);
856				}
857		};
858
859		module.removeModuleChild = function(mod) {
860			$.each(module.moduleTree.children, function(j, child) {
861				if (child==mod)
862					{
863					 module.moduleTree.children.splice(j, 1);
864					 return false;
865					}
866			});
867		};
868
869		module.readExternalParameters = function() {
870
871			// read jsinfos
872			var jsinfos = $(">.JSinfos", module).html();
873			if (jsinfos)
874				{
875				 var config = eval("("+jsinfos.trim()+")");
876				 $(">.JSinfos", module).remove();
877				 $.extend(true, module.config, config);
878				}
879			var $nextInfos = $(module).next('.JSinfos');
880			if ($nextInfos.length>0)
881				{
882				 var jsinfos = $nextInfos.html();
883				 if (jsinfos)
884				 	{
885					 var config = eval("("+jsinfos.trim()+")");
886					 $.extend(true, module.config, config);
887					}
888				 $nextInfos.remove();
889				}
890
891			// read tag parameters
892			for (var i=0; i<module.attributes.length; i++)
893				{
894				 var attr = module.attributes[i],
895				 	 attrName = attr.name;
896				 if (attrName.match(/^data-/i))
897				 	{
898				 	 attrName = attrName.replace(/^data-/i, '');
899					}
900					else attrName = attr.name;
901				 if (['class','id','style','align','title','detail','help'].indexOf(attrName.toLowerCase())==-1)
902					{
903					 module.config[attr.name] = attr.specified? true : attr.value;
904					}
905				}
906		};
907
908
909
910		module.initModule();
911		module.initModuleTree();
912		module.readExternalParameters();
913		module.initDone = true;
914
915	},
916
917	addContextObj: function(domElement, Object) {
918		if (!domElement.contextObjects) domElement.contextObjects = [];
919		var found = false;
920		$.each(domElement.contextObjects, function(i, obj) {
921			if (obj==Object)
922				{
923				 found = true;
924				 return false;
925				}
926		});
927		if (!found)
928			{
929			 domElement.contextObjects.push(Object);
930			}
931	},
932
933	checkUrlApp: function() {
934		if (_.urlParams.get('daR') && _.urlParams.get('daD'))
935			{
936			 // accès direct
937			 var route = _.urlParams.get('daR').split('/');
938			 var data = JSON.parse(_.urlParams.get('daD'));
939			 _.actions({sct:route[0], obj:route[1], xAct:route[2], data:data});
940			}
941
942	},
943
944	ce: function(e, c, attr, html) {
945		e = document.createElement(e);
946		if (e=='button' && (!attr || !attr.type))
947			{
948			 if (!attr) attr = {};
949			 attr.type = 'button';
950			}
951		if (c) $(e).addClass(c);
952		if (attr) $(e).attr(attr);
953		if (html) $(e).html(html);
954		return e;
955	},
956	ceNS: function(e,c, attr) {
957		e = document.createElementNS('http://www.w3.org/2000/svg', e);
958		if (c) $(e).attr({class:c});
959		if (attr)
960			{
961			 $.each(attr, function(a, v) {
962			 	e.setAttribute(a, v);
963			 });
964			}
965		if (attr && attr['xlink:href']) e.setAttributeNS('http://www.w3.org/1999/xlink','href',attr['xlink:href']);
966		return e;
967	},
968
969	ceField: function(def, dest) {
970		var art = _.ce('article', def.fldClass? def.fldClass : '', {}, '<label>'+def.label+'</label><div></div>');
971		var $fld;
972		switch (def.format)
973			{
974			 case 'boolean':
975			 	var fldHtml = '<input type="checkbox" class="checkbox" name="'+def.name+'" '+(_.intval(def.defaultValue)? "checked" : "")+' value="1">';
976				if (def.switch) fldHtml+= '<span>'+def.switch.on+'|'+def.switch.off+'</span>';
977			 	$fld = $(fldHtml);
978			 break;
979			 case 'color':
980			 	$('>div', art).addClass('colors');
981
982				if (def.info && def.info.mode=='picker')
983					{
984					 $fld = $('<input type="text" class="jColorPicker" name="'+def.name+'" value="'+(def.defaultValue? def.defaultValue : '')+'" '+(def.info.jsinfos? 'jsinfos="'+def.info.jsinfos+'"':'')+'>');
985					 break;
986					}
987					else
988					{
989			 		 def.values = [];
990				 	 if (def.colors)
991				 		{
992					 	 $.each(def.colors, function(i, color) {
993					 	 	def.values.push({value:color, background:color, text:''});
994						 });
995						}
996					}
997			 case 'radiogrp':
998			 	$('>div', art).addClass('radio-grp wrap');
999			 	if (def.values)
1000			 		{
1001				 	 $.each(def.values, function(i, value) {
1002				 	 	var r = _.ce('input', '', {name:def.name, value:value.value, type:'radio'});
1003				 	 	var l = _.ce('label', '', {}, value.text);
1004						if (value.selected || def.selected==value.value) $(r).prop('checked', true);
1005				 	 	if (value.background)
1006				 	 		{
1007				 	 		 $(l).append('<div></div>');
1008				 	 		 $('>div', l).css({background:value.background});
1009							}
1010						$('>div', art).append(r,l);
1011					 });
1012
1013					}
1014			 break;
1015			 case 'date': $fld = $('<input type="text" class="datepicker" name="'+def.name+'" step="1" value="'+(def.defaultValue? def.defaultValue : '')+'">'); break;
1016			 case 'datetime': $fld = $('<input type="text" class="datepicker hour" name="'+def.name+'" step="1" value="'+(def.defaultValue? def.defaultValue : '')+'">'); break;
1017			 case 'nb': $fld = $('<input type="text" name="'+def.name+'" value="'+(def.defaultValue? def.defaultValue : '0')+'">'); break;
1018			 case 'select':
1019			 	$fld = $('<select name="'+def.name+'"></select>');
1020			 	_.populate($fld, def.values);
1021			 break;
1022			 case 'slider':
1023			 	$fld = $('<input type="text" class="new-slider" app-module="SliderField" min="'+(def.min? def.min : 0)+'" max="'+(def.max? def.max : 100)+'" step="'+(def.step? def.step : 1)+'" name="'+def.name+'" step="1" value="'+(def.value? def.value : (def.defaultValue? def.defaultValue : '0'))+'">');
1024				if (def.info) $fld.attr('jsinfos', def.info);
1025			 break;
1026			 case 'sugg-select':
1027			 case 'sugg':
1028			 	if (def.format=='sugg-select')
1029					{
1030					 var fld = _.ce('select', '', {name:def.name, placeholder:def.placeHolder});
1031					 _.populate(fld, def.values);
1032					}
1033					else var fld = _.ce('input', '', {type:'text', name:def.name, placeholder:def.placeHolder});
1034				def.info.type = 'sugg';
1035				if (!def.info.format) def.info.format = 'short';
1036				$fld = $('<div/>');
1037				$fld.append(fld);
1038				var cmp = _.components.add(def.info, fld);
1039			 break;
1040			 case 'static': $fld = $('<div>'+def.value+'</div>'); $(art).addClass('static'); break;
1041			 case 'img': $fld = $('<div><img src="'+def.src+'"></div>'); $(art).addClass('static img'); break;
1042			 case 'time': $fld = $('<input type="text" class="new-slider" app-module="SliderField" min="'+(def.min? def.min : 0)+'" max="'+(def.max? def.max : 3600)+'" name="'+def.name+'" step="1" value="'+(def.defaultValue? def.defaultValue : '0')+'">'); break;
1043			 case 'text': $fld = $('<input type="text" name="'+def.name+'" value="'+(def.defaultValue? def.defaultValue : '')+'">'); break;
1044			 case 'textarea': $fld = $('<textarea name="'+def.name+'"></textarea>'); break;
1045			 case 'wy':
1046			 	$fld = $('<textarea name="'+def.name+'" class="jWysiwyg '+def.id+'" id="'+def.id+'"></textarea>');
1047			 break;
1048			 case 'json': $fld = $('<textarea name="'+def.name+'"></textarea>'); $(art).addClass('jsonEditor'); break;
1049			 case 'module':
1050			 	var fld = _.ce(def.fld, 'ce-module', def.attrs? def.attrs : {});
1051				$fld = $('<div/>');
1052				$fld.append(fld);
1053			 break;
1054			}
1055
1056		if (def.placeHolder) $fld.attr('placeholder', def.placeHolder);
1057		if (def.divClass) $('>div', art).addClass(def.divClass);
1058		if (def.cls) $fld.addClass(def.cls);
1059		if (def.css) $fld.css(def.css);
1060		if (def.title) $fld.attr('title', def.title);
1061		if (def.detail) $fld.attr('detail', def.detail);
1062
1063		if ($fld) $('>div', art).append($fld);
1064		if (def.suffix) $('>div', art).append(def.suffix);
1065
1066		if (dest)
1067			{
1068			 $(dest).append(art);
1069			 switch (def.format)
1070				{
1071				 case 'boolean': break;
1072				 case 'time': break;
1073				 case 'slider': break;
1074				 case 'wy':
1075				 	setTimeout(function() {
1076				 		_.wy({
1077							select: "."+def.id,
1078							mode:'light',
1079							height:def.height? def.height : 200,
1080							options: $.extend({
1081								min_height: def.height? def.height : 200,
1082								max_height: def.height? 2*def.height : 200,
1083								setup: function(editor) {
1084									editor.on('init', function(e) {
1085										_.actions({trigger:'mysiwyg-loaded'});
1086									});
1087								}
1088							}, def.options || {}),
1089						});
1090					}, 1000);
1091				 break;
1092
1093				 case 'module':
1094				 	_.loadModule(fld, def.info, {}, def.moduleId);
1095				 break;
1096				}
1097			}
1098
1099		return {article:art, field:$fld? $fld[0] : false};
1100	},
1101
1102	ceStatic: function(def, dest) {
1103		var art = _.ce(def.tag? def.tag : 'article', 'static '+(def.code? def.code : ''), {}, '');
1104		var $content = false;
1105		switch (def.format)
1106			{
1107			 case 'text': $content = $('<div>'+def.text+'</div>'); break;
1108			 case 'img': $content = $('<img src="'+def.src+'">'); break;
1109			}
1110
1111		if (def.cls) $content.addClass(def.cls);
1112		if (def.css) $content.css(def.css);
1113
1114		if ($content) $(art).append($content);
1115
1116		if (dest) $(dest).append(art);
1117
1118		return {article:art, content:$content? $content[0] : false};
1119	},
1120
1121	ceFldGroup: function(group, dest) {
1122		if (group.definition)
1123			{
1124			 var div = _.ce('div'),
1125			 	 art = _.ce('article', group.fldClass? group.fldClass : '', {}, '<label>'+group.label+'</label>');
1126			 $(art).append(div);
1127			 if (group.divClass) $(div).addClass(group.divClass);
1128
1129			 $.each(group.definition, function(i, def) {
1130			 	def.fieldOnly = true;
1131			 	var res = _.ceField(def);
1132				if (res.article) $(div).append($('>div', res.article));
1133			 });
1134			 $(dest).append(art);
1135			}
1136	},
1137
1138	ceGroup: function(group, dest) {
1139		if (group.definition)
1140			{
1141			 var title = _.ce('h5', 'title');
1142			 if (group.open) $(title).addClass('opened');
1143			 $(title).html(group.name);
1144			 $(dest).append(title);
1145
1146			 var section = _.ce('section', 'formGroup');
1147			 _.ceFormDef(group.definition, section);
1148			 $(dest).append(section);
1149			 $(title).on('click', ()=>{
1150			 	if ($(title).hasClass('closed'))
1151					{
1152					 $(section).slideDown('fast',  ()=>{
1153					 	$(section).css('overflow', 'visible');
1154					 });
1155					 $(title).removeClass('closed');
1156					}
1157					else
1158					{
1159					 $(section).slideUp('fast');
1160					 $(title).addClass('closed');
1161					}
1162			 });
1163			}
1164	},
1165
1166	ceFormDef: function(definition, dest) {
1167		$.each(definition, function(i, def) {
1168			 if (def.type=='group')
1169			 	{
1170			 	 _.ceGroup(def, dest);
1171				}
1172				else if (def.type=='field-grp')
1173			 	{
1174			 	 _.ceFldGroup(def, dest);
1175				}
1176				else if (def.type=='field')
1177				{
1178				 _.ceField(def, dest);
1179				}
1180				else if (def.type=='static')
1181				{
1182				 _.ceStatic(def, dest);
1183				}
1184				else if (def.type=='hidden')
1185				{
1186				 var hidden = _.ce('input', def.cls? def.cls : '', {type:'hidden', name:def.name, value:def.value? def.value : ''});
1187				 $(dest).append(hidden);
1188				}
1189		});
1190		_.bringSectionToLife(dest);
1191	},
1192
1193	inputCalcul: function(input) {
1194		return new Promise((resolve)=>{
1195			var val = input instanceof HTMLElement? $(input).val() : input;
1196			if (val && val.match(/[*/+()]|\d\s*[-]/))
1197				{
1198					 _.calculator().then(calculator => {
1199					 	val = _.preCleanNum(val);
1200						try {
1201							var result = calculator.calculate(val);
1202						}
1203						catch(e) {
1204							var result = _.doubleval(val);
1205						}
1206						if (Number.isFinite(result))
1207							{
1208							 if (input instanceof HTMLElement)
1209							 	{
1210								 $(input).val(result);
1211								}
1212							}
1213						resolve(result);
1214					 });
1215				}
1216				else resolve(val);
1217		});
1218	},
1219
1220	clipping: function(clippingNr, fusionPreviewData) {
1221		return new Promise((resolve, reject) => {
1222			_.actions({route:'cfg/clip//load', data:{nr: clippingNr}, onSuccess: (res) => {
1223				let clipping = res.clipping;
1224				clipping.content = clipping.description;
1225				delete clipping.description;
1226				if (clipping.content.match(/#/) || (clipping.subject && clipping.subject.match(/#/))) {
1227					let ajaxData = $.extend({contents: {subject: clipping.subject || '', content: clipping.content}}, fusionPreviewData || {});
1228					_.actions({route: 'tl/fusion//preview', data: ajaxData, onSuccess: (data) => {
1229						clipping.content = data.content;
1230						clipping.subject = data.subject;
1231						resolve(clipping);
1232					}});
1233				}
1234				else {
1235					resolve(clipping);
1236				}
1237			}});
1238		});
1239	},
1240
1241	set: function(key, val, session) {
1242		if (typeof val == "undefined") val = "";
1243		if (session) res = window.sessionStorage.setItem(key, JSON.stringify(val));
1244		else res = window.localStorage.setItem(key, JSON.stringify(val));
1245		return res;
1246	},
1247	get: function(key, session) {
1248		if (session) var val = window.sessionStorage.getItem(key);
1249		else var val = window.localStorage.getItem(key);
1250		if (typeof val !="undefined") return JSON.parse(val);
1251		return false;
1252	},
1253	round: function(num, nb) {
1254		if (!nb) nb = 0;
1255		num = parseFloat(num);
1256		var pow = Math.pow(10,nb)
1257		return Math.round(num*pow)/pow;
1258		// num = num*pow; // je ne sais plus pourquoi c'était aussi alambiqué ... probablement le contournement d'un glitch
1259		// if (nb<6) num = Math.round(num*100000)/100000;
1260		// return Math.round(num)/pow;
1261	},
1262	roundExpr: function(num, nb=3, half=true) {
1263		if (!num) return 0;
1264		var d = Math.ceil(Math.log10(num < 0? -num : +num)),
1265			power = nb - d,
1266			magn = Math.pow(10, power);
1267		if (half) magn*= 2;
1268		var shifted = Math.round(num*magn);
1269		return shifted/magn;
1270	},
1271	preCleanNum: function(num) {
1272		if (typeof num =="string")
1273			{
1274			 num = num.replace(",", ".").replace(" ", "").replace("&nbsp;", "");
1275			}
1276		return num;
1277	},
1278	cleanNum: function(num) {
1279		if (!num) num = 0;
1280		if (typeof num =="string")
1281			{
1282			 num = _.preCleanNum(num);
1283			 num = num.replace('O', '0');
1284			 num = num.replace(/[^0-9,.-]+/, '');
1285			 if (!num) num = 0;
1286			 num = parseFloat(num);
1287			}
1288		return num;
1289	},
1290	formatBigNb: function(n, format, full) {
1291		if (full)
1292			{
1293			 switch (format)
1294				{
1295				 case 'price':	return Intl.NumberFormat('fr-FR', { style: 'currency', currency: _.cfg.currencyCode, maximumSignificantDigits: 10 }).format(Math.round(n)); break;
1296				 case 'int':	return Intl.NumberFormat('fr-FR', { style: 'decimal', maximumSignificantDigits: 10 }).format(Math.round(n)); break;
1297				 case 'percent': return Intl.NumberFormat('fr-FR', { style: 'decimal' }).format(Math.round(n))+' %'; break;
1298				 case 'float':	return Intl.NumberFormat('fr-FR', { style: 'decimal' }).format(Math.round(n)); break;
1299				}
1300			}
1301			else
1302			{
1303			 n = parseFloat(n);
1304			 var v=0, l='';
1305			 if (n>20000000) { n = (n/1000000).toFixed(0); l='M';}
1306			 else if (n>2000000) { n = (n/1000000).toFixed(1); l='M';}
1307			 else if (n>800000) { n = (n/1000000).toFixed(2); l='M';}
1308			 else if (n>20000) { n = (n/1000).toFixed(0); l='K';}
1309			 else if (n>2000) { n = (n/1000).toFixed(1); l='K';}
1310			 else if (n>20) { n = n.toFixed(0); l='';}
1311			 switch (format)
1312				{
1313				 case 'price':
1314					if (n>20) v = number_format(Math.round(n), 0, ",", " ")+l+" <span class='cur'>"+_.cfg.currency+"</span>";
1315					else v = parseFloat(n).toFixed(2)+l+" <span class='cur'>"+_.cfg.currency+"</span>";
1316					return v;
1317				 break;
1318				 case 'percent':
1319					return parseFloat(n)+l+" <span class='cur'>%</span>";
1320				 break;
1321				 case 'float':
1322					if (n>20) v = Math.round(n);
1323					else if (n>10) v = n.toFixed(1);
1324					else if (n>1) v = n.toFixed(2);
1325					else if (n>0.1) v = n.toFixed(3);
1326				 break;
1327				 case 'int':
1328				 default : v = Math.round(n); break;
1329				}
1330			 return v+''+l;
1331			}
1332	},
1333	formatCode: function(code) {
1334		var text = [];
1335		if (typeof code == "string")
1336			{
1337			 var parts = [...code.matchAll(/[A-Z][a-z-]+|[A-Z]+|[a-z-]+|[0-9.]/g)];
1338			 $.each(Array.from(parts), function(i, part) {
1339				text.push(part[0].ucwords());
1340			 });
1341			}
1342
1343		return text.join(" ");
1344	},
1345	nephew: function(n, t) {return $(t).siblings().find(n);},
1346
1347	lr:function(fr, en, ru) {
1348		if (digiSuiteVars.locale=='en') return en;
1349		else if (digiSuiteVars.locale=='ru') return ru;
1350		else return fr;
1351	},
1352	isMobile: function() {
1353		var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
1354		var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
1355
1356		return mobile;
1357	},
1358	removeAccents: function(str) {
1359		if (!str) str = '';
1360		return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "");
1361	},
1362	cleanEmoji: function(str) {
1363		var reg = /\ud83c[\udf00-\udfff]|\ud83d[\udc00-\ude4f]|\ud83d[\ude80-\udeff]/g;
1364		str = str.replace(reg, ' ');
1365		return str;
1366	},
1367	cleanRegEx: function(str) {
1368		var reg = /[*+?\[\]\(\)]/g;
1369		str = str.replace(reg, ' ');
1370		str = str.replace(/ +/, ' ');
1371		return str;
1372	},
1373	closeToolTips: function() {
1374		if (_.tooltipTarget && _.tooltipTarget.closeToolTip) _.tooltipTarget.closeToolTip();
1375		$('.toolTip').hide();
1376	},
1377	populate: function(s, vals, test, valFunc, selected, clear) {
1378		if (clear) $('>*', s).remove();
1379		var v = $(s).val();
1380		if (v) selected = v;
1381		if (vals)
1382			{
1383			 $.each(vals, function(k, v) {
1384				var icon = false;
1385				var color = false;
1386				if (Array.isArray(v))
1387					{
1388					 k = v[0]; v = v[1];
1389					}
1390				if (typeof v == "object")
1391					{
1392					 if (typeof v.key!='undefined')
1393						{
1394						 if (v.icon) icon = v.icon;
1395						 if (v.color) color = v.color;
1396						 k = v.key; v = v.val || v.value;
1397						}
1398						else if (typeof v.text!='undefined')
1399						{
1400						 if (v.icon) icon = v.icon;
1401						 if (v.color) color = v.color;
1402						 k = v.value; v = v.text;
1403						}
1404					}
1405				if (!test || test(k, v))
1406					{
1407					 var opt = _.ce('option');
1408					 $(opt).attr('value',k).html(valFunc? valFunc(k, v) : v);
1409					 if (selected!==false && ((Array.isArray(selected) && selected.indexOf(k)!=-1) || selected==k))
1410				 		{
1411				 		 $(opt).prop('selected', true);
1412						}
1413					 $(s).append(opt);
1414					}
1415				if (icon) $(opt).attr('icon', icon);
1416				if (color) $(opt).attr('color', color);
1417			 });
1418			}
1419	},
1420	goUrl: function(url, ev) {
1421		var a = _.ce("a"),
1422			pos = ev? _.eventXY(ev) : {x:0,y:0},
1423			evParams = {
1424				button:0,
1425				clientX:ev? pos.x : _.pageX,
1426				clientY:ev? pos.y : _.pageY,
1427				bubbles: true,
1428				cancelable: true,
1429				view: window,
1430			 },
1431			 evt = new MouseEvent("click", evParams);
1432		$(a).attr('href',url).attr('target','_blank');
1433
1434		a.dispatchEvent(evt);
1435
1436	},
1437
1438	getHtmlBloc: function(url, callback) {
1439	 $.ajax({
1440		type: 'GET',
1441		url: url,
1442		data: {},
1443		dataType: 'html',
1444		cache:false,
1445		timeout:10000,
1446		success: function(res) {
1447			callback(res);
1448		}
1449	 });
1450	},
1451
1452	getContentBloc: function(data) {
1453
1454		return new Promise(function(resolve, reject) {
1455
1456			var content = "";
1457			if (data.template) content = $(data.template).html();
1458			if (!content && data.content) content = data.content;
1459			if (content)
1460				{
1461				 resolve(content);
1462				}
1463				else if (data.code || data.urlsite || data.url)
1464				{
1465				 var params = data.params? data.params : "",
1466				 	 url = '';
1467				 if (!params && data.data) params = data.transData? data.transData : data.data;
1468				 if (data.url)
1469					{
1470					 url = _.cfg.urladmin+"/"+data.url.replace(/&amp;/g, '&');
1471					 url+= (url.indexOf('?')!=-1? '&' : '?')+'spaceSelect=1';
1472					}
1473					else if (data.urlsite) url = _.cfg.urlsite+"/"+data.urlsite;
1474					else url = _.cfg.urladmin+"/cfg/modal/ajax/"+data.code;
1475
1476				 _.digi.request(url, params, {dataType:'html'}).then((res)=>{
1477				 	if (res)
1478						{
1479				 		 resolve(res);
1480						}
1481						else
1482						{
1483						 reject(new Error('The URL or Code ('+url+') hasn\'t return any content'));
1484						}
1485				 }).catch((error) => {
1486                    reject(error);
1487                 });
1488				}
1489				else
1490				{
1491				 reject(new Error('No content specified to get (or url or code)'));
1492				}
1493		});
1494	},
1495
1496	copyToClipboard: function(text, select) {
1497		if (select) text = $(select).html();
1498
1499		function listener(e) {
1500			var plainText = $('<div>'+text+'</div>').text().trim();
1501
1502		    e.clipboardData.setData("text/plain", plainText);
1503		    e.clipboardData.setData("text/html", text);
1504
1505		    e.preventDefault();
1506		}
1507
1508		document.addEventListener("copy", listener);
1509		document.execCommand("copy");
1510		document.removeEventListener("copy", listener);
1511	},
1512
1513	wyChangeHeight: function(id, h) {
1514		if (typeof tinymce != "undefined")
1515			{
1516			 var wy = tinymce.get(id);
1517			 if (wy) $(wy.getContainer()).css('height', h);
1518			}
1519	},
1520	wy5: function(config, cross) {
1521		if (typeof tinymce == "undefined" && !cross)
1522			{
1523			 window.dependencyLoader.loadScripts([
1524				_.cfg.urlrealadmin+'/inc/tinymce5/tinymce.min.js'
1525			 ]).then(() => {
1526				_.wy5(config, true);
1527			 });
1528
1529			 return;
1530			}
1531
1532		_.clearDetachedWYEditors();
1533
1534		var config = $.extend({
1535			height: 250,
1536			menu: false,
1537			status: false,
1538		}, config || {});
1539
1540		let defaultOptions = {};
1541
1542		switch (config.mode)
1543			{
1544			 case 'full':
1545				defaultOptions.fontsize_formats = "8px 9px 10px 11px 12px 14px 16px 18px 22px 26px 32px 36px 48px 60px 76px";
1546
1547				defaultOptions.plugins = [
1548					'print preview paste searchreplace autolink directionality code visualblocks ', // importcss
1549					'visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking ',
1550					'anchor toc insertdatetime advlist lists wordcount imagetools textpattern noneditable help charmap quickbars' // emoticons
1551				];
1552				if (config.menu===true) config.menu = 'file edit view insert format tools table help';
1553				defaultOptions.toolbar = "undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | al
1553ignleft aligncenter alignright alignjustify | "
1554										+"outdent indent | numlist bullist | forecolor backcolor removeformat | link image | fullscreen preview | "
1555										+"anchor media insertfile template codesample | pagebreak | charmap"; // emoticons
1556				defaultOptions.quickbars_selection_toolbar = 'bold italic | h1 h2 h3 blockquote | removeformat';
1557
1558				if (config.fullpage)
1559					{
1560					 defaultOptions.plugins.push('fullpage');
1561					 defaultOptions.fullpage_default_encoding = 'UTF-8';
1562					}
1563
1564				defaultOptions.file_picker_callback = function(callback, value, meta) { // file_browser_callback
1565					var inputFile = _.ce('input', '', {type:'file', accept:'image/*'});
1566					inputFile.onchange = function () {
1567						var file = this.files[0];
1568						var reader = new FileReader();
1569						reader.onload = function () {
1570					        var id = file.name;
1571					        var blobCache =  tinymce.activeEditor.editorUpload.blobCache;
1572					        var base64 = reader.result.split(',')[1];
1573					        var blobInfo = blobCache.create(id, file, base64);
1574					        blobCache.add(blobInfo);
1575
1576					        callback(blobInfo.blobUri(), { title: file.name });
1577      					};
1578      					reader.readAsDataURL(file);
1579    				};
1580
1581    				inputFile.click();
1582
1583				};
1584				if (config.uploadType)
1585					{
1586					 defaultOptions.images_upload_url = _.cfg.urladmin+"/ajax/ups/ajax/n?tiny=1&type="+config.uploadType+"&nr="+config.uploadNr;
1587					 defaultOptions.images_upload_credentials = true;
1588					//  defaultOptions.images_reuse_filename = true;
1589					 defaultOptions.paste_data_images = true;
1590					 defaultOptions.file_picker_types = 'image media';
1591					}
1592				defaultOptions.convert_urls = true;
1593				defaultOptions.image_caption = true;
1594
1595				defaultOptions.block_formats = 'Bloc=div; Paragraphe=p; Titre 1=h1; Titre 2=h2; Titre 3=h3; Titre 4=h4; Titre 5=h5; Titre 6=h6; Preformaté=pre';
1596				defaultOptions.content_style = 'body { font-family:Arial,Helvetica,sans-serif; font-size:14px }';
1597			 break;
1598
1599			 case 'light':
1600			 case 'lighter':
1601			 	defaultOptions.toolbar = config.mode=='lighter'? "bold italic | bullist | link | customAddFile customSendButton"
1602			 													:"bold italic | bullist numlist | link blockquote | codesample | customAddFile customSendButton";
1603				defaultOptions.plugins = [
1604					'paste autolink code image link codesample lists imagetools'
1605				];
1606				if (!config.options || config.options.autoresize!==false) defaultOptions.plugins[0]+= ' autoresize';
1607				defaultOptions.toolbar_location = 'bottom';
1608				defaultOptions.force_p_newlines = false;
1609				defaultOptions.force_br_newlines = true;
1610				defaultOptions.convert_newlines_to_brs = true;
1611				defaultOptions.forced_root_block = '';
1612				defaultOptions.images_file_types = '';
1613//				defaultOptions.height = 80;
1614				defaultOptions.autoresize_bottom_margin = 0;
1615				if (!defaultOptions.min_height) defaultOptions.min_height = 80;
1616				if (!defaultOptions.max_height) defaultOptions.max_height = 250;
1617
1618				defaultOptions.content_style = 'body { font-family:Arial,Helvetica,sans-serif; font-size:12px; margin:0.2rem 0.5rem; }';
1619				defaultOptions.valid_elements = '@[id|class|style|title],-a[href|target=_blank],pre,b/strong,-div[align],blockquote,br,-i/em,-s/strike,-u,hr,br,-p,-ul[type|compact],-ol[type|compact],-li,-span,-h1,-h2,-h3,-h4,-h5,-h6';
1620				defaultOptions.quickbars_selection_toolbar = false;
1621			 break;
1622
1623
1624			 case 'message':
1625				defaultOptions.fontsize_formats = "8px 9px 10px 11px 12px 14px 16px 18px 22px 26px 32px 36px 48px 60px 76px";
1626//				defaultOptions.icons = 'small';
1627				defaultOptions.toolbar = "undo | fontsizeselect | bold italic | alignleft aligncenter alignright | bullist numlist outdent indent | link image | forecolor backcolor";
1628				defaultOptions.plugins = [
1629					'paste autolink code visualblocks ',
1630					'visualchars image link media template codesample table charmap hr pagebreak nonbreaking ',
1631					'anchor insertdatetime advlist lists imagetools textpattern noneditable charmap quickbars'
1632				];
1633				defaultOptions.content_style = 'body { font-family:Arial,Helvetica,sans-serif; font-size:14px }';
1634				defaultOptions.valid_elements = '@[id|class|style|title],-a[href|target=_blank],b/strong,-div[align],br,-i/em,-s/strike,-u,hr,br,-p,-ul[type|compact],-ol[type|compact],-li,-span,-h1,-h2,-h3,-h4,-h5,-h6';
1635				defaultOptions.quickbars_selection_toolbar = 'bold italic underline strikethrough | h1 h2 h3 blockquote';
1636
1637			 break;
1638
1639			 case 'simple':
1640				defaultOptions.toolbar = "bold italic | alignleft aligncenter alignright | bullist numlist outde
1640nt indent | link | forecolor";
1641				defaultOptions.plugins = [
1642					'paste autolink code visualblocks ',
1643					'visualchars image link media template codesample table charmap hr pagebreak nonbreaking ',
1644					'anchor insertdatetime advlist lists imagetools textpattern noneditable charmap quickbars'
1645				];
1646				defaultOptions.content_style = 'body { font-family:Arial,Helvetica,sans-serif; font-size:14px }';
1647			 break;
1648			}
1649
1650		config.options = $.extend(defaultOptions, config.options || {});
1651		if (!config.autosave_disable) config.options.plugins.push('autosave');
1652		if (config.options.toolbarPrepend) config.options.toolbar = config.options.toolbarPrepend + config.options.toolbar;
1653		if (config.options.addPlugins) config.options.plugins+= ' '+config.options.addPlugins;
1654
1655		var init = {
1656			selector: config.select,
1657			base_url: _.cfg.urlrealadmin+'/inc/tinymce5',
1658			skin: 'oxide',
1659			relative_urls: false,
1660			convert_urls: false,
1661//			remove_script_host : false,
1662			toolbar_sticky: true,
1663
1664			autosave_ask_before_unload: false,
1665			autosave_interval: '10s',
1666			autosave_prefix: 'df3-{path}{query}-{id}-',
1667			autosave_restore_when_empty: true,
1668			autosave_retention: '12m',
1669
1670			imagetools_cors_hosts: ['picsum.photos'],
1671			language : _.cfg.locale=='fr'? "fr_FR" : "en_GB",
1672			width: "100%",
1673			height: config.height,
1674			menubar: config.menu,
1675			statusbar: config.status,
1676			toolbar_mode: 'sliding',
1677
1678			help_tabs: [ 'shortcuts', 'keyboardnav'],
1679			quickbars_insert_toolbar: false,
1680			contextmenu: 'link image imagetools table',
1681			textpattern_patterns: [
1682				{start: '1. ', cmd: 'InsertOrderedList'},
1683				{start: '* ', cmd: 'InsertUnorderedList'},
1684				{start: '- ', cmd: 'InsertUnorderedList' }
1685			],
1686//			paste_data_images: true,
1687			paste_preprocess: function(plugin, args) {
1688
1689			},
1690			paste_postprocess: function(plugins, args) {
1691    		}
1692		};
1693		if (config.options)
1694			{
1695			 init = $.extend(init, config.options);
1696			}
1697		tinymce.suffix = '.min';
1698		tinymce.uploadType = config.type;
1699		tinymce.uploadNr = config.nr;
1700		tinymce.init(init);
1701
1702		_.wyAutoCleanDraft();
1703	},
1704
1705	wyAutoCleanDraft: function() {
1706		var keys = Object.keys(window.localStorage);
1707		$.each(keys, function(i, key) {
1708			var match = key.match(/df3-(.*)-time/);
1709			if (match)
1710				{
1711				 if (parseInt(window.localStorage.getItem(key)) < (Date.now()-12*60*1000))
1712				 	{
1713				 	 window.localStorage.removeItem(key);
1714				 	 window.localStorage.removeItem('df3-'+match[1]+'-draft');
1715					}
1716				}
1717		});
1718	},
1719
1720
1721
1722	wy: function(config) { // type, [nr], select, [height], [menu]
1723		return _.wy5(config);
1724	},
1725
1726	// @deprecated : use _.clearDetachedWYEditors()
1727	wyRemoveAll: function(wId) {
1728		_.clearDetachedWYEditors();
1729	},
1730
1731
1732	simplifyHtml: function(html) {	// , keep
1733		//var toKeep = ['a', 'i', 'em', 'b', 'strong', 'br', 'p', 'div', 'span', 'ul', 'li', 'ol', 'u', 's'];
1734		html = html.replace(/<script[^>]*>/ig, "");
1735		if (html.match(/<a([^>]+?)href\s*=\s*["']([^"']+)["'][^>]*>/ig))
1736			{
1737			 html = html.replace(/<a[^>]+?href\s*=\s*["']([^"']+)["'][^>]*>/ig, "<a href=\"$1\">");
1738			}
1739			else html = html.replace(/<([a-z0-9]+)[^>]*>/ig, "<$1>");
1740		html = html.replace(/<\/([a-z0-9]+)[^>]*>/ig, "</$1>");
1741
1742		return html;
1743	},
1744
1745	changeStrPattern: function(str, patterns) {
1746
1747		// Liens
1748		var linkReg = /(^|[\(:]\s*|<[a-z]+[^>]*>|\s+|;)(((https|http|ftp):\/\/|www\.)?(([a-z0-9][a-z0-9-]*)(\.[a-z0-9][a-z0-9-]*)*\.[a-z]+)(\/[^<\"' \t\n\r]*)?)(\s*<\/[a-z]+>|[<\"' \t\n\r;,!*?.&()]|\s|$)/ig;
1749		var donePatterns = [];
1750		while ((match = linkReg.exec(str))!==null)
1751			{
1752			 if (match[0].toLowerCase().indexOf('</a>')==-1)
1753				{
1754				 var link = match[2];
1755				 if (!link.match(/(https|http|ftp):\/\//i)) link = "https://"+link;
1756				 str = str.strReplace(match[0], match[1]+'<a href="'+link+'" target="_blank">'+(match[2].length>60? match[2].substring(0,57)+"...": match[2])+'</a>'+match[9]);
1757				}
1758				else
1759				{
1760				 // trouver le <a> et rajouter target="_blank"
1761				}
1762			}
1763
1764		// Emails
1765		var matches = str.matchAll(/(^|\s+|[\(:])([_.0-9a-z]([-+=_.0-9a-z])*@[0-9a-z]([-.]?[0-9a-z]+)*\.[a-z]{2,13})([<\t\n\r,;:!?()|=&]|$)/ig);
1766		var donePatterns = [];
1767		$.each(matches, function(i, match) {
1768			 var link = match[2];
1769			 str = str.strReplace(match[2], '<a href="mailto:'+match[2]+'" target="_blank">'+(match[2].length>60? match[2].substring(0,57)+"...": match[2])+'</a>');
1770		});
1771
1772		// patterns pré-détectés
1773		if (patterns)
1774			{
1775			 $.each(patterns, function(i, pattern) {
1776				if (donePatterns.indexOf(pattern.ref)==-1)
1777			 		{
1778					 donePatterns.push(pattern.ref);
1779					 var rep = pattern.name? pattern.name : pattern.ref;
1780					 var pre="", title="", url='';
1781					 switch (pattern.type)
1782				 		{
1783				 		 case 'contact': pre = '<i class="i-user"></i> '; title="Contact"; url = 'crm/ct/view?nr='+pattern.nr; break;
1784				 		 case 'tk': pre = '<i class="i-tag"></i> '; title="Ticket"; url = 'crm/ticket/view?nr='+pattern.nr; break;
1785				 		 case 'fa': pre = '<i class="i-shopping-cart"></i> '; title="Facture"; url = 'com/cto/viewPayment?nr='+pattern.nr; break;
1786				 		 case 'bl': pre = '<i class="i-shopping-cart"></i> '; title="Bon de Livraison"; url = 'com/cto/viewLogistic?nr='+pattern.nr; break;
1787				 		 case 'bc': pre = '<i class="i-shopping-cart"></i> '; title="Vente / Bon de commande"; url = 'com/cto/view?nr='+pattern.nr; break;
1788				 		 case 'cpy': pre = '<i class="i-building"></i> '; title="Société"; url = 'crm/cpy/view?nr='+pattern.nr; break;
1789				 		 case 'qt': pre = '<i class="i-file-spreadsheet far"></i> ';  title="Devis"; url = 'crm/oppt/qtView?qnr='+pattern.nr; break;
1790				 		 case 'user': pre = '@';  title=""; break;
1791						}
1792					 if (pattern.name) title+= " - "+pattern.ref;
1793					 if (url)
1794					 	{
1795						 rep = '<span title="'+title+'" class="jaction ref known" jsinfos="{url: \''+url+'\', target:\'_blank\'}">'+pre+rep+'</span>';
1796						}
1797						else
1798						{
1799						 rep = '<b class="known">'+pre+rep+'</b>';
1800						}
1801					 if (pattern.str && pattern.str.length > 0)
1802						{
1803						 str = str.strReplace(pattern.str, rep);
1804						}
1805					}
1806			 });
1807			}
1808
1809		// autre pattern identifiable
1810			// prix
1811		str = str.replace(/(^|\s|>)(-\s?[0-9][0-9 .,]*\s*(€|$|£|&euro;|euros?))/ig, '$1<span class="price neg">$2</span>');
1812		str = str.replace(/(^|\s|>)([0-9][0-9 .,]*\s*(€|$|£|&euro;|euros?))/ig, '$1<span class="price">$2</span>');
1813
1814			// références-like
1815		var matches = str.matchAll(/(^|\s+|<[a-z0-9]+\s+[^>]*|=\s*"||=\s*'|[\(:>
1815"'])([a-z]+[0-9_-]+[0-9a-z_-]*[0-9a-z]|[0-9]+[a-z_-]+[0-9a-z_-]*[0-9a-z]|[0-9a-z]+[_-][0-9a-z_-]*[0-9a-z](\s+[0-9]{2}:[0-9]{2}:[0-9]{2})?)($|\s+|[\)<"'])/ig);
1816		$.each(matches, function(i, match) {
1817			 if (donePatterns.indexOf(match[2])==-1 && !match[1].match(/^<[a-z0-9]/i) && !match[1].match(/^=\s*['"]/i) && match[2].match(/[0-9]/)
1818				 && (match[2].length>5 || match[2].match(/^[A-Z0-9_-]+$/)))
1819				{
1820				 donePatterns.push(match[2]);
1821				 str = str.strReplace(match[2], '<span class="ref">'+match[2]+'</span>');
1822				}
1823		});
1824
1825		return str;
1826	},
1827
1828	showContentInfo: function(data, bloc, subSelect) {
1829		if ($(subSelect, bloc).length>0)
1830			{
1831			 setTimeout(function() {
1832				 $(subSelect+' img', bloc).attr('cmp', 'img');
1833				 $(subSelect+' img', bloc).cmpImg();
1834			 }, 500);
1835			}
1836		 if (data.tags && data.tags.length>0)
1837			{
1838			 $(bloc).append('<ul class="tags"></ul>');
1839			 $.each(data.tags, function(i, tag) {
1840				var li = _.ce('li', 'tag', {}, "#"+tag.name); // A faire : Ajouter un jsinfos pour aller sur le channel de ce tag
1841				$('ul.tags', bloc).append(li);
1842			 });
1843			}
1844		 if (data.links && data.links.length>0)
1845			{
1846			 var links = _.ce('ul', 'links');
1847			 var imgLinks = [];
1848			 $(bloc).append(links);
1849
1850			 $.each(data.links, function(i, link) {
1851			 	if (link.type=="image")
1852			 		{
1853			 		 imgLinks.push({isPic:true, thumb:link.urlimg, src:link.urlimg, mime:'jpg'});
1854					}
1855			 		else if (link.title || link.description || link.urlimg)
1856				 	{
1857					 var linkLi = _.ce('li', 'link'),
1858					 	 lkTitle = _.ce('h6', '', {}, link.title),
1859					 	 lkDesc = _.ce('p', '', {}, link.description.length>140? link.description.substring(0,137)+'...' : link.description);
1860					 if (link.urlimg)
1861					 	{
1862					 	 $(linkLi).append('<figure/>');
1863					 	 $('figure', linkLi).html('<img src="'+link.urlimg+'">');
1864						}
1865				 	 $(linkLi).append('<div/>');
1866				 	 $('>div', linkLi).append(lkTitle, lkDesc);
1867					 $(links).append(linkLi);
1868					 $(linkLi).click(function(ev) {
1869					 	_.actions({act:'href', href: link.link, target:'_blank'}, false, ev);
1870					 });
1871					}
1872			 });
1873			}
1874		 if (data.files && data.files.length>0)
1875			{
1876			 var listCmp = _.components.add({type:'filelist', format:'mix', data:{list:data.files}});
1877			 $(bloc).append(listCmp);
1878			}
1879		 if (imgLinks && imgLinks.length>0)
1880			{
1881			 var listCmp = _.components.add({type:'filelist', format:'mix', data:{list:imgLinks}});
1882			 $(bloc).append(listCmp);
1883			}
1884	},
1885
1886
1887	filterItems: function(search, items, flds, max, wordSearch) {
1888		if (!search)
1889			{
1890			 return items.slice();
1891			}
1892			else
1893			{
1894			 if (!Array.isArray(flds)) flds = [flds];
1895			 var words = _.cleanRegEx(_.removeAccents(search)).split(" ");
1896			 var filterItems = [];
1897			 for (var j=0; j<items.length; j++)
1898				{
1899				 // split de la value en mots
1900				 var dataVal = "";
1901				 for (var f=0; f<flds.length; f++)
1902				 	{
1903					 var _str = items[j][flds[f]];
1904					 if (typeof _str == "object" && Array.isArray(_str))
1905					 	{
1906						 _str = _str.join(" ");
1907						}
1908				 	 dataVal+= " "+_.removeAccents(_str);
1909					}
1910				 var dataOk = true;
1911
1912				 // recherche des mots dans la valeur à recherche (à concaténer)
1913				 for (var k=0; k<words.length; k++)
1914					{
1915					 var regex = new RegExp((wordSearch? "\\b" : "")+words[k], "gi");
1916					 if (!dataVal.match(regex))
1917						{
1918						 dataOk = false;
1919						}
1920					}
1921				 if (dataOk)
1922					{
1923					 filterItems.push(items[j]);
1924					}
1925				 if (max && filterItems.length>max) return filterItems;
1926				}
1927			 return filterItems;
1928			}
1929	},
1930
1931	boldSearch: function(str, search) {
1932		// bug probable : si le contenu retourné contient des tags html ET que le début de la
1933		// recherche correspond à un nom de tag ou un attribut de tag, par exemple src =
1934		if (!search) return str;
1935		if (!str) str = "";
1936		var s = search.split(" "),
1937			words = str.split(" "),
1938			nbs = s.length,
1939			nb = words.length;
1940
1941		for (var k=0; k<nb; k++)
1942			{
1943			 if (!words[k].match(/[<>="]/i)) // pour ne pas traiter des mots qui seraient des morceaux de tag ou attributs
1944				{
1945				 for (var j=0; j<nbs; j++)
1946					{
1947					 w = _.removeAccents(words[k]).toLowerCase();
1948
1949					 var sj = _.removeAccents(s[j]).toLowerCase();
1950					 sj = sj.replace('\^', '');
1951					 pos = w.indexOf(sj);
1952
1953					 if (pos>=0 && words[k].indexOf("<b>")==-1) // pour éviter d'éventuels ajouts de <b>
1954						{
1955						 slen = sj.length;
1956						 wlen = words[k].length;
1957						 words[k] = words[k].substr(0, pos)+"<b>"+words[k].substr(pos, slen)+"</b>"+words[k].substr(slen+pos);
1958						}
1959					}
1960				}
1961			}
1962		return words.join(" ");
1963	},
1964
1965
1966	isObject: function(o) {
1967		return (typeof o==="object" && !Array.isArray(o));
1968	},
1969	isEmpty: function(o) {
1970		if (o==null) return true;
1971		if (Array.isArray(o) || typeof o == 'string') return o.length==0;
1972		for (var k in o) if (o.hasOwnProperty(k)) return false;
1973		return true;
1974	},
1975	objLength: function(o) {
1976		var n = 0;
1977		for (var k in o) if (o.hasOwnProperty(k)) n++;
1978		return n;
1979	},
1980	splitKeys(keys) {
1981		if (typeof keys == 'string') keys = keys.split(',');
1982		return keys;
1983	},
1984	reduceKeys: function (obj, keys) {
1985		keys = this.splitKeys(keys);
1986
1987		return Object.entries(obj).filter((entry) => keys.includes(entry[0])).reduce((accumulator, entry) => { accumulator[entry[0]] = entry[1]; return accumulator; }, {});
1988	},
1989	isHTML: function(str) {
1990		return str.match(/<(br|p|div|ul|li|ol|pre)[^>]*>/i);
1991	},
1992
1993	intersect: function(a, b) {
1994		var ai=0, bi=0, r = [];
1995
1996		while (ai<a.length && bi<b.length)
1997			{
1998			 if (a[ai] < b[bi] ) ai++;
1999			 else if (a[ai] > b[bi] ) bi++;
2000			 else
2001				{
2002				 r.push(a[ai]);
2003				 ai++; bi++;
2004				}
2005			}
2006		return r;
2007	},
2008
2009	eventXY: function (e) {
2010		var out = {x:0, y:0};
2011		if (e.type.indexOf('touch')!=-1)
2012			{
2013			 var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
2014			 out.x = touch.pageX; out.y = touch.pageY;
2015			 out.target = document.elementFromPoint(out.x - window.pageXOffset, out.y - window.pageYOffset);
2016			}
2017			else if (e.type.indexOf('mouse')!=-1 || e.type=='contextmenu' || e.type=='click')
2018			{
2019			 out.x = e.pageX; out.y = e.pageY;
2020			 out.target = e.target;
2021			}
2022			else if ((e.type=='drop' || e.type=='dragover' || e.type=='dragenter' || e.type == 'dragleave') && e.originalEvent)
2023			{
2024			 out.x = e.originalEvent.pageX; out.y = e.originalEvent.pageY;
2025			 out.target = e.originalEvent.target;
2026			}
2027			else if (e.page && e.nativeEvent) // PIXI
2028			{
2029			 out.x = e.page.x;
2030			 out.y = e.page.y;
2031			}
2032		if ((typeof out.target=='undefined' && typeof _.pageX!='undefined') || e.type=="paste")
2033			{
2034			 out.x = _.pageX;
2035			 out.y = _.pageY;
2036			}
2037			else if (typeof out.x!='undefined' && typeof out.y!='undefined')
2038			{
2039			 _.pageX = out.x;
2040			 _.pageY = out.y;
2041			}
2042
2043		let originalEvent = 'originalEvent' in e ? e.originalEvent : e;
2044		if (originalEvent && originalEvent.view)
2045			{
2046			 let eventDocument = originalEvent.view.document;
2047			 if (eventDocument !== document) {
2048				let iframe = originalEvent.view.frameElement;
2049				let iframePos = $(iframe).offset();
2050				let yScroll = eventDocument.documentElement.scrollTop;
2051				let xScroll = eventDocument.documentElement.scrollLeft;
2052				out.iframeRelativePos = {
2053					x: out.x - xScroll,
2054					y: out.y - yScroll,
2055				};
2056				out.x += iframePos.left - xScroll;
2057				out.y += iframePos.top - yScroll;
2058			 }
2059		}
2060
2061		return out;
2062	},
2063
2064	followTheMouse: function() {
2065		if (!_.metaKey)
2066			{
2067			 _.pageX = 0;
2068			 _.pageY = 0;
2069			 _.metaKey = {};
2070			 _.pageLastTarget = false;
2071			 $(document).ready(function() {
2072				$(document).on('mousemove', (e)=>{ //  dragover drag
2073					var pos = _.eventXY(e);
2074					_.pageX = pos.x;
2075					_.pageY = pos.y;
2076					_.pageLastTarget = e.target;
2077					_.metaKey = {
2078						ctrl:e.ctrlKey,
2079						shift:e.shiftKey,
2080						alt:e.altKey,
2081						key:e.keyCode
2082					};
2083				});
2084			 });
2085			}
2086	},
2087
2088	autoHscroll: function(section) {
2089
2090		_.followTheMouse();
2091
2092		var moveTimer = setInterval(function() {
2093			if (_.pageLastTarget && $(_.pageLastTarget).closest('.latPanel').length>0)
2094				{
2095				}
2096				else
2097				{
2098				 var activate = false;
2099				 if (!section) activate = true;
2100				 else
2101				 	{
2102					 var o = $(section).offset();
2103					 if (_.pageY>=o.top && _.pageY<=o.top+$(section).outerHeight())
2104						 {
2105						  activate = true;
2106						 }
2107					}
2108				 var $excludedZones = $('.chat-room,nav.leftMenu');
2109				 if ($excludedZones.length>0)
2110				 	{
2111					 $excludedZones.each(function(i, toExclude) {
2112					 	var exO = $(toExclude).offset();
2113					 	if (_.pageX>=exO.left && _.pageX<=exO.left+$(toExclude).width() &&
2114							_.pageY>=exO.top && _.pageY<=exO.top+$(toExclude).height() )
2115							{
2116							 activate = false;
2117							}
2118					 });
2119					}
2120				 if (activate)
2121				 	{
2122					 var left = $(document).scrollLeft();
2123					 var lw = _.pageX-left;
2124					 if (lw>30)
2125						{
2126						 if (lw<120 && left>20)
2127							{
2128							 if (lw>100) { left-=20; _.pageX-= 20; }
2129							 else if (lw>70) { left-=50; _.pageX-= 50; }
2130							 else { left-=80; _.pageX-= 80; }
2131
2132							 if (left<0) left = 0;
2133							 $(document).scrollLeft(left);
2134							}
2135							else
2136							{
2137							 var w = $(window).width(),
2138								 totw = $('.scrollZone').width();
2139							 var rw = w - (_.pageX - left);
2140
2141							 if (rw<120 && left<totw-20)
2142								{
2143								 if (rw>100) { left+=20; _.pageX+= 20; }
2144								 else if (rw>70) { left+=50; _.pageX+= 50; }
2145								 else { left+=80; _.pageX+= 80; }
2146
2147								 $(document).scrollLeft(left);
2148								}
2149							}
2150						}
2151					}
2152				}
2153
2154		}, 100);
2155
2156	},
2157
2158	autoDragScroll: function() {
2159
2160		_.draggingTimer = setInterval(function() {
2161
2162			 // ScrollScreen while drag
2163			 var scrTop = $('html').scrollTop();
2164			 if (scrTop+50 > _.pageY && _.pageY>0)
2165				{
2166				 var diff = 4;
2167				 if (scrTop+30 > _.pageY) diff = 8;
2168				 if (scrTop+10 > _.pageY) diff = 16;
2169				 if (scrTop-5 > _.pageY) diff = 30;
2170				 $('html').scrollTop(scrTop-diff);
2171				}
2172
2173			 var scrBottom = scrTop + $(window).height();
2174
2175			 if (scrBottom-50 < _.pageY && scrBottom<$(document).height()-$(_.dragToMove).height()-20)
2176				{
2177				 var diff = 4;
2178				 if (scrBottom-30 < _.pageY) diff = 8;
2179				 if (scrBottom-10 < _.pageY) diff = 16;
2180				 if (scrBottom+5 < _.pageY) diff = 30;
2181				 $('html').scrollTop(scrTop+diff);
2182				}
2183		}, 200);
2184
2185
2186	},
2187	autoDragScrollStop: function() {
2188		clearInterval(_.draggingTimer);
2189		_.draggingTimer = false;
2190		_.draggingLastY = false;
2191	},
2192
2193	findClosestMarkedScrollableElement: function (element) {
2194		let closestMarkedScrollableElement = $(element).closest('.df3-scrollable').get(0);
2195		return closestMarkedScrollableElement || null;
2196	},
2197
2198	markScrollableElements: function (direction) {
2199		if (typeof direction == 'undefined') direction = 'vertical';
2200		let elements = [];
2201		if (direction == 'vertical' || direction == 'both') elements = elements.concat(this.getScrollableElements('vertical'));
2202		if (direction == 'horizontal' || direction == 'both') elements = elements.concat(this.getScrollableElements('horizontal'));
2203
2204		_.unmarkScrollableElements();
2205		$(elements).addClass('df3-scrollable');
2206	},
2207
2208	unmarkScrollableElements: function () {
2209		$('.df3-scrollable').removeClass('df3-scrollable');
2210	},
2211
2212	getScrollableElements: function (direction) {
2213		if (typeof direction == 'undefined') direction = 'vertical';
2214		return [...document.querySelectorAll('*')].filter(x => _.isElementScrollable(x, direction));
2215	},
2216
2217	isElementScrollable: function(element, direction) {
2218		let scrollProperty = (direction !== 'horizontal') ?
2219					'scrollTop' : 'scrollLeft';
2220		let overflowProperty = (direction !== 'horizontal') ?
2221					'overflow-y' : 'overflow-x';
2222
2223		if (getComputedStyle(element).getPropertyValue(overflowProperty) == 'hidden') {
2224			return false;
2225		}
2226
2227		var res = !! element[scrollProperty];
2228
2229		if (!res) {
2230			element[scrollProperty] = 1;
2231			res = !!element[scrollProperty];
2232			element[scrollProperty] = 0;
2233		}
2234
2235		return res;
2236	},
2237
2238	isElementPartiallyVisible: function (element, container, scrollValue) {
2239		if (typeof container == 'undefined' || !container) container = document.body;
2240
2241		const elementClientRect = element.getBoundingClientRect();
2242		const elementTop = elementClientRect.top;
2243		const elementBottom = elementClientRect.bottom;
2244
2245		let containerTop = null;
2246		let containerBottom = null;
2247		if (container.window === container) {
2248			containerTop = scrollValue || container.document.body.scrollTop;
2249			containerBottom = containerTop + container.innerHeight;
2250		} else {
2251			containerTop = scrollValue || container.scrollTop;
2252			containerBottom = container.getBoundingClientRect().bottom;
2253		}
2254
2255		// Some part of the element is visible in the container
2256		return (elementTop >= containerTop && (elementTop + 20) < containerBottom)
2257			|| (elementBottom <= containerBottom && (elementBottom + 20) > containerTop)
2258			|| (elementTop < containerTop && elementBottom > containerBottom);
2259	},
2260
2261	getCoord: function(item) {
2262		var itemCoord = false;
2263		if (typeof item == 'string') item = $(item);
2264		if (typeof item == 'object' && !Array.isArray(item))
2265			{
2266			 if (item instanceof jQuery) item = item[0];
2267 			 if (item instanceof HTMLElement)
2268			 	{
2269				 itemCoord = [$(item).offset()];
2270				 var w = $(item).outerWidth(),
2271				 	 h = $(item).outerHeight();
2272				}
2273				else
2274				{
2275				 itemCoord = [{top:_.pageY, left:_.pageX}];
2276				 var w = 0,
2277				 	 h = 0;
2278				}
2279			 itemCoord[1] = {
2280			 	left: itemCoord[0].left + w,
2281			 	top: itemCoord[0].top + h
2282			 };
2283			}
2284			else if (Array.isArray(item))
2285			{
2286			 itemCoord = item;
2287			}
2288		return itemCoord;
2289	},
2290
2291	getPosition: function(item, outsideOf, insideOf, options) {
2292		var itemCoord = _.getCoord(item), // item à placer
2293			outCoord = _.getCoord(outsideOf), // autour duquel ?
2294			inCoord = _.getCoord(insideOf),
2295			win = {
2296				h: $(window).height(),
2297				w: $(window).width(),
2298				top: options.window? 0 : $(window).scrollTop(),
2299				left: options.window? 0 : $(window).scrollLeft(),
2300			};
2301
2302		if (itemCoord)
2303			{
2304			 if (!options) options = {};
2305			 else options = $.extend(true, {}, options);
2306			 if (!options.prio) options.prio = 'bottom'; // right, top
2307			 if (!options.pos) options.pos = 'half'; // // top, bottom
2308			 if (!options.padding) options.padding = 0;
2309
2310			 if (!Array.isArray(options.prio))
2311			 	{
2312				 if (options.prio=='bottom') options.prio = ['bottom', 'top', 'left', 'right'];
2313				 else if (options.prio=='right') options.prio = ['right', 'left', 'top', 'bottom'];
2314				 else if (options.prio=='left') options.prio = ['left', 'right', 'bottom', 'top'];
2315				 else if (options.prio=='top') options.prio = ['top', 'bottom', 'left', 'right'];
2316				 options.prio.push('bottomRight');
2317				 options.prio.push('bottomLeft');
2318				 options.prio.push('topLeft');
2319				}
2320				else options.prioOnly = true;
2321			 if (options.over) options.prio.push('over');
2322
2323			 // prio bas, puis droite, puis gauche, puis haut
2324			 var propItem = (options.pos=='half')? 0.5 : 0,
2325			 	 w = $(item).outerWidth(),
2326			 	 h = $(item).outerHeight();
2327
2328			 // ajuster dans quel cadre on peut placer l'élément
2329			 if (inCoord)
2330			 	{
2331			 	 if (inCoord[0].left<win.left) inCoord[0].left = win.left;
2332			 	 if (inCoord[0].top<win.top) inCoord[0].top = win.top;
2333			 	 if (inCoord[1].left>win.left+win.w) inCoord[1].left = win.left+win.w;
2334			 	 if (inCoord[1].top>win.top+win.h) inCoord[1].top = win.top+win.h;
2335				}
2336				else
2337				{
2338				 inCoord = [ win, {left:win.w+win.left, top:win.h+win.top} ];
2339				}
2340			 if (!outCoord)
2341			 	{
2342			 	 outCoord = [ inCoord[1], inCoord[0], ];
2343				}
2344			 var outW = outCoord[1].left - outCoord[0].left,
2345			 	 outH = outCoord[1].top - outCoord[0].top;
2346
2347			 var tests = {
2348			 	left:	{length:w, compare:outCoord[0].left - inCoord[0].left,	pos:{left:outCoord[0].left - w - options.padding,	top:outCoord[0].top + outH*propItem - h*propItem}},
2349			 	right:	{length:w, compare:inCoord[1].left - outCoord[1].left,	pos:{left:outCoord[1].left + options.padding, 		top:outCoord[0].top + outH*propItem - h*propItem}},
2350			 	top:	{length:h, compare:outCoord[0].top - inCoord[0].top, 	pos:{left:outCoord[0].left + outW*propItem - w*propItem,	top:outCoord[0].top - h - options.padding}},
2351			 	topLeft:{length:h, compare:outCoord[0].top - inCoord[0].top, 	pos:{left:outCoord[0].left - w + outW,	top:outCoord[0].top - h - options.padding}},
2352			 	bottom:	{length:h, compare:inCoord[1].top - outCoord[1].top,	pos:{left:outCoord[0].left + outW*propItem - w*propItem,	top:outCoord[1].top + options.padding}},
2353			 	bottomRight:{length:w, compare:inCoord[1].left - outCoord[1].left, pos:{left:outCoord[1].left + options.padding,	top:outCoord[1].top - h}},
2354			 	bottomLeft:	{length:w, compare:outCoord[0].left - inCoord[0].left, pos:{left:outCoord[0].left - w - options.padding,	top:outCoord[1].top - h}},
2355			 };
2356
2357			 if (options.over)
2358			 	{
2359				 tests.over =	{length:0, compare:outCoord[1].top - outCoord[0].top, 	pos:{
2360					left:inCoord[0].left + (inCoord[1].left - inCoord[0].left - w)/2 + options.padding,
2361					top: inCoord[0].top + (inCoord[1].top - inCoord[0].top - h)/2
2362				 }};
2363				}
2364
2365			 var enoughtSpace = [];
2366
2367			 for (var i=0; i<options.prio.length; i++)
2368			 	{
2369			 	 var test = tests[options.prio[i]];
2370				 if (test.compare>0 && test.length<=test.compare)
2371			 	 	{
2372					 enoughtSpace.push({direction: options.prio[i], pos:test.pos});
2373			 	 	 // check inside
2374			 	 	 if ((inCoord[0].left<= test.pos.left && (inCoord[1].left>= test.pos.left+w || (options.prio[i]=="topLeft")) &&
2375			 	 	 	 inCoord[0].top<= test.pos.top && inCoord[1].top>= test.pos.top+h) || !test.pos)
2376			 	 	 	 {
2377			 		 	  return {direction: options.prio[i], pos:test.pos};
2378						 }
2379					}
2380				}
2381			 if (!options.prioOnly)
2382			 	{
2383				 if (options.pos!='half')
2384				 	{
2385					 options.pos = 'half';
2386					 return _.getPosition(item, outsideOf, insideOf, options);
2387					}
2388				 	else if (!options.over)
2389				 	{
2390					 options.over = true;
2391					 return _.getPosition(item, outsideOf, insideOf, options);
2392					}
2393				}
2394			 // rien ne rentre, on applique la position prioritaire, tant pis
2395			 var test = tests[options.prio[0]];;
2396			 return {direction: options.prio[0], pos:test.pos};
2397			}
2398
2399
2400	},
2401
2402	unique: function(a) { return a.filter(function(e, i, ar) { return i==ar.indexOf(e); }); },
2403
2404
2405	readLocation: function() {
2406		return {
2407			params: $.deparam(window.location.search),
2408			path: window.location.pathname,
2409		};
2410	},
2411
2412	pushState: function(state) {
2413		var url = _.readLocation();
2414		window.history.pushState(state,"", url.path+"?"+$.param($.extend(url.params, state)));
2415	},
2416	removeUrlParam: function(toremove) {
2417		var url = _.readLocation();
2418		for (let rmv of toremove)
2419			{
2420			 delete url.params[rmv];
2421			}
2422		window.history.replaceState({}, "", url.path+"?"+$.param(url.params));
2423	},
2424
2425	url:function(q) {
2426		if (typeof q!=='string') { return {}; }
2427		q = q.trim().replace(/^(\?|#)/, '');
2428		if (!q) { return {}; }
2429		return q.trim().split('&').reduce(function (k, v) {
2430			var parts = v.replace(/\+/g, ' ').split('=');
2431			var key = parts[0];
2432			var val = parts[1];
2433
2434			key = decodeURIComponent(key);
2435
2436			val = val === undefined? null : decodeURIComponent(val);
2437			if (!k.hasOwnProperty(key)) k[key] = val;
2438			else if (Array.isArray(k[key])) k[key].push(val);
2439			else k[key] = [k[key], val];
2440			return k;
2441		}, {});
2442	},
2443
2444	isEqual: function (a, b, ignoreKeys) {
2445		var kA = Object.keys(a),
2446			kB = Object.keys(b);
2447		if (kA.length!=kB.length) return false;
2448
2449		var res = true;
2450		$.each(kA, function(ikey, key) {
2451			if (!ignoreKeys || ignoreKeys.indexOf(key)==-1)
2452				{
2453				 var vA = a[key],
2454					 vB = b[key],
2455					 isObj = vA!=null && typeof vA==="object" && vB!=null && typeof vB==="object"? true : false;
2456
2457				 if ((isObj && !_.isEqual(vA, vB)) || (!isObj && vA!==vB))
2458					{
2459		    		 res = false;
2460		    		 return false;
2461					}
2462				}
2463		});
2464
2465		return res;
2466	},
2467	intval: function(n) {
2468		if (!n) return 0;
2469		if (n===true) return 1;
2470		if (Array.isArray(n)) return n.map(function(value) { return _.intval(value); });
2471		if (typeof n !='string' && isNaN(n)) return 0;
2472		return parseInt(n);
2473	},
2474	doubleval: function(n) {
2475		if (!n) return 0;
2476		if (typeof n !='string' && isNaN(n)) return 0;
2477		if (typeof n === 'number') return n;
2478		n = n.replace(/ /, '').replace(/,/, '.');
2479		return parseFloat(n);
2480	},
2481	clamp: function(val, min, max) {
2482		return Math.min(Math.max(val, min), max);
2483	},
2484	mod:(n, m) => ((n % m) + m) % m,
2485
2486	funcList : {
2487		round:function(param){return parseFloat(param[0]).toFixed(param[1]);},
2488		num:function(param) {return parseFloat(param[0]);},
2489		number:function(param, dec) {if (typeof dec == "undefined") dec=2; return number_format(parseFloat(param[0]), dec, ",", " ");},
2490		price:function(param){return number_format(parseFloat(param[0]), 2, ",", " ")+" "+(_.cfg.currency || '&euro;');}, //.toFixed(2).replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, " ")
2491		date:function(param){return moment(param[0]).format(param[1]);},
2492		lower:function(param){return param[0]? param[0].toLowerCase() : '';},
2493		upper:function(param){return param[0]? param[0].toUpperCase() : '';},
2494		ucfirst:function(param){if (param[0]) param[0].ucfirst(); return param[0]? param[0] : '';},
2495		ucwords:function(param){if (param[0]) param[0].ucwords(); return param[0]? param[0] : '';},
2496		repeat:function(param){
2497			var str = '';
2498			for(var ir=0; ir<param[1]; ir++) str+= param[0];
2499			return str;
2500		},
2501		substr:function(param){return param[0]? param[0].substring(param[1], param[2]) : '';},
2502		calc:function(param){return eval(param[0]);},
2503		'if':function(param, Datas){
2504			if (param[0]) return param[1];
2505			else return param[2];
2506		},
2507	},
2508	price: function(p) {return this.funcList.price([p]); },
2509	number:  function(p, dec) {return this.funcList.number([p], dec); },
2510	formatDate: function(value, format) {
2511		if (value)
2512			{
2513			 if (!format) format = 'short';
2514			 var m = moment(value);
2515			 if (format=='full')
2516			 	{
2517			 	 day = 'dddd D MMMM';
2518				}
2519				else day = "DD MMM";
2520			 if (m.format("YY")==moment().format("YY")) value = m.format(day);
2521			 else value = m.format(day+" YYYY");
2522			}
2523		return value;
2524	},
2525	formatDateTime: function(value, format) {
2526		if (value)
2527			{
2528			 if (!format) format = 'short';
2529			 var m = moment(value);
2530			 if (format=='full')
2531			 	{
2532			 	 day = 'dddd D MMMM';
2533				}
2534				else
2535				{
2536				 day = "D MMM";
2537				}
2538			 var m = moment(value, ["DD-MM-YYYY", "YYYY-MM-DD", "DD-MM-YYYY HH:mm", "YYYY-MM-DD HH:mm"]);
2539			 if (m.format("YY")==moment().format("YY")) value = m.format(day);
2540			 else value = m.format(day+" YYYY");
2541			 if (m.format("HH:mm")!="00:00")
2542				{
2543				 value += " - "+m.format("H")+"h";
2544				 if (m.format("mm")!="00") value+= m.format("mm");
2545				}
2546			}
2547		return value;
2548	},
2549	formatTime: function(timestamp, format = "", hourPerDay = 24, style = "") {
2550		let seconds = timestamp % 60;
2551		timestamp = Math.floor(timestamp / 60);
2552		let minutes = timestamp % 60;
2553		if (seconds > 10) minutes++;
2554		let totalHours = Math.floor(timestamp / 60);
2555		let hours = format==="d|Hhm?|m:s" ? totalHours % hourPerDay : totalHours;
2556		let days = format==="d|Hhm?|m:s" ? Math.floor(totalHours / hourPerDay) : 0;
2557
2558		let pad = n => String(n).padStart(2, "0");
2559		let plural = (n, w) => n + " " + w + (n > 1 ? "s" : "");
2560
2561		switch (format) {
2562			case "Hhm?": return minutes ? `${hours}h${pad(minutes)}` : `${hours}h`;
2563			case "Hh": return `${hours}h`;
2564			case "h:m?": return minutes ? `${hours}:${pad(minutes)}` : `${hours}`;
2565			case "d|Hhm?|m:s":
2566				if (days && hours) return `${plural(days, "jour")} ${hours}h`;
2567				if (days) return plural(days, "jour");
2568				if (hours) return minutes ? `${hours}h${pad(minutes)}` : `${hours}h`;
2569				return style === "full"
2570					? `${plural(minutes, "minute")}` + (seconds ? pad(seconds) : "")
2571					: `${minutes}:${pad(seconds)}`;
2572			default: return `${hours}h${pad(minutes)}`;
2573		}
2574	},
2575	formatFileSize: function(size, considerZeroAsMissing) {
2576		if(typeof considerZeroAsMissing === "undefined") considerZeroAsMissing = true;
2577
2578		if (size>900000000) size = Math.round(size/107374182)/10+" Go";
2579		else if (size>900000) size = Math.round(size/104857)/10+" Mo";
2580		else if (size>900) size = Math.round(size/102.4)/10+" Ko";
2581		else if (size>0) size = size+" o";
2582		else if (considerZeroAsMissing && (!size || size==-1)) size = "<notag class='error'>File is missing</notag>";
2583
2584		return size;
2585	},
2586
2587	checkHour: function(h) {
2588
2589		if (h.length==5) var h = h.substring(0,2)+":"+h.substring(3, 5); // hh:mm
2590		else if (h.length==4)
2591			{
2592			 if (parseInt(h.substring(0,2)) && !parseInt(h[2])) h = h.substring(0,2)+":0"+parseInt(h[3]); // hh:m
2593			 else  h = h.substring(0,2)+":"+h.substring(2,4); // hhmm
2594			}
2595			else if (h.length<4 && h)
2596			{
2597			 //if (h.length==2 && h.match(/^[0-9]{2}/)) h = parseInt(h); // hh:
2598			 if (h.length==3 && !parseInt(h[1]))
2599				{
2600				 h = "0"+parseInt(h[0])+":0"+parseInt(h[2]); // h:m
2601				}
2602				else if (h.length==3 && ((parseInt(h.substring(1))+"").length==2 || ((parseInt(h.substring(1))+"").length==1 && h[1]=="0" && parseInt(h[2]))))
2603				{ // hmm
2604				 h = "0"+parseInt(h.substring(0,1))+":"+parseInt(h.substring(1));
2605				}
2606				else
2607				{ // hh ou h
2608				 h = ('00' + parseInt(h.substring(0,2))).slice(-2)+":00";
2609				}
2610			}
2611		return h;
2612	},
2613
2614	getHolidays: function(year) {
2615		if (!year) year = moment().format('YYYY');
2616
2617		// jours fixes
2618		_.holidays = {};
2619		_.holidays[year] = {};
2620		_.holidays[year][year+'-01-01'] = true; // 1er janvier
2621		_.holidays[year][year+'-05-01'] = true; // Fête du travail
2622		_.holidays[year][year+'-05-08'] = true; // Victoire des alliés
2623		_.holidays[year][year+'-07-14'] = true; // Fête nationale
2624		_.holidays[year][year+'-08-15'] = true; // Assomption
2625		_.holidays[year][year+'-11-01'] = true; // Toussaint
2626		_.holidays[year][year+'-11-11'] = true; // Armistice
2627		_.holidays[year][year+'-12-25'] = true; // Noël
2628
2629		 var easterDates = {
2630		 	2014:'04-20', 2015:'04-05', 2016:'03-27', 2017:'04-16', 2018:'04-01', 2019:'04-21', 2020:'04-12', 2021:'04-04', 2022:'04-17', 2023:'04-09', 2024:'03-31', 2025:'04-20',
2631			2026:'04-05', 2027:'03-28', 2028:'04-16', 2029:'04-01', 2030:'04-21', 2031:'04-13',  2032:'03-28',   2033:'04-17'
2632		 };
2633		 var easter = moment(year+"-"+easterDates[year]);
2634
2635		 _.holidays[year][moment(easter).add(1, 'day').format("YYYY-MM-DD")] = true; // pâques lui-même.
2636		 _.holidays[year][moment(easter).add(39, 'day').format("YYYY-MM-DD")] = true; // Ascension
2637		 _.holidays[year][moment(easter).add(50, 'day').format("YYYY-MM-DD")] = true; // lundi de pentecôte
2638
2639	},
2640
2641	isHoliday: function(date) {
2642		var d = moment(date);
2643		var year = d.format('YYYY');
2644		if (!_.holidays || !_.holidays[year]) _.getHolidays(year);
2645		return _.holidays[year][d.format('YYYY-MM-DD')]? true : false;
2646	},
2647
2648	organizeGrid: function(items) {
2649		var itemCols = [[]],
2650			 nbCols = 1,
2651			 colW = 100,
2652			 ic=0,
2653			 ii=0,
2654			 nbitem=0,
2655			 freespace = true,
2656			 sum=0;
2657
2658		$.each(items, function(it, item) {
2659			item.endMinutes = item.startMinutes+item.duration;
2660
2661			var len = item.duration;
2662
2663			sum+= len;
2664
2665			for(ic=0; ic<nbCols; ic++)
2666				{
2667				 freespace = true;
2668				 nbitem = itemCols[ic].length;
2669				 for(ii=0; ii<nbitem; ii++)
2670					{
2671					 item2 = itemCols[ic][ii];
2672					 if ((item2.startMinutes <= item.startMinutes && item2.endMinutes > item.startMinutes) || (item2.startMinutes >= item.startMinutes && item2.startMinutes < item.endMinutes))
2673						{
2674						 freespace = false;
2675						 // Intersection, on ajoute une colonne ? ou on passe à la suivante.
2676						 if (ic>=nbCols-1)
2677							{
2678							 colW = Math.floor(100/(nbCols+1));
2679							 for (var icr=0; icr<nbCols; icr++)
2680								{
2681								 var nbitemR = itemCols[icr].length;
2682								 for (var iir=0; iir<nbitemR; iir++)
2683									{
2684									 itemCols[icr][iir].width = colW-2;
2685									 itemCols[icr][iir].left = icr*colW;
2686									}
2687								}
2688							 itemCols[nbCols] = [];
2689							 nbCols++;
2690							}
2691						 break;
2692						}
2693					}
2694				 if (freespace) break;
2695				}
2696			itemCols[ic][ii] = item;
2697
2698			item.width = colW-2;
2699			item.left = ic*colW;
2700
2701		 });
2702
2703		for (var ic=0; ic<nbCols; ic++)
2704			{
2705			 nbitem = itemCols[ic].length;
2706			 for(var ii=0; ii<nbitem; ii++)
2707				{
2708				 var colSpan = 1;
2709				 var item = itemCols[ic][ii];
2710				 for (var ic2=ic+1; ic2<nbCols; ic2++)
2711					{
2712					 var nbitem2 = itemCols[ic2].length;
2713					 freespace = true;
2714					 for (var ii2=0; ii2<nbitem2; ii2++)
2715						{
2716						 item2 = itemCols[ic2][ii2];
2717						 if ((item2.startMinutes <= item.startMinutes && item2.endMinutes > item.startMinutes)
2718						 	|| (item2.startMinutes >= item.startMinutes && item2.startMinutes < item.endMinutes))
2719							 {
2720							  freespace = false;
2721							  break;
2722							 }
2723						}
2724					 if (freespace) colSpan++;
2725					 else break;
2726					}
2727				 if (colSpan>1)
2728					{
2729					 item.width = colW*colSpan - 2
2730					 item.left = ic*colW;
2731					}
2732				}
2733			}
2734
2735		return itemCols;
2736	},
2737
2738	picAsCanvas: function(pic) {
2739		var canvas = _.ce('canvas');
2740		canvas.width = pic.width;
2741		canvas.height = pic.height;
2742		canvas.getContext('2d').drawImage(pic, 0, 0, pic.width, pic.height);
2743		return canvas;
2744	},
2745
2746	getPicPix: function(pic, x, y) {
2747		if (!x) x = 0;
2748		if (!y) y = 0;
2749		var canvas = _.picAsCanvas(pic);
2750		var pix = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
2751		return pix;
2752	},
2753
2754	addCacheDate: function(d, type) {
2755		if (d)
2756			{
2757			 if (!_.cache.dates) _.cache.dates = [];
2758			 var m = moment(d, ["DD-MM-YYYY", "YYYY-MM-DD", "DD-MM-YYYY HH:mm", "YYYY-MM-DD HH:mm"]);
2759			 _.cache.dates.push({date:m.format('YYYY-MM-DD HH:mm'), type:type? parseInt(type) : 0});
2760			}
2761	},
2762
2763	getCacheDates: function(max, wHour) {
2764		var dates = [];
2765		if (!max) max = 8;
2766		if (_.cache.dates)
2767			{
2768			 _.cache.dates.sort(function(a, b) {
2769				if (a.type==b.type)
2770					{
2771					 if (a.date==b.date) return 0;
2772					 return a.date<b.date? -1 : 1;
2773					}
2774					else return a.type<b.type? 1 : -1;
2775			 });
2776			 var nbByDate = {};
2777			 $.each(_.cache.dates, function(i, d) {
2778				var date = moment(d.date).format(wHour? 'YYYY-MM-DD HH:mm' : 'YYYY-MM-DD');
2779				if (!nbByDate[date]) nbByDate[date] = {date:date, type:d.type? d.type : 0, nb:0};
2780				nbByDate[date].nb++;
2781			 });
2782			 $.each(nbByDate, function(date, d) {
2783				dates.push(d);
2784			 });
2785			 dates.sort(function(a,b) {
2786				if (a.type==b.type)
2787					{
2788					 if (a.type>0)
2789						{
2790						 if (a.date==b.date) return 0;
2791						 return a.date<b.date? -1 : 1;
2792						}
2793						else
2794						{
2795						 if (a.nb==b.nb) return 0;
2796						 return a.nb<b.nb? -1 : 1;
2797						}
2798					}
2799					else return a.type<b.type? 1 : -1;
2800			 });
2801
2802			 dates = dates.slice(0, max);
2803
2804			 dates.sort(function(a,b) {
2805				if (a.type==b.type)
2806					{
2807					 if (a.date==b.date) return 0;
2808					 return a.date<b.date? -1 : 1;
2809					}
2810					else return a.type<b.type? 1 : -1;
2811			 });
2812
2813			 return dates;
2814			}
2815		return false;
2816	},
2817
2818
2819	genId: function(nb) {
2820		var r='', pool='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', pnb = pool.length;
2821		for (var i=0; i<nb; i++) r+= pool.charAt(Math.floor(Math.random()*pnb));
2822		return r;
2823	},
2824
2825	actions: function(actions, datas, ev, context) {
2826		if (!Array.isArray(actions)) actions = [actions];
2827		var acts = {actions:actions};
2828		if (context) acts.actions.splice(0, 0, {context:context});
2829		if (datas) acts.datas = datas;
2830		if (ev) acts.event = ev;
2831		$('<div/>').jAction(acts);
2832	},
2833
2834	base64Encode: function (str) {
2835		return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
2836			function toSolidBytes(match, p1) {
2837				return String.fromCharCode('0x' + p1);
2838		}));
2839	},
2840	base64Decode: function (str) {
2841		return decodeURIComponent(atob(str).split('').map(function(c) {
2842            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
2843        }).join(''));
2844	},
2845
2846	isElementFocusable(element) {
2847		if (element instanceof jQuery) {
2848			element = element.get(0);
2849		}
2850
2851		if(!element) return false;
2852
2853		if ($(element).is(":hidden") || $(element).is(":disabled") ) {
2854			return false;
2855		}
2856
2857		let tabIndex = parseInt($(element).attr("tabindex"));
2858		tabIndex = isNaN(tabIndex) ? -1 : tabIndex;
2859		return $(element).is( ":input, a[href], area[href], iframe" ) || tabIndex > -1;
2860	},
2861
2862	getEntities: function () {
2863		var jobLoadEntities = Job.getUnique('loadEntities', function () {
2864			return new Job(function (job) {
2865				if (_.entities) {
2866					job.success(_.entities);
2867				} else {
2868					_.actions({
2869						route: 'cfg/charter//get-entities',
2870						data: {},
2871						then: function(data) {
2872							_.entities = data.entities;
2873							job.success(_.entities);
2874						}
2875					});
2876				}
2877			});
2878        });
2879
2880        return jobLoadEntities.getPromise();
2881	},
2882
2883	getCachedList: function (code) {
2884		if (!code.includes('::')) code = 'std::'+code;
2885		if (!(code in this.cache.lists.values)) return undefined;
2886		return this.cache.lists.values[code];
2887	},
2888
2889	getCachedStdList: function (code) {
2890		return this.getCachedList('std::'+code);
2891	},
2892
2893	getCachedSpecDataList: function (code) {
2894		return this.getCachedList('spec::'+code);
2895	},
2896
2897	getList: function (query, defaultType, forceReload) {
2898		if (typeof defaultType == 'undefined') defaultType = 'std';
2899
2900		if (!Array.isArray(query)) query = query.split(',');
2901
2902		if (query.length == 0) return Promise.resolve([]);
2903
2904		query = query.map((item) => {
2905			let outputKey = null;
2906
2907			if (typeof item == 'string') {
2908				let type = defaultType;
2909				let code = item;
2910
2911				outputKey = code;
2912
2913				if (code.includes('::')) {
2914					let parts = item.split('::', 2);
2915					type = parts[0];
2916					code = parts[1];
2917				}
2918				item = {
2919					code: code,
2920					type: type,
2921				};
2922			} else {
2923				if (!('type' in item)) item.type = defaultType;
2924				outputKey = item.outputKey || item.code;
2925			}
2926
2927			if (!('outputKey' in item)) {
2928				item.outputKey = outputKey;
2929			}
2930
2931			if (!('cacheKey' in item)) {
2932				if ('params' in item) {
2933					item.cacheKey = this.uid();
2934					item.noCache = true;
2935				} else {
2936					item.cacheKey = item.type+'::'+item.code;
2937				}
2938			}
2939
2940			return item;
2941		});
2942
2943		let types = {};
2944		for (let item of query) {
2945			types[item.type] = true;
2946		}
2947		types = Object.keys(types);
2948
2949		let toLoadQuery = [];
2950		if (forceReload) {
2951			toLoadQuery = query;
2952		} else {
2953			for (let item of query) {
2954				if (item.noCache || !(item.cacheKey in this.cache.lists.loaders)) toLoadQuery.push(item);
2955			}
2956		}
2957
2958		if (toLoadQuery.length > 0) {
2959			let ajaxData = {
2960				query: toLoadQuery,
2961			};
2962			let loader = new Promise((resolve, reject) => {
2963				_.actions({route: 'ajax/list//multi', dataFormat: 'json', data:ajaxData, then: (data) => {
2964					for (let item of Object.values(data)) {
2965						this.cache.lists.values[item.query.cacheKey] = item.result || undefined;
2966					}
2967					resolve(data);
2968				}});
2969			});
2970			for (let item of toLoadQuery) {
2971				this.cache.lists.loaders[item.cacheKey] = loader;
2972			}
2973		}
2974
2975		let promises = [];
2976		for (let item of query) {
2977			promises.push(this.cache.lists.loaders[item.cacheKey]);
2978		}
2979		return new Promise((resolve, reject) => {
2980			Promise.all(promises).then((outputs) => {
2981				let result = {};
2982
2983				for (let output of outputs) {
2984					for (let item of Object.values(output)) {
2985						result[item.query.outputKey] = item.result;
2986					}
2987				}
2988
2989				if (query.length == 1) resolve(Object.values(result)[0]);
2990				else resolve(result);
2991			});
2992		});
2993	},
2994
2995	getStdList: function (codes, forceReload) {
2996		return _.getList(codes, forceReload);
2997	},
2998
2999	getSpecDataList: function (codes, forceReload) {
3000		if (!Array.isArray(codes)) codes = codes.split(',');
3001		codes = codes.map((code) => {
3002			if (!code.includes('::')) code = 'spec::'+code;
3003			return code;
3004		});
3005		return _.getList(codes, forceReload);
3006	},
3007
3008	getStyleSets: function () {
3009		var uniqueJob = Job.getUnique('loadStyleSets', function () {
3010			return new Job(function (job) {
3011				if (_.styleSets) {
3012					job.success(_.styleSets);
3013				} else {
3014					_.actions({
3015						route: 'cfg/charter//get-styleSets',
3016						data: {},
3017						then: function(data) {
3018							_.styleSets = data.styleSets;
3019							job.success(_.styleSets);
3020						}
3021					});
3022				}
3023			});
3024        });
3025
3026        return uniqueJob.getPromise();
3027	},
3028
3029
3030	getDetailedStyleSets: function (forceReload) {
3031		if (forceReload) {
3032			_.styleSetsDetailed = null;
3033			Job.clearUnique('loadDetailedStyleSets');
3034		}
3035
3036		var uniqueJob = Job.getUnique('loadDetailedStyleSets', function () {
3037			return new Job(function (job) {
3038				if (_.styleSetsDetailed) {
3039					job.success(_.styleSetsDetailed);
3040				} else {
3041					_.actions({
3042						route: 'cfg/style//getList',
3043						data: {},
3044						then: function(data) {
3045							_.styleSetsDetailed = data.styleSets;
3046							job.success(_.styleSetsDetailed);
3047						}
3048					});
3049				}
3050			});
3051        });
3052
3053        return uniqueJob.getPromise();
3054	},
3055
3056	resolveStyleSetValue: function (value, filters) {
3057		if (_.isStyleSetReferenceValue(value)) {
3058			return this.getStyleSetValue(value.__styleSetKey, filters);
3059		}
3060
3061		return Promise.resolve(value);
3062	},
3063
3064	isStyleSetReferenceValue: function (value) {
3065		return null !== value && typeof value == 'object' && '__styleSetKey' in value;
3066	},
3067
3068	getStyleSetReferenceValue: function (key) {
3069		return {__styleSetKey: key};
3070	},
3071
3072	getStyleSetValue: function (key, filters) {
3073		if (!filters) filters = {};
3074
3075		return new Promise((resolve, reject) => {
3076			this.getDetailedStyleSets().then((styleSets) => {
3077				let sources = [];
3078				if (filters.styleSetNr) {
3079					sources.push(filters.styleSetNr);
3080				} else if (filters.entityNr) {
3081					for (let styleSetNr of (styleSets.entityDefault[filters.entityNr] || [])) {
3082						sources.push(styleSetNr);
3083					}
3084				} else {
3085					for (let styleSetNr of (styleSets.globalDefault || [])) {
3086						sources.push(styleSetNr);
3087					}
3088				}
3089
3090				for (let styleSetNr of sources) {
3091					let styleSetData = styleSets.data[styleSetNr];
3092					if (key in styleSetData) {
3093						resolve($.extend({key: key}, styleSetData[key]));
3094					}
3095				}
3096
3097				resolve(null);
3098			});
3099		});
3100	},
3101
3102	getCharterColors: function () {
3103		var jobLoadCharterColors = Job.getUnique('loadCharterColors', function () {
3104			return new Job(function (job) {
3105				if (_.charterColors) {
3106					job.success(_.charterColors);
3107				} else {
3108					_.actions({
3109						route: 'cfg/charter//get-colors',
3110						data: {},
3111						then: function(data) {
3112							_.charterColors = data.colors;
3113							job.success(_.charterColors);
3114						}
3115					});
3116				}
3117			});
3118        });
3119
3120        return jobLoadCharterColors.getPromise();
3121	},
3122
3123	toHSL(rgb) { // copied from js/inc/color.js (for quick use without ressource)
3124		let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255;
3125
3126		let cmin = Math.min(r,g,b), cmax = Math.max(r,g,b),
3127			delta = cmax - cmin,
3128			h = 0, s = 0, l = 0;
3129
3130		if (delta == 0) h = 0;
3131		else if (cmax == r) h = ((g - b) / delta) % 6;
3132		else if (cmax == g) h = (b - r) / delta + 2;
3133		else h = (r - g) / delta + 4;
3134
3135		h = Math.round(h * 60);
3136		if (h < 0) h += 360;
3137		l = (cmax + cmin) / 2;
3138		s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
3139		s = +Math.round(s * 100);
3140		l = +Math.round(l * 100);
3141
3142		return [h, s, l];
3143    },
3144
3145	hexToRGB(hex) {
3146        let result = /^#?(?:([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})|([a-f\d])([a-f\d])([a-f\d]))/i.exec(hex);
3147        return result ? {
3148            r: parseInt(result[1] || result[4].repeat(2), 16),
3149            g: parseInt(result[2] || result[5].repeat(2), 16),
3150            b: parseInt(result[3] || result[6].repeat(2), 16)
3151        } : {r: 255, g: 255, b: 255};
3152    },
3153
3154	getScript: function(url, callback) {
3155		window.dependencyLoader.loadScripts([url]).then(()=>{
3156			if (callback) callback();
3157		});
3158	},
3159
3160	addContacts: function (contacts){
3161		if (contacts)
3162			{
3163			 if (!_.cache.Contacts) _.cache.Contacts = {};
3164			 $.each (contacts, (nr, c)=>{
3165				if (!_.cache.Contacts[c.nr]) _.cache.Contacts[c.nr] = c;
3166			 });
3167			}
3168	},
3169	addUsers: function(users){
3170		if (users)
3171			{
3172			 $.each (users, (nr, u)=>{
3173			 	if (!+u.nr && nr) u.nr = +nr;
3174				if (!_.Users[+u.nr])
3175					{
3176					 if (u.firstname) u.fn = u.firstname;
3177					 if (u.lastname) u.ln = u.lastname;
3178					 _.Users[+u.nr] = {
3179					 	nr: +u.nr,
3180					 	firstname:u.fn,
3181					 	lastname:u.ln,
3182						str: u.fn+' '+u.ln,
3183					 	name:u.fn+' '+u.ln,
3184					 	pic:u.pic,
3185						func:'',
3186						cpy:'',
3187					 	active:u.active,
3188						assignment:u.assignment || ''
3189					 };
3190					}
3191			 });
3192			}
3193	},
3194
3195	getUsers: function(teamNr, callBack) {
3196		if ((!teamNr && !_.flags.usersAllLoaded) || (teamNr && (!_.cache.teams || !_.cache.teams[teamNr])))
3197			{
3198			 _.actions({route:'user/usr//get-list', data:{teamnr:teamNr}, then:function(data) {
3199			 	_.addUsers(data.users);
3200				if (!teamNr) _.flags.usersAllLoaded = true;
3201
3202				// A faire : compléter le système pour les teams
3203
3204				if (callBack) callBack();
3205			 }});
3206			}
3207			else
3208			{
3209			 if (callBack) callBack();
3210			}
3211
3212	},
3213	isElementInside: function (element, containers) {
3214		if (typeof containers != 'object' || !('length' in containers)) {
3215			containers = [containers];
3216		}
3217
3218		if (element instanceof jQuery) element = element.get(0);
3219		if (!element || !(element instanceof HTMLElement)) return false;
3220
3221		for (let container of containers) {
3222			if (container instanceof jQuery) container = container.get(0);
3223			if (!container || !(container instanceof HTMLElement)) continue;
3224
3225			if (element == container || -1 != $.inArray(container, $(element).parents())) {
3226				return true;
3227			}
3228		}
3229
3230		return false;
3231	},
3232	logEventAction: function (event, actionCode, data) {
3233		if ('originalEvent' in event) event = event.originalEvent;
3234
3235		if (!('__actions__' in event)) {
3236			event.__actions__ = [];
3237		}
3238		event.__actions__.push({code: actionCode || 'default', data: data || {}});
3239	},
3240	hasEventAction: function (event, actionCode) {
3241		if ('originalEvent' in event) event = event.originalEvent;
3242
3243		if (!('__actions__' in event)) return false;
3244
3245		if (typeof actionCode == 'undefined') return event.__actions__.length > 0;
3246
3247		return event.__actions__.findIndex((e) => e.code == actionCode) >= 0;
3248	},
3249	getEventActions: function (event, actionCode) {
3250		if ('originalEvent' in event) event = event.originalEvent;
3251
3252		if (!('__actions__' in event)) return [];
3253
3254		if (typeof actionCode == 'undefined') return event.__actions__;
3255
3256		return event.__actions__.filter((e) => e.code == actionCode);
3257	},
3258	clearDetachedWYEditors: function () {
3259		if (typeof tinymce != 'object' || !('editors' in tinymce)) return;
3260
3261		for (let editor of Object.values(tinymce.editors)) {
3262			if (!editor.editorContainer || $(editor.editorContainer).closest('body').length > 0) continue;
3263
3264			tinymce.execCommand('mceRemoveEditor', true, editor.id);
3265		}
3266	},
3267	debounce: function (fn, delay) {
3268		let timeoutId;
3269		return function (...args) {
3270			clearTimeout(timeoutId);
3271			timeoutId = setTimeout(() => {
3272				fn(...args);
3273			}, delay);
3274		};
3275	},
3276	getDragService: function () {
3277		if (null === _.dragService) {
3278			_.dragService = new Promise((resolve, reject) => {
3279				window.dependencyLoader.loadModularScripts([{load: '/admin/inc/js/mod/drag/DragService.js'}]).then((modules) => {
3280					resolve(new modules.DragService());
3281				});
3282			});
3283		}
3284
3285		return _.dragService;
3286	},
3287
3288	// petite light factorisation, en attendant d'avoir un proper service dédié aux panels en général
3289	panel: {
3290		toggle: () =>{
3291			var $panel = $('.SidePanel');
3292			if ($panel.attr('state')=='close') _.panel.open();
3293			else _.panel.close();
3294		},
3295		open: () => {
3296			var $panel = $('.SidePanel');
3297			$panel.attr('state', 'open');
3298
3299		},
3300
3301		close: () => {
3302			var $panel = $('.SidePanel');
3303			$panel.attr('state', 'close');
3304		}
3305	},
3306
3307	closeToolPanels: function() {
3308		 setTimeout(function() {
3309			$('.contentPanel.tool-panel').each(function() {this.close();});
3310		 }, 100);
3311	},
3312
3313
3314	appModules: function () {
3315		if (!('loadAppModulesPromise' in _)) {
3316			_.loadAppModulesPromise = new Promise((resolve, reject) => {
3317				if (window.appModules) resolve(window.appModules);
3318				else {
3319					import('/admin/inc/js/mod/app/AppModuleService.js').then((importedObject) => {
3320						if (!'AppModuleService' in importedObject) {
3321							throw new Error('AppModuleService was not found after import');
3322						} else {
3323							window.appModules = new importedObject['AppModuleService']();
3324							resolve(window.appModules);
3325						}
3326					});
3327				}
3328			});
3329		}
3330		return _.loadAppModulesPromise;
3331	},
3332	loadModule: function (element, options, data, moduleId, forceReload) {
3333		if (typeof element=="string") element = $(element)[0];
3334		return new Promise((resolve, reject) => {
3335			_.appModules().then((appModules) => {
3336				appModules.load(element, options, data, moduleId, forceReload)
3337					.then((module) => resolve(module))
3338					.catch((error) => reject(error));
3339			});
3340		});
3341
3342	},
3343	calculator: function () {
3344		return new Promise((resolve, reject) => {
3345			window.dependencyLoader.loadScripts(['/admin/inc/js/math.js']).then(() => {
3346				let calculator = {
3347					stdFunctions: {
3348						price: function (result) {
3349							return _.price(result);
3350						},
3351						percent: function (result) {
3352							return result+' %';
3353						},
3354						formatTime: function (result) {
3355							return _.formatTime(result);
3356						},
3357					},
3358					customFunctions: {},
3359					math: math,
3360				};
3361				calculator.addFunction = (name, callback) => {
3362					if (typeof callback != 'function') throw new Error("callback must be a function");
3363					calculator.customFunctions[name] = callback;
3364				};
3365				calculator.sanitizeExpression = (expression) => {
3366					let sanitizedExpression = expression.toString().replaceAll(/(?:<>|&&|\|\|)/g, (match) => {
3367						switch (match) {
3368							case '<>': return '!=';
3369							case '&&': return 'and';
3370							case '||': return 'or';
3371						}
3372						return match;
3373					});
3374					sanitizedExpression = sanitizedExpression.replaceAll(/((?:[a-z_][a-z0-9_]*)(?:(?:\.|->)(?:[a-z_][a-z0-9_]*))*)/g, (match) => {
3375						return calculator.sanitizeParameterName(match);
3376					});
3377					return sanitizedExpression;
3378				};
3379				calculator.sanitizeParameters = (parameters) => {
3380					let sanitizedParameters = {};
3381					for (let [key, value] of Object.entries(parameters)) {
3382						sanitizedParameters[calculator.sanitizeParameterName(key)] = value;
3383					}
3384					return sanitizedParameters;
3385				};
3386				calculator.unsanitizeParameters = (parameters) => {
3387					let unsanitizedParameters = {};
3388					for (let [key, value] of Object.entries(parameters)) {
3389						unsanitizedParameters[calculator.unsanitizeParameterName(key)] = value;
3390					}
3391					return unsanitizedParameters;
3392				};
3393				calculator.sanitizeParameterName = (name) => {
3394					return name.replaceAll(/\./g, '___DOT___').replaceAll(/->/g, '___ARW___');
3395				};
3396				calculator.unsanitizeParameterName = (name) => {
3397					return name.replaceAll(/___DOT___/g, '.').replaceAll(/___ARW___/g, '->');
3398				};
3399				calculator.calculate = (expression, parameters, expose) =>  {
3400					if (typeof parameters == 'undefined') parameters = {};
3401
3402					if (typeof expression != 'object' || !('length' in expression)) {
3403						expression = [expression];
3404					}
3405
3406					let outputData = {};
3407					let previousOutput = '__previous__' in parameters ? parameters.__previous__ : undefined;
3408					let extendedParameters = $.extend({}, parameters, outputData, {__previous__: previousOutput || 0});
3409					extendedParameters = calculator.sanitizeParameters(extendedParameters);
3410					for (let expr of Object.values(expression)) {
3411						let parserExpression = typeof expr == 'string' ? expr : expr.expr;
3412
3413						let conditionResult = true;
3414						if (typeof expr == 'object' && 'cond' in expr) {
3415							conditionResult = !!calculator.calculate(expr.cond, calculator.unsanitizeParameters(extendedParameters));
3416							if (!conditionResult && 'else' in expr) {
3417								parserExpression = expr.else;
3418								conditionResult = true;
3419							}
3420						}
3421
3422						if (conditionResult) {
3423							let allFunctions = $.extend({}, calculator.stdFunctions, calculator.customFunctions);
3424							let allSymbols = $.extend({}, extendedParameters, allFunctions);
3425
3426							let whileResult = true;
3427							if (typeof expr == 'object' && 'while' in expr) {
3428								whileResult = !!calculator.calculate(expr.while, calculator.unsanitizeParameters(extendedParameters));
3429							}
3430
3431							const toEvaluateExpression = calculator.sanitizeExpression(parserExpression);
3432							const node = math.parse(toEvaluateExpression);
3433							let args = {};
3434							node.traverse((leaf, path, parent) => {
3435								if (leaf.type == 'SymbolNode') {
3436									if (!parent || parent.type != 'FunctionNode' || parent.name != leaf.name) {
3437										args[leaf.name] = true;
3438									}
3439								}
3440							});
3441							let hasMissingArguments = false;
3442							for (let arg of Object.keys(args)) {
3443								if (!(arg in allSymbols) || typeof allSymbols[arg] == 'undefined') {
3444									hasMissingArguments = true;
3445									break;
3446								}
3447							}
3448
3449							let iterations = 0;
3450							while (whileResult && iterations < 100) {
3451								iterations++;
3452
3453								const parser = math.parser();
3454								for (let [k, v] of Object.entries(extendedParameters)) {
3455									parser.set(k, v);
3456								}
3457								for (let [name, callback] of Object.entries(allFunctions)) {
3458									parser.set(name, callback);
3459								}
3460
3461								try {
3462									let result = hasMissingArguments ? undefined : parser.evaluate(toEvaluateExpression);
3463									if (typeof result == 'string' && parseFloat(result).toString() == result) result = parseFloat(result);
3464
3465									previousOutput = result;
3466									if (typeof expr == 'object' && 'name' in expr) {
3467										outputData[calculator.sanitizeParameterName(expr.name)] = result;
3468									}
3469									if (typeof expr == 'object' && 'push' in expr) {
3470										if (!(expr.push in outputData)) {
3471											outputData[calculator.sanitizeParameterName(expr.push)] = [];
3472										}
3473
3474										outputData[calculator.sanitizeParameterName(expr.push)].push(result);
3475									}
3476								} catch (e) {
3477									console.debug('Calculation failed', {
3478										expression: parserExpression,
3479										data: calculator.unsanitizeParameters(extendedParameters),
3480									});
3481									throw e;
3482								}
3483
3484								extendedParameters = $.extend({}, parameters, outputData, {__previous__: previousOutput});
3485								if (typeof expr == 'object' && 'while' in expr) {
3486									whileResult = !!calculator.calculate(expr.while, calculator.unsanitizeParameters(extendedParameters));
3487								} else {
3488									whileResult = false;
3489								}
3490							}
3491						}
3492					}
3493
3494					if (typeof expose == 'undefined') expose = true;
3495
3496					if (!expose || Object.keys(outputData).length == 0) return previousOutput;
3497
3498					if (typeof expose == 'object') {
3499						let exposedData = {};
3500						for (let key of Object.values(expose)) {
3501							if (!(key in outputData)) continue;
3502
3503							exposedData[key] = outputData[key];
3504						}
3505						outputData = exposedData;
3506					}
3507
3508					outputData['__output__'] = previousOutput;
3509					return calculator.unsanitizeParameters(outputData);
3510				}
3511				resolve(calculator);
3512			});
3513		});
3514	},
3515	calculate: function (expression, parameters, expose) {
3516		return new Promise((resolve, reject) => {
3517			_.calculator().then((calculator) => {
3518				resolve(calculator.calculate(expression, parameters, expose));
3519			});
3520		});
3521	},
3522};
3523
3524
3525// plugin jquery pour prendre une query string et en faire un objet.
3526
3527
3528(function ($) {
3529	var re = /([^&=]+)=?([^&]*)/g;
3530	var decode = function (str) {
3531		return decodeURIComponent(str.replace(/\+/g, ' '));
3532	};
3533
3534	$.deparam = function( params, coerce ) {
3535		var obj = {},
3536			coerce_types = { 'true': !0, 'false': !1, 'null': null };
3537
3538		params = params.trim().replace(/^(\?|#)/, '');
3539
3540		// Iterate over all name=value pairs.
3541		$.each( params.replace( /\+/g, ' ' ).split( '&' ), function(j,v){
3542			var param = v.split( '=' ),
3543				key = decode( param[0] ),
3544				val,
3545				cur = obj,
3546				i = 0,
3547
3548				// If key is more complex than 'foo', like 'a[]' or 'a[b][c]', split it
3549				// into its component parts.
3550				keys = key.split( '][' ),
3551				keys_last = keys.length - 1;
3552
3553			// If the first keys part contains [ and the last ends with ], then []
3554			// are correctly balanced.
3555			if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
3556				// Remove the trailing ] from the last keys part.
3557				keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
3558
3559				// Split first keys part into two parts on the [ and add them back onto
3560				// the beginning of the keys array.
3561				keys = keys.shift().split('[').concat( keys );
3562
3563				keys_last = keys.length - 1;
3564			} else {
3565				// Basic 'foo' style key.
3566				keys_last = 0;
3567			}
3568
3569			// Are we dealing with a name=value pair, or just a name?
3570			if ( param.length === 2 ) {
3571				val = decode( param[1] );
3572
3573				// Coerce values.
3574				if ( coerce ) {
3575					val = val && !isNaN(val)						? +val							// number
3576						: val === 'undefined'						 ? undefined				 // undefined
3577						: coerce_types[val] !== undefined ? coerce_types[val] // true, false, null
3578						: val;																								// string
3579				}
3580
3581				if ( keys_last ) {
3582					// Complex key, build deep object structure based on a few rules:
3583					// * The 'cur' pointer starts at the object top-level.
3584					// * [] = array push (n is set to array length), [n] = array if n is
3585					//	 numeric, otherwise object.
3586					// * If at the last keys part, set the value.
3587					// * For each keys part, if the current level is undefined create an
3588					//	 object or array based on the type of the next keys part.
3589					// * Move the 'cur' pointer to the next level.
3590					// * Rinse & repeat.
3591					for ( ; i <= keys_last; i++ ) {
3592						key = keys[i] === '' ? cur.length : keys[i];
3593						cur = cur[key] = i < keys_last
3594							? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
3595							: val;
3596					}
3597
3598				} else {
3599					// Simple key, even simpler rules, since only scalars and shallow
3600					// arrays are allowed.
3601
3602					if ( Array.isArray( obj[key] ) ) {
3603						// val is already an array, so push on the next value.
3604						obj[key].push( val );
3605
3606					} else if ( obj[key] !== undefined ) {
3607						// val isn't an array, but since a second value has been specified,
3608						// convert val into an array.
3609						obj[key] = [ obj[key], val ];
3610
3611					} else {
3612						// val is a scalar.
3613						obj[key] = val;
3614					}
3615				}
3616
3617			} else if ( key ) {
3618				// No value was defined, so set something meaningful.
3619				obj[key] = coerce
3620					? undefined
3621					: '';
3622			}
3623		});
3624
3625		return obj;
3626	};
3627
3628	$.deserialize = function (query) {
3629		function subDeserialize(params, key, value) {
3630			key = key + '';
3631			if (key.indexOf('.') !== -1)
3632				{
3633				 var list = key.split('.');
3634				 var new_key = key.split(/\.(.+)?/)[1];
3635				 if (!params[list[0]]) params[list[0]] = {};
3636				 if (new_key !== '') subDeserialize(params[list[0]], new_key, value);
3637				}
3638				else if (key.indexOf('[') !== -1)
3639				{
3640				 var list = key.split('[');
3641				 key = list[0];
3642				 var list = list[1].split(']');
3643				 var index = list[0]
3644				 if (index == '')
3645					{
3646					 if (!params) params = {};
3647					 if (!params[key] || !Array.isArray(params[key])) params[key] = [];
3648					 params[key].push(value);
3649					}
3650					else
3651					{
3652					 if (!params) params = {};
3653					 if (!params[key]) params[key] = {};
3654					 params[key][index] = value;
3655					}
3656				}
3657				else
3658				{
3659				 if (!params) params = {};
3660				 params[key] = value;
3661				}
3662				}
3663		query = query + '';
3664		if (query === '') query = window.location + '';
3665		var params = {}, e;
3666		if (query) {
3667			if (query.indexOf('#') !== -1) {
3668				query = query.substr(0, query.indexOf('#'));
3669			}
3670
3671			if (query.indexOf('?') !== -1) {
3672				query = query.substr(query.indexOf('?') + 1, query.length);
3673			} else return {};
3674			if (query == '') return {};
3675			while (e = re.exec(query)) {
3676				var key = decode(e[1]);
3677				var value = decode(e[2]);
3678				subDeserialize(params, key, value);
3679			}
3680		}
3681		return params;
3682	};
3683})(jQuery);
3684
3685
3686
3687// Permet un $(target).on('destroyed', handler);
3688// le "remove" du special event est le fait qu'un event soit retiré de l'élément... ce qui est
3689// aussi appelé à la suppression de l'élément qui nettoie tous les events de l'élément supprimé.
3690$.event.special.destroyed = {
3691	remove: function(o)	{
3692		if (o.handler) o.handler()
3693	}
3694};
3695
3696
3697$.fn.getInfos = function () {
3698	this.each(function() {
3699		var Obj = this;
3700		var infos = null;
3701		var infosTag = $('>.JSinfos', Obj);
3702		if (infosTag.length == 0 || !infosTag.html()) {
3703			infosTag = $(Obj).next('.JSinfos');
3704		}
3705		if (infosTag.length == 0 || !infosTag.html()) {
3706			infos = $(Obj).attr('jsinfos');
3707			if (infos) $(Obj).removeAttr('jsinfos');
3708		} else {
3709			infos = infosTag.html();
3710			if (infosTag.hasClass('base64')) {
3711				infos = _.base64Decode(infos);
3712			}
3713			infosTag.remove();
3714		}
3715		if (infos) infos = eval("("+infos+")");
3716		else infos = {};
3717		Obj.infos = infos;
3718	});
3719};
3720
3721$.fn.initComponent = function (options) {
3722	this.each(function() {
3723		var Obj = this;
3724		$(Obj).addClass('cmpDone');
3725		$(Obj).getInfos();
3726		Obj.infos = $.extend(true, options, Obj.infos);
3727		if (!Obj.infos.data) Obj.infos.data = {};
3728		if (!Obj.infos.config) Obj.infos.config = {};
3729		Obj.infos.cmp = $(Obj).attr('cmp');
3730	});
3731};
3732
3733$.fn.caret = function (begin, end) {
3734	if (this.length == 0) return;
3735	var target = this[0];
3736	var isContentEditable = target.contentEditable === 'true';
3737	if (typeof begin == 'number')
3738		{
3739		 end = (typeof end == 'number') ? end : begin;
3740		 return this.each(function () {
3741			if (this.setSelectionRange) this.setSelectionRange(begin, end);
3742			else if (this.createTextRange)
3743				{
3744				 var range = this.createTextRange();
3745				 range.collapse(true);
3746				 range.moveEnd('character', end);
3747				 range.moveStart('character', begin);
3748				 try { range.select(); } catch (ex) { }
3749				}
3750		 });
3751		}
3752		else
3753		{
3754		 if (target.setSelectionRange)
3755			{
3756			 begin = target.selectionStart;
3757			 end = target.selectionEnd;
3758			}
3759			else if (target.ownerDocument.selection && target.ownerDocument.selection.createRange)
3760			{
3761			 var range = target.ownerDocument.selection.createRange();
3762			 begin = 0 - range.duplicate().moveStart('character', -100000);
3763			 end = begin + range.text.length;
3764			}
3765		 return { begin: begin, end: end };
3766		}
3767};
3768
3769
3770$.fn.imagesLoaded = function(callback){
3771  var elems = this.filter('img'),
3772	  len   = elems.length,
3773	  blank = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
3774
3775  elems.bind('load.imgloaded',function(){
3776	  if (--len <= 0 && this.src !== blank){
3777		elems.unbind('load.imgloaded');
3778		callback.call(elems,this);
3779	  }
3780  }).each(function(){
3781	 // cached images don't fire load sometimes, so we reset src.
3782	 if (this.complete || this.complete === undefined){
3783		var src = this.src;
3784		// webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
3785		// data uri bypasses webkit log warning (thx doug jones)
3786		this.src = blank;
3787		this.src = src;
3788	 }
3789  });
3790
3791  return this;
3792};
3793
3794
3795
3796function openSidePanel(panel)
3797{
3798 var $aside = $("aside."+panel);
3799 $aside.addClass('on floating');
3800 if (!$aside[0].close) $aside[0].close = function() {$aside.removeClass('on floating');};
3801 return $aside;
3802}
3803
3804
3805function number_format(number, decimals, dec_point, thousands_sep) {
3806	var n = !isFinite(+number) ? 0 : +number,
3807		prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
3808		sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
3809		dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
3810		toFixedFix = function (n, prec) {
3811			var k = Math.pow(10, prec);
3812			return Math.round(n * k) / k;
3813		},
3814		s = (prec ? toFixedFix(n, prec) : Math.round(n)).toString().split('.');
3815if (s[0].length >
3815 3) s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
3816if ((s[1] || '').length < prec)
3817	{
3818	 s[1] = s[1] || '';
3819	 s[1] += new Array(prec - s[1].length + 1).join('0');
3820	}
3821 return s.join(dec);
3822}
3823
3824function sortObject(obj, num) { // numéric or string
3825	var arr = [];
3826	for (var prop in obj) {
3827		if (obj.hasOwnProperty(prop)) {
3828			arr.push({k: prop, v: obj[prop]});
3829		}
3830	}
3831	if (num) arr.sort(function(a, b) { return a.v - b.v; });
3832	else arr.sort(function(a, b) {
3833		if (typeof a.v!="object") { var sa = a.v, sb=b.v;}
3834		else { var sa = a.v[0], sb=b.v[0];}
3835		return sa.toLowerCase().localeCompare(sb.toLowerCase());
3836	});
3837	return arr;
3838}
3839
3840String.prototype.replaceAt = function(i, c) {
3841	return this.substr(0, i) + c + this.substr(i + c.length);
3842}
3843String.prototype.ucfirst = function() {
3844	return this.charAt(0).toUpperCase() + this.slice(1);
3845}
3846String.prototype.ucwords = function() {
3847	var w = this.split(" ");
3848	for(var i=0; i<w.length; i++)
3849		{
3850		 w[i] = w[i].toLowerCase();
3851		 w[i] = w[i].ucfirst();
3852		}
3853	return w.join(" ");
3854}
3855String.prototype.plurial = function(nb) {
3856	if (nb<=1) return this;
3857	var W = this.split(" ");
3858	for(var i=0; i<W.length; i++)
3859		{
3860		 var w = W[i].toLowerCase();
3861		 if (w.length>=3)
3862		 	{
3863			 if (!['s', 'x', 'z'].includes(w.slice(-1)))
3864			 	{
3865				 if (['bal', 'cal', 'carnaval', 'caracal', 'choral', 'étal', 'récital ', 'val', 'chacal', 'festival', 'régal', 'pal', 'pneu', 'bleu', 'émeu'].includes(w)) W[i]+= 's';
3866				 else if (["bijou", 'caillou', 'joujou', 'hibou'].includes(w) || ['au', 'eu'].includes(w.slice(-2))) W[i]+= "x";
3867				 else if (w.slice(-2)=="al") W[i] = W.slice(0, -2)+"aux";
3868				 else if (['bail', 'corail', 'émail', 'soupirail', 'travail', 'vantail', 'vitrail'].includes(w)) W[i] = W.slice(0, -2)+"ux";
3869				 else W[i]+= "s";
3870				}
3871			}
3872		}
3873	return W.join(" ");
3874}
3875String.prototype.strReplace = function(needle, replace) {
3876	needle = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3877	var r = new RegExp(needle, 'g');
3878	return this.replaceAll(r, replace);
3879}
3880
3881function timeDiff(datas, jAct)
3882{
3883 var $f = $(jAct).closest('fieldset, form'),
3884	 s = $("[name='startT']", $f).val(),
3885	 e = $("[name='endT']", $f).val();
3886
3887 s = _.checkHour(s);
3888 e = _.checkHour(e);
3889
3890 var d = moment("2000-01-01 "+e).diff("2000-01-01 "+s, 'minutes');
3891 $('.time-diff', $f).removeClass('neg over');
3892 if (d<0) $('.time-diff', $f).addClass('neg');
3893 if (Math.abs(d)<60)
3894	{
3895	 $('.time-diff', $f).html(d+"'&nbsp;");
3896	}
3897	else
3898	{
3899	 var m = d%60,
3900		 h = Math.floor(d/60);
3901	 $('.time-diff', $f).html(h+"h"+m+"&nbsp;");
3902	 if (h>4) $('.time-diff', $f).addClass('over');
3903	}
3904}
3905
3906function extractFormData(elements) {
3907	if(!(elements instanceof jQuery)) {
3908		elements = $(elements);
3909	}
3910
3911	let data = {};
3912	elements.each((index, element) => {
3913		let form = $(element).closest("form");
3914		if(form.length==0) {
3915			let clone = $("<div>").append($(element).clone());
3916			clone.find('input, select, textarea').each(function(i) {
3917				let sourceElement = $(element).find('input, select, textarea').eq(i);
3918				$(this).val(sourceElement.val());
3919				if(sourceElement.is(':checked')) {
3920					$(this).prop('checked', true);
3921				}
3922			});
3923			form = $("<form>").append(clone);
3924		}
3925
3926		let rawDatas = form.serializeArray();
3927		for(let rawData of rawDatas) {
3928			let key = rawData["name"];
3929			let value = rawData["value"];
3930			if(data.hasOwnProperty(key) && !Array.isArray(data[key])) {
3931				data[key] = [data[key]];
3932			}
3933			if(Array.isArray(data[key])) {
3934				data[key].push(value);
3935			} else {
3936				data[key] = value;
3937			}
3938		}
3939	});
3940	return data;
3941}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.