PageSourceSearch

https://www.incotex.ru/local/templates/main/js/indi.js?155302494439031

js incotex.ru collected 2026-09-29 16:31:12 UTC 39,031 bytes, 1,557 lines download raw bytes

1/**
2 * @category	Individ
3 * @link		http://individ.ru
4 * @revision	$Revision$
5 * @date		$Date$
6 */
7
8'use strict';
9
10/**
11 * XXX: Individ namespace
12 */
13var indi = new function()
14{
15	/**
16	 * Признак инициализации
17	 *
18	 * @var boolean
19	 */
20	var initialized = false;
21
22	/**
23	 * Инициализирует все функциональные части
24	 *
25	 * @return void
26	 */
27	this.init = function()
28	{
29		if (initialized) {
30			return;
31		}
32		initialized = true;
33
34		indi.app.init();
35		indi.ui.init();
36	};
37};
38
39
40
41
42/**
43 * XXX: Приложение
44 */
45indi.app = new function()
46{
47	/**
48	 * Инициализирует приложение
49	 *
50	 * @return void
51	 */
52	this.init = function()
53	{
54		//Инициализируем локаль
55		this.locale.init();
56
57		//Инициализируем функциональные блоки
58		this.blocks.init();
59	};
60};
61
62
63
64
65/**
66 * XXX: Функциональные блоки приложения
67 */
68indi.app.blocks = new function()
69{
70	/**
71	 * Ф-ии обратного вызова о готовности всех функциональных блоков
72	 *
73	 * @var array
74	 */
75	var initCallbacks = [];
76
77	/**
78	 * Зарезервированные названия блоков, которые нельзя переопределять
79	 *
80	 * @var object
81	 */
82	var reserved = {};
83
84	/**
85	 * Добавляет ф-ю обратного вызова о готовности всех блоков
86	 *
87	 * @param object callback Ф-я обратного вызова
88	 * @param object context Контекст вызова ф-ии
89	 * @return void
90	 */
91	this.onInit = function(callback, context)
92	{
93		initCallbacks.push({
94			callback: callback,
95			context: context = context || this
96		});
97	};
98
99	/**
100	 * Инициализирует функциональные блоки
101	 *
102	 * @return void
103	 */
104	this.init = function()
105	{
106		for (var key in this) {
107			if (reserved[key]) {
108				continue;
109			}
110
111			var blockConstructor = this[key];
112			if (typeof blockConstructor == 'function') {
113				var blockExists = blockConstructor.exists ? blockConstructor.exists() : true;
114				if (blockExists) {
115					this[key] = new blockConstructor();
116				}
117			}
118		}
119
120		for (var i = 0; i < initCallbacks.length; i++) {
121			initCallbacks[i].callback.call(
122				initCallbacks[i].context
123			);
124		};
125	};
126
127	//Заполняем зарезервированные названия блоков
128	for (var key in this) {
129		reserved[key] = true;
130	}
131};
132
133
134
135
136/**
137 * XXX: Локаль
138 */
139indi.app.locale = new function()
140{
141	/**
142	 * Настройки локали
143	 *
144	 * @var object
145	 */
146	this.settings = {
147		date: 'dd.mm.yy',
148		time: 'hh:mm',
149		dateTime: 'dd.mm.yy hh:mm',
150		firstDay: 1,
151		isRTL: false
152	};
153
154	/**
155	 * Сообщения локали
156	 *
157	 * @var object
158	 */
159	this.messages = {
160		monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
161		monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
162		dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
163		dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
164		dayNamesMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
165		close: 'Close',
166		datePicker: {
167			prev: '&larr;Previous',
168			next: 'Next&rarr;',
169			current: 'Today',
170			showMonthAfterYear: false,
171			weekHeader: 'Wk',
172			yearSuffix: ''
173		},
174		timePicker: {
175			timeOnlyTitle: 'Choose time',
176			timeText: 'Time',
177			hourText: 'Hours',
178			minuteText: 'Minutes',
179			secondText: 'Seconds',
180			millisecText: 'Milliseconds',
181			currentText: 'Now',
182			ampm: true
183		},
184		alert: {
185			title: 'System message',
186			ok: 'Ok'
187		}
188	};
189
190	/**
191	 * XXX: Возвращает отформатированную строку
192	 * Требуется jQuery UI datepicker.
193	 *
194	 * @param Date date Дата для форматирования
195	 * @param String format Формат (по-умолчанию indi.app.locale.settings.date)
196	 * @return String
197	 */
198	this.format = function(date, format)
199	{
200		format = format === undefined ? this.settings.date : format;
201
202		return $.datepicker.formatDate(format, date);
203	};
204
205	/**
206	 * XXX: Разбирает отформатированную строку
207	 * Требуется jQuery UI datepicker.
208	 *
209	 * @param String date Отформатированная дата
210	 * @param String format Формат (по-умолчанию indi.app.locale.settings.date)
211	 * @return Date
212	 */
213	this.parse = function(date, format)
214	{
215		format = format === undefined ? this.settings.date : format;
216
217		return $.datepicker.parseDate(format, date);
218	};
219
220	/**
221	 * Инициализирует локаль
222	 *
223	 * @return void
224	 */
225	this.init = function()
226	{
227		if($.datepicker)
228		{
229			$.datepicker.regional[''] = {
230				closeText: this.messages.close,
231				prevText: this.messages.datePicker.prev,
232				nextText: this.messages.datePicker.next,
233				currentText: this.messages.datePicker.current,
234				monthNames: this.messages.monthNames,
235				monthNamesShort: this.messages.monthNamesShort,
236				dayNames: this.messages.dayNames,
237				dayNamesShort: this.messages.dayNamesShort,
238				dayNamesMin: this.messages.dayNamesMin,
239				weekHeader: this.messages.datePicker.weekHeader,
240				dateFormat: this.settings.date,
241				firstDay: this.settings.firstDay,
242				isRTL: this.settings.isRTL,
243				showMonthAfterYear: this.messages.datePicker.showMonthAfterYear,
244				yearSuffix: this.messages.datePicker.yearSuffix
245			};
246			$.datepicker.setDefaults($.datepicker.regional['']);
247		}
248
249		if($.timepicker)
250		{
251			$.timepicker.regional[''] = {
252				timeOnlyTitle: this.messages.timePicker.timeOnlyTitle,
253				timeText: this.messages.timePicker.timeText,
254				hourText: this.messages.timePicker.hourText,
255				minuteText: this.messages.timePicker.minuteText,
256				secondText: this.messages.timePicker.secondText,
257				millisecText: this.messages.timePicker.millisecText,
258				currentText: this.messages.timePicker.currentText,
259				closeText: this.messages.close,
260				ampm: this.messages.timePicker.ampm
261			};
262			$.timepicker.setDefaults($.timepicker.regional['']);
263		}
264	};
265};
266
267
268
269
270/**
271 * XXX: User interface
272 */
273indi.ui = new function()
274{
275	/**
276	 * Ф-ии обратного вызова о готовности виджетов
277	 *
278	 * @var array
279	 */
280	var initCallbacks = [];
281
282	/**
283	 * XXX: Отображает диалог с сообщением
284	 *
285	 * @param string messge Текст сообщения
286	 * @param object config Конфигурация диалога
287	 * Конфиг может содержать следующие параметры type - тип оповещения(модальное окно или замещение элементов),
288	 * status - статус оповещения (success, warning, notification и т.д.), elem - заменяемый элемент обязателен для замещения элементов
289	 * @return void
290	 */
291	this.alert = function(message, config)
292	{
293		config = $.extend(
294			{type: 'modal', engine: 'bootstrap', status: 'notification'},
295			config || {}
296		);
297
298		switch(config.type){
299			case 'modal':
300				if( config.engine == 'fancybox' && typeof $.fancybox == 'function' ){
301					var message =	'<p class="notification notification_' + config.status + '">' +
302						'<span class="notification__text">' + message + '</span>' +
303						'</p>';
304					$.fancybox(message);
305				}
306				else{
307					var message = '<div id="notification-modal" class="modal fade" tabindex="-1" role="dialog">' +
308						'<div class="modal-dialog">' +
309						'<div class="modal-content">' +
310						'<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
311						'<div class="modal-body">' +
312						'<p class="notification notification_' + config.status + '">' +
313						'<span class="notification__text">' + message + '</span>' +
314						'</p>' +
315						'</div>' +
316						'</div>' +
317						'</div>' +
318						'</div>';
319					$(message).appendTo('body').modal('show');
320				}
321
322				break;
323
324			case 'elem':
325				if( !(config.elem.length) ){
326					return false;
327				}
328
329				var $messHtml = '<p class="notification notification_' + config.status + '">' +
330					'<span class="notification__text">' + message + '</span>' +
331					'</p>';
332				config.elem.after($messHtml).hide();
333		}
334	};
335
336	/**
337	 * Добавляет ф-ю обратного вызова при инициализации UI
338	 *
339	 * @param object callback Ф-я обратного вызова
340	 * @param object context Контекст вызова ф-ии
341	 * @return void
342	 */
343	this.onInit = function(callback, context)
344	{
345		initCallbacks.push({
346			callback: callback,
347			context: context || this
348		});
349	};
350
351	/**
352	 * Инициализирует UI
353	 *
354	 * @param string selector Родительский элемент
355	 * @return void
356	 */
357	this.init = function(selector)
358	{
359		if (selector === undefined) {
360			selector = $('body');
361		} else {
362			selector = $(selector);
363		}
364
365		this.widgets.init(selector);
366
367		for (var i = 0; i < initCallbacks.length; i++) {
368			initCallbacks[i].callback.call(
369				initCallbacks[i].context,
370				selector
371
372			);
373		};
374	}
375};
376
377/**
378 * XXX: Виджеты
379 */
380indi.ui.widgets = new function()
381{
382	/**
383	 * Конструкторы виджетов
384	 *
385	 * @var object
386	 */
387	this.items = {};
388
389	/**
390	 * XXX: Создает стилизованное поле загрузки файла. Если указан multiple - множественная загрузка
391	 * Пример вызова:
392	 * <form method="post"enctype="multipart/form-data">
393	 * <div class="span_file js-file-add">
394	 *		 <input name="FILE[]" multiple data-max-files="3" type="file" class="widget uploadpicker" placeholder="Прикрепить файл">
395	 *		 <span class="question question--file" title="Например, фото продукта. Не более 5 штук."></span>
396	 *		 <div class="is-label">
397	 *			 <small class="block-form__delete">
398	 *			    <span class="upload-field-label"><i class="js-file-reset"></i></span>
399	 *			 </small>
400	 *		 </div>
401	 *	 </div>
402	 *	 <input type="submit">
403	 * </form>
404	 *
405	 * @param string|object selector Селектор узлов DOM
406	 * @return void
407	 */
408	this.items['uploadpicker'] = function(selector)
409	{
410		/*if (!$.uploadpicker) {
411			return;
412		}*/
413
414		$(selector).each(function() {
415			var field = $(this);
416			var required = field.is('[required]');
417			var disabled = field.is('[disabled]');
418			var isMultiple = field.is('[multiple]');
419			var placeholder = field.attr('placeholder');
420			var maxFiles = field.data("max-files");
421			if(!maxFiles)
422				maxFiles = 10;
423
424			field
425				.addClass('upload-field-overlay')
426				.removeAttr('required')
427				.css({
428					cursor: 'pointer',
429					fontSize: '200px',
430					height: 'auto',
431					opacity: 0,
432					position: 'absolute',
433					right: 0,
434					top: '-0.5em',
435					width: 'auto'
436				})
437				.wrap('<span class="widget-upload-field"/>')
438
439			var wrapper = field.parent();
440			wrapper
441				.css({
442					backgroundColor: 'transparent',
443					display: 'block',
444					overflow: 'hidden',
445					position: 'relative'
446				})
447				.prepend('<input class="upload-field-value form-control" type="text"'
448					+ (required ? ' required=""' : '')
449					+ (disabled ? ' disabled=""' : '')
450					+ (placeholder ? ' placeholder="' + placeholder + '"' : '')
451					+ ' />')
452				.prepend("<a class='add-file fake'>"+field.attr("placeholder")+"</a>");
453			var $fileAdd = wrapper.closest(".js-file-add");
454			var $fileAddLabel = $fileAdd.find('.upload-field-label');
455			field.on({'change': function() {
456				//Если множественная загрузка
457				if(isMultiple){
458					if($(this).closest(".js-new-add-container").length == 0){
459						$(this).closest(".span_file").wrap("<div class='js-new-add-container'></div>");
460					}
461					var count = selector.closest(".js-new-add-container").find(".js-file-add").length;
462					/*console.log(maxFiles);
463					console.log(count);*/
464					if(maxFiles < count){
465						alert("Можно прикрепить максимум "+maxFiles+" файлов");
466						return false;
467					}
468
469					$(this).closest(".js-new-add-container").append('<div class="span_file span_file--new js-file-add js-file-add--new">'+
470						'<input  name="FILE[]" type="file" multiple class="widget uploadpicker js-new-uploadpicker js-new-uploadpicker-'+count+'" data-max-files="'+maxFiles+'" placeholder="Еще">'+
471						'<div class="is-label">'+
472						'<small class="block-form__delete">'+
473						'<span class="upload-field-label"><i class="js-file-reset"></i></span>'+
474						'</small>'+
475						'</div>'+
476						'</div>');
477					indi.ui.widgets.items.uploadpicker($(".js-new-uploadpicker-"+count));
478					$(this).closest(".js-new-add-container").find(".js-file-add--new:not(:last-child)").find(".add-file").remove();
479				}
480
481				var values = [this.value.split(/[\/\\]/).pop()];
482				if (this.files) {
483					values = [];
484					for (var i = 0; i < this.files.length; i++) {
485						values.push(this.files[i].name);
486					}
487				}
488				values.length ? wrapper.addClass('has-value') : wrapper.removeClass('has-value');
489				wrapper.find('.upload-field-value').val(values.join(', '));
490			}, 'keypress': function(event) {
491				if (event.key == 'Backspace') {
492					if (this.value) {
493						event.preventDefault();
494						this.value = '';
495						$(this).trigger('change');
496					}
497				}
498			}
499			});
500		});
501	};
502
503	/**
504	 * XXX: Создает поле выбора даты
505	 *
506	 * @param string|object selector Селектор узлов DOM
507	 * @return void
508	 * Параметр config передается в таком виде data-config='{"minDate": 0}'
509	 */
510	this.items['datepicker'] = function (selector) {
511		if (!$.datepicker) {
512			return;
513		}
514
515		var defaults = indi.ui.widgets.items.datepicker.defaults;
516
517		$(selector).each(function () {
518			var element = $(this);
519
520			var $datapickerWrap = $(element).parent();
521			$datapickerWrap.on('click', '.datepicker-clear', function() {
522				var $iconDatePicker = $datapickerWrap.find('.datepicker-icon');
523				element.datepicker('setDate', null);
524				$iconDatePicker.show();
525				$(this).hide();
526			});
527
528			element.datepicker($.extend(
529				{},
530				defaults,
531				element.data('config') || {}
532			));
533
534			if (element.is(':input')) {
535				element
536					.keypress(function (event) {
537						switch (event.keyCode) {
538							case 13:
539								//Если Enter нажимается при открытом календаре - убираем каленрарь и сабмитим форму
540								if ($(this).datepicker('widget').css('display') != 'none') {
541									event.stopPropagation();
542									$(this).datepicker('hide');
543									if (this.form) {
544										$(this.form).trigger('submit');
545									}
546								}
547								break;
548						}
549					})
550					.after(
551						'<span class="datepicker-icon" title="Выбрать дату"></span>'
552					)
553					.mask("99.99.9999")
554					.next('.datepicker-icon')
555					.click(function () {
556						$(this).prev('.datepicker').datepicker('show');
557					});
558			}
559		});
560	};
561
562	this.items['datepicker'].defaults = {
563		changeMonth: false,
564		changeYear: false,
565		minDate: new Date(1900, 1, 1),
566		maxDate: '',
567		onClose: function (selectedDate, widgetInstance) {
568			if ($('.startDate').length && $('.endDate').length) {
569				if ($(widgetInstance.input[0]).hasClass('startDate'))
570					$('.endDate').datepicker("option", "minDate", selectedDate);
571				else if ($(widgetInstance.input[0]).hasClass('endDate'))
572					$('.startDate').datepicker("option", "maxDate", selectedDate);
573			}
574		},
575		onSelect: function(date, inst) {
576			var $iconClear = $(this).parent().find('.datepicker-clear');
577			var $iconDatePicker = $(this).parent().find('.datepicker-icon');
578			if(!$iconClear.length) {
579				$(this).after(
580					'<span class="datepicker-clear" title="Очистить дату"></span>'
581				)
582			}
583			else {
584				$iconClear.show();
585			}
586
587			$iconDatePicker.hide();
588		}
589	};
590
591	/**
592	 * XXX: Создает поле выбора времени
593	 *
594	 * @param string|object selector Селектор узлов DOM
595	 * @return void
596	 */
597	this.items['timepicker'] = function(selector)
598	{
599		if (!$.timepicker || !$.mask) {
600			return;
601		}
602
603		$(selector)
604			.timepicker()
605			.mask('99:99');
606	};
607
608	/**
609	 * Виджет кастомизации селектов
610	 * http://selectize.github.io/selectize.js/
611	 *
612	 * @param selector
613	 */
614	this.items['selectize'] = function(selector)
615	{
616		if (!$.fn.selectize) {
617			return;
618		}
619
620		var defaults = indi.ui.widgets.items.selectize.defaults;
621
622		$(selector).each(function() {
623			var element = $(this);
624
625			var config = $.extend(
626				{},
627				defaults,
628				element.data('config') || {}
629			)
630
631			element.selectize(config);
632		});
633	};
634
635	this.items['selectize'].defaults = {};
636
637
638	/**
639	 * Виджет слайдера
640	 * http://kenwheeler.github.io/slick/
641	 *
642	 * @param selector
643	 */
644	this.items['slick'] = function(selector)
645	{
646
647		if (!$.fn.slick) {
648			return;
649		}
650
651		var defaults = indi.ui.widgets.items.slick.defaults;
652
653		$(selector).each(function() {
654			var element = $(this);
655
656			var config = $.extend(
657				{},
658				defaults,
659				element.data('config') || {}
660			)
661
662			element.slick(config);
663		});
664	};
665
666	this.items['slick'].defaults = {
667		initialSlide: 0,
668		autoplay: true,
669		autoplaySpeed: 5000
670	};
671
672	/**
673	 * XXX: Создает переключатель в виде закладок (табов)
674	 * Примерный markup:
675	 * <div class="widget tabpane">
676	 * 	<ul class="tabs">
677	 * 		<li><a class="fake" href="#tab-1">Tab 1</a></li>
678	 * 		<li><a class="fake" href="#tab-2">Tab 2</a></li>
679	 * 	</ul>
680	 * 	<div class="panes">
681	 * 		<div id="tab-1" class="pane">Content 1</div>
682	 * 		<div id="tab-2" class="pane">Content 2</div>
683	 * 	</div>
684	 * </div>
685	 *
686	 * @param string|object selector Селектор узлов DOM
687	 * @return void
688	 */
689	this.items['tabpane'] = function(selector)
690	{
691		var defaults = indi.ui.widgets.items.tabpane.defaults;
692
693		$(selector).each(function() {
694			//Виджет
695			var tabPane = $(this);
696
697			//Конфигурация
698			var config = $.extend(
699				{},
700				defaults,
701				tabPane.data('config') || {}
702			);
703
704			//Ищет контроллер закладки с заданным hash
705			var findTabController = function(hash) {
706				return tabPane.find(config.tabsSelector + ' a[href="' + hash + '"]');
707			};
708
709			//Активирует закладку с заданным hash
710			var activateTab = function(hash, reason) {
711				if (hash.substr(0, 1) != '#') {
712					if (reason == 'click') {
713						document.location.href = hash;
714					}
715					return;
716				}
717				var controller = findTabController(hash);
718				var item = controller.closest('li').length ? controller.closest('li') : controller;
719				item
720					.addClass('active')
721					.siblings().removeClass('active');
722
723				tabPane.find(config.tabsSelector + ' a').each(function() {
724					var href = $(this).attr('href');
725					if (href.substr(0, 1) == '#') {
726						var pane = tabPane.find(
727							config.panesSelector + href
728						);
729						if (this === controller.get(0)) {
730							pane
731								.show()
732								.trigger('widget.tabpane:show')
733						} else {
734							pane
735								.hide()
736								.trigger('widget.tabpane:hide');
737						}
738					}
739				});
740
741				//Если виджет в виджете, то нужно запустить цепочку переключений вверх по DOM
742				if (!tabPane.is(':visible')) {
743					tabPane.closest(config.panesSelector).trigger('widget.tabpane:activate');
744				}
745			};
746
747			//Событие активации, позволяет управлять виджетом извне
748			tabPane.on('widget.tabpane:activate', function(event) {
749				var id = $(event.target).attr('id');
750				if (id) {
751					activateTab('#' + id, 'activate');
752				}
753			});
754
755			//Класс закладок
756			if (config.tabsClass) {
757				tabPane.find(config.tabsSelector).addClass(config.tabsClass);
758			}
759
760			//Клики по закладкам
761			tabPane.find(config.tabsSelector + ' a').click(function(event) {
762				if (event.isPropagationStopped()) {
763					return false;
764				}
765				event.stopPropagation();
766
767				activateTab($(this).attr('href'), 'click');
768
769				return false;
770			});
771
772			//Клики по родителям закладок
773			if (config.tabsHandleParents) {
774				tabPane.find(config.tabsSelector + ' a').parent().click(function(event) {
775					$(this).find('a').trigger('click');
776				});
777			}
778
779			//Класс панелей
780			if (config.panesClass) {
781				tabPane.find(config.panesSelector).addClass(config.panesClass);
782			}
783
784			//Заголовки панелей
785			if (config.panesTitleAdd) {
786				tabPane.find(config.tabsSelector + ' a').each(function() {
787					var href = $(this).attr('href');
788					if (href.substr(0, 1) == '#') {
789						var pane = tabPane.find(
790							config.panesSelector + href
791						);
792						$('<' + config.panesTitleTag + '/>')
793							.addClass(config.panesTitleClass)
794							.html($(this).html())
795							.prependTo(pane);
796					}
797				});
798			}
799
800			//Активная по умолчанию закладка
801			var activeTabLink = null;
802			if (config.followHash) {
803				activeTabLink = findTabController(document.location.hash);
804			}
805			if (!activeTabLink || !activeTabLink.length) {
806				activeTabLink = tabPane.find(config.tabsSelector + ' a:first');
807			}
808			if (activeTabLink.length) {
809				activateTab(activeTabLink.attr('href'), 'init');
810			}
811		});
812	};
813
814	this.items['tabpane'].defaults = {
815		tabsSelector: '> .tabs',//Селектор закладок
816		tabsClass: '',//Добавить класс для закладок
817		tabsHandleParents: true,//Отслеживать клики по родителям ссылок в закладках
818		panesSelector: '> .panes > .pane',//Селектор панелей
819		panesClass: '',//Добавить класс в каждую панель
820		panesTitleAdd: false,//Добавлять заголовок перед панелями
821		panesTitleTag: 'h2',//Тег для заголовка панели
822		panesTitleClass: 'pane-title',//Класс для заголовка панели
823		followHash: true//Активировать закладку по hash в document.location
824	};
825
826	/**
827	 * XXX: Создает таблицу с thead, фиксирующимся при прокрутке документа в окне браузера
828	 *
829	 * @param string|object selector Селектор узлов DOM
830	 * @return void
831	 */
832	this.items['fixedtable'] = function(selector)
833	{
834		$(selector).each(function() {
835			var table = $(this);
836			var thead = table.find('> thead').eq(0);
837			var isFixed = null;
838
839			var applyWidth = function(cell) {
840				cell.find('> div').width(cell.width());
841			};
842
843			var onScroll = function() {
844				var scrollTop = $(window).scrollTop();
845				var offset = table.offset();
846
847				if (
848					scrollTop > offset.top
849					&& scrollTop < offset.top + table.height() - thead.height()
850				) {
851					if(isFixed !== true) {
852						isFixed = true;
853						thead.css({'position': 'fixed'});
854					}
855				} else {
856					if (isFixed !== false) {
857						isFixed = false;
858						thead.css({'position': 'absolute'});
859					}
860				}
861			};
862
863			table.css('position', 'relative');
864
865			thead
866				.width(thead.width())
867				.height(thead.height());
868
869			table.find('> thead > tr > *, > tbody > tr > *').wrapInner(
870				$('<div>')
871					.addClass('fixedtable-cell-wrapper')
872					.css('min-height', '1px')
873			);
874			table.find('> thead > tr > *').each(function() {
875				applyWidth($(this));
876			});
877			table.find('> tbody > tr:first > *').each(function() {
878				applyWidth($(this));
879			});
880
881			var theadPlacheholder = thead
882				.clone()
883				.css('visibility', 'hidden');
884			theadPlacheholder.insertAfter(thead);
885
886			thead.css({'top': '0'});
887
888			$(window).scroll(onScroll);
889			onScroll();
890		});
891	};
892
893	/**
894	 * XXX: Создает таблицу с интерлейсом
895	 *
896	 * @param string|object selector Селектор узлов DOM
897	 * @return void
898	 */
899	this.items['interlace'] = function(selector)
900	{
901		$(selector).find('tr:even').addClass('even');
902		$(selector).find('tr:odd').addClass('odd');
903	};
904
905	/**
906	 * Инициализирует виджеты
907	 *
908	 * @param string selector Родительский элемент
909	 * @return void
910	 */
911	this.init = function(selector)
912	{
913		if (selector === undefined) {
914			selector = $('body');
915		} else {
916			selector = $(selector);
917		}
918
919		$.each(this.items, function(name) {
920			this.call(this, selector.find('.widget.' + name));
921		});
922	};
923};
924
925
926
927
928/**
929 * XXX: Создает индикатор загрузки
930 * Пример использования:
931 * var loading = new indi.ui.loading('#content');
932 * ...
933 * loading.hide();
934 *
935 * @param string|object selector Селектор элемента, у которого отображается индикатор
936 * @return void
937 */
938indi.ui.loading = function(selector)
939{
940	if (indi.ui.loading.template === undefined) {
941		indi.ui.loading.template = '<div class="loading-layer"></div>' +
942			'<div class="loading-icon">' +
943			$('#loading-indicator-template').html() +
944			'</div>';
945	}
946
947	/**
948	 * Показывает индикатор загрузки
949	 *
950	 * @return void
951	 */
952	this.show = function()
953	{
954		$(selector)
955			.addClass('loading-indicator')
956			.append(indi.ui.loading.template);
957	};
958
959	/**
960	 * Скрывает индикатор загрузки
961	 *
962	 * @return void
963	 */
964	this.hide = function()
965	{
966		$(selector)
967			.removeClass('loading-indicator')
968			.find('> .loading-layer, > .loading-icon').remove();
969	};
970
971	selector = selector || 'body';
972
973	this.show();
974};
975
976/**
977 * XXX: Работа с историей браузера
978 *
979 * @param string callback Имя ф-ии обратного вызова, вызываемой при переходах по истории
980 * @param object data Данные текущей страницы
981 */
982indi.history = function(callback, data)
983{
984	/**
985	 * Формирует history state
986	 *
987	 * @param object data State data
988	 * @return object
989	 */
990	var buildState = function(data)
991	{
992		var state = {
993			data: data instanceof Object ? data : {}
994		};
995
996		if (state.data.title === undefined) {
997			state.data.title = document.title;
998		}
999
1000		state.generatedByIndiHistory = true;
1001		state.handledBy = callback;
1002
1003		return state;
1004	};
1005
1006	/**
1007	 * XXX: Добавляет обработчик браузерного события
1008	 *
1009	 * @return void
1010	 */
1011	var addListener = function()
1012	{
1013		if (window.addEventListener) {
1014			window.addEventListener('popstate', function(event) {
1015				//Пропускаем события, созданные не через indi.history (например, Chrome такие генерирует)
1016				if (!(event.state instanceof Object)
1017					|| event.state.generatedByIndiHistory === undefined
1018				) {
1019					return;
1020				}
1021
1022				if (event.state.data !== undefined
1023					&& event.state.data.title !== undefined
1024				) {
1025					document.title = event.state.data.title;
1026				}
1027
1028				//Дергаем ф-ии обратного вызова только для своих хозяев
1029				if (event.state.handledBy && event.state.handledBy == callback) {
1030					(new Function(
1031						'url, data',
1032						event.state.handledBy + '(url, data);'
1033					))(
1034						document.location.href,
1035						event.state.data
1036					);
1037				}
1038			}, false);
1039		}
1040	};
1041
1042	/**
1043	 * XXX: Возвращает текущий пункт истории
1044	 *
1045	 * @return object
1046	 */
1047	this.current = function()
1048	{
1049		return history.state;
1050	};
1051
1052	/**
1053	 * XXX: Обновляет текущий пункт истории
1054	 *
1055	 * @param string url URL
1056	 * @param object data Данные
1057	 * @return void
1058	 */
1059	this.replace = function(url, data)
1060	{
1061		if (history.replaceState) {
1062			var state = buildState(data);
1063
1064			history.replaceState(state, state.data.title, indi.utils.url.getFull(url));
1065		}
1066	};
1067
1068	/**
1069	 * XXX: Добавляет новый пункт в историю
1070	 *
1071	 * @param string url URL
1072	 * @param object data Данные
1073	 * @return void
1074	 */
1075	this.push = function(url, data)
1076	{
1077		if (history.pushState) {
1078			var state = buildState(data);
1079
1080			history.pushState(state, state.data.title, indi.utils.url.getFull(url));
1081		}
1082	};
1083
1084
1085
1086
1087	/**
1088	 * Инициализия
1089	 */
1090	this.replace(document.location.href, data);
1091
1092	addListener();
1093};
1094
1095/**
1096 * Позволяет получить singleton instance indi.history
1097 *
1098 * @param string callback Имя ф-ии обратного вызова, вызываемой при переходах
1099 * @param object data Данные текущей страницы
1100 *
1101 * @return indi.history
1102 */
1103indi.history.getInstance = function(callback, data)
1104{
1105	if (this.instances === undefined) {
1106		this.instances = {};
1107	}
1108
1109	if (this.instances[callback] === undefined) {
1110		this.instances[callback] = new indi.history(callback, data);
1111	}
1112
1113	return this.instances[callback];
1114};
1115
1116
1117
1118
1119/**
1120 * XXX: Утилиты для работы с Cookie
1121 */
1122indi.cookie = {
1123	/**
1124	 * XXX: Set cookie
1125	 *
1126	 * @param string name Cookie name
1127	 * @param string value Cookie value
1128	 * @param string time Lifetime
1129	 * @param string path
1130	 * @return void
1131	 */
1132	set: function(name, value, time, path)
1133	{
1134		var time = time === undefined ? 0 : time;
1135		var path = path === undefined ? '/' : path;
1136
1137		var expires = new Date();
1138		time = expires.getTime() + time * 1000;
1139		expires.setTime(time);
1140
1141		document.cookie = name + '=' + value + '; expires=' + expires.toGMTString() + '; path=' + path;
1142	},
1143
1144	/**
1145	 * XXX: Get cookie
1146	 *
1147	 * @param string name Cookie name
1148	 * @return string Cookie value
1149	 */
1150	get: function(name)
1151	{
1152		var cookie = ' ' + document.cookie;
1153		var search = ' ' + name + '=';
1154		var setStr = null;
1155		var offset = 0;
1156		var end = 0;
1157		if (cookie.length > 0) {
1158			offset = cookie.indexOf(search);
1159			if (offset != -1) {
1160				offset += search.length;
1161				end = cookie.indexOf(';', offset);
1162				if (end == -1) {
1163					end = cookie.length;
1164				}
1165				setStr = unescape(cookie.substring(offset, end));
1166			}
1167		}
1168
1169		return setStr;
1170	}
1171};
1172
1173
1174
1175
1176/**
1177 * XXX: User-agent пользователя
1178 */
1179indi.ua = new function()
1180{
1181	/**
1182	 * User agent - iPad
1183	 *
1184	 * @var boolean
1185	 */
1186	this.isIPad = /iPad/.test(navigator.userAgent);
1187
1188	/**
1189	 * User agent - iPhone
1190	 *
1191	 * @var boolean
1192	 */
1193	this.isIPhone = /iPhone/.test(navigator.userAgent);
1194
1195	/**
1196	 * User agent - iPod
1197	 *
1198	 * @var boolean
1199	 */
1200	this.isIPod = /iPod/.test(navigator.userAgent);
1201
1202	/**
1203	 * User agent - iOS
1204	 *
1205	 * @var boolean
1206	 */
1207	this.isIOS = this.isIPad || this.isIPhone || this.isIPod;
1208
1209	/**
1210	 * User agent - Android
1211	 *
1212	 * @var boolean
1213	 */
1214	this.isAndroid = /Android/.test(navigator.userAgent);
1215
1216	/**
1217	 * User agent работает на WebKit
1218	 *
1219	 * @var boolean
1220	 */
1221	this.isWebKit = /WebKit/.test(navigator.userAgent);
1222
1223	/**
1224	 * User agent поддерживает тоuch интерфейс
1225	 *
1226	 * @var boolean
1227	 */
1228	this.isTouchable = function()
1229	{
1230		var result = (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch);
1231		this.isTouchable = function() {
1232			return result;
1233		};
1234		return result;
1235	};
1236};
1237
1238
1239
1240
1241/**
1242 * XXX: Утилиты
1243 */
1244indi.utils = {
1245	/**
1246	 * XXX: Наследует один класс от другого
1247	 *
1248	 * @param object child Потомок
1249	 * @param object parent Родитель
1250	 * @param object overrides Перезаписываемые св-ва и методы
1251	 * @return object Потомок
1252	 */
1253	extend: function(child, parent, overrides)
1254	{
1255		if (typeof parent == 'object') {
1256			overrides = parent;
1257			parent = child;
1258			child = function() {
1259				child.superclass.constructor.apply(this, arguments)
1260			};
1261		}
1262
1263		var f = function(){};
1264		f.prototype = parent.prototype;
1265		child.prototype = new f();
1266		child.prototype.constructor = child;
1267		child.superclass = parent.prototype;
1268
1269		this.override(child, overrides);
1270
1271		return child;
1272	},
1273
1274	/**
1275	 * XXX: Перезаписывает св-ва и методы класса
1276	 *
1277	 * @param object clss Перезаписываемый класс
1278	 * @param object parent Родитель
1279	 * @param object overrides Перезаписываемые св-ва и методы
1280	 * @return void
1281	 */
1282	override: function(clss, overrides)
1283	{
1284		this.apply(clss.prototype, overrides);
1285	},
1286
1287	/**
1288	 * XXX: Расширяет сво-ва одного объекта другим
1289	 *
1290	 * @param object object Расширяемый объект
1291	 * @param object overrides Перезаписываемые св-ва
1292	 * @return void
1293	 */
1294	apply: function(object, overrides)
1295	{
1296		overrides = overrides || {};
1297		var primitiveObject = {};
1298		for (var i in overrides) {
1299			if (typeof primitiveObject[i] == 'undefined' || primitiveObject[i] != overrides[i]) {
1300				object[i] = overrides[i];
1301			}
1302		}
1303	},
1304
1305	/**
1306	 * XXX: Возвращает уникальный ID
1307	 *
1308	 * @return integer|string
1309	 */
1310	getId: function(prefix)
1311	{
1312		return prefix + (new Date().getTime()) + Math.floor(999 * Math.random());
1313	},
1314
1315	/**
1316	 * XXX: Заменяет в шаблоне конструкции {name} на значения из объекта с данными
1317	 * Пример:
1318	 * indi.utils.parse('<a href="{url}">{title}</a>', {url: 'http://my.server/', title: 'My link'});
1319	 *
1320	 * @param string template Шаблон
1321	 * @param object data Данные
1322	 * @param string def Значение по умолчанию
1323	 * @return string
1324	 */
1325	parse: function(template, data, def)
1326	{
1327		def = def || '';
1328		return template.replace(/{([^}]*)}/gm, function() {
1329			return arguments.length > 1 ? data[arguments[1]] : '';
1330		});
1331	},
1332
1333	/**
1334	 * XXX: Возвращает текст в атрибуте href гиперссылки, чтоящий после #
1335	 * Учитывает баг IE, который выдает полный URL для атрибута href
1336	 *
1337	 * @param object|string link Элемент-ссылка
1338	 * @return string
1339	 */
1340	getHashWord: function(link)
1341	{
1342		return $(link).attr('href').replace(/^.*#/, '#');
1343	},
1344
1345	/**
1346	 * XXX: Предзагрузчик изображений с поддержкой jQuery deferred
1347	 * Пример:
1348	 * $.when(
1349	 *     indi.utils.preloadImage(src1),
1350	 *     indi.utils.preloadImage([src2, src3])
1351	 * ).done(function()
1352	 * {
1353	 *     //Images are preloaded
1354	 * });
1355	 *
1356	 * @param string|array|jQuery sources Адрес(а) изображений
1357	 * @return object
1358	 */
1359	preloadImage: function(sources)
1360	{
1361		if (sources instanceof jQuery) {
1362			var urls = [];
1363			sources.each(function() {
1364				if ($(this).is('img')) {
1365					urls.push($(this).attr('src'));
1366				} else if ($(this).is('a')) {
1367					urls.push($(this).attr('href'));
1368				}
1369			});
1370			return this.preloadImage(urls);
1371		}
1372
1373		if (!(sources instanceof Array)) {
1374			sources = [sources];
1375		}
1376
1377		var defer = $.Deferred();
1378		var images = {};
1379		var totalCount = 0;
1380		for (var i = 0; i < sources.length; i++) {
1381			var url = sources[i];
1382			var id = 'id' + url.replace(/[^A-z0-9]/g, '');
1383			if (images[id] === undefined) {
1384				images[id] = {
1385					image: new Image(),
1386					url: url
1387				};
1388				totalCount++;
1389			}
1390		}
1391		var loadedCount = 0;
1392		for (var id in images) {
1393			images[id].image.onload = function() {
1394				if (++loadedCount >= totalCount) {
1395					defer.resolve();
1396				}
1397			};
1398			images[id].image.src = images[id].url;
1399		}
1400
1401		return defer.promise();
1402	},
1403
1404	/**
1405	 * XXX: Склоняет существительное с числительным
1406	 *
1407	 * @param integer number Число
1408	 * @param array cases Варианты существительного в разных падежах и числах (nominativ, genetiv, plural). Пример: ['комментарий', 'комментария', 'комментариев']
1409	 * @param boolean incNum Добавить само число в результат (по умолчанию true)
1410	 * @return string
1411	 */
1412	getNumEnding: function(number, cases, incNum)
1413	{
1414		var numberMod = number % 100;
1415		incNum = incNum === undefined ? true : incNum;
1416		var result = '';
1417
1418		if (numberMod >= 11 && numberMod <= 19) {
1419			result = cases[2];
1420		} else {
1421			numberMod = numberMod % 10;
1422			switch (numberMod) {
1423				case 1:
1424					result = cases[0];
1425					break;
1426				case 2:
1427				case 3:
1428				case 4:
1429					result = cases[1];
1430					break;
1431				default:
1432					result = cases[2];
1433			}
1434		}
1435
1436		return incNum ? number + ' ' + result : result;
1437	},
1438
1439	/**
1440	 * XXX: Форматирует число
1441	 *
1442	 * @param mixed number Число
1443	 * @param integer decimals Кол-во знаков после запятой (по умолчанию 0)
1444	 * @param string decPoint Разделитель целой и дробной части (по умолчанию '.')
1445	 * @param string thousandsSep Разделитель сотен (по умолчанию '`')
1446	 * @return string
1447	 */
1448	numberFormat: function(number, decimals, decPoint, thousandsSep)
1449	{
1450		number = (number + '').replace(/[^0-9+\-Ee.]/g, '');
1451		var n = !isFinite(+number) ? 0 : +number,
1452			prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
1453			sep = (typeof thousandsSep === 'undefined') ? '`' : thousandsSep,
1454			dec = (typeof decPoint === 'undefined') ? '.' : decPoint,
1455			s = '',
1456			toFixedFix = function(n, prec) {
1457				var k = Math.pow(10, prec);
1458				return '' + Math.round(n * k) / k;
1459			};
1460
1461		// Fix for IE parseFloat(0.55).toFixed(0) = 0;
1462		s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
1463
1464		if (s[0].length > 3) {
1465			s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
1466		}
1467		if ((s[1] || '').length < prec) {
1468			s[1] = s[1] || '';
1469			s[1] += new Array(prec - s[1].length + 1).join('0');
1470		}
1471
1472		return s.join(dec);
1473	}
1474};
1475
1476/**
1477 * XXX: Утилиты для работы с URL
1478 */
1479indi.utils.url =
1480{
1481	/**
1482	 * XXX: Возвращает полный URL текущей страницы, включая домен и протокольный префикс
1483	 *
1484	 * @param string path Pathname
1485	 * @return string
1486	 */
1487	getFull: function(path)
1488	{
1489		return path.search('//') == -1 ?
1490		document.location.protocol + '//' + document.location.host + path
1491			:
1492			path;
1493	},
1494
1495	/**
1496	 * XXX: Разбирает URL на части
1497	 *
1498	 * @param string url URL
1499	 * @return object {protocol, host, port, pathname, query, hash}
1500	 */
1501	parse: function(url)
1502	{
1503		var result = {
1504			protocol: '',
1505			host: '',
1506			port: '',
1507			pathname: '',
1508			query: '',
1509			hash: ''
1510		}
1511
1512		//Get protocol
1513		url = url.split('://');
1514		if (url.length > 1) {
1515			result.protocol = url.shift();
1516			url[0] = '//' + url[0];
1517		}
1518		url = url.join('://');
1519
1520		//Get host
1521		url = url.split('//');
1522		if (url.length > 1) {
1523			url.shift();
1524			url = url.join('//').split('/');
1525			result.host = url.shift();
1526			url = '/' + url.join('/');
1527		} else {
1528			url = url.join('//');
1529		}
1530
1531		//Get port
1532		result.host = result.host.split(':');
1533		if (result.host.length > 1) {
1534			result.port = result.host.pop();
1535		}
1536		result.host = result.host.join(':');
1537
1538		//Get hash
1539		url = url.split('#');
1540		if (url.length > 1) {
1541			result.hash = url.pop();
1542		}
1543		url = url.join('#');
1544
1545		//Get query
1546		url = url.split('?');
1547		if (url.length > 1) {
1548			result.query = url.pop();
1549		}
1550		url = url.join('?');
1551
1552		//Get path
1553		result.pathname = url;
1554
1555		return result;
1556	}
1557};

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.