PageSourceSearch

https://wiki.incrediblepbx.com/lib/jquery_tiki/tiki-jquery.js

js incrediblepbx.com collected 2026-10-06 06:51:29 UTC 124,755 bytes, 4,335 lines download raw bytes

1/* global $ */
2
3// $Id$
4// JavaScript glue for jQuery in Tiki
5//
6// Tiki 6 - $ is now initialised in jquery.js
7// but let's keep $jq available too for legacy custom code
8
9var $jq = $,
10	$window = $(window),
11	$document = $(document);
12
13// Check / Uncheck all Checkboxes
14function switchCheckboxes (tform, elements_name, state, hiddenToo) {
15	// checkboxes need to have the same name elements_name
16	// e.g. <input type="checkbox" name="my_ename[]">, will arrive as Array in php.
17	if (hiddenToo == undefined) {
18		hiddenToo = false;
19	}
20	var closeTag;
21	if (hiddenToo) {
22		closeTag = '"]';
23	} else {
24		closeTag = '"]:visible';
25	}
26	$(tform).contents().find('input[name="' + jQuery.escapeSelector(elements_name) + closeTag).prop('checked', state).change();
27}
28
29// add id's of any elements that don't like being animated here
30var jqNoAnimElements = ['help_sections', 'ajaxLoading'];
31
32function show(foo, f, section) {
33	if ($.inArray(foo, jqNoAnimElements) > -1 || typeof jqueryTiki === 'undefined') {		// exceptions that don't animate reliably
34		$("#" + foo).show();
35	} else if ($("#" + foo).hasClass("tabcontent")) {		// different anim prefs for tabs
36		showJQ("#" + foo, jqueryTiki.effect_tabs, jqueryTiki.effect_tabs_speed, jqueryTiki.effect_tabs_direction);
37	} else {
38		showJQ("#" + foo, jqueryTiki.effect, jqueryTiki.effect_speed, jqueryTiki.effect_direction);
39	}
40	if (f) {setCookie(foo, "o", section);}
41}
42
43function hide(foo, f, section) {
44	if ($.inArray(foo, jqNoAnimElements) > -1 || typeof jqueryTiki === 'undefined') {		// exceptions
45		$("#" + foo).hide();
46	} else if ($("#" + foo).hasClass("tabcontent")) {
47		hideJQ("#" + foo, jqueryTiki.effect_tabs, jqueryTiki.effect_tabs_speed, jqueryTiki.effect_tabs_direction);
48	} else {
49		hideJQ("#" + foo, jqueryTiki.effect, jqueryTiki.effect_speed, jqueryTiki.effect_direction);
50	}
51	if (f) {
52//		var wasnot = getCookie(foo, section, 'x') == 'x';
53		setCookie(foo, "c", section);
54//		if (wasnot) {
55//			history.go(0);	// used to reload the page with all menu items closed - broken since 3.x
56//		}
57	}
58}
59
60// flip function... unfortunately didn't use show/hide (ay?)
61function flip(foo, style) {
62	var $foo = $("#" + foo);
63	if (style && style !== 'block' || foo === 'help_sections' || foo === 'fgalexplorer' || typeof jqueryTiki === 'undefined') {	// TODO find a better way?
64		$foo.toggle();	// inlines don't animate reliably (yet) (also help)
65		if ($foo.css('display') === 'none') {
66			setSessionVar('show_' + tiki_encodeURIComponent(foo), 'n');
67		} else {
68			setSessionVar('show_' + tiki_encodeURIComponent(foo), 'y');
69		}
70	} else {
71		if ($foo.css("display") === "none") {
72			setSessionVar('show_' + tiki_encodeURIComponent(foo), 'y');
73			show(foo);
74		}
75		else {
76			setSessionVar('show_' + tiki_encodeURIComponent(foo), 'n');
77			hide(foo);
78		}
79	}
80}
81
82// handle JQ effects
83function showJQ(selector, effect, speed, dir) {
84	if (effect === 'none') {
85		$(selector).show();
86	} else if (effect === '' || effect === 'normal') {
87		$(selector).show(400);	// jquery 1.4 no longer seems to understand 'nnormal' as a speed
88	} else if (effect == 'slide') {
89		$(selector).slideDown(speed);
90	} else if (effect === 'fade') {
91		$(selector).fadeIn(speed);
92	} else if (effect.match(/(.*)_ui$/).length > 1) {
93		$(selector).show(effect.match(/(.*)_ui$/)[1], {direction: dir}, speed);
94	} else {
95		$(selector).show();
96	}
97}
98
99function hideJQ(selector, effect, speed, dir) {
100	if (effect === 'none') {
101		$(selector).hide();
102	} else if (effect === '' || effect === 'normal') {
103		$(selector).hide(400);	// jquery 1.4 no longer seems to understand 'nnormal' as a speed
104	} else if (effect === 'slide') {
105		$(selector).slideUp(speed);
106	} else if (effect === 'fade') {
107		$(selector).fadeOut(speed);
108	} else if (effect.match(/(.*)_ui$/).length > 1) {
109		$(selector).hide(effect.match(/(.*)_ui$/)[1], {direction: dir}, speed);
110	} else {
111		$(selector).hide();
112	}
113}
114
115// ajax loading indicator
116
117function ajaxLoadingShow(destName) {
118	var $dest, $loading, pos, x, y, w, h;
119
120	if (typeof destName === 'string') {
121		$dest = $('#' + destName);
122	} else {
123		$dest = $(destName);
124	}
125	if ($dest.length === 0 || $dest.parents(":hidden").length > 0) {
126		return;
127	}
128	$loading = $('#ajaxLoading');
129
130	// find area of destination element
131	pos = $dest.offset();
132	// clip to page
133	if (pos.left + $dest.width() > $window.width()) {
134		w = $window.width() - pos.left;
135	} else {
136		w = $dest.width();
137	}
138	if (pos.top + $dest.height() > $window.height()) {
139		h = $window.height() - pos.top;
140	} else {
141		h = $dest.height();
142	}
143	x = pos.left + (w / 2) - ($loading.width() / 2);
144	y = pos.top + (h / 2) - ($loading.height() / 2);
145
146
147	// position loading div
148	$loading.css('left', x).css('top', y);
149	// now BG
150	x = pos.left + ccsValueToInteger($dest.css("margin-left"));
151	y = pos.top + ccsValueToInteger($dest.css("margin-top"));
152	w = ccsValueToInteger($dest.css("padding-left")) + $dest.width() + ccsValueToInteger($dest.css("padding-right"));
153	h = ccsValueToInteger($dest.css("padding-top")) + $dest.height() + ccsValueToInteger($dest.css("padding-bottom"));
154	$('#ajaxLoadingBG').css('left', pos.left).css('top', pos.top).width(w).height(h).fadeIn("fast");
155
156	show('ajaxLoading');
157
158
159}
160
161function ajaxLoadingHide() {
162	hide('ajaxLoading');
163	$('#ajaxLoadingBG').fadeOut("fast");
164}
165
166
167function ajaxSubmitEventHandler(successCallback, dataType) {
168	return function (e) {
169		e.preventDefault();
170		var form = this, act;
171		act = $(form).attr('action');
172		var modal = $(form).closest('.modal-dialog');
173
174		if (! act) {
175			act = url;
176		}
177
178		dataType = dataType || 'json';
179
180		if (typeof $(form).valid === "function") {
181			if (!$(form).valid()) {
182				return false;
183			} else if ($(form).validate().pendingRequest > 0) {
184				$(form).validate();
185				setTimeout(function() {$(form).submit();}, 500);
186				return false;
187			}
188		}
189
190		modal.tikiModal(tr('Loading...'));
191
192		// if there is a file is included in form, use FormData, otherwise, serialize the form input values.
193		// FormData still has issues in IE, though they've been fixed in Edge.
194		if ($(form).find("input[type=file]").length){
195			var formData = new FormData(form);
196		} else {
197			var formData= $(form).serialize();
198		}
199
200		var formSubmission = {
201			type: 'POST',
202			data: formData,
203			dataType: dataType,
204			success: function (data) {
205				successCallback.apply(form, [data]);
206			},
207			error: function (jqxhr) {
208				// 	Headers sent from Feedback class already handled through an ajaxComplete
209				if (! jqxhr.getResponseHeader('X-Tiki-Feedback')) {
210					modal.tikiModal();
211					$(form).showError(jqxhr);
212				}
213			},
214			complete: function () {
215				modal.tikiModal();
216			}
217		};
218
219		// if the encryption type on the form is set to 'multipart/form-data' or formData is a FormData object
220		// we must set contentType and processData to false on the ajax submission
221		if (form.enctype === "multipart/form-data" || formData.constructor === FormData) {
222			formSubmission.contentType = false;
223			formSubmission.processData = false;
224		}
225
226		$.ajax(act, formSubmission);
227		return false;
228	};
229}
230
231function checkDuplicateRows( button, columnSelector, rowSelector, parentSelector ) {
232	if (typeof columnSelector === 'undefined') {
233		columnSelector = "td";
234	}
235	if (typeof rowSelector === 'undefined') {
236		rowSelector = "tr:not(:first)";
237	}
238	if (typeof parentSelector === 'undefined') {
239		parentSelector = "table:first";
240	}
241	var $rows = $(button).parents(parentSelector).find(rowSelector);
242	$rows.each(function( ix, el ){
243		if ($("input:checked", el).length === 0) {
244			var $el = $(el);
245			var line = $el.find(columnSelector).text();
246			$rows.each(function( ix, el ){
247				if ($el[0] !== el && $("input:checked", el).length === 0) {
248					if (line === $(el).find(columnSelector).text()) {
249						$(":checkbox:first", el).prop("checked", true);
250					}
251				}
252			});
253		}
254	});
255}
256
257$.fn.tiki_popover = function () {
258	var list, $container = this;
259
260	// To allow table elements etc in tips and popovers
261	if (typeof $.fn.tooltip.Constructor.Default.whiteList === "object") {
262		var myDefaultWhiteList = $.fn.tooltip.Constructor.Default.whiteList;
263
264		myDefaultWhiteList.table = [];
265		myDefaultWhiteList.thead = [];
266		myDefaultWhiteList.tbody = [];
267		myDefaultWhiteList.tr = [];
268		myDefaultWhiteList.th = [];
269		myDefaultWhiteList.td = [];
270		myDefaultWhiteList.form = ["action", "method"];
271		myDefaultWhiteList.input = ["name", "value", "type"];
272		myDefaultWhiteList.button = ["type", "disabled", "name", "value", "onclick"];
273		myDefaultWhiteList.time = ["datetime"];	// for timeago
274		myDefaultWhiteList.a = ["target", "href", "title", "rel", "data-toggle", "data-backdrop",
275								"data-target", "onclick"];	// data items for smarty_function_bootstrap_modal
276	}
277
278	/*
279	 * Prepare the data so all elements so the data is all in the right format for bootstrap popovers
280	 */
281	list = $container.find('.tips[title!=""], .tikihelp[title!=""]')
282		.each(function () {
283			var $element = $(this);
284
285			if ($element.attr('title')) {
286				$.each(['|', ':', '<br/>', '<br>'], function (key, sep) {
287					var parts = $element.attr('title').split(sep);
288					if (parts.length > 1) {
289						$element.attr('title', parts.shift());
290						$element.data('content', parts.join(sep));
291					}
292				});
293			} else {
294				$element.attr('title', '');
295			}
296
297			if (! $element.data('trigger')) {
298				$element.data('trigger', 'hover');
299			}
300			// default Tiki delay
301			$element.data('delay', { "show": 0, "hide": 10 });
302		});
303
304	$.merge(list, $container.find("a[data-toggle=popover]:not(.tips[title!='']):not(.tikihelp[title!=''])"));
305
306	list.filter('.bottom').data('placement', 'bottom');
307	list.filter('.left').data('placement', 'left');
308	list.filter('.slow').data('delay', { "show": 500, "hide": 0 });
309
310	list.find('img').attr('title', ''); // Remove the img title to avoid browser tooltip
311	list.filter('[data-trigger="click"]')
312		.click(function (e) {
313			e.preventDefault();
314		});
315
316	// Handle common cases
317	list
318		.popover({
319			container: 'body',
320			html: true,
321			boundary: "window",
322			placement: $.tikiPopoverWhereToPlace
323		});
324
325	$container.find('.ajaxtips').each(function() {
326		var me = $(this),
327			trigger = me.data('trigger') || 'hover';
328
329		$(this).popover({
330			trigger: trigger,
331			html: true,
332			delay: { "show": 0, "hide": 10 },
333			placement: $.tikiPopoverWhereToPlace,
334			boundary: "window",
335			content: function () {
336				var link = this, content = $(this).data('content');
337
338				if (!content) {
339					$.get($(this).data('ajaxtips'), function (data) {
340						content = data;
341
342						$(link).data('content', content);
343						$(link).popover('show');
344					});
345
346					// display a spinner while waiting for the ajax call to return the content
347					content = "<div class='text-center p-3'><img src='img/spinner.gif' alt='Loading...'></div>";
348				}
349
350				return content;
351			}
352		});
353	});
354
355	// only have one popover showing at a time
356	$document.on("show.bs.popover", function ( e ) {
357		var event = e;
358		$('.popover:visible:not(.tour-tour)').each(function () {
359			if (this.previousElementSibling !== event.target) {
360				$(this).popover('hide');
361			}
362		});
363	});
364
365	$document.on("hide.bs.popover", function ( e ) {
366		var $popover = $('.popover:visible:not(.tour-tour)');
367
368		// if mouse is over the popover
369		if ($popover.find(":hover").length) {
370			// change the leave event to be when leaving the popover
371			$popover.mouseleave(function () {
372				$(this).popover("hide");
373			});
374			// and cancel the hide event
375			e.preventDefault();
376			return false;
377		}
378	});
379
380	return $container;
381};
382
383$.tikiPopoverWhereToPlace = function (pop, el) {
384	var pxNum = function(str) {
385			return (str || '').replace('px', '') * 1;
386		},
387		$win = $(window),
388		$el = $(el),
389		width = $el.offsetParent().width(),
390		height = $el.offsetParent().height(),
391		$pop = $(pop),
392		allowedImgWidth = width * 0.60,
393		allowedImgHeight = height * 0.60,
394		manualImageWidth = $el.data('width'),
395		leftPos = $el.offset().left,
396		rightPos = leftPos + $el.outerWidth(),
397		bottomPos = $el.offset().top + $el.outerHeight() - $win.scrollTop(),
398		$img = $pop.find('div[style*="background-image"],img').first(),
399		$imgContainer = $img.parent(),
400		$imgPopover = $imgContainer.parent(),
401		imgWidth = pxNum($img.css('width')),
402		imgHeight = pxNum($img.css('height')),
403		newImgWidth,
404		newImgHeight,
405		widthBuffer,
406		heightBuffer;
407
408	if ($el.data("placement")) {
409		return $el.data("placement");	// element already has popover placement set
410	}
411
412	if (manualImageWidth) {
413		$img.css({
414			width: manualImageWidth + 'px'
415		});
416
417		$pop.css({
418			"max-width" : '100%'
419		});
420
421		imgWidth = manualImageWidth;
422	}
423
424	//lets check the size of the popover img
425	if (imgWidth > allowedImgWidth || imgHeight > allowedImgHeight) {
426		widthBuffer = (pxNum($imgContainer.css('padding-left')) + pxNum($imgContainer.css('margin-left')) + pxNum($imgContainer.css('border-left-width'))) * 2;
427		heightBuffer = (pxNum($imgContainer.css('padding-top')) + pxNum($imgContainer.css('margin-top')) + pxNum($imgContainer.css('border-top-width'))) * 2;
428
429		// proportionate the image relative to what is allowed
430		if(allowedImgWidth/imgWidth > allowedImgHeight/imgHeight){
431			newImgWidth = allowedImgWidth;
432			newImgHeight = imgHeight*(allowedImgWidth/imgWidth);
433		} else {
434			newImgWidth = imgWidth*(allowedImgHeight/imgHeight);
435			newImgHeight = allowedImgHeight;
436		}
437
438		$img.css({
439			backgroundSize: newImgWidth + 'px ' + newImgHeight + 'px',
440			width: newImgWidth + 'px',
441			height: newImgHeight + 'px'
442		});
443
444		$imgPopover.css({
445			maxWidth: (newImgWidth + widthBuffer) + 'px',
446			maxHeight: (newImgHeight + heightBuffer) +'px'
447		});
448	}
449
450
451	var $popTemp = $("<div class='popover temp'><div class='popover-body'>" + $el.data("content") + "</div></div>");
452	$("body").append($popTemp);
453	var popWidth = $popTemp.outerWidth(),
454		popHeight = $popTemp.outerHeight();
455
456	$popTemp.remove();
457
458	if (width - leftPos < popWidth && width - rightPos < popWidth) {
459		if (bottomPos > popHeight ||
460			bottomPos + popHeight > $win.height()) {
461			return 'top';
462		} else {
463			return 'bottom';
464		}
465	} else if (width - leftPos > popWidth) {
466		return 'left';
467	} else if (width - rightPos > popWidth) {
468		return 'right';
469	}
470	if (imgWidth && width - leftPos + imgWidth > width) return 'bottom';
471
472	return 'auto';
473};
474
475/*
476	keep the popover open while the user is hovering over it
477	based on http://jsfiddle.net/raving/2thfaxeu thanks
478*/
479if ($.fn.popover) {
480	var originalLeave = $.fn.popover.Constructor.prototype.leave;
481	$.fn.popover.Constructor.prototype.leave = function(obj){
482		var self = obj instanceof this.constructor ?
483			obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type);
484		var container, timeout;
485
486		originalLeave.call(this, obj);
487
488		if(self.$tip) {
489			container = self.$tip;
490			timeout = self.timeout;
491			container.one('mouseenter', function(){
492				//We entered the actual popover – call off the dogs
493				clearTimeout(timeout);
494				//Let's monitor popover content instead
495				container.one('mouseleave', function(){
496					$.fn.popover.Constructor.prototype.leave.call(self, self);
497				});
498			})
499		}
500	};
501}
502
503
504
505$(function() { // JQuery's DOM is ready event - before onload
506	if (!window.jqueryTiki) window.jqueryTiki = {};
507
508	// Reflections
509	if (jqueryTiki.reflection) {
510		$("img.reflect").reflect({});
511	}
512
513	if (jqueryTiki.tooltips) {
514		$(document).tiki_popover();
515	}
516
517	// superfish setup (CSS menu effects)
518	if (jqueryTiki.superfish) {
519		$('ul.cssmenu_horiz').supersubs({
520						minWidth:    11,   // minimum width of sub-menus in em units
521						maxWidth:    20,   // maximum width of sub-menus in em units
522						extraWidth:  1     // extra width can ensure lines don't sometimes turn over
523															 // due to slight rounding differences and font-family
524		});
525		$('ul.cssmenu_vert').supersubs({
526						minWidth:    11,   // minimum width of sub-menus in em units
527						maxWidth:    20,   // maximum width of sub-menus in em units
528						extraWidth:  1     // extra width can ensure lines don't sometimes turn over
529															 // due to slight rounding differences and font-family
530		});
531		$('ul.cssmenu_horiz').superfish({
532			animation: {opacity:'show', height:'show'},	// fade-in and slide-down animation
533			speed: 'fast',								// faster animation speed
534			onShow: function(){
535				if ($(this).data('active')) return;
536
537				$(this)
538					.data('active', true)
539					.moveToWithinWindow();
540			},
541			onHide: function(){
542				$(this).removeData('active');
543			}
544		});
545		$('ul.cssmenu_vert').superfish({
546			animation: {opacity:'show', height:'show'},	// fade-in and slide-down animation
547			speed: 'fast',								// faster animation speed
548			onShow: function(){
549				if ($(this).data('active')) return;
550
551				$(this)
552					.data('active', true)
553					.moveToWithinWindow();
554			},
555			onHide: function(){
556				$(this).removeData('active');
557			}
558		});
559	}
560
561	$.fn.applyColorbox = function() {
562		$(this).find("a[data-box*='box']").colorbox({
563			rel: function(){
564				return $(this).attr('data-box');
565			},
566			transition: "elastic",
567			maxHeight:"95%",
568			maxWidth:"95%",
569			overlayClose: true,
570			current: jqueryTiki.cboxCurrent
571		});
572
573		// now, first let suppose that we want to display images in ColorBox by default:
574
575		// this matches data-box attributes containing type=img or no type= specified
576		$(this).find("a[data-box*='box'][data-box*='type=img'], a[data-box*='box'][data-box!='type=']:not([data-is-text])").colorbox({
577			photo: true
578		});
579
580		// data-box attributes containing slideshow (this one must be without #col1)
581		$(this).find("a[data-box*='box'][data-box*='slideshow']").colorbox({
582			photo: true,
583			slideshow: true,
584			slideshowSpeed: 3500,
585			preloading: false,
586			width: "100%",
587			height: "100%"
588		});
589		// this are the defaults matching all *box links which are not obviously links to images...
590		// (if we need to support more, add here... otherwise it is possible to override with type=iframe in data-box attribute of a link)
591		//  (from here one to speed it up, matches any link in #col1 only - the main content column)
592		$(this).find("#col1 a[data-box*='box']:not([data-box*='type=img']):not([href*='display']):not([href*='preview']):not([href*='thumb']):not([data-box*='slideshow']):not([href*='image']):not([href$='\.jpg']):not([href$='\.jpeg']):not([href$='\.png']):not([href$='\.gif'])").colorbox({
593			iframe: true,
594			width: "95%",
595			height: "95%"
596		});
597		// hrefs starting with ftp(s)
598		$(this).find("#col1 a[data-box*='box'][href^='ftp://'], #col1 a[data-box*='box'][href^='ftps://']").colorbox({
599			iframe: true,
600			width: "95%",
601			height: "95%"
602		});
603		// data-box attributes containing type=flash
604		$(this).find("#col1 a[data-box*='box'][data-box*='type=flash']").colorbox({
605			inline: true,
606			width: "60%",
607			height: "60%",
608			href: function () {
609				var $el = $("#cb_swf_player");
610				if ($el.length === 0) {
611					$el = $("<div id='cb_swf_player' />");
612					$(document.body).append($("<div />").hide().append($el));
613				}
614				//$(this).media.swf(el, { width: 400, height: 300, autoplay: true, src: $(this).attr("href") });
615				swfobject.embedSWF($(this).attr("href"), "cb_swf_player", "100%", "90%", "9.0.0", "vendor_bundled/vendor/bower-asset/swfobject/swfobject/expressInstall.swf");
616				return $("#cb_swf_player");
617			}
618		});
619
620		// data-box attributes containing type=video
621		$(this).find("#col1 a[data-box*='box'][data-box*='type=video']").colorbox({
622			iframe: true,
623			fastIframe: false,
624			height:"90%",
625			width:"90%",
626			html: function() {
627				return '<video controls autoplay>' +
628					'<source src="' + $(this).attr('href') + '">' +
629					+ 'Your browser does not support the video tag.' +
630					'</video>';
631			},
632			onComplete : function() {
633				let video = $('#colorbox iframe').contents().find('video');
634
635				setTimeout(function() {
636					$(this).colorbox.resize({
637						innerHeight : video.height() + 'px',
638						innerWidth : video.width() + 'px'
639					});
640				}, 100);
641			}
642		});
643
644		// data-box attributes with type=iframe (if someone needs to override anything above)
645		$(this).find("#col1 a[data-box*='box'][data-box*='type=iframe']").colorbox({
646			iframe: true
647		});
648		// inline content: hrefs starting with #
649		$(this).find("#col1 a[data-box*='box'][href^='#']").colorbox({
650			inline: true,
651			width: "50%",
652			height: "50%",
653			href: function(){
654				return $(this).attr('href');
655			}
656		});
657
658		// titles (for captions):
659
660		// by default get title from the title attribute of the link (in all columns)
661		$(this).find("a[data-box*='box'][title]").colorbox({
662			title: function(){
663				return $(this).attr('title');
664			}
665		});
666		// but prefer the title from title attribute of a wrapped image if any (in all columns)
667		$(this).find("a[data-box*='box'] img[title]").colorbox({
668			title: function(){
669				return $(this).attr('title');
670			},
671			photo: true,				// and if you take title from the image you need photo
672			href: function(){			// and href as well (for colobox 1.3.6 tiki 5.0)
673				return $(this).parent().attr("href");
674			}
675		});
676
677		/* Shadowbox params compatibility extracted using regexp functions */
678		var re, ret;
679		// data-box attributes containing title param overrides title attribute of the link (shadowbox compatible)
680		$(this).find("#col1 a[data-box*='box'][data-box*='title=']").colorbox({
681			title: function () {
682				re = /(title=([^;\"]+))/i;
683				ret = $(this).attr("data-box").match(re);
684				return ret[2];
685			}
686		});
687		// data-box attributes containing height param (shadowbox compatible)
688		$(this).find("#col1 a[data-box*='box'][data-box*='height=']").colorbox({
689			height: function () {
690				re = /(height=([^;\"]+))/i;
691				ret = $(this).attr("data-box").match(re);
692				return ret[2];
693			}
694		});
695		// data-box attributes containing width param (shadowbox compatible)
696		$(this).find("#col1 a[data-box*='box'][data-box*='width=']").colorbox({
697			width: function () {
698				re = /(width=([^;\"]+))/i;
699				ret = $(this).attr("data-box").match(re);
700				return ret[2];
701			}
702		});
703
704		// links generated by the {COLORBOX} plugin
705		if (jqueryTiki.colorbox) {
706			$(this).find("a[data-box^='shadowbox[colorbox']").each(function () {$(this).attr('savedTitle', $(this).attr('title'));});
707			$(this).find("a[data-box^='shadowbox[colorbox']").colorbox({
708				title: function() {
709					return $(this).attr('savedTitle');	// this fix not required is colorbox was disabled
710				}
711			});
712		}
713	};
714
715	$.applyColorbox = function() {
716		if (jqueryTiki.colorbox) {
717			$('body').applyColorbox();
718		}
719	};
720
721	// ColorBox setup (Shadowbox, actually "<any>box" replacement)
722	if (jqueryTiki.colorbox && !jqueryTiki.mobile) {
723		$().on('cbox_complete', function(){
724			$("#cboxTitle").wrapInner("<div></div>");
725		});
726
727		$.applyColorbox();
728	}	// end if (jqueryTiki.colorbox)
729
730	if (jqueryTiki.zoom) {
731		$("a[data-box*=zoom]").each(function () {
732			$(this)
733				.wrap('<span class="img_zoom"></span>')
734				.parent()
735				.zoom({
736					url: $(this).attr("href")
737				});
738		});
739	}
740
741	$.fn.applyChosen = function () {
742		if (jqueryTiki.chosen) {
743			$("select:not(.allow_single_deselect):not(.nochosen)").tiki("chosen");
744		}
745	};
746
747	$.applyChosen = function() {
748		return $('body').applyChosen();
749	};
750
751	if (jqueryTiki.chosen) {
752		$.applyChosen();
753	}
754
755	$( function() {
756		$("#keepOpenCbx").click(function() {
757			if (this.checked) {
758				setCookie("fgalKeepOpen", "1");
759			} else {
760				setCookie("fgalKeepOpen", "");
761			}
762		});
763		var keepopen = getCookie("fgalKeepOpen");
764		$("#keepOpenCbx").prop("checked", !! keepopen);
765	});
766	// end fgal fns
767
768
769	$.paginationHelper();
770
771	// bind clickModal to links with or in click-modal class
772	$(document).on('click', 'a.click-modal, .click-modal a', $.clickModal({
773		size: 'modal-lg',
774		backdrop: 'static',
775		success: function (data) {
776			let redirect = $(this).data('modal-submit-redirect-url') || $(this).parent().data('modal-submit-redirect-url');
777			window.location.href = redirect || window.location.href.replace(/#.*$/, '');
778		}
779	}));
780
781	if (jqueryTiki.numericFieldScroll === "none" || jqueryTiki.numericFieldScroll === null){
782		// disable mousewheel on a input number field when in focus
783		// (to prevent  browsers change the value when scrolling)
784		$('form').on('focus', 'input[type=number]', function (e) {
785			$(this).on('wheel.disableScroll', function (e) {
786				e.preventDefault()
787			})
788		});
789		$('form').on('blur', 'input[type=number]', function (e) {
790			$(this).off('wheel.disableScroll')
791		});
792	}
793});		// end $document.ready
794
795//For ajax/custom search
796$document.on('pageSearchReady', function() {
797	$.paginationHelper();
798});
799
800// moved from tiki-list_file_gallery.tpl in tiki 6
801function checkClose() {
802	if (!$("#keepOpenCbx").prop("checked")) {
803		window.close();
804	} else {
805		window.blur();
806		if (window.opener) {
807			window.opener.focus();
808		}
809	}
810}
811
812
813/*
814 * JS only textarea fullscreen function (for Tiki 5+)
815 */
816
817$(function() {	// if in translation-diff-mode go fullscreen automatically
818	if ($("#diff_outer").length && !$.trim($(".wikipreview .wikitext").html()).length) {	// but not if previewing (TODO better)
819		toggleFullScreen("editwiki");
820	}
821});
822
823function sideBySideDiff() {
824	if ($('.side-by-side-fullscreen').size()) {
825		$('.side-by-side-fullscreen').remove();
826		return;
827	}
828
829	var $diff = $('#diff_outer').remove(), $zone = $('.edit-zone');
830	$zone.after($diff.addClass('side-by-side-fullscreen'));
831	$diff.find('#diff_history').height('');
832}
833
834function toggleFullScreen(area_id) {
835
836	if ($("input[name=wysiwyg]").val() === "y") {		// quick fix to disable side-by-side translation for wysiwyg
837		$("#diff_outer").css({
838			position: "inherit",
839			height: "400px",
840			overflowX: "auto"
841		});
842		return;
843	}
844
845	var textarea = $("#" + area_id);
846
847	//codemirror interation and preservation
848	var textareaEditor = syntaxHighlighter.get(textarea);
849	if (textareaEditor) {
850		syntaxHighlighter.fullscreen(textarea);
851		sideBySideDiff();
852		return;
853	}
854
855	var toolbar = $('#editwiki_toolbar'),
856		preview = $("#autosave_preview"),
857		comment = $("#comment").parents("fieldset:first"),
858		screen = $('.TextArea-fullscreen'),
859		zone = $('.edit-zone', screen);
860
861	screen.add(textarea).css('height', '');
862
863	//removes wiki command buttons (save, cancel, preview) from fullscreen view
864	$('.TextArea-fullscreen .actions').remove();
865	if (textarea.parent().hasClass("ui-wrapper")) {
866		textarea.resizable("destroy");	// if codemirror is off, jquery-ui resizable messes this up
867	}
868
869	var textareaParent = textarea.parents(".tab-content:first").toggleClass('TextArea-fullscreen');
870
871	if (textareaParent.hasClass('TextArea-fullscreen')) {
872		$('body').css('overflow', 'hidden');
873		$('.tabs,.rbox-title').toggle();
874		$('#fullscreenbutton').hide();
875
876		var win = $window
877			.data('cm-resize', true),
878			diff = $("#diff_outer"),
879			msg = $(".translation_message"),
880			actions = $('.actions', textarea.parents("form"));
881
882		//adds wiki command buttons (save, cancel, preview) to fullscreen view
883		actions.clone().appendTo('.TextArea-fullscreen');
884		actions = $('.actions', $('.TextArea-fullscreen'));
885
886		comment.css({   // fix comments fieldset to bottom and hide others (like contributions)
887			position: "absolute",
888			bottom: actions.outerHeight() + "px",
889			width: "100%"
890		}).nextAll("fieldset").hide();
891
892		preview.css({
893			position: "absolute",
894			top: 0,
895			left: 0
896		});
897
898		win.resize(function() {
899			screen = $('.TextArea-fullscreen');
900			actions = $('.actions', screen);
901			comment = $("#comment").parents("fieldset:first");
902			if (win.data('cm-resize') && screen) {
903				screen.css('height', win.height() + 'px');
904				var swidth = win.width() + "px";
905				var commentMargin = parseInt(comment.css("paddingTop").replace("px", "")) * 4;
906				commentMargin += parseInt(comment.css("borderBottomWidth").replace("px", "")) * 2;
907				var innerHeight = win.height() - comment.outerHeight() - commentMargin - actions.outerHeight();
908						 // reducing innerHeight by 85px in prev line makes the "Describe the change you made:" and
909						 // "Monitor this page:" edit fields visible and usable. Tested in all 22 themes in Tiki-12 r.48429
910
911				if (diff.length) {
912					swidth = (screen.width() / 2) + "px";
913					innerHeight -= msg.outerHeight();
914					msg.css("width", (screen.width() / 2 - msg.css("paddingLeft").replace("px", "") - msg.css("paddingRight").replace("px", "")) + "px");
915					diff.css({
916						width: swidth,
917						height: innerHeight + 'px'
918					});
919					$('#diff_history').height(innerHeight + "px");
920				}
921				textarea.css("width", swidth);
922				toolbar.css('width', swidth);
923				zone.css("width", swidth);
924				preview.css("width", swidth);
925				textarea.css('height', (innerHeight - toolbar.outerHeight()) + "px");
926			}
927		});
928		setTimeout(function () {$window.resize();}, 500);	// some themes (coelesce) don't show scrollbars unless this is delayed a bit
929	} else {
930		textarea.css("width", "");
931		toolbar.css('width', "");
932		zone.css({ width: "", height: ""});
933		screen.css("width", "");
934		comment.css({ position: "", bottom: "", width: "" }).nextAll("fieldset").show();
935		preview.css({ position: "", top: "", left: "" });
936		$('body').css('overflow', '');
937		$('.tabs,.rbox-title').toggle();
938		$('#fullscreenbutton').show();
939		$window.removeData('cm-resize');
940	}
941
942	sideBySideDiff();
943}
944
945/* Simple tiki plugin for jQuery
946 * Helpers for autocomplete and sheet
947 */
948var xhrCache = {}, lastXhr;	// for jq-ui autocomplete
949
950$.fn.tiki = function(func, type, options, excludepage) {
951	var opts = {}, opt;
952	switch (func) {
953		case "autocomplete":
954			if (jqueryTiki.autocomplete && jqueryTiki.ui) {
955				if (typeof type === 'undefined') { // func and type given
956					// setup error - alert here?
957					return null;
958				}
959				options = options || {};
960				var requestData = {}, _renderItem = null;
961
962				var url = "";
963				switch (type) {
964					case "pagename":
965						url = "tiki-listpages.php?listonly&initial=" + (options.initial ? options.initial + "&nonamespace" : "")+"&exclude_page="+excludepage;
966						break;
967					case "groupname":
968						url = "tiki-ajax_services.php?listonly=groups";
969						break;
970					case "username":
971						url = "tiki-ajax_services.php?listonly=users";
972						break;
973					case "usersandcontacts":
974						url = "tiki-ajax_services.php?listonly=usersandcontacts";
975						break;
976					case "userrealname":
977						url = "tiki-ajax_services.php?listonly=userrealnames";
978						break;
979					case "tag":
980						url = "tiki-ajax_services.php?listonly=tags&separator=+";
981						break;
982					case "icon":
983						url = null;
984						opts.source = Object.keys(jqueryTiki.iconset.icons);
985
986						_renderItem = function(ul, item) {
987							return $("<li>")
988									.attr("data-value", item.value )
989									.append($().getIcon(item.value))
990									.append(" ")
991									.append(item.label)
992									.appendTo(ul);
993						};
994						break;
995					case 'trackername':
996						url = "tiki-ajax_services.php?listonly=trackername";
997						break;
998					case 'trackervalue':
999						if (typeof options.fieldId === "undefined") {
1000							// error
1001							return null;
1002						}
1003						$.extend( requestData, options );
1004						options = {};
1005						url = "list-tracker_field_values_ajax.php";
1006						break;
1007					case "reference":
1008						url = "tiki-ajax_services.php?listonly=references";
1009						break;
1010				}
1011				var multiple = options.multiple && (type == 'usersandcontacts' || type == 'userrealname' || type == 'username' || type == 'reference');
1012				opts = $.extend({		//  default options for autocompletes in tiki
1013					minLength: 2,
1014					source: function( request, response ) {
1015						if( multiple ) {
1016							request.term = (''+request.term).split( /,\s*/ ).pop();
1017						}
1018						if (options.tiki_replace_term) {
1019							request.term = options.tiki_replace_term.apply(null, [request.term]);
1020						}
1021						var cacheKey = "ac." + type + "." + request.term;
1022						if ( cacheKey in xhrCache ) {
1023							response( xhrCache[ cacheKey ] );
1024							return;
1025						}
1026						request.q = request.term;
1027						$.extend( request, requestData );
1028						lastXhr = $.getJSON( url, request, function( data, status, xhr ) {
1029							xhrCache[ cacheKey ] = data;
1030							if ( xhr === lastXhr ) {
1031								response( data, function (item) {
1032									return item
1033								})
1034							}});
1035					},
1036					focus: function(ev) {
1037						// Important for usability handling below to prevent non-valid selections
1038						ev.preventDefault();
1039					},
1040					search: function() {
1041						if( multiple ) {
1042							// custom minLength
1043							var term = (''+this.value).split( /,\s*/ ).pop();
1044							if ( term.length < 2 ) {
1045								return false;
1046							}
1047						}
1048					},
1049					select: function(e, ui) {
1050						if( multiple ) {
1051							var terms = ''+this.value;
1052							terms = terms.replace(';', ',');
1053							terms = terms.split( /,\s*/ );
1054							// remove the current input
1055							terms.pop();
1056							// add the selected item
1057							terms.push( ui.item.value );
1058							// add placeholder to get the comma-and-space at the end
1059							terms.push( "" );
1060							this.value = terms.join( ", " );
1061							return false;
1062						} else {
1063							$(this).data('selected', true);
1064						}
1065					}
1066				}, opts);
1067				$.extend(opts, options);
1068
1069				if(options.mustMatch && multiple) {
1070					// Control editing of autocomplete to avoid messing with selection
1071					this.on("keydown", function (e) {
1072						if (e.which === 8 || e.which === 46) {
1073							e.preventDefault();
1074							var terms = ''+this.value;
1075							terms = terms.replace(';', ',');
1076							terms = terms.split( /,\s*/ );
1077							// remove the current input and the last previous item
1078							var lastterm = terms.pop();
1079							if (lastterm === '') {
1080								terms.pop();
1081							}
1082							// add placeholder to get the comma-and-space at the end
1083							terms.push( "" );
1084							this.value = terms.join( ", " );
1085						} else if (e.which === 37 || e.which === 39) {
1086							e.preventDefault();
1087						}
1088					});
1089					this.on("focus click", function() {
1090						var currentVal = $(this).val();
1091						$(this).val('').val(currentVal);
1092					});
1093				} else if (options.mustMatch) {
1094					if ($(this).val()) {
1095						// if there is value to begin then consider as selected
1096						$(this).data('selected', true);
1097					}
1098					$(this).on("blur", function() {
1099						if (! $(this).data('selected')) {
1100							$(this).val('');
1101						}
1102					});
1103					$(this).on("keydown", function(e) {
1104						if ($(this).data('selected') && (e.which === 8 || e.which === 46)) {
1105							e.preventDefault();
1106							$(this).val('');
1107							$(this).data('selected', false);
1108						} else if ($(this).data('selected') && (e.which > 47 || e.which === 32)) {
1109							e.preventDefault();
1110						} else if (e.which === 13 && !$(this).data('selected')) {
1111							e.preventDefault();
1112						}
1113					});
1114				}
1115
1116		 		return this.each(function() {
1117					var $element = $(this).autocomplete(opts).blur( function() {
1118						$(this).removeClass( "ui-autocomplete-loading").change();
1119					});
1120					if (_renderItem && $element.length) {
1121						$element.autocomplete("instance")._renderItem = _renderItem;
1122					}
1123				});
1124			}
1125			break;
1126		case "carousel":
1127			if (jqueryTiki.carousel) {
1128				opts = {
1129						imagePath: "vendor_bundled/vendor/jquery-plugins/infinitecarousel/images/",
1130						autoPilot: true
1131					};
1132				$.extend(opts, options);
1133				return this.each(function() {
1134					$(this).infiniteCarousel(opts);
1135				});
1136			}
1137			break;
1138		case "datepicker":
1139		case "datetimepicker":
1140			if (jqueryTiki.ui) {
1141				switch (type) {
1142					case "jscalendar":	// replacements for jscalendar
1143										// timestamp result goes in the options.altField
1144						if (typeof options.altField === "undefined") {
1145							alert("jQuery.ui datepicker jscalendar replacement setup error: options.altField not set for " + $(this).attr("id"));
1146							debugger;
1147						}
1148						opts = {
1149							showOn: "both",
1150							buttonText: '',
1151							dateFormat: jqueryTiki.shortDateFormat,
1152							timeFormat: jqueryTiki.shortTimeFormat,
1153							showButtonPanel: true,
1154							altFormat: "@",
1155							altFieldTimeOnly: false,
1156							onClose: function(dateText, inst) {
1157								$.datepickerAdjustAltField(func, inst);
1158							}
1159						};
1160						break;
1161					default:
1162						opts = {
1163							showOn: "both",
1164							buttonText: '',
1165							dateFormat: jqueryTiki.shortDateFormat,
1166							showButtonPanel: true,
1167							firstDay: jqueryTiki.firstDayofWeek
1168						};
1169						break;
1170				}
1171				$.extend(opts, options);
1172				if (func === "datetimepicker") {
1173					return this.each(function() {
1174							$(this).datetimepicker(opts);
1175						});
1176				} else {
1177					return this.each(function() {
1178						$(this).datepicker(opts);
1179					});
1180				}
1181			}
1182			break;
1183		case "accordion":
1184			if (jqueryTiki.ui) {
1185				opts = {
1186						autoHeight: false,
1187						collapsible: true,
1188						navigation: true
1189//						change: function(event, ui) {
1190//							// sadly accordion active property is broken in 1.7, but fix is coming in 1.8 so TODO
1191//							setCookie(ui, ui.options.active, "accordion");
1192//						}
1193					};
1194				$.extend(opts, options);
1195				return this.each(function() {
1196					$(this).accordion(opts);
1197				});
1198			}
1199			break;
1200		case "chosen":
1201			if (jqueryTiki.chosen) {
1202				opts = { allow_single_deselect: true, search_contains: true };		// allow_single_deselect happens if first item is empty
1203				if ($("html").attr("dir") === "rtl") {
1204					$(this).addClass("chosen-rtl");
1205				}
1206				$.map({		// translate the strings
1207					placeholder_text_multiple: "Select Some Options",
1208					placeholder_text_single: "Select an Option",
1209					no_results_text: "No results match"
1210				}, function (v, k) {
1211					opts[k] = tr(v);
1212				});
1213				$.extend(opts, options);
1214				return this.each(function() {
1215					var opts2 = $.extend({}, opts),
1216						$select = $(this);
1217
1218					if ($select.is(":hidden") && !opts.width) {
1219						// from https://github.com/harvesthq/chosen/pull/1580
1220						var $hiddenElement = $select.clone().appendTo("body");
1221						if ($select.is('.form-control')) {
1222							opts2.width = '100%';
1223						} else {
1224							opts2.width = $hiddenElement.outerWidth();
1225						}
1226						$hiddenElement.remove();
1227					}
1228					var hidden = $();
1229					$select.chosen(opts2)
1230						.on('chosen:showing_dropdown', function(){
1231							hidden = $(this).parents(":not(body,.modal)").filter(function(){
1232								return $(this).css('overflow') === 'hidden';
1233							}).css('overflow', 'visible');
1234						})
1235						.on('chosen:hiding_dropdown', function(){
1236							hidden.css('overflow', 'hidden');
1237						});
1238
1239					if (jqueryTiki.chosen_sortable && $select.prop("multiple")) {
1240						var $choices = $select.next(".chosen-container-multi").find("> .chosen-choices");
1241						$choices
1242							.mousedown(function (event) {
1243								if ($(event.target).is('span')) {
1244									event.stopPropagation();
1245								}
1246							})
1247							.sortable({
1248								items: "li:not(.search-field)"
1249							})
1250							.parents("form:first").submit(function () {
1251								if (! $(this).data("chosen-multi-submitted") && ! $select.hasClass("nochosen")) {
1252									var $options = $choices.find("li:not(.search-field) a").map(function () {
1253										return $select.find("option")[$(this).data("option-array-index")];
1254									});
1255									if ($options.length) {
1256										$select.children().remove();
1257										$select.append($options).change();
1258									}
1259									$(this).data("chosen-multi-submitted", true);
1260								}
1261							});
1262					}
1263				});
1264			}
1265			break;
1266	}	// end switch(func)
1267};
1268
1269(function($) {
1270	$.datepickerAdjustAltField = function(func, inst) {
1271		$.datepicker._updateAlternate(inst);	// make sure the hidden field is up to date
1272		var val = $(inst.settings.altField).val(), timestamp;
1273		if (func === "datetimepicker") {
1274			val = val.substring(0, val.indexOf(" "));
1275			timestamp = parseInt(val / 1000, 10);
1276			if (!timestamp || isNaN(timestamp)) {
1277				$.datepicker._setDateFromField(inst);	// seems to need reminding when starting empty
1278				$.datepicker._updateAlternate(inst);
1279				val = $(inst.settings.altField).val();
1280				val = val.substring(0, val.indexOf(" "));
1281				timestamp = parseInt(val / 1000, 10);
1282			}
1283			if (timestamp && inst.settings && inst.settings.timepicker) {	// if it's a datetimepicker add on the time
1284				var time = inst.settings.timepicker.hour * 3600 +
1285					inst.settings.timepicker.minute * 60 +
1286					inst.settings.timepicker.second;
1287				timestamp += time;
1288			}
1289		} else {
1290			timestamp = parseInt(val / 1000, 10);
1291		}
1292		$(inst.settings.altField).val(timestamp ? timestamp : "").change();
1293	};
1294
1295	// the jquery.ui _gotoToday function doesn't seem to work any more, so override that and add a call to _setDate
1296	$.datepicker._jquibase_gotoToday = $.datepicker._gotoToday;
1297	$.datepicker._gotoToday = function (id) {
1298		var inst = this._getInst($(id)[0]);
1299		this._jquibase_gotoToday(id);
1300		this._setDate(inst, new Date());
1301		// the alternate field gets updated when the dialog closes
1302	};
1303
1304
1305	/**
1306	 * Adds annotations to the content of text in ''container'' based on the
1307	 * content found in selected dts.
1308	 *
1309	 * Used in comments.tpl
1310	 */
1311	$.fn.addnotes = function( container ) {
1312		return this.each(function(){
1313			var comment = this;
1314			var text = $('dt:contains("note")', comment).next('dd').text();
1315			var title = $('h4:first', comment).clone();
1316			var body = $('.comment-body:first', comment).clone();
1317			body.find('dt:contains("note")').closest('dl').remove().addClass('card');
1318
1319			if( text.length > 0 ) {
1320				var parents = container.find(':contains("' + text + '")').parent();
1321				var node = container.find(':contains("' + text + '")').not(parents)
1322					.addClass('note-editor-text alert-info')
1323					.each( function() {
1324						var child = $('dl.note-list',this);
1325						if( ! child.length ) {
1326							child = $('<dl class="note-list list-group-item-info"/>')
1327								.appendTo(this)
1328								.hide();
1329
1330							$(this).click( function() {
1331								child.toggle();
1332							} );
1333						}
1334
1335						child.append( title )
1336							.append( $('<dd/>').append(body) );
1337					} );
1338			}
1339		});
1340	};
1341
1342	/**
1343	 * Convert a zone to a note editor by attaching handlers on mouse events.
1344	 */
1345	$.fn.noteeditor = function (editlink, link) {
1346		var hiddenParents = null;
1347		var annote = $(link)
1348			.click( function( e ) {
1349				e.preventDefault();
1350
1351				var $block = $('<div/>');
1352				var annotation = $(this).attr('annotation');
1353				$(this).fadeOut(100);
1354
1355				$block.load(editlink.attr('href'), function () {
1356					var msg = "";
1357					if (annotation.length < 20) {
1358						msg = tr("The text you have selected is quite short. Select a longer piece to ensure the note is associated with the correct text.") + "<br />";
1359					}
1360
1361					msg = "<p class='description comment-info'>" + msg + tr("Tip: Leave the first line as it is, starting with \";note:\". This is required") + "</p>";
1362					$block.prepend($(msg));
1363					$('textarea', this)
1364						.val(';note:' + annotation + "\n\n").focus();
1365
1366					$('form', this).submit(function () {
1367						$.post($(this).attr('action'), $(this).serialize(), function () {
1368							$block.dialog('destroy');
1369							// update the comments list
1370							editlink.closest('.comment-container').reload();
1371						});
1372						return false;
1373					});
1374
1375					$block.dialog({
1376						modal: true,
1377						width: 500,
1378						height: 400
1379					});
1380				});
1381			} )
1382			.appendTo(document.body);
1383
1384			$(this).mouseup(function( e ) {
1385				var range;
1386				if( window.getSelection && window.getSelection().rangeCount ) {
1387					range = window.getSelection().getRangeAt(0);
1388				} else if( window.selection ) {
1389					range = window.selection.getRangeAt(0);
1390				}
1391
1392				if( range ) {
1393					var str = $.trim( range.toString() );
1394
1395					if( str.length && -1 === str.indexOf( "\n" ) ) {
1396						annote.attr('annotation', str);
1397						annote.fadeIn(100).position( {
1398							of: e,
1399							at: 'bottom left',
1400							my: 'top left',
1401							offset: '20 20'
1402						} );
1403					} else {
1404						if (annote.css("display") != "none") {
1405							annote.fadeOut(100);
1406						}
1407						if ($("form.comments").css("display") == "none") {
1408							$("form.comments").show();
1409						}
1410						if (hiddenParents) {
1411							hiddenParents.hide();
1412							hiddenParents = null;
1413						}
1414					}
1415				}
1416			});
1417	};
1418
1419	$.fn.browse_tree = function () {
1420		this.each(function () {
1421			$('.treenode:not(.done)', this)
1422				.addClass('done')
1423				.each(function () {
1424					if (getCookie($('ul:first', this).attr('data-id'), $('ul:first', this).attr('data-prefix')) !== 'o') {
1425						$('ul:first', this).css('display', 'none');
1426					}
1427					var $placeholder = $('span.ui-icon:first:not(.control)', this);
1428					if ($('ul:first', this).length) {
1429						var dir = $('ul:first', this).css('display') === 'block' ? 's' : 'e';
1430						if ($placeholder.length) {
1431							$placeholder.replaceWith('<span class="flipper ui-icon ui-icon-triangle-1-' + dir + '" style="float: left;margin-top:.2em;"/>');
1432						} else {
1433							$(this).prepend('<span class="flipper ui-icon ui-icon-triangle-1-' + dir + '" style="float: left;margin-top:.2em;"/>');
1434						}
1435					} else {
1436						if ($placeholder.length) {
1437							$placeholder.replaceWith('<span style="float:left;width:16px;height:16px;margin-top:.2em;"/>');
1438						} else {
1439							$(this).prepend('<span style="float:left;width:16px;height:16px;margin-top:.2em;"/>');
1440						}
1441					}
1442					if ($('div.checkbox', this).length) {
1443						$('div.checkbox', this).css("margin-left", "16px");
1444					}
1445				});
1446
1447			$('.flipper:not(.done)')
1448				.addClass('done')
1449				.css('cursor', 'pointer')
1450				.click(function () {
1451					var body = $(this).parent().find('ul:first');
1452					if ('block' === body.css('display')) {
1453						$(this).removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');
1454						body.hide('fast');
1455						setCookie(body.data("id"), "", body.data("prefix"));
1456					} else {
1457						$(this).removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-s');
1458						body.show('fast');
1459						setCookie(body.data("id"), "o", body.data("prefix"));
1460					}
1461				});
1462		});
1463
1464		return this;
1465	};
1466
1467	var fancy_filter_create_token = function(value, label) {
1468		var close, token;
1469		console.log();
1470
1471		close = $('<span class="ui-icon ui-icon-close"/>')
1472			.click(function () {
1473				var ed = $(this).parent().parent();
1474				$(this).parent().remove();
1475				ed.change();
1476				return false;
1477			});
1478
1479		token = $('<span class="token"/>')
1480			.attr('data-value', value)
1481			.text(label)
1482			.attr('contenteditable', false)
1483			.disableSelection()
1484			.append(close);
1485
1486		return token[0];
1487	};
1488
1489	var fancy_filter_build_init = function(editable, str, options) {
1490		if (str === '') {
1491			str = '&nbsp;';
1492		}
1493
1494		editable.html(str.replace(/(\d+)/g, '<span>$1</span>'));
1495
1496		if (options && options.map) {
1497			editable.find('span').each(function () {
1498				var val = $(this).text();
1499				$(this).replaceWith(fancy_filter_create_token(val, JSON.parse(options.map)[val] ? JSON.parse(options.map)[val] : val));
1500			});
1501		}
1502	};
1503
1504	$.fn.fancy_filter = function (operation, options) {
1505		this.each(function () {
1506			switch (operation) {
1507			case 'init':
1508				var editable = $('<div class="fancyfilter form-control"/>'), input = this;
1509
1510				if (editable[0].contentEditable !== null) {
1511					fancy_filter_build_init(editable, $(this).val(), options);
1512					editable.attr('contenteditable', true);
1513					$(this).after(editable).hide();
1514				}
1515
1516				editable
1517					.keyup(function() {
1518						$(this).change();
1519						$(this).mouseup();
1520					})
1521					.change(function () {
1522						$(input).val($('<span/>')
1523							.html(editable.html())
1524							.find('span').each(function() {
1525								$(this).replaceWith(' ' + $(this).attr('data-value') + ' ');
1526							})
1527							.end().text().replace(/\s+/g, ' '));
1528					})
1529					.mouseup(function () {
1530						input.lastRange = window.getSelection().getRangeAt(0);
1531					});
1532
1533				break;
1534			case 'add':
1535				var editable = $(this).next();
1536				editable.find("span[data-value='"+ options.token +"']").remove();
1537				var node = fancy_filter_create_token(options.token, options.label);
1538				editable.append(node);
1539				editable.change();
1540				break;
1541			}
1542		});
1543
1544		return this;
1545	};
1546
1547	$.fn.drawGraph = function () {
1548		this.each(function () {
1549			var $this = $(this);
1550			var width = $this.width();
1551			var height = $this.height() ? $this.height() : Math.ceil( width * 9 / 16 );
1552			var nodes = $this.data('graph-nodes');
1553			var edges = $this.data('graph-edges');
1554
1555			var g = new Graph;
1556			$.each(nodes, function (k, i) {
1557				g.addNode(i);
1558			});
1559			$.each(edges, function (k, i) {
1560				var style = { directed: true };
1561				if( i.preserve ) {
1562					style.color = 'red';
1563				}
1564				g.addEdge( i.from, i.to, style );
1565			});
1566
1567			var layouter = new Graph.Layout.Spring(g);
1568			layouter.layout();
1569
1570			var renderer = new Graph.Renderer.Raphael($this.attr('id'), g, width, height );
1571			renderer.draw();
1572		});
1573
1574		return this;
1575	};
1576
1577	/**
1578	 * Handle textarea and input text selections
1579	 * Code from:
1580	 *
1581	 * jQuery Autocomplete plugin 1.1
1582	 * Copyright (c) 2009 Jörn Zaefferer
1583	 *
1584	 * Dual licensed under the MIT and GPL licenses:
1585	 *   http://www.opensource.org/licenses/mit-license.php
1586	 *   http://www.gnu.org/licenses/gpl.html
1587	 *
1588	 * Now deprecated and replaced in Tiki 7 by jquery-ui autocomplete
1589	 */
1590	$.fn.selection = function(start, end) {
1591		if (start !== undefined) {
1592			if (end === undefined) {
1593				end = start;
1594			}
1595			return this.each(function() {
1596				if( this.selectionStart) {
1597					this.selectionStart = start;
1598					this.selectionEnd = end;
1599				} else if( this.setSelectionRange ){
1600					this.setSelectionRange(start, end);
1601				} else if( this.createTextRange ){
1602					var selRange = this.createTextRange();
1603					if (start == end) {
1604						selRange.move("character", start);
1605						selRange.select();
1606					} else {
1607						selRange.collapse(true);
1608						selRange.moveStart("character", start);
1609						selRange.moveEnd("character", end - start);	// moveEnd is relative
1610						selRange.select();
1611					}
1612				}
1613			});
1614		}
1615		var field = this[0];
1616		if( field.selectionStart !== undefined) {
1617			return {
1618				start: field.selectionStart,
1619				end: field.selectionEnd
1620			}
1621		} else if ( field.createTextRange ) {
1622			// from http://the-stickman.com/web-development/javascript/finding-selection-cursor-position-in-a-textarea-in-internet-explorer/
1623			// The current selection
1624			var range = document.selection.createRange();
1625			// We'll use this as a 'dummy'
1626			var stored_range = range.duplicate();
1627			// Select all text
1628			stored_range.moveToElementText( field );
1629			// Now move 'dummy' end point to end point of original range
1630			stored_range.setEndPoint( 'EndToEnd', range );
1631			// Now we can calculate start and end points
1632			var textProperty = range.htmlText ? "htmlText" : "text";	// behaviour changed in IE10 (approx) so htmlText has unix line-ends which w
1632orks (not 100% sure why)
1633			var selectionStart = stored_range[textProperty].length - range[textProperty].length;
1634			var selectionEnd = selectionStart + range[textProperty].length;
1635			return {
1636				start: selectionStart,
1637				end: selectionEnd
1638			}
1639
1640		}	};
1641
1642	$.fn.comment_toggle = function () {
1643		this.each(function () {
1644			var $target = $(this.hash);
1645			$target.hide();
1646
1647			$(this).click(function () {
1648				if ($target.is(':visible')) {
1649					$target.hide(function () {
1650						$(this).empty();
1651					});
1652				} else {
1653					$target.comment_load($(this).attr('href'));
1654				}
1655
1656				return false;
1657			});
1658			if (location.search.indexOf("comzone=show") > -1 || location.hash.match(/threadId=?(\d+)/)) {
1659				var comButton = this;
1660				setTimeout(function() {
1661					$(comButton).click();
1662				}, 500);
1663			}
1664		});
1665
1666		return this;
1667	};
1668
1669	$.fn.comment_load = function (url) {
1670		var $top = $('#top');
1671		$('.note-list', $top).remove();
1672
1673		this.each(function () {
1674			var comment_container = this;
1675			if (! comment_container.reload) {
1676				comment_container.reload = function () {
1677					$(comment_container).empty().comment_load(url);
1678				};
1679			}
1680			$(this).addClass('comment-container');
1681			$(this).load(url, function (response, status) {
1682				$(this).show();
1683
1684				if (jqueryTiki.useInlineComment && ! jqueryTiki.useInlineAnnotations) {
1685					$('.comment.inline dt:contains("note")', this)
1686						.closest('.comment')
1687						.addnotes($top);
1688
1689					$top.noteeditor($('.comment-form:last a', comment_container), '#note-editor-comment');
1690				}
1691
1692				$('.button.comment-form.autoshow a').addClass('autoshown').click().removeClass('autoshown'); // allow autoshowing of comment forms through autoshow css class
1693
1694				var match = location.hash.match(/threadId=?(\d+)/);
1695				if (match) {
1696					var $tab = $(this).parents(".tab-pane"),
1697						$tabContent = $(this).parents(".tab-content");
1698
1699					// if we're in an inactive tab then show the right one for this comment threadId
1700					if ($tab.length && ! $tab.is(".active")) {
1701						$tabContent.find(".tab-pane").each(function (index) {
1702							if (this === $tab[0]) {
1703								$(".nav-tabs li:nth(" + index + ") a", $tabContent.parent()).tab("show");
1704							}
1705						})
1706					}
1707					var $comment = $(".comment[data-comment-thread-id=" + match[1] + "]");
1708					var top = $comment.offset().top;
1709					$('html, body').animate({
1710						scrollTop: top
1711					}, 2000, function () {
1712						$comment.animate({
1713							backgroundColor: "#ff8"
1714						}, 250, function () {
1715							$comment.animate({
1716								backgroundColor: ""
1717							}, 1000);
1718						});
1719					});
1720
1721				}
1722			});
1723		});
1724
1725		return this;
1726	};
1727
1728	$(document).on('click', '.comment-form.buttons > a.btn', function () {
1729		var comment_container = $(this).closest('.comment-container, .ui-dialog-content')[0];
1730
1731		$('.comment-form form:not(.commentRatingForm)', comment_container).each(function() {		// remove other forms apart from the ratings vote form
1732			var $p = $(this).parent();
1733			$p.empty().addClass('button').addClass('buttons').append($p.data('previous'));
1734		});
1735		if (!$(this).hasClass('autoshown')) {
1736			$(".comment").each(function () {
1737				$("article > *:not(ol)", this).each(function () {
1738					$(this).css("opacity", 0.6);
1739				});
1740			});
1741		}
1742		$(this).parents('.comment:first').find("*").css("opacity", 1);
1743
1744		var $formContainer = null;
1745		if ($(this).data('target')) {
1746			$formContainer = $($(this).data('target'));
1747		} else {
1748			$formContainer = $(this).parents('.buttons');
1749		}
1750		$(this).parents('.buttons').data('previous', $(this).siblings().addBack()).empty().removeClass('buttons').removeClass('button');
1751
1752		// Update buttons if loaded as a modal
1753		$('.modal.fade.show').trigger('tiki.modal.redraw');
1754
1755		$formContainer.load($(this).attr('href'), function () {
1756			var form = $('form', this).submit(function () {
1757				var errors, current = this;
1758				$(current).tikiModal(tr("Saving..."));
1759				//Synchronize textarea and codemirror before comment is posted
1760				if (typeof syntaxHighlighter.sync === 'function') {
1761					syntaxHighlighter.sync($(current).find("textarea.wikiedit"));
1762				}
1763				$.post($(current).attr('action'), $(current).serialize(), function (data, st) {
1764					$(current).tikiModal();
1765					if (data.threadId) {
1766						let threadId = data.threadId;
1767						$(current).closest('.comment-container').reload();
1768						$('span.count_comments').each(function () {
1769
1770							var action = $(current).attr('action').match(/tiki-comment-(\w*)/),
1771								count = parseInt($(this).text());
1772
1773							if (action) {
1774								//noinspection FallThroughInSwitchStatementJS
1775								switch (action[1]) {
1776									case "remove":
1777										count--;
1778										break;
1779									case "post":
1780										count++;
1781										// fall through to adjust threadId if necessary
1782									case "edit":
1783									case "moderate":
1784										location.hash = location.hash.replace(/threadId=\d+/, "threadId=" + threadId);
1785										break;
1786								}
1787								$(this).text(count);
1788							}
1789						});
1790						if (data.feedback && data.feedback[0]) {
1791							alert(data.feedback.join("\n"));
1792						}
1793					} else {
1794						errors = $('ol.errors', form).empty();
1795						if (!errors.length) {
1796							$(':submit', current).after(errors = $('<ul class="alert-warning"/>'));
1797						}
1798
1799						$.each(data.errors, function (k, v) {
1800							errors.append($('<li/>').text(v));
1801						});
1802					}
1803				}, 'json');
1804				return false;
1805			});
1806
1807			//allow syntax highlighting
1808			if ($.fn.flexibleSyntaxHighlighter) {
1809				window.codeMirrorEditor = [];
1810				form.find('textarea.wikiedit').flexibleSyntaxHighlighter();
1811			}
1812		});
1813		return false;
1814	});
1815
1816	// scroll to post if #threadId on url in forums
1817	if ($("body.tiki_forums").length) {
1818		let match = location.hash.match(/threadId=?(\d+)/);
1819		if (match) {
1820			let $comment = $("#" + match[0] + ".post");
1821			let top = $comment.offset().top;
1822			$('html, body').animate({
1823				scrollTop: top
1824			}, 2000, function () {
1825				$comment.animate({
1826					backgroundColor: "#ff8"
1827				}, 250, function () {
1828					$comment.animate({
1829						backgroundColor: ""
1830					}, 1000);
1831				});
1832			});
1833		}
1834	}
1835
1836	$.fn.input_csv = function (operation, separator, value) {
1837		this.each(function () {
1838			var values = $(this).val().split(separator);
1839			if (values[0] === '') {
1840				values.shift();
1841			}
1842
1843			if (operation === 'add' && -1 === values.indexOf("" + value)) {
1844				values.push(value);
1845			} else if (operation === 'delete') {
1846				value = String(value);
1847				while (-1 !== $.inArray(value, values)) {
1848					values.splice($.inArray(value, values), 1);
1849				}
1850			}
1851
1852			$(this).val(values.join(separator));
1853		});
1854
1855		return this;
1856	};
1857
1858	$.service = function (controller, action, query) {
1859		var append = '';
1860
1861		if (query) {
1862			append = '?' + $.buildParams(query);
1863		}
1864
1865		if (action) {
1866			return 'tiki-' + controller + '-' + action + append;
1867		} else {
1868			return 'tiki-' + controller + '-x' + append;
1869		}
1870	};
1871
1872	$.serviceUrl = function (options) {
1873		var o = $.extend({}, options), controller = options.controller, action = options.action;
1874		delete(o.controller);
1875		delete(o.action);
1876		return $.service(controller, action, o);
1877	};
1878
1879	$.buildParams = function (query, prefix, suffix) {
1880		prefix = prefix || '';
1881		suffix = suffix || '';
1882
1883		return $.map(query, function (v, k) {
1884			if ($.isPlainObject(v)) {
1885				return $.buildParams(v, k + '[', ']');
1886			} else {
1887				return prefix + k + suffix + '=' + tiki_encodeURIComponent(v);
1888			}
1889		}).join('&');
1890	};
1891
1892	$.fn.serviceDialog = function (options) {
1893		this.each(function () {
1894			var $dialog = $('<div/>'), origin = this, buttons = {};
1895			$(this).append($dialog).data('serviceDialog', $dialog);
1896
1897			if (! options.hideButtons) {
1898				buttons[tr('OK')] = function () {
1899					$dialog.find('form:visible').submit();
1900				};
1901				buttons[tr('Cancel')] = function () {
1902					$dialog.dialog('close');
1903					if ($dialog.data('ui-dialog')) {
1904						$dialog.dialog('destroy');
1905					}
1906				};
1907			}
1908
1909			$dialog.dialog({
1910				title: options.title,
1911				minWidth: options.width ? options.width : 500,
1912				height: (options.fullscreen ? $window.height() - 20 : (options.height ? options.height : 600)),
1913				width: (options.fullscreen ? $window.width() - 20 : null),
1914				close: function () {
1915					if (options.close) {
1916						options.close.apply([], this);
1917					}
1918					if ($(this).data('ui-dialog')) {
1919						$(this).dialog('destroy').remove();
1920					}
1921				},
1922				buttons: buttons,
1923				modal: options.modal,
1924				zIndex: options.zIndex
1925			});
1926
1927			$dialog.loadService(options.data, $.extend(options, {origin: origin}));
1928		});
1929
1930		return this;
1931	};
1932	$.fn.loadService =  function (data, options) {
1933		var $dialog = this, controller = options.controller, action = options.action, url;
1934
1935		this.each(function () {
1936			if (! this.reload) {
1937				this.reload = function () {
1938					$(this).loadService(data, options);
1939				};
1940			}
1941		});
1942
1943		if (typeof data === "string") {
1944			data = parseQuery(data);
1945		}
1946		if (data && data.controller) {
1947			controller = data.controller;
1948		}
1949
1950		if (data && data.action) {
1951			action = data.action;
1952		}
1953
1954		if (options.origin && $(options.origin).is('a')) {
1955			url = $(options.origin).attr('href');
1956		} else if (options.url) {
1957			url = options.url;
1958		} else {
1959			url = $.service(controller, action);
1960		}
1961
1962		$dialog.tikiModal(tr("Loading..."));
1963
1964		$.ajax(url, {
1965			data: data,
1966			error: function (jqxhr) {
1967				$dialog.html(jqxhr.responseText);
1968			},
1969			success: function (data) {
1970				$dialog.html(data);
1971				$dialog.find('.ajax').click(function (e) {
1972					$dialog.loadService(null, {origin: this});
1973					return false;
1974				});
1975				$dialog.find('.service-dialog').click(function (e) {
1976					if ($dialog.data('ui-dialog')) {
1977						$dialog.dialog('close');
1978					}
1979					return true;
1980				});
1981
1982				$dialog.find('form .submit').hide();
1983
1984				$dialog.find('form:not(.no-ajax)').off("submit").submit(ajaxSubmitEventHandler(function (data) {
1985					data = (data ? data : {});
1986
1987					if (data.FORWARD) {
1988						$dialog.loadService(data.FORWARD, options);
1989					} else if ($dialog.data('ui-dialog')) {
1990						$dialog.dialog('destroy').remove();
1991					}
1992
1993					if (options.success) {
1994						options.success.apply(options.origin, [data]);
1995					}
1996				}));
1997
1998				if (options.load) {
1999					options.load.apply($dialog[0], [data]);
2000				}
2001
2002				$('.confirm-prompt', this).requireConfirm({
2003					success: function (data) {
2004						if (data.FORWARD) {
2005							$dialog.loadService(data.FORWARD, options);
2006						} else {
2007							$dialog.loadService(options.data, options);
2008						}
2009					}
2010				});
2011			},
2012			complete: function () {
2013				$dialog.tikiModal();
2014				if ($dialog.find('form').size() == 0 && $dialog.data('ui-dialog')) {
2015					// If the result contains no form, skip OK/Cancel, and just allow to close
2016					var buttons = $dialog.dialog('option', 'buttons'), n = {};
2017					if (buttons[tr('Cancel')]) {
2018						n[tr('OK')] = buttons[tr('Cancel')];
2019						$dialog.dialog('option', 'buttons', n);
2020					}
2021				}
2022			}
2023		});
2024	};
2025
2026	$.fn.confirmationDialog = function (options) {
2027		var modal = $('#bootstrap-modal'),
2028			modalHeader = modal.find('.modal-header'),
2029			modalBody = $('<div class="modal-body"><p></p></div>'),
2030			modalFooter = $('<div class="modal-footer"></div>'),
2031			modalFooterConfirm = $('<button type="button" class="btn btn-success">' + tr('Confirm') + '</button>'),
2032			modalFooterClose = $('<button type="button" class="btn btn-secondary" data-dismiss="modal">' + tr('Close') + '</button>');
2033
2034		//needed because modal-content is globally being cleared after appearing for the first time
2035		if (!modalHeader || modalHeader.length === 0) {
2036			modal.find('.modal-content').append('<div class="modal-header"><h4></h4></div>');
2037			modalHeader = modal.find('.modal-header');
2038		}
2039
2040		if (options.title) {
2041			modalHeader.find('h4').text(options.title);
2042		} else {
2043			modalHeader.find('h4').text(tr('Confirmation Modal'));
2044		}
2045
2046		if (options.message) {
2047			modalBody.find('p').text(options.message);
2048		} else {
2049			modalBody.find('p').text(tr('Please confirm you want to perform this action.'));
2050		}
2051
2052		modalFooter.append(modalFooterClose, modalFooterConfirm);
2053		modalHeader.after(modalBody);
2054		modalBody.after(modalFooter);
2055
2056		modal.on('hidden.bs.modal', function (e) {
2057			if (options.close) {
2058				options.close();
2059			}
2060		});
2061
2062		modalFooterConfirm.on('click', function (e) {
2063			modal.modal('hide');
2064			if (options.success) {
2065				options.success();
2066			}
2067		});
2068
2069		modal.modal('show');
2070
2071		return this;
2072	};
2073
2074	$.fn.requireConfirm = function (options) {
2075		this.click(function (e) {
2076			e.preventDefault();
2077			$(this).doConfirm(options);
2078			return false;
2079		});
2080
2081		return this;
2082	};
2083
2084	$.fn.doConfirm = function (options) {
2085		var message = options.message, link = this;
2086
2087		if (! message) {
2088			message = $(this).data('confirm');
2089		}
2090
2091		if (confirm (message)) {
2092			var $this = $(this);
2093			$this.tikiModal(" ");
2094
2095			$.ajax($(this).attr('href'), {
2096				type: 'POST',
2097				dataType: 'json',
2098				data: {
2099					'confirm': 1
2100				},
2101				success: function (data) {
2102					$this.tikiModal();
2103					options.success.apply(link, [data]);
2104				},
2105				error: function (jqxhr) {
2106					$this.tikiModal();
2107					$(link).closest('form').showError(jqxhr);
2108				}
2109			});
2110		}
2111	};
2112
2113	$.fn.showError = function (message) {
2114		if (message.responseText) {
2115			if (message.getResponseHeader("Content-Type").indexOf("text/html") === -1) {
2116				var data = JSON.parse(message.responseText);
2117				message = data.message;
2118			} else {
2119				message = $(message.responseText).text();	// can be html
2120			}
2121		} else if (typeof message !== 'string') {
2122			message = "";
2123		}
2124		this.each(function () {
2125			var parts, that = this;
2126			if (parts = message.match(/^<!--field\[([^\]]+)\]-->(.*)$/)) {
2127				field = parts[1];
2128				message = parts[2];
2129
2130				if (that[field]) {
2131					that = that[field];
2132				}
2133			}
2134
2135			var validate = false, errors = {}, field, $additional = $('<ul>');
2136
2137			if (jqueryTiki.validate) {
2138				validate = $(that).closest('form').validate()
2139			}
2140
2141			if (validate) {
2142				if (! $(that).attr('name')) {
2143					$(that).attr('name', $(that).attr('id'));
2144				}
2145
2146				if (that !== validate.currentForm) {
2147					field = $(that).attr('name');
2148				}
2149
2150				if (field) {
2151					errors[field] = message;
2152					validate.showErrors(errors);
2153				} else {
2154					// No specific field, assign as form error
2155					$additional.append($('<li>').text(message));
2156				}
2157
2158				setTimeout(function () {
2159					$('#tikifeedback li').filter(function () {
2160						return $(this).text() === message;
2161					}).remove();
2162
2163					if ($('#tikifeedback ul').is(':empty')) {
2164						$('#tikifeedback').empty();
2165					}
2166				}, 100);
2167			} else {
2168				$additional.append($('<li>').text(message));
2169			}
2170
2171			if (! $additional.is(':empty')) {
2172				// Write form errors at the top, please stop removing them
2173				$('.ajax-errors', this).remove();
2174				$('<div class="ajax-errors alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button></div>')
2175					.prependTo(this)
2176					.append($additional);
2177			}
2178
2179			// Style the bootstrap form-group as an error
2180			$('.form-group').removeClass('has-error')
2181				.find('label.error:visible')
2182				.addClass('form-text')
2183				.prepend('<span class="fas fa-flag"/> ')
2184				.closest('.form-group').addClass('has-error')
2185		});
2186
2187		return this;
2188	};
2189
2190	$.fn.clearError = function () {
2191		this.each(function () {
2192			$(this).closest('form').find('label.error[for="' + $(this).attr('name') + '"]').remove();
2193			$(this).closest('form').find('.form-group.has-error').removeClass('has-error');
2194		});
2195
2196		return this;
2197	};
2198
2199	// sort result containing all galleries
2200	function sortResult(result) {
2201		result.sort(function(a, b) {
2202			var titleA = a.parent_title.toUpperCase(); // ignore upper and lowercase
2203			var titleB = b.parent_title.toUpperCase(); // ignore upper and lowercase
2204			if (titleA < titleB) {
2205				return -1;
2206			}
2207			if (titleA > titleB) {
2208				return 1;
2209			}
2210
2211			// names must be equal
2212			return 0;
2213		});
2214
2215	}
2216
2217	// search the title of each parent gallery
2218	function parentTitle(result, parent) {
2219		var title='';
2220		$.each(result, function(key, value) {
2221			if(value.object_id == parent){
2222				title= value.title + ' > ';
2223			}
2224		});
2225		return title;
2226	}
2227
2228	function loadSelectorData(filter, args, success) {
2229		if (! $.object_selector_cache) {
2230			$.object_selector_cache = {};
2231		}
2232
2233		if ($.isFunction(args)) {
2234			success = args;
2235			args = {};
2236		}
2237
2238		var item, url;
2239
2240		url = $.service('search', 'lookup', $.extend(args, {
2241			filter: filter
2242		}));
2243
2244		if (item = $.object_selector_cache[url]) {
2245			if (item.data) {
2246				success(item.data);
2247			} else {
2248				item.queue.push(success);
2249			}
2250		} else {
2251			item = $.object_selector_cache[url] = {
2252				data: null,
2253				queue: [success]
2254			};
2255			$.getJSON(url, function (data) {
2256				item.data = data;
2257				$.each(item.queue, function (k, f) {
2258					f(data);
2259				});
2260				item.queue = [];
2261			});
2262		}
2263	}
2264	$._object_selector_add_item = function (type, $select, $results, parent_title, item, title, status_icon, selected) {
2265		var checkname = $select.closest('.object-selector, .object-selector-multi')
2266			.find('.primary').attr('id') + '_sel';
2267		var suffix = $results.find('.form-check').lenght || 0;
2268
2269		$('<option>')
2270			.val(item)
2271			.data('label', title)
2272			.text(parent_title + '' + title.replace(/\n/g, " / "))  //replace newline with a slash since it's in a select
2273			.prop('selected', selected)
2274			.appendTo($select);
2275
2276		$('<div class="form-check"><input type="' + type + '" class="form-check-input" ><label class="form-check-label"></label></div>')
2277			.find('label').append(status_icon ? status_icon + ' ' + title : title).end()
2278			.find(':radio, :checkbox')
2279				.attr('name', checkname)
2280				.prop('checked', selected)
2281				.val(item)
2282			.end()
2283			.appendTo($results);
2284	};
2285
2286	$.fn._object_selector_update_results = function (type, result, initial) {
2287		var $container = this,
2288			$results = $container.find('.results'),
2289			$select = $container.find('select'),
2290			$noresults = $('.no-result', this),
2291			selection = [];
2292
2293		this.find(':radio:checked, :checkbox:checked')
2294			.not('.protected')
2295			.each(function () {
2296				selection.push($(this).val());
2297			});
2298
2299		this.find(':radio:not(:checked), :checkbox:not(:checked)')
2300			.not('.protected')
2301			.closest('.form-check')
2302			.remove();
2303
2304		$select
2305			.find('option:not(:checked)')
2306			.not('.protected')
2307			.remove();
2308
2309		$noresults.toggleClass('d-none', selection.length !== 0);
2310
2311		// add all galleries parent titles
2312		$.each(result, function (key, value) {
2313			value.parent_title = parentTitle(result, value.parent_id);
2314			result[key] = value;
2315		});
2316
2317		// sort result by galleries parent titles
2318		sortResult(result);
2319
2320		$.each(result, function (key, value) {
2321			var current = value.object_type + ':' + value.object_id;
2322			var selected = false;
2323
2324			if (value.object_id == '') {
2325				current = value.object_type;
2326			}
2327
2328			var currentValue = $select.data('current-value');
2329			if (currentValue == current) {
2330				selected = true;
2331			}
2332
2333			if (-1 === selection.indexOf(current)) {
2334				if (initial) {
2335					$._object_selector_add_item(type, $select, $([]), value.parent_title, current, value.title, value.status_icon, selected);
2336				} else {
2337					$._object_selector_add_item(type, $select, $results, value.parent_title, current, value.title, value.status_icon, selected);
2338				}
2339			} else {
2340				$("option[value='" + current + "']", $select).text(value.title);
2341			}
2342		});
2343
2344		$select.trigger('chosen:updated');
2345	};
2346
2347	$.fn.object_selector = function (action, value, title) {
2348		var args = arguments;
2349
2350		this.each(function () {
2351			var input = this
2352				, $simple = $(this).prev()
2353				, filter = $(input).data('filters')
2354				, threshold = $(input).data('threshold')
2355				, format = $(input).data('format') || ''
2356				, sort = $(input).data('sort') || 'score_desc'
2357				, parentobject = $(input).data('parent')
2358				, parentkey = $(input).data('parentkey')
2359				, searchField = $(input).data('searchfield') || 'title'
2360			;
2361
2362			$(input).addClass('primary').hide();
2363			$simple.hide();
2364
2365			var $spinner = $(this).parent(),
2366				$container = $(input).closest('.object-selector'),
2367				$select = $container.find('select').first(),
2368				$filter = $container.find(':text.filter').first(),
2369				$search = $container.find('input.search').first(),
2370				$panel = $container.find('.card').first();
2371
2372			if (action === 'set') {
2373				$select.val(value);
2374				if ($select.val() !== value && title) {
2375					// for multilingual, object returned is JSON. try to parse and split
2376					// with ' / ', otherwise catch and use title as is.
2377					try
2378					{
2379						var titleObj = JSON.parse(title);
2380						var titleArr = $.map(titleObj, function(el) { return el });
2381						title = titleArr.join(" / ");
2382					}
2383					catch(e)
2384					{
2385						// do nothing
2386					}
2387					$._object_selector_add_item('radio', $select, $container.find('.results'), null, value, title, null, true);
2388
2389					$select.trigger('chosen:updated');
2390				}
2391
2392				$(input)
2393					.val(value)
2394					.data('label', title)
2395					.change();
2396
2397				return;
2398			}
2399
2400			if (action === 'setfilter') {
2401				filter[args[1]] = args[2];
2402				$(input).data('filters', filter);
2403				$container.find('.too-many').hide();
2404				$search.click();
2405				return;
2406			}
2407
2408			if (parentobject && parentkey) {
2409				filter[parentkey] = $(parentobject).val();
2410				$(parentobject).on('change', function () {
2411					$(input)
2412						.data('use-threshold', 1)
2413						.object_selector('setfilter', parentkey, $(this).val());
2414				});
2415			}
2416
2417			if (threshold !== -1) {
2418				$spinner.tikiModal(" ");
2419				loadSelectorData(filter, {maxRecords: threshold, format: format, sort_order: sort}, function (data) {
2420					$container._object_selector_update_results('radio', data.resultset.result, true);
2421
2422					$spinner.tikiModal();
2423
2424					if (data.resultset.count <= threshold) {
2425						$select.parent().removeClass('d-none');
2426					} else {
2427						$panel.removeClass('d-none');
2428					}
2429				});
2430			} else {
2431				$panel.removeClass('d-none');
2432			}
2433			$panel.on('click', ':radio', function () {
2434				if ($(this).is(':checked')) {
2435					$(input).object_selector('set', $(this).val(), $(this).parent().text());
2436				}
2437			});
2438
2439			$(input).change(function () {
2440				var val = $(this).val(), id = null;
2441				if (val) {
2442					var splitarray = val.split(':');
2443					id = splitarray[1];
2444					if(splitarray.length > 2){
2445						for(var i = 2; i < splitarray.length; i++)
2446						{
2447							id = id + ':' + splitarray[i];
2448						}
2449					}
2450				}
2451
2452				if ($simple.val() != id) {
2453					$simple.val(id).change();
2454				}
2455			});
2456			$simple.change(function () {
2457				var target = filter.type + ':' + $(this).val();
2458
2459				if (filter.type && $(input).val() != target) {
2460					$(input).val(target).change();
2461				}
2462			});
2463			$select.change(function () {
2464				if ($(input).val() != $select.val()) {
2465					$(input).data('label', $select.find('option:selected').text());
2466					$(input).val($select.val()).change();
2467				}
2468			});
2469
2470			$search.click(function () {
2471				$spinner = $filter.parent().tikiModal(" ");
2472				var selectorArgs = {format: format, sort_order: sort};
2473				if ($(input).data('use-threshold') && threshold !== -1) {
2474					selectorArgs.maxRecords = threshold;
2475					$(input).data('use-threshold', 0);
2476				}
2477				filter[searchField] = $filter.val();
2478				loadSelectorData(filter, selectorArgs, function (data) {
2479					$container._object_selector_update_results('radio', data.resultset.result, false);
2480
2481					$spinner.tikiModal();
2482				});
2483			});
2484
2485			$filter.keypress(function (e) {
2486				if (e.which === 13) {
2487					e.preventDefault();
2488					$search.click();
2489				}
2490			});
2491		});
2492
2493		return this;
2494	};
2495
2496	$.fn.object_selector_multi = function (action) {
2497		var args = arguments;
2498		this.each(function () {
2499			var $textarea = $(this).hide().addClass('primary')
2500				, $container = $(this).closest('.object-selector-multi')
2501				, $select = $container.find('select')
2502				, $simpleinput = $textarea.prev(':text').hide()
2503				, $basic = $container.find('.basic-selector')
2504				, $panel = $container.find('.card')
2505				, $search = $container.find('input.search').first()
2506				, $filter = $container.find(':text.filter').first()
2507				, filter = $textarea.data('filters')
2508				, wildcard = $textarea.data('wildcard')
2509				, threshold = $textarea.data('threshold')
2510				, format = $textarea.data('format') || ''
2511				, parentobject = $textarea.data('parent')
2512				, parentkey = $textarea.data('parentkey')
2513				, sort = $textarea.data('sort') || 'score_desc'
2514				, initialValues = $select.val()
2515				, separator = $simpleinput.data('separator')
2516				, searchField = $textarea.data('searchfield') || 'title'
2517				, extratype = $textarea.data('extratype')
2518				;
2519
2520			if (action === 'setfilter') {
2521				filter[args[1]] = args[2];
2522				$textarea.data('filters', filter);
2523				$container.find('.too-many').hide();
2524				$search.click();
2525				return;
2526			}
2527
2528			if (parentobject && parentkey) {
2529				filter[parentkey] = $(parentobject).val();
2530				$(parentobject).on('change', function () {
2531					$textarea
2532						.data('use-threshold', 1)
2533						.object_selector_multi('setfilter', parentkey, $(this).val());
2534				});
2535			}
2536
2537			if (threshold !== -1) {
2538				$container.tikiModal(' ');
2539				loadSelectorData(filter, {maxRecords: threshold, format: format, sort_order: sort}, function (data) {
2540					$container.tikiModal('');
2541					var results = data.resultset.result;
2542					if (extratype) {
2543						var objectIndex = data.resultset.count;
2544						$.each(extratype, function(key, value) {
2545							results[objectIndex] = {"object_type": key, "object_id": "", "title": value};
2546							objectIndex++;
2547						});
2548					}
2549					$container._object_selector_update_results('checkbox', results, true);
2550
2551					if (data.resultset.count <= threshold) {
2552						$basic.removeClass('d-none');
2553					} else {
2554						$panel.removeClass('d-none');
2555						// add .nochosen here even though Chosen has been applied previously so chosen_sortable doesn't update the value
2556						$select.addClass("nochosen");
2557					}
2558				});
2559			} else {
2560				$panel.removeClass('d-none');
2561			}
2562
2563			$filter.keypress(function (e) {
2564				if (e.which === 13) {
2565					e.preventDefault();
2566					$search.click();
2567				}
2568			});
2569
2570			$search.click(function () {
2571				var $spinner = $filter.parent().tikiModal(" ");
2572				var selectorArgs = {format: format, sort_order: sort};
2573				if ($textarea.data('use-threshold') && threshold !== -1) {
2574					selectorArgs.maxRecords = threshold;
2575					$textarea.data('use-threshold', 0);
2576				}
2577				if (wildcard == 'y') {
2578					filter[searchField] = '*' + $filter.val() + '*';
2579				} else {
2580					filter[searchField] = $filter.val();
2581				}
2582				loadSelectorData(filter, selectorArgs, function (data) {
2583					$container._object_selector_update_results('checkbox', data.resultset.result, false);
2584					$spinner.tikiModal();
2585				});
2586			});
2587
2588			$panel.on('click', ':checkbox', function () {
2589				var list = $.makeArray($container.find(':checkbox:checked').map(function () {
2590					return $(this).val();
2591				}));
2592				$textarea.val(list.join("\n")).change();
2593			});
2594			$select.on('change', function () {
2595				var list = $(this).val() || [];
2596				$textarea.val(list.join("\n")).change();
2597			});
2598
2599			if (separator) {
2600				$textarea.on('change', function () {
2601					var lines = $(this).val().split("\n"), ids = [];
2602					$.each(lines, function (k, line) {
2603						var parts = line.split(':');
2604						if (parts.length === 2) {
2605							ids.push(parts[1]);
2606						}
2607					});
2608					$simpleinput.val(ids.join(separator)).change();
2609				});
2610			}
2611		});
2612	};
2613
2614	$.fn.sortList = function () {
2615		var list = $(this), items = list.children('li').get();
2616
2617		items.sort(function(a, b) {
2618			var compA = $(a).text().toUpperCase();
2619			var compB = $(b).text().toUpperCase();
2620			return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
2621		});
2622
2623		$.each(items, function(idx, itm) {
2624			list.append(itm);
2625		});
2626	};
2627	$.localStorage = {
2628		store: function (key, value) {
2629			var fullKey = this._build(key);
2630			if (window.localStorage) {
2631				if (value) {
2632					window.localStorage[fullKey] = $.toJSON({
2633						date: Date.now(),
2634						data: value
2635					});
2636				} else {
2637					delete window.localStorage[fullKey];
2638				}
2639			}
2640		},
2641		load: function (key, callback, fetch, duration) {
2642			var payload, fullKey = this._build(key);
2643
2644			if (window.localStorage && window.localStorage[fullKey]) {
2645				payload = JSON.parse(window.localStorage[fullKey]);
2646
2647				if (duration) {
2648					// Expired, refetch
2649					if (payload.date + duration*1000 < Date.now()) {
2650						fetch(function (data) {
2651							$.localStorage.store(key, data);
2652							callback(data);
2653						});
2654						return;
2655					}
2656				}
2657
2658				callback(payload.data);
2659			} else {
2660				fetch(function (data) {
2661					$.localStorage.store(key, data);
2662					callback(data);
2663				});
2664			}
2665		},
2666		_build: function (key) {
2667			// Use an alternate key to ensure old data structure
2668			// does not collide
2669			return key + "_2";
2670		}
2671	};
2672
2673	var favoriteList = [];
2674	$.fn.favoriteToggle = function () {
2675		this
2676			.each(function () {
2677				var type, obj, isFavorite, link = this;
2678				type = $(this).queryParam('type');
2679				obj = $(this).queryParam('object');
2680
2681
2682				isFavorite = function () {
2683					var ret = false;
2684					$.each(favoriteList, function (k, v) {
2685						if (v === type + ':' + obj) {
2686							ret = true;
2687							return false;
2688						}
2689					});
2690
2691					return ret;
2692				};
2693
2694				$(this).find('span').remove(); //removes the previous star icon
2695				$(this).prepend($('<span />').attr({
2696					'class' : isFavorite() ? 'fas fa-star fa-fw' : 'far fa-star fa-fw',
2697					'title' : isFavorite() ? tr('Remove from favorites') : tr('Add to favorites')
2698				}));
2699
2700				if (isFavorite()) {
2701					$(this).addClass( 'favorite_selected' );
2702					$(this).removeClass( 'favorite_unselected' );
2703				} else {
2704					$(this).addClass( 'favorite_unselected' );
2705					$(this).removeClass( 'favorite_selected' );
2706				}
2707				$(this)
2708					.filter(':not(".register")')
2709					.addClass('register')
2710					.click(function () {
2711						$.post($(this).attr('href'), {
2712							target: isFavorite() ? 0 : 1
2713						}, function (data) {
2714							favoriteList = data.list;
2715							$.localStorage.store($(link).data('key'), favoriteList);
2716
2717							$(link).favoriteToggle();
2718						}, 'json');
2719						return false;
2720					});
2721			});
2722		return this;
2723	};
2724
2725	$.fn.queryParam = function (name) {
2726		name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
2727		var regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
2728		var results = regex.exec(this[0].href);
2729
2730		if(results == null) {
2731			return "";
2732		} else {
2733			return decodeURIComponent(results[1].replace(/\+/g, " "));
2734		}
2735	};
2736
2737	$(function () {
2738		var list = $('.favorite-toggle');
2739
2740		if (list.length > 0) {
2741			$.localStorage.load(
2742				list.data('key'),
2743				function (data) {
2744					favoriteList = data;
2745					list
2746						.favoriteToggle()
2747						.removeClass('favorite-toggle');
2748				},
2749				function (recv) {
2750					$.getJSON($.service('favorite', 'list'), recv);
2751				},
2752				3600 // Valid for 1h
2753			);
2754		}
2755	});
2756
2757	// global ajax event handlers
2758	$document.ajaxComplete(function () {
2759		$('.favorite-toggle')
2760			.favoriteToggle()
2761			.removeClass('favorite-toggle');
2762	});
2763
2764	$document.ajaxError(function (event, jqxhr, settings, thrownError) {
2765
2766		if (settings.preventGlobalErrorHandle) {
2767			return;
2768		}
2769
2770		$(".modal.fade.show").modal("hide");
2771		var message;
2772		if (!thrownError && jqxhr.status !== 200) {
2773			if (jqxhr.status) {
2774				message = jqxhr.status + " " + jqxhr.statusText;
2775			} else {
2776				message = "AJAX: " + jqxhr.statusText + " " + jqxhr.status + " (" + jqxhr.state() + ") for URL: " + settings.url;
2777			}
2778		}
2779		if (message) {
2780			$("#tikifeedback").showError(message);
2781		}
2782	});
2783
2784	/**
2785	 * Show a loading spinner on top of a button (or whatever)
2786	 *
2787	 * @param $spinner empty or jq object $spinner		if empty, spinner is added and returned and element "disabled"
2788	 * 											if spinner then spinner is removed and element returned to normal
2789	 *
2790	 * @return jq object $spinner being shown or null when removing
2791	 */
2792
2793	$.fn.showBusy = function( $spinner ) {
2794		if (!$spinner) {
2795			var pos = $(this).position();
2796			$spinner = $("<img src='img/spinner.gif' alt='" + tr("Wait") + "' class='ajax-spinner' />").
2797					css({
2798						"position": "absolute",
2799						"top": pos.top + ($(this).height() / 2),
2800						"left": pos.left + ($(this).width() / 2) - 8
2801					}).data("target", this);
2802			$(this).parent().find(".ajax-spinner").remove();
2803			$(this).parent().append($spinner);
2804			$(this).attr("disabled", true).css("opacity", 0.5);
2805			return $spinner;
2806		} else {
2807			$($spinner.data("target")).attr("disabled", false).css("opacity", 1);
2808			$spinner.remove();
2809			return null;
2810		}
2811	}
2812
2813	//	copy tracker action column to 1st row if table has horizontal scrolling
2814	$('.table-responsive:not(.article-types)').each(function () {
2815		var table = $(this);
2816		// mobile friendly tables
2817		if (table.find('table:not(.tablesorter) tbody tr').width() - 10 > table.width()) {
2818			if (!table.hasClass('large-table-no-wrap')) table.addClass('large-table-no-wrap');
2819			if (screen.width <= 767) {
2820				$('<div class="toggle-table-wrap d-md-none"><button type="button" class="btn btn-link fas fa-toggle-off"></button></div>').insertBefore(table);
2821				var checkall = false;
2822				table.find('table.table:not(.caltable) th').each(function (e) {
2823					var header = $(this).html();
2824					if ($(this).find('div').hasClass('form-check') || ($(this).find('input').hasClass('form-check-input') && e == 0)) {
2825						$(this).addClass('visible-header');
2826						header = "";
2827					}
2828
2829					// page history exception
2830					if ($(this).find('input[name=compare]').length) {
2831						$(this).addClass('visible-header').addClass('compare-pages');
2832						header = "";
2833					}
2834
2835					table.find('table.table tbody tr').each(function () {
2836						var cell = $(this).find('td').eq(e);
2837						if (checkall) cell.addClass('checkmargin');
2838						if ($.trim(cell.html()) == '') {
2839							cell.addClass('hidecell');
2840						} else {
2841							cell.prepend("<div class='header' style='display:none'>" + header + "</div>");
2842						}
2843						if (e == 0) {
2844							if ((cell.hasClass('checkbox-cell') || cell.find('input:checkbox')) && cell.find('.header').html() == '') {
2845								cell.addClass('checkall').removeClass('checkmargin');
2846								checkall = true;
2847							}
2848						}
2849					});
2850				});
2851
2852				// this is for calendars
2853				table.find('table.caltable > tbody > tr:first > td.heading').each(function (e) {
2854					var header = $(this).html();
2855					table.find('table.caltable > tbody > tr:not(:first)').each(function () {
2856						var cell = $(this).find('> td').eq(e);
2857						cell.prepend("<div class='header' style='display:none'>" + header + "</div>");
2858					});
2859				});
2860			}
2861		}
2862
2863		// action column
2864		if (table.find('table').width() - 5 > table.width()) {
2865			if (screen.width > 767) {
2866				if (table.find('table td.action').length) {
2867					table.find('table td.action').each(function () {
2868						$(this).parent().prepend($(this).clone());
2869					});
2870					table.find('table tr').eq(0).prepend('<th style="width:20px;"></th>');
2871				}
2872			}
2873		}
2874	});
2875
2876	// mobile: wrap large table data on click
2877	$('.toggle-table-wrap button').each(function () {
2878		$(this).on('click', function () {
2879			if ($(this).hasClass('fa-toggle-on')) {
2880				$(this).removeClass('fa-toggle-on').addClass('fa-toggle-off');
2881				$(this).parent().next().removeClass('large-table').addClass('large-table-no-wrap');
2882			} else {
2883				$(this).removeClass('fa-toggle-off').addClass('fa-toggle-on');
2884				$(this).parent().next().removeClass('large-table-no-wrap').addClass('large-table');
2885			}
2886		});
2887	});
2888
2889})(jQuery);
2890
2891// Prevent memory leaks in IE
2892// Window isn't included so as not to unbind existing unload events
2893// More info:
2894//	- http://isaacschlueter.com/2006/10/msie-memory-leaks/
2895if ( window.attachEvent && !window.addEventListener ) {
2896	window.attachEvent("onunload", function() {
2897		for ( var id in jQuery.cache ) {
2898			var item = jQuery.cache[ id ];
2899			if ( item.handle ) {
2900				if ( item.handle.elem === window ) {
2901					for ( var type in item.events ) {
2902						if ( type !== "unload" ) {
2903							// Try/Catch is to handle iframes being unloaded, see #4280
2904							try {
2905								jQuery.event.remove( item.handle.elem, type );
2906							} catch(e) {}
2907						}
2908					}
2909				} else {
2910					// Try/Catch is to handle iframes being unloaded, see #4280
2911					try {
2912						jQuery.event.remove( item.handle.elem );
2913					} catch(e) {}
2914				}
2915			}
2916		}
2917	});
2918}
2919
2920$.tikiModal = function(msg) {
2921	return $('body').tikiModal(msg);
2922};
2923
2924//Makes modal over window or object so ajax can load and user can't prevent action
2925$.fn.tikiModal = function(msg) {
2926	var obj = $(this);
2927	if (!obj.length) {
2928		return null;			// happens after search index rebuild in some conditions
2929	}
2930	var lastModal = obj.data('lastModal');
2931
2932	if (!lastModal) {
2933		lastModal = Math.floor(Math.random() * 1000);
2934		obj.data('lastModal', lastModal);
2935	}
2936	var box = {
2937		top: obj.offset().top,
2938		left: obj.offset().left,
2939		height: obj.outerHeight(),
2940		width: obj.outerWidth()
2941	};
2942	var modal = $('body').find('#modal_' + lastModal);
2943	var spinner = $('<img src="img/spinner.gif" style="vertical-align: top; margin-right: .5em;" />');
2944
2945	if (!msg) {
2946		modal
2947			.fadeOut(function() {
2948				$(this).remove();
2949			});
2950		obj.removeData('lastModal');
2951		return obj;
2952	}
2953
2954	if (modal.length) {
2955		modal
2956			.find('.dialog')
2957			.empty()
2958			.html(spinner)
2959			.append(msg);
2960		return obj;
2961	}
2962
2963	modal = $('<div id="modal_' + lastModal + '" class="tiki-modal">' +
2964					'<div class="mask" />' +
2965					'<div class="dialog"></div>' +
2966			'</div>')
2967		.appendTo('body');
2968
2969	var zIndex = 0;
2970	if (obj.is("body")) {
2971		zIndex = 2147483646 - 1;	// maximum
2972		box.top = obj.offset().top + $window.scrollTop();
2973		box.left = obj.offset().left + $window.scrollLeft();
2974	} else {
2975		obj.parents().addBack().each(function () {
2976			var z = $(this).css("z-index");
2977			if (z && z !== 'auto' && z > zIndex) {
2978				zIndex = Number(z);
2979			}
2980		});
2981	}
2982
2983	//Set height and width to mask to fill up the whole screen or the single element
2984	modal
2985		.width(box.width)
2986		.height(box.height)
2987		.css('top', 	box.top + 'px')
2988		.css('left', 	box.left + 'px')
2989		.find('.mask')
2990			.height(box.height)
2991			.fadeTo(1000, 0.6)
2992		.parent()
2993		.find('.dialog')
2994			.hide()
2995			.append(spinner)
2996			.append(msg);
2997	var dialog = modal.find('.dialog');
2998	if (obj.is("body")) {
2999		dialog.css({
3000			top: (box.top + $window.innerHeight()/2 - $window.scrollTop()) + "px",
3001			left: (box.left + $window.innerWidth()/2 - $window.scrollLeft()) + "px"
3002		});
3003	}
3004	dialog.css({
3005		marginTop: (dialog.height() / -2) + "px",
3006		marginLeft: (dialog.width() / -2) + "px"
3007	}).show();
3008
3009	if (zIndex) {
3010		modal.css("z-index", zIndex + 1);
3011	}
3012	return obj;
3013};
3014
3015//makes the width of an input change to the value
3016$.fn.valWidth = function() {
3017	var me = $(this);
3018	return me.ready(function() {
3019		var h = me.height();
3020		if (!h) {
3021			h = me.offsetParent().css("font-size");
3022			if (h) {
3023				h = parseInt(h.replace("px", ""));
3024			}
3025		}
3026		me.keyup(function() {
3027			var width = me.val().length * h;
3028
3029			me
3030				.stop()
3031				.animate({
3032					width: (width > h ? width : h)
3033				}, 200);
3034		})
3035		.keyup();
3036	});
3037};
3038
3039//For making pagination have the ability to enter page/offset number and go
3040$.paginationHelper = function() {
3041	$('.pagenums').each(function() {
3042		var me = $(this);
3043		var step = me.find('input.pagenumstep');
3044		var endOffset = (me.find('input.pagenumend').val() - 1) * step.data('step');
3045		var url = step.data('url');
3046		var offset_jsvar = step.data('offset_jsvar');
3047		var offset_arg = step.data('offset_arg');
3048
3049		me.find('span.pagenumstep').replaceWith(
3050			$('<input type="text" style="font-size: inherit; " />')
3051				.val(step.val())
3052				.change(function() {
3053					var newOffset = step.data('step') * ($(this).val() - 1);
3054
3055					if (newOffset >= 0) {
3056						//make sure the offset isn't too high
3057						newOffset = (newOffset > endOffset ? endOffset : newOffset);
3058
3059						//THis is for custom/ajax search handling
3060						window[offset_jsvar] = newOffset;
3061						if (step[0]) {
3062							if (step.attr('onclick')) {
3063								step[0].onclick();
3064								return;
3065							}
3066						}
3067
3068						//if the above behavior isn't there, we update location
3069						document.location = url + offset_arg + "=" + newOffset;
3070					}
3071				})
3072				.keyup(function(e) {
3073					switch(e.which) {
3074						case 13: $(this).blur();
3075					}
3076				})
3077				.valWidth()
3078		);
3079	});
3080};
3081
3082//a sudo "onvisible" event
3083$.fn.visible = function(fn, isOne) {
3084	if (fn) {
3085		$(this).each(function() {
3086			var me = $(this);
3087			if (isOne) {
3088				me.one('visible', fn);
3089			} else {
3090				me.on('visible', fn);
3091			}
3092
3093			function visibilityHelper() {
3094				if (!me.is(':visible')) {
3095					setTimeout(visibilityHelper, 500);
3096				} else {
3097					me.trigger('visible');
3098				}
3099			}
3100
3101			visibilityHelper();
3102		});
3103	} else {
3104		$(this).trigger('visible');
3105	}
3106
3107	return this;
3108};
3109
3110$.download = function(url, data, method){
3111	//url and data options required
3112	if( url && data ){
3113		//data can be string of parameters or array/object
3114		data = typeof data == 'string' ? data : jQuery.param(data);
3115		//split params into form inputs
3116		var inputs = '';
3117		jQuery.each(data.split('&'), function(){
3118			var pair = this.split('=');
3119			inputs+='<input type="hidden" name="'+ pair[0] +'" value="'+ pair[1] +'" />';
3120		});
3121		//send request
3122		jQuery('<form action="'+ url +'" method="'+ (method||'post') +'">'+inputs+'</form>')
3123		.appendTo('body').submit().remove();
3124	}
3125};
3126
3127$.uiIcon = function(type) {
3128	return $('<div style="width: 1.4em; height: 1.4em; margin: .2em; display: inline-block; cursor: pointer;">' +
3129		'<span class="ui-icon ui-icon-' + type + '">&nbsp;</span>' +
3130	'</div>')
3131	.hover(function(){
3132		$(this).addClass('ui-state-highlight');
3133	}, function() {
3134		$(this).removeClass('ui-state-highlight');
3135	});
3136};
3137
3138$.uiIconButton = function(type) {
3139	return $.uiIcon(type).addClass('ui-state-default ui-corner-all');
3140};
3141
3142$.rangySupported = function(fn) {
3143	if (window.rangy) {
3144		rangy.init();
3145		var cssClassApplierModule = rangy.modules.CssClassApplier;
3146		return fn();
3147	}
3148};
3149
3150$.fn.rangy = function(fn) {
3151	var me = $(this);
3152	$.rangySupported(function() {
3153		$document.mouseup(function(e) {
3154			if (me.data('rangyBusy')) return;
3155
3156			var selection = rangy.getSelection();
3157			var html = selection.toHtml();
3158			var text = selection.toString();
3159
3160			if (text.length > 3 && rangy.isUnique(me[0], text)) {
3161					if (fn)
3162						if ($.isFunction(fn))
3163							fn({
3164								text: text,
3165								x: e.pageX,
3166								y: e.pageY
3167							});
3168			}
3169		});
3170	});
3171	return this;
3172};
3173
3174$.fn.rangyRestore = function(phrase, fn) {
3175	var me = $(this);
3176	$.rangySupported(function() {
3177		phrase = rangy.setPhrase(me[0], phrase);
3178
3179		if (fn)
3180			if ($.isFunction(fn))
3181				fn(phrase);
3182	});
3183	return this;
3184};
3185
3186$.fn.rangyRestoreSelection = function(phrase, fn) {
3187	var me = $(this);
3188	$.rangySupported(function() {
3189		phrase = rangy.setPhraseSelection(me[0], phrase);
3190
3191		if (fn)
3192			if ($.isFunction(fn))
3193				fn(phrase);
3194	});
3195	return this;
3196};
3197
3198$.fn.realHighlight = function() {
3199	var o = $(this);
3200	$.rangySupported(function() {
3201		rangy.setPhraseBetweenNodes(o.first(), o.last(), document);
3202	});
3203	return this;
3204};
3205
3206$.fn.ajaxEditDraw = function(options) {
3207	var me = $(this).attr('href', 'tiki-ajax_services.php');
3208
3209	//defaults
3210	options = $.extend({
3211		saved: function() {},
3212		closed: function() {}
3213	}, options);
3214
3215	$.tikiModal(tr('Loading editor'));
3216
3217	me.serviceDialog({
3218		title: me.attr('title'),
3219		data: {
3220			controller: 'draw',
3221			action: 'edit',
3222			fileId: me.data('fileid'),
3223			galleryId: me.data('galleryid'),
3224			imgParams: me.data('imgparams'),
3225			raw: true
3226		},
3227		modal: true,
3228		zIndex: 9999,
3229		fullscreen: true,
3230		load: function (data) {
3231			//prevent from happeneing over and over again
3232			if (me.data('drawLoaded')) return false;
3233
3234			me.data('drawLoaded', true);
3235
3236			me.drawing = $('#tiki_draw')
3237				.loadDraw({
3238					fileId: me.data('fileid'),
3239					galleryId: me.data('galleryid'),
3240					name: me.data('name'),
3241					imgParams: me.data('imgparams'),
3242					data: $('#fileData').val()
3243				})
3244				.on('savedDraw', function(e, o) {
3245					me.data('drawLoaded', false);
3246					me.drawing.parent().dialog('destroy');
3247					me.drawing.remove();
3248
3249					//update the image that did exist in the page with the new one that now exists
3250					var img = $('.pluginImg' + me.data('fileid')).show();
3251
3252					if (img.length < 1) document.location = document.location + '';
3253
3254					var w = img.width(), h = img.height();
3255
3256					if (img.hasClass('regImage')) {
3257						var replacement = $('<div />')
3258							.attr('class', img.attr('class'))
3259							.attr('style', img.attr('style'))
3260							.attr('id', img.attr('id'))
3261							.insertAfter(img);
3262
3263						img.remove();
3264						img = replacement;
3265					}
3266
3267					var src = me.data('src');
3268
3269					$('<div class=\"svgImage\" />')
3270						.load(src ? src : 'tiki-download_file.php?fileId=' + o.fileId + '&display', function() {
3271
3272							$(this)
3273								.css('position', 'absolute')
3274								.fadeTo(0, 0.01)
3275								.prependTo('body')
3276								.find('img,svg')
3277								.scaleImg({
3278									width: w,
3279									height: h
3280								});
3281
3282							img.html($(this).children());
3283
3284							$(this).remove();
3285						});
3286
3287					if (!options.saved) return;
3288
3289					options.saved(o.fileId);
3290
3291					me.data('fileid', o.fileId);			// replace fileId on edit button
3292					if (o.imgParams && o.imgParams.fileId) {
3293						o.imgParams.fileId = o.fileId;
3294						me.data('imgparams', o.imgParams);
3295					}
3296				})
3297				.submit(function() {
3298					me.drawing.saveDraw();
3299					return false;
3300				})
3301				.on('loadedDraw', function() {
3302					//kill the padding around the dialog so it looks like svg-edit is one single box
3303					me.drawing
3304						.parent()
3305						.css('padding', '0px');
3306
3307					var serviceDialog = me.data('serviceDialog');
3308					if (serviceDialog) {
3309						var drawFrame = $('#svgedit');
3310						serviceDialog
3311							.on('dialogresize', function() {
3312								drawFrame.height(serviceDialog.height() - 4);
3313							})
3314							.trigger('dialogresize');
3315					}
3316
3317					$.tikiModal();
3318				});
3319
3320			me.drawing.find('#drawMenu').remove();
3321		},
3322		close: function() {
3323			if (me.data('drawLoaded')) {
3324				me.data('drawLoaded', false);
3325				me.drawing.remove();
3326
3327				if (!options.closed) return;
3328				options.closed(me);
3329			}
3330		}
3331	});
3332
3333	return false;
3334};
3335
3336$.notify = function(msg, settings) {
3337	settings = $.extend({
3338		speed: 10000
3339	},settings);
3340
3341	var notify = $('#notify');
3342
3343	if (!notify.length) {
3344		notify = $('<div id="notify" />')
3345			.css('top', '5px')
3346			.css('right', '5px')
3347			.css('position', 'fixed')
3348			.css('z-index', 9999999)
3349			.css('padding', '5px')
3350			.width($window.width() / 5)
3351			.prependTo('body');
3352	}
3353
3354	var note = $('<div class="notify ui-state-error ui-corner-all ui-widget ui-widget-content" />')
3355		.append(msg)
3356		.css('padding', '5px')
3357		.css('margin', '5px')
3358		.mousedown(function() {
3359			return false;
3360		})
3361		.hover(function() {
3362			$(this)
3363				.stop()
3364				.fadeTo(500, 0.3)
3365		}, function() {
3366			$(this)
3367				.stop()
3368				.fadeTo(500, 1)
3369		})
3370		.prependTo(notify);
3371
3372	setTimeout(function() {
3373		note
3374			.fadeOut()
3375			.slideUp();
3376
3377		//added outside of fadeOut to ensure removal
3378		setTimeout(function() {
3379			note.remove();
3380		}, 1000);
3381
3382	}, settings.speed);
3383};
3384
3385function delayedExecutor(delay, callback)
3386{
3387	var timeout;
3388
3389	return function () {
3390		var args = arguments;
3391		if (timeout) {
3392			clearTimeout(timeout);
3393			timeout = null;
3394		}
3395
3396		timeout = setTimeout(function () {
3397			callback.apply(this, args)
3398		}, delay);
3399	};
3400}
3401
3402$(function () {
3403
3404		// Show/hide the sidebars //
3405		////////////////////////////
3406
3407	$(".toggle_zone").click(function () {
3408		var $this = $(this), zone="",
3409			icon_left = "toggle-left", icon_right = "toggle-right";
3410
3411		if ($this.is(".right")) {
3412			zone = "right";
3413			icon_left = "toggle-right";
3414			icon_right = "toggle-left"
3415		} else if ($this.is(".left")) {
3416			zone = "left";
3417		}
3418		if ($this.find(".icon-" + icon_left).length) {	// hide it
3419			$this.find(".icon").setIcon(icon_right);
3420			setCookie("hide_zone_" + zone, 'y');
3421			$("body").addClass("hide_zone_" + zone);
3422		} else {
3423			$this.find(".icon").setIcon(icon_left);
3424			deleteCookie("hide_zone_" + zone);
3425			$("body").removeClass("hide_zone_" + zone);
3426		}
3427
3428		$(window).trigger("resize");
3429
3430		return false; // do not modify URL by adding # on the click
3431	});
3432});
3433
3434// try and reposition the menu ul within the browser window
3435$.fn.moveToWithinWindow = function() {
3436	var $el = $(this);
3437	var h = $el.height(),
3438	w = $el.width(),
3439	o = $el.offset(),
3440	po = $el.parent().offset(),
3441	st = $window.scrollTop(),
3442	sl = $window.scrollLeft(),
3443	wh = $window.height(),
3444	ww = $window.width();
3445
3446	if (w + o.left > sl + ww) {
3447		$el.animate({'left': sl + ww - w - po.left}, 'fast');
3448	}
3449	if (h + o.top > st + wh) {
3450		$el.animate({'top': st + wh - (h > wh ? wh : h) - po.top}, 'fast');
3451	} else if (o.top < st) {
3452		$el.animate({'top': st - po.top}, 'fast');
3453	}
3454};
3455
3456$.fn.scaleImg = function (max) {
3457	$(this).each(function() {
3458		//Here we want to make sure that the displayed contents is the right size
3459		var h, w, img = $(this),
3460		actual = {
3461			height: img.height(),
3462			width: img.width()
3463		},
3464		original = $(this).clone(),
3465		parent = img.parent();
3466
3467		var winner = '';
3468
3469		if (actual.height > max.height) {
3470			winner = 'height';
3471		} else if (actual.width > max.width) {
3472			winner = 'width';
3473		}
3474
3475		//if there is no winner, there is no need to resize
3476		if (winner) {
3477			//we resize both images and svg, we check svg first
3478			var g = img.find('g');
3479			if (g.length) {
3480				img
3481					.attr('preserveAspectRatio', 'xMinYMin meet');
3482
3483				parent
3484					.css('overflow', 'hidden')
3485					.width(max.width)
3486					.height(max.height);
3487
3488				g.attr('transform', 'scale( ' + (100 / (actual[winner] / max[winner]) * 0.01)  + ' )');
3489			} else {
3490				//now we resize regular images
3491				if (actual.height > actual.width) {
3492					h = max.height;
3493					w = Math.ceil(actual.width / actual.height * max.height);
3494				} else {
3495					w = max.width;
3496					h = Math.ceil(actual.height / actual.width * max.width);
3497				}
3498				img.css({ height: h, width: w });
3499			}
3500
3501			img
3502				.css('cursor', "url(img/icons/zoom.gif),auto")
3503				.click(function () {
3504					$('<div/>').append(original).dialog({
3505						modal: true,
3506						width: Math.min($(window).width(), actual.width + 20),
3507						height: Math.min($(window).height(), actual.height + 50)
3508					});
3509					return false;
3510				});
3511		}
3512	});
3513
3514	return this;
3515};
3516
3517
3518// Compatibility to old jquery to resolve a bug in fullcalendar
3519$.curCSS = function (element, property) {
3520	return $(element).css(property);
3521};
3522
3523
3524$.fn.registerFacet = function () {
3525	this.each(function () {
3526		var element = this, entries = $($(this).data('for')).val()
3527			.split(" " + $(this).data('join') + " ")
3528			.map(function (value) {
3529				return (value.charAt(0) === '"') ? value.substr(1, value.length - 2) : value;
3530			});
3531
3532		function applyFilter(value) {
3533			if (value) {
3534				value = $.makeArray(value);
3535				value = value
3536					.map(function (value) {
3537						return (-1 === value.indexOf(' ')) ? value : ('"' + value + '"');
3538					})
3539					.join(" " + $(element).data('join') + " ");
3540			}
3541			$($(element).data('for')).val(value).change();
3542		}
3543
3544		if ($(element).is('select')) {
3545			$(this)
3546				.val(entries)
3547				.trigger("chosen:updated") // for chosen
3548				.change(function () {
3549					var value = $(this).val();
3550					applyFilter(value);
3551				});
3552		} else if ($(element).has(':checkbox').length) {
3553			$(':checkbox', element)
3554				.each(function () {
3555					if (-1 !== $.inArray($(this).val(), entries)) {
3556						$(this).prop('checked', true);
3557					}
3558				})
3559				.on('click', function () {
3560					applyFilter($(':checked', element).map(function () {
3561						return $(this).val();
3562					}));
3563				});
3564		}
3565
3566		var selected = $('option:selected, :checkbox:checked', this).length,
3567			all = $('option, :checkbox', this).length;
3568
3569		if (all === 1 && selected === 0) {
3570			$(this).closest('.facet-hide-group').hide();
3571		}
3572	});
3573
3574	return this;
3575};
3576
3577$.fn.reload = function () {
3578	this.each(function () {
3579		if (this.reload) {
3580			this.reload();
3581		} else if($(this).data('reload')) {
3582			$(this).loadService({}, {
3583				url: $(this).data('reload')
3584			});
3585		}
3586	});
3587	return this;
3588};
3589
3590$(document).on('mouseover', '.media[data-href]', function () {
3591	$(this).css('cursor', 'pointer');
3592});
3593$(document).on('mouseout', '.media[data-href]', function () {
3594	$(this).css('cursor', 'default');
3595});
3596$(document).on('click', '.media[data-href]', function () {
3597	document.location.href = $(this).data('href');
3598});
3599
3600$(document).on('hidden.bs.modal', '.footer-modal.fade', function () {
3601	// Required for bootstrap to allow changing the content of a modal
3602	$(this).removeData('bs.modal').find('.modal-content').empty();
3603
3604	// With multiple stacked modals, which is not officially supported by bootstrap,
3605	// focus on the last modal would be lost.
3606	$('body').toggleClass('modal-open', $('.modal.fade.show').length > 0);
3607	$(".modal-backdrop.show:first").remove();
3608});
3609
3610$(document).on('submit', '.modal-body form:not(.no-ajax)', ajaxSubmitEventHandler(function (data) {
3611	//if FORWARD is set in the returned data, load the passed service into the modal
3612	// rather than close the modal and refresh the page.
3613	if (data && data.FORWARD) {
3614		var $this = $(this);
3615		if ($this.is("form")) {
3616			$this = $this.parent();
3617		}
3618		$this.children().remove();
3619		$this.loadService(data.FORWARD, {
3620			origin: this,
3621			load: function () {
3622				$(this).closest('.modal').trigger('tiki.modal.redraw');
3623			}
3624		});
3625	} else {
3626		// reload() causes a request to update the browser cache - similar to pressing the reload button.
3627		// so we must not reload() but set the href. This behaves simililar to clicking a link - which keeps the browser cache.
3628		// The difference is: NOT loading about 50+ js / css files!
3629		//document.location.reload();
3630		document.location.href = document.location.href.replace(/#.*$/, "");	// remove the hash from the URL if there is one otherwise the page doesn't reload
3631	}
3632}));
3633
3634// When data-size is set on the toggle-link, alter the size of the modal
3635$(document).on('click', '[data-toggle=modal][data-size]', function () {
3636	var target = $(this).data('target'), size = $(this).data('size');
3637
3638	$(target)
3639		.one('hidden.bs.modal', function () {
3640			$('.modal-dialog', this).removeClass(size);
3641		})
3642		.find('.modal-dialog').addClass(size)
3643		;
3644});
3645
3646$(document).on('click', '[data-toggle=modal][data-modal-title]', function () {
3647	var target = $(this).data('target'), title = $(this).data('modal-title');
3648
3649	$(target)
3650		.one('loaded.bs.modal', function () {
3651			$('.modal-title', this).text(title);
3652		})
3653		;
3654});
3655
3656$(document).on('loaded.bs.modal', '.modal.fade', function () {
3657	$(this).trigger('tiki.modal.redraw');
3658});
3659
3660// START BOOTSTRAP 4 CHANGE
3661$(document).on('shown.bs.modal', '.modal', function (event) {
3662	// fixes for bootstrap4
3663	var $button = $(event.relatedTarget); // Button that triggered the modal
3664	var remote = $button.data('remote'); // Extract info from data-* attributes
3665	var $modal = $(this);
3666
3667	var href = remote ? remote : $button.attr("href");
3668
3669	if (href) {
3670		$modal.find('.modal-content').load(href, function () {
3671			$(this).trigger("tiki.modal.redraw");
3672		});
3673	} else {
3674		$modal.trigger("tiki.modal.redraw");
3675	}
3676	// END BOOTSTRAP 4 CHANGE
3677});
3678
3679$(document).on('tiki.modal.redraw', '.modal.fade', function () {
3680	var modal = this, $button;
3681
3682	// On Modal show, find all buttons part of a .submit block and create
3683	// proxies of them in the modal footer
3684	$('.modal-footer .auto-btn', modal).remove();
3685	$('div.submit .btn', modal).each(function () {
3686		var $submit = $(this);
3687		if ($submit.is('a:not(.custom-handling)')) {
3688			$button = $submit;
3689		} else {
3690			$submit.hide();
3691			$button = $('<button>')
3692				.text($submit.val() || $submit.text())
3693				.attr('class', $submit.attr('class'))
3694				.attr('data-dismiss', 'modal')
3695				.addClass('auto-btn')
3696				.click(function () {
3697					if ($submit.data("alt_controller") && $submit.data("alt_action")) {
3698						$submit.parents("form").attr("action", $.service($submit.data("alt_controller"), $submit.data("alt_action")));
3699					}
3700					$submit.click();
3701					if (typeof $submit.parents("form").validate !== "function") {
3702						// make the button look disabled and ignore further clicks
3703						$button.off("click").css("opacity", 0.3);
3704					}
3705				});
3706		}
3707		$('.modal-footer', modal).append($button);
3708	});
3709
3710	if ($.fn.flexibleSyntaxHighlighter) {
3711		$('textarea', modal).flexibleSyntaxHighlighter();
3712	}
3713
3714	$(".nav-tabs", this).each(function () {
3715		if ($(".active", this).length === 0) {
3716			$("li:first-child a", this).tab("show");
3717		}
3718	});
3719
3720	if ($.applyChosen) {
3721		$(this).applyChosen();
3722	}
3723
3724	if (jqueryTiki.colorbox) {
3725		$(this).applyColorbox();
3726	}
3727
3728	if (jqueryTiki.tooltips) {
3729		$(this).tiki_popover();
3730	}
3731	// START BOOTSTRAP 4 CHANGE
3732
3733	$('.modal-body :input', modal).first().focus();
3734	$('.modal-backdrop.show:not(.fade)').remove(); // Bootstrap keeps adding more of these
3735
3736	// handle $ajaxtimer for alerts that are not using the confirmAction handler
3737	if ($("#timer-seconds", modal).length) {
3738		var $seconds = $("#timer-seconds"),
3739			counter = $seconds.text(),
3740			timer = setInterval(function () {
3741				$seconds.text(--counter);
3742				if (counter === 0 || counter < 0) {
3743					$.closeModal();
3744					window.location = window.location.href;
3745				}
3746			}, 1000);
3747	}
3748	// END BOOTSTRAP 4 CHANGE
3749});
3750
3751/**
3752 * Make .depends elements show or hide depending on the state of the "on" element
3753 * for checkboxes only so far
3754 */
3755$(document).on("ready tiki.modal.redraw", function () {
3756	$(".depends").each(function () {
3757		var $depends = $(this),
3758			on = $depends.data("on");
3759
3760		$("[name=" + on + "]").change(function () {
3761			if ($(this).is("input[type=checkbox]") && $(this).is(":checked")) {
3762				$depends.show();
3763			} else {
3764				$depends.hide();
3765			}
3766		}).change();
3767
3768	});
3769});
3770
3771$(function () {
3772	var $tabs = $('a[data-toggle=tab][href="' + document.location.hash + '"]'),
3773		tabShown = false,
3774		notShown = [];
3775
3776	if (document.location.search.match(/cookietab=/)) {
3777		tabShown = true;
3778	} else if (document.location.hash && $tabs.length) {
3779		$tabs.tab('show');
3780		tabShown = true;
3781	} else {
3782		$(".tabs").each(function () {
3783			var name = $(this).data("name"),
3784				t = getCookie(name, "tabs", "notfound");
3785
3786			var $tab = $('a[data-toggle=tab].active', this);	// class "active" set serverside from $cookietab var
3787			if (t && $tab.length === 0) {
3788				$tab = $('a[data-toggle=tab][href="' + t + '"]');
3789			}
3790
3791			if ($tab.length) {
3792				$tab.tab('show');
3793				tabShown = true;
3794			} else if (name) {
3795				notShown.push(name);
3796			}
3797		});
3798	}
3799	if (typeof $().tab === "function") {
3800		if (!tabShown && !notShown.length) {
3801			$("a[data-toggle=tab]:first").tab("show");
3802		} else if (notShown.length) {
3803			for (var i = 0; i < notShown.length; i++) {
3804				$(".tabs[data-name=" + notShown[i] + "] a[data-toggle=tab]:first").tab("show");
3805			}
3806		}
3807	}
3808
3809	$('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
3810		if ($(this).parents(".tab-content").length === 0) {
3811			document.location.hash = $(e.target).attr("href");
3812		}
3813		setCookieBrowser($(this).parents(".tabs:first").data("name"), $(e.target).attr("href"), "tabs");
3814	}).click(function () {
3815		var scroll = $window.scrollTop();	// prevent window jumping to tabs on click
3816		$(this).tab('show');
3817		$window.scrollTop(scroll);
3818	});
3819
3820	$("input[name='session_protected']").on('click', function () {
3821		if ($(this).prop('type') == 'checkbox' && $(this).data('tiki-admin-child-block') == '#session_protected_childcontainer') {
3822			var checkbox = $("input[name='session_protected']");
3823			if (checkbox.prop('checked') && location.protocol != 'https:') {
3824				$(this).confirmationDialog({
3825					title: tr('Warning - Protect all sessions with HTTPS'),
3826					message: tr('You seem to be accessing the website using HTTP only, if your HTTPS settings are not correct, you will be locked out of the website'),
3827					success: function () {
3828						checkbox.prop('checked', true);
3829					}
3830				});
3831				return false;
3832			}
3833		}
3834	});
3835});
3836
3837$.openModal = function (options) {
3838	var href = options.remote; 	// BOOTSTRAP 4 CHANGE
3839
3840	if (-1 === href.indexOf('?')) {
3841		href += '?modal=1';
3842	} else {
3843		href += '&modal=1';
3844	}
3845
3846	// START BOOTSTRAP 4 CHANGE
3847	$('.modal.fade:not(.show):first')
3848		// Bind a single event to trigger as soon as the form appears
3849		.one('hidden.bs.modal', options.close || function () {})
3850		// Make the form load
3851		.find(".modal-content")
3852		.load(href, function () {
3853
3854			var $modal = $(this).parents(".modal");
3855
3856			$modal.modal(options);
3857
3858			if ($modal.is(':visible')){
3859				$modal.trigger("tiki.modal.redraw");
3860			} else {
3861				$modal.modal("show");
3862			}
3863
3864			$('.modal-title', this).text(options.title);
3865
3866			if (options.size) {
3867				$modal.find('.modal-dialog').addClass(options.size);
3868				$modal.one('hidden.bs.modal', function () {
3869					$('.modal-dialog', this).removeClass(options.size);
3870				});
3871			}
3872
3873			if (options.open) {
3874				options.open.apply(this);
3875			}
3876
3877			// END BOOTSTRAP 4 CHANGE
3878		});
3879
3880};
3881
3882$.closeModal = function (options) {
3883	options = options || {};
3884	var done = options.done;
3885	if (done) {
3886		done = function () {
3887			// Wait until the event loop ends before considering really done
3888			setTimeout(options.done, 0);
3889		};
3890	}
3891
3892	$('.modal.fade.show').last()	// BOOTSTRAP 4 CHANGE
3893		.one('hidden.bs.modal', done || function () {})
3894		.modal('hide');
3895};
3896
3897$.fn.clickModal = function (options, href) {
3898	this.click($.clickModal(options, href));
3899	return this;
3900};
3901
3902$.clickModal = function (options, href) {
3903	return function (e) {
3904		var control = this, url;
3905		if (! href) {
3906			url = $(this).attr('href');
3907		} else {
3908			url = href;
3909		}
3910		if ($.isFunction(e.preventDefault)) {
3911			e.preventDefault();
3912		}
3913
3914		$.openModal({
3915			title: options.title,
3916			size: options.size,
3917			remote: url,
3918			open: function () {
3919				if (options.open) {
3920					options.open.apply(this, []);
3921				}
3922
3923				$('form:not(.no-ajax)', this)
3924					.addClass('no-ajax') // Remove default ajax handling, we replace it
3925					.submit(ajaxSubmitEventHandler(function (data) {
3926						if (options.success) {
3927							options.success.apply(control, [data]);
3928						}
3929					}));
3930			}
3931		});
3932	};
3933};
3934
3935/**
3936 * Open a tab on the current page, e.g.:
3937 * <a href="#" onclick="showTab(2); return false;">Open tab 2 on this page</a>
3938 *
3939 * Assumes the tab is in the main column of the page and that one tab is already showing.
3940 *
3941 * @param tabNumber         number of the tab on the current page to show
3942 * @returns {boolean}
3943 */
3944function showTab(tabNumber) {
3945	var thisTabId = $('#col1').find('.tab-pane.active').attr('id'),
3946		tabNames = thisTabId.substr(0, thisTabId.indexOf('-') + 1)
3947	;
3948	$('a[href="#' + tabNames + tabNumber + '"]').tab('show');
3949}
3950
3951/**
3952 * Send feedback to a popup modal or to div#tikifeedback using bootstrap alert variations (error, warning, success, info)
3953 *
3954 * @param mes           array       The message
3955 * @param type          string      Type of alert: error, warning, success or info (default)
3956 * @param modal         boolean     true for popup modal, false (default) to use the div#tikifeedback that is on every page
3957 * @param title         string      Custom message title
3958 * @param icon          string      Custom icon
3959 * @param killall       boolean     true for removing other feedbacks already open, false (default) (only for non modal)
3960 * @param custom        string      Custom target in jquery selection notation (only for non modal)
3961 */
3962function feedback (mes, type, modal, title, icon, killall, custom)
3963{
3964	mes = mes || [];
3965	if (!$.isArray(mes)) {
3966		mes = [mes];
3967	}
3968	if (mes.length == 1) {
3969		var meshtml = [mes][0];
3970	} else {
3971		var meshtml = '<ul>';
3972		$.each(mes, function(i, val) {
3973			if (val) {
3974				meshtml += '<li>' + val + '</li>';
3975			}
3976		});
3977		meshtml += '</ul>';
3978	}
3979	type = type || 'info'; modal = modal || false; killall = killall || false; custom = $(custom).length ? $(custom).first() : null;
3980	var target, map =
3981	{
3982		'error': {title:tr('Error'), class:'danger', icon:'error'},
3983		'warning': {title:tr('Warning'), class:'warning', icon:'warning'},
3984		'success': {title:tr('Success'), class:'success', icon:'success'},
3985		'info': {title:tr('Note'), class:'info', icon:'information'}
3986	};
3987	var check = ['error', 'warning', 'success', 'info'];
3988	type = $.inArray(type, check) > -1 ? type : 'info';
3989	title = title || map[type]['title'];
3990	icon = icon || map[type]['icon'];
3991	icon = $.fn.getIcon(icon);
3992	if (modal) {
3993		if (mes.length > 0) {
3994			meshtml = '<div class="alert alert-dismissable alert-' + map[type]['class'] + '">' + meshtml + '</div>';
3995		} else {
3996			meshtml = '';
3997		}
3998		target = $('.modal.fade:not(.show)').first();
3999		$('.modal-content', target).html(
4000			'<div class="modal-header">' +
4001				'<h4 class="text-' + map[type]['class'] + '">' + icon[0].outerHTML + ' ' + title + '</h4>' +
4002				'<button type="button" class="close pull-right" data-dismiss="modal">x</button>' +
4003			'</div>' +
4004			'<div class="modal-body">' +
4005				meshtml +
4006			'</div>'
4007		);
4008		target.modal();
4009	} else {
4010		var tfb = $(custom ? custom : 'div#tikifeedback');
4011		if (killall) {
4012			tfb.find('div.alert.alert-dismissable').remove();
4013		}
4014		if (mes.length == 0) {
4015			meshtml = '';
4016		}
4017		tfb.append(
4018			'<div class="alert alert-dismissable alert-' + map[type]['class'] + '">' +
4019				'<button type="button" class="close" data-dismiss="modal">x</button>' +
4020				'<h4>' + icon[0].outerHTML + ' ' + title + '</h4>' +
4021				meshtml +
4022			'</div>'
4023		).on('click' , 'button.close' , function() {
4024			$(this).parent().remove();
4025		});
4026		placeFeedback(tfb);
4027	}
4028}
4029
4030/**
4031 * Utility for tikifeedback to place the feedback at the top of the page (at div#tikifeedback) or viewport, whichever is
4032 * lower to allow it to be seen. If shown at the top of the view port, after 5 seconds it will move to its normal
4033 * position, which is the first element in div#col1
4034 *
4035 * @param object
4036 */
4037function placeFeedback(object) {
4038	if ($('.modal.fade.show').length) {
4039		$('#col1, #col2, #col3').css('z-index', 'auto');
4040		object.css('z-index', 3000);
4041		object.find('div').css('z-index', 3000);
4042	}
4043	if (object.offset().top < $(window).scrollTop()) {
4044		object.find('div.alert').append('<div id="move-message" style="font-size:smaller;margin-top:10px"><em>'
4045			+ tr('This message will move to the top of the page after a few seconds.') + '</em>');
4046		//it's important to not define top until after the tfb object has been manipulated
4047		object.offset({'top': $(window).scrollTop()});
4048		object.css('z-index', 3000);
4049		setTimeout(function() {
4050			object.fadeOut(1000, function() {
4051				//move back to usual position and clear style attribute so subsequent feedback appears properly
4052				$('div#col1').prepend(object);
4053				object.css({'z-index':'', 'position':'', 'top': ''});
4054				object.find('div#move-message').css('visibility', 'hidden');
4055				object.fadeIn();
4056			});
4057		}, 5000);
4058	}
4059
4060}
4061
4062// thanks to Rob W on https://stackoverflow.com/a/8962023/2459703
4063$.fn.closestDescendent = function(filter) {
4064		var $found = $(),
4065				$currentSet = this; // Current place
4066		while ($currentSet.length) {
4067				$found = $currentSet.filter(filter);
4068				if ($found.length) break;  // At least one match: break loop
4069				// Get all children of the current set
4070				$currentSet = $currentSet.children();
4071		}
4072		return $found.first(); // Return first match of the collection
4073};
4074
4075window.regCapsLock = function () {};
4076
4077// Avoid that jquery appends a cachebuster to scripts loaded via a regular script tag when the base content was loaded from an xhr call
4078// I.e xhr call loads same html boilerplate and that boilerplate contains a script tag that loads some .js script.
4079// In this case, jquery would add a cachebuster to the js request, and no cache would be work.
4080$.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
4081	if ( options.dataType == 'script' || originalOptions.dataType == 'script' ) {
4082		options.cache = true;
4083	}
4084});
4085
4086//Preview for the upload avatar popup
4087function readURL(input) {
4088	if (input.files && input.files[0]) {
4089		$(".btn-upload-avatar").removeClass('disabled');
4090		var reader = new FileReader();
4091		reader.onload = function (e) {
4092			$('.user-avatar-preview img').attr('src', e.target.result);
4093		}
4094		reader.readAsDataURL(input.files[0]);
4095	}
4096}
4097$(document).on('change', '#userfile', function(){
4098	readURL(this);
4099});
4100
4101
4102function objectLockToggle(icon) {
4103
4104	var $this = $(icon).tikiModal(" "),
4105		action = $this.data("is_locked") ? "unlock" : "lock";
4106
4107	$.post($.service(
4108		"object",
4109		action,
4110		{
4111			type: $this.data("type"),
4112			object: $this.data("object"),
4113			value: $this.data("is_locked") ? "" : jqueryTiki.username
4114		}
4115		), function (data) {
4116			if (data && data.locked) {
4117				$this.find(".icon").setIcon("lock");
4118				$this.data("is_locked", "1")
4119					.attr("title", tr("Locked by " + jqueryTiki.userRealName))
4120					.parent().find("input[name=locked]").val(jqueryTiki.username);
4121			} else {
4122				$this.find(".icon").setIcon("unlock");
4123				$this.data("is_locked", "")
4124					.attr("title", "")
4125					.parent().find("input[name=locked]").val("");
4126			}
4127		},
4128		"json").done(function () {
4129		$this.tikiModal();
4130	});
4131
4132	return false;
4133}
4134
4135
4136/**
4137 * Remove accents from chars
4138 * vendor_bundled/vendor/mottie/tablesorter/js/jquery.tablesorter.combined.js:2373
4139 *
4140 * @param str
4141 */
4142(function setup_removediacritics_function(){
4143	var characterEquivalents = {
4144	'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå
4145	'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ
4146	'c' : '\u00e7\u0107\u010d', // çćč
4147	'C' : '\u00c7\u0106\u010c', // ÇĆČ
4148	'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę
4149	'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ
4150	'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı
4151	'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ
4152	'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō
4153	'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ
4154	'ss': '\u00df', // ß (s sharp)
4155	'SS': '\u1e9e', // ẞ (Capital sharp s)
4156	'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů
4157	'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ
4158	};
4159
4160	var characterRegex, characterRegexArray;
4161
4162	function removeDiacritics(str) {
4163		var chr,
4164			acc = '[',
4165			eq = characterEquivalents;
4166
4167		if ( !characterRegex ) {
4168			characterRegexArray = {};
4169			for ( chr in eq ) {
4170				if ( typeof chr === 'string' ) {
4171					acc += eq[ chr ];
4172					characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' );
4173				}
4174			}
4175			characterRegex = new RegExp( acc + ']' );
4176		}
4177		if ( characterRegex.test( str ) ) {
4178			for ( chr in eq ) {
4179				if ( typeof chr === 'string' ) {
4180					str = str.replace( characterRegexArray[ chr ], chr );
4181				}
4182			}
4183		}
4184		return str;
4185	}
4186
4187	window.removeDiacritics = removeDiacritics;
4188})();
4189
4190/**
4191 * Fetch feedback sent through ajax and place into the tikifeedback div that is on each page
4192 * If tikifeedback div is outside of the viewport, place it at the top of the viewport and have it move to the
4193 * normal position (first element in div#col1) after 5 seconds
4194 */
4195$(document).ajaxComplete(function (e, jqxhr) {
4196	var feedback = jqxhr.getResponseHeader('X-Tiki-Feedback'),
4197		tfb = $('#tikifeedback');
4198	if (feedback) {
4199		feedback = decodeURIComponent(feedback); // decodeURIComponent() reverses rawurlencode().
4200		tfb.fadeIn(200, function() {
4201			//place html from ajax X-Tiki-Feedback into the div#tikifeedback
4202			tfb.html($($.parseHTML(feedback)).filter('#tikifeedback').html());
4203			tfb.find('div.alert').each(function() {
4204				var	title = $(this).find('span.rboxtitle').text().trim(),
4205					content = $(this).find('div.rboxcontent').text().trim();
4206				$(this).find('span.rboxtitle').text(title);
4207				$(this).find('div.rboxcontent').text(content);
4208			});
4209			//place tikifeedback div into window view if necessary
4210			placeFeedback(tfb);
4211		});
4212	}
4213	tfb.find('.clear').on('click', function () {
4214		$(tfb).empty();
4215		//move back to usual position and clear style attribute so subsequent feedback appears properly
4216		$('div#col1').prepend(tfb);
4217		tfb.css({'z-index':'', 'position':'', 'top': ''});
4218		return true;
4219	});
4220});
4221
4222$(document).on('keydown', 'textarea.autoheight', function(evt){
4223	var el = this;
4224	var height = Math.max(el.clientHeight, el.offsetHeight, el.scrollHeight);
4225	el.style.cssText = 'height:' + height + 'px; overflow-y: hidden';
4226	setTimeout(function(){
4227		el.scrollTo(0,0);
4228	}, 0);
4229});
4230
4231$(document).on('change', '.preference :checkbox:not(.pref-reset)', function () {
4232	var childBlock = $(this).data('tiki-admin-child-block')
4233		, childMode = $(this).data('tiki-admin-child-mode')
4234		, checked = $(this).is(':checked')
4235		, disabled = $(this).prop('disabled')
4236		, $depedencies = $(this).parents(".adminoption").find(".pref_dependency")
4237		, childrenElements = null
4238	;
4239	var childrenElements = $(this).parents('.adminoptionbox').nextAll('.adminoptionboxchild').eq(0).find(':input[id^="pref-"]');
4240
4241	if (childBlock) {
4242		childrenElements = $(childBlock).find(':input[id^="pref-"]');
4243	}
4244
4245	if (childMode === 'invert') {
4246		// FIXME: Should only affect childBlock, not $depedencies. From r54386
4247		checked = ! checked;
4248	}
4249
4250	if (disabled && checked) {
4251		$(childBlock).show('fast');
4252		$depedencies.show('fast');
4253	} else if (disabled || ! checked) {
4254		/* Only hides child preferences if they are all at default values.
4255		Purpose questioned in https://sourceforge.net/p/tikiwiki/mailman/tikiwiki-cvs/thread/F2DE8896807BF045932776107E2E783D350674DB%40CT20SEXCHP02.FONCIERQC.INTRA/#msg36171225
4256		 */
4257		var hideBlock = true;
4258		childrenElements.each(function( index ) {
4259			var value = $( this ).val();
4260			var valueDefault = $( this ).siblings('span.pref-reset-wrapper').children('.pref-reset').attr('data-preference-default');
4261
4262			if (typeof valueDefault != 'undefined' && value != valueDefault) {
4263				hideBlock = false;
4264			}
4265		});
4266
4267		if (hideBlock) {
4268			$(childBlock).hide('fast');
4269			$depedencies.hide('fast');
4270		}
4271	} else {
4272		$(childBlock).show('fast');
4273		$depedencies.show('fast');
4274	}
4275});
4276
4277$(document).on('click', '.pref-reset-wrapper a', function () {
4278	var box = $(this).closest('span').find(':checkbox');
4279	box.click();
4280	$(this).closest('span').children( ".pref-reset-undo, .pref-reset-redo" ).toggle();
4281	return false;
4282});
4283
4284$(document).on('click', '.pref-reset', function() {
4285	var c = $(this).prop('checked');
4286	var $el = $(this).closest('.adminoptionbox').find('input:not(:hidden),select,textarea')
4287		.not('.system').attr( 'disabled', c )
4288		.css("opacity", c ? .6 : 1 );
4289	var defval = $(this).data('preference-default');
4290
4291	if ($el.is(':checkbox')) {
4292		$(this).data('preference-default', $el.prop('checked') ? 'y' : 'n');
4293		$el.prop('checked', defval === "y");
4294	} else {
4295		$(this).data('preference-default', $el.val());
4296		$el.val(defval);
4297	}
4298	$el.change();
4299	if (jqueryTiki.chosen) {
4300		$el.trigger("chosen:updated");
4301	}
4302});
4303
4304$(document).on('change', '.preference select', function () {
4305	var childBlock = $(this).data('tiki-admin-child-block')
4306		, selected = $(this).val()
4307		, childMode = $(this).data('tiki-admin-child-mode')
4308	;
4309
4310	$(childBlock).hide();
4311	$(childBlock + ' .modified').show();
4312	$(childBlock + ' .modified').parent().show();
4313
4314	if (selected && /^[\w-]+$/.test(selected)) {
4315		$(childBlock).filter('.' + selected).show();
4316	}
4317	if (childMode === 'notempty' && selected.length) {
4318		$(childBlock).show();
4319	}
4320});
4321
4322$(document).on('change', '.preference :radio', function () {
4323	var childBlock = $(this).data('tiki-admin-child-block');
4324
4325	if ($(this).prop('checked')) {
4326		$(childBlock).show('fast');
4327		$(this).closest('.preference').find(':radio').not(this).change();
4328	} else {
4329		$(childBlock).hide();
4330	}
4331});
4332
4333$(function () {
4334	$('.preference :checkbox, .preference select, .preference :radio').change();
4335});

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.