/* Object.assign polyfill */
if ( !Object.assign ) {
	Object.defineProperty( Object, 'assign', {
		enumerable: false,
		configurable: true,
		writable: true,
		value: function( target, firstSource ) {
			'use strict';
			if ( target === undefined || target === null ) {
				throw new TypeError('Cannot convert first argument to object');
			}

			var to = Object( target );
			for ( var i = 1; i < arguments.length; i++ ) {
				var nextSource = arguments[i];
				if ( nextSource === undefined || nextSource === null ) {
					continue;
				}

				var keysArray = Object.keys( Object( nextSource ) );
				for ( var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++ ) {
					var nextKey = keysArray[ nextIndex ];
					var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
					if ( desc !== undefined && desc.enumerable ) {
						to[ nextKey ] = nextSource[ nextKey ];
					}
				}
			}
			return to;
		}
	});
}

//	Array.find polyfill
// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
	Object.defineProperty(Array.prototype, 'find', {
	  value: function(predicate) {
		// 1. Let O be ? ToObject(this value).
		if (this == null) {
		  throw TypeError('"this" is null or not defined');
		}
  
		var o = Object(this);
  
		// 2. Let len be ? ToLength(? Get(O, "length")).
		var len = o.length >>> 0;
  
		// 3. If IsCallable(predicate) is false, throw a TypeError exception.
		if (typeof predicate !== 'function') {
		  throw TypeError('predicate must be a function');
		}
  
		// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
		var thisArg = arguments[1];
  
		// 5. Let k be 0.
		var k = 0;
  
		// 6. Repeat, while k < len
		while (k < len) {
		  // a. Let Pk be ! ToString(k).
		  // b. Let kValue be ? Get(O, Pk).
		  // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
		  // d. If testResult is true, return kValue.
		  var kValue = o[k];
		  if (predicate.call(thisArg, kValue, k, o)) {
			return kValue;
		  }
		  // e. Increase k by 1.
		  k++;
		}
  
		// 7. Return undefined.
		return undefined;
	  },
	  configurable: true,
	  writable: true
	});
  }

  if (!String.prototype.padStart) {
	String.prototype.padStart = function padStart(targetLength, padString) {
	  targetLength = targetLength >> 0; //floor if number or convert non-number to 0;
	  padString = String(typeof padString !== 'undefined' ? padString : ' ');
	  if (this.length > targetLength) {
		return String(this);
	  } else {
		targetLength = targetLength - this.length;
		if (targetLength > padString.length) {
		  padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
		}
		return padString.slice(0, targetLength) + String(this);
	  }
	};
  }
  

  // polyfill
if ( !String.prototype.trim ) { 
	/**
	 * Remove whitespace from both ends of the string
	 * @return {string}
	 */
	String.prototype.trim = function() {
		return this.replace(/^\s+|\s+$/g, "");
	}; 
};


// https://tc39.github.io/ecma262/#sec-array.prototype.findindex
if (!Array.prototype.findIndex) {
	Object.defineProperty(Array.prototype, 'findIndex', {
	  value: function(predicate) {
	   // 1. Let O be ? ToObject(this value).
		if (this == null) {
		  throw new TypeError('"this" is null or not defined');
		}
  
		var o = Object(this);
  
		// 2. Let len be ? ToLength(? Get(O, "length")).
		var len = o.length >>> 0;
  
		// 3. If IsCallable(predicate) is false, throw a TypeError exception.
		if (typeof predicate !== 'function') {
		  throw new TypeError('predicate must be a function');
		}
  
		// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
		var thisArg = arguments[1];
  
		// 5. Let k be 0.
		var k = 0;
  
		// 6. Repeat, while k < len
		while (k < len) {
		  // a. Let Pk be ! ToString(k).
		  // b. Let kValue be ? Get(O, Pk).
		  // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
		  // d. If testResult is true, return k.
		  var kValue = o[k];
		  if (predicate.call(thisArg, kValue, k, o)) {
			return k;
		  }
		  // e. Increase k by 1.
		  k++;
		}
  
		// 7. Return -1.
		return -1;
	  },
	  configurable: true,
	  writable: true
	});
  }
Runner.namespace('Runner.util');

(function() {
	/**
	 * Create a delayed for given number of milliseconds (delay) event handler	
	 * @param {function} hn
	 * @param {number} delay
	 * @param {object} scope
	 * @return {function}
	 */	
	var createDelayed = function( hn, delay, scope ) {
		return function() {
			var args = arguments;
			setTimeout( function() {
				hn.apply(scope, args);
			}, delay );
		};
	};
	
	/**
	 * Create an event handler that will be executed at most once
	 * @param {function} hn
	 * @param {object} e
	 * @param {function} fn
	 * @param {object} scope
	 * @return {function}	 
	 */		
	var createSingle = function( hn, e, fn, scope ) {
		return function() {
			e.removeListener(fn, scope);
			return hn.apply(scope, arguments);
		};
	};
	
	/**
	 * Create an event handler that won't be triggered, 
	 * as long as it continues to be invoked 
	 * during a given number of milliseconds (buffer). 	
	 * @param {function} hn
	 * @param {number} buffer
	 * @param {object} scope
	 * @return {function}	 
	 */
	var createBuffered = function( hn, buffer, scope ) {
		var task = new Runner.util.DelayedTask();
		return function() {
			task.delay( buffer, hn, scope, Array.prototype.slice.call( arguments ) );
		};
	};
	
	/**
	 * Runner.util.Event constructor
	 * @param {object} obj
	 * @param {string} name
	 */ 
	Runner.util.Event = function( obj, name ) {
		this.name = name;
		this.obj = obj;
		this.listeners = [];
	};
	
	
	Runner.util.Event.prototype = {
		/**
		 * Create a custom event listener object
		 * @param {function} fn
		 * @param {object} scope
		 * @param {objct} options
		 * @return {object}
		 */		
		createListener: function( fn, scope, options ) {
			var hn = fn;
			
			scope = scope || this.obj;
			options = options || {};

			if ( options.delay ) {
				hn = createDelayed(hn, options.delay, scope);
			}
			if ( options.single ) {
				hn = createSingle(hn, this, fn, scope);
			}
			if ( options.buffer ) {
				hn = createBuffered(hn, options.buffer, scope);
			}
			
			return {
				fn: fn, 
				fireFn: hn,
				scope: scope,
				options: options
			};
		},
		
		fire: function() {
			var i, ls;
				
			if ( this.listeners.length > 0 ) {
				this.firing = true;
				for (i = 0; i < this.listeners.length; i++) {
					ls = this.listeners[i];
					
					if ( ls.fireFn.apply(ls.scope || this.obj || window, arguments) === false ) {
						this.firing = false;
						return false;
					}
				}
				this.firing = false;
			}
			return true;
		},
		
		on: function( fn, scope, options ) {
			scope = scope || this.obj;
			if ( !this.isListening(fn, scope) ) {
				var ls = this.createListener(fn, scope, options);

				/*if (this.firing) {
					this.listeners = this.listeners.slice(0);
				}*/
				this.listeners.push(ls);
			}
		},
		
		isListening: function( fn, scope ) {
			return this.getListenerIndex(fn, scope) != -1;
		},
		
		getListenerIndex: function( fn, scope ) {
			var length = this.listeners.length,
				i, ls;
				
			scope = scope || this.obj;	
				
			for (i = 0; i < length; i++) {
				ls = this.listeners[i];
				if ( ls.fn == fn && ls.scope == scope ) {
					return i;
				}
			}
			return -1;
		},		
		
		/**
		 * Remove a listener
		 * @param {function} fn
		 * @param {object} scope
		 */
		removeListener: function( fn, scope ) {
			var index = this.getListenerIndex(fn, scope);
			if ( index === -1 ) {
				return;
			}
			
			if (this.firing) {
				this.listeners = this.listeners.slice(0);
			}
			this.listeners.splice(index, 1);
		}
	};
})();


/**
 * @class Runner.util.Observable
 * Observer-subscriber class
 * Provides base functionality for PHPRunner's custom events handling
 */
Runner.util.Observable = Runner.extend( Runner.emptyFn, {	
	/**
	 * The cutom event objects storage
	 * @type {Object}
	 */
	events: null,
	
	/**
	 * The regexp checking if the string is otions string
	 * @type {RegExp}
	 */
	filterOptRe: /^(?:scope|delay|buffer|single)$/,
	
	/**
	 * Add the events with names that are listed in arguments
	 */
	addEvents: function() {
		var eventsNames = Array.prototype.slice.call( arguments ),
			evName, i;
		
		this.events = this.events || {};
		
		for (i = 0; i < eventsNames.length; i++) {
			evName = eventsNames[i].toLowerCase();
			this.events[ evName ] = this.events[ evName ] || true;
		}	
	},
	
	/**
	 * Fire a custom event
	 * The first argument must be an event name,
	 * the other will be passed as the event's arguments 
	 * @return {boolean}
	 */
	fireEvent: function() {
		var evName = arguments[0],
			ce;
		
		if ( this.isEventExist( evName ) ) {
			ce = this.events[ evName.toLowerCase() ];
			return ce.fire.apply( ce, Array.prototype.slice.call(arguments, 1) );
		}
		return true;
	},
	
	/**
	 * Check if a custom event with a particualr name exists
	 * @param {string} evName
	 * @return {boolean}
	 */
	isEventExist: function( evName ) {
		if ( this.eventsSuspended === true ) {
			return false;	
		}
		
		var ce = this.events[ evName.toLowerCase() ];
		return ce instanceof Runner.util.Event;
	},

	/**
	 * Subscribe a custom event's handler
	 * @param {string | object} evName
	 * @param {function} fn
	 * @param {object} scope
	 * @param {object} options
	 */ 	
	on: function( evName, fn, scope, options ) {
		var event, ce;
		
		if ( typeof evName === "object" ) {
			options = evName;
			
			for (event in options) {
				if ( this.filterOptRe.test( event ) ) {
					continue;
				}
				
				if ( typeof options[ event ] === "function" ) {
					this.on( event, options[ event ], options.scope, options );
				} else {
					this.on( event, options[ event ].fn, options[ event ].scope, options[ event ] );
				}
			}
			
			return;
		}
		
		options = options || {};
		evName = evName.toLowerCase();
		
		ce = this.events[ evName ];

		if ( !(ce instanceof Runner.util.Event) ) {
			ce = new Runner.util.Event(this, evName);
			this.events[ evName ] = ce;
		}
		
		ce.on( fn, scope, options );
	},
	
	/**
	 * Unsubscribe a custom event's handler
	 * @param {string} evName
	 * @param {function} fn
	 * @param {object} scope
	 */ 
	un: function( evName, fn, scope ) {
		var ce = this.events[ evName.toLowerCase() ];
				
		if ( ce instanceof Runner.util.Event ) {
			ce.removeListener(fn, scope);
		}
	},
	
	/**
	 * Purge (clear) array of listeners for events
	 */
	purgeListeners: function() {
		for ( var event in this.events ) {
			if ( this.events[ event ] instanceof Runner.util.Event ) {
				this.events[ event ].listeners = [];
			}
		}
	},
	
	/**
	 * Make events suspended (delayed) for fire
	 */
	suspendEvents: function() {
		this.eventsSuspended = true;
	},
	
	/**
	 * Make events available for fire
	 */
	resumeEvents: function() {
		this.eventsSuspended = false;
	}
});
Runner.anychartLicense = 'xlinesoft-9faa5dd-332123fd';
 
// register new namespace
Runner.namespace( 'Runner.util' );


Runner.util.getHiddenDimensions = function ( $elem ) {
	var position, cloned = false;

	if ( $elem.is( ":hidden" ) ) {
		$elem = $elem.clone()
			.css( {
				display: "inline-block",
				position: "absolute",
				visibility: "hidden"
			} )
			.appendTo( $elem.closest( ":visible" ) );
		cloned = true;
	}

	position = {
		outerWidth: $elem.outerWidth(),
		outerHeight: $elem.outerHeight(),
		width: $elem.width(),
		height: $elem.height()
	};

	if ( cloned ) {
		$elem.remove();
	}

	return position;
};

Runner.util.getChildDisplayedElems = function ( elem ) {
	var foundElems = [];

	$.each( elem.children(), function ( i, item ) {
		if ( $( item ).css( "display" ) != "none" ) {
			foundElems.push( item );
		}
	} );

	return foundElems;
};

/**
 *
 */
Runner.util.dropPositionSet = function ( $dropButton ) {

	$dropButton.parent()
		.on( "shown.bs.dropdown", function () {
			var $el = $( this ),
				$ul = $el.children( ".dropdown-menu" ),
				$button = $el.children( ".dropdown-toggle" ),
				ulOffset = $ul.offset(),
				spaceUp = ulOffset.top - $button.height() - $ul.height() - $( window ).scrollTop(),
				spaceDown = $( window ).scrollTop() + $( window ).height() - ulOffset.top - $ul.height();

			if ( spaceDown < 0 && ( spaceUp >= 0 || spaceUp > spaceDown ) ) {
				$el.addClass( "dropup" );
			}
		} )
		.on( "hidden.bs.dropdown", function () {
			$( this ).removeClass( "dropup" );
		} );
};


/**
 * @class Runner.util.DelayedTask
 * method for performing setTimeout where a new timeout cancels the old timeout.
 * @param {Function} fn (optional) The default function to timeout
 * @param {Object} scope (optional) The default scope of that timeout
 * @param {Array} args (optional) The default Array of arguments
 */
Runner.util.DelayedTask = function ( fn, scope, args ) {
	var id = null,
		delay, time;

	var call = function () {
		var now = new Date().getTime();
		if ( now - time >= delay ) {
			clearInterval( id );
			id = null;
			fn.apply( scope, args || [] );
		}
	};

	this.started = function () {
		return id != null;
	}

	/**
	 * Cancels any pending timeout and queues a new one
	 * @param {Number} delay The milliseconds to delay
	 * @param {Function} newFn (optional) Overrides function passed to constructor
	 * @param {Object} newScope (optional) Overrides scope passed to constructor
	 * @param {Array} newArgs (optional) Overrides args passed to constructor
	 */
	this.delay = function ( newDelay, newFn, newScope, newArgs ) {
		if ( id /*&& delay != newDelay */ ) {
			this.cancel();
		}
		delay = newDelay;
		time = new Date().getTime();
		fn = newFn || fn;
		scope = newScope || scope;
		args = newArgs || args;
		if ( !id ) {
			id = setInterval( call, delay );
		}
	};

	/**
	 * Cancel the last queued timeout
	 */
	this.cancel = function () {
		if ( id ) {
			clearInterval( id );
			id = null;
		}
	};
};

/**
 * 	Returns current device class basing on the screen dimensions
 *	@returns {Number} one of the constants from DESKTOP to SMARTPHONE_PORTRAIT
 */
Runner.util.getDeviceClass = function () {
	return window.screen.width >= 768
		? Runner.pages.constants.DESKTOP
		: Runner.pages.constants.SMARTPHONE_PORTRAIT;
}

/**
 * 	Returns current device "macro class" which is DESKTOP or TABLET or PHONE
 *	@returns {Number} 0 - DESKTOP, 1 - TABLET, 2 - PHONE
 */
Runner.util.getDeviceMacroClass = function () {
	var c = Runner.util.getDeviceClass();

	if ( c == Runner.pages.constants.SMARTPHONE_LANDSCAPE || Runner.pages.constants.SMARTPHONE_PORTRAIT ) {
		return 2;
	}

	if ( c == Runner.pages.constants.TABLET_7_IN || c == Runner.pages.constants.TABLET_10_IN ) {
		return 1;
	}

	return 0;
}


/**
 * Global object for loading scripts and css files
 * @object
 */
Runner.util.ScriptLoader = Runner.extend( Runner.util.Observable, {
	/**
	 * Array of CSS files for loading
	 * @type {array}
	 */
	cssFiles: [],

	/**
	 * Array of file names for load
	 * @type {array}
	 */
	jsFiles: [],

	/**
	 * The number of JS files loading session
	 * @type {number}
	 */
	session: 0,

	/**
	 * An array storing the closed sessions of JS files loading.
	 * Each session is started with the 'load' method and is closed
	 * when all the session's JS files are loaded
	 * @type {array}
	 */
	loadedSes: [],

	constructor: function ( cfg ) {
		Runner.util.ScriptLoader.superclass.constructor.call( this, cfg );

		this.addEvents();

		this.onFilesLoaded( function () {
			if ( Runner.pages ) {
				Runner.pages.PageManager.initPages();
			}
		}, this );
	},

	/**
	 * Add js file to load queue
	 * @param {array} files
	 * @param any param except first is added to requirements array
	 */
	addJS: function ( files ) {
		var isAdded, i, j;
		// loop through all files to add
		for ( i = 0; i < files.length; i++ ) {
			isAdded = false;

			// check if such file was added before
			for ( j = 0; j < this.jsFiles.length; j++ ) {
				if ( this.jsFiles[j].name == files[i] ) {
					isAdded = true;
					break;
				}
			}
			// add only new files
			if ( !isAdded ) {
				// add files to array of file names
				this.jsFiles.push( {
					name: files[i],
					isLoaded: false,
					// add requirements, all passed arguments, except first
					requirements: Array.prototype.slice.call( arguments, 1 ),
					session: parseInt( this.session )
				} );
			}
		}
	},

	/**
	 * Method produces a link to IE8-compatible stylesheet if needed
	 * @param {string} url
	 * @return {string} url
	 */
	cssProxy: ( function () {
		return function ( url ) {
			return url;
		}
	} )(),

	/**
	 * Check if the css file with a particular name is loaded
	 * @param {string} fileName
	 * @return {boolean}
	 */
	isCSSfileLoaded: function ( fileName ) {
		for ( var i = 0; i < this.cssFiles.length; i++ ) {
			if ( this.cssFiles[i].name == fileName && this.cssFiles[i].isLoaded ) {
				return true;
			}
		}
		return false;
	},

	isSystemCSSFile: function ( filePath ) {
		return filePath.indexOf( "/pages/" ) === -1
			&& filePath.indexOf( "/custom/" ) === -1;
	},

	/**
	 * Method for load CSS files
	 * @param files {array}
	 */
	loadCSS: function ( files ) {
		var _self = this;

		if ( !files ) {
			return;
		}

		files = typeof files === "string" ? [files] : files;

		jQuery.each( files, function ( i, file ) {
			if ( !file ) {
				return;
			}
			file = _self.cssProxy( file );

			file = _self.cssFilePath( file );

			// return if file is already added and loaded
			if ( _self.isCSSfileLoaded( file ) ) {
				return;
			}

			_self.cssFiles.push( {
				name: file,
				isLoaded: true
			} )

			var existInHead = false,
				head, css, filePath;

			$( 'head link[rel="stylesheet"]' ).each( function ( index, element ) {
				if ( $( element ).attr( 'href' ) == file ) {
					existInHead = true;
					return false;
				}
			} );

			// load file
			if ( !existInHead ) {
				head = $( document ).find( 'head' )[0];

				if ( Runner.settings.project.ext == "aspx" ) {
					filePath = Runner.getFullResourcePath( _self.cssFilePath( files[i] ) );
				} else {
					filePath = _self.cssFilePath( files[i] );
				}

				css = document.createElement( 'link' );
				css.setAttribute( 'rel', 'stylesheet' );
				css.setAttribute( 'type', 'text/css' );
				css.setAttribute( 'href', filePath );
				head.appendChild( css );
			}
		} );
	},

	cssFilePath: function ( file ) {
		if ( file.indexOf( "style.css" ) !== -1 ) {
			return file + "?" + settings.global.wizardBuildKey + "&" + settings.global.projectBuildKey;
		} else if ( this.isSystemCSSFile( file ) ) {
			return file + "?" + settings.global.wizardBuildKey;
		} else {
			return file + "?" + settings.global.projectBuildKey;
		}
	},

	load: function () {
		if ( this.jsFiles.length == 0 ) {
			this.fireEvent( 'filesLoaded' + this.session );
		}

		this.session = this.session + 1;

		for ( var i = 0; i < this.jsFiles.length; i++ ) {
			this.loadJS( i );
		}
	},

	/**
	 * Load a file from the files queue
	 * @param {number} idx file index
	 * @return {boolean} true if success
	 * @method
	 * @private
	 */
	loadJS: function ( idx ) {
		// return if no file obj for this file
		if ( !this.jsFiles[idx] ) {
			return false;
		}
		// if loaded, load dependent files
		if ( this.jsFiles[idx].isLoaded ) {
			this.jsFiles[idx].session = this.session - 1;
			this.postLoad( idx );
			return true;
		}
		// check requirements
		if ( !this.checkReq( this.jsFiles[idx] ) ) {
			return false;
		}
		// file loading started already
		if ( this.jsFiles[idx].isStarted ) {
			return false;
		}
		// load file
		this.jsFiles[idx].isStarted = true;

		var self = this,
			js = document.createElement( 'script' ),
			// a definition of such a function is the way to invoke Runner custom code when the external js file is loaded
			initFuncName = this.jsFiles[idx].name.replace( '.js', '' ).replace( /[\/\\.\-]/g, "_" ).replace( /^_+/, '' ) + '_init',
			postLoadFunction = function () {
				var tl = true;

				if ( typeof Runner[initFuncName] === "function" ) {
					tl = Runner[initFuncName].call( self, idx );
				}

				if ( tl !== false ) {
					self.postLoad( idx );
				}
			};

		js.setAttribute( 'type', 'text/javascript' );

		if ( Runner.isIE ) {
			js.onreadystatechange = function () {
				if ( js.readyState == 'complete' || js.readyState == 'loaded' ) {
					postLoadFunction();
				}
			};
		} else {
			js.onload = postLoadFunction;
		}

		var addKey = "?" + ( this.isSystemJSFile( this.jsFiles[idx].name ) ?
			settings.global.wizardBuildKey :
			settings.global.projectBuildKey );

		js.setAttribute( 'src', this.jsFiles[idx].name + addKey );
		document.getElementsByTagName( 'HEAD' )[0].appendChild( js );
		return true;
	},

	isSystemJSFile: function ( filePath ) {
		return filePath.indexOf( "usercode" ) === -1;
	},

	/**
	 * Checks is required files are loaded
	 * @param {object} fileObj
	 * @return {Boolean}
	 */
	checkReq: function ( fileObj ) {
		// loop through all files
		for ( var i = 0; i < fileObj.requirements.length; i++ ) {
			// loop through all req
			for ( var j = 0; j < this.jsFiles.length; j++ ) {
				// if req cotains loaded file, than try to load it
				if ( fileObj.requirements[i] == this.jsFiles[j].name && !this.jsFiles[j].isLoaded ) {
					return false;
				}
			}
		}
		return true;
	},

	/**
	 * After event handler. Called after file loaded.
	 * @param {number} idx
	 * @param {session} session
	 * @method
	 */
	postLoad: function ( idx, session ) {
		var loadedAll = true;
		if ( idx !== undefined ) {
			this.jsFiles[idx].isLoaded = true;
			this.loadDependent( idx );
			var session = this.jsFiles[idx].session;
		}
		for ( var i = 0; i < this.jsFiles.length; i++ ) {
			if ( !this.jsFiles[i].isLoaded ) {
				loadedAll = false;
				break;
			}
		}
		if ( loadedAll ) {
			var self = this;
			if ( $.inArray( session, this.loadedSes ) == -1 ) {
				window.setTimeout( function () {
					self.fireEvent( 'filesLoaded' + session );
				}, 0 );

				this.loadedSes.push( session );
			}
			if ( idx !== undefined ) {
				for ( var i = 0; i < this.session; i++ ) {
					this.postLoad( undefined, i );
				}
			}
		}
	},

	/**
	 * Add the loading session's 'filesLoaded' event and its handler
	 * @param {function} callback
	 * @param {object} scope
	 */
	onFilesLoaded: function ( callback, scope ) {
		this.on( 'filesLoaded' + this.session, callback, scope, { single: true } );
	},

	/**
	 * Call load for files, which are dependent to file with index = idx
	 * @param {number} idx
	 */
	loadDependent: function ( idx ) {
		// loop through all files
		for ( var i = 0; i < this.jsFiles.length; i++ ) {
			// loop through all req
			for ( var j = 0; j < this.jsFiles[i].requirements.length; j++ ) {
				// if req cotains loaded file, than try to load it
				if ( i != idx && this.jsFiles[i].requirements[j] == this.jsFiles[idx].name ) {
					this.loadJS( i );
				}
			}
		}
	}
} );

Runner.util.ScriptLoader = new Runner.util.ScriptLoader();


/**
 *
 */
Runner.Charts = {
	/**
	 * A storage of user's chart processing handlers
	 */
	chartModifyHandlers: {},

	/**
	 * Create chart object and save it to the local storage or page object
	 * @param {object} chartParams
	 */
	createChart: function ( chartParams ) {
		if ( typeof anychart === "undefined" ) {
			return;
		}

		var modifyChartHandler = this.chartModifyHandlers[chartParams.chartName];

		anychart.graphics.useAbsoluteReferences( false );
		anychart.licenseKey( Runner.anychartLicense );

		$.get( chartParams.xmlFile, function ( data ) {
			var chart, gwidth, i, pageObj, hHeight, fHeight;

			if ( chartParams.pageId !== undefined ) {
				pageObj = Runner.pages.PageManager.getById( chartParams.pageId );
			}

			if ( data.noDataMessage ) {
				if ( pageObj ) {
					pageObj.showPageMessageWithErrorText( data.noDataMessage );
				} else if ( chartParams.pageId !== undefined ) {
					Runner.util.ScriptLoader.onFilesLoaded( function () {
						pageObj = Runner.pages.PageManager.getById( chartParams.pageId );

						pageObj.showPageMessageWithErrorText( data.noDataMessage );
						pageObj.asyncInitCall();
					} );
				}

				if ( chartParams.refreshTime ) {
					setTimeout( function () {
						pageObj && pageObj.hideElement( "message" );
						Runner.Charts.createChart( chartParams );
					}, chartParams.refreshTime * 1000 );
				}

				return;
			}

			if ( data.gauge ) {
				$( '<div id="' + chartParams.containerId + '_header"></div>' )
					.html( data.header )
					.css( "text-align", "center" )
					.appendTo( $( "#" + chartParams.containerId ) );

				$( '<div id="' + chartParams.containerId + '_footer"></div>' )
					.html( data.footer )
					.css( {
						"text-align": "center",
						"clear": "both"
					} )
					.appendTo( $( "#" + chartParams.containerId ) );

				hHeight = $( "#" + chartParams.containerId + "_header" ).height();
				fHeight = $( "#" + chartParams.containerId + "_footer" ).height();
				for ( i = 0; i < data.gauge.length; i++ ) {
					var $gCont = $( '<div id="' + chartParams.containerId + 'g' + i + '"></div>' )
						.css( {
							width: "100%",
							height: "100%", // may have a text value - 100px, 5cm, 33% etc
							"float": "left",
							"margin-right": "15px"
						} )
						.insertAfter( $( "#" + chartParams.containerId + "_header" ) );

					$gCont.css( "height", "-=" + ( hHeight + fHeight ) + "px" );
					$gCont.css( "width", ( $gCont.width() / data.gauge.length ) + "px" )
						.css( "width", "-=15px" );

					Runner.Charts.setupChart( data.gauge[i], {
						height: $gCont.height(),
						refreshTime: chartParams.refreshTime,
						pageId: chartParams.pageId,
						containerId: chartParams.containerId + 'g' + i,
						width: $gCont.width(),
						chartName: chartParams.chartName,
						seriaNum: i,
						chartType: 'Gauge'
					},
						modifyChartHandler );
				}
				return;
			}

			chart = Runner.Charts.setupChart( data, chartParams, modifyChartHandler );

			if ( pageObj ) {
				pageObj.setChart( chart );
			}

			// event listener
			chart.listen( "pointClick", function ( event ) {
				var clickData = data.chart.singleSeries ? data.chart.clickData : data.chart.series && data.chart.series[0].clickData;

				if ( !clickData || clickData[event.pointIndex] === null || isNaN( event.pointIndex ) ) {
					return;
				}

				if ( clickData[event.pointIndex].url ) {
					location.href = clickData[event.pointIndex].url;
					return;
				}

				if ( !pageObj ) {
					pageObj = Runner.pages.PageManager.getById( chartParams.pageId );
					pageObj.setChart( chart );
				}

				if ( pageObj.handleChartClick ) {
					pageObj.handleChartClick( clickData[event.pointIndex] );
				}
			} );

		}, 'json' );
	},

	/**
	 * @param {object} data
	 * @param {object} chartParams
	 * @param {function} modifyChartHandler
	 */
	setupChart: function ( data, chartParams, modifyChartHandler ) {
		if ( typeof anychart === "undefined" ) {
			return;
		}

		var chart = anychart.fromJson( data ),
			pageObj = false, credits;

		// setup custom context menu	
		chart
			.contextMenu()
			.itemsProvider( function () {
				return {
					'item1': {
						'text': Runner.lang.constants.EXPORT_TO_PDF || 'Export to PDF',
						'action': function () {
							chart.saveAsPdf();
						}
					},
					'item2': {
						'text': Runner.lang.constants.EXPORT_TO_CSV || 'Export to CSV',
						'action': function () {
							chart.saveAsCsv();
						}
					},
					'item3': {
						'text': Runner.lang.constants.SAVE_AS_PNG || 'Save as PNG',
						'action': function () {
							chart.saveAsPng();
						}
					},
					'item4': {
						'text': Runner.lang.constants.PRINT || 'Print',
						'action': function () {
							chart.print();
						}
					},
				}
			} );

		//		chart.width = chartParams.width;
		//		chart.height = chartParams.height;
		chart.wMode = 'opaque';
		chart.id = 'chart_' + chartParams.chartName;
		if ( data.gauge ) {
			chart.id += 'g' + chartParams.seriaNum;
		}

		chart.container( chartParams.containerId );

		if ( chartParams.pageId ) {
			pageObj = Runner.pages.PageManager.getById( chartParams.pageId );
		}

		if ( modifyChartHandler ) {
			modifyChartHandler.call( pageObj, chart, pageObj.proxy, pageObj );
		}

		chart.draw();

		//	Gauges don't have data.chart
		if ( data.chart && data.chart.type === "financial" ) {
			chart.xScale().minimum( chart.xScale().minimum() );
			chart.xScale().maximum( chart.xScale().maximum() );
		}

		if ( chartParams.refreshTime ) {
			chartParams.singleSeries = data.chart && data.chart.singleSeries;
			chart.reloadIntervalId = setInterval( function () {
				Runner.Charts.updateChart( chart, chartParams, data.chart || data.gauge );
			}, chartParams.refreshTime * 1000 );
		}

		return chart;
	},

	/**
	 * @param {object} chart
	 * @param {object} chartParams
	 */
	updateChart: function ( chart, chartParams, currentChartData ) {
		var pageObj = Runner.pages.PageManager.getById( chartParams.pageId ),
			params = {
				action: 'refresh',
				rndval: Math.random(),
				chartname: chartParams.chartName,
				showDetails: chartParams.showDetails,
				pageId: chartParams.pageId
			};

		params = Object.assign( params, chartParams.stateParams || {} );
		if ( chartParams.dashChart ) {
			params.dashChart = chartParams.dashChart;
			params.dashTName = chartParams.dashTName;
			params.dashPage = chartParams.dashPage;
			params.dashElementName = chartParams.dashElementName;
		}

		$.post.trackingUserActivity = false;
		$.post( Runner.getPageUrl( "dchartdata" ), params, function ( json ) {
			var i, masterData,
				seriesData = JSON.parse( json ),
				firstMasterData = null,
				masterKeysUpdated = true;

			if ( currentChartData.type === "financial" ) {
				chart.xScale().minimum( null );
				chart.xScale().maximum( null );
			}

			if ( chartParams.chartType === "Gauge" || chartParams.singleSeries ) {
				var seriesNumber = seriesData.length - (chartParams.seriaNum || 0) - 1;
				chart.data( anychart.data.set( seriesData[ seriesNumber ].data ) );
				if ( chartParams.chartType === "Gauge" ) {
					chart.label()
						.text( seriesData[ seriesNumber ].labelText );
				} else {
					currentChartData.clickData = seriesData[0].clickData;
				}
			} else {
				//chart.removeAllSeries();
				for ( i = 0; i < seriesData.length; i++ ) {
					//chart.addSeries( seriesData[i].data );
					chart.getSeriesAt( i ).data( seriesData[i].data );
				}
				currentChartData.series[0].clickData = seriesData[0].clickData;
			}

			chart.draw();

			if ( currentChartData.type === "financial" ) {
				chart.xScale().minimum( chart.xScale().minimum() );
				chart.xScale().maximum( chart.xScale().maximum() );
			}

			if ( chartParams.dashChart && seriesData.length > 1 ) {
				for ( i = 0; i < seriesData.length; ++i ) {
					masterData = seriesData[i].clickData;
					firstMasterData = firstMasterData || masterData;

					if ( pageObj.checkIfMasterKeysUpdated( masterData ) ) {
						masterKeysUpdated = false;
					}
				}

				pageObj.refreshDChartData( firstMasterData, masterKeysUpdated );
			}
		} );
	}
};

/*
##foreach @BUILDER.tables as @t filter IsChart(@t)##
##foreach @t.arrEventHandlers as @h filter @h.strEventID=="EVENT_JS_MODIFY_CHART"##
Runner.Charts.chartModifyHandlers['##@t.strShortTableName##'] = function(chart, proxy, pageObj) {
	##@h.strEventCode##;
};
##endfor##
##endfor##
*/

/**
 * Runner.Async constructor
 * 	Usage:
	Runner.Async.loop( collection, handler );

	Runner.Async.loop( handler )
	is equal to
	Runner.Async.loop( [null], handler )

 *	Sample code:
	------------------------
		Runner.Async.loop( [1,2,3], function(i, n) {
			console.log(n);
			Runner.Async.loop( ['a','b','c'], function(i, n) {
				console.log(n);
			});
			Runner.Async.loop( ['x','y','z'], function(i, n) {
				console.log(n);
			});
		})
	------------------------
	Output:
	------------------------
	1
	a
	b
	c
	x
	y
	z
	2
	a
	b
	c
	x
	y
	z
	3
	a
	b
	c
	x
	y
	z
	------------------------
 */
Runner.Async = function () {
	/**
	 * The asynchronous loops data storage
	 * @type {array}
	 */
	this.stack = [];

	/**
	 * Register a new asynchronous loop block,
	 * start the loop handler async execution
	 * @param {array | function} _collection
	 * @param {function} _handler
	 */
	this.loop = function ( _collection, _handler ) {
		var stackHead;

		//	only one parameter passed
		if ( typeof _handler === "undefined" ) {
			_handler = _collection;
			_collection = [null];
		}

		//		if ( !_collection.length ) {
		//			return;
		//		}

		if ( !this.stack.length ) {
			this.stack.push( {
				buffer: []
			} );
		}

		stackHead = this.stack[this.stack.length - 1];
		stackHead.buffer.push( {
			collection: _collection,
			index: 0,
			handler: _handler
		} );

		setTimeout( this.processFunc, 0 );
	}

	/**
	 * Choose the loop handler to execute along with
	 * its context and params.
	 * Set the priotity to new loops added during
	 * the loop handler execution
	 */
	this.processFunc = $.proxy( function () {
		var stackHead, element, bufLength, handlerResult,
			stackLength = this.stack.length;

		if ( !stackLength ) {
			return;
		}

		stackHead = this.stack[stackLength - 1];
		// an element containig handler data
		element = stackHead.buffer[0];

		if ( element.index < element.collection.length ) {

			bufLength = stackHead.buffer.length;
			handlerResult = element.handler.call( null, element.index, element.collection[element.index] );

			if ( stackHead.buffer.length > bufLength ) {
				//	new tasks were added during the step execution
				//	move them to the stack head
				this.stack.push( {
					buffer: stackHead.buffer.splice( bufLength, stackHead.buffer.length - bufLength )
				} );
			}
		}

		element.index++;
		if ( element.index >= element.collection.length && this.stack.length == stackLength || handlerResult === false ) {
			// there is no elements left in the loop collection
			stackHead.buffer.splice( 0, 1 );

			if ( !stackHead.buffer.length ) {
				this.stack.splice( stackLength - 1, 1 );
			}
		}

		setTimeout( this.processFunc, 0 );
	}, this );
}

/**
 * Runner asynchronous loops controller
 */
Runner.Async = new Runner.Async();

/**
 * Runner hash lib
 */
Runner.Hash = {
	/**
	 * get hash vars
	 * @return {object}
	 */
	get: function () {
		var vars = {},
			hash, splitter, hashes, pos, i;

		if ( !this.oldbrowser() ) {
			pos = window.location.href.indexOf( '?' );
			hashes = ( pos != -1 ) ? decodeURIComponent( window.location.href.substr( pos + 1 ) ) : '';
			splitter = '&';
		} else {
			hashes = decodeURIComponent( window.location.hash.substr( 1 ) );
			splitter = '/';
		}

		if ( !hashes.length ) {
			return null;
		}

		hashes = hashes.split( splitter );

		for ( i in hashes ) {
			if ( hashes.hasOwnProperty( i ) ) {
				hash = hashes[i].split( '=' );
				if ( typeof hash[1] === 'undefined' ) {
					vars['anchor'] = hash[0];
				} else {
					vars[hash[0]] = hash[1];
				}
			}
		}

		return vars;
	},
	/**
	 * Set array vars in to hash
	 * @param {object} vars
	 * @param {boolean} isReplace
	 */
	set: function ( vars, isReplace ) {
		var hash = '', i;

		for ( i in vars ) {
			if ( vars.hasOwnProperty( i ) ) {
				hash += '&' + i + '=' + vars[i];
			}
		}

		if ( hash ) {
			hash = '?' + hash.substr( 1 );
		}

		if ( this.oldbrowser() ) {
			window.location.hash = hash;
			return;
		}

		if ( isReplace ) {
			window.history.replaceState( hash, '', document.location.pathname + hash );
		} else {
			window.history.pushState( hash, '', document.location.pathname + hash );
		}
	},

	/**
	 * for window back/forward button click
	 * @param {function} callback
	 */
	changeUrlHandler: function ( callback ) {
		var urlParams = Runner.Hash.get(),
			newUrl = "", i;

		if ( !urlParams ) {
			return;
		}

		// unset session params
		if ( urlParams.goto === undefined ) {
			urlParams.goto = 1;
		}

		if ( urlParams.orderby === undefined ) {
			urlParams.orderby = "noorder";
		}

		if ( urlParams.f === undefined ) {
			urlParams.f = "all";
		}

		for ( i in urlParams ) {
			if ( urlParams.hasOwnProperty( i ) ) {
				newUrl += '&' + i + '=' + urlParams[i];
			}
		}

		if ( newUrl ) {
			newUrl = '?' + newUrl.substr( 1 );
		}

		callback( newUrl );
	},

	/**
	 * for window back/forward button click
	 * @param {function} listnerCallBack
	 */
	changeUrlListener: function ( listnerCallBack ) {
		var self = this;

		if ( !this.oldbrowser() ) {
			$( window ).on( "popstate", function () {
				self.changeUrlHandler( listnerCallBack );
			} );
		} else if ( window.addEventListener ) {
			window.addEventListener( "hashchange", function () {
				//console.log("OLD BROWSER!");
				self.changeUrlHandler( listnerCallBack );
			} );
		}
	},

	/**
	 * Add simple hash key in to hash
	 * @param {string} key
	 * @param {string} val
	 */
	add: function ( key, val ) {
		var hash = this.get() || {};

		hash[key] = val;
		this.set( hash );
	},

	/**
	 * remove simple key from hash
	 * @param {string} key
	 */
	remove: function ( key ) {
		var hash = this.get();
		if ( hash ) {
			delete hash[key];
			this.set( hash );
		}
	},

	/**
	 * clear hash
	 */
	clear: function () {
		this.set( {}, true );
	},

	/**
	 * Check for support history api
	 * @return {boolean}
	 */
	oldbrowser: function () {
		return !window.history || !history.pushState;
	}
};

/**
 * Check if the two arrays are equal
 * @param {array} arr1
 * @param {array} arr2
 * @param {boolean} typeInsensitive (optional) to compare cases ["1", "2"], [1, 2]
 * @return {boolean}
 */

Runner.arraysEqual = function ( arr1, arr2, typeInsensitive ) {
	if ( arr1.length !== arr2.length ) {
		return false;
	}

	for ( var i = arr1.length; i--; ) {
		if ( !typeInsensitive && arr1[i] !== arr2[i] || typeInsensitive && arr1[i] != arr2[i] ) {
			return false;
		}
	}

	return true;
}

/**
 * @param {object} model
 * @return {object}
 */
Runner.normalizeClickActionSettings = function ( model ) {
	var normalizeSubmodel = function ( submodel ) {
		submodel = submodel || {};
		submodel.action = submodel.action || "noaction";
		submodel.openData = submodel.openData || {};
		submodel.gridData = submodel.gridData || {};
		submodel.codeData = submodel.codeData || {};

		return submodel;
	}

	if ( !model || typeof model !== "object" ) { //if ( !(model instanceof Object) ) {
		model = {};
		//return Runner.normalizeClickActionSettings( {} );
	}

	model.row = normalizeSubmodel( model.row );
	model.fields = model.fields || {};

	for ( var f in model.fields ) {
		model.fields[f] = normalizeSubmodel( model.fields[f] );
	}

	return model;
}

/**
 * Generate Runner.md5
 * Source http://www.myersdaily.org/joseph/javascript/md5.js
 * @param {mixed}
 * @return {string}
 */
Runner.md5 = ( function () {
	function md5cycle( x, k ) {
		var a = x[0], b = x[1], c = x[2], d = x[3];

		a = ff( a, b, c, d, k[0], 7, -680876936 );
		d = ff( d, a, b, c, k[1], 12, -389564586 );
		c = ff( c, d, a, b, k[2], 17, 606105819 );
		b = ff( b, c, d, a, k[3], 22, -1044525330 );
		a = ff( a, b, c, d, k[4], 7, -176418897 );
		d = ff( d, a, b, c, k[5], 12, 1200080426 );
		c = ff( c, d, a, b, k[6], 17, -1473231341 );
		b = ff( b, c, d, a, k[7], 22, -45705983 );
		a = ff( a, b, c, d, k[8], 7, 1770035416 );
		d = ff( d, a, b, c, k[9], 12, -1958414417 );
		c = ff( c, d, a, b, k[10], 17, -42063 );
		b = ff( b, c, d, a, k[11], 22, -1990404162 );
		a = ff( a, b, c, d, k[12], 7, 1804603682 );
		d = ff( d, a, b, c, k[13], 12, -40341101 );
		c = ff( c, d, a, b, k[14], 17, -1502002290 );
		b = ff( b, c, d, a, k[15], 22, 1236535329 );

		a = gg( a, b, c, d, k[1], 5, -165796510 );
		d = gg( d, a, b, c, k[6], 9, -1069501632 );
		c = gg( c, d, a, b, k[11], 14, 643717713 );
		b = gg( b, c, d, a, k[0], 20, -373897302 );
		a = gg( a, b, c, d, k[5], 5, -701558691 );
		d = gg( d, a, b, c, k[10], 9, 38016083 );
		c = gg( c, d, a, b, k[15], 14, -660478335 );
		b = gg( b, c, d, a, k[4], 20, -405537848 );
		a = gg( a, b, c, d, k[9], 5, 568446438 );
		d = gg( d, a, b, c, k[14], 9, -1019803690 );
		c = gg( c, d, a, b, k[3], 14, -187363961 );
		b = gg( b, c, d, a, k[8], 20, 1163531501 );
		a = gg( a, b, c, d, k[13], 5, -1444681467 );
		d = gg( d, a, b, c, k[2], 9, -51403784 );
		c = gg( c, d, a, b, k[7], 14, 1735328473 );
		b = gg( b, c, d, a, k[12], 20, -1926607734 );

		a = hh( a, b, c, d, k[5], 4, -378558 );
		d = hh( d, a, b, c, k[8], 11, -2022574463 );
		c = hh( c, d, a, b, k[11], 16, 1839030562 );
		b = hh( b, c, d, a, k[14], 23, -35309556 );
		a = hh( a, b, c, d, k[1], 4, -1530992060 );
		d = hh( d, a, b, c, k[4], 11, 1272893353 );
		c = hh( c, d, a, b, k[7], 16, -155497632 );
		b = hh( b, c, d, a, k[10], 23, -1094730640 );
		a = hh( a, b, c, d, k[13], 4, 681279174 );
		d = hh( d, a, b, c, k[0], 11, -358537222 );
		c = hh( c, d, a, b, k[3], 16, -722521979 );
		b = hh( b, c, d, a, k[6], 23, 76029189 );
		a = hh( a, b, c, d, k[9], 4, -640364487 );
		d = hh( d, a, b, c, k[12], 11, -421815835 );
		c = hh( c, d, a, b, k[15], 16, 530742520 );
		b = hh( b, c, d, a, k[2], 23, -995338651 );

		a = ii( a, b, c, d, k[0], 6, -198630844 );
		d = ii( d, a, b, c, k[7], 10, 1126891415 );
		c = ii( c, d, a, b, k[14], 15, -1416354905 );
		b = ii( b, c, d, a, k[5], 21, -57434055 );
		a = ii( a, b, c, d, k[12], 6, 1700485571 );
		d = ii( d, a, b, c, k[3], 10, -1894986606 );
		c = ii( c, d, a, b, k[10], 15, -1051523 );
		b = ii( b, c, d, a, k[1], 21, -2054922799 );
		a = ii( a, b, c, d, k[8], 6, 1873313359 );
		d = ii( d, a, b, c, k[15], 10, -30611744 );
		c = ii( c, d, a, b, k[6], 15, -1560198380 );
		b = ii( b, c, d, a, k[13], 21, 1309151649 );
		a = ii( a, b, c, d, k[4], 6, -145523070 );
		d = ii( d, a, b, c, k[11], 10, -1120210379 );
		c = ii( c, d, a, b, k[2], 15, 718787259 );
		b = ii( b, c, d, a, k[9], 21, -343485551 );

		x[0] = add32( a, x[0] );
		x[1] = add32( b, x[1] );
		x[2] = add32( c, x[2] );
		x[3] = add32( d, x[3] );
	}

	function cmn( q, a, b, x, s, t ) {
		a = add32( add32( a, q ), add32( x, t ) );
		return add32( ( a << s ) | ( a >>> ( 32 - s ) ), b );
	}

	function ff( a, b, c, d, x, s, t ) {
		return cmn( ( b & c ) | ( ( ~b ) & d ), a, b, x, s, t );
	}

	function gg( a, b, c, d, x, s, t ) {
		return cmn( ( b & d ) | ( c & ( ~d ) ), a, b, x, s, t );
	}

	function hh( a, b, c, d, x, s, t ) {
		return cmn( b ^ c ^ d, a, b, x, s, t );
	}

	function ii( a, b, c, d, x, s, t ) {
		return cmn( c ^ ( b | ( ~d ) ), a, b, x, s, t );
	}

	function md51( s ) {
		var n = s.length,
			tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
			state = [1732584193, -271733879, -1732584194, 271733878],
			i;

		for ( i = 64; i <= s.length; i += 64 ) {
			md5cycle( state, md5blk( s.substring( i - 64, i ) ) );
		}
		s = s.substring( i - 64 );

		for ( i = 0; i < s.length; i++ ) {
			tail[i >> 2] |= s.charCodeAt( i ) << ( ( i % 4 ) << 3 );
		}
		tail[i >> 2] |= 0x80 << ( ( i % 4 ) << 3 );

		if ( i > 55 ) {
			md5cycle( state, tail );
			for ( i = 0; i < 16; i++ ) {
				tail[i] = 0;
			}
		}

		tail[14] = n * 8;
		md5cycle( state, tail );

		return state;
	}

	/* there needs to be support for Unicode here,
	 * unless we pretend that we can redefine the MD-5
	 * algorithm for multi-byte characters (perhaps
	 * by adding every four 16-bit characters and
	 * shortening the sum to 32 bits). Otherwise
	 * I suggest performing MD-5 as if every character
	 * was two bytes--e.g., 0040 0025 = @%--but then
	 * how will an ordinary MD-5 sum be matched?
	 * There is no way to standardize text to something
	 * like UTF-8 before transformation; speed cost is
	 * utterly prohibitive. The JavaScript standard
	 * itself needs to look at this: it should start
	 * providing access to strings as preformed UTF-8
	 * 8-bit unsigned value arrays.
	 */
	function md5blk( s ) { /* I figured global was faster.   */
		var md5blks = [], i; /* Andy King said do it this way. */
		for ( i = 0; i < 64; i += 4 ) {
			md5blks[i >> 2] = s.charCodeAt( i )
				+ ( s.charCodeAt( i + 1 ) << 8 )
				+ ( s.charCodeAt( i + 2 ) << 16 )
				+ ( s.charCodeAt( i + 3 ) << 24 );
		}
		return md5blks;
	}

	var hex_chr = '0123456789abcdef'.split( '' );

	function rhex( n ) {
		for ( var s = '', j = 0; j < 4; j++ ) {
			s += hex_chr[( n >> ( j * 8 + 4 ) ) & 0x0F] + hex_chr[( n >> ( j * 8 ) ) & 0x0F];
		}
		return s;
	}

	function hex( x ) {
		for ( var i = 0; i < x.length; i++ ) {
			x[i] = rhex( x[i] );
		}
		return x.join( '' );
	}

	function md5( s ) {
		return hex( md51( s ) );
	}

	/* this function is much faster,
	so if possible we use it. Some IEs
	are the only ones I know of that
	need the idiotic second function,
	generated by an if clause.  */

	function add32( a, b ) {
		return ( a + b ) & 0xFFFFFFFF;
	}

	if ( md5( 'hello' ) != '5d41402abc4b2a76b9719d911017c592' ) {
		function add32( x, y ) {
			var lsw = ( x & 0xFFFF ) + ( y & 0xFFFF ),
				msw = ( x >> 16 ) + ( y >> 16 ) + ( lsw >> 16 );
			return ( msw << 16 ) | ( lsw & 0xFFFF );
		}
	}

	return md5;
}() );

Runner.getCustomLabel = function ( name ) {
	return Runner.lang.customlabels[name];
};

/**
 *	Convert absolute-positioned element to fixed preserving screen coordinates
 */
Runner.util.absToFixed = function ( $e ) {
	var offsetParent = null,
		css = {
			position: 'fixed',
			right: 'auto',
			bottom: 'auto'
		},
		offset = $e.offset(),
		parentOffset;

	if ( !Runner.isIE && !Runner.isIE11 ) {
		$e.parents().each( function ( i, p ) {
			if ( $( p ).css( 'transform' ) != 'none' ) {
				offsetParent = p;
				return false;
			}
		} );
	}

	if ( offsetParent === null ) {
		css.top = offset.top - ( $( document.body ).scrollTop() || $( window ).scrollTop() );  //pageYOffset
		css.left = offset.left - ( $( window ).scrollLeft() || $( document.body ).scrollLeft() );
	} else {
		parentOffset = $( offsetParent ).offset();
		css.top = offset.top - parentOffset.top;
		css.left = offset.left - parentOffset.left;
	}

	$e.css( css );
};


/**
 * @constructor
 * @param {jQuery object} $tabContext
 */
Runner.Tabs = function ( $tabContext ) {
	var $tabNav = $( "ul.nav-tabs", $tabContext ),
		$tabContent = $( ".tab-content", $tabContext );

	Runner.Tabs.preventTabShow = Runner.Tabs.preventTabShow || function ( e ) {
		e.preventDefault();
	};


	/**
	 * @return {number}
	 */
	this.count = function () {
		return $tabNav.children( "li" ).length;
	};

	/**
	 * @param {number} n
	 */
	this.activate = function ( n ) {
		var $li = $tabNav.children( "li" ).eq( n );

		if ( $li.is( ":visible" ) && !$li.hasClass( "disable" ) ) {
			$( "a[data-toggle='tab']", $li ).tab( "show" );
		}
	};

	/**
	 * @return {number}
	 */
	this.activeIdx = function () {
		$activeLi = $tabNav.children( "li.active" );

		return $tabNav.children( "li" ).index( $activeLi );
	};

	/**
	 * @param {number} n
	 */
	this.hide = function ( n ) {
		var $newActiveLi,
			$liToHide = $tabNav.children( "li" ).eq( n );

		if ( n == this.activeIdx() ) {
			$newActiveLi = $tabNav.children( "li:visible" ).not( ".active", ".disabled" );
			$( "a[data-toggle='tab']", $newActiveLi ).tab( "show" );

			if ( !$newActiveLi.length ) {
				this.bodyElement( n ).removeClass( "active" );
				$liToHide.removeClass( "active" );
			}
		}

		$liToHide.hide();
	};

	/**
	 * @param {number} n
	 */
	this.show = function ( n ) {
		$tabNav.children( "li" ).eq( n ).show();
		if ( this.activeIdx() === -1 ) {
			this.activate( n );
		}
	};

	/**
	 * @param {number} n
	 */
	this.disable = function ( n ) {
		var $li = $tabNav.children( "li" ).eq( n );

		$li.addClass( "disabled" );
		$( "a[data-toggle='tab']", $li ).on( "show.bs.tab", Runner.Tabs.preventTabShow );
	};

	/**
	 * @param {number} n
	 */
	this.enable = function ( n ) {
		var $li = $tabNav.children( "li" ).eq( n );

		$li.removeClass( "disabled" );
		$( "a[data-toggle='tab']", $li ).off( "show.bs.tab", Runner.Tabs.preventTabShow );
	};

	/**
	 * @param {number} n
	 * @return {jQuery object}
	   */
	this.headerElement = function ( n ) {
		return $( "a[data-toggle='tab']", $tabNav.children( "li" ).eq( n ) );
	};

	/**
	 * @param {number} n
	 * @return {jQuery object}
	   */
	this.bodyElement = function ( n ) {
		var idSelector = $( "a[data-toggle='tab']", $tabNav.children( "li" ).eq( n ) ).attr( "href" );
		return $( idSelector, $tabContent );
	};

	/**
	 * @param {string} headerHtml
	 * @param {string} panelHtml
	 * @retrn {number}
	 */
	this.addTab = function ( headerHtml, panelHtml ) {
		var tabIdx = this.count() + 1,
			tabId = "newTab" + tabIdx,
			$a = $( '<a role="tab" data-toggle="tab" href="#' + tabId + '"></a>' ).html( headerHtml );

		$( '<li></li>' ).append( $a ).appendTo( $tabNav );
		$( '<div id="' + tabId + '" role="tabpanel" class="tab-pane"></div>' ).html( panelHtml ).appendTo( $tabContent );
		$a.tab();

		return tabIdx;
	};

	/**
	 * @param {number} n
	 * @return {jQuery object}
	   */
	this.removeTab = function ( n ) {
		var $li = $tabNav.children( "li" ).eq( n ),
			$content = $tabContent.children( "div" ).eq( n );

		if ( $li.length ) {
			$li.remove();
			$content.remove()
			this.activate( 0 );
		}
	};

	/**
	 * @param {number} n
	 * @param {number} m
	 */
	this.moveTo = function ( n, m ) {
		var $liToMove = $tabNav.children( "li" ).eq( n );

		if ( m == n || !$liToMove.length ) {
			return;
		}

		if ( m < 1 ) {
			$tabNav.prepend( $liToMove );
			return;
		}

		if ( m >= this.count() ) {
			$tabNav.append( $liToMove );
			return;
		}

		if ( n < m ) {
			$liToMove.insertAfter( $tabNav.children( "li" ).eq( m ) );
		} else {
			$liToMove.insertBefore( $tabNav.children( "li" ).eq( m ) );
		}
	};
};

/**
 * @constructor
 * @param {jQuery object} $context
 */
Runner.Section = function ( $context ) {
	var $toggler = $( '[data-toggle="collapse"]', $context ),
		$toggledPanel = $( $toggler.attr( "href" ), $context );

	/**
	 * @return {jQuery object}
	   */
	this.headerElement = function () {
		return $( '[data-toggle="collapse"]', $context );
	};

	/**
	 * @return {jQuery object}
	   */
	this.bodyElement = function () {
		return $toggledPanel.children( ".panel-body" );
	};

	this.expand = function () {
		$toggledPanel.collapse( "show" )
	};

	this.collapse = function () {
		$toggledPanel.collapse( "hide" );
	};

	this.get = function () {
		return $toggledPanel;
	};
};


/* Bootstrap Dropdown fork to allow nested dropdowns */

+function ( $ ) {
	'use strict';

	// DROPDOWN CLASS DEFINITION
	// =========================

	var backdrop = '.dropdown-backdrop'
	var toggle = '[data-toggle="nested-dropdown"]'
	var Dropdown = function ( element ) {
		$( element ).on( 'click.bs.dropdown', this.toggle )
	}

	Dropdown.VERSION = '3.3.5'

	function getParent( $this ) {
		var selector = $this.attr( 'data-target' )

		if ( !selector ) {
			selector = $this.attr( 'href' )
			selector = selector && /#[A-Za-z]/.test( selector ) && selector.replace( /.*(?=#[^\s]*$)/, '' ) // strip for ie7
		}

		var $parent = selector && $( selector )

		return $parent && $parent.length ? $parent : $this.parent()
	}

	function clearMenus( e ) {
		if ( e && e.which === 3 ) return
		$( backdrop ).remove()
		$( toggle ).each( function () {
			var $this = $( this );
			var $clickedMenu = $( e.target ).closest( toggle );
			if ( $this.length == 0 ) {
				$this = $( this );
			}
			var $parent = getParent( $this )
			var relatedTarget = { relatedTarget: this }

			if ( !$parent.hasClass( 'open' ) ) return;


			if ( e && e.type == 'click' && /input|textarea/i.test( e.target.tagName ) && $.contains( $parent[0], e.target ) ) return

			//check if $parent is ascendant of e.target, but not its direct parent
			//	  if (e && !$(e.target).parent().is($parent) &&  $parent.has( e.target ).length )
			//			return;
			if ( $clickedMenu.closest( $parent ).length ) {
				return;
			}

			$parent.trigger( e = $.Event( 'hide.bs.dropdown', relatedTarget ) )

			if ( e.isDefaultPrevented() ) return

			$this.attr( 'aria-expanded', 'false' )
			$parent.removeClass( 'open' ).trigger( 'hidden.bs.dropdown', relatedTarget )
		} )
	}

	Dropdown.prototype.toggle = function ( e ) {
		var $this = $( this )

		if ( $this.is( '.disabled, :disabled' ) ) return

		var $parent = getParent( $this )
		var isActive = $parent.hasClass( 'open' )

		clearMenus( e )

		if ( !isActive ) {
			if ( 'ontouchstart' in document.documentElement && !$parent.closest( '.navbar-nav' ).length ) {
				// if mobile we use a backdrop because click events don't delegate
				$( document.createElement( 'div' ) )
					.addClass( 'dropdown-backdrop' )
					.insertAfter( $( this ) )
					.on( 'click', clearMenus )
			}

			var relatedTarget = { relatedTarget: this }
			$parent.trigger( e = $.Event( 'show.bs.dropdown', relatedTarget ) )

			if ( e.isDefaultPrevented() ) return

			$this
				.trigger( 'focus' )
				.attr( 'aria-expanded', 'true' )

			$parent
				.toggleClass( 'open' )
				.trigger( 'shown.bs.dropdown', relatedTarget )
		}

		return false
	}

	Dropdown.prototype.keydown = function ( e ) {
		if ( !/(38|40|27|32)/.test( e.which ) || /input|textarea/i.test( e.target.tagName ) ) return

		var $this = $( this )

		e.preventDefault()
		e.stopPropagation()

		if ( $this.is( '.disabled, :disabled' ) ) return

		var $parent = getParent( $this )
		var isActive = $parent.hasClass( 'open' )

		if ( !isActive && e.which != 27 || isActive && e.which == 27 ) {
			if ( e.which == 27 ) $parent.find( toggle ).trigger( 'focus' )
			return $this.trigger( 'click' )
		}

		var desc = ' li:not(.disabled):visible a'
		var $items = $parent.find( '.dropdown-menu' + desc )

		if ( !$items.length ) return

		var index = $items.index( e.target )

		if ( e.which == 38 && index > 0 ) index--         // up
		if ( e.which == 40 && index < $items.length - 1 ) index++         // down
		if ( !~index ) index = 0

		$items.eq( index ).trigger( 'focus' )
	}


	// DROPDOWN PLUGIN DEFINITION
	// ==========================

	function Plugin( option ) {
		return this.each( function () {
			var $this = $( this )
			var data = $this.data( 'bs.dropdown' )

			if ( !data ) $this.data( 'bs.dropdown', ( data = new Dropdown( this ) ) )
			if ( typeof option == 'string' ) data[option].call( $this )
		} )
	}

	var old = $.fn.dropdown

	$.fn.dropdown = Plugin
	$.fn.dropdown.Constructor = Dropdown


	// DROPDOWN NO CONFLICT
	// ====================

	$.fn.dropdown.noConflict = function () {
		$.fn.dropdown = old
		return this
	}

	Runner.getUrlParams = function () {
		return Object.fromEntries( new URLSearchParams( window.location.search ) );
	}


	// APPLY TO STANDARD DROPDOWN ELEMENTS
	// ===================================

	$( document )
		.on( 'click.bs.dropdown.data-api', clearMenus )
		.on( 'click.bs.dropdown.data-api', '.dropdown form', function ( e ) { e.stopPropagation() } )
		.on( 'click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle )
		.on( 'keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown )
		.on( 'keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown )

}( jQuery );

Runner.escapeHtml = function ( str ) {
	if ( str === null || str === undefined ) {
		return "";
	}
	return str.toString()
		.replace( /&/g, "&amp;" )
		.replace( /</g, "&lt;" )
		.replace( />/g, "&gt;" )
		.replace( /"/g, "&quot;" )
		.replace( /'/g, "&#039;" );
}


Runner.generateId = ( function () {
	var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
	return function ( length ) {
		var ret = '';
		for ( var i = 0; i < length; ++i ) {
			ret += chars.charAt( Math.floor( Math.random() * chars.length ) );
		}
		return ret;
	}
} )();

/**
* @param {string} message
* @param {string} previewMessage (optional)
*/

Runner.displayGenericAjaxError = function ( message, previewMessage ) {
	var showDetailedError = Runner.pages.PageSettings.getGlobalData( "showDetailedError" ),
		$indicator, $error, $removelink;

	if ( !previewMessage ) {
		previewMessage = showDetailedError ? Runner.lang.constants.TEXT_SERVER_ERROR_OCCURRED : Runner.pages.PageSettings.getGlobalData( "customErrorMessage" )
	}

	if ( !previewMessage ) {
		return;
	}

	if ( showDetailedError ) {
		$indicator = $( '<div class="rnr-error-indicator">' + previewMessage
			+ '. <a href=# class="expandlink">' + Runner.lang.constants.TEXT_SEE_DETAILS + '</a></div>' );
	} else {
		$indicator = $( '<div class="rnr-error-indicator">' + previewMessage + '</div>' );
	}

	$error = $( '<div class="rnr-error-message"></div>' )
		.html( message )
		.hide()
		.appendTo( document.body );

	$removelink = $( '<a class="removelink" href="#">Close</a>' )
		.appendTo( $error );

	$indicator.css( {
		'margin-left': '-' + $( document.body ).css( "padding-left" ).replace( "px", "" ) + 'px',
		'margin-top': '-' + $( document.body ).css( "padding-top" ).replace( "px", "" ) + 'px',
		'margin-bottom': ( 1 + parseInt( $( document.body ).css( "padding-top" ).replace( "px", "" ) ) ) + 'px'
	} )
		.prependTo( document.body );

	$removelink.on( 'click', function ( e ) {
		$error.remove();
		$indicator.remove();
		return false;
	} );

	$indicator.on( 'click', '.expandlink', function ( e ) {
		$error.show();
		$indicator.hide();
		return false;
	} );
};




Runner.util.generatePassword = function ( length ) {
	var result = '';
	var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
	var charactersLength = characters.length;
	for ( var i = 0; i < length; i++ ) {
		result += characters.charAt( Math.floor( Math.random() * charactersLength ) );
	}
	return result;
};

Runner.monthNames = [
	Runner.lang.constants.MONTH_JAN,
	Runner.lang.constants.MONTH_FEB,
	Runner.lang.constants.MONTH_MAR,
	Runner.lang.constants.MONTH_APR,
	Runner.lang.constants.MONTH_MAY,
	Runner.lang.constants.MONTH_JUN,
	Runner.lang.constants.MONTH_JUL,
	Runner.lang.constants.MONTH_AUG,
	Runner.lang.constants.MONTH_SEP,
	Runner.lang.constants.MONTH_OCT,
	Runner.lang.constants.MONTH_NOV,
	Runner.lang.constants.MONTH_DEC
],

	Runner.dayNames = [
		Runner.lang.constants.DAY_SU,
		Runner.lang.constants.DAY_MO,
		Runner.lang.constants.DAY_TU,
		Runner.lang.constants.DAY_WE,
		Runner.lang.constants.DAY_TH,
		Runner.lang.constants.DAY_FR,
		Runner.lang.constants.DAY_SA
	];

Runner.setDatePickerLocale = function () {
	moment.updateLocale( 'en', {
		months: Runner.monthNames,
		monthsShort: Runner.monthNames.map( function ( val ) {
			return val.substr( 0, 3 );
		} ),
		weekdaysMin: Runner.dayNames,
		week: {
			dow: ( parseInt( Runner.pages.PageSettings.getGlobalData( "locale" ).startWeekDay ) + 1 ) % 7, // Monday is the first day of the week.	        		
		},
		meridiem: function ( hour, minute, isLowercase ) {
			if ( hour >= 12 ) {
				return Runner.pages.PageSettings.getGlobalData( "locale" ).timePmLetter;
			}
			return Runner.pages.PageSettings.getGlobalData( "locale" ).timeAmLetter;
		}
	} );
};


/**
 * turn on user activity tracking
 */
Runner.trackUserActivity = Runner.emptyFn;
( function () {
	var sessionControl = window.settings.global && window.settings.global["sessionControl"];
	if ( sessionControl && sessionControl["forceExpire"] ) {
		Runner.trackUserActivity = function () {
			Runner.lastUserActivity = Date.now();
		};

		// patch jQuery ajax methods to control user activity 
		var originaljQueryPost = $.post;
		jQuery.post = $.post = function () {
			$.ajax.trackingUserActivity = $.post.trackingUserActivity;

			var result = originaljQueryPost.apply( this, arguments );
			$.post.trackingUserActivity = true;
			return result;
		};
		$.post.trackingUserActivity = true;

		var originaljQueryGet = $.get;
		jQuery.get = $.get = function () {
			$.ajax.trackingUserActivity = $.get.trackingUserActivity;

			var result = originaljQueryGet.apply( this, arguments );
			$.get.trackingUserActivity = true;
			return result;
		};
		$.get.trackingUserActivity = true;

		var originaljQueryAjax = $.ajax;
		jQuery.ajax = $.ajax = function () {
			$.ajax.trackingUserActivity && Runner.trackUserActivity();

			var result = originaljQueryAjax.apply( this, arguments );
			$.ajax.trackingUserActivity = true;
			return result;
		};
		$.ajax.trackingUserActivity = true;
	}


} )();

/**
 * Returns moment.js-compatibel format string for local date
 * @returns String
 */
Runner.getDateFormatString = function () {
	var dateFormat = Runner.pages.PageSettings.getGlobalData( "locale" ).dateFormat;
	var dateDelimiter = Runner.pages.PageSettings.getGlobalData( "locale" )["dateDelimiter"];
	var dateFormat;

	// get date format
	switch ( parseInt( dateFormat ) ) {
		case 0: dateFormat = 'MM/DD/YYYY'.replace( /\//g, dateDelimiter ); break;
		case 1: dateFormat = 'DD/MM/YYYY'.replace( /\//g, dateDelimiter ); break;
		case 2: dateFormat = 'YYYY/MM/DD'.replace( /\//g, dateDelimiter ); break;
		case -1: default: dateFormat = 'dd-mm-yy';
	}

	return dateFormat;
};

/**
 * Returns moment.js-compatibel format string for local time
 * @returns String
 */
Runner.getTimeFormatString = function ( showSeconds, is24hoursFormat ) {
	var leadingZero = Runner.pages.PageSettings.getGlobalData( "locale" ).leadingZero == "1",
		timeDelimiter = Runner.pages.PageSettings.getGlobalData( "locale" ).timeDelimiter;

	if ( is24hoursFormat === undefined ) {
		is24hoursFormat = Runner.pages.PageSettings.getGlobalData( "locale" ).is24hoursFormat == "1";
	}

	var timeFormat,
		hourFormat = is24hoursFormat ? 'H' : 'h',
		hour = leadingZero ? hourFormat + hourFormat : hourFormat,
		minute = 'mm',
		secound = 'ss';

	timeFormat = hour;
	timeFormat += timeDelimiter + minute;
	if ( showSeconds )
		timeFormat += timeDelimiter + secound;
	timeFormat += !is24hoursFormat ? " a" : "";

	return timeFormat;
};

/**
 *
 */
Runner.printTimeFromDate = function ( dateValue, showSeconds, is24hoursFormat ) {
	if ( !( dateValue instanceof Date ) ) {
		return "";
	}

	var leadingZero = Runner.pages.PageSettings.getGlobalData( "locale" ).leadingZero == "1",
		timeDelimiter = Runner.pages.PageSettings.getGlobalData( "locale" ).timeDelimiter,
		timePmLetter = Runner.pages.PageSettings.getGlobalData( "locale" ).timePmLetter
	timeAmLetter = Runner.pages.PageSettings.getGlobalData( "locale" ).timeAmLetter;


	if ( is24hoursFormat === undefined ) {
		is24hoursFormat = Runner.pages.PageSettings.getGlobalData( "locale" ).is24hoursFormat == "1";
	}

	var hours = dateValue.getHours(),
		minutes = dateValue.getMinutes(),
		seconds = dateValue.getSeconds(),
		time,
		ampm = "";

	if ( !is24hoursFormat ) {
		ampm = " ";
		if ( hours > 12 ) {
			hours = hours - 12;
			ampm += timePmLetter;
		} else if ( hours == 12 ) {
			ampm += timePmLetter;
		} else if ( hours === 0 ) {
			hours = 12;
			ampm += timeAmLetter;
		} else {
			ampm += timeAmLetter;
		}
	}

	time = leadingZero && hours < 10 ? '0' + hours : hours;
	time += timeDelimiter + ( minutes < 10 ? '0' + minutes : minutes );

	if ( showSeconds ) {
		time += timeDelimiter + ( seconds < 10 ? '0' + seconds : seconds );
	}

	return time + ampm;
},

	/**
	 *
	 */
	Runner.util.isInViewport = function ( el, horizontally ) {
		const rect = el.getBoundingClientRect();
		if ( horizontally ) {
			return (
				rect.left >= 0 &&
				rect.right <= ( window.innerWidth || document.documentElement.clientWidth )
			);
		}
		return (
			rect.top >= 0 &&
			rect.left >= 0 &&
			rect.bottom <= ( window.innerHeight || document.documentElement.clientHeight ) &&
			rect.right <= ( window.innerWidth || document.documentElement.clientWidth )
		);
	};

/**
 *
 */
Runner.util.isInContainer = function ( el, co, horizontally ) {
	const rectEl = el.getBoundingClientRect();
	const rectCo = co.getBoundingClientRect();
	if ( horizontally ) {
		return (
			rectEl.left >= rectCo.left &&
			rectEl.right <= rectCo.right
		);
	}
	return (
		rectEl.top >= rectCo.top &&
		rectEl.left >= rectCo.left &&
		rectEl.bottom <= rectCo.bottom &&
		rectEl.right <= rectCo.right
	);
};

/**
 * Sweet Alert Confirm Dialog
 * @param {*} message - text in dialog
 * @param {*} confirmAction 
 */
Runner.util.swalConfirm = function ( message, confirmAction ) {
	swal( message, {
		buttons: {
			cancel: Runner.lang.constants.CANCEL,
			proceed: {
				text: Runner.lang.constants.OK,
				value: "ok",
			}
		},
	} )
		.then( function ( value ) {
			switch ( value ) {
				case "ok":
					confirmAction();
					break;
				default:
					break;
			}
		} );
};

/**
 * SweerAlert, SweetAlert2 lazy load
 */
( function () {
	window.swal = function () {
		var _args = arguments;
		Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/sweetalert.min.js'] );

		return new Promise( function ( resolve, reject ) {
			Runner.util.ScriptLoader.onFilesLoaded( function () {
				swal.apply( undefined, _args ).then( function ( value ) {
					resolve( value );
				} );
			} );
			Runner.util.ScriptLoader.load();
		} );
	};


	window.Swal = {
		fire: function () {
			var _args = arguments;
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/sweetalert2.all.min.js'] );

			return new Promise( function ( resolve, reject ) {
				Runner.util.ScriptLoader.onFilesLoaded( function () {
					Swal.fire.apply( Swal, _args ).then( function ( value ) {
						resolve( value );
					} );
				} );
				Runner.util.ScriptLoader.load();
			} );
		}
	}
} )();

/**
 * fit image to specified max width/height
 * 
 * @param {Function} fn Callback, that called with blob result
 * @param {string} imageSrc
 * @param {Number} imageSize maximum width or height of the image in pixels
 */
Runner.util.resizeImage = function ( imageSrc, imageSize, blobType, callbackFn ) {

	var canvas = document.createElement( "canvas" );
	var ctx = canvas.getContext( "2d" );
	var img = new Image();

	// prevents "Tainted canvases may not be exported" error
	img.crossOrigin = "Anonymous";

	img.onload = function () {
		var imgwidth = img.width;
		var imgheight = img.height;

		if ( imgwidth <= imageSize && imgheight <= imageSize ) {
			canvas.width = imgwidth;
			canvas.height = imgheight;
		} else {
			var whRatio = imgwidth / imgheight;
			if ( whRatio >= 1 ) {
				canvas.width = imageSize;
				canvas.height = canvas.width / whRatio;
			} else {
				canvas.height = imageSize;
				canvas.width = canvas.height * whRatio;
			}
		}

		ctx.drawImage(
			img, 0, 0, imgwidth, imgheight, 0, 0, canvas.width, canvas.height
		);

		canvas.toBlob( function ( blob ) {
			callbackFn( blob );
		}, blobType );

	}

	img.src = imageSrc;
};

Runner.util.arrayBufferToBase64 = function ( buffer ) {
	var binary = '';
	var bytes = new Uint8Array( buffer );
	var len = bytes.byteLength;
	for ( var i = 0; i < len; i++ ) {
		binary += String.fromCharCode( bytes[i] );
	}
	return btoa( binary );
};

/**
 * Add number separators for 'view as number', 'view as currency' formatting 
 * @param {number} number
 * @param {object} settings
 * @return {string}
 */
Runner.util.prepareNumberFormatting = function ( number, settings ) {
	var iDigits = settings["decimalDigits"],
		grouping = settings["grouping"],
		ptr, i, outStart, outEnd;

	number = number.toFixed( iDigits );
	intPart = number >= 0 ? Math.floor( number ) : Math.floor( -number );
	fracPart = number >= 0 ? number - intPart : -number - intPart;
	out = intPart.toFixed( 0 );

	// add sousand separators to the integer part of the number
	if ( grouping.length && parseInt( grouping[0], 10 ) ) {
		ptr = out.toString().length;
		for ( i = 0; i < grouping.length; i++ ) {
			if ( !parseInt( grouping[i], 10 ) ) {
				i--;
			}
			if ( ptr <= grouping[i] ) {
				break;
			}
			ptr = ptr - grouping[i];
			outStart = out.slice( 0, ptr );
			outEnd = out.slice( ptr );
			out = outStart + settings["thousandSep"] + outEnd;
		}
	}

	// add a decimal separator and the fractional part to the integer part 
	if ( iDigits > 0 ) {
		fracPart = ( fracPart * Math.pow( 10, iDigits ) ).toFixed( 0 );
		while ( fracPart.length < iDigits ) {
			fracPart = "0" + fracPart;
		}
		out += settings["decimalSep"] + fracPart;
	}
	return out;
};

/**
 * Get 'view as number' formatted value
 * @param {number} number
 * @param {object} numberSettings
 */
Runner.util.getNumberFormatted = function ( number, numberSettings ) {
	var positiveSign = number >= 0,
		outString = Runner.util.prepareNumberFormatting( number, numberSettings );

	// add the positive/negative signs to the formatted number
	if ( positiveSign || parseInt( outString, 10 ) === 0 ) {
		return numberSettings["LOCALE_SPOSITIVESIGN"] + outString;
	}

	switch ( numberSettings["LOCALE_INEGNUMBER"] ) {
		case "0":
			return "(" + outString + ")";
		case "1":
			return "-" + outString;
		case "2":
			return "- " + outString;
		case "3":
			return outString + "-";
		case "4":
			return outString + " -";
		default:
			return outString;
	}
};

/**
 * Get 'view as currency' formatted value
 * @param {number} number
 * @param {object} currencySettings
 */
Runner.util.getCurrencyFormatted = function ( number, currencySettings ) {
	var positiveSign = number >= 0,
		outString = Runner.util.prepareNumberFormatting( number, currencySettings );

	if ( positiveSign || parseInt( outString, 10 ) === 0 ) {
		switch ( currencySettings["LOCALE_ICURRENCY"] ) {
			case "0":
				return currencySettings["LOCALE_SCURRENCY"] + outString;
			case "1":
			case "2":
				return currencySettings["LOCALE_SCURRENCY"] + " " + outString;
			case "3":
				return outString + " " + currencySettings["LOCALE_SCURRENCY"];
			default:
				return outString;
		}
	}

	switch ( currencySettings["LOCALE_INEGCURR"] ) {
		case "0":
			return "(" + currencySettings["LOCALE_SCURRENCY"] + outString + ")";
		case "1":
			return "-" + currencySettings["LOCALE_SCURRENCY"] + outString;
		case "2":
			return currencySettings["LOCALE_SCURRENCY"] + "-" + outString;
		case "3":
			return currencySettings["LOCALE_SCURRENCY"] + outString + "-";
		case "4":
			return "(" + outString + currencySettings["LOCALE_SCURRENCY"] + ")";
		case "5":
			return "-" + outString + currencySettings["LOCALE_SCURRENCY"];
		case "6":
			return outString + "-" + currencySettings["LOCALE_SCURRENCY"];
		case "7":
			return outString + currencySettings["LOCALE_SCURRENCY"] + "-";
		case "8":
			return "-" + outString + " " + currencySettings["LOCALE_SCURRENCY"];
		case "9":
			return "-" + currencySettings["LOCALE_SCURRENCY"] + " " + outString;
		case "10":
			return outString + " " + currencySettings["LOCALE_SCURRENCY"] + "-";
		case "11":
			return currencySettings["LOCALE_SCURRENCY"] + " " + outString + "-";
		case "12":
			return currencySettings["LOCALE_SCURRENCY"] + " -" + outString;
		case "13":
			return outString + "- " + currencySettings["LOCALE_SCURRENCY"];
		case "14":
			return "(" + currencySettings["LOCALE_SCURRENCY"] + " " + outString + ")";
		case "15":
			return "(" + outString + " " + currencySettings["LOCALE_SCURRENCY"] + ")";
		default:
			return outString;
	}
};

Runner.util.getTimeForTotal = function ( totalVal ) {
	var s = totalVal % 60;
	totalVal -= s;
	totalVal /= 60;

	var m = totalVal % 60;
	totalVal -= m;
	totalVal /= 60;

	var h = totalVal % 24;
	totalVal -= h;
	totalVal /= 24;

	var d = totalVal;
	return ( d > 0 ? d + 'd ' : '' ) + ( h == 0 ? '00' : h )
		+ ':' + ( m > 9 ? m : ( m == 0 ? '00' : '0' + m ) )
		+ ':' + ( s > 9 ? s : ( s == 0 ? '00' : '0' + s ) );
};

Runner.util.FieldFilterResizable = function ( fieldName, $dropdown, pageObj ) {
	this.$dropdown = $dropdown;
	this.$ddmenu = $dropdown.find( ".dropdown-menu" );
	this.$wrapper = this.$ddmenu.children( ".resizable" );

	this.pageObj = pageObj;
	this.fieldName = fieldName;

	return this;
}


Runner.util.FieldFilterResizable.prototype = {
	makeResizable: function () {
		var self = this;

		if ( this.$wrapper.is( ".ui-resizable" ) ) {
			this.$wrapper.resizable( "destroy" );
		}

		this.$wrapper.resizable( {
			handles: {
				'se': this.$wrapper.children( ".ui-resizable-se" ),
				'sw': this.$wrapper.children( ".ui-resizable-sw" )
			},
			minWidth: 250,
			minHeight: 150,
			resize: function ( event, ui ) {
				//	left and right are only applied to .dropdown-menu
				self.$wrapper.css( 'left', 0 );
			},
			stop: function ( event, ui ) {
				// part of 'prevent dropdown from closing on resize' device
				setTimeout( function () {
					self.$dropdown.off( 'hide.bs.dropdown' );
				}, 500 );

				self.saveFieldFilterSize( [self.$wrapper.css( 'width' ), self.$wrapper.css( 'height' ), self.$ddmenu.css( 'left' ), self.$ddmenu.css( 'right' )] );
			},
			start: function ( event, ui ) {
				//	prevent dropdown from closing on resize
				self.$dropdown.on( 'hide.bs.dropdown', function () { return false } );

				//	when dragging the right corner, left side stays intact. Vice versa when dragging the left corner.
				var se = $( event.currentTarget ).is( ".ui-resizable-se" );
				var pos = self.$ddmenu.position();
				var width = self.$ddmenu.width();
				if ( !se ) {
					self.$ddmenu.css( {
						'left': 'auto',
						'right': ( self.$dropdown.width() - pos.left - width ) + 'px'
					} );
				} else {
					self.$ddmenu.css( {
						'left': ( pos.left ) + 'px',
						'right': 'auto'
					} );
				}
			}
		} );
	},


	updateSize: function () {
		var size = this.getFieldFilterSize();
		if ( !( size instanceof Array ) || size.length < 4 ) {
			return;
		}
		this.$wrapper.css( {
			width: size[0],
			height: size[1]
		} );
		this.$ddmenu.css( {
			left: size[2],
			right: size[3]
		} );
	},

	saveFieldFilterSize: function ( size ) {
		var rff = JSON.parse( localStorage.getItem( "runnerFieldFilter" ) ) || {};
		rff[this.pageObj.tName] = rff[this.pageObj.tName] || {}
		rff[this.pageObj.tName][this.pageObj.pageName] = rff[this.pageObj.tName][this.pageObj.pageName] || {};
		rff[this.pageObj.tName][this.pageObj.pageName][this.fieldName] = size;

		localStorage.setItem( "runnerFieldFilter", JSON.stringify( rff ) );
	},

	getFieldFilterSize: function () {
		var rff = JSON.parse( localStorage.getItem( "runnerFieldFilter" ) );
		return rff && rff[this.pageObj.tName] && rff[this.pageObj.tName][this.pageObj.pageName]
			&& rff[this.pageObj.tName][this.pageObj.pageName][this.fieldName];
	},
}

Runner.util.timeString = ( dt ) => {
	const zeroPad = ( n, places) =>  String( n ).padStart( places, '0' );
	return `${ zeroPad( dt.getFullYear(), 4 ) }-${ zeroPad( dt.getMonth() + 1, 2 )}-${ zeroPad( dt.getDate(), 2 )} ${zeroPad( dt.getHours(), 2)}:${zeroPad( dt.getMinutes(), 2 )}:${zeroPad( dt.getSeconds(), 2 )}`
}

Runner.util.timeStringUTC = ( dt ) => {
	const zeroPad = ( n, places) =>  String( n ).padStart( places, '0' );
	return `${ zeroPad( dt.getUTCFullYear(), 4 ) }-${ zeroPad( dt.getUTCMonth() + 1, 2 )}-${ zeroPad( dt.getUTCDate(), 2 )} ${zeroPad( dt.getUTCHours(), 2)}:${zeroPad( dt.getUTCMinutes(), 2 )}:${zeroPad( dt.getUTCSeconds(), 2 )}`
}

Runner.util.localDateFromDbString = ( dateString ) => {
	// dates with time are usually parsed as local, but days only - as UIC.
	//	add 'Z' to always parse as UTC
	var utcDate = new Date( dateString + 'Z' );
	return new Date( utcDate.valueOf() + ( new Date().getTimezoneOffset() ) * 60000 );
}

Runner.globalEvents = {}; 
/**
 * params:
 * 		title: string
 * 		header: string
 * 		fields: [ name | {
 * 			name: string,
 * 			label: string,
 * 			type: 'text' | 'textarea',
 * 			value: string,
 * 		}]
 * 		ok: string
 * 		cancel: string
 * 		afterCreate: function
 * 		beforeOK: function
 * 		beforeCancel: function
 *	 	prefix: string
 *		width: number
 *		height: number
 *
 *
 */
 Runner.Dialog = function( params, button ) {
	params = params || {};
	params.fields = params.fields || [];

	//Style may be not loaded
	Runner.util.ScriptLoader.loadCSS( 'include/bootstrap/css/bootstrap-datetimepicker.min.css' );
	
	var title = params['title'] || '',
		_fields = params['fields'] || [],
		header = params['header'] || '',
		footer = params['bottom'] || '';
		
	var okString = params.ok !== undefined
		? params.ok
		: 'OK';
		
	var cancelString = params.cancel !== undefined
		? params.cancel
		: 'Cancel';

	var fields = _fields.map( function( field, idx ) {
		if ( typeof field !== 'object' ) {
			field = {
				name: field
			};
		}
		field.name = field.name || '';
		// pass '' for label to omit it
		field.label = field.label === undefined ? field.name : field.label;
		return field;
	});
	var html = '';
	if ( header !== undefined ) {
		html = '<div>' + header + '</div>';
	}

	var prefix = params['prefix'] || (button && button.btnName) || Runner.util.generatePassword(10);
	var controls = [];

	var fieldControls = fields.map( function( field, idx ) {
		var controlModel = Runner.Dialog.controlBasic;
		if( field.type == 'date' ) {
			controlModel = Runner.Dialog.controlDate;
		} else if( field.type == 'date-text' ) {
			controlModel = Runner.Dialog.controlDateText;
		} else if( field.type == 'time' ) {
			controlModel = Runner.Dialog.controlTime;
		} else if( field.type == 'datetime' ) {
			controlModel = Runner.Dialog.controlDateTime;
		}
		var fieldId = prefix + '_control_' + idx
		var $control = controlModel.create( fieldId, field, idx );
		$control.attr( 'data-controltype', field.type );
		['getStringValue', 'getDateValue', 'setValue', 'getValue'].forEach( function (m) {
			$control[m] = controlModel[m];
		});
		$control.data( 'field', field );

		if( field.value !== undefined ) {
			$control.setValue( field.value );
		}
		var $label = $('<label for="' + fieldId + '"></label>').html( field.label );
		if( field.required ) {
			$label.append( ' <span class="icon-required"></span>' );
			$control.find( 'select, input[type!=hidden]').add( $control ).on( 'change', function(e) {
				if( $control.getStringValue() != '' ) {
					Runner.Dialog.markValid( $control );
				}
			});
		}
		controls.push( $control );
		return $("<div></div>")
			.addClass('r-control')
			.append( $label )
			.append( $control )
			.data( "control", $control );
	}).reduce($.merge, $() );

	var okButtonId = prefix + '_button_ok',
		cancelButtonId = prefix + '_button_cancel';

	var okButton = okString !== ''
		? ('<button class="btn btn-primary" id="' + okButtonId + '">' + okString + '</button>')
		: '';
	var cancelButton = cancelString !== ''
		? ('<button class="btn btn-default" id="' + cancelButtonId + '">' + cancelString + '</button>')
		: '';
		
	if ( Runner.mobileScreen() ) {
		// full screen dialog
		params.width = window.innerWidth;
		params.height = window.innerHeight;
	}
	params.width = params.width || 600;
	params.height = params.height || ( params.fields.length * 80 + 150 );
	if ( params.height > window.innerHeight ) {
		params.height = window.innerHeight - 50;
	}
		
	var win = Runner.displayPopup({
		html: html + '<div class="r-controls"></div>' + footer,
		header: title,
		width: params.width,
		height: params.height,		
		footer: okButton + ' ' + cancelButton,
		afterCreate: function( popup ) {
			//	create controls
			$( '.r-controls', popup.body() ).append( fieldControls );

			// set focus to the first control
			var $firstControl = fieldControls.first().data("control");
			if ( $firstControl && $firstControl.length ) {
				$firstControl.trigger("focus");
			}

			if ( typeof params['afterCreate'] === 'function' ) {
				params['afterCreate'].call( this, popup, controls, prefix );
			}
		}
	});

	$('#' + cancelButtonId).on('click', function() {
		if ( typeof params['beforeCancel'] === 'function' ) {
			if ( params['beforeCancel'].call( this, win, controls ) === false ) {
				return;
			}
		}
		win.close();
	});

	$('#' + okButtonId ).on('click', function() {
		// validate 
		var valid = true;
		fields.forEach( function( field, idx ) {
			//	correct the values
			controls[ idx ].setValue( controls[ idx ].getStringValue() );
			var value = controls[ idx ].getStringValue();
			if( !field.required || ( value != '' && value !== undefined && value !== null ) ) {
				return;
			}
			if( valid ) {
				//	set focus to first invalid field
				controls[ idx ]
					.closest('.r-control')
					.find( 'select, input[type!=hidden]').first()
					.trigger("focus");
			}
			valid = false;
			Runner.Dialog.markInvalid( controls[ idx ] );
		});
		if( !valid ) {
			return;
		}
		

		/* call user event */
		if ( typeof params['beforeOK'] === 'function' ) {
			if ( params['beforeOK'].call( this, win, controls ) === false ) {
				return;
			}
		}
		/* submit */
		if ( button ) {
			fields.forEach( function( field, idx ) {
				button.params[ field.name ] = controls[ idx ].getStringValue();
			});
			win.close();
			button.submitHandler();
		} else {
			win.close();
		}
	});
	return false;
};

Runner.Dialog.controlBasic = {
	create: function( fieldId, field, idx ) {
		var $control;
		if ( field.type === 'textarea' ) {
			return $('<textarea></textarea>')
				.attr( 'id', fieldId )
				.addClass('form-control');
		} else if( field.type == 'lookup') {
			$control = $('<select></select>')
				.attr( 'id', fieldId )
				.addClass('form-control');
			field.options.forEach( function( o ) {
				var v, d;
				if( o instanceof Array ) {
					v = o[0];
					d = o[1];
				} else {
					v = d = o;
				}
				$control.append( $('<option></option>')
					.attr('value', v )
					.text( d )
				);
			});
			return $control;
		} else if( field.type == 'radio') {
			$control = $('<div></div>')
				.attr( 'id', fieldId );
			if( field.horizontal ) {
				$control.attr('data-horizontal', '' );
			}
			field.options.forEach( function( o ) {
				var v, d;
				if( o instanceof Array ) {
					v = o[0];
					d = o[1];
				} else {
					v = d = o;
				}
				$('<div></div>')
					.addClass('radio')
					.append( 
						$('<label></label>')
						.append( $('<input type="radio">')
							.attr('name', fieldId )
							.attr('value', v ) )
						.append( d )
					)
				.appendTo( $control );
			});
			return $control;
		} else if( field.type == 'checkbox' ) {
			return $('<div></div>')
				.addClass('checkbox')
				.append( $('<label></label>')
					.append( $('<input>')
						.attr( 'type', 'checkbox' )
						.attr( 'id', fieldId )
					).append( field.label )
				);

		}
		return $('<input type="text">')
			.attr( 'id', fieldId )
			.addClass('form-control');
	},
	getValue: function() {
		return this.getStringValue();
	},
	getStringValue: function() {
		var field = this.data('field');
		if( field.type === 'radio' ) {
			return this.find( 'input:checked' ).val();
		} else if( field.type === 'checkbox' ) {
			return this.find('input[type=checkbox]').is( ':checked' ) ? '1' : '';
		}
		return this.val();
	},
	getDateValue: function() {
		return this.getValue();
	},
	setValue: function( val ) {
		var field = this.data('field');
		if( field.type === 'radio' ) {
			this.find( 'input' ).val( [val] );
			return;
		} else if( field.type === 'checkbox' ) {
			this.find('input[type=checkbox]').prop( 'checked', !!val );
		}
		this.val( val );
	}
};

Runner.Dialog.controlDate = {
	create: function( fieldId, field, idx ) {
		var $control = $("<span></span>")
			.attr( 'id', fieldId + '_container' )
			.addClass( 'form-inline' )
			.addClass( 'bs-date-control' );
		var currentYear = (new Date()).getFullYear();
		var $yearDropdown = $('<select></select>')
			.attr( 'id', fieldId + '_year' )
			.attr( 'data-year', '' )
			.addClass('form-control')
			.append( $('<option></option>') )
			.after( ' ' );
		for( var i = currentYear - 100; i<= currentYear + 10; ++i ) {
			$yearDropdown.append( $('<option></option>')
				.attr('value', i )
				.text( i )
			);
		}
		var $monthDropdown = $('<select></select>')
			.attr( 'id', fieldId + '_month' )
			.attr( 'data-month', '' )
			.addClass('form-control')
			.append( $('<option></option>') )
			.after( ' ' );
		for( var i = 0; i < 12; ++i ) {
			$monthDropdown.append( $('<option></option>')
				.attr('value', i )
				.text( Runner.monthNames[ i ] )
			);
		}
		var $dayDropdown = $('<select></select>')
			.attr( 'id', fieldId )	// set cursor here
			.attr( 'data-day', '' )
			.addClass('form-control')
			.append( $('<option></option>') )
			.after( ' ' );
		for( var i = 0; i < 31; ++i ) {
			$dayDropdown.append( $('<option></option>')
				.attr('value', i + 1 )
				.text( i + 1 )
			);
		}
		var dateFormat = parseInt( Runner.pages.PageSettings.getGlobalData("locale").dateFormat, 10 );
		var parts = dateFormat == 0 
			? [ $monthDropdown, $dayDropdown, $yearDropdown ]
			: ( dateFormat == 1
				? [ $dayDropdown, $monthDropdown, $yearDropdown ]
				: [ $yearDropdown, $monthDropdown, $dayDropdown ] );
		parts.forEach( function ( $p ) {
			$control.append( $p ).append( ' ' );
		});

		//	create datepicker
		if( field.datepicker ) {
			Runner.Dialog.createDatePicker( $control, field, fieldId );
		}
		return $control;

	},
	getValue: function() {
		return this.getStringValue();
	},
	getStringValue: function() {
		var field = this.data('field');
		var date = this.getDateValue();
		if( date === null ) {
			return '';
		}
		return Runner.formatDate( date );
	},
	getDateValue: function() {
		var yearString = this.find( '[data-year]').val();
		var monthString = this.find( '[data-month]').val();
		var dayString = this.find( '[data-day]').val();
		if( yearString == '' || dayString == '' || monthString == '' ) {
			return null;
		}
		return new Date( parseInt( yearString, 10 ), parseInt( monthString, 10 ), parseInt( dayString, 10 ) );
	},
	setValue: function( val ) {
		var date = val;
		if( typeof val === "string" ) {
			date = Runner.parseDate( val );
		}
		this.find( '[data-year]').val( date && date.getFullYear() );
		this.find( '[data-month]').val( date && date.getMonth() );
		this.find( '[data-day]').val( date && date.getDate() );
	}
};

Runner.Dialog.controlDateText = {
	create: function( fieldId, field, idx ) {
		var $control = $("<span></span>")
			.attr( 'id', fieldId + '_container' )
			.addClass( 'form-inline' )
			.addClass( 'bs-date-control' );
		var dateInput = $( '<input>' )
			.attr( 'type', 'text' )
			.attr( 'id', fieldId )
			.attr( 'data-date', '' )
			.addClass( 'form-control' );

		$control.append(dateInput);
		//	create datepicker
		if( field.datepicker ) {
			Runner.Dialog.createDatePicker( $control, field, fieldId, true );
		}
		return $control;

	},
	getValue: function() {
		return this.getStringValue();
	},
	getStringValue: function() {
		var field = this.data('field');
		var date = this.getDateValue();
		if( date === null ) {
			return '';
		}
		return Runner.formatDate( date );
	},
	getDateValue: function() {
		return Runner.parseLocalDate( this.find( '[data-date]' ).val() )
	},
	
	setValue: function( val ) {
		var date = val;
		if( typeof val === "string" ) {
			date = Runner.parseDate( val );
		}
		this.find( '[data-date]' ).val( date ? Runner.formatLocalDate( date ) : '' );
	}
};

Runner.Dialog.controlTime = {
	create: function( fieldId, field, idx ) {
		var $control = $( '<input>' )
			.attr( 'type', 'text' )
			.attr( 'id', fieldId )
			.attr( 'data-time', '' )
			.addClass( 'form-control' );
		
		return $control;

	},
	getValue: function() {
		return this.getStringValue();
	},
	getStringValue: function() {
		return Runner.normalizeLocalTime( this.val() );
	},
	getDateValue: function() {
		return this.getStringValue();
	},
	
	setValue: function( val ) {
		var field = this.data('field');
		this.val( Runner.formatLocalTime( val, field.omitSeconds ));
	}
};

Runner.Dialog.controlDateTime = {
	create: Runner.Dialog.controlDateText.create,
	getValue: function() {
		return this.getStringValue();
	},
	getStringValue: function() {
		var field = this.data('field');
		var date = this.getDateValue();
		if( date === null ) {
			return '';
		}
		return Runner.formatDate( date );
	},
	getDateValue: function() {
		return Runner.parseLocalDateTime( this.find( '[data-date]' ).val() )
	},
	
	setValue: function( val ) {
		var date = val;
		if( typeof val === "string" ) {
			date = Runner.parseDate( val );
		}
		this.find( '[data-date]' ).val( Runner.formatLocalDateTime( date ) );
	}
};

Runner.Dialog.createDatePicker = function( $control, field, fieldId, isText) {
	var $valueCtl = $("<input>")
		.attr('type', 'hidden');
	var $dpButton;
	if (isText) {
		$dpButton = $('<span class="btn btn-default r-calendar" aria-hidden="true"><span class="glyphicon glyphicon-calendar"></span></span>');
	} else {
		$dpButton = $('<button class="btn btn-default r-calendar" aria-hidden="true"><span class="glyphicon glyphicon-calendar"></span></button>');
	}

	$dpButton.css('position', 'relative' )	
		.attr( 'id', fieldId + '_calendar' );
	$control.data( 'valueCtl', $valueCtl )
	$control.append( $valueCtl );

	$control.datetimepicker( {
		showTodayButton: true,
		showClose: true,
		format: Runner.getDateFormatString() + ( field.type == 'datetime' ? ' H:mm:ss' : '' ),
		showTime: field.type == 'datetime',
		widgetPositioning: {
			horizontal: Runner.isDirRTL() ? 'left' : 'right'
		},
		tooltips: Runner.datePickerTooltips(),			
	} );
	$control.append( $dpButton );
	Runner.setDatePickerLocale();

	var datePicker = $control.data("DateTimePicker");
	$dpButton.on('click', function(e) {
		$valueCtl.val( $control.getStringValue() );
		datePicker.toggle();
	})
	.on("blur", function() {
		datePicker.hide();
	})
	.on("mousedown", function(e) {
		if (isText) {
			e.preventDefault();
		}
	});

	$control.on("dp.change", function(e) {
		$control.setValue(e.date.toDate());
		//datePicker.hide();
	})
	.on("dp.show", function(e) {	
		Runner.util.absToFixed( $(".bootstrap-datetimepicker-widget", this) );
	})
}

Runner.Dialog.markInvalid = function( $control ) {
	var $container = $control.closest( '.r-control' );
	$container.addClass( 'has-error');
	if( $container.find( '.help-block' ).length == 0 ) {
		$container.append( '<strong class="help-block">' + Runner.lang.constants.INLINE_FIELD_REQUIRED + '</strong>' );
	}
}

Runner.Dialog.markValid = function( $control ) {
	var $container = $control.closest( '.r-control' );
	$container.removeClass( 'has-error');
	$container.find( ".help-block" ).remove();
}
 
// create namespace
Runner.namespace('Runner.bricks');

/**
 * General class for manage bricks and containers
 * @class Runner.bricks.BrickManager
 */
Runner.bricks.BrickManager = Runner.extend( Runner.emptyFn, {
	/**
	 * jQuery element
	 * Block, container or element wich contain brick
	 * @type {object}
	 */
	elem: null,
	/**
	 * Layout version
	 * 2 - regular
	 * 3 - bootstrap
	 * @type {int}
	 */
	layoutVersion: 2,
	/**
	 * Name of container, brick or block
	 * Without contain 'runner-c-', 'runner-b-' or 'runner-'
	 * @type {string}
	 */
	name: '',
	/**
	 * Page container
	 * Dom element
	 * @type {string}
	 */
	pageCont: null,
	/**
	 * First part of all Runner-related classes
	 * @type {string}
	 */
	baseName: '',
	/**
	 * First part of base class
	 * Without contain name
	 * @type {string}
	 */
	baseClass: '',
	/**
	 * Class Name for hide container, brick or block
	 * @type {string}
	 */
	hiddenClass: '',
		
		
	constructor: function(cfg) {
		Runner.apply(this, cfg);
		this.baseName = "rnr-";
	},
	
	/**
	 * Get element name of brick, container or block 
	 * without base part of class name
	 * @param object element table for container
	 */
	getElemName: function(elem) {
		if (this.name) {
			return;
		}
		if (typeof elem == 'undefined') {
			elem = this.elem;
		}
		// always elem must has attribute "class"
		// this "if" only for against a risk
		if ( !elem.attr('class') ) {
			return;
		}
		
		var cls = elem.attr('class').split(' '),
			i, pos;
		for (i = 0; i < cls.length; i++) {
			if (cls[i]) {
				pos = cls[i].indexOf(this.baseClass);
				if (pos > -1) {
					this.name = cls[i].substr(pos + this.baseClass.length).trim();
					break;
				}
			}	
		}
	},
	
	/**
	 * Hide element 
	 * Add hidden class
	 */
	hide: function() {
		if ( this.layoutVersion != 3 ) {
			if ( !this.elem.hasClass( this.hiddenClass ) ) {
				this.elem.addClass( this.hiddenClass );
			}
		} else {
				this.elem.attr( "data-hidden", '' );
		}
	},
	
	/**
	 * Show element 
	 * Remove hidden class
	 */
	show: function() {
		this.elem.removeClass( this.hiddenClass );
		if ( this.layoutVersion == 3 ) {
				this.elem.removeAttr( "data-hidden" );
		}
	},
	
	/**
	 * Show/hide element basing on the toggler value
	 * @param {boolean} toggler
	 */
	toggle: function( toggler ) {
		toggler ? this.show() : this.hide();
	},	
	
	/**
	 * Check on visible element
	 * @return {boolean}
	 */
	visible: function() {
		//if class of this.elem not contain 'runner-b-wrapper' that
		if ( this.layoutVersion == 3 ) {
			return !this.elem.is( "[data-hidden]" );
		}
		
		return !this.elem.hasClass( this.hiddenClass );
	}
});

/**
 * @class Runner.bricks.Brick
 */
Runner.bricks.Brick = Runner.extend( Runner.bricks.BrickManager, {
	
	contObj: false,
	
	contentElem: false,

	name: "",
	
	constructor: function(cfg) {
		Runner.bricks.Brick.superclass.constructor.call(this, cfg);
		
		this.baseClass = this.baseName + 'b-';
		this.hiddenClass = this.baseName + 'hiddenbrick';
		this.getElemName();
		this.getContainer();
		this.getContentElem();
		if ( this.elem.hasClass(this.baseClass + "wrapper") ) {
			this.hiddenClass = this.baseName + "hiddencontainer";
		}
	},
	
	/**
	 * Get all or a specific brick from a container 
	 * @param {String} name
	 * @param {DOM elem} pageCont
	 * @param {Boolean} all
	 * @param {number} version
	 * @param {number} pageId
	 * @method
	 */
	getBrickObjs: function(name, pageCont, all, version, pageId) {
		var query,	brickObjs = [],
			self = this;
			
		version = version || 2;
		
		if ( version == 2 ) {
			query = all ? '[class*=rnr-b-]' : '.rnr-b-' + name;
			$(query, pageCont).each( function() {
				var $elem = $(this),
					foundPageClass = false,
					foundPageCont = false;
				
				$elem.parents().each( function( n, parentElem ) {
					if ( Runner.bricks.Brick.prototype.hasPageClass( parentElem ) ) {
						if ( foundPageClass ) {
							//	
							return false;
						}
						foundPageClass = true;
					}
					if ( $(pageCont).is( parentElem ) ) {
						foundPageCont = true;
						return false;
					}
				});
				
				if ( !foundPageCont ) {
					//	page container was not found among brick parents
					return;
				}
				
				brickObjs.push( new Runner.bricks.Brick({
					name: name,
					elem: $elem,
					pageCont: pageCont,
					layoutVersion: version
				}));
			});
		} else {
			//	bootstrap layout
			query = '[data-pageid=' + pageId + ']' + (all ? '[data-brick]' : '[data-brick=' + name + ']');
			$(query, pageCont ).each( function() {
				brickObjs.push( new Runner.bricks.Brick({
					name: name,
					elem: $(this),
					pageCont: pageCont,
					layoutVersion: version
				}));
			});
		}
		
		
		return brickObjs;
	},

	/**
	 * Check if the element has a class starting from "page-"
	 * @param {DOM element | jQuery object}
	 */
	hasPageClass: function( container ) {
		var $container = container instanceof jQuery ? container : $(container),
			classes = $container.attr("class"),
			classRegExp = /^\s*page-|\s*page-/;

		return classRegExp.test( classes ) ; 		
	},

	/**
	 * Get the element's class starting from "page-"
	 * @param {DOM element | jQuery object}
	 * @return {String}
	 */
	getPageClass: function( container ) {
		var $container = container instanceof jQuery ? container : $(container),
			classes = $container.attr("class"),
			classRegExp = /^\s*page-[^\s]+|\s*page-[^\s]+/;

		return classRegExp.exec( classes )[0].trim();	
	},
	
	/**
	 * Get content HTML element 
	 * If there isn't element with class runner-brickcontents in this brick
	 * Then content element it is this brick element
	 * @method
	 */
	getContentElem: function() {
		var brickContents = $('.' + this.baseName + 'brickcontents', this.elem);
		
		this.contentElem = brickContents.length ? brickContents : this.elem;
	},
	
	/**
	 * Assign the container HTML element 
	 * to the brik's 'contObj' prop 
	 * @method
	 */
	getContainer: function() {
		var contElem, 
			_self = this;
		if ( this.layoutVersion == 3 ) {
			contElem = this.elem.closest('[data-pageid=' + this.elem.data('pageid') + '][data-container]');
		} else {
		
			// try finding the closest container: a div element with a class 'runner-s-...' 
			// not containing the class "not-container". 
			this.elem.parents('div:not(".not-container")').each(function() {	
				var classes = [], i,
					$elem = $(this);
					
				if ( $elem.attr('class') !== undefined ) {
				   classes = $elem.attr('class').split(' ');
				}

				for (i = 0; i < classes.length; i++) {
					if ( classes[i].indexOf( _self.baseName + "s-" ) === 0 ) {
						contElem = $elem;
						return false;
					}
				}		
			});
		}
		
		if (contElem && contElem.length) {
			this.contObj = new Runner.bricks.Container({
				elem: contElem,
				pageCont: this.pageCont,
				layoutVersion: this.layoutVersion
			});
		}
	},
	
	/**
	 * Hide brick
	 * If this brick the single in the container
	 * Then the container must be hidden too!
	 */
	hide: function() {
		Runner.bricks.Brick.superclass.hide.call(this);
		if ( this.contObj ) {
			this.contObj.updateVisibility();
		}
	},
	
	/**
	 * Show brick
	 * If this brick position in the hidden container
	 * Than the container must be shown too!
	 */
	show: function() {
		Runner.bricks.Brick.superclass.show.call(this);
		if ( this.contObj ) {
			this.contObj.show();
		}
	},
	
	/**
	 * Hide/Show brick depending on newHTML
	 * @param {mixed} new HTML for replace
	 * @param {boolean} true, if new brick has hidden class
	 */
	prepareToReplaceWith: function( newHTML, isHidden ) {
		this.toggle( !isHidden && newHTML );
	},
	
	/**
	 * Replace brick elem
	 * @param {mixed} new html
	 */
	replaceWith: function( newHTML ) {
		this.prepareToReplaceWith( newHTML );
		this.elem.replaceWith( newHTML );
		
	},
	
	/**
	 * Replace html for brick
	 * @param {mixed} new html
	 */
	replaceHTMLWith: function( newHTML ) {
		this.prepareToReplaceWith( newHTML.html, newHTML.isHidden );
		this.elem.empty().html(newHTML.html);
	},
	
	/**
	 * Replace content elem of brick
	 * @param {mixed} new brick content
	 */
	replaceContentWith: function( newContent ) {
		this.prepareToReplaceWith( newContent );
		this.contentElem.replaceWith( newContent );
	},
	
	/**
	 * Replace html for brick content
	 * @param {mixed} new contents
	 */
	replaceContentHTMLWith: function( newHTML ) {
		this.prepareToReplaceWith( newHTML );
		this.contentElem.empty().html( newHTML );
	},
	
	/**
	 * Move brick to another container
	 * @param {mixed} Container object to move brick to
	 */
	moveTo: function( newContainer ) {
		var oldContainer = this.contObj;
		//	create a placeholder
		this.elem.after( this.createPlaceholder() );
		//	move the brick
		newContainer.addBrick(this);
		this.contObj = newContainer;
		newContainer.updateVisibility();
		oldContainer.updateVisibility();
	},
	
	/**
	* Construct brick placeholder
	*/
	createPlaceholder: function() {
		var tagName = this.elem.get(0).tagName;
		return $('<' + tagName + ' class="rnr-ph-' + this.name + '"></' + tagName + '>');
	},
	
	/**
	* Check if the brick is visible element
	* @return {boolean}
	*/
	visible: function() {
		var isVisible = Runner.bricks.Brick.superclass.visible.call(this);
		
		if (this.contObj) {
			isVisible = isVisible && this.contObj.visible();
		}
		
		return isVisible;
	}
});

/**
 * @class Runner.bricks.Container
 */
Runner.bricks.Container = Runner.extend( Runner.bricks.BrickManager, {
	
	blockObj: false,
	
	constructor: function(cfg) {
		Runner.bricks.Container.superclass.constructor.call(this, cfg);	
		this.baseClass = this.baseName + 'c-';
		this.hiddenClass = this.baseName + 'hiddencontainer';
		this.getElemName();
		this.getBlock();
	},
	
	getElemName: function() {
		Runner.bricks.Container.superclass.getElemName.call(this, $('.' + this.baseName + 'c:first',this.elem));
	},
	
	/**
	 * Hide container if it contains no visible bricks and show otherwise
	 */
	updateVisibility: function() {
		var bricks = this.getBricks(),
			visible = false, i;
			
		for (i = 0; i < bricks.length; i++) {
			if ( bricks[i].visible() ) {
				visible = true;
				break;
			}
		}
		
		this.toggle( visible );
	},
	
	/**
	 * Hide container
	 * If this container the single in the block
	 * Then the block must be hidden too!
	 */
	hide: function() {
		Runner.bricks.Container.superclass.hide.call(this);

		//	hide wrapper if exists
		if ( this.layoutVersion != 3 ) {
			var $bWrapperParent = this.getBWrapperParent();
			
			if ( $bWrapperParent.length ) {
				new Runner.bricks.Brick({
					elem: $bWrapperParent,
					layoutVersion: this.layoutVersion,
					pageCont: this.pageCont
				}).hide();
				return;
			}
		} else if ( this.elem.parent('[data-container]').length ) {
			new Runner.bricks.Container({
				elem: this.elem.parent(),
				pageCont: this.pageCont,
				layoutVersion: this.layoutVersion
			}).updateVisibility();
		}
		
		if ( this.blockObj ) {
			this.blockObj.updateVisibility();
		}
	},
	
	/**
	 * Find the container's parent having 'runner-b-wrapper' class
	 * @return {jQuery object} 
	 */
	getBWrapperParent: function() {
		if ( this.pageCont ) {
			return $( this.pageCont ).find( $(this.elem).closest('.' + this.baseName + 'b-wrapper') );
		}

		return $(this.elem).closest('.' + this.baseName + 'b-wrapper');
	},
	
	/**
	 * Show container
	 * If this container position in the hidden block
	 * Than the block must be shown too!
	 */
	show: function() {
		Runner.bricks.Container.superclass.show.call(this);
		//	show wrapper if exists
		var $bWrapperParent = this.getBWrapperParent();
		
		if ( this.layoutVersion != 3 ) {
			if ( $bWrapperParent.length ) {
				new Runner.bricks.Brick({
					elem: $bWrapperParent,
					layoutVersion: this.layoutVersion,
					pageCont: this.pageCont
				}).show();
				return;
			}
		} else if ( this.elem.parent('[data-container]').length ) {
			new Runner.bricks.Container({
				elem: this.elem.parent(),
				pageCont: this.pageCont,
				layoutVersion: this.layoutVersion
			}).show();
		}
		
		
		if ( this.blockObj ) {
			this.blockObj.show();
		}
	},
	
	/**
	 * Get array of bricks for container
	 * @method
	 * @return {array}  
	 */
	getBricks: function() {
		var bricksArr = [],
			_self = this;
			
		if ( this.layoutVersion == 3 ) {
			$( '[data-pageid=' + this.elem.data('pageid') + '][data-brick]', this.elem ).each( function() {
				bricksArr.push( new Runner.bricks.Brick({
					elem: $(this),
					layoutVersion: _self.layoutVersion
				}) );
			})

		} else {
			$('[class*="' + this.baseName + 'b-"]', this.elem).each( function() {
				var $elem = $(this),
					foundContainer = false;
				
				$elem.parents().each( function( n, parentElem ) {
					if ( Runner.bricks.Brick.prototype.hasPageClass( parentElem ) || $(parentElem).hasClass("rnr-dbelemtable") ) {
						return false;
					}
					
					if ( _self.elem.is( parentElem ) ) {
						foundContainer = true;
						return false;
					}
				});
				
				if ( !foundContainer ) {
					//	the brick is inside another page
					return;
				}
				
				bricksArr.push( new Runner.bricks.Brick({
					elem: $(this),
					layoutVersion: _self.layoutVersion
				}) );
			});
		}
			
		return bricksArr;
	},
	
	/**
	 * Get block HTML element 
	 * @method
	 * @return {object}  
	 */
	getBlock: function( elem ) {
		var blockSelector, blockElem;
		
		if ( this.layoutVersion == 2 ) {
			blockSelector = '.rnr-top, .rnr-left, .rnr-center, .rnr-right, .rnr-bottom';
		} else if ( this.layoutVersion == 3 ) {
			blockSelector = '.bs-topmiddle, .bs-left, .bs-center, .bs-right, .sidebar';
		} else {
			blockSelector = 'td';
		}
	
		if (typeof elem == 'undefined') {
			blockElem = this.elem.closest( blockSelector );
		} else {
			blockElem = elem.parent().closest( blockSelector );
		}
		
		if ( blockElem.hasClass(this.baseName + 'wrapper') ) {
			this.getBlock( blockElem );
			return;
		}
		if (blockElem.length) {
			this.blockObj = new Runner.bricks.Block({
				elem: blockElem,
				layoutVersion: this.layoutVersion
			});
		}
	},
	/**
	 * Clone container object and HTML structure
	 * @method
	 * @return {object}  
	 */
	cloneEmpty: function() {
		var bricks = this.getBricks(),
			clone = this.elem,
			cloner;
		
		cloner = function(_clone) {
			var cloned, i,
				$elem = $(this),
				matchedBrick = null;
			
			for (i = bricks.length - 1; i >= 0; --i) {
				if ( bricks[i].elem.is(this) ) {
					matchedBrick = bricks[i];
					break;
				}
			}
			if ( $elem.hasClass("rnr-hfiller") || $elem.hasClass("filler") ) {
				return;
			}
			if (matchedBrick) {
			//	insert brick placeholder instead of a brick
				if (_clone) {
					$(_clone).append( matchedBrick.createPlaceholder() );
				}
				return;
			}
			
			cloned = $elem.get(0).cloneNode(false);
			if (_clone) {
				$(_clone).append( cloned );
			}
			$elem.children().each( function() {
				cloner.call(this, cloned);
			});
			
			return cloned;
		}
		
		return new Runner.bricks.Container({
				elem: $( cloner.call( this.elem ) ),
				pageCont: this.pageCont,
				layoutVersion: this.layoutVersion
			});
	},

	/**
	 * Append container HTML code to element
	 * @method
	 */
	appendTo: function( elem ) {
		$(elem).append( this.elem );
	},

	/**
	 * add brick to container
	 * @method
	 * @param {Brick} Brick object
	 */
	addBrick: function( brick ) { 
		var placeholder = this.findPlaceholder( brick.name );
		
		if ( placeholder.length ) {
			placeholder.replaceWith( brick.elem );
		} else {
			this.elem.children().first().append( brick.elem );
		}
	},
	
	/**
	 * Find brick placeholder inside a container
	 */
	findPlaceholder: function( name ) {
		return $(".rnr-ph-" + name, this.elem);
	},
	
	/**
	 * Tests if container objects are equal
	 * @method
	 * @param {Container} Container object
	 * @return {bool} 
	 */
	is: function( contObj ) {
		return this.elem.is( contObj.elem );
	}
});

/**
 * @class Runner.bricks.Block
 */
Runner.bricks.Block = Runner.extend( Runner.bricks.BrickManager, {

	constructor: function(cfg) {
		Runner.bricks.Block.superclass.constructor.call(this, cfg);	
		this.baseClass = this.baseName;
		this.hiddenClass = this.baseName + 'hiddenblock';
		this.getElemName();
	},
	
	/**
	 * Get array of containers for block
	 * @method
	 * @return {array}  
	 */
	getContainers: function() {
		var contsArr = [], 
			selector = '[class*="' + this.baseName + 's-"]',
			block = this;
		
		if ( this.layoutVersion == 3 ) { 
			selector = '[data-container]';
		}
			
		$(selector, this.elem).each( function() {
			contsArr.push( new Runner.bricks.Container({
				elem: $(this),
				layoutVersion: block.layoutVersion
			}) );
		});
		
		return contsArr;
	},
	
	/**
	 * Hide block if it contains no visible bricks and show otherwise
	 */
	updateVisibility: function() {
		var conts = this.getContainers(),
			visible = false;
			
		for (var i = 0; i < conts.length; i++) {
			if ( conts[i].visible() ) {
				visible = true;
				break;
			}
		}
		this.toggle( visible );
	}
});
// create namespace
Runner.namespace('Runner.menu');

/**
 * @class Runner.menu.Manager
 * Abstract base class that provides common menu functionality. 
 */
Runner.menu.Manager = Runner.extend( Runner.emptyFn, {
	/**
	 * Is use RTL on page
	 * @param {boolean}
	 */	
	isDirRTL: false,
	
	init: function() {
		var dir = $('html').attr('dir') || '';
		if ( dir.toLowerCase() == 'rtl' ) {
			this.isDirRTL = true;
		}
	},
	
	/**
	 * Click handler function for menu items
	 * @param {object} event
	 */	
	itemClickHandler: function(event) {
		var target = event.target,
			link = $('a:first', this)[0];
		
		if (typeof link === "undefined") {
			return false;
		}
		
		if ($(this).hasClass('Group') && target.nodeName!='IMG' && !link.href) {
			$('.groupImg:first', this).trigger("click");
			return;
		}
		
		if (target.nodeName == "A" ) {
			if (target.rel == 'external') {
				$(target).attr('target', "_blank");
				//return !window.open(target.href);
			}
			return;
		}
		
		Runner.Event.prototype.stopEvent(event);
		if (link.href) {
			if (link.rel == 'external') {
				return !window.open(link.href);
			}
			window.location = link.href;
		}
	}
});

/**
 * @class Runner.menu.QuickJump
 * Class that provides quick jump menu functionality. 
 */
Runner.menu.QuickJump = Runner.extend( Runner.emptyFn, {
	/**
	 * Current selected item in dropdown
	 * @type Integer
	 */
	selectCurrent: -1,
	/**
	 * Init quickjump menu
	 * Bind events handlers on dropdown
	 */
	init: function() {
		var menuObj = this;
		$(".runner-quickjump:first").prop("initialized","true");
		$(".runner-quickjump")
			.on('focus', function() {
				menuObj.selectCurrent = this.selectedIndex; 
			})
			.on('change', function() {
				if (this.options[this.selectedIndex].value) {
					if ($(this.options[this.selectedIndex]).attr('link') == 'External') {
						window.open(this.options[this.selectedIndex].value);
					} else {
						window.location.href = this.options[this.selectedIndex].value;
					}
				} else {
					this.selectedIndex = menuObj.selectCurrent;
				}
			});	
	}	
});

/**
 * @class Runner.menu.Horizontal
 * Class that provides horizontal menu functionality. 
 */
Runner.menu.Horizontal = Runner.extend( Runner.menu.Manager, {
	/**
	 * Max submenu width
	 * @type Integer
	 */
	maxSubMenuWidth: 0, 
	/**
	 * Max group (ul) width
	 * @type Integer
	 */
	maxGroupWidth: 0,
	/**
	 * Max item (li) width
	 * @type Integer
	 */
	maxItemWidth: 0,
	/** 
	 * Absolute posution for top item
	 * @type object
	 */
	topItemAbsPos: null,
	/**
	 * Was item hovered or not
	 * @type Boolean
	 */
	itemHovered: false,
	/**
	 * Sub menu for hovered item
	 * @type array
	 */
	subMenu: [],
	
	init: function() {
		Runner.menu.Horizontal.superclass.init.call(this);
		this.bindHoverOnItems();
		this.bindClickOnItems();
		this.setRaquoToTopItems();
		$(".runner-hmenu:first").prop("initialized","true");
	},
	
	/**
	 * Find sub menu for item
	 * @param {obj} elem
	 */
	findSubMenu: function(elem) {
		this.subMenu = $('ul:first:has(li)', elem);
	},
	
	/**
	 * Manage add/remove class Active
	 * @param {obj} elem
	 * @param {boolean} toggle
	 */
	manageActiveClass: function(elem, toggle) {
		if ($(elem).hasClass('Separator')) {
			return;
		}
		if ($('table:first', elem).length) { 
			//elem is a top item 
			$('.runner-menutab', elem).toggleClass('active', toggle);
			return;	
		}
		$(elem).toggleClass('active', toggle);
	},
	
	/**
	 * Manage add/remove class "expanded"
	 * @param {obj} elem
	 * @param {boolean} toggle
	 */		
	manageExpandedClass: function(elem, toggle) {
		if ($('table:first', elem).length) {
			$('.runner-menutab', elem).toggleClass('expanded', toggle);
			return;
		}
		$(elem).toggleClass('expanded', toggle);
	},
	
	/**
	 * Bind hover event on every menu item
	 * For hovered items add class 'active'
	 * For items losted hover event remove class 'active'
	 * Add the "expanded" class on hover for items that have any sub-items and
	 * remove the "expanded" class for these items when the mouse pointer leaves them
	 */
	bindHoverOnItems: function() {
		var menuObj = this;
		
		$('.runner-hmenu td, .runner-hmenu ul li').hover(
			function() {
				var isTopItem, $raquo;
				
				menuObj.manageActiveClass(this, true);		
				menuObj.findSubMenu(this);
				if (!menuObj.subMenu.length) {
					return;
				}
				menuObj.manageExpandedClass(this, true);	
				menuObj.maxGroupWidth = 0;
				
				isTopItem = $(this).is("td");
				
				if (isTopItem) {
					menuObj.topItemAbsPos = $(this).position();
					menuObj.maxItemWidth = menuObj.topItemAbsPos.width;
				}
							
				$('li[parent=' + (menuObj.subMenu.attr('id')) + '] a', menuObj.subMenu).each(function() {
					if (menuObj.maxGroupWidth < this.offsetWidth) {
						menuObj.maxGroupWidth = this.offsetWidth
					}
				});
				
				if (isTopItem && menuObj.maxGroupWidth < menuObj.maxItemWidth) {
					menuObj.maxSubMenuWidth = menuObj.maxItemWidth;
				} else {
					menuObj.maxSubMenuWidth = menuObj.maxGroupWidth;
				}
				
				menuObj.maxSubMenuWidth += 10;
				menuObj.subMenu.css('width', menuObj.maxSubMenuWidth + 'px');
				
				if (isTopItem) { //td is hovered
					if (menuObj.isDirRTL) {
						menuObj.subMenu.css('left', menuObj.topItemAbsPos.left + ($(this).outerWidth() - menuObj.subMenu[0].offsetWidth) + 'px');
					} else {
						menuObj.subMenu.css('left', menuObj.topItemAbsPos.left + 'px');
					}
					menuObj.subMenu.css('top', menuObj.topItemAbsPos.top + $(this).outerHeight() - 1 + 'px');
				} else { //li is hovered
					$raquo = $('.raquo', this);
					leftPosition = $raquo.position().left + $raquo.get(0).offsetWidth - $raquo.position().left + 10;
					if ( $(this).outerWidth() < leftPosition ) {
						leftPosition = $(this).outerWidth() - 1;
					}
					menuObj.subMenu.css({
						'top': this.offsetTop + 'px',
						'left': leftPosition + 'px'
					});
				}
				
				$('li[parent=' + menuObj.subMenu.attr('id') + ']', menuObj.subMenu).each( function() {
					$(this).css('width', menuObj.maxSubMenuWidth + 'px');
					$('ul:first', this).css( (menuObj.isDirRTL ? 'right' : 'left'), menuObj.maxSubMenuWidth + 'px' );
				});		
			},
			function() {
				menuObj.findSubMenu(this);
				if ( menuObj.subMenu.length ) { 
					menuObj.manageExpandedClass(this, false);	
				}
				menuObj.manageActiveClass(this, false);
			}
		);
	},	
	
	/**
	 * Bind the click event's handler with the menu items
	 */
	bindClickOnItems: function() {
		$('.runner-hmenu td, .runner-hmenu ul li').on("click", this.itemClickHandler);
	},
	
	/**
	 * Set raquo for top items which has submenus
	 */	
	setRaquoToTopItems: function() {	
		var curTopItem;
		
		$('a:first', '.runner-hmenu td:has(ul:has(li)), .runner-hmenu li:has(ul:has(li))').each(function() {
			if (!$('b',this).length) {
				$(this).after('<b class="raquo">&nbsp;&raquo;</b>');
			}
		});
		//if submenus of top item has current item, then add current item title to top item after raquo 
		curTopItem = $('.runner-hmenu td:has(ul:has(li.current))');
		$('b.raquo:first', curTopItem).append("&nbsp;<b class='subcur'>" + $(".runner-hmenu .curlink").attr('itemtitle') + "</b>");
		$('tr.runner-menutab', curTopItem).addClass('current');
		$('.runner-hmenu ul li ul li ul').css('top','0px');
	}
});

/**
 * @class Runner.menu.SimpleVmenu
 * Class that provides vertical simple menu functionality. 
 */
Runner.menu.SimpleVmenu = Runner.extend( Runner.menu.Manager,{
	/**
	 * Sub menu for hovered item
	 * @type array
	 */
	subMenu: [],
	
	init: function() {
		Runner.menu.SimpleVmenu.superclass.init.call(this);
		this.bindHoverOnItems();
		this.bindClickOnItems();
		this.setRaquoToTopItems();	
		$(".runner-vmenu.simple:first").prop("initialized","true");
	},
	
	/**
	 * Find sub menu for item
	 * @param {obj} elem
	 */
	findSubMenu: function(elem) {
		this.subMenu = $('ul:first:has(li)', elem);
	},
	
	/**
	 * Manage add/remove class Active
	 * @param {obj} elem
	 * @param {boolean} toggle
	 */
	manageActiveClass: function(elem, toggle) {
		if ( !$(elem).hasClass('Separator') ) {
			$(elem).toggleClass('active', toggle);
		}
	},
	
	/**
	 * Manage add/remove class "expanded"
	 * @param {obj} elem
	 * @param {boolean} toggle
	 */	
	manageExpandedClass: function(elem, toggle) {
		$(elem).toggleClass('expanded', toggle);
	},
	
	/**
	 * Bind hover event on every menu item
	 * For hovered items add class 'active'
	 * For items losted hover event remove class 'active'
	 * Add the "expanded" class on hover for items that have any sub-items and
	 * remove the "expanded" class for these items when the mouse pointer leaves them
	 */
	bindHoverOnItems: function() {
		var menuObj = this;
		
		$('.runner-vmenu.simple, .runner-vmenu.simple ul li').hover(
			//hover: handlerIn
			function() {
				var	elParentUlPosition, elParentUlWidth, 
					offsetHSide, offsetValue, isTopItem;
				
				menuObj.manageActiveClass(this, true);	
				menuObj.findSubMenu(this);
				if (!menuObj.subMenu.length) {
					return;
				}
				menuObj.manageExpandedClass(this, true);
				
				isTopItem = $(this).hasClass("runner-vmenu"); //is true if the hovered element is tr	
				elParentUlWidth = isTopItem? $(this).width() : $(this).closest("ul").width();			
				elParentUlPosition = $(this).position(); //the element hasn't a positioned ancestor
				offsetHSide = menuObj.isDirRTL ? 'right': 'left';
				
				if (!isTopItem) {
					//li element is hovered
					offsetValue = elParentUlWidth;
				} else {
					//tr element is hovered			
					offsetValue = menuObj.isDirRTL ? $(window).width() - elParentUlPosition.left : elParentUlWidth + elParentUlPosition.left ;
				}
				
				menuObj.subMenu.css(offsetHSide , offsetValue + 'px');
				menuObj.subMenu.css('top', (elParentUlPosition.top) + 'px');
			},
			//hover: handlerOut
			function() {
				menuObj.findSubMenu(this);
				if (menuObj.subMenu.length) {
					menuObj.manageExpandedClass(this, false);
				}
				menuObj.manageActiveClass(this, false);
			}
		);
	},
	
	/**
	 * Bind the click event's handler with the menu items
	 */
	bindClickOnItems: function() {
		$('.runner-vmenu.simple, .runner-vmenu.simple ul li').on("click", this.itemClickHandler);
	},
	
	/**
	 * Set raquo for top items which has submenus
	 */
	setRaquoToTopItems: function() {
		var curTopItem;
		
		$('.runner-vmenu.simple:has(ul:has(li)), .runner-vmenu.simple li:has(ul:has(li))').find('a:first').each(function() {
			if (!$('b',this).length) {
				$(this).after('<b class="raquo">&nbsp;&raquo;</b>');
			}
		});
		//if submenus of top item has current item, then add current item title to top item after raquo 
		curTopItem = $('.runner-vmenu.simple:has(ul:has(li.current))');
		curTopItem.find("b.raquo:first").append("&nbsp;<b class='subcur'>" + $(".runner-vmenu.simple a.curlink").attr('itemtitle') + "</b>");
		curTopItem.addClass('current');
	}
});


/**
 * @class Runner.menu.TreeLikeVmenu
 * Class that provides vertical tree-like menu functionality. 
 */
Runner.menu.TreeLikeVmenu = Runner.extend( Runner.menu.Manager, {
	/**
	 * Cookie root
	 */
	cookieRoot: "",	
	/**
	 * Current item
	 */
	curItem: null,
	/**
	 * Id of current item
	 */
	curItemId: "",
	/**
	 * Level of current item
	 */
	curItemLevel: -1,
	
	init: function() {
		if ($('.runner-vmenu.tree .curlink').length) {
			this.curItem = $('.runner-vmenu.tree .curlink').closest('tr');
			this.curItemId = $(this.curItem).attr('id');
			this.curItemLevel = this.getItemLevel(this.curItem);
		}
		//hide all submenus
		$('.runner-vmenu.tree[parent]').addClass('runner-hiddenelem');
		
		//init menu
		this.bindHoverOnItems();
		this.setCurrentStyle();
		this.toggleMenuGroup();
		this.manageExpandCollapse();
		
		//open menu group saved in cookie
		this.openMenuOnLoad();
		$(".runner-vmenu.tree:first").prop("initialized","true");
	},
	
	/**
	 * Get menu item level
	 * There are 5 levels in tree like menu
	 * Top items have zero level
	 * Default value -1, level is not define
	 * @param {object} jquery item object
	 * @return {number} number of level
	 */
	getItemLevel: function(item) {
		var clsItem = $(item).attr('class'),
			pos = clsItem.indexOf("level");
		//if not found level, return zero - top item
		if (pos == -1) {
			return 0;
		}
		//cut number of level, parse it to int and return number
		return parseInt(clsItem.substr(pos + 5, 1), 10);
	},
	
	/**
	 * Bind hover event on every menu item
	 * For hovered items add class 'active'
	 * For items losted hover event remove class 'active'
	 */
	bindHoverOnItems: function() {
		$('.runner-vmenu.tree[id^=item]').hover(function() {
			if ($(this).parent().attr('class')!='Separator') {
				$(this).addClass('active');
			}
		},
		function() {
			if ($(this).parent().attr('class')!='Separator') {
				$(this).removeClass('active'); 
			}
		});
		
		$('.runner-vmenu.tree[id^=item]').on("click", this.itemClickHandler);
	},
	
	/**
	 * Set current style for top group
	 * which has current item in children
	 */
	setCurrentStyle: function() {
		$('.runner-vmenu.tree.Group[view=topitem]').each(function() {
			var group = this;
			$('.runner-vmenu.tree[topparent=' + this.id + ']').each(function() {
				if ($(this).hasClass('current')) {
					$(group).addClass('current');
				}
			});
		});
	},
	
	/**
	 * Bind click event on span to toggle menu group
	 */
	toggleMenuGroup: function() {
		var menuObj = this;
		
		$('.runner-vmenu.tree.Group span').on("click", function() {
			
			var spanItem = $(this).closest("tr"),
				spanItemId = spanItem.attr('id');
			
			if ($('.groupImg',this).attr('src') == Runner.pages.constants.PLUS_GIF) {
				//show all children to the current item
				menuObj.showGroupChildren(spanItem, spanItemId);
			} else if ($('.groupImg',this).attr('src') == Runner.pages.constants.MINUS_GIF) {
				//hide all children of closed group
				menuObj.hideGroupChildren(spanItem, spanItemId);
				
				if (spanItemId!=menuObj.curItemId && menuObj.curItemLevel > menuObj.getItemLevel(spanItem) && menuObj.hasCurrentItem(spanItem, spanItemId)) {
					spanItem.addClass('current');
				}
			}
			return false;
		});
	},
	
	/**
	 * Show all children of closed group
	 * If this group has current item,
	 * then show all groups in this group, which has current item 
	 * @param {object}
	 */
	showGroupChildren: function(item, itemId) {
		var menuObj = this;
		if (!itemId) {
			itemId = $(item).attr('id');
		}
		
		//set image minus for closed group
		$('.groupImg',item).attr('src', Runner.pages.constants.MINUS_GIF);
		
		if (itemId!=this.curItemId && $(item).hasClass('current'))
			$(item).removeClass('current');
		
		$('.runner-vmenu.tree[parent=' + itemId + ']').each(function() {
			$(this).removeClass('runner-hiddenelem');
			if ($(this).hasClass('Group') && menuObj.curItemLevel > menuObj.getItemLevel(this) && menuObj.hasCurrentItem(this)) {
				menuObj.showGroupChildren(this);
			}
		});
		
		// add to cookie opened group
		this.addToCookie(itemId);
	},
	
	/**
	 * Hide all children of group
	 * @param {object}
	 */
	hideGroupChildren: function(item, itemId) {
		var menuObj = this;
		if (!itemId) {
			itemId = $(item).attr('id');
		}
		
		//set image plus for closed group
		$('.groupImg',item).attr('src', Runner.pages.constants.PLUS_GIF);
		
		$('.runner-vmenu.tree[parent=' + itemId + ']').each(function() {
			$(this).addClass('runner-hiddenelem');
			if ($(this).hasClass('Group'))
				menuObj.hideGroupChildren(this);
		});
		
		// remove from cookie closed group
		this.removeFromCookie(itemId);
	},
	
	/**
	 * Check has item in children current item or not
	 * @param {object}
	 * @return {boolean}
	 */	
	hasCurrentItem: function(item, itemId) {
		if (!itemId) {
			itemId = $(item).attr('id');
		}
		if ($('.runner-vmenu.tree.[parent=' + itemId + ']').hasClass('current')) {			
			return true;
		}
		
		var colSubGroups = $('.runner-vmenu.tree.Group.[parent=' + itemId + ']').length;
		for (var i = 0; i < colSubGroups; i++) {
			if (this.hasCurrentItem($('.runner-vmenu.tree.Group.[parent=' + itemId + ']').get(i)))			
				return true;
		}
		return false;
	},
	
	/**
	 * Menage group menu with expand/collapse button control
	 */
	manageExpandCollapse: function() {
		
		var menuObj = this,
			expand = false;
		
		if ($('.runner-vmenu.tree.Group[view=topitem]').length) {
			$('.manage').css('display','inline');
			$('.manage a').on("click", function() {
				if (expand) {
					expand = false;
					
					$('.runner-vmenu.tree[parent]').addClass('runner-hiddenelem');
					$('.manage a').empty();
					$('img.groupImg').attr('src', Runner.pages.constants.PLUS_GIF);
					$('.manage a').append('<img src=\"' + Runner.pages.constants.PLUS_GIF + '\" border=0> &nbsp;&nbsp;' + Runner.lang.constants.EXPAND_ALL);
					
					// on collapse all, remove all ids from cookie
					delete_cookie('openMenuGroupIds', menuObj.cookieRoot, '');
					
					if (menuObj.curItem) {
						$('#' + menuObj.curItem.attr('topparent')).addClass('current');
					}
				} else {
					expand = true;
					
					$('.runner-vmenu.tree[parent]').removeClass('runner-hiddenelem');
					$('.manage a').empty();
					$('img.groupImg').attr('src',Runner.pages.constants.MINUS_GIF);
					$('.manage a').append('<img src=\"' + Runner.pages.constants.MINUS_GIF 
							+ '\" border=0> &nbsp;&nbsp;' + Runner.lang.constants.COLLAPSE_ALL);
					
					// on expand all add all group ids to cookie
					$('.runner-vmenu.tree.Group').each(function() {
						menuObj.addToCookie(this.id);
					});
					
					if (menuObj.curItem) {
						$('#' + menuObj.curItem.attr('topparent')).removeClass('current');
					}	
				}
				return false; 
			});
		}
	},
	
	/**
	 * Add opened menu group id to cookie
	 * @param {string} group id
	 */
	addToCookie: function(menuGroupId) {
		var openMenuGroupIds = get_cookie('openMenuGroupIds');
		
		if (openMenuGroupIds) {
			if (openMenuGroupIds.indexOf(menuGroupId) == -1) {
				openMenuGroupIds += ";" + menuGroupId;
			}
		} else {
			openMenuGroupIds = menuGroupId;
		}
		set_cookie( 'openMenuGroupIds', openMenuGroupIds, '', this.cookieRoot );
		this.toggleExpandCollapse();
	},
	
	/**
	 * Remove opened menu group id from cookie
	 * @param {string} group id
	 */ 
	removeFromCookie: function(menuGroupId) {
		var openMenuGroupIds = get_cookie('openMenuGroupIds');
		
		if (openMenuGroupIds) {
			openMenuGroupIds = openMenuGroupIds.replace((";" + menuGroupId), "");
			openMenuGroupIds = openMenuGroupIds.replace(menuGroupId, "");
		
			if (openMenuGroupIds.indexOf(';')==0)
				openMenuGroupIds = openMenuGroupIds.substr(1,openMenuGroupIds.length);
			
			set_cookie( 'openMenuGroupIds', openMenuGroupIds, '', this.cookieRoot );
		}
		
		setTimeout(function() {
			Runner.menu.TreeLikeVmenu.prototype.toggleExpandCollapse();
		},500);
	},
	
	/**
	 * Expand/Collapse button control
	 * If all menu groups was opened then change img for minus
	 * Else if all menu groups was closed then change img for minus
	 */
	toggleExpandCollapse: function() {
		var visibleLength = $(".runner-vmenu.tree.subitem:visible").length,
			hiddenLength = $(".runner-vmenu.tree.subitem:hidden").length;
		
		if (visibleLength == 0 && hiddenLength > 0) {
			$('.manage a').empty();
			$('.groupImg').attr('src', Runner.pages.constants.PLUS_GIF);
			$('.manage a').append('<img src=\"' + Runner.pages.constants.PLUS_GIF + '\" border=0> &nbsp;&nbsp;' + Runner.lang.constants.EXPAND_ALL);	
		} else if (visibleLength != 0 && hiddenLength == 0) {
			$('.manage a').empty();
			$('.groupImg').attr('src', Runner.pages.constants.MINUS_GIF);
			$('.manage a').append('<img src=\"' + Runner.pages.constants.MINUS_GIF + '\" border=0> &nbsp;&nbsp;' + Runner.lang.constants.COLLAPSE_ALL);
		}
	},
	
	/**
	 * Open menu's group on page after load
	 */
	openMenuOnLoad: function() {
		this.cookieRoot = Runner.getCookieRoot();
		
		var openMenuGroupIds = get_cookie('openMenuGroupIds');
		if (openMenuGroupIds) {
			var groupForOpenArr = openMenuGroupIds.split(";");
			for (var i = 0; i < groupForOpenArr.length; i++) {
				if (groupForOpenArr[i].indexOf('item') == -1) {
					continue;
				}
				var group = $('#' + groupForOpenArr[i]);
				group.removeClass('runner-hiddenelem');
				$('.runner-vmenu.tree[parent=' + groupForOpenArr[i] + ']').removeClass('runner-hiddenelem');
				$(".groupImg", group).attr("src", Runner.pages.constants.MINUS_GIF);
			}
			this.afterOpenMenuOnLoad();
		}
		this.toggleExpandCollapse();
	},
	
	/**
	 * Set current style to opened group after open menu on load
	 * @param {object}
	 */
	afterOpenMenuOnLoad: function(item) {
		if (typeof item == 'undefined') {
			if (!this.curItem) {
				return;
			} 		
			item = this.curItem;
		}
		//if item topItem 
		if (item.attr('view') == 'topitem') {
			return;
		}
		//remove current class from top item
		$('#' + item.attr('topparent')).removeClass('current');
		//get item parent
		var itemPar = $('#' + item.attr('parent'));
		//if parent open, then check recursively next parent 
		if ($(".groupImg", itemPar).attr('src') == Runner.pages.constants.PLUS_GIF) {
			itemPar.addClass('current');
			if (itemPar.attr('view')!='topitem') {
				this.afterOpenMenuOnLoad(itemPar);
			}
		}
	}
});


/**
 * @class Runner.menu.QuickJumpNew
 * Class that provides new quick jump menu functionality. 
 */
Runner.menu.QuickJumpNew = Runner.extend( Runner.emptyFn, {
	/**
	 * Current selected item in dropdown
	 * @type Integer
	 */
	selectCurrent: -1,
	/**
	 * Init quickjump menu
	 * Bind events handlers on dropdown
	 */
	init: function() {
		var menuObj = this;
		$(".rnr-quickjump:first").prop("initialized", "true");
		
		$(".rnr-quickjump")
			.on("focus", function() {
				menuObj.selectCurrent = this.selectedIndex; 
			})
			.on("change", function() {
				if ( this.options[this.selectedIndex].value ) {
					if ( $(this.options[ this.selectedIndex ]).attr('link') == 'External' ) {
						window.open( this.options[ this.selectedIndex ].value );
						return;
					} 
					window.location.href = this.options[ this.selectedIndex ].value;
					return;
				} 
				this.selectedIndex = menuObj.selectCurrent;
			});	
	}	
});

/**
 * @class Runner.menu.SimpleVmenuNew
 * Class that provides simple vertical menu functionality. 
 */
Runner.menu.SimpleVmenuNew = Runner.extend( Runner.menu.Manager, {
	/**
	* The jQuery object representing 
	* the element containing a menu (ul)
	* @type {object}
	*/
	menuContainer: {},

	/**
	* @param {object } jQmenuElem 
	*/
	init: function( jQmenuElem ) {
		this.setMenuObject( jQmenuElem );
		
		this.bindHoverOnItems();
		this.bindClickOnItems();
		this.setRaquoToTopItems();	
		this.setRaquoToSubItems();	
	},
	
	/**
	* Assign the menu object with 
	* the corresponding jQuery element
	* @param {object} jQmenuElem
	*/
	setMenuObject: function( jQmenuElem ) {
		this.menuContainer = jQmenuElem;
	},
	
	/**
	* Bind the hover events with every menu item
	* Add the "expanded" class on hover for items 
	* that have any sub-items and
	* remove the "expanded" class for these items 
	* when the mouse pointer leaves them
	*/
	bindHoverOnItems: function() {	
		$('.current', this.menuContainer).attr('data-current', 1);
		
		$("li", this.menuContainer).hover(		
			function() {
				// add expanded class to all li parents up to .runner-b-vmenu
				$(this).parentsUntil(".rnr-b-vmenu.simple", ".rnr-b-vmenu.simple li").addClass("expanded").removeClass("current");
			},
			function() {
				var $items = $(this).parent().closest('.rnr-b-vmenu.simple li')				
					.removeClass("expanded");
				
				$items.filter("[data-current=1]")
					.addClass("current");
			}
		);
	},
	
	/**
	* Bind the click event's handler with the menu items
	*/
	bindClickOnItems: function() {
		$('li', this.menuContainer)
			.on('click', this.itemClickHandler)
			.on('touchend', function() {
				$(this).trigger('click');
			});
	},
	
	/**
	* Set raquo for top items that have submenus
	*/
	setRaquoToTopItems: function() {
		var curTopItem = this.menuContainer.children(':has(ul:has(li.current))'),
			title = this.menuContainer.find("li.current:last").find("a:first").attr('itemtitle'),
			currentSubitemText = "<b class='subcur'>&nbsp;&raquo;&nbsp;" + title + "&nbsp;</b>",
			raquo = '<b class="raquo">&raquo;</b>',
			margin = Runner.isDirRTL() ? "margin-left" : "margin-right";
		
		this.menuContainer.children('*:has(ul:has(li))').find('a:first').each( function( index, elem ) {	
			var $elem = $(elem),
				$rightMostElem = $elem;
			
			if ( $elem.siblings('b').length ) {
				return;
			}
			
			if ( $elem.closest(curTopItem).length ) {
				$rightMostElem = $(currentSubitemText).insertAfter($elem);
				
				if ( !$rightMostElem.is(":visible") ) {
					$rightMostElem = $elem;
				}
				
			} 

			$elem.parent()
				.on('touchstart' + ( navigator.msMaxTouchPoints ? ' pointerover' : '' ), function() { // IE + touchscreen and Safari + IOS fix #9045
					var $groups = $elem.parents('.Group'),
						$groups = $groups.length ? $groups : $elem.parents('.menuGroup'),
						$currentGroup = $groups.eq(0),
						toggler = !$currentGroup.hasClass('touchHover');
						
					$groups.addClass('touchHover');
					$currentGroup.toggleClass('touchHover', toggler);
				})			
				.prepend( raquo );
			
			$rightMostElem.css( margin, "1em" );
		});
	},

	/**
	* Set raquo to subitems that have submenus
	*/
	setRaquoToSubItems: function() {
		var raquo = '<b class="raquo">&raquo;</b>',
			margin = Runner.isDirRTL() ? "margin-left" : "margin-right";;
		
		$('ul li:has(ul:has(li))', this.menuContainer).find('a:first').each( function(index, elem) {
			var $elem = $(elem);
			
			if ( $elem.siblings('b').length ) {
				return;
			}
			
			$elem.css( margin, Runner.isIE ? "1em" : "0.2em" )
			$elem.parent()
				.on('touchstart' + ( navigator.msMaxTouchPoints ? ' pointerover' : '' ), function() { // IE + touchscreen and Safari + IOS fix #9045
					var $groups = $elem.parents('.Group'),
						$groups = $groups.length ? $groups : $elem.parents('.menuGroup'),
						$currentGroup = $groups.eq(0),
						toggler = !$currentGroup.hasClass('touchHover');
						
					$groups.addClass('touchHover');
					$currentGroup.toggleClass('touchHover', toggler);
				})
				.prepend( raquo );
		});
	}
});

/**
 * @class Runner.menu.HorizontalNew
 * Class that provides horizontal menu functionality. 
 */
Runner.menu.HorizontalNew = Runner.extend( Runner.menu.Manager, {
	/**
	* The jQuery container object representing 
	* the element containing a menu (div)
	* @type {object}
	*/
	menuContainer: {},

	/**
	* @param {object} jQmenuContainer 
	*/	
	init: function( jQmenuContainer ) {
		this.setMenuObject( jQmenuContainer );
		
		this.bindHoverOnItems();
		this.bindClickOnItems();
		this.setRaquoToTopItems();
		this.setRaquoToSubItems();
	},

	/**
	* Assign the menu container object with 
	* the corresponding jQuery element
	* @param {object} jQmenuElem
	*/
	setMenuObject: function( jQmenuContainer ) {
		this.menuContainer = jQmenuContainer;
	},
	
	/**
	* Bind hover event with menu items 
	* to provide expanded class functionality
	*/
	bindHoverOnItems: function() {
		$('.current', this.menuContainer).attr('data-current', 1);
		
		$('.rnr-b-hmenu, .rnr-b-hmenu li', this.menuContainer).hover(
			function() {
				// add expanded class to all li parents up to .runner-b-vmenu
				$(this).parentsUntil(".rnr-ch", ".rnr-b-hmenu li, .rnr-b-hmenu").addClass("expanded").removeClass("current");
			},
			function() {
				var $items = $(this).parent().closest('.rnr-b-hmenu li, .rnr-b-hmenu')
					.removeClass("expanded");
					
				$items.filter("[data-current=1]")
					.addClass("current");
			}
		);
	},
	
	/**
	* Bind the click event's handler with the menu items
	*/
	bindClickOnItems: function() {
		$('.rnr-b-hmenu li, .rnr-b-hmenu > div:has(a[id^="itemlink"])', this.menuContainer)
			.on('click', this.itemClickHandler)
			.on('touchend', function() {
				$(this).trigger('click');
			});
	},

	/**
	* Set raquo for top items that have submenus
	*/
	setRaquoToTopItems: Runner.menu.SimpleVmenuNew.prototype.setRaquoToTopItems,

	/**
	* Set raquo to subitems that have submenus
	*/
	setRaquoToSubItems: Runner.menu.SimpleVmenuNew.prototype.setRaquoToSubItems
});

/**
 * @class Runner.menu.TreeLikeVmenuNew
 * Class that provides vertical tree-like menu functionality. 
 */
Runner.menu.TreeLikeVmenuNew = Runner.extend( Runner.menu.Manager, {
	/**
	* The jQuery container object representing 
	* the element containing a menu (ul) 
	* @type {object}
	*/
	menuContainer: {},
	
	/**
	* The menu name
	* @type {string}
	*/
	menuName: "",
	
	/**
	* Initialization flag
	* @type {bool}
	*/
	initialized: false,
	
	/**
	* Flags whether all groups are expanded
	* @type {bool}
	*/
	allExpanded: false,

	/**
	* @param {object } jQmenuElem 
	*/
	init: function( jQmenuElem ) {
		this.setMenuObject( jQmenuElem );
		
		this.bindClickOnItems();
		this.initSubItems();
		this.bindShowHideGroupClick();
		this.bindExpandAllClick();
		//open menu groups saved in cookies
		this.openMenuOnLoad();		
		this.toggleExpandCollapse();
		this.initialized = true;
	},

	/**
	* Assign the menu container object with the corresponding jQuery element
	* and set the menu name
	* @param {object} jQmenuElem
	*/
	setMenuObject: function( jQmenuElem ) {
		this.menuContainer = jQmenuElem;
		this.menuName = jQmenuElem.data('menuname');
	},

	/**
	* Bind the click event's handler with the menu items
	*/
	bindClickOnItems: function() {
		$('.menuLeaf', this.menuContainer).on('click', this.itemClickHandler);
	},	
	
	/**
	* Hide all sub-menus and add the click hanlers for all sub-items
	*/
	initSubItems: function() { 
		var subItems = this.menuContainer.children('.subitem');
		subItems.addClass('rnr-hiddenelem').on('click', this.itemClickHandler);			
	},
	
	/**
	* Get menu item level
	* There are up to 5 levels in tree like menu
	* Top items have level 1
	* @param {object} jquery item object
	* @return {number} number of levels
	*/
	getItemLevel: function( item ) {
		return parseInt( $(item).attr("level"), 10 );
	},
	
	/**
	* Gets a jQuery object representing
	* the current (selected) menu element
	* @return {object} 
	*/
	getCurrentElements: function() {
		return this.menuContainer.children('.current');
	},
	
	/**
	* Hides group of elements with all subgroups
	* @param {DOM elem} item
	*/
	hideGroup: function( item ) {	
		var childItems = this.getChildItems(item),
			menuObj = this;

		if ( !childItems.length ) {
			return;
		}
		
		if (this.initialized) {
			childItems.filter(":not([level=1])").slideUp(100, function() {menuObj.toggleExpandCollapse();});
		} else {
			childItems.filter(":not([level=1])").hide();
		}		
		
		childItems.each( function() {
			menuObj.hideGroup(this);
		});
		
		this.removeFromCookie( $(item).attr('id') );
		$(".groupImg", item).attr('src', Runner.pages.constants.PLUS_GIF);
	},

	/**
	* Shows group of elements
	* @param {DOM elem} item
	*/
	showGroup: function( item ) {	
		var childItems = this.getChildItems(item),
			menuObj = this;
		
		if ( childItems.length ) {
			
			if (this.initialized) {
				childItems.slideDown(100, function() {menuObj.toggleExpandCollapse();});
			} else {
				childItems.show();
			}
			this.addToCookie( $(item).attr('id') );
			$(".groupImg", item).attr('src', Runner.pages.constants.MINUS_GIF);
		}
	},
	
	/**
	* Set current style for top group
	* which has current item in children
	*/
	setCurrentItemStyle: function() { 
		var $items = this.getCurrentElements();
		
		$( '.current', this.menuContainer ).removeClass( 'current' );
		
		while ( $items.length ) {
			$items.each( function( itemId, item ) {			
				var $grCtrl = $( '.groupImg', item );
				
				if ( !$grCtrl.length || $grCtrl.attr( 'src' ) !== Runner.pages.constants.MINUS_GIF ) {
					// not collapsed
					$( item ).addClass('current');
				}
			});
			
			if ( $items.is(':visible') ) {
				break;
			}
			
			$items = this.getParentItem( $items );
		}
	},
	
	/**
	* Bind click event with the "+"/"-" images to toggle the menu groups
	*/
	bindShowHideGroupClick: function() {
		var menuObj = this;
		
		// click in any place of the Group Item except the correct link should toggle the group
		$('.groupImg', this.menuContainer).closest('li').on("click", function(e) {
			var target = $(e.target),
				liElement, show;
				
			if ( target.is('a') && target.href && target.href !== "#" ) {
				return;
			}
			
			liElement = $(this);
			show = $('.groupImg', this).attr('src') === Runner.pages.constants.PLUS_GIF;

			show ? menuObj.showGroup( liElement ) : menuObj.hideGroup( liElement ); 			
			menuObj.setCurrentItemStyle();
			return false;
		});
	},

	/**
	* Get a jQuery object representing the item's parent element
	* @param {DOM elem} item
	* @return {object}	
	*/
	getParentItem: function(item) {
		var level = this.getItemLevel(item);
		
		return $(item).prevUntil("[level=" + (level - 1) + "]").add( item ).first().prev();
	},

	/**
	* Get a jQuery object representing the item's children elements
	* @param {DOM elem} item
	* @return {object}	 
	*/
	getChildItems: function( item ) {
		var level = this.getItemLevel(item);
		
		if ( isNaN(level) ) {
			return this.menuContainer.children("[level=1]");
		}
		return $(item).nextUntil("[level=" + level + "]", "[level=" + (level + 1) + "]");
	},

	/**
	* Expand or collapse the menu groups with the expand/collapse button
	*/
	bindExpandAllClick: function() {
		var menuObj = this;
		$(".manage", this.menuContainer).on("click", function() {
			if (menuObj.allExpanded) {
				menuObj.hideGroup();
				// on collapse all, remove all ids from cookies
				delete_cookie('openMenuGroupIds_' + menuObj.menuName, menuObj.cookieRoot, '');	
			} else {
				menuObj.menuContainer.children('[level]').each( function() {
					menuObj.showGroup(this);
				});
			}
			return false; 
		});
	},
	
	/**
	* Add opened menu group id to cookie
	* @param {string} menuGroupId
	*/
	addToCookie: function( menuGroupId ) {
		var key = 'openMenuGroupIds_' + this.menuName,
			openMenuGroupIds = get_cookie(key);
		
		if ( openMenuGroupIds && openMenuGroupIds.indexOf(menuGroupId) === -1 ) {
			openMenuGroupIds += ";" + menuGroupId;
		}
		
		set_cookie( key, openMenuGroupIds || menuGroupId, '', this.cookieRoot );
	},
	
	/**
	* Remove opened menu group id from cookie
	* @param {string} menuGroupId
	*/ 
	removeFromCookie: function( menuGroupId ) {
		var key = 'openMenuGroupIds_' + this.menuName,
			openMenuGroupIds = get_cookie(key);
		
		if (!openMenuGroupIds) {
			return;
		}
		
		openMenuGroupIds = openMenuGroupIds.replace( ";" + menuGroupId, "");
		openMenuGroupIds = openMenuGroupIds.replace(menuGroupId, "");
	
		if (openMenuGroupIds.indexOf(';') === 0) {
			openMenuGroupIds = openMenuGroupIds.substr(1);
		}
		
		set_cookie( key, openMenuGroupIds, '', this.cookieRoot );
	},
	
	/**
	* The Expand/Collapse button's toggler
	* If all menu groups are opened then change img to minus
	* If all menu groups are closed then change img to plus
	*/
	toggleExpandCollapse: function() {
		var menuContainer = this.menuContainer,
			manageBlock = menuContainer.children('.manage'),
			hiddenSubItems = $(".subitem:hidden", menuContainer),
			src;

		this.allExpanded = !hiddenSubItems.length;
		src = this.allExpanded ? Runner.pages.constants.MINUS_GIF : Runner.pages.constants.PLUS_GIF;
		
		$('.expand', manageBlock).toggle( !this.allExpanded );
		$('.hide', manageBlock).toggle( this.allExpanded );
		$('img', manageBlock).attr('src', src); 
		
		this.setCurrentItemStyle();
	},
	
	/**
	* Open the menu's group/groups after the menu is loaded
	*/
	openMenuOnLoad: function() {
		var openMenuGroupIds, groupForOpenArr, i;
		
		this.cookieRoot = Runner.getCookieRoot();
		openMenuGroupIds = get_cookie('openMenuGroupIds_' + this.menuName);
		
		if (!openMenuGroupIds) {
			return;
		}
		groupForOpenArr = openMenuGroupIds.split(";");
		
		for (i = 0; i < groupForOpenArr.length; i++) {
			if ( groupForOpenArr[i].indexOf('item') !== -1 ) {
				$('#' + groupForOpenArr[i] + ' .groupImg', this.menuContainer).trigger("click");
			}
		};	
	}
});
/**
 * Global object for locking and unlocking records
 * @object
 */
Runner.Locking = Runner.extend( Runner.emptyFn, {
	/**
	 * Table name
	 * @type {string}
	 */
	tName: "",
	
	/**
	 * Page id
	 * @type {integer}
	 */
	pageId: 0,
	/**
	 * Page object
	 * @type {object}
	 */
	pageObj: null,
	
	/**
	 * @type {string}
	 */
	pageUrl: "",
	
	rowLocking: [],
	
	/**
	 * EditPage/InlineEdit object
	 */
	hostObj: null,
	
	constructor: function( cfg ) {
		Runner.apply( this, cfg );
		
		this.pageObj = Runner.pages.PageManager.getAt( this.tName, this.pageId ); 
		this.shortTName = Runner.pages.PageSettings.getShortTName( this.tName );
		this.pageUrl = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );
	},
	
	/**
	 * Unlock record
	 * @param {string} keys
	 * @param {integer} session id
	 * @param {function} function
	 */
	unlockRecord: function( keys, sid, callback ) {
		window.clearInterval( this.rowLocking[ "timeid" + keys ] );
		
		this.unlockDetails();
		
		$.get.trackingUserActivity = false;		
		var xmlhttp = $.get( this.pageUrl, {
				action: 'unlock',
				keys: escape( keys ),
				sid: sid,
				rndval: Math.random()
			})
			.always( function() {
				if ( typeof callback === "function" ) {
					callback();
				}
			});
	},
	
	/**
	 * Confirm lock	 
	 * @param {string} keys
	 * @param {number} rowid
	 */
	confirmLock: function( keys, rowId ) {
		var lockObj = this,
			params = {
				action: 'confirm',
				keys: escape( keys ),
				rndval: Math.random()
			};
		
		$.get.trackingUserActivity = false;
		$.get( this.pageUrl, params, function( message ) {
			if ( message != '' ) {
				lockObj.hostObj.disableLockedReckordPage &&
					lockObj.hostObj.disableLockedReckordPage( rowId, message );
					
				$('.rnr-locking').html( message )
					.show();

				clearInterval( lockObj.rowLocking[ 'timeid' + keys ] );
			}
		}, "html" );
	},
	
	
	/**
	 * unlockAdmin
	 * @param {object} keys
	 * @param {integer} old page id
	 * @param {boolean} start edit or ot
	 */
	unlockAdmin: function( keys, startEdit ) {
		var lockObj = this,
			params = {
				action: 'lockadmin',
				keys: escape( keys ),
				startEdit: startEdit,
				rndval: Math.random()
			};
		
		$.get.trackingUserActivity = false;	
		$.get( this.pageUrl, params, function( action ) {
			$('.rnr-locking').hide();
			
			if ( action == 'lock' ) {
				lockObj.hostObj.enableLockedReckordPage &&
					lockObj.hostObj.enableLockedReckordPage();
			}
		});
	},
	
	/**
	 * Unlock inline editing record
	 * @param {object} keys
	 * @param {boolean} sync
	 */
	unlockRecordInline: function( keys, sync ) {
		if ( !keys ) {
			return;
		}
		
		window.clearInterval( this.rowLocking[ "timeid" + keys ] );
		this.unlockDetails();
		
		var keyArr = [], 
			tmpArr = keys.split("&"),
			pos, getParams, i;
		
		for (i = 0; i < tmpArr.length; i++) {
			pos = tmpArr[i].indexOf("=");
			keyArr.push( tmpArr[i].substr(pos + 1) );
		}
		
		getParams = {
			url: this.pageUrl,
			data: {
				action: 'unlock',
				keys: escape( keyArr.join("&") ),
				rndval: Math.random()
			},
			success: function(xml) {},
		};
		
		if ( sync ) {
			getParams.async = false;
		}
		
		$.ajax.trackingUserActivity = false;
		$.ajax( getParams );		
	},
	
	/**
	 * Send 'confirm lock' requests
	 * @param {number} rowId
	 * @param {string} sKeys
	 */
	startLocking: function( rowId, sKeys ) {
		var locking = this;
		var confirmTime = Runner.pages.PageSettings.getTableData( this.tName, "confirmTime" );
		
		this.rowLocking[ 'timeid' + sKeys ] = setInterval( function() {
				locking.confirmLock( sKeys, rowId );
			}, confirmTime * 1000 );
	},
	
	/**
	 * @param {string} keys
	 */
	resetSetInterval: function( keys ) {
		keys && window.clearInterval( this.rowLocking[ "timeid" + keys ] );	
	},
	
	/**
	 * Unlock details' records
	 */
	unlockDetails: function() {
		if ( !Runner.pages.PageSettings.getTableData( this.pageObj.tName, "isShowDetails" ) ) {
			return;
		}
		
		( this.pageObj.controlsMap.dpTablesParams || [] ).forEach( function( dpTableData ) {
				var detailPage = this.pageObj.dpObjs[ dpTableData.tName ].detailsPageObj;
				if ( detailPage && ( detailPage.inlineAdd || detailPage.inlineEdit ) ) {
					( detailPage.inlineAdd || detailPage.inlineEdit ).cancelAll();
				}
			}, this );
	}
});
// create namespace
Runner.namespace('Runner.s508');

/**
 * @class Runner.s508
 * Abstract base class that provides section508 functionality. 
 */
Runner.s508 = Runner.extend(Runner.emptyFn, {
	/**
	 * Inner flag
	 * @type {bool}
	 */
	calc508column: true,
	/**
	 * TD which contain current control
	 * @type {object}
	 */
	s508td: null,
	/**
	 * Number of grid column which contain current control
	 * @type {integer}
	 */
	s508column:-1,
	/**
	 * Grid object
	 * @type {object}
	 */
	gridObj: null,
	/**
	 * ID of current page
	 * @type {integer}
	 */
	pageId: -1,
	/**
	 * URL of current page
	 * @type {string}
	 */
	pageURL: null,
	/**
	 * Count of pages in pagination
	 */
	maxPages: 1,
	/**
	 * Use InlineEdit or not
	 * @type {boolean}
	 */
	isUseInlineEdit: false,

	constructor: function(cfg) {
		Runner.apply(this, cfg);
	},
	
	init: function() {
		this.section508setEvents();
		this.s508pagination();
		this.s508jumpto();
		if (this.isUseInlineEdit) {
			this.s508inlineEdit();
		}
	},
	
	/**
	 * section508setEvents
	 * @param {integer} pageid - id of page,
	 * @param {} focusedControl
	 * @param {} setFocusInTD
	 */
	section508setEvents: function(focusedControl, setFocusInTD) {
		if (!this.gridObj) {
			return false;
		}
		if (this.gridObj.attr("s508table") == undefined) {
			this.gridObj.attr("s508table", "true");
			this.setFocusFirstElement();
		}
		var self = this,
			// define handler functions
			s508eventArrow = function(e) {
				self.calc508column = true;
				switch (e.which) {
					case 40:
						self.calc508column = false;
						self.click_arrow(this, "next", self.s508column);
						break;
					case 38:
						self.calc508column = false;
						self.click_arrow(this, "prev", self.s508column);
						break;
					case 9:
						self.calc508column = true;
						break;
					default:
						self.calc508column = true;
						break;
				}
			},
			focusHandler = function() {
				if (self.calc508column) {
					self.s508column = self.getControlColumn(this);
				}
			};
			
		// assign handler functions
		$("input,a,textarea,select", this.gridObj).each(function() {
			$elem = $(this);
			if ( !$elem.attr("event508") ) {
				$elem.attr("event508", "true")
					.on( 'keydown', s508eventArrow )
					.on( 'focus', focusHandler );
			}
		});

		// calc focused column
		if (focusedControl) {
			this.s508column = this.getControlColumn(focusedControl.spanContElem);
		}

		// set focus to visible element in current td 
		// set event alt+e to Inline Edit
		if (setFocusInTD && this.s508td) {
			var func = function() {
				$("input:first,a:first,textarea:first,select:first", self.s508td).trigger("focus");
			};
			setTimeout(func, 100);
				
			$("[id^=iEditLink" + this.pageId + "_]", this.s508td).on("keydown", function(e) {
				if (e.altKey && e.which == 69) {
					$(this).trigger("click");
					Runner.Event.prototype.stopEvent(e);
				}
			});
		}
	},

	/**
	 * getControlColumn
	 * Find index of column which contain the control
	 * @param {object} control - control inside the grid
	 * @return {integer} index of column
	 */
	getControlColumn: function(control) {
		var parentTr = this.find508tr(control),
			parentTrFound = false,
			tdObj = false;

		// find parent TD
		$(control).parents().each(function(k, elem) {
			if (parentTrFound) {
				return;
			}
			if (elem === parentTr) {
				parentTrFound = true;
			}
			if ( $(elem).is("td") ) {
				tdObj = elem;
			}
		});
		
		// find parent TD's index
		var numberTd = 0,
			colspanAttr = "",
			tdFound = false;

		$(parentTr).children().each(function(k,elem) {
			if (elem === tdObj) {
				tdFound = true;
			}
			if (tdFound) {
				return;
			}
			numberTd++;
			colspanAttr = $(elem).attr("colspan");
			if (colspanAttr) {
				numberTd += parseInt(colspanAttr) - 1;
			}
		});
		this.s508td = tdObj;
		return numberTd;
	},

	/**
	 * find506tr
	 * Find the main table TR which contain the control
	 * @param {object} control
	 * @return {object} TR which contain the control
	 */
	find508tr: function(control) {
		var trObj = false,
			mainTableFound = false;

		$(control).parents().each(function(k, elem) {
			if (mainTableFound) {
				return;
			}
			if ( $(elem).is("tr") ) {
				trObj = elem;
			}
			if ($(elem).attr("s508table") == "true") {
				mainTableFound = true;
			}
		});
		return trObj;
	},

	/**
	 * click_arrow
	 * Move focus to the next/previous row in table when down/up arrow pressed
	 * @param {object} control wich handling event
	 * @param {string} moving direction
	 * @param {integer} number of column wich contain control
	 */
	click_arrow: function(control, direction, colnum) {
		// get target TR
		var tr = $(this.find508tr(control)),
			isNext = direction == "next",
			trobj = isNext ? $(tr).next() : $(tr).prev();

		// skip empty and hidden TRs 
		while($("input,a,textarea,select", trobj).length == 0 && $(trobj).get(0) 
			|| $(trobj).css("display") == "none") {
			trobj = isNext ? $(trobj).next() : $(trobj).prev();
		}

		// skip from THEAD to TBODY and vice versa
		if (!isNext && !$(trobj).get(0) && $(tr).parents().is("tbody") 
			&& $("thead tr:visible", this.gridObj).length > 0) {
			trobj = $("thead tr:visible", this.gridObj).get(0);
		}
		if (isNext && !$(trobj).get(0) && $(tr).parents().is("thead") 
			&& $("tbody tr:visible", this.gridObj).length > 0) {
			trobj = $("tbody tr:visible", this.gridObj).get(0);
		}
		
		// find element in the target row and set focus to it
		var altElem = focusSet = false;
		if (colnum == -1) {
			$("input:first,a:first,textarea:first,select:first", trobj).trigger("focus");
		} else {
			$(trobj).children().each(function(k, elem) {
				if (focusSet) {
					return;
				}
				if ($("input:first,a:first,textarea:first,select:first", elem).length > 0) {
					// find left neighbour
					if (k < colnum && altElem || !altElem) {
						altElem = elem;
					}
					// exact fit
					if (k == colnum) {
						$("input:first,a:first,textarea:first,select:first", elem).trigger("focus");
						focusSet = true;
					}
				}
			});
			if (!focusSet && altElem) {
				$("input:first,a:first,textarea:first,select:first", altElem).trigger("focus");
			}
		}
	},

	/**
	 * setFocusFirstElement
	 * Set focus to the first element in the grid body
	 */
	setFocusFirstElement: function(sObj) {
		$("input:first,a:first,textarea:first,select:first", $("tbody tr:visible:first", this.gridObj)).trigger("focus");
	},

	/**
	 * getURLParam
	 * Return value of given get-parameter from request URL
	 * @param {string} parameter name
	 * @return {string} parameter value
	 */
	getURLParam: function(name) {
		name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
		var regexS = "[\\?&]" + name + "=([^&#]*)",
			regex = new RegExp(regexS),
			results = regex.exec( window.location.href );
		return results == null ? "" : results[1];
	},

	/**
	 * s508pagination
	 * Bind pagination to keydown event of the page
	 */
	s508pagination: function() {
		var self = this,
			s508eventPagination = function(e) {
				NumberPage = self.getURLParam("goto");
				if (NumberPage == "") {
					NumberPage = 1;
				}	
				if (e.ctrlKey && e.which == 37) {
					if (NumberPage > 1) {
						NumberPage--;
						window.location.href = self.pageURL + "?goto=" + NumberPage;
					} else {
						NumberPage = 1;
					}	
				}
				if (e.ctrlKey && e.which == 39) {
					if (NumberPage < self.maxPages) {
						NumberPage++;
						window.location.href = self.pageURL + "?goto=" + NumberPage;
					} else {
						NumberPage = self.maxPages;
					}	
				}
			};
		$(document).on("keydown", s508eventPagination);
	},

	/**
	 * s508jumpto
	 * Bind "jumpto" to keydown event of the page
	 */
	s508jumpto: function() {
		var self = this,
			s508eventJumpTo = function(e) {
				if (e.altKey && e.which == 70) { // alt+f jump to search
					$("#ctlSearchFor" + self.pageId).trigger("focus");
					Runner.Event.prototype.stopEvent(e);
				}
				if (e.altKey && e.which == 77) { // alt+m jump to menu
					$("[id^=itemlink]:first").trigger("focus");
					Runner.Event.prototype.stopEvent(e);
				}
			};
		$(document).on("keydown", s508eventJumpTo);
	},
	
	s508inlineSaveCancel: function(tr_id) {
		var self = this;
		$("#gridRow" + tr_id).off("keydown").on("keydown",function(e) {
			if (e.altKey && e.which == 83) { //alt+s save inline record
				$("#saveLink" + tr_id).trigger("click");
				Runner.Event.prototype.stopEvent(e);
			}
			if (e.altKey && e.which == 67) { //alt+c cancel inline record
				$("#revertLink" + tr_id).trigger("click");
				self.s508setFocusToRow(tr_id);
				Runner.Event.prototype.stopEvent(e);
			}
		});
	},
	
	s508setFocusToRow: function(id) {
		$("#gridRow" + id).off("keydown");
		$("#gridRow" + id).find("input:first,a:first,textarea:first,select:first").trigger("focus");
	},
	
	/**
	 * s508inlineEdit
	 */
	s508inlineEdit: function() {
		var self = this;
		$("[id^=iEditLink]").each(function() {
			var parent_tr = $(this).parent("td").parent("tr"), 
				record_id = this.id;
				
			$("input,a,textarea,select", parent_tr).each(function() {
				$(this).on("keydown", function(e) {
					if (e.altKey && e.which == 69) {
						$("#" + record_id).trigger("click");
						Runner.Event.prototype.stopEvent(e);
					}
				});
			});
		});
	}
});

/**
 * @class Runner.controls.constants
 */
Runner.controls.constants = {
	/**
	 * View format constants
	 * @type String
	 */
	FORMAT_NONE: '',	
	FORMAT_DATE_SHORT: "Short Date",	
	FORMAT_DATE_LONG: "Long Date",
	FORMAT_DATE_TIME: "Datetime",
	FORMAT_TIME: "Time",
	FORMAT_CURRENCY: "Currency",
	FORMAT_PERCENT: "Percent",
	FORMAT_HYPERLINK: "Hyperlink",
	FORMAT_EMAILHYPERLINK: "Email Hyperlink",
	FORMAT_FILE_IMAGE: "File-based Image",
	FORMAT_DATABASE_IMAGE: "Database Image",
	FORMAT_DATABASE_FILE: "Database File",
	FORMAT_FILE: "Document Download",
	FORMAT_LOOKUP_WIZARD: "Lookup wizard",
	FORMAT_PHONE_NUMBER: "Phone Number",
	FORMAT_NUMBER: "Number",
	FORMAT_HTML: "HTML",
	FORMAT_CHECKBOX: "Checkbox",
	FORMAT_MAP: "Map",
	FORMAT_CUSTOM: "Custom",
	/**
	 * edit format constants
	 * @type String
	 */
	EDIT_FORMAT_NONE: "",
	EDIT_FORMAT_TEXT_FIELD: "Text field",
	EDIT_FORMAT_TEXT_AREA: "Text area",
	
	EDIT_FORMAT_RTE: "BASIC",
	EDIT_FORMAT_RTEINNOVA: "INNOVA",
	EDIT_FORMAT_RTECK: "CK",
	EDIT_FORMAT_RTECK_NEW: "CK5",
	
	EDIT_FORMAT_PASSWORD: "Password",
	EDIT_FORMAT_DATE: "Date",
	//Date control types
	EDIT_DATE_SIMPLE: 0,
	EDIT_DATE_SIMPLE_INLINE: 2,
	EDIT_DATE_SIMPLE_DP: 11,
	EDIT_DATE_DD: 12,
	EDIT_DATE_DD_INLINE: 5,
	EDIT_DATE_DD_DP: 13,
	//eo Date control types
	EDIT_FORMAT_TIME: "Time",
	EDIT_FORMAT_RADIO: "Radio button",
	EDIT_FORMAT_CHECKBOX: "Checkbox",
	EDIT_FORMAT_DATABASE_IMAGE: "Database image",
	EDIT_FORMAT_DATABASE_FILE: "Database file",
	EDIT_FORMAT_FILE: "Document upload",
	EDIT_FORMAT_LOOKUP_WIZARD: "Lookup wizard",
	EDIT_FORMAT_HIDDEN: "Hidden field",
	EDIT_FORMAT_READONLY: "Readonly",
	
	/**
	 * Lookup wizard constants
	 */
	
	LCT_DROPDOWN: 0,
	LCT_AJAX: 1,
	LCT_LIST: 2,
	LCT_CBLIST: 3,
	LCT_RADIO: 4,
	
	LT_LISTOFVALUES: 0,
	LT_LOOKUPTABLE: 1,
	LT_CUSTOM: 2,
	
	
	MODE_ADD: 0,
	MODE_EDIT: 1,
	MODE_SEARCH: 2,
	MODE_LIST: 3,
	MODE_PRINT: 4,
	MODE_VIEW: 5,
	MODE_INLINE_ADD: 6,
	MODE_INLINE_EDIT: 7,
	MODE_EXPORT: 8,
	
	/**
	* Filter format constants
	*/
	FILTER_FORMAT_VALUES_LIST: "Values list",
	FILTER_FORMAT_BOOLEAN: "Options list",
	FILTER_FORMAT_INTERVAL_LIST: "Interval list",
	FILTER_FORMAT_INTERVAL_SLIDER: "Interval slider",
	
	FILTER_MULTISELECT_NONE: 0,
	FILTER_MULTISELECT_ON_DEMAND: 1,
	FILTER_MULTISELECT_ALWAYS: 2,
	
	FILTER_KNOBS_BOTH: 0,
	FILTER_KNOBS_MIN_ONLY: 1,
	FILTER_KNOBS_MAX_ONLY: 2,
	
	FILTER_STEP_SEC: 0,
	FILTER_STEP_MIN: 1,
	FILTER_STEP_HOUR: 2,
	FILTER_STEP_DAY: 3,
	FILTER_STEP_MONTH: 4,
	FILTER_STEP_YEAR: 5,
	
	GOOGLE_MAPS: 0,
	OPEN_STREET_MAPS: 1,
	BING_MAPS: 2,
	HERE_MAPS: 3,
	MAPQUEST_MAPS: 4
};/**
 * @class Runner.Event
 * Abstract base class that provides event functionality. 
 * Example of usage:
	Employee = function(name) {
		this.name = name;
		this.addEvent(["blur", "change"]);
		this.init();
	}
	Runner.extend(Employee, Runner.Event);
===================================================================
Predefined, javascript events:
	abort	  - Loading of an image is interrupted
	blur	  - An element loses focus
	change	  - The user changes the content of a field
	click	  - Mouse clicks an object
	dblclick  - Mouse double-clicks an object
	error	  - An error occurs when loading a document or an image
	focus	  - An element gets focus
	keydown   - A keyboard key is pressed
	keypress  - A keyboard key is pressed or held down
	keyup	  - A keyboard key is released
	load	  - A page or an image is finished loading
	mousedown - A mouse button is pressed
	mousemove - The mouse is moved
	mouseout  - The mouse is moved off an element
	mouseover - The mouse is moved over an element
	mouseup	  - A mouse button is released
	reset	  - The reset button is clicked
	resize	  - A window or frame is resized
	select	  - Text is selected
	submit	  - The submit button is clicked
	unload	  - The user exits the page
*/

Runner.Event = Runner.extend( Runner.emptyFn, {
	/**
	 * Array of predefined events
	 * @type {array}
	 */
	events: null,
	/**
	 * Array of predefined listeners
	 * @type {array}
	 */
	listeners: null,
	/**
	 * Array of elements, on which listeners should be added
	 * @type {array}
	 */
	elemsForEvent: null,
	/**
	 * Array of events that are suspended for this control
	 * @type {array}
	 */
	suspendedEvents: null,
	
	/**
	 * The flag indicating that events are initialized	
	 * @type {Boolean}
	 */
	eventsAreInitialized: false,
	
	/**
	 * @constructor
	 */
	constructor: function() {
		// recreate objects, to prevent memory mix
		this.listeners = [];
		this.elemsForEvent = [];
		this.suspendedEvents = [];
	},
	
	/**
	 * Init method, should be called by class contructor, for event initialization
	 * @method
	 */	
	init: function() {
		if ( !this.events.length ) {
			return;
		}
		
		var i, eventName;
		
		for (i = 0; i < this.events.length; i++) {
			eventName = this.events[i];
			// pass event name and event standard handler
			if ( typeof this[ eventName ] == "function" ) {
				this.on( eventName, this[ eventName ] );
			} else if ( typeof this[ eventName ] == "object" ) {
				this.on( eventName, this[ eventName ].fn, this[ eventName ].options, this[ eventName ].scope );
			}
		}
		
		this.eventsAreInitialized = true;
	},
	
	/**
	 * @param {array} eventArr
	 */
	suspendEvent: function( eventArr ) {
		for (var i = 0; i < eventArr.length; i++) {
			if ( $.inArray( eventArr[i], this.suspendedEvents ) === -1 ) {
				this.suspendedEvents.push( eventArr[i] );
			}
		}
	},
	
	/**
	 * @param {array} eventArr 
	 */
	resumeEvent: function( eventArr ) {
		var eventInd, i;
		
		for (i = 0; i < eventArr.length; i++) {
			eventInd = $.inArray( eventArr[i], this.suspendedEvents );
			
			if ( eventInd !== -1 ) {
				this.suspendedEvents.splice(eventInd, 1);
			}
		}
	},
	
	/**
	 * Create a delayed event handler	
	 * @param {function} handler
	 * @param {number} timeout
	 * @return {function}	 
	 */
	createDelayed: function( handler, timeout ) {
		return function() {
			var obj = this, 
				args = arguments;
				
			setTimeout( function() {
				handler.apply(obj, args);
			}, timeout || 10 );
		};
	},

	/**
	 * Create an event handler that won't be triggered, 
	 * as long as it continues to be invoked 
	 * during a given number of milliseconds (buffer). 	
	 * @param {function} handler
	 * @param {number} buffer
	 * @return {function}	 
	 */	
	createBuffered: function( handler, buffer ) {
		var task = new Runner.util.DelayedTask( handler );
		return function() {
			task.delay( buffer, handler, null, Array.prototype.slice.call( arguments ) );
		};
	},
	
	/**
	 * Create an event handler that will be executed at most once	
	 * @param {function} handler
	 * @param {srting} eventName
	 * @return {function}
	 */		
	createSingle: function( handler, eventName ) {
		var eventObj = this,
			singleHandler = function() {
				handler.apply( eventObj, arguments );
				eventObj.clearEventHandler( eventName, singleHandler );
			};
			
		return singleHandler
	},
	
	/**
	 * Bind an event handler to the events elements using jQuery bind method
	 * @param {string} eventName
	 * @param {function} callHandler
	 */ 	
	bindHn: function( eventName, callHandler ) {
		if ( !callHandler || !eventName ) {
			return;
		}

		// adding listeners for all elems for event
		for (var i = 0; i < this.elemsForEvent.length; i++) {
			$( this.elemsForEvent[i] ).on(eventName, {hn: callHandler, obj: this}, callHandler);
		}
	},
	
	/**
	 * Unbind DOM events from events elements using jQuery unbind method
	 * @param {string} eventName
	 * @param {function} callHandler
	 */ 
	unbindHn: function( eventName, callHandler ) {
		if ( !callHandler || !eventName ) {
			return;
		}

		// remove listeners for all elems for event	
		for (var i = 0; i < this.elemsForEvent.length; i++) {
			$( this.elemsForEvent[i] ).off(eventName, null, callHandler);
		}
	},
	
	/**
	 * Add events to the object. Events names should be similar to predefined
	 * javascript DOM element event names.
	 * @method
	 * @param {string} eventName
	 * @param {functions} fn
	 * @param {object} options
	 * 		@param {array} options.args Optional. Array of arguments, that should be passed to event handler
	 * 		@param {bool} options.single Optional. Pass true to fire event only once
	 *		@param {int} options.timeout Optional. Pass number of miliseconds to create delayed handler
	 * 		@param {int} options.buffer Optional. Pass number of miliseconds to buffer. Usefull for keypress events and validations. 
	 * @param {link} scope
	 * @return {boolean}
	 */
	on: function( eventName, fn, options, scope ) {
		if ( !this.elemsForEvent.length || !fn ) {
			return false;
		}
		if( !eventName ) {
			return false;
		}

		eventName = eventName.toLowerCase();
		this.addEvent( [eventName] );
		
		// prepare event name, for DOM scpecifications
		if ( eventName.indexOf("on") === 0 ) {
			eventName = eventName.slice(2);
		}

		var callHandler,
			scope = scope || this, 
			objScope = this, 
			options = options || {},
			args = options.args || [], 
			single = options.single || false, 
			timeout = options.timeout || 0, 
			buffer = options.buffer || 0;
			
		callHandler = function(e) {
			// prevent call if event suspended 
			if ( $.inArray( eventName, objScope.suspendedEvents ) !== -1 ) {
				return;
			}
			var newArgs = Array.prototype.slice.call( arguments );
			//the order of arguments: 	event object, arguments passed to event handler, arguments passed to fire event
			fn.apply( scope, newArgs.slice(0, 1).concat(args).concat( newArgs.slice(1) ) );
		}
		
		// creating delayed handler, usefull for validations etc.
		if ( timeout ) {
			callHandler = this.createDelayed( callHandler, timeout );
		}
		// function will clear itself after called, usefull when function need to be called once
		if ( single ) {
			callHandler = this.createSingle( callHandler, eventName )
		}
		if ( buffer ) {
			callHandler = this.createBuffered( callHandler, buffer );
		}
		
		this.listeners.push({
			name: eventName,
			handler: fn,
			callHandler: callHandler,
			options: options,
			scope: scope,
			index: this.listeners.length
		});
		
		this.bindHn( eventName, callHandler );
		return true;
	},
	
	/**
	 * Add events to object, make list of predefined events, before call init method
	 * @method
	 * @param {array} eventNameArr
	 */
	addEvent: function( eventNameArr ) {
		this.events = this.events || [];

		this.addEvent = function( eventNameArr ) {
			for (var i = 0; i < eventNameArr.length; i++) {
				// check if this event already added
				if ( $.inArray( eventNameArr[i], this.events ) === -1 ) {
					this.events.push( eventNameArr[i] );
				}
			}
		}
		this.addEvent( eventNameArr );
	},
	
	/**
	 * Kill event handling, sets empty fn as handler
	 * @method
	 * @param {string} eventName
	 */
	killEvent: function( eventName ) {
		var eventInd = $.inArray( eventName, this.events )
		if ( eventInd === -1 ) {
			return;
		}
		
		this.clearEvent( eventName );
		// remove native event handler
		this.unbindHn( eventName, this[ eventName ] );
		// delete event handler from object
		delete this[ eventName ];
		//kill event
		this.events.splice(eventInd, 1);
	},
	
	/**
	 * Clear events' handlers
	 */
	purgeListeners: function() {
		if (!this.events) {
			return;
		}
		//get a shallow copy of the events array
		var copyEvents = this.events.slice(0),
			i;
		
		for (i = 0; i < copyEvents.length; i++) {
			this.killEvent( copyEvents[i] );	
		}
	},
	
	/**
	 * Clear custom event handlers
	 * @param {string} eventName
	 */
	clearEvent: function( eventName ) {		
		for (var i = this.listeners.length; i--; ) {
			if ( this.listeners[i].name == eventName ) {
				this.unbindHn( eventName, this.listeners[i].callHandler );
				this.listeners.splice( i, 1 );
			}
		}	
	},
	
	/**
	 * Remove a particular custom event handler
	 * @param {string} eventName
	 * @param {function} callHandler
	 */
	clearEventHandler: function( eventName, callHandler ) {		
		for (var i = this.listeners.length; i--; ) {
			if ( this.listeners[i].name == eventName && this.listeners[i].callHandler === callHandler ) {
				this.unbindHn( eventName, callHandler );
				this.listeners.splice( i, 1 );
			}
		}	
	},	
	
	stopEvent: function(e) {
		this.stopPropagation(e);
		this.preventDefault(e);
	},
	
	stopPropagation: function(e) {
		e = this.getEvent(e);
		if (e && e.stopPropagation) {
			e.stopPropagation();
		} else if (e) {
			e.cancelBubble = true;
		}
	},
	
	preventDefault: function(e) {
		e = this.getEvent(e);
		if (e && e.preventDefault) {
			e.preventDefault();
		} else if (e) {
			e.returnValue = false;
		}
	},
	
	getEvent: function(e) {
		return e || window.event;
	},
	
	getTarget: function(e) {
		if (e) {
			return e.target || e.srcElement;
		}
	},
	
	/**
	 * Fire handlers for a particular event with parameters passed 
	 * (except of the first one, the event's name).
	 * @param {String} eventName
	 */
	fireEvent: function( eventName ) {
		var listeners = this.getListeners( eventName );
		for (var i = 0; i < listeners.length; i++) {
			// null - instead of event object
			listeners[i].callHandler.apply(this, [ null ].concat( Array.prototype.slice.call(arguments, 1) ));
		}
	},
	
	/**
	 * Get the listeners for a particular event
	 * @param {string} eventName
	 * @return {array}
	 */
	getListeners: function( eventName ) {
		var listeneresArr = [];
		for (var i = 0; i < this.listeners.length; i++) {
			if ( this.listeners[i].name == eventName ) {	
				listeneresArr.push( this.listeners[i] );
			}
		}
		return listeneresArr;
	}
});
/**
 * Global validtion object checking controls' values
 * @type {object}
 */
Runner.validation = {
	/**
	 * The object containing validators' types
	 */
	validatorConsts: {
		predefined: 1,
		user: 2,
		notFound: 3,
		userNew:4
	},


	newUserValidators: {},

	/**
	 * The predefined validators' names
	 */
	predefinedValidatorsArr: [
		'isrequired',
		'isnumeric',
		'ispassword',
		'isemail',
		'ismoney',
		'iszipcode',
		'isphonenumber',
		'isstate',
		'isssn',
		'iscc',
		'istime',
		'regexp',
		'denyduplicated',
		'checkstrongpassword',
		'datevalidation',
		'acceptfiletypes'
	],

	/**
	 * Array of names of states for validate
	 */
	arrStates: ['AL','AK','AS','AZ','AR','CA','CO','CT','DE','DC','FM','FL','GA','GU','HI','ID','IL','IN','IA','KS','KY','LA','ME','MH','MD','MA','MI','MN','MS','MO','MT','NE','NV',
				'NH','NJ','NM','NY','NC','ND','MP','OH','OK','OR','PW','PA','PR','RI','SC','SD','TN','TX','UT', 'VT','VI','VA','WA','WV','WI','WY'],

	/**
	 * The object contained the current control object beeing used for the validation
	 */
	control: {},

	/**
	 * Main function that provides object validation
	 * @param {array} validArr
	 * @param {object} control
	 * @return {object}
	 */
	validate: function( validArr, control ) {
		var validationRes, validatorName, validatorType, i,
			result = this.getTrueValidationResult( validArr );

		this.control = control;

		//traverse all the field's validators
		for (i = 0; i < validArr.length; i++)  {
			// to prevent check for undefined values, that mistically appears in IE!
			if ( !validArr[i] ) {
				continue;
			}
			validatorName = validArr[i];
			// get type of validator
			validatorType = this.getValidatorType( validatorName );

			switch ( validatorType ) {
				case this.validatorConsts.predefined:
					// validation method is one of the predefined validators
					validationRes = this[ validatorName ]( control.getValueToValidate() );
				break;

				case this.validatorConsts.userNew:
					// validation method is set by user
					validationRes = this.newUserValidators[validatorName].validate( control.getValueToValidate(), control );
					if ( validationRes === false ) {
						validationRes = this.newUserValidators[validatorName].message;
					}
				break;

				default:
					// the validator is not found
					validationRes = true;
			};
			// update validations result object
			this.updateResultObject(validationRes, validatorName, result);
		}

		return result;
	},

	/**
	 * Get the initial result validation object structure
	 * that is similar to a validation result object for
	 * an object that passed validation
	 * @param {array} validArr
	 * @return {object}
	 */
	getTrueValidationResult: function( validArr ) {
		return {
			result: true,
			messagesData: {},
			delayedValidators: this.getDelayedValidators( validArr )
		};
	},

	/**
	 * Construct the delayed validators list for the control
	 * @param {Array} validArr
	 * @return {Array}
	 */
	getDelayedValidators: function( validArr ) {
		if ( $.inArray( "DenyDuplicated", validArr ) === -1 ) {
			return [];
		}
		return [ "DenyDuplicated" ];
	},

	/**
	 * Check validator function type.
	 * @param {string} validatorName
	 * @return {string} property from validatorConsts object
	 */
	getValidatorType: function( validatorName ) {
		var userValidatorType;

		if ( $.inArray( validatorName.toString().toLowerCase(), this.predefinedValidatorsArr ) !== -1 ) {
			return this.validatorConsts.predefined;
		}

		if ( typeof this.newUserValidators[ validatorName ] != "undefined" ) {
			return this.validatorConsts.userNew;
		}

		return this.validatorConsts.notFound;
	},

	/**
	 * Extract the validator's error message form the messageData prop
	 * of the validation result object
	 * @param {string} validatorName
	 * @param {object} messagesData
	 * @return {string}
	 */
	extractValidationMessage: function( validatorName, messagesData ) {
		if ( !messagesData[ validatorName ] ) {
			return ""
		}
		return messagesData[ validatorName ].join("<br>");
	},

	/**
	 * Get an updated result object
	 * @param {mixed} res result from any validation function true, or error text
	 * @param {object} validatorName
	 * @param {object} resultObj final result object
	 */
	updateResultObject: function( res, validatorName, resultObj ) {
		if (res === true) {
			return;
		}
		var messages = [];

		resultObj.result = false;
		// if res is array of messages, add each message to array
		if ( Runner.isArray( res ) ) {
			for (var i = 0; i < res.length; i++) {
				messages.push( res[i] );
			}
		} else {
			messages.push( res );
		}

		resultObj.messagesData[ validatorName ] = messages;
	},

	/**
	 * The handler loading custom validation functions from files.
	 * @param {object} ctrl
	 */
	registerCustomValidation: function( ctrl ) {
		var validatorType, i;

		// loop for all validations
		for (i = 0; i < ctrl.validationArr.length; i++) {
			// to prevent check undefined vals
			if ( !ctrl.validationArr[i] ) {
				continue;
			}

			// get validator type
			validatorType = this.getValidatorType( ctrl.validationArr[i] );
			if ( validatorType === this.validatorConsts.predefined ) {
				//current validator is predefined
				continue;
			}

		}
	},

	"IsRequired": function() {
		if ( this.control.isEmpty() ) {
			return Runner.lang.constants.INLINE_FIELD_REQUIRED;
		}
		return true;
	},

	"IsNumeric": function(sVal)	{
		sVal = sVal.replace(/,/g,"");
		if ( isNaN(sVal) ) {
			return Runner.lang.constants.INLINE_FIELD_NUMBER;
		}
		return true;
	},

	"IsPassword": function(sVal) {
		var regexp1 = /^password$/,
			regexp2 = /.{4,}/;

		if ( sVal.match(regexp1) ) {
			return Runner.lang.constants.INLINE_FIELD_PASSWORD1;
		}
		if ( !sVal.match(regexp2) ) {
			return Runner.lang.constants.INLINE_FIELD_PASSWORD2;
		}
		return true;
	},

	"IsEmail": function(sVal) {
		/**
		 *	- only one @ in the address;
		 *	- address can't start with the space, '@' or '.';
		 *	  besides that any characters are allowed in the local part;
		 *	- domain part can have only alpahnumeric chars, hyphen and dot;
		 *	  domain part can't start or end with a dot;
		 *    two dots in a row are not allowed.
		 */

		var regexp = /^[^\@\.\s][^\@]*\@[_\-\w]+(\.[_\w-]+)+$/;
		if ( sVal.match(/.+/) && !sVal.match( regexp ) ) {
			return Runner.lang.constants.INLINE_FIELD_EMAIL;
		}
		return true;
	},

	"IsMoney": function(sVal) {
		var regexp = /^(\d*)[.,]?(\d*)$/;
		if ( sVal.match(/.+/) && !sVal.match(regexp) ) {
			return Runner.lang.constants.INLINE_FIELD_CURRENCY;
		}
		return true;
	},

	"IsZipCode": function(sVal) {
		var regexp = /^\d{5}([\-]\d{4})?$/;
		if ( sVal.match(/.+/) && !sVal.match(regexp) ) {
			return Runner.lang.constants.INLINE_FIELD_ZIPCODE;
		}
		return true;
	},

	"IsPhoneNumber": function(sVal)	{
		var regexp = /^\(\d{3}\)\s?\d{3}\-\d{4}$/,
			stripped = sVal.replace(/[\(\)\.\-\ ]/g, '');

		if ( sVal.match(/.+/) && ( isNaN( parseInt(stripped) ) || stripped.length != 10 ) ) {
			return Runner.lang.constants.INLINE_FIELD_PHONE;
		}
		return true;
	},

	"IsState": function(sVal) {
		if ( sVal.match(/.+/) && $.inArray( sVal, this.arrStates ) === -1 ) {
			return Runner.lang.constants.INLINE_FIELD_STATE;
		}
		return true;
	},

	"IsSSN": function(sVal)	{
		// 123-45-6789 or 123 45 6789
		var regexp = /^\d{3}(-|\s)\d{2}(-|\s)\d{4}$/;
		if ( sVal.match(/.+/) && !sVal.match(regexp) ) {
			return Runner.lang.constants.INLINE_FIELD_SSN;
		}
		return true;
	},

	"IsCC": function(sVal) {
		//Visa, Master Card, American Express, Discover, JCB
		var regexp = /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/;
		if ( sVal.match(/.+/) && !sVal.match(regexp) ) {
			return Runner.lang.constants.INLINE_FIELD_CC;
		}
		return true;
	},

	"IsTime": function(sVal) {
		if ( sVal === "" ) {
			return true;
		}
		var common24Rgxp = /^(?:[0-1]?[0-9]|2[0-3])(?::[0-5][0-9](?::[0-5][0-9])?)?$/,
			common12Rgxp = /^(?:1[0-2]|0?[1-9])(?::[0-5][0-9](?::[0-5][0-9])?)?[\s]{0,2}(?:am|pm)?$/i,
			localTimeRgxp;

		sVal = sVal.trim();

		//check if the contol's value matches the 24 hours or 12 hours common format
		if ( common24Rgxp.test(sVal) || common12Rgxp.test(sVal) ) {
			return true;
		}

		localTimeRgxp = new RegExp( this.control.regExp, "i" );
		//check if the contol's value matches the local time format
		if ( localTimeRgxp.test(sVal) ) {
			return true;
		}

		return Runner.lang.constants.INLINE_FIELD_TIME;
	},

	"RegExp": function(sVal) {
		if (sVal === "") {
			return true;
		}

		var re = new RegExp( this.control.regExp ),
			regExpMessageData = this.control.customValidationFailedMessages[ "RegExp" ];

		// test against regExp
		if ( !re.test(sVal) || re.exec(sVal)[0] != sVal ) {
			// return error text
			if ( regExpMessageData.type === 0 ) {
				return regExpMessageData.text;
			}
			
			/* old format, support for some custom code */ 
			if( regExpMessageData.messageType == 'Text'  ) {
				return regExpMessageData.message;
			}
			
			return Runner.getCustomLabel( regExpMessageData.label );
		}

		return true;
	},

	"DenyDuplicated": function( sVal ) {
		var	control = this.control,
			pageObj = Runner.pages.PageManager.getAt( control.table, control.id ), 
			messageData, ajaxParams;

		var backToDefaultValue = false;
		if ( control.pageType === Runner.pages.constants.PAGE_EDIT || control.pageType === Runner.pages.constants.PAGE_USERINFO ) {
			backToDefaultValue = sVal === control.getDefaultControlValueToValidate();
		}
			
		if ( control.isEmpty() || backToDefaultValue ) {
			if ( control.isInvalid ) {
				control.clearInvalid();
			}			
			if ( pageObj ) {
				pageObj.fireEvent("ddValidationResolved", control.fieldName, false );
			}
			return true;
		}

		var displayValue = control.getDisplayValue();
		// get the custom validation error message's data
		messageData = control.customValidationFailedMessages[ "DenyDuplicated" ];
		//It helps the validation process be invoked by submit to run this validator
		control.isInvalid = false;

		ajaxParams = {
			tableName: control.shortTableName,
			pageType: control.pageType,
			page: control.pageObj.pageName,
			fieldName: control.fieldName,
			fieldControlType: control.ctrlType,
			value: sVal
		};

		Runner.runnerAJAX( Runner.getPageUrl("checkduplicates"), ajaxParams, function( respObj ) {
			var message;
			
			// drop obsolete responses for spreadsheet mode
			if ( sVal !== control.getValueToValidate() 
				|| control.pageObj.spreadsheet && sVal === control.getDefaultControlValueToValidate() ) {
				return;
			}

			if ( !respObj.success ) {
				return;
			}

			if ( pageObj ) {
				pageObj.fireEvent("ddValidationResolved", control.fieldName, respObj.hasDuplicates );
			}

			if ( !respObj.hasDuplicates ) {
				if ( !control.isInvalid ) {
					control.clearInvalid();
				}
				return;
			}

			message = messageData.type === 0 ? messageData.text : Runner.getCustomLabel( messageData.label );
			message = message.replace( '%value%', Runner.escapeHtml( displayValue.slice(0, 100) ) );

			control.markInvalidWidthCusttomMessages( "DenyDuplicated", [ message ] );

			if ( control.isFocusAllowed() && !control.isSetFocus && Runner.pages.RunnerPage.prototype.isFirstInvalidControl( control ) ) {
				control.setFocus();
			}
		});

		return true;
	},

	"DateValidation": function() {
		return this.control.validateDate();
	},

	"CheckStrongPassword": function(sVal) {
		this.control.clearInvalid();

		if ( !sVal.length ) {
			return true;
		}

		var messages = [],
			pwdLen = Runner.pages.PageSettings.getGlobalData("pwdLen"),
			pwdUnique = Runner.pages.PageSettings.getGlobalData("pwdUnique"),
			pwdDigits = Runner.pages.PageSettings.getGlobalData("pwdDigits"),
			pwdUpperLower = Runner.pages.PageSettings.getGlobalData("pwdUpperLower");

		if ( pwdLen && sVal.length < pwdLen ) {
			messages.push( Runner.lang.constants.SEC_PWD_LEN.replace('%%', pwdLen) );
		}

		if ( pwdUnique ) {
			var cUnique = {},
				countUnique = 0;

			for (var i = 0; i < sVal.length; i++) {
				cUnique[sVal[i]] = 1;
			}
			for (var c in cUnique) {
				countUnique++;
			}
			if (countUnique < pwdUnique) {
				messages.push( Runner.lang.constants.SEC_PWD_UNIQUE.replace('%%', pwdUnique) );
			}
		}

		if ( pwdDigits ) {
			var regExpDigit =  /(\d)|([^A-Za-z0-9])/g,
				result = sVal.match(regExpDigit);

			if (!result || result.length < pwdDigits) {
				messages.push( Runner.lang.constants.SEC_PWD_DIGIT.replace('%%', pwdDigits) );
			}
		}

		if ( pwdUpperLower ) {
			var regExpLower = /([a-z])/g,
				regExpUpper = /([A-Z])/g;

			if (( !sVal.match(regExpLower) || !sVal.match(regExpUpper) ) && pwdUpperLower) {
				messages.push( Runner.lang.constants.SEC_PWD_CASE );
			}
		}

		if ( messages.length ) {
			return messages;
		}
		return true;
	},

	"AcceptFileTypes": function( sVal ) {
		var re = new RegExp(this.control.acceptFileTypes || ".+$", "i");

		var pos = Math.max(sVal.lastIndexOf('\\'),	sVal.lastIndexOf('/'));
		var fileName = sVal.substr(pos+1);

		if (fileName && !re.test(fileName)) {
			return Runner.lang.constants.ERROR_ACCEPT_FILE_TYPES;
		}

		return true;
	},

	/**
	 *	Create custom validation type
	 *	parameters:
	 *		"name" (required string) - unique name of the validator. Will be used in control.addValidation(name), removeValidation(name)
	 *		"message" (optional string) - 'field is invalid' message
	 *		"validate" (required function) - function( value, ctrl ). Returns true if control is valid. Returns false or validation message. params.message will be displayed is false is returned
	 */
	addUserValidator: function( params ) {
		this.newUserValidators[ params.name ] = params;
	}

}
Runner.controls.ControlManager = function() {
	
	return {
		/**
		 * Register a control
		 * @param {Object} control
		 */
		register: function( control ) {
			if (!control) {
				return false;
			}
			return Runner.controls.ControlStorage.register( control.id, control.table, control.fieldName, control.ctrlInd, control);
		},

		/**	
		 * DEPRECATED
		 * For legacy purposes only ( business templates )
		 *
		 * Returns control or array of controls by following params
		 * @param {string} tName
		 * @param {string} rowId Pass false or null to get all controls of the table
		 * @param {string} fName Pass false or null to get all controls of the row
		 * @param {int} ctrlInd Pass false or null to get first control of the field
		 * @return {object} return control, array of controls or false
		 * @intellisense
		 */
		getAt: function( tName, rowId, fName, ctrlInd ) {
			if ( typeof fName == "undefined" || fName === false ) {
				//	return list of fields
				if ( !rowId ) {
					return Runner.controls.ControlStorage.byTable( tName );
				}

				return Runner.controls.ControlStorage.byId( rowId );
			}
			
			ctrlInd = ctrlInd || 0;
			return Runner.controls.ControlStorage.byAll( rowId, fName, ctrlInd || 0 );
		},
	};
}();


/**
 *	This object stores control objects. Objects accessible by 4 coordinates:
 *	table - table name
 *	id - page id/row id. 
 *	field - field name
 *  ctrlInd - 0 or 1. 1 is only used for search Between controls
 *	
 *	Typical queries are
 *	- get all controls for given id
 *	- get all controls for given table (sic!)
 *	- get specific control by all 4 coordinates
 *	- get control by id & fieldname ( ctrlidx assumed 0 )
 */
  
Runner.controls.ControlStorage = (function() {
	
	var idxMain = {},	//	id/field/ctrlInd
		idxIdField = {},
		idxIdFIELD = {},
		idxTableId = {};	//	table/id/field/ctrlInd
	
	return {
		register: function( id, table, field, ctrlInd, control ) {			
			idxMain[ id ] = idxMain[ id ] || {};
			idxMain[ id ][ field ] = idxMain[ id ][ field ] || {};
			idxMain[ id ][ field ][ ctrlInd ] = control;

			idxTableId[ table ] = idxTableId[ table ] || {};
			idxTableId[ table ][ id ] = idxMain[ id ];

			
			if ( !ctrlInd ) {
				idxIdField[ id ] = idxIdField[ id ] || {};
				idxIdField[ id ][ field ] = control;
				
				idxIdFIELD[ id ] = idxIdFIELD[ id ] || {};
				idxIdFIELD[ id ][ field.toUpperCase() ] = control;
			}
		},
		unregister: function( id, field, ctrlInd ) {
			var control = this.byAll( id, field, ctrlInd );
			
			if ( !control ) {
				console.log('error deleting control');
				return;
			}
			
			if ( typeof control.destructor === "function" ) {
				control.destructor();
			}
			
			delete idxMain[ id ][ field ][ ctrlInd ];
			
			if ( 0 == ctrlInd ) {
				delete idxIdField[ id ][ field ];
				delete idxIdFIELD[ id ][ field.toUpperCase() ];
			}
		},
		
		unregisterById: function( id ) {
			var field, ctrlInd, controls = [],
				objField;
			
			for( field in idxMain[ id ] ) {
				objField = idxMain[ id ][ field ];
				for( ctrlInd in objField ) 
					this.unregister( id, field, ctrlInd );
			}
			delete idxMain[ id ];
			return;
		},
		
		/**
		 *	the only case-insensitive query
		 */
		byIdField: function( id, field ) {
			if( !field ) {
				return undefined;
			}
			return ( idxIdField[ id ] || {} )[ field ] || ( idxIdFIELD[ id ] || {} )[ field.toUpperCase() ];
		},
		
		byTable: function( table ) {
			var id, field, ctrlInd, controls = [],
				objId, objField;
			
			for( id in idxTableId[ table ] || {} ) {
				objId = idxTableId[ table ][ id ];
				for( field in objId ) {
					objField = objId[ field ];
					for( ctrlInd in objField ) 
						controls.push( objField[ ctrlInd ] );
				}
			}
			return controls;
		},
		
		byId: function( id ) {
			var field, ctrlInd, controls = [],
				objField;
			
			for( field in idxMain[ id ] ) {
				objField = idxMain[ id ][ field ];
				for( ctrlInd in objField )
					controls.push( objField[ ctrlInd ] );
			}
			return controls;
		},
		
		byAll: function( id, field, ctrlInd ) {
			return (( idxMain[ id ] || {} )[ field ] || {} )[ ctrlInd ];
		}


	}
})();// register new namespace
Runner.namespace('Runner.form');
/**
 * @class Runner.form.Button
 * @intellisense
 */
Runner.form.Button = Runner.extend( Runner.Event, {
	/**
	 * Control id
	 * @type {string}
	 */
	id: "",
	
	/**
	 * The button's name
	 * @type {string}
	 */
	btnName: "",
	
	/**
	 * The button's message container
	 * @type {jQuery object}
	 */
	messageCont: null,
	
	/**
	 * jQuery object of button element
	 * @type {jQuery object}
	 */
	elem: null,

	/**
	 * User-supplied parameters for the server part of the button
	 */
	params: {},

	/**
	 * submit function
	 */
	submitHandler: null,

	/**
	 * Current Page object
	 */
	pageObj: null,
	
	constructor: function( cfg ) {
		// copy properties from cfg to obj
		Runner.apply(this, cfg);
		Runner.form.Button.superclass.constructor.call(this, cfg);	

		// get button
		this.elem = $( '[data-event=' + this.id + ']' );
		// create empty button, for correct script work, if button tag not exists 
		this.elem = this.elem.length ? this.elem : $('<input type="button">'); 

		this.elemsForEvent = [ this.elem.get(0) ];
	},
	
	/**
	 * Initialize the button click functionality
	 * @param {object} args		The object containing click handler's extra arguments. 
	 * It has the following structure: { args: [%extra params list%] }
	 */
	init: function( args ) {
		this.on("click", this.clickHandler, args);
	},
	
	/**
	 * Set the button disabled
	 * @intellisense
	 */
	setDisabled: function() {
		if ( this.elem.is("input") ) {
			this.elem.prop("disabled", true);
			return;
		}
		this.suspendEvent( ['click'] );
	},
	
	/**
	 * Set the button enabled
	 * @intellisense
	 */
	setEnabled: function() {
		if ( this.elem.is("input") ) {
			this.elem.prop("disabled", false);
			return;
		}
		this.resumeEvent( ['click'] );	
	},
	
	/**
	 * Set the message to the button's message container
	 * @param {string} txt
	 * @intellisense
	 */
	setMessage: function( txt ) {
		this.initMessCont();

		this.setMessage = function(txt) {
			this.messageCont.html( txt );
		}
		this.setMessage( txt );
	},
	
	/**
	 * Clear the button's message container
	 * @intellisense
	 */
	removeMessage: function() {
		this.initMessCont();

		this.removeMessage = function() {
			this.messageCont.empty();
		}
		this.removeMessage();
	},
	
	/**
	 * Initialize the button's message container
	 * @intellisense
	 */
	initMessCont: function() {
		if ( this.messageCont ) {
			return;
		}
		
		var messContId = this.id + "_messCont";
		this.messageCont = $('<div id="' + messContId + '"></div>').insertAfter( this.elem );
	},
	
	/**
	 * @param {object} pageObj
	 * @param {boolean} isInlineAdd
	 * @return {object}
	 */
	getRowData: function( pageObj, isInlineAdd ) {
		var i, addedRow,
			$button = this.elem,
			$row = $button.closest('[id^="gridRow"]'),
			$cell = $button.closest('[data-record-id]'),			
			rowData = {
				id: -1, 
				keys: [], 
				fields: {}
			};

		if ( $cell.length ) {
			//	direct method first
			rowData.id = $cell.data('record-id');
		} else {
			//	indirect method, inspect parents and neighbors
			if ( !$row.length ) {
				return rowData;
			}
			
			rowData.id = parseInt( $row.attr('id').substr(7), 10 );
			$row.children().each( function() {
				if (this.recordId) {
					rowData.id = this.recordId;
				}
				if ( $button.closest(this).length ) {
					return false;
				}
			});
		}
		
		if ( rowData.id ) {
			if ( typeof pageObj.controlsMap.gridRows !== 'undefined' && !isInlineAdd ) {
				for (i = 0; i < pageObj.controlsMap.gridRows.length; i++) {
					if (pageObj.controlsMap.gridRows[i].id == rowData.id) {
						rowData.keys = pageObj.controlsMap.gridRows[i].keys;
						break;
					}
				}
			}
			if (isInlineAdd) {
				addedRow = pageObj.inlineAdd.getRowById(rowData.id);
				rowData.keys = addedRow.keys;
			}
			if ( typeof pageObj.listFields !== 'undefined' ) {
				for (i = 0; i < pageObj.listFields.length; i++) {
					rowData.fields[pageObj.listFields[i]] = Runner.getFieldSpan(pageObj.listFields[i], rowData.id);
				}
			}
		} else {
			rowData.id = -1;
		}
		
		return rowData;
	},
	
	/**
	 * @param {object} pageObj
	 * @param {number} rowId
	 * @return {object}
	 */ 
	getInlineRow: function( pageObj, rowId ) {
		if (  pageObj.inlineAdd || pageObj.inlineEdit ) {
			return ( pageObj.inlineAdd || pageObj.inlineEdit ).getRowById( rowId );
		}
		//attempt
		var inlineEdit = new Runner.util.inlineEditing.InlineEdit({
			id: pageObj.pageId,
			tName: pageObj.tName,
			fNames: pageObj.listFields || [],
			rows: pageObj.controlsMap && pageObj.controlsMap.gridRows || []
		});
	
		return inlineEdit.getRowById( rowId );
	},
	
	/**
	 * The button's 'click' handler
	 * @param {Event object} e
	 * @param {object} pageObj
	 * @param {object} proxy
	 * @param {string} pageid
	 * @param {boolean} isInlineAdd 	A button was added to a new inline-added grid row
	 * @param {object} inlineAddRow (optional)
	 */
	clickHandler: function( e, pageObj, proxy, pageid, isInlineAdd, inlineAddRow ) {
		this.pageObj = pageObj;
		if ( Runner.isDisabledButton( this.elem ) ) {
			e.preventDefault();
			e.stopPropagation();
			return;
		}
		
		var ctrl = this,
			ajax = ctrl,
			rowData = this.getRowData( pageObj, isInlineAdd ),
			// create ajax params, before executing user code
			params = {
				buttId: this.btnName,
				rndVal: new Date().getTime(),
				fieldsData: {},
				masterData: {},
				masterTable: pageObj.masterTable || null,
				table: pageObj.pageTable,
				page: pageObj.pageName
			}, 
			isManyKeys = 0,
			keyObject = {}, 
			location, res, $checkboxes, i, j, reqParams,
			row = null,
			recordControls = [],
			masterPageObj = null,
			submit;
			
		if ( pageObj.masterTable) { 
			var $masterGrid = pageObj.findItemType( "master_info" );
			if ( !!$masterGrid.find( this.elem ).length ) {
				params.table = pageObj.masterTable;
				params.page = pageObj.pageData["masterPageName"];
				
				location = "grid";
				keyObject = pageObj.pageData["masterPageKeys"];
			}
		}
		
		if ( inlineAddRow || rowData.id !== -1 ) {
			row = new Runner.AjaxRow( pageObj, inlineAddRow || this.getInlineRow( pageObj, rowData.id ) );
			recordControls = Runner.controls.ControlStorage.byId( row.row.id );
			location = "grid"; 
		} else if ( pageObj.pageType == Runner.pages.constants.PAGE_EDIT || pageObj.pageType == Runner.pages.constants.PAGE_ADD ) {
			recordControls = pageObj.getFormControls();
		}

		// Button is located in record add/edit form
		if ( recordControls.length ) {
			$.each( recordControls , function(i, control) {
				params.fieldsData[ control.fieldName ] = control.getValue();
			});	
		}
		
		// Button is located in details grid
		if ( pageObj.pageMode === Runner.pages.constants.MODE_LIST_DETAILS ) {
			masterPageObj = Runner.pages.PageManager.getAt( pageObj.masterTName, pageObj.parId );
			// master - ADD/EDIT mode
			if ( masterPageObj.pageType === Runner.pages.constants.PAGE_ADD || masterPageObj.pageType === Runner.pages.constants.PAGE_EDIT ) {
				$.each( masterPageObj.getFormControls() , function(i, control) {
					params.masterData[ control.fieldName ] = control.getValue();
				});			
			}
		}
		var submitted = false;
		submit = function() {
			if ( submitted ) {
				return;
			}
			submitted = true;
			// add key values for edit and view page
			if ( pageObj.keys ) {
				keyObject = pageObj.keys;
				location = pageObj.pageType;
			}
			// add selected recs values code for list page button handlers
			// !!!!!!!!!!!! for edit, view page with details, work not correct !!!!!!!!!!!!! 
			else if ( $('input[type=checkbox][name^=selection]', pageObj.pageCont).length ) {
				$checkboxes = $('input[type="checkbox"][id^="check"][name^="selection"]:checked', pageObj.pageCont);
			
				if ( $checkboxes.length ) {
					location = pageObj.pageType;
					isManyKeys = 1; 
					j = 0;
					
					if ( rowData.keys.length ) {
						keyObject[ j++ ] = rowData.keys.join('&');
						location = 'grid';
					} 
					
					$checkboxes.each( function( idx, checkbox) {
						keyObject[ j++ ] = checkbox.value;
					})
				}
			}
			// add keys for current grid row
			if ( !isManyKeys && rowData.keys.length ) {
				keyObject = rowData.keys;
				location = 'grid';
			}

			// send request and handle it
			reqParams = {
				params: JSON.stringify( params ), 
				keys: JSON.stringify( keyObject ), 
				isManyKeys: isManyKeys,
				location: location || ""
			};
			
			if ( pageObj.masterTable ) {
				reqParams.masterTable = pageObj.masterTable;
				reqParams = Runner.apply( reqParams, pageObj.masterKeys );
			}
				
			$.post( Runner.getPageUrl("buttonhandler"), reqParams, function( result ) {
				var _result;
				
				try {
					_result = JSON.parse( result );
				} catch(e) {				
					Runner.displayGenericAjaxError( result );				
				}
				
				// execute EventAfter code
				pageObj.buttonEventAfter[ ctrl.btnName ]( _result, ctrl, pageObj, proxy, pageid, rowData, row, params );
				ctrl.setEnabled();
			})
			.fail( function( jqXHR, textStatus, errorThrown ) {
				Runner.displayGenericAjaxError( jqXHR.responseText || textStatus + ' ' + errorThrown  );
			});
		}
		
		// set button disabled before executing code
		this.setDisabled();
		// execute code before

		ctrl.params = params;
		ctrl.submitHandler = submit;
		
		//	alias for API
		ctrl.submit = submit;

		res = pageObj.buttonEventBefore[ this.btnName ]( params, ctrl, pageObj, proxy, pageid, rowData, row, submit );
		
		if ( res === false ) {
			// if in EventBefore code returned false, we need to set the button enabled
			this.setEnabled();
			e.preventDefault();
			e.stopPropagation();
			return;
		}
		submit();		
		
		e.preventDefault();
		e.stopPropagation();
	},

	dialog: function( params ) {
		return Runner.Dialog( params, this );
	},

	addPDF: function( name, params, callback ) {
		var button = this;
		
		Runner.PDF.create( params, this.pageObj, function( pdf ) {
			pdf.getBase64().then( function( blob ) {
				button.params[ name ] = blob;
				button.params._base64fields = button.params._base64fields || [];
				button.params._base64fields.push( name );
				if ( callback ) {
					return callback();
				} else {
					button.submit();
				}
			});
		});
		return false;
	},
});	
/**
 * InlineEditor row object wrapper
 * @constructor
 * @param {object} inlineRow
 */
Runner.AjaxRow = function( pageObj, inlineRow ) {
	this.row = inlineRow;
	this.pageObj = pageObj;
	
	if ( !inlineRow ) {
		// A stub
		this.row = {
			id: 1,
			gridLayout: ""
		};
	}
}

Runner.AjaxRow.prototype = {
	/**
	 * @param {string} fName
	 * @return {jQuery object}
	 */
	fieldCell: function( fName ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );

		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT ) {
			return $span.closest("div");
		}
		
		return $span.closest("[data-field]");
	},
	
	/**
	 * @param {string} fName
	 * @return {string}
	 */
	getFieldText: function( fName ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		return $span.html();
	},
	
	/**
	 * @param {string} fName
	 * @param {string} text
	 */	
	setFieldText: function( fName, text ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		$span.html( text );		
	},
	
	/**
	 * @param {string} fName
	 * @return {mixed}
	 */
	getFieldValue: function( fName ) {
		var ctrl = Runner.getControl( this.row.id, fName );
		if ( ctrl ) {
			return ctrl.getValue();
		}
		if ( this.row.rowVals && typeof this.row.rowVals[ fName ] != 'undefined' ) {
			return this.row.rowVals[ fName ];
		}
		
		var $span = Runner.getFieldSpan( fName, this.row.id );
		return ($span.attr("val") || "").trim();		
	},
	
	/**
	 * @param {string} fName
	 * @param {mixed} value
	 */	
	setFieldValue: function( fName, value ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		$span.attr("val", value);		
	},
	
	/**
	 * @return {jQuery object}
	 */
	record: function() {
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			return $(".panel-body", "[data-record-id='" + this.row.id + "']");
		}	
		return $("[data-record-id='" + this.row.id + "'][data-field]"); //layout dep ?
	},
	
	/**
	 * @return {jQuery object}
	 */
	getDefaultMessageCell: function() {
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			return this.record();
		}
		
		return this.record().eq(0);
	},
	
	/**
	 * @param {string} message
	 * @param {mixed} container
	 */
	setMessage: function( message, container ) {
		var $messageWrap;
		
		if ( !container && this.row.messageContainer ) {
			this.row.messageContainer.html( message );
			return;	
		}

		container = container || this.getDefaultMessageCell();
		$messageWrap = $(".row-message", container);
			
		if ( $messageWrap.length ) {
			this.row.messageContainer = $messageWrap.html( message );
			return;
		}
		
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			this.row.messageContainer = $("<div class='row-message'></div>").html( message ).prependTo( container );
		} else {
			this.row.messageContainer = $("<span class='row-message'></span>").html( message ).appendTo( container );
		}		
	},
	
	/**
	 *
	 */
	getMessage: function() {
		if ( this.row.messageContainer ) {
			return this.row.messageContainer.html();
		}
		
		return "";
	},
	
	/**
	 * @return {string}
	 */
	id: function() {
		return this.row.id;
	},
	
	recordId: function() {
		return this.row.id;
	},

	/**
	 * @return {object}
	 */
	getRowObject: function() {
		return this.row;
	},

	/**
	 * @return {Array} - array of key field values
	 */
	getKeys: function() {
		return this.row.keys;
	},

	getDetails: function() {
		return this.row.detPage;
	}

};
	
/**
 * @param {object} baseCfg
 * @param {string} pageType
 * @param {boolean} isInline
 */
Runner.controls.ControlFactory = function( baseCfg, pageType, isInline, pageObj ) {
	// adjust pageType value for search controls
	pageType = baseCfg.mode == Runner.controls.constants.MODE_SEARCH ? Runner.pages.constants.PAGE_SEARCH : pageType;
	if ( ( baseCfg.mode === "inline_edit" || baseCfg.mode === Runner.controls.constants.MODE_INLINE_EDIT ) 
			&& pageType === Runner.pages.constants.PAGE_LIST ) {
		pageType = Runner.pages.constants.PAGE_EDIT;
	}

	var tName = pageObj.tName,
		fName = baseCfg.fieldName,
		cfg = {
			rowId: baseCfg.id,
			table: tName,
			pageObj: pageObj,
			isInline: isInline,
			pageType: pageType,
			goodFieldName: Runner.goodFieldName( baseCfg.fieldName ),
			shortTableName: Runner.pages.PageSettings.getShortTName( tName ),
			editFormat: Runner.pages.PageSettings.getEditFormat( tName, fName, pageType ),
			validation: Runner.pages.PageSettings.getValidations( tName, fName, pageType ),
			ajaxLikeEventsData: Runner.pages.PageSettings.getFieldData( tName, fName, "events", pageType )
		};

	// add baseCfg's properties to cfg replacing props with the same name
	cfg = Runner.apply( cfg, baseCfg );

	switch ( cfg.editFormat ) {
		case Runner.controls.constants.EDIT_FORMAT_NONE:
		case Runner.controls.constants.EDIT_FORMAT_PASSWORD:
		case Runner.controls.constants.EDIT_FORMAT_TEXT_FIELD:
			cfg.mask = Runner.pages.PageSettings.getFieldData( tName, fName, "mask", pageType );
			return new Runner.controls.TextField( cfg );

		case Runner.controls.constants.EDIT_FORMAT_TEXT_AREA:
			if ( cfg.RTEType === undefined ) {
				cfg.RTEType = Runner.pages.PageSettings.getFieldData( tName, fName, "RTEType", pageType );
			}
			switch (cfg.RTEType) {
				case Runner.controls.constants.EDIT_FORMAT_RTE:
					return new Runner.controls.RTETinyMCE( cfg );

				case Runner.controls.constants.EDIT_FORMAT_RTEINNOVA:
					//cfg.useRTE = "INNOVA";
					return new Runner.controls.RTEInnova( cfg );

				case Runner.controls.constants.EDIT_FORMAT_RTECK:
					return new Runner.controls.RTECK( cfg );
					
				case Runner.controls.constants.EDIT_FORMAT_RTECK_NEW:
					return new Runner.controls.RTECK5( cfg );	

				default:
					return new Runner.controls.TextArea( cfg );
			}

		// different date controls
		case Runner.controls.constants.EDIT_FORMAT_DATE:
			if ( cfg.dateEditType === undefined ) {
				cfg.dateEditType = Runner.pages.PageSettings.getFieldData( tName, fName, "dateEditType", pageType );
			}
			cfg.ctrlType = "date" + cfg.dateEditType;
			switch ( cfg.dateEditType ) {
				case Runner.controls.constants.EDIT_DATE_SIMPLE_INLINE:
					cfg.useDatePicker = true;
					return new Runner.controls.DateTextField( cfg );

				case Runner.controls.constants.EDIT_DATE_SIMPLE_DP:
					cfg.useDatePicker = true;
					return new Runner.controls.DateTextField( cfg );

				case Runner.controls.constants.EDIT_DATE_DD:
					return new Runner.controls.DateDropDown( cfg );

				case Runner.controls.constants.EDIT_DATE_DD_INLINE:
					cfg.useDatePicker = true;
					return new Runner.controls.DateDropDown( cfg );

				case Runner.controls.constants.EDIT_DATE_DD_DP:
					cfg.useDatePicker = true;
					return new Runner.controls.DateDropDown( cfg );

				case Runner.controls.constants.EDIT_DATE_SIMPLE:
				default:
					return new Runner.controls.DateTextField( cfg );
			}

		case Runner.controls.constants.EDIT_FORMAT_TIME:
			return new Runner.controls.TimeField( cfg );

		case Runner.controls.constants.EDIT_FORMAT_CHECKBOX:
			if ( cfg.mode == Runner.controls.constants.MODE_SEARCH ) {
				cfg.ctrlType = "checkbox";
				return new Runner.controls.DropDownLookup( cfg );
			}
			return new Runner.controls.CheckBox( cfg );

		case Runner.controls.constants.EDIT_FORMAT_DATABASE_IMAGE:
			cfg.acceptFileTypes = Runner.pages.PageSettings.getFieldData( tName, fName, "acceptFileTypes", pageType );
			return new Runner.controls.ImageField( cfg );

		case Runner.controls.constants.EDIT_FORMAT_DATABASE_FILE:
			cfg.acceptFileTypes = Runner.pages.PageSettings.getFieldData( tName, fName, "acceptFileTypes", pageType );
			return new Runner.controls.FileField( cfg );

		case Runner.controls.constants.EDIT_FORMAT_FILE:
			if ( Runner.pages.PageSettings.getFieldData( tName, fName, "compatibilityMode", pageType ) ) {
				cfg.acceptFileTypes = Runner.pages.PageSettings.getFieldData( tName, fName, "acceptFileTypes", pageType );
				return new Runner.controls.FileField( cfg );
			}
			cfg.acceptFileTypes = Runner.pages.PageSettings.getFieldData( tName, fName, "acceptFileTypes", pageType );
			cfg.maxFileSize = Runner.pages.PageSettings.getFieldData( tName, fName, "maxFileSize", pageType );
			cfg.maxTotalFilesSize = Runner.pages.PageSettings.getFieldData( tName, fName, "maxTotalFilesSize", pageType );
			cfg.maxNumberOfFiles = Runner.pages.PageSettings.getFieldData( tName, fName, "maxNumberOfFiles", pageType );
			cfg.fileStorageProvider = Runner.pages.PageSettings.getFieldData( tName, fName, "fileStorageProvider", pageType );
			return new Runner.controls.MultiUploadField( cfg );

		case Runner.controls.constants.EDIT_FORMAT_LOOKUP_WIZARD:
			cfg.lcType = Runner.pages.PageSettings.getLCT( tName, fName, pageType );
			cfg.lookupTable = Runner.pages.PageSettings.getLookupTable( tName, fName, pageType );

			cfg.parentFieldNames = Runner.pages.PageSettings.getFieldData( tName, fName, "parentFields", pageType ) || [];

			cfg.linkField = Runner.pages.PageSettings.getFieldData( tName, fName, "linkField", pageType );
			cfg.dispField = Runner.pages.PageSettings.getFieldData( tName, fName, "dispField", pageType );

			cfg.multiSelectionAllowed = Runner.pages.PageSettings.getFieldData( tName, fName, "Multiselect", pageType );
			cfg.isHorizontalLookup = Runner.pages.PageSettings.getFieldData( tName, fName, "HorizontalLookup", pageType );
			cfg.autoCompleteFields = Runner.pages.PageSettings.getFieldData( tName, fName, "autoCompleteFields", pageType );

			cfg.masterTName = pageObj.masterTable;
			cfg.masterTId = pageObj.parId;
			cfg.mainFields = Runner.pages.PageSettings.getFieldData( tName, fName, "mainFields", pageType ) || [];
			cfg.mainMasterFields = Runner.pages.PageSettings.getFieldData( tName, fName, "mainMasterFields", pageType ) || [];
			cfg.listPageId = Runner.pages.PageSettings.getFieldData( tName, fName, "listPageId", pageType ) || "";
			cfg.addPageId = Runner.pages.PageSettings.getFieldData( tName, fName, "addPageId", pageType ) || "";


			// parse additional lookup controls params
			switch ( cfg.lcType ) {
				case Runner.controls.constants.LCT_DROPDOWN:
					cfg.selectSize = Runner.pages.PageSettings.getLookupSize( tName, fName, pageType );
					return new Runner.controls.DropDownLookup( cfg );

				case Runner.controls.constants.LCT_AJAX:
					cfg.freeInput = Runner.pages.PageSettings.getFieldData( tName, fName, "freeInput", pageType );
					return new Runner.controls.EditBoxLookup( cfg );

				case Runner.controls.constants.LCT_LIST:
					return new Runner.controls.ListPageLookup( cfg );

				case Runner.controls.constants.LCT_CBLIST:
					cfg.lcSize = 2;
					return new Runner.controls.CheckBoxLookup( cfg );

				case Runner.controls.constants.LCT_RADIO:
					cfg.lcSize = 2;
					return new Runner.controls.RadioControl( cfg );

				default:
					throw ('Invalid lookup wizard type = ' + cfg.lcType + '. Cannot create lookup!');
					return;
			};

		case Runner.controls.constants.EDIT_FORMAT_READONLY:
			return new Runner.controls.ReadOnly( cfg );

		default:
			if ( typeof Runner.controls.constants[ "Edit" + cfg.editFormat ] !== "undefined" ) {
				return new Runner.controls[ "Edit" + cfg.editFormat ]( cfg );
			}
			throw ('Invalid control edit format = ' + cfg.editFormat + '. Cannot create control!');
			return;
	};
};


/**
 * Base abstract class for all controls, should not be created directly
 * @requires runner, ControlManager, validate, Event
 * @class Runner.controls.Control
 */
Runner.controls.Control = Runner.extend( Runner.Event, {
	/**
	 * Name of control
	 * @type string
	 * @intellisense
	 */
	fieldName: "",
	/**
	 * Name used for HTML tags, attrs
	 * @type String
	 * @intellisense
	 */
	goodFieldName: "",
	/**
	 * table name for urls request
	 * @type String
	 * @intellisense
	 */
	shortTableName: "",
	/**
	 * Control id
	 * @type string
	 * @intellisense
	 */
	id: "",

	/**
	 * The host row's/page's id
	 * @type {string}
	 */
	rowId: "",

	/**
	 * A flag indicating if the search panel control is cached.
	 * It's always equal to false for not a search panel control
	 * @type {boolean}
	 */
	cached: false,

	/**
	 * A Type of the page containing the control
	 * @type String
	 */
	pageType: "",

	/**
	 * custom CSS classes
	 * @type string
	 * @intellisense
	 */
	css: "",
	/**
	 * Custom css styles
	 * @type String
	 * @intellisense
	 */
	style: "",
	/**
	 * Value DOM element id
	 * @type string
	 * @intellisense
	 */
	valContId: "",
	/**
	 * Object, value DOM element
	 * @type {object}
	 * @intellisense
	 */
	valueElem: null,
	/**
	 * Span container element id
	 * @type {string}
	 * @intellisense
	 */
	spanContId: "",
	/**
	 * Span jQuery object
	 * @type {object}
	 * @intellisense
	 */
	spanContElem: null,
	/**
	 * Error container id
	 * @type {string}
	 * @intellisense
	 */
	errContId: "",
	/**
	 * Error container, div
	 * @type {object}
	 * @intellisense
	 */
	errContainer: null,
	/**
	 * Array of validation types
	 * @type array of string
	 * @intellisense
	 */
	validationArr: null,

	/**
	 * Value after initialization
	 * @intellisense
	 */
	defaultValue: null,
	/**
	 * Is reset form happend or not
	 * @intellisense
	 */
	isClearHappend: false,
	/**
	 * Source table
	 * @intellisense
	 */
	table: "",
	/**
	 * Defined the control's validation regExp
	 * @type {object}
	 * @intellisense
	 */
	regExp: null,

	/**
	 * Edit type of control, that used to process data on server
	 * Was created for search submit
	 * @type String
	 * @intellisense
	 */
	ctrlType: "",
	/**
	 * Is editable elems shown
	 * @type {boolean}
	 * @intellisense
	 */
	showStatus: true,
	/**
	 * Number of control for the field. In advanced search page only 2 controls may appear for the field.
	 * But ControlManager can add any ammount of controls to the field
	 * @type number
	 * @intellisense
	 */
	ctrlInd: -1,
	/**
	 * Indicator, is focused element or not
	 * @type Boolean
	 * @intellisense
	 */
	isSetFocus: false,
	/**
	 * Hidden property
	 * @type Boolean
	 * @intellisense
	 */
	hidden: false,
	
	/**
	 * Indicator, is field wich control belong hided
	 * @type Boolean
	 * @intellisense
	 */
//	hiddenByField: false,
	/**
	 * Mode of using control add|adit|search
	 * @type String
	 * @intellisense
	 */
	mode: '',

	/**
	 * Indicator, true if control was marked as invalid.
	 * Usefull for password matching and validation etc.
	 * @type Boolean
	 * @intellisense
	 */
	isInvalid: false,

	/**
	 * MLString objects
	 * @type {object}
	 */
	customValidationFailedMessages: {},

	/**
	 * Array dropDownControls which are dependent to this ctrl
	 * @type {array}
	 */
	dependentCtrls: null,

	/**
	 * RunnerPage object this control belongs to
	 * @type {object}
	 */
	pageObj: null,

	/**
	 * If control is in Inline Add/Edit mode
	 * @type {boolean}
	 */
	isInline: false,

	/**
	 * @type {object}
	 */
	ajaxLikeEventsData: null,

	$tooltipContainer: null,

	/**
	 * readOnly state
	 */
	readOnly: false,

	/**
	 * Class constructor
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		// copy properties from cfg to controller obj
		Runner.apply( this, cfg );

		Runner.controls.Control.superclass.constructor.call( this, cfg );

		this.initValidationParams( cfg.validation );

		this.assignBasicElementsIdValues();
		this.assignValueElem();
		this.assignSpanContainerElem();

		//	init tooltip container (bootstrap only)
		this.$tooltipContainer = $('[data-signal-error-for=' + this.goodFieldName + this.rowId + '] [data-helpfor=' + this.goodFieldName + ']');

		if ( cfg.hidden ) {
			this.hide();
		}

		this.setDefaultValue();

		this.dependentCtrls = [];

		this.addStyle( this.style );
		this.addClass( this.css );

		// register the new control
		Runner.controls.ControlManager.register( this );

		this.initCustomEvents();
	},

	/**
	 * Init the control's validators and
	 * custom validation messages
	 * @param {object} validationParams
	 */
	initValidationParams: function( validationParams ) {
		this.validationArr = validationParams.validationArr || [];
		this.customValidationFailedMessages = validationParams.customMessages || {}
		this.regExp = validationParams.regExp;

		// register user's custom validation functions
		Runner.validation.registerCustomValidation( this );
	},

	/**
	 * Set the basic control's elements id values
	 */
	assignBasicElementsIdValues: function() {
		// value element id
		this.valContId = "value" + ( this.ctrlInd || "" ) + "_" + this.goodFieldName + "_" + this.rowId;
		// error DOM element id
		this.errContId = "errorCont" + this.ctrlInd + "_" + this.valContId;
	},

	/**
	 * Set the value element and associated properties
	 */
	assignValueElem: function() {
		this.valueElem = this.valueElem || $("#" + this.valContId);
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		if ( this.appearOnPage() && !this.elemsForEvent.length ) {
			//set the DOM elements array, to add custom listeners then
			this.elemsForEvent = [ this.valueElem.get(0) ];
		}
	},

	/**
	 * Set the control's events list
	 */
	assignListOfEvents: function() {
		this.addEvent( ["click", "blur", "change", "keyup", "edited", "editing", "input", "editingresolved"] );
	},

	/**
	 * Add and init custom events fot the control
	 */
	initCustomEvents: function() {
		this.assignsElemsForEvent();
		this.assignListOfEvents();
		//the event's initialization allows the control's props/methods
		//with event names to be invoked when the event is triggered
		//the init method is called from superclass to protect its
		//invocation from any child sclasses' overrided 'init' methods
		Runner.controls.Control.superclass.init.call( this );

		this.addEditEditingListeners();

		( this.ajaxLikeEventsData || [] ).forEach( function( data, idx ) {
			this.on( data.event, function( e, updateContext ) {
				if ( typeof Runner.pages.fieldsEvents[ data.handlerId ] === "function" ) {
					Runner.pages.fieldsEvents[ data.handlerId ].call( this, this.pageObj, this.rowId, this.pageObj.getRowById( this.rowId ) );
				}
			});
		}, this );
		
		this.onCustomEventsAdded();
	},
	
	/** 
	 * a moment when all control custom events are added
	 */
	onCustomEventsAdded: function() {
		this.on("editing", function() {
			Runner.trackUserActivity();
		});
	},

	/**
	 * Set the control's span container element and its id
	 */
	assignSpanContainerElem: function() {
		this.spanContId = "edit" + this.rowId + "_" + this.goodFieldName + "_" + this.ctrlInd;
		this.spanContElem = $("#" + this.spanContId);
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		this.on("change", function( e, updateContext ) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			this.fireEvent("edited", updateContext);
			this.fireEvent("editing", updateContext);
		});
	},

	/**
	 * The method is invoked when the "edited" event is fired
	 * @param {object} nullEvent		 Equals to null if function is invoked
	 * 								with the fireEvent method
	 * @param {object} updateContext
	 */
	"edited": function( nullEvent, updateContext ) {
		var valArr = this.getPreparedValidationArray();

		if ( valArr.length ) {
			this.validate( valArr );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.processResolvedEvent( updateContext );
	},

	/**
	 * Get the validation array copy. Remove from the result array
	 * the "IsRequired" element if the control is not marked as invalid
	 * @retur {array}
	 */
	getPreparedValidationArray: function() {
		var valArr = this.validationArr.slice(0), i;

		if ( !this.invalid() ) {
			for (i = 0; i < valArr.length; i++) {
				if ( valArr[i] == "IsRequired" ) {
					valArr.splice(i, 1);
					break;
				}
			}
		}
		return valArr;
	},

	destructor: function() {
		this.purgeListeners();

		delete this.dependentCtrls;
	},

	unregister: function() {
		Runner.controls.ControlStorage.unregister( this.rowId, this.fieldName, this.ctrlInd );
	},

	/**
	 * Add styles to the value element
	 * @param {string} styleToAdd
	 * @intellisense
	 */
	addStyle: function( styleToAdd ) {
		if ( !styleToAdd ) {
			return;
		}

		var stylesArr = styleToAdd.split(';'),
			i, style, propertyName, value;

		for (i = 0; i < stylesArr.length; i++) {
			style = stylesArr[ i ].split(":");
			propertyName = style[0].toString().trim();

			if ( propertyName ) {
				value = style[1].toString().trim();
				this.getDispElem().css( propertyName, value );
			}
		}
	},

	/**
	 * Validate the control with validators set in the array-param passed
	 * or in the control's 'validationArr' array
	 * @method validate
	 * @params {array} valArr - array of validation for event blur only
	 * @return {object}
	 * @intellisense
	 */
	validate: function( valArr ) {
		if ( !this.appearOnPage() || this.hiddenField() ) {
			this.isInvalid = false;
			return Runner.validation.getTrueValidationResult();
		}

		var validators = valArr || this.validationArr,
			vRes = Runner.validation.validate( validators, this );

		// change invalid status only if any validation were made, to prevent init error container
		if ( validators.length ) {
			if ( !vRes.result ) {
				this.markInvalid( validators, vRes.messagesData );
			} else {
				this.clearInvalid( vRes.delayedValidators );
			}
		}

		return vRes;
	},

	/**
	 * removes validation from control.
	 * @param {string} vType
	 * @return {boolean} If success true, false otherwise
	 * @intellisense
	 */
	removeValidation: function( vType ) {
		if ( typeof vType !== "string" ) {
			this.regExp = null;
			vType = "RegExp";
		}

		const newValidation = this.validationArr.filter( type => type != vType );
		if( this.validationArr.length === newValidation.length ) {
			return false;
		}
		
		this.validationArr = newValidation;
		if ( vType === "IsRequired" ) {
			var $fieldItem = this.valueElem.closest( "[data-itemtype]" )
				.removeAttr( "data-required" );
			
			if ( $fieldItem.attr("data-itemtype") == "edit_field" ) {
				$fieldItem.siblings("[data-itemtype='edit_field_label']")
					.removeAttr( "data-required" );
			}
		}
		
		this.clearInvalid();
		return true;
	},

	/**
	 * Adds validation to control
	 * @param {string} vType
	 * @intellisense
	 */
	addValidation: function( vType ) {
		if ( this.isSearchControl() ) {
			return;
		}

		if ( typeof vType != "string" ) {
			this.regExp = vType;
			vType = "RegExp";
		}
		
		if ( !this.isSetValidation( vType ) ) {
			if ( vType === "IsRequired" ) {
				var $fieldItem = this.valueElem.closest( "[data-itemtype]" )
					.attr( "data-required", true );
					
				if ( $fieldItem.attr("data-itemtype") == "edit_field" ) {
					$fieldItem.siblings("[data-itemtype='edit_field_label']")
						.attr( "data-required", true );
				}
			}
			this.validationArr.push( vType );
		}
	},

	/**
	 * Checks if validation added
	 * @param {string} vType
	 * @return {boolean} If success true, false otherwise
	 * @intellisense
	 */
	isSetValidation: function( vType ) {
		if ( !Runner.validation[ vType ] ) {
			return false;
		}

		for (var i = 0; i < this.validationArr.length; i++) {
			if ( this.validationArr[i] == vType ) {
				return true;
			}
		}

		return false;
	},

	/**
	 * Validates control value against vType validation
	 * @param {string} vType
	 * @return {mixed}
	 * @intellisense
	 */
	validateAs: function( vType ) {
		if ( !this.appearOnPage() ) {
			this.isInvalid = false;
			return Runner.validation.getTrueValidationResult();
		}

		Runner.validation.control = this;
		return Runner.validation[ vType ]( this.getValue() );
	},

	/**
	 * Helper func for lazy init error container
	 * @private
	 * @intellisense
	 */
	initErrorCont: function() {
		// create error container
		var $helpContainer = $('[data-helpfor=' + this.goodFieldName + ']', this.pageObj.pageCont);

		if ( !this.isInline ) {
			this.errContainer = $('<strong class="help-block"></strong>');
		} else {
			this.errContainer = $('<span class="help-block"></span>');
		}

		this.errContainer
			.attr('id', this.errContId)
			.css('display', 'none');

		if ( $helpContainer.length ) {
			this.errContainer.prependTo( $helpContainer );
		} else {
			this.errContainer.appendTo( this.spanContElem );
		}

		this.initErrorCont = Runner.emptyFn;
	},

	/**
	 * Sets error messages after validation
	 * @param {array} validators
	 * @param {object} messagesData
	 * @intellisense
	 */
	markInvalid: function( validators, messagesData ) {
		this.initErrorCont();

		this.markInvalid = function( validators, messagesData ) {
			var that = this, obj = {};

			/*
			//	normalize parameters
			if ( typeof messagesData == "undefined" ) {
				messagesData = validators;
				validators = "userDefined";
			}

			if ( typeof validators == "string" ) {
				validators = [ validators ];
			}

			if ( typeof messagesData == "string" ) {
				obj[ validators[0] ] = [messagesData];
				messagesData = obj;
			}
			*/


			this.pageObj.validateTimer = setTimeout( function() {
				that.errContainer.show();
				
				that.errContainer.parent( '[data-helpfor=' + that.goodFieldName + ']' )
					.show()
					.removeAttr('data-hidden');
				
				if ( that.pageObj ) {
					that.pageObj.adjustHeightAndWidth();
				}
			}, 100);


			$('[data-signal-error-for=' + this.goodFieldName + this.rowId + ']').toggleClass( 'has-error', true );
			
			this.addValidationMessagesToErrorCont( validators, messagesData );
			// set invalid indicator
			this.isInvalid = true;
		}

		this.markInvalid( validators, messagesData );
	},

	/**
	 * Show custom error message
	 * @param {string} validatorName
	 * @param {array} messages
	 */
	markInvalidWidthCusttomMessages: function( validatorName, messages ) {
		var messagesData = {};
		messagesData[ validatorName ] = messages;
		this.markInvalid( [validatorName], messagesData );
	},

	/**
	 * Add the failed validation messages to the control's error container
	 * Each validator has its own container to dispaly errors
	 * @param {array} validators
	 * @param {object} messagesData
	 */
	addValidationMessagesToErrorCont: function( validators, messagesData ) {
		var i, message,
			suffix = this.goodFieldName + "_" + this.rowId;

		for (i = 0; i < validators.length; i++) {

			message = Runner.validation.extractValidationMessage( validators[i], messagesData )

			//	Sort of hack: don't remove DenyDuplicate validator message.
			//	If the value is valid, the message will be removed later, after the server response.
			if ( message == "" && validators[i] == "DenyDuplicated" ) {
				continue;
			}

			this.setValidationMessage( validators[i] + suffix, message );
		}
	},

	/**
	 * Get the validator error container
	 * and fill it with validation error message
	 * @param {string} contId
	 * @param {string} message
	 */
	setValidationMessage: function( contId, message ) {
		var validatorContainer = this.getValidationMessageContainer( contId );

		validatorContainer.html( message ).show();
	},

	/**
	 * Get existing or create the validator error container
	 * @param {string} contId
	 * @return {jQuery object}
	 */
	getValidationMessageContainer: function( contId ) {
		var validatorContainer = $("#" + contId, this.errContainer);

		if ( !validatorContainer.length ) {
			validatorContainer = $('<div id="' + contId + '"></div>')
				.appendTo( this.errContainer );
		}

		return validatorContainer;
	},

	/**
	 * Get the existing validators error messages containers from the
	 * control's error containter
	 * @return {jQuery collection object}
	 */
	getValidationMessagesContainers: function() {
		return this.errContainer.children('div');
	},

	/**
	 * Clears invalid state
	 * @param {Array} delayedValidators	(optional)
	 * @method
	 * @intellisense
	 */
	clearInvalid: function( delayedValidators ) {
		this.initErrorCont();

		this.clearInvalid = function( delayedValidators ) {
			// set invalid indicator
			this.isInvalid = false;

			this.hideNotDelayeValidationMessages( delayedValidators || [], this.goodFieldName + "_" + this.rowId );
			this.updateErrorsContainerVisibility();

			$('[data-signal-error-for=' + this.goodFieldName + this.rowId + ']').toggleClass( 'has-error', false );
		}

		this.clearInvalid();
	},

	/**
	 * Update the control's error container visibility
	 */
	updateErrorsContainerVisibility: function() {
		if ( !this.errContainer.children("div:visible").length ) {
			this.errContainer.hide();
		}
	},

	/**
	 * Hide the validation error messages nod connected with delayed validators
	 * The tempotary class 'delayedValidation' is used to mark delayed messages
	 * @param {Array} delayedValidators
	 */
	hideNotDelayeValidationMessages: function( delayedValidators, suffix ) {
		var messageConts = this.getValidationMessagesContainers(), i

		for (i = 0; i < delayedValidators.length; i++) {
			this.getValidationMessageContainer( delayedValidators[i] + suffix )
				.addClass( "delayedValidation" );
		}

		messageConts.not(".delayedValidation").hide().empty();
		messageConts.filter(".delayedValidation").removeClass( "delayedValidation" );
	},

	/**
	 * Return invalid state of control
	 * @return {boolean}
	 * @intellisense
	 */
	invalid: function() {
		return this.isInvalid;
	},

	/**
	 * Scrolls the element's offset parent so I shows the element
	 * @param {jQuery object} elem
	 */
	scrollToElem: function( elem ) {
		var offsetParent = elem.offsetParent(),
			elemPosition = elem.position(),
			elemWidth = elem.width(),
			scrollTo = offsetParent.scrollTop() + elemPosition.top,
			scrollLeft = offsetParent.scrollLeft() + elemPosition.left + elemWidth - offsetParent.width(),
			inlineMode = this.mode === Runner.pages.constants.PAGE_INLINE_EDIT || this.mode === Runner.pages.constants.PAGE_INLINE_ADD;

		if ( offsetParent.css("position") === "static" ) {
			//the page isn't in popup mode
			offsetParent = $("html");
			scrollTo = elemPosition.top;
			offsetLeft = offsetParent.scrollLeft() + elemPosition.left + elemWidth - $(window).width();
		}

		if ( this.pageType === Runner.pages.constants.PAGE_LIST && inlineMode ) {
			//the list page's inline modes
			offsetParent.scrollLeft( scrollLeft + 5 );
			return;
		}

		offsetParent.scrollTop( scrollTo - 5 );
	},

	/**
	 * Set the default value to the control
	 * @param {object} updContext
	 * @method
	 * @intellisense
	 */
	reset: function( updContext ) {
		this.setValue( this.defaultValue, true, updContext );
		this.clearInvalid();
	},

	/**
	 * Sets empty value to control
	 * return true if success. otherwise false
	 * @method
	 * @intellisense
	 */
	clear: function() {
		this.isClearHappend = true;
		this.setValue('', false);
		this.clearInvalid();
		this.isClearHappend = false;
	},

	/**
	 * Hide control - set display attr none
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	hide: function() {
		this.spanContElem.css("display", "none");
		this.showStatus = false;
	},

	/**
	 * Show control - set display attr block
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	show: function() {
		this.spanContElem.css("display", "");
		this.showStatus = true;
	},

	/**
	* Toggle a control basing on 'visibility' param
	* @param {Boolean} visibility
	*/
	toggle: function( visibility ) {
		if ( visibility ) {
			this.show();
		} else {
			this.hide();
		}
	},

	/**
	 * Toggle show/hide status
	 * @intellisense
	 */
	toggleHide: function() {
		this.toggle( !this.showStatus );
	},

	/**
	 * Get value from value element.
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	getValue: function() {
		if ( this.valueElem instanceof jQuery && this.valueElem.length) {
			return this.valueElem.val();
		}
		return false;
	},

	/**
	 * Set the control's default value
	 * @return {string}
	 */
	setDefaultValue: function() {
		this.defaultValue = this.getValue();
	},

	/**
	 * Return value as string
	 * @return {string}
	 * @intellisense
	 */
	getStringValue: function() {
		return this.getValue();
	},

	/**
	 * Return value as a user-friendly string
	 * @return {string}
	 * @intellisense
	 */
	getUserValue: function() {
		return this.getStringValue();
	},
	
	getValueToValidate: function() {
		return this.getStringValue();
	},	

	/**
	 * Sets value to value DOM elem
	 * Should be overriden for sophisticated controls
	 * @method
	 * @param {mixed} val
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @intellisense
	 */
	setValue: function( val, triggerEvent, updContext ) {
		var readOnly = this.isReadonly();

		if ( this.valueElem instanceof jQuery ) {
			if ( readOnly ) {
				this.makeReadWrite();
			}

			this.valueElem.val( val );

			if ( triggerEvent === true ) {
				this.fireEvent("change", updContext);
			}

			if ( readOnly ) {
				this.makeReadonly();
			}

			return true;
		}

		return false;
	},

	/**
	 * Sets display value for control
	 * Should be overriden for sophisticated controls
	 * @method
	 * @param {mixed} val
	 * @intellisense
	 */
	setDisplayValue: function( val ) {
	},

	/**
	 * Sets disable attr true
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	setDisabled: function() {
		if ( this.valueElem.length ) {
			this.valueElem.prop("disabled", true);
			return true;
		}
		return false;
	},

	/**
	 * Sets disaqble attr false
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	setEnabled: function() {
		if ( this.valueElem.length ) {
			this.valueElem.prop("disabled", false);
			return true;
		}
		return false;
	},

	/**
	 * Check if it's possible to set focus on the control
	 * @return {boolean}
	 */
	isFocusAllowed: function() {
		return !this.hiddenField();
	},

	/**
	 * Sets focus to the control's element.
	 * @param {boolean} triggerEvent
	 * @method
	 * @intellisense
	 */
	setFocus: function( triggerEvent ) {
		var ctrlElem;

		this.isSetFocus = false;

		// can't set focus on disabled element. This may cause IE error
		if ( !this.appearOnPage() || this.valueElem.prop('disabled') || !this.showStatus ) {
			return;
		}

		ctrlElem = this.valueElem.get(0);

		if ( !this.valueElem.is(":visible") ) {
		// for scroll to any hidden controls
			//reCaptcha
			$captchaBlock = this.valueElem.parents(".captcha_block");
			if ( $captchaBlock.length ) {
				this.valueElem.attr("type", "text");

				ctrlElem.focus();
				this.isSetFocus = true;
				this.valueElem.attr("type", "hidden");
			}
			return;
		}


		try {
			if ( ctrlElem !== document.activeElement && ctrlElem.focus ) {
				ctrlElem.focus();
				this.isSetFocus = true;
				if ( triggerEvent === true ) {
					this.fireEvent("focus");
				}
				return false;
			}
		} catch ( e ) {
			// Support: IE<9
		}
	},
	
	/**
	 * Set focus
	 * For text nodes set cursor to the end of text
	 */
	setEndOfTextFocus: function() {
		this.setFocus();
		if ( !( this.valueElem instanceof jQuery ) ) {
			return;
		}
		
		var ctrlElem = this.valueElem.get(0);
		if ( ctrlElem && ctrlElem.setSelectionRange && ctrlElem.type == 'text' ) {
			setTimeout( function() {
					var valLength = ctrlElem.value.length;
					ctrlElem.setSelectionRange( valLength, valLength );
				}, 0 );
		}
	},

	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * @method
	 * @return {boolean}
	 * @intellisense
	 */
	isEmpty: function() {
		return this.getValue().toString() === "";
	},
	
	isEmptyForSearch: function() {
		return this.isEmpty() || this.getValue().toString().trim() === "";
	},

	/**
	 * Custom function for onblur event
	 * @param {Object} e
	 * @intellisense
	 */
	"blur": function(e) {
		this.stopEvent(e);
		this.isSetFocus = false;
	},

	/**
	 * Sets focus indicator true when click on elem
	 * @param {event} e
	 * @intellisense
	 */
	"click": function(e) {
		this.isSetFocus = true;
	},

	/**
	 * Removes css class to value element
	 */
	removeCSS: function( className ) {
		this.valueElem.removeClass( className );
	},

	/**
	 * Removes css class to value element
	 * @param {string} className
	 * @intellisense
	 */
	removeClass: function( className ) {
		if ( this.valueElem instanceof jQuery ) {
			this.valueElem.removeClass( className );
		}
	},

	/**
	 * Adds css class to value element
	 */
	addCSS: function( className ) {
		if ( this.valueElem instanceof jQuery && className ) {
			this.valueElem.addClass( className );
		}
	},

	/**
	 * Adds css class to value element
	 * @param {string} className
	 * @intellisense
	 */
	addClass: function( className ) {
		if ( this.valueElem instanceof jQuery && className ) {
			this.valueElem.addClass( className );
		}
	},

	/**
	 * Returns specified attribute from value element
	 * @param {string} attrName
	 * @intellisense
	 */
	getAttr: function( attrName ) {
		return this.valueElem.attr( attrName );
	},

	/**
	 * Return element that used as display.
	 * Usefull for suggest div positioning
	 * @return {object}
	 * @intellisense
	 */
	getDispElem: function() {
		return this.valueElem;
	},

	/**
	 * Clone html for iframe submit
	 * @return {array}
	 * @intellisense
	 */
	getForSubmit: function() {
		return this.appearOnPage() ? [ this.valueElem.clone().val( this.valueElem.val() ) ] : [];
	},

	/**
	 * Make readonly control
	 * Get control's clone, make it readonly
	 * And make contol hidden
	 *
	 * @return {boolean}
	 * @intellisense
	 */
	makeReadonly: function() {
		if ( !this.appearOnPage() || this.isReadonly() ) {
			return false;
		}
		
		this.readOnly = true;

		this.readonlyElem = this.valueElem.clone();
		this.readonlyElem.val( this.valueElem.val() );

		this.readonlyElem.prop({
			'id': 'readonly_' + this.valContId,
			'name': 'readonly_' + this.valContId,
			'disabled': true,
			'readonly': true
		});

		this.readonlyElem.prependTo( this.spanContElem );

		this.valueElem.hide();
		return true;
	},

	/**
	 * Change readonly control to readwrite
	 * Remove control's clone and make contol visible
	 *
	 * @return {boolean}
	 * @intellisense
	 */
	makeReadWrite: function() {
		if ( !this.appearOnPage() || !this.isReadonly() ) {
			return false;
		}

		this.readOnly = false;

		this.readonlyElem.remove();
		this.readonlyElem = undefined;
		this.valueElem.css('display', '');
		return true;
	},

	/**
	 * Check is control readonly or not
	 * @return {boolean}
	 * @intellisense
	 */
	isReadonly: function() {
		//return !!$('#readonly_' + this.valContId).length;
		return this.readOnly;
	},

	/**
	 * Check is control exist or not
	 * @return {boolean}
	 * @intellisense
	 */
	appearOnPage: function() {
		return !!this.valueElem.length;
	},

	/**
	 * @param {string}
	 * @return {mixed}
	 */
	getFieldSetting: function( key ) {
		return Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, key, this.pageType );
	},

	/**
	 * Get the displayed field's value
	 * Overrided for lookup wizard controls
	 * @intellisense
	 */
	getDisplayValue: function() {
		return this.getValue();
	},

	/**
	 * Get the default control's value
	 * @return {String | Number}
	 */
	getDefaultControlValue: function() {
		return this.defaultValue;
	},
	
	getDefaultControlValueToValidate: function() {
		return this.getDefaultControlValue();
	},

	/**
	 * Validate the control with the 'DenyDuplicated' validator
	 * if the 'DenyDuplicated' validation is set for the field
	 */
	checkDuplicates: function() {
		if ( !this.isSetValidation('DenyDuplicated') ) {
			return;
		}
		this.validate( ['DenyDuplicated'] );
	},

	/**
	 * The stub for the not to-states search controls
	 * @param {string} searchOption
	 */
	updateAppearance: function( searchOption ) {},

	/**
	 * Check if search suggests need initializing for the control #8538
	 * @return {boolean}
	 */
	useSearchSuggests: function() {
		return this.editFormat === Runner.controls.constants.EDIT_FORMAT_TEXT_FIELD;
	},

	/**
	 * Check if suggests should be displayed for the control
	 * @return {boolean}
	 */
	implySuggests: function() {
		return this.editFormat === Runner.controls.constants.EDIT_FORMAT_TEXT_FIELD;
	},

	/**
	 * A stub
	 * @param {string} tName
	 * @param {string} pageId
	 */
	setDependencies: function( tName, pageId ) {
	},

	/**
	 * A stub
	 * @return {array}
	 */
	getParentLookupFieldNames: function() {
		return [];
	},

	/**
	 * A stub
	 * @return Boolean
	 */
	hasDependencies: function() {
		return false;
	},

	/**
	 * Add dependent controls to array of controls
	 * @method
	 * @param {array} ctrlDD array of control objects
	 */
	addDependentCtrls: function( ctrlsArr ) {
		for (var i = 0; i < ctrlsArr.length; i++) {
			this.dependentCtrls.push( ctrlsArr[i] );
		}
	},

	/**
	 * Call reload method of each dependent DD
	 * @method
	 * @param {object} updContext
	 * @param {mixed} e
	 */
	reloadDependentCtrls: function( updContext, e ) {
		var ctrl = this;
		
		if ( !this.dependentCtrls || !this.dependentCtrls.length ) {
			return;
		}
		
		if ( !updContext || typeof updContext !== "object" ) {
			updContext = {};
		} 

		updContext.reloadsStarted = updContext.reloadsStarted || 0;
		updContext.initiator = ctrl;
		
		/**
		 * updContext.reloadControls: Array<{
		 * 		ctrl: Control,
		 * 		reloadCount: number
		 * }>
		 */
		updContext.reloadControls = updContext.reloadControls || [];
		this.dependentCtrls.forEach( function( dctrl ) {
			//	don't reload the initiator
			if ( !dctrl || dctrl == updContext.initiator ) {
				return;
			}
			var reloadData = updContext.reloadControls.find( function( data ) {
				return dctrl == data.ctrl;
			} );
			if( !reloadData ) {
				reloadData = {
					ctrl: dctrl,
					reloadCount: 0
				};
				updContext.reloadControls.push( reloadData );
			}
			if( reloadData.reloadCouunt >= (dctrl.parentCtrls || []).length ) {
				return;
			}
			dctrl.reload( updContext );
		});

/*		
		updContext.reloadDependencies = updContext.reloadDependencies || [];
		
		//	do nothing if dependencies were already reloaded within the current update operation
		var reloaded = !!updContext.reloadDependencies.filter( function( data ) { 
				return data.fName == ctrl.fieldName;
			}).length;
		
		if ( reloaded ) {
			return;
		}

		updContext.reloadDependencies.push({ 
			fName: this.fieldName, 
			dependentNames: this.dependentCtrls.map( function( dctrl ) { 
				return dctrl && dctrl.fieldName; 
			}), 
			resolved: [] 
		});
		
		this.dependentCtrls.forEach( function( dctrl ) {
			if ( !dctrl ) {
				return;
			}
			var depInitiators = updContext.reloadDependencies.filter( function( data ) {
					return data.fName == dctrl.fieldName
				}, this );

			//	don't update the initial control
			if ( !depInitiators.length ) {
				dctrl.reload( updContext );
			} else {
				depInitiators.forEach( function( data ) {
					data.resolved = data.dependentNames.slice(0);
				});
			}
		}, this );
*/
	},

	clearInvalidOnDependences: function() {
		if ( !this.dependentCtrls ) {
			return;
		}
		for (var i = 0; i < this.dependentCtrls.length; i++) {
			// if children exists
			if ( this.dependentCtrls[i] ) {
				this.dependentCtrls[i].clearInvalid();
			}
		}
	},

	/**
	 * @return {boolean}
	 */
	isLookupWizard: function() {
		return false;
	},

	/**
	 * Check if the control is a search panel control
	 * @return {boolean}
	 */
	isSearchControl: function() {
		return this.mode === Runner.controls.constants.MODE_SEARCH;
	},

	/**
	 * Returns ID of the display element of the control.
	 * Can be used as a source for the LABEL FOR attribute.
	 * @return {string}
	 */
	getDispId: function() {
		var dispElem = this.getDispElem();

		if ( dispElem instanceof jQuery ) {
			return dispElem.attr( 'id' );
		}

		jQuery.each( dispElem, function( i, obj ) {
			if ( obj instanceof jQuery ) {
				return obj.attr( 'id' );
			}
		});

		return '';
	},

	/**
	 *	Get peer control from the same page or the same inline Add/Edit row
	 *	@param String fieldName
	 *	@return Object
	 */
	getPeer: function( fieldName ) {
		return Runner.controls.ControlStorage.byIdField( this.rowId, fieldName );
	},

	getTooltip: function() {
		return this.$tooltipContainer.find('.text-muted').html();
	},
	setTooltip: function( html ) {
		if ( html ) {
			this.$tooltipContainer
				.show()
				.removeAttr('data-hidden');	
		}	
		
		return this.$tooltipContainer.find('.text-muted').html( html );
	},
	addTooltip: function( html ) {
		if ( html ) {
			this.$tooltipContainer
				.show()
				.removeAttr('data-hidden');	
		}
		
		return this.$tooltipContainer.find('.text-muted').append( $('<div></div>').html( html ) );
	},

	setInvalid: function( message ) {
		var validator = "userInvalidation" + this.rowId + this.fieldName,
			messagesObj = {};
		Runner.validation.addUserValidator( {
			name: validator,
			message: message,
			validate: function() {return false;}
		});
		this.addValidation(validator);
		messagesObj[ validator ] = [message];
		this.markInvalid( [validator], messagesObj );
	},

	setValid: function() {
		var validator = "userInvalidation" + this.rowId + this.fieldName;
		this.removeValidation(validator);
		this.clearInvalid();
	},

	deleteOptions: function() {
	},

	addOption: function( value, label ) {
	},

	/**
	 * Get all controls' data
	 * @return {object}
	 */
	getControlsData: function() {
		var data = {},
			ctrls;

		if ( this.isInline ) {
			ctrls = Runner.controls.ControlStorage.byId( this.rowId );
		} else {
			ctrls = this.pageObj.getFormControls();
		}

		ctrls.forEach( function( ctrl, i ) {
			data[ ctrl.fieldName ] = ctrl.getUserValue();
		});

		return data;
	},

	/**
	 * Get the parent controls' data
	 * @return {object}
	 */
	getParentControlsData: function() {
		var data = {}, i, dFieldName, value;

		if ( !this.parentCtrls ) {
			return data;
		}

		for (i = 0; i < this.parentCtrls.length; i++) {
			dFieldName = this.parentCtrls[ i ].isLookupWizard() ? this.parentCtrls[ i ].originalField : this.parentCtrls[ i ].fieldName;
			value = this.parentCtrls[ i ].getStringValue();

			if ( this.parentCtrls[ i ].editFormat == "Checkbox" ) {
				value = ( value == "on" ) ? "1" : "0";
			}

			data[ dFieldName ] = value;
		}

		return data;
	},
	
	"editing": function( nullEvent, updateContext ) {
		this.processResolvedEvent( updateContext );
	},
	
	/**
	 * @param {object} updateContext
	 */
	processResolvedEvent: function( updateContext ) {
		if ( this.reloadResolved( updateContext ) && this.autocompleteResolved( updateContext ) ) {
			this.fireEvent("editingresolved");
		}
	},
	
	/**
	 * @param {object} updateContext
	 * @retrun {boolean}
	 */	
	reloadResolved: function( updateContext ) {
		if ( !updateContext  ) {
			return !this.dependentCtrls || !this.dependentCtrls.length;
		}
		return !updateContext.reloadsStarted;

/*
		if ( !updateContext || !updateContext.reloadDependencies ) {
			return !this.dependentCtrls || !this.dependentCtrls.length;
		}
		
		if ( ( !updateContext || !updateContext.reloadDependencies ) && ( !this.dependentCtrls || !this.dependentCtrls.length ) ) {
			return true;
		}
		
		var reloadResolved = !( updateContext.reloadDependencies || [] ).filter( function( data ) {
				return !!data.dependentNames.filter( function( dfName ) {
						return data.resolved.indexOf( dfName ) === -1
					}).length;
			}).length;
		
		return reloadResolved;
*/		
	},
	
	/**
	 * @param {object} updateContext
	 * @retrun {boolean}
	 */		
	autocompleteResolved: function( updateContext ) {
		if ( !updateContext || !updateContext.autofillDependencies ) {
			return !this.autoCompleteFields || !this.autoCompleteFields.length || updateContext && updateContext.resetHappend;
		}
		
		var autocompleteResolved = !( updateContext.autofillDependencies || [] ).filter( function( data ) {
				return !data.resolved;
			}).length;
		
		return autocompleteResolved;
	},

	/**
	 * @return Boolean
	 */
	focused: function() {
		return this.valueElem.filter( function() {
			return this === document.activeElement;
		}).length != 0;
	},

	/**
	 * Tells if it is safe to set control value, that it won't interfere with user typing something into the control
	 * This implementation works for most controls.
	 * @return Boolean
	 */
	safeToSetValue: function() {
		return !this.focused();
	},
	
	/**
	 * A fast changing in a control value may cause 
	 * a race condition with spreadsheet mode submitting
	 * @return boolean
	 */ 
	spEasyChangeable: function() {
		return true;
	},

	/**
	 * Whether the field is hidden by one of the following methods
	 * JS: pageObj.hideField()
	 * JS: pageObj.hideItem()
	 * PHP: $pageObject->hideField()
	 * Page Designer - field visible on Mobile only/Desktop only
	 * @return Boolean
	 */
	hiddenField: function() {
		if ( this.valueElem instanceof jQuery ) {
			return this.valueElem.closest( "[data-itemtype]" ).is( '[data-hidden], [data-media-hidden]' );
		}
		return false;
	}
});

/**
 * @param {object} baseCfg
 * @param {string} pageType
 * @param {string} tName
 */
Runner.controls.MockControlFactory = function( baseCfg, pageType, tName, pageObj ) {	
	var cfg = {
			id: baseCfg.id,
			fieldName: baseCfg.fieldName,
			pageType: pageType, 
			pageObj: pageObj, 
			goodFieldName: Runner.goodFieldName( baseCfg.fieldName ),
			table: tName,
			shortTableName: Runner.pages.PageSettings.getShortTName( baseCfg.table ),
			ctrlInd: baseCfg.ctrlInd,
			ajaxLikeEventsData: Runner.pages.PageSettings.getFieldData( tName, baseCfg.fieldName, "events", pageType )
			
		};

	switch ( pageType ) {
		case Runner.pages.constants.PAGE_VIEW:
			return new Runner.controls.ViewMockControl( cfg );
		default:
			return false;
	}	
};

/**
 * Base abstract class for all mock controls
 * @requires runner, ControlManager, Event
 * @class Runner.controls.MockControl
 */
Runner.controls.MockControl = Runner.extend( Runner.Event, {
	/**
	 * Control id
	 * @type string
	 * @intellisense
	 */
	id: "",
	/**
	 * Name of control
	 * @type string
	 * @intellisense
	 */
	fieldName: "",
	/**
	 * Name used for HTML tags, attrs
	 * @type String
	 * @intellisense
	 */
	goodFieldName: "",
	/**
	 * table name for urls request
	 * @type String
	 * @intellisense
	 */
	shortTableName: "",
	/**
	 * Source table
	 * @type String
	 * @intellisense
	 */
	table: "",
	/**
	 * DOM context of control's page 
	 * @type String
	 * @intellisense
	 */
	pageContext: null,
	/**
	 * Page type
	 * @type String
	 * @intellisense
	 */
	pageType: "", 
	/**
	 * Number of control for the field. In advanced search page only 2 controls may appear for the field.
	 * But ControlManager can add any ammount of controls to the field 
	 * @type number
	 * @intellisense
	 */
	ctrlInd: -1,
	/**
	 * Span container element id
	 * @type {string}
	 * @intellisense
	 */
	spanContId: "",
	/**
	 * Span jQuery object
	 * @type {object}
	 * @intellisense
	 */
	spanContElem: null,
	
	pageObj: null,
	
	/**
	 * @constructor
	 */
	constructor: function( cfg ) {
		var ctrl = this;
		Runner.apply( this, cfg );
		Runner.controls.MockControl.superclass.constructor.call(this, cfg);
		this.assignSpanContainerElem();
		Runner.controls.ControlManager.register( this );
		
		( this.ajaxLikeEventsData || [] ).forEach( function( data, idx ) {
			this.on( data.event, function( e, updateContext ) {
				if ( typeof Runner.pages.fieldsEvents[ data.handlerId ] === "function" ) {
					Runner.pages.fieldsEvents[ data.handlerId ].call( ctrl, ctrl.pageObj, ctrl.id );
				}
			});
		}, this );
	}
});

/**
 * class for view mock controls
 * @requires Runner, ControlManager, MockControl
 * @class Runner.controls.ViewMockControl
 */
Runner.controls.ViewMockControl = Runner.extend( Runner.controls.MockControl, {
	/**
	 * Set the control's span container element and its id 
	 */
	assignSpanContainerElem: function() {
		this.spanContId = "view" + this.id + "_" + this.goodFieldName;
		this.spanContElem = $("#" + this.spanContId);			
	},

	getDispElem: function() {
		return this.spanContElem;
	},

	/**
	 * Hide control - set display attr none
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	hide: function() {
		this.getDispElem().css("display", "none");
	},
	
	/**
	 * Show control - set display attr block
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	show: function() {
		this.getDispElem().css("display", "");
	},	

	/**
	 * Get value 
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	getValue: function() {
		return !!this.spanContElem.length ? this.spanContElem.text() : false;
	},
	
	getStringValue: function() {
		return this.getValue();
	},
	
	getUserValue: function() {
		return this.getValue();
	},
	
	/**
	 * Sets value to value DOM elem
	 * Should be overriden for sophisticated controls
	 * @method
	 * @param {mixed} val
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @intellisense
	 */
	setValue: function( val )	 {
		if ( this.getDispElem() instanceof jQuery ) {
			this.getDispElem().html( val );
			return true;
		}
		
		return false;
	},

	/**
	 * Clear value 
	 * Should be overriden for sophisticated controls
	 * @method
	 * @intellisense
	 */
	clear: function() {
		return this.setValue("");
	},

	/**
	 * Add styles to the control span element
	 * @param {string} styleToAdd
	 * @intellisense
	 */
	addStyle: function( styleToAdd ) {	
		if ( !styleToAdd || !(this.getDispElem() instanceof jQuery) ) {
			return;
		}
		
		var stylesArr = styleToAdd.split(';'), 
			i, style, propertyName, value;
		
		for (i = 0; i < stylesArr.length; i++) {
			style = stylesArr[ i ].split(":");
			propertyName = style[0].toString().trim();
			
			if  ( propertyName ) {
				value = style[1].toString().trim();
				this.getDispElem().css( propertyName, value );
			}
		}
	},
	/**
	 * Adds css class to value element
	 * @param {string} className
	 * @intellisense
	 */
	addClass: function( className ) {
		if ( this.getDispElem() instanceof jQuery && className ) {
			this.getDispElem().addClass( className );
		}
	},

	/**
	 * Removes css class to value element
	 * @param {string} className
	 * @intellisense
	 */
	removeClass: function( className ) {
		if ( this.getDispElem() instanceof jQuery ) {
			this.getDispElem().removeClass( className );
		}
	},

	/**
	 * Adds an event to the control and transfer the array of arguments to the handler.
	 * @param {string} eventName
	 * @param {function} call
	 * @intellisense
	 */
	on: function( eventName, call ) {
		if ( this.getDispElem() instanceof jQuery && eventName ) {
			this.getDispElem().on( eventName, call );
		}
	},
	
	/**
	 * Check if it's possible to set focus on the control
	 * @return {boolean}
	 */
	isFocusAllowed: function() {
		return false;
	}
});
/**
 * Create a view control
 */
Runner.viewControls.ViewControlFactory = function( baseCfg, pageType, pageContext, pageObject, pageId ) {
	// make an object copy, not reference
	var cfg = {
			table: pageObject.tName,  // not used ?
			pageType: pageType, 
			goodFieldName: Runner.goodFieldName( baseCfg.fieldName ),
			pageContext: pageContext,
			pageObject: pageObject,
			pageId: pageId || pageObject.id,
		};
		
	Runner.apply( cfg, baseCfg );
		
	if ( typeof Runner.viewControls[ cfg.viewFormat ] === "function" ) {
		return new Runner.viewControls[ cfg.viewFormat ]( cfg );
	} 
	
	throw ('Invalid control view format = ' + cfg.viewFormat + '. Cannot create control!');
};


/**
 * Base abstract class for all view controls, should not be created directly
 * @requires runner
 * @class Runner.controls.ViewControl
 */
Runner.viewControls.ViewControl = Runner.extend( Runner.emptyFn, {
	/**
	 * A control's page id
	 * pageObject.id may differ from the real page id 
	 * ( eg view controls on master list)
	 */
	pageId: "",
	
	/**
	 * Name of control
	 * @type string
	 * @intellisense
	 */
	fieldName: "",
	/**
	 * Name used for HTML tags, attrs
	 * @type String
	 * @intellisense
	 */
	goodFieldName: "",
	/**
	 * table name for urls request
	 * @type String
	 * @intellisense
	 */
	shortTableName: "",
	/**
	 * Source table
	 * @type String
	 * @intellisense
	 */
	table: "",
	/**
	 * DOM context of control's page 
	 * @type String
	 * @intellisense
	 */
	pageContext: null,
	/**
	 * Page type
	 * @type String
	 * @intellisense
	 */
	pageType: "", 
	
	/**
	 * @constructor
	 */
	constructor: function( cfg ) {
		// copy properties from cfg to controller obj
		Runner.apply( this, cfg );
	},
	
	/**
	 * A stub
	 */
	 init: Runner.emptyFn
});Runner.viewControls.ViewVideoField = Runner.extend( Runner.viewControls.ViewControl, {

	/**
	 * Override constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {		
		// call parent
		Runner.viewControls.ViewVideoField.superclass.constructor.call(this, cfg);
	},

	// initialized with data-setup attr, added at Server-Side
	init: function() {
		var $controlCtx = $('[data-itemid][data-pageid=' + this.pageId + '][data-field="' + this.goodFieldName + '"]');
		setTimeout(function() {
			$(".video-js", $controlCtx).each( function() {
				var player = videojs(this, {
					controls: true,
					autoplay: false,
					preload: 'none',
					controlBar: {
						volumePanel: {
							inline: false
						},
						pictureInPictureToggle: false
					},
				});

				player.ready(function() {
					// show error if video not supported
					if(!player.canPlayType(player.currentType())) {
						player.error("Unable to play provided video type");
					}
				});						
			});
		}, 10);
	}
});

Runner.viewControls["Video file"] = Runner.viewControls.ViewVideoField;
Runner.viewControls["Database video"] = Runner.viewControls.ViewVideoField;Runner.viewControls.ViewImageField = Runner.extend( Runner.viewControls.ViewControl, {
	/**
	 * The host page's object
	 * @type {object}
	 */
	pageObject: null,
	
	/**
	 * imgConfig: {
	 * multiple: number,
	 * maxImages:number,
	 * gallery: boolean,
	 * galleryMode: number,
	 * captionMode: number,
	 * width: number,
	 * height: number,
	 * thWidth: number,
	 * thHeight: number,
	 * }
	 */	
	imgConfig: null,

	/**
	 * Override constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {		
		Runner.viewControls.ViewImageField.superclass.constructor.call(this, cfg);
	},

	/**
	 * init is called only once on a page for all field values
	 */
	init: function() {
		var ctrl = this;
		var $imageItems = $('[data-itemid][data-pageid=' + this.pageId + '][data-field="' + this.goodFieldName + '"]');

		$imageItems.each( function() {
			var $img = $(this).find( '.r-images:not([data-initialized])' );
			if ( !$img.length ) {
				return;
			}
			ctrl.initImageControl( $img, $(this) );
		});		
	},

	/**
	 * See this.imngConfig
	 * 
	 */
	getConfig: function( $img ) {
		if ( this instanceof Runner.viewControls.ViewImageField && this.imgConfig ) {
			return this.imgConfig
		}
		
		var config = {
			multiple: $img.data("multiple"),
			maxImages: $img.data("max-images"),
			thumbnails: $img.data("thumbnails"),
			gallery: $img.data("gallery"),
			galleryMode: $img.data("gallery-mode"),
			captionMode: $img.data("caption-mode"),
			width: $img.data("width"),
			height: $img.data("height"),
			thWidth: $img.data("th-width"),
			thHeight: $img.data("th-height"),
			fullWidth: !!$img.data("fullwidth"),
			border: !!$img.data("border"),
		};

		if ( this instanceof Runner.viewControls.ViewImageField ) {
			this.imgConfig = config;
		}
		
		return config;
	},

	initImageControl: function( $img, $item ) {
		var imgConfig = this.getConfig( $img ),
			recordId = $item.data('record-id'),
			cssPrefix = '[data-pageid="' + this.pageId + '"]'
				+ '[data-itemid="' + $item.data('itemid') + '"]'
				+ ( recordId ? '[data-record-id="' + recordId + '"]': '' );
				
		$img.attr('data-initialized', '');
		var images = ($img.data('images') || []).map( function( imgDesc ) {
			return {
				url: imgDesc.thumbnail || imgDesc.image,
			}
		});
		
		if ( !images || !images.length ) {
			$img.hide();
			return;
		}
		
		if ( this.imgConfig.gallery ) {
			this.initGallery();
		}

		if ( imgConfig.maxImages == 1 ) {
			this.createSingle( $img, cssPrefix, images );
		} else if ( imgConfig.multiple == 1 ) {
			this.createMasonry( $img, cssPrefix, images );
		} else if ( !imgConfig.thumbnails ) {
			this.createBasic( $img, cssPrefix, images );
		} else {
			this.createThumbnails( $img, cssPrefix, images );
		}
	},

	createBasic: function( $img, cssPrefix, images ) {
		var imgConfig = this.getConfig( $img );
		
		$img.addClass( imgConfig.multiple == 2 ? 'r-vertcal-images' : 'r-horizontal-images' );
		
		var imgIdx = 0;
		images.slice( 0, imgConfig.maxImages || undefined ).forEach( function ( pic ) {
			$('<img>')
				.attr( 'src', pic.url )
				.attr( 'data-gallery-image', imgIdx++ )
				.appendTo( $img );
		});
		
		var desktopStyle = '';
		if ( imgConfig.width ) {
			desktopStyle += 'max-width:' + imgConfig.width + 'px;\n';
			desktopStyle += 'width:' + imgConfig.width + 'px;\n';
		}
		if ( imgConfig.height) {
			desktopStyle += 'max-height:' + imgConfig.height + 'px;\n';
		}
		
		var desktopStyles = {};
		desktopStyles[ cssPrefix + ' .r-images > img' ] = desktopStyle;
		var mobileStyles = {};
		mobileStyles[ cssPrefix + ' .r-images > img' ] = 'width: 100%;';
		this.appendStyles( $img, mobileStyles, desktopStyles );
	},

	createThumbnails: function( $img, cssPrefix, images ) {
		var imgConfig = this.getConfig( $img );
		
		$img.addClass( imgConfig.multiple == 2 ? 'r-vertcal-thumbs' : 'r-horizontal-thumbs' );
		
		var imgIdx = 0;
		images = images.slice( 0, imgConfig.maxImages || undefined );
		images.forEach( function ( pic ) {
			$('<div>')
				.css({
//					'background-image': 'url("' + pic.url + '")',
				})
				.attr('data-gallery-image', imgIdx++ )
				.appendTo( $img )
				.append( $("<img></img>")
					.attr('src', pic.url ) 
					.attr('referrerPolicy', "no-referrer" ) // attempt to avoid google's Rate-limit exceeded error
				);

		});
		
		if ( !imgConfig.thWidth ) {
			imgConfig.thWidth = imgConfig.thHeight || 150;
		}
		imgConfig.thHeight = imgConfig.thHeight || imgConfig.thWidth;

		var imgMargin = 1;
		var style = '', desktopStyle = '';		
		style += 'width:' + ( imgConfig.thWidth - 2 * imgMargin ) + 'px;\n';
		style += 'height:' + ( imgConfig.thHeight - 2 * imgMargin ) + 'px;\n';
		
		// thumbnails on mobiles should take 50% or 100% of screen width. 250 is the magic number
		var mobileStyle = imgConfig.thWidth < 250 && images.length > 1
			? 'width: calc( 50% - '+ (2 * imgMargin) +'px );\nheight: ' + (50 * imgConfig.thHeight / imgConfig.thWidth ) * 0.8 + 'vw;'
			: 'width: 100%;\nheight: ' + (100 * imgConfig.thHeight / imgConfig.thWidth ) * 0.8 + 'vw;'

		
		var desktopStyles = {}, mobileStyles = {};
		desktopStyles[ cssPrefix + ' .r-images > div' ] = style;
		mobileStyles[ cssPrefix + ' .r-images > div' ] = mobileStyle;
				
		var vScrollBarWidth = imgConfig.multiple == 2 ? 20 : 0;
		var hScrollBarWidth = imgConfig.multiple == 2 ? 0 : 20;
		var imgAreaWidth = imgConfig.maxImages != 1 ? imgConfig.width : imgConfig.thWidth;
		var imgAreaHeight = imgConfig.maxImages != 1 ? imgConfig.height : imgConfig.thHeight;
		if ( imgAreaWidth ) {
			desktopStyle += 'width:' + ( imgAreaWidth + hScrollBarWidth ) + 'px;\n';
		}
		if ( imgAreaHeight ) {
			desktopStyle += 'max-height:' + ( imgAreaHeight + vScrollBarWidth ) + 'px;\n';
		}
		
		desktopStyles[ cssPrefix + ' .r-images' ] = desktopStyle;
		
		this.appendStyles( $img, {}, desktopStyles, mobileStyles );
	},

	createSingle: function( $img, cssPrefix, images ) {
		var imgConfig = this.getConfig( $img );
		if ( imgConfig.thumbnails ) {
			imgConfig.width = imgConfig.thWidth;
			imgConfig.height = imgConfig.tHeight;
		}
		this.createBasic( $img, cssPrefix, images );
		
	},

	createMasonry: function( $img, cssPrefix, images ) {
		$img.addClass('r-masonry');
		var imgConfig = this.getConfig( $img );
			
		if ( !imgConfig.width ) {
			imgConfig.width = imgConfig.height ? imgConfig.height * 1.5 : 600;
		}
		if ( !imgConfig.height ) {
			imgConfig.height = imgConfig.height ? imgConfig.height / 1.5 : 400;
		}
		
		var styles = {};
		
		var desktopStyle = 'width: ' + imgConfig.width + 'px;\n' + 'height: ' + imgConfig.height + 'px;\n';	
		var mobileStyle = 'height: ' + Math.floor( 100 * imgConfig.height/imgConfig.width ) + 'vw;\n';
		
		var imgIdx = 0;

		var addRow = function( urls, height, rowIdx, morePics ) {
			rowIdx = rowIdx || 0;
			var $row = $("<div></div>")
				.addClass('r-masonry-row')
				.attr('data-row', rowIdx)
				.appendTo( $img );
				
			urls.forEach( function( url, n ) {
				var pic = $("<div>")
					.addClass('')
					.css({
//						'background-image': 'url("' + url.url + '")',
					})
					.appendTo( $row )
					.append( $("<img></img>")
						.attr('src', url.url ) 
						.attr('referrerPolicy', "no-referrer" ) // attempt to avoid google's Rate-limit exceeded error
					);
					
				if ( imgConfig.gallery ) {
					pic.attr('data-gallery-image', imgIdx++ )
				}
				
				if ( n == urls.length - 1  && morePics ) {
					pic.attr('data-more-pics', morePics )  
				}
			});
			
			var rowSelector = cssPrefix + ' .r-images > [data-row="' + rowIdx + '"]';
			if ( height ) {
				styles[ rowSelector ] = ( styles[ rowSelector ] || '' ) 
					+ 'height: ' + height + ';\n';
			}
			
			// picSelector = rowSelector + ' > div'; // ?
		}
		
		if ( images.length == 1 ) {
			addRow( images, '100%' );
		} else if ( images.length == 2 ) {
			var vertical = !imgConfig.height || imgConfig.width && imgConfig.width < 1.3 * imgConfig.height;
			if ( vertical ) {
				addRow( images.slice( 0, 1 ), '50%', 0 );
				addRow( images.slice( 1 ), '50%', 1 );
			} else {
				addRow( images );
			}
		} else if ( images.length == 3 ) {
			addRow( images.slice( 0, 1 ), '60%', 0 );
			addRow( images.slice( 1 ), '40%', 1 );
		} else if ( images.length == 4 ) {
			addRow( images.slice( 0, 2 ), '50%', 0 );
			addRow( images.slice( 2 ), '50%', 1 );
		} else if ( images.length >= 5 ) {
			addRow( images.slice( 0, 2 ), '66%', 0 );
			addRow( images.slice( 2, 5 ), '34%', 1, images.length - 5 );	
		}
		
		var selImages = cssPrefix + ' .r-images';

		var desktopStyles = {}, mobileStyles = {};
		desktopStyles[ selImages ] =  desktopStyle;
		mobileStyles[ selImages ] =  mobileStyle;
		
		this.appendStyles( $img, styles, desktopStyles, mobileStyles );
	},

	appendStyles: function( $img, styles, desktopStyles, mobileStyles ) {
		var block = Object.keys( styles ).map( function( sel ) {
			return sel + '{\n' + styles[sel] + '}';
		} ).join('\n');
		
		var dblock = Object.keys( desktopStyles || {} ).map( function( sel ) {
			return sel + '{\n' + desktopStyles[sel] + '}';
		} ).join('\n');

		var mblock = Object.keys( mobileStyles || {} ).map( function( sel ) {
			return sel + '{\n' + mobileStyles[sel] + '}';
		} ).join('\n');

		if ( this.imgConfig.fullWidth ) {
			$('<style>' +
				block +
				'@media print, (min-width: 768px) {\n' + 
				dblock + 
				'}\n' + 
				'@media screen and (max-width: 767px) {\n' + 
				mblock + 
				'}\n' + 
				'</style>')
				.appendTo( $img );
		} else {
			$('<style>' +
				block + '\n' + 
				dblock + 
				'</style>')
				.appendTo( $img );
		}
	},
	
	/**
	 * not used
	 */
	imageItemTypes: function() {
		return [
			'grid_field',
			'edit_field',
			'integrated_edit_field',
			'report_grid_field',
			'report_group_field',
		];
	},

	initGallery: function() {	
		$( this.pageContext )
			.off( 'click.photoswipe' )
			.on( 'click.photoswipe', 
			'[data-itemtype][data-pageid='+ this.pageId +'] [data-gallery-image]', 
			function() {
				var index = $(this).attr('data-gallery-image');
				Runner.viewControls.ViewImageField.prototype.openGallery( $(this).closest('.r-images'), parseInt( index ) );
			});
		
		if ( 0 == $('.pswp').length ) {
			$('<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">'+
'					<div class="pswp__bg"></div>'+
'					<div class="pswp__scroll-wrap">'+
'						<div class="pswp__container">'+
'							<div class="pswp__item"></div>'+
'							<div class="pswp__item"></div>'+
'							<div class="pswp__item"></div>'+
'						</div>'+
'						<div class="pswp__ui pswp__ui--hidden">'+
'							<div class="pswp__top-bar">'+
'								<div class="pswp__counter"></div>'+
'								<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>'+
'								<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>'+
'								<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>'+
'								<div class="pswp__preloader">'+
'									<div class="pswp__preloader__icn">'+
'									<div class="pswp__preloader__cut">'+
'										<div class="pswp__preloader__donut"></div>'+
'									</div>'+
'									</div>'+
'								</div>'+
'							</div>'+
'							<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">'+
'								<div class="pswp__share-tooltip"></div> '+
'							</div>'+
'							<button class="pswp__button pswp__button--arrow--left" >'+
'							<span class="glyphicon glyphicon-arrow-left"></span>'+
'							</button>'+
'							<button class="pswp__button pswp__button--arrow--right"> '+
'							<span class="glyphicon glyphicon-arrow-right"></span>'+
'							</button>'+
'							<div class="pswp__caption">'+
'								<div class="pswp__caption__center"></div>'+
'							</div>'+
'						</div>'+
'					</div>'+
'				</div>'). appendTo( document.body );
		}	
	},
	
	/**
	 * don't rely on 'this' here!
	 */
	openGallery: function( $img, idx ) {
		var imgConfig = Runner.viewControls.ViewImageField.prototype.getConfig( $img );
		if ( !imgConfig.gallery ) {
			return;
		}
		
		var $item = $img.closest( '[data-itemid]' );
		
		var selector;
		if ( imgConfig.galleryMode == 1 ) {
			// this field only
			selector = $img;
		} else if ( imgConfig.galleryMode == 2 && $item.is('[data-record-id]')) {
			// pics from this record
			selector = '[data-pageid="' + $item.data('pageid') + '"][data-record-id="' + $item.data('record-id') + '"] .r-images';
		} else {
			// all page pics
			selector = '[data-pageid="' + $item.data('pageid') + '"] .r-images';
		}
		
		var images = [];
		$( selector ).each( function() {
			if ( $(this).is( $img ) ) {
				idx += images.length;
			}
			images = images.concat( Runner.viewControls.ViewImageField.prototype.readPictures( $(this) ) );
		});
		
		if ( !images.length ) {
			return;
		}
		
		var gallery = new PhotoSwipe( $('.pswp')[0], PhotoSwipeUI_Default, images, {index: idx } );
		gallery.init();
	},

	readPictures: function( $img ) {
		return $img.data('images').map( function( pic ) {
			var ret = {
				src: pic.image,
			};
			
			var captionMode = $img.data('caption-mode');
			if ( captionMode == 2 ) {
				ret.title = pic.filename;
			} else if ( captionMode == 3 ) {
				ret.title = $img.data('caption');
			}
			
			return ret;
		});
	}
});

Runner.viewControls["File-based Image"] = Runner.viewControls.ViewImageField;
Runner.viewControls["Database Image"] = Runner.viewControls.ViewImageField;Runner.viewControls.ViewFileField = Runner.extend( Runner.viewControls.ViewControl, {
	/**
	 * Override constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {		
		// call parent
		Runner.viewControls.ViewFileField.superclass.constructor.call(this, cfg);
	}
});

Runner.viewControls["Document Download"] = Runner.viewControls.ViewFileField;Runner.viewControls.ViewDatabaseImageField = Runner.extend( Runner.viewControls.ViewControl, {
	/**
	 * Override constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {		
		// call parent
		Runner.viewControls.ViewDatabaseImageField.superclass.constructor.call(this, cfg);
	}
});

/*Runner.viewControls["Database Image"] = Runner.viewControls.ViewDatabaseImageField;*/
Runner.viewControls["Old file-based Image"] = Runner.viewControls.ViewDatabaseImageField;
/**
 * Class for read only control
 */
Runner.controls.ReadOnly = Runner.extend( Runner.controls.Control, {
	/**
	 * @type {jQuery object}
	 */ 
	readonlyElem: null,
	
	reservedBoolean: null,
	controlType: null,
	
	readonly: true,
	
	
	constructor: function( cfg ) {
		Runner.controls.ReadOnly.superclass.constructor.call(this, cfg);
		
		// process the special case when lookupwizard field is transformed to readonly with a master key value
		if ( this.pageType === Runner.pages.constants.PAGE_ADD && !this.isEmpty() ) {
			this.linkField = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "linkField", this.pageType );
			this.autoCompleteFields = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "autoCompleteFields", this.pageType );			
			
			this.doAutoCompleteFields();
		}
	},

	/**
	 * Set the value element and associated properties
	 */
	assignValueElem: function() {		
		Runner.controls.ReadOnly.superclass.assignValueElem.call( this );
		this.readonlyElem = $('#readonly_' + this.valContId);
		
		// process the special case when lookupwizard field is transformed to readonly with a master key value
		if ( this.pageType === Runner.pages.constants.PAGE_ADD && this.value !== '' ) {
			this.linkField = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "linkField", this.pageType );
			this.autoCompleteFields = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "autoCompleteFields", this.pageType );			
			
			this.doAutoCompleteFields();
		}	

		// to set value correctly for postgre
		this.reservedBoolean = { 
			'true': this.valueElem.data('true'),
			'false': this.valueElem.data('false'),
		};
		this.controlType = this.valueElem.data("controlType");
	},
	
	
	/**
	 * Validate the control
	 * @return {object}
	 */
	validate: function() {
		return {
			result: true
		};
	},
	
	/**
	 * Check if it's possible to set focus on the control
	 * @return {boolean}
	 */	
	isFocusAllowed: function() {
		return false;
	},
	
	/**
	 * The stub preventing the control 
	 * from setting focus on it
	 */
	setFocus: Runner.emptyFn,
	setEndOfTextFocus: Runner.emptyFn,
	
	
	setValue: function( val, triggerEvent, updContext ) {
		if ( this.controlType === "checkbox" ) {
			val = val === false || val === "0" ? "" : val;
		}
		Runner.controls.ReadOnly.superclass.setValue.call( this, val, triggerEvent, updContext );
		this.readonlyElem.html( val );
	},
	
	setDisplayValue: function( val ) {
		this.readonlyElem.html( val );
	},

	getDispElem: function() {
		return this.readonlyElem;
	},
	
	/**
	 * A wrapper for a Runner.controls.LookupWizard.doAutoCompleteFields method
	 */
	doAutoCompleteFields: function() {
		Runner.controls.LookupWizard.prototype.doAutoCompleteFields.call( this );
	},

	makeReadonly: Runner.emptyFn,
	
	makeReadWrite: Runner.emptyFn,

	"edited": function(nullEvent, updateContext) {
		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );		
	},
	
	/**
	 * Clone html for iframe submit
	 * @return {array}
	 * @intellisense
	 */
	getForSubmit: function() {
		if ( this.controlType === "checkbox" ) {
			var cloneArr = [],
				value = this.valueElem.val(),
				$cb, $type;
			
			if ( value && value !== "0" && value !== this.reservedBoolean['false'] ) {
				$cb = $('<input>')
					.attr('type', 'hidden')	
					.attr('id', this.valueElem.attr('id'))
					.attr('name', this.valueElem.attr('name'))
					.val( "on" );

				cloneArr.push( $cb.get(0) );			
			}
			
			$type = $('<input>')
					.attr('type', 'hidden')	
					.attr('id', "type_" + this.goodFieldName + "_" + this.rowId )
					.attr('name', "type_" + this.goodFieldName + "_" + this.rowId )
					.val( "checkbox" );
			
			cloneArr.push( $type.get(0) );
			return cloneArr;
		}
		
		return this.appearOnPage() ? [ this.valueElem.clone().val( this.valueElem.val() ) ] : [];	
	},

});/**
 * TextArea control class
 */
Runner.controls.TextArea = Runner.extend( Runner.controls.Control, {
	/**
	 * Override constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {		
		Runner.controls.TextArea.superclass.constructor.call(this, cfg);
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */	
	addEditEditingListeners: function() {
		this.on("change", function(e, updateContext) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			this.fireEvent("edited", updateContext);
		});
		 
		this.on("keyup", function(e, updateContext) {
			if ( !e || !Runner.isAcceptableKeyCode(e) ) {
				return false;
			}
			this.fireEvent("editing", updateContext);
		}, {buffer: 300});
	},
	
	/**
	 * The method is invoked when the "editing" event is fired 
	 */
	"editing": function( nullEvent, updateContext ) {
		this.checkDuplicates();

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );
		
		this.processResolvedEvent( updateContext );
	},		

	"edited": function( nullEvent, updateContext ) {
		var valArr = this.getPreparedValidationArray();

		if ( valArr.length ) {
			this.validate( valArr );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );

		this.processResolvedEvent( updateContext );
	},		
			
	/**
	 * Clone html for iframe submit
	 * @return {array}
	 */
	getForSubmit: function() {
		if ( !this.appearOnPage() ) {
			return [];
		}
		return [ this.valueElem.clone().val( this.getValue() ) ];
	}
});/**
 * Class for text fields control
 */
Runner.controls.TextField = Runner.extend( Runner.controls.Control, {
	/**
	 * Text field input mask
	 * @type string
	 * @intellisense
	 */
	mask: "",

	
	constructor: function( cfg ) {
		Runner.controls.TextField.superclass.constructor.call( this, cfg );
		
		if ( this.isSetValidation('DenyDuplicated') ) {
			this.valueElem.attr('autocomplete', 'off');
		}
		
		if ( this.mask !== "" && this.mask !== false ) {
            this.valueElem.mask( this.mask );
		}	
	},
	
	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		this.on("change", function(e, updateContext) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			this.fireEvent("edited", updateContext);
		});
		 
		this.on("keyup", function(e, updateContext) {
			if ( !e || !Runner.isAcceptableKeyCode(e) ) {
				return false;
			}
			this.fireEvent("editing", updateContext);
		}, { buffer: 300 });
	},
	
	/**
	 * The method is invoked when the "editing" event is fired 
	 */
	"editing": function(nullEvent, updateContext) {
		this.checkDuplicates();

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );

		this.processResolvedEvent( updateContext );	
	},
	
	"edited": function(nullEvent, updateContext) {
		var valArr = this.getPreparedValidationArray();

		if ( valArr.length ) {
			this.validate( valArr );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );

		this.processResolvedEvent( updateContext );	
	}	

});
/**
 * Class for time fields with textField value editor, and timepicker optional
 */
Runner.controls.TimeField = Runner.extend( Runner.controls.Control, {
	/**
	 * Id of type elem. Need for submit, which used on serverside
	 * @type {string}
	 */
	typeHiddId: "",

	/**
	 * jQuery object of type elem format hidden element, which used on serverside
	 * @type {Object}
	 */
	typeHiddElem: null,


	timePmLetter: "PM",
	timeAmLetter: "AM",	
	is24hoursFormat: false,
	

	/**
	 * Overrides parent constructor
	 * @param {Object} cfg
	 * @param {bool} cfg.useDatePicker
	 */
	constructor: function(cfg) {
		Runner.controls.TimeField.superclass.constructor.call(this, cfg);
		this.ctrlType = "time";

		// add hidden field for date format on serverside
		this.typeHiddId = "type_" + this.goodFieldName + "_" + this.rowId;
		this.typeHiddElem = $("#" + this.typeHiddId);

		if ( this.isSetValidation('DenyDuplicated') ) {
			this.valueElem.attr('autocomplete', 'off');
		}

		this.timePmLetter = Runner.pages.PageSettings.getGlobalData("locale").timePmLetter;
		this.timeAmLetter = Runner.pages.PageSettings.getGlobalData("locale").timeAmLetter;

		this.is24hoursFormat = Runner.pages.PageSettings.getGlobalData("locale").is24hoursFormat == "1";

		this.imgTime = $("#trigger-test-" + this.valContId)
			.css('visibility','visible');

		this.tpSettings = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, 'timePick', this.pageType );
		if ( this.tpSettings ) {
			this.is24hoursFormat = this.tpSettings['convention'] == 24; 
			this.initTimePicker();
		}
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		var ctrl = this;

		this.on("change", function( e, updateContext, triggered ) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			this.fireEvent("edited", updateContext);
		});

		this.on("keyup", function(e) {
			if ( !e || !Runner.isAcceptableKeyCode(e) ) {
				return false;
			}
			this.fireEvent("editing");
		}, { buffer: 300 } );


		var changed = false;
		this.valueElem.parents(".input-group")
			.on("dp.show", function(e) {
				changed = false;
			})
			.on("dp.change", function(e) {
				changed = true;
			})
			.on("dp.hide", function(e) {
				if( changed ) {
					ctrl.fireEvent( "change", null );
				}
			});
	},

	"editing": function(nullEvent, updateContext) {
		// override default handler to skip saving
	},

	/**
	 *
	 */
	initTimePicker: function() {
		var	ctrl = this,
			$pickerElem = this.valueElem.parents(".input-group");

		if ( !this.tpSettings ) {
			return;
		}

		this.setCorrectAmPmMoment();
		this.imgTime.on("click", function() {
			// setting global moment
			ctrl.setCorrectAmPmMoment();
		});


		$pickerElem
			.datetimepicker({
				showClose: true,
				format: Runner.getTimeFormatString( this.tpSettings['showSec'], this.is24hoursFormat ),
				stepping: this.tpSettings['minutes'],
			});

		this.setDefaultValue();

		if ( this.pageObj.useResize || this.isInline || this.pageObj.openMode != Runner.pages.constants.OPENMODE_PRELOADED ) {
			$pickerElem.on("dp.show", function(e) {
				Runner.util.absToFixed( $(".bootstrap-datetimepicker-widget", this) );
	        });
		}
	},

	/**
	 * setting global AM/PM moment.js
	 */
	setCorrectAmPmMoment: function() {
		var ctrl = this;

		moment.updateLocale('en', {
    		meridiem : function( hour, minute, isLowercase ) {
    			if ( hour >= 12 ) {
					return ctrl.timePmLetter || 'pm';
				}

				return ctrl.timeAmLetter || 'am';
    		},
		});
	},


	destructor: function() {
		var pk = this.valueElem.parents(".input-group").data('DateTimePicker')
		pk && pk.destroy();
		
		Runner.controls.TimeField.superclass.destructor.call( this );
	},


	/**
	 * Override addValidation
	 * @param {string} type
	 */
	addValidation: function( type ) {
		// date field can be validated only as isRequired or DenyDuplicated
		if ( type != "IsRequired" && type != "DenyDuplicated" || this.mode === Runner.controls.constants.MODE_SEARCH ) {
			return;
		}
		Runner.controls.TimeField.superclass.addValidation.call(this, type);
	},

	/**
	 * Clone html for iframe submit
	 * @method
	 * @return {array}
	 */
	getForSubmit: function() {
		return [ this.valueElem.clone(), this.typeHiddElem.clone() ];
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr true
	 * Sets hidden css style true for image "time"
	 * @method
	 */
	setDisabled: function() {
		if ( this.valueElem.length && this.imgTime ) {
			this.valueElem.get(0).disabled = true;
			this.imgTime.css('visibility','hidden');
			return true;
		}
		return false;
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr false
	 * Sets visible css style true for image "time"
	 * @method
	 */
	setEnabled: function() {
		if ( this.valueElem.length ) {
			this.valueElem.get(0).disabled = false;
			if ( !Runner.isIE ) {
				this.imgTime.css('visibility','visible');
			}
			return true;
		}
		return false;
	},

	makeReadonly: function() {
		Runner.controls.TimeField.superclass.makeReadonly.call( this );
		this.imgTime.hide();
		return true;
	},

	makeReadWrite: function() {
		Runner.controls.TimeField.superclass.makeReadWrite.call( this );
		this.imgTime.show();
		return true;
	},
	
	setValue: function( val, triggerEvent, updContext ) {
		var dateValue = val, 
			showSeconds;
		
		if ( val && typeof val === "string" ) {
			dateValue = new Date( val );
			if ( isNaN( dateValue ) ) {
				// add a temp date to a time string
				dateValue = new Date( '1970-01-01T' + val );
			}
		}
		
		if ( dateValue instanceof Date && !isNaN( dateValue ) ) {
			showSeconds = this.tpSettings ? this.tpSettings['showSec'] : true;
			val = Runner.printTimeFromDate( dateValue, showSeconds, this.is24hoursFormat );
		}
		
		Runner.controls.TimeField.superclass.setValue.call( this, val, triggerEvent, updContext );
	},	
});/**
 * Common base class for rte fields
 */
Runner.controls.RTEField = Runner.extend( Runner.controls.Control, {
	/**
	 * The jQuery object representing the DOM element that will be hidden 
	 * if the control is disabled
	 * @type {object}
	 */
	controlElementToDisable: {}, 
	
	/**
	 * The jQuery object representing the DOM element that will be shown instead
	 * if the control is disabled
	 * @type {object}
 	 */
	disabledContorlElement: {},
	
	/**
	 * The storage for control's custom events listeners
	 * @type {array}
	 */
	delayedEventsHandlers: [],
	
	iframeElemId: "",
	
	iframeElem: null,

	/**
	 * Indicates used datepicker with control or not
	 * @type {bool} cfg
	 */
	useRTE: false,
	
	constructor: function( cfg ) {
		Runner.controls.RTEField.superclass.constructor.call( this, cfg );

		this.delayedEventsHandlers = [];
		this.disabledContorlElement = {};
		this.disabledControlElementId = "disabledRTE" + this.fieldName + '_' + this.rowId;

		//redefine the initCustomEvents method
		this.initCustomEvents = Runner.controls.RTEField.superclass.initCustomEvents;	
	},
	
	/**
	 * The temporary stub preventing the base control's 'initCustomEvents' 
	 * method from beeing invoked from the base contructor
	 */
	initCustomEvents: Runner.emptyFn,
	
	/**
	 * Set the value element and init the iframe element
	 */
	assignValueElem: function() {
		Runner.controls.RTEField.superclass.assignValueElem.call(this);
		this.initIframeElem();	
	},
	
	initIframeElem: function() {
		if ( !this.iframeElemId ) {
			this.iframeElemId = this.valContId;
		}
		if ( !this.iframeElem ) {
			this.iframeElem = $('#' + this.iframeElemId);
		}
	},
	
	/**
	 * Override addValidation
	 * @param {string} type
	 */
	addValidation: function( type ) {
		// date field can be validated only as isRequired or DenyDuplicated
		if ( type != "IsRequired" && type != "DenyDuplicated" || this.mode === Runner.controls.constants.MODE_SEARCH ) {
			return;
		}

		Runner.controls.RTEField.superclass.addValidation.call( this, type );
	},
	
	/**
	 * Get the control's value to submit
	 * @return {array}
	 */ 
	getForSubmit: function() {
		if ( !this.appearOnPage() ) {
			return [];
		}
		
		var clElem = $('<input type="hidden" name="' + this.iframeElemId + '">')//.clone()
				.val( this.getValue() );
				
		return [ clElem ];
	},
	
	setDisabled: function() {
		if ( !this.controlElementToDisable ) {
			return false;
		}
		
		if ( !this.disabledContorlElement.length ) {
			//create the element that will shown instead of the initial control' elements
			this.disabledContorlElement = $('<div id="' + this.disabledControlElementId + '"></div>')
				.prependTo( this.spanContElem );	
		}

		this.controlElementToDisable.hide();
		this.disabledContorlElement.html( this.getValue() ).show();
		
		return true;
	},
	
	setEnabled: function() {
		if ( !this.controlElementToDisable ) {
			return false;
		}
		
		this.controlElementToDisable.show();	
		this.disabledContorlElement.hide();
		
		return true;
	},
	
	makeReadonly: function() {
		var ctrl = this;
		
		this.readOnly = true;

		if ( this.isIframeElemLoaded() ) {
			this.setDisabled();
			return;
		}
		
		this.iframeElem.on('load', function(e) {
			ctrl.setDisabled();
		});		
	},
	
	makeReadWrite: function() {
		var ctrl = this;
		
		this.readOnly = false;
		
		if ( this.isIframeElemLoaded() ) {
			this.setEnabled();
			return;
		}
		
		this.iframeElem.on('load', function(e) {
			ctrl.setEnabled();
		});
	},
	
	/**
	 * Check if the editor's iframe is lodaded or not
	 * @return {Boolean}
	 */
	isIframeElemLoaded: function() {		
		if ( this.loadIframe ) {
			return true;
		}
		
		if ( this.isIframeLoaded( this.iframeElem ) ) {
			this.loadIframe = true;
			return true;
		}
		
		return false;
	},

	/**
	 * Check if an iframe represented by the jQuery object reference is loaded
	 * @param {jQuery element}
	 * @return {Boolean}
	 */
	isIframeLoaded: function( $iframe ) {
		if ( !$iframe.length || !$iframe.is("iframe") ) {
			return false;
		}
		
		var iframeDoc = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
		return iframeDoc.readyState === 'complete';
	},
	
	/**
	 * Show if the control is disabled or not
	 * @return {Boolean}
	 */
	isDisabled: function() {
		return this.disabledContorlElement.length && this.disabledContorlElement.is(':visible');
	},
	
	/**
	 * Add the handler that is fired wheh the custom control events have been already initialized
	 * @param {string} eventName
	 * @param {function} handler
	 * @param {object} options
	 * @param {object} scope
	 */
	addDelayedCustomEvent: function( eventName, handler, options, scope ) {
		this.delayedEventsHandlers.push( {eventName: eventName, handler: handler, options: options, scope: scope} );
	},
	
	/**
	 * The method should be invoked when custom control events are initialized.
	 * It adds to the control custom events handlers
	 */	 
	addDelayedEventsListeners: function() {
		var i, data;
		
		for (i = 0; i < this.delayedEventsHandlers.length; i++) {
			data = this.delayedEventsHandlers[i]; 
			this.on( data.eventName, data.handler, data.options, data.scope );
		}
		
		this.delayedEventsHandlers = [];
	},

	/**
	 * Add events to the control's object
	 * @param {string} eventName
	 * @param {function} handler
	 * @param {object} options
	 * @param {object} scope
	 */
	on: function( eventName, fn, options, scope ) {
		if ( this.eventsAreInitialized ) {
			this.on = Runner.controls.Control.superclass.on;
			this.on( eventName, fn, options, scope );
			return;
		}
		this.addDelayedCustomEvent( eventName, fn, options, scope );
	}
});


/**
 * Innova rte without outer iframe
 */
Runner.controls.RTEInnova = Runner.extend( Runner.controls.RTEField, {
	/**
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {	
		Runner.controls.RTEInnova.superclass.constructor.call( this, cfg );

		this.disabledControlElementId = "disabledInnova_" + this.valContId;
		this.controlElementToDisable = $( "#" + this.disabledControlElementId );
		
		this.initEditor();

		this.initControlEvents();
	},

	isIframeElemLoaded: function() {
		return true;
	},

	initEditor: function() {
		// it must be global
		window[ "Editor" + this.valContId ] = new InnovaEditor( "Editor" + this.valContId );
		window[ "Editor" + this.valContId ].mode = "HTMLBody";
		window[ "Editor" + this.valContId ].height = $( "#" + this.valContId + "innova" ).height() + "px";

		// plain view
		//window["Editor" + this.valContId].useTab = false;

		// to show asset manager in editor's dialogs
		var assetManagerUrl = $( "#disabledInnova_" + this.valContId ).data("am");
		window[ "Editor" + this.valContId ].cmdAssetManager = "modalDialogShow( '" + assetManagerUrl + "', 640, 445 )";


		window[ "Editor" + this.valContId ].REPLACE( this.valContId, this.valContId + "innova" );
		this.editor = window[ "Editor" + this.valContId ];
	},

	destructor: function() {
		delete this.editor;
		delete window[ "Editor" + this.valContId ];		
	},

	/**
	 * Set the control's events list
	 */	
	assignListOfEvents: function() {
		this.addEvent( ["keyup", "editing", "edited"] );
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		var bodyElem = this.getEditorBodyElem();
		this.elemsForEvent = [ bodyElem.get(0) ];
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		this.on("keyup", function(e) {
			if ( !e || !Runner.isAcceptableKeyCode(e) ) {
				return;
			}
			this.fireEvent("edited");
			this.fireEvent("editing");
		});
		this.addDelayedEventsListeners();
	},
	
	/**
	 * Init the contol's custom events 
	 * basing on the editor's state
	 */
	initControlEvents: function() {
		var ctrl = this;

		this.innerIframeId = this.innerIframeId || 'idContentEditor' + this.valContId;
		var $innerIframe = $( '#' + this.innerIframeId );
		if ( !this.isIframeLoaded( $innerIframe ) ) {
			$innerIframe.on('load', function(e) {
				ctrl.initCustomEvents();
			});
			return;	
		}

		//the editor's iframe has already loaded
		var pageObj = Runner.pages.PageManager.getAt( this.table, this.rowId );
		if ( pageObj.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			pageObj.on("windowSizeCorrected", function() {
				ctrl.initCustomEvents();
			});
			return;
		}

		ctrl.initCustomEvents();		
	},

	/**
	 * Get the body element's jQuery object from the editor's iframes hierarchy
	 * @return {object}
	 */
	getEditorBodyElem: function() {		
		//innova editor
		if ( !this.innerIframeId ) {
			this.innerIframeId = 'idContentEditor' + this.valContId;
		}
		return $( '#' + this.innerIframeId ).contents().find('body');
	},

	getValue: function() {
		if ( !this.editor ) {
			return this.valueElem.val();
		}
		
		var val = this.editor.getHTMLBody();
		// wrapping val with "div" helps then to get all the Dom text nodes with the jQuery text() method 	
		var $valDiv = $('<div>' + val + '</div>');		
		if ( !$valDiv.find(":not(br)").length && !$valDiv.text().trim() ) {
			// the field value contains only "br" tags and space marks
			return '';
		}
		
		return val;
	},
	
	/**
	 * Set the control's value 
	 * @param {String} val
	 */	
	setValue: function( val ) {
		var readOnly = this.isReadonly();

		readOnly && this.makeReadWrite();
		this.getEditorBodyElem().html( val );
		readOnly && this.makeReadonly();
	},

	setFocus: function( triggerEvent ) {
		if ( !this.appearOnPage() || !this.showStatus || this.isDisabled() ) {
			return;
		}
		
		this.scrollToElem( this.valueElem );
		this.getEditorBodyElem().focus();
		//this.editor && this.editor.focus();
		
		if ( triggerEvent === true ) {
			this.fireEvent("focus");
		}
	}
});


Runner.controls.RTECKBase = Runner.extend( Runner.controls.RTEField, {
	/**
	 * CK editor instance
	 */
	editor: null,
	
	constructor: function( cfg ) {
		Runner.controls.RTECKBase.superclass.constructor.call( this, cfg );
		this.controlElementToDisable = $("#disabledCKE_" + this.valContId);
		
		this.initCKEditor();
		
		if ( this.editor ) {
			this.initCustomEvents();
		}		
	},

	getEditor: function() {
		return this.editor || false;
	},
	
	/**
	 * Set the control's events list
	 */		
	assignListOfEvents: function() {
		this.addEvent( ["edited", "editing"] );
	},	

	getValue: function() {
		return this.editor ? this.editor.getData() : false;
	},
	
	setValue: function( val ) {
		var readOnly = this.isReadonly();

		if ( readOnly ) {
			this.makeReadWrite();
		}					
		
		if ( !this.editor ) {
			readOnly && this.makeReadonly();		
			return false;	
		}
		
		this.editor.setData( val );
		readOnly && this.makeReadonly();
		return true;	
	},
	
	makeReadonly: function() {
		this.readOnly = true;
		this.setDisabled();
	},
	
	makeReadWrite: function() {
		this.readOnly = false;
		this.setEnabled();
	},
		
	/**
	 * Sets focus to the element.
	 * @method
	 * @return {boolean}
	 * @intellisense
	 */
	setFocus: function( triggerEvent ) {
		var pageObj,
			ctrl = this;
		
		if ( !this.appearOnPage() || !this.showStatus || this.isDisabled() ) {
			return false;
		}
			
		pageObj = Runner.pages.PageManager.getAt( this.table, this.rowId );
		if ( pageObj.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			pageObj.on("windowSizeCorrected", function() {
				ctrl.setFocusOnEditor();
			});
		}
	
		this.setFocusOnEditor();
		
		if ( triggerEvent === true ) {
			this.fireEvent("focus");
		}
		
		return true;
	}	
});

Runner.controls.RTECK = Runner.extend( Runner.controls.RTECKBase, {
	/**
	 *
	 */
	initCKEditor: function() {
		if ( !this.appearOnPage() || !window.CKEDITOR ) {
			return;
		}
		
		var nHeight = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName,'nHeight', this.pageType );
	
		//v4: replace the existing <textarea> element with CKEditor.
		CKEDITOR.replace( this.valContId, { height: nHeight } );
		
		this.editor = window.CKEDITOR.instances[ this.valContId ];
	},	

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		var ctrl = this;	
			
		if ( !this.editor ) {
			return;	
		}	
			
		this.editor.on("instanceReady", function() { 
			ctrl.editor.document.on("keyup", function(e) {
				if ( !e || !Runner.isAcceptableKeyCode(e) ) {
					return;
				}			
				ctrl.fireEvent("editing");
			});
		});	

		this.editor.on("blur", function(e) {	
			ctrl.fireEvent("edited");
		});
		
		this.addDelayedEventsListeners();
	},	
	
	destructor: function() {
		this.editor && CKEDITOR.remove( this.editor );
	},

	/**
	 * Set focus on the editor's element
	 */
	setFocusOnEditor: function() {
		var ctrl = this;
			
		if ( !this.editor ) {
			return;
		}
		
		if ( !this.editor.container ) {
			this.editor.on('instanceReady', function() { 
				ctrl.editor.focus();
				ctrl.scrollToElem( $( ctrl.editor.container.$ ) );
			});
			return;	
		}
		
		this.editor.focus();
		this.scrollToElem( $( this.editor.container.$ ) );
	},
});

Runner.controls.RTECK5 = Runner.extend( Runner.controls.RTECKBase, {
	/**
	 *
	 */
	creationPromise: null,
	
	/**
	 *
	 */
	initCKEditor: function() {
		if ( !this.appearOnPage() ) {
			return;
		}
			
		var ctrl = this;
		
		this.loadClassicEditor( function() {		
			if ( !window.ClassicEditor ) {
				return;
			}
				
			ctrl.creationPromise = ClassicEditor
				.create( document.querySelector( '#' + ctrl.valContId ) )
				.then( function( editor ) {
					ctrl.editor = editor;
					ctrl.initCustomEvents();
				});
		});
	},
	
	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		var ctrl = this,
			editor = this.getEditor();	
			
		if ( !editor ) {
			return;	
		}	

		this.editor.model.document.on( 'change:data', function( evt, name, value) {
			ctrl.fireEvent("editing");
			// ctrl.fireEvent("edited"); ?
		});
		
		this.editor.editing.view.document.on( 'change:isFocused', function( evt, name, value ) {
			if ( !value ) {
				ctrl.fireEvent("edited");
			}
		});
		
		this.addDelayedEventsListeners();
	},	
	
	destructor: function() {
		this.editor && this.editor.destroy();		
	},
	
	/**
	 * Set focus on the editor's element
	 */
	setFocusOnEditor: function() {
		var ctrl = this;
			
		if ( this.editor ) {
			this.editor.editing.view.focus();
			return;
		}
		
		if ( !this.creationPromise ) {
			// smth goes wrong
			return;
		}
		
		this.creationPromise.then( function() {
			ctrl.editor.editing.view.focus();
		});
	},
	
	loadClassicEditor: function( callback ) {
		var path = settings.global["webRootPath"] + 'plugins/ckeditor/ckeditor.js';

		Runner.util.ScriptLoader.addJS( [ path ] );
		Runner.util.ScriptLoader.onFilesLoaded( function() {
			callback();
		});
		Runner.util.ScriptLoader.load();		
	}
});


Runner.controls.RTETinyMCE = Runner.extend( Runner.controls.RTEField, {	
	/**
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {	
		Runner.controls.RTETinyMCE.superclass.constructor.call( this, cfg );

		this.disabledControlElementId = "disabledTinyMCE_" + this.valContId;
		this.controlElementToDisable = $( "#" + this.disabledControlElementId );
		
		this.initEditor();
	},
	
	isIframeElemLoaded: function() {
		return true;
	},

	initEditor: function() {
		var ctrl = this;
		
		const settings = { 
			selector: "#" + this.valContId,
			menubar: false,
			branding: false,
			setup: ( editor ) => {
				this.editor = editor;
				editor.on('init', ( e ) => {
					this.setDefaultValue();
					this.initCustomEvents();
					
					// ugly fix
					$("[title='Insert/edit image']", editor.getContainer())
						.on("click", function() {
							if ( $( this.pageObj.pageCont ).is(".bs-popup") ) {
								$( this.pageObj.pageCont ).removeAttr("tabIndex");
							}
						});
							
				});		

				if ( this.valueElem.closest(".bs-popup").length ) {
					// see Modal.prototype.enforceFocus 
					// mce plugins markup is outside the popup content
					// plugin element focus simulates popup focus and then stopes
					$(document).off("focusin.bs.modal");
				}					
			},
			plugins: 'lists code link image charmap hr table',
			height: $("#" + this.valContId).height(),
			toolbar: [ 'styleselect fontselect fontsizeselect  forecolor backcolor table',
			  'bold italic underline |  alignleft aligncenter alignright  hr | ' + 
			  ' bullist numlist outdent indent | code link image charmap '],	  
			/*inline: ctrl.isInline,*/  
		};
		if( Runner.globalEvents.EVENT_JS_CREATE_TINYMCE ) {
			Runner.globalEvents.EVENT_JS_CREATE_TINYMCE.call( this, this.table, this.fieldName, settings, this.pageObj.proxy, this.pageObj );
		} else {
			tinymce.init( settings );
		}
	},
	
	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		if ( !this.editor ) {
			return;	
		}	

		var ctrl = this;		
		this.editor.on( "input Change keyup", tinymce.util.Delay.debounce( function( e ) {
				if ( e && e.type === "keyup" && !Runner.isAcceptableKeyCode( e ) ) {
					return false;
				}

				ctrl.fireEvent("editing");
				ctrl.fireEvent("edited");
			}, 200 ) );

		this.addDelayedEventsListeners();
	},
	
	getValue: function() {
		return this.editor ? this.editor.getContent() : "";
	},
	
	setValue: function( val ) {
		var readOnly = this.isReadonly();

		if ( readOnly ) {
			this.makeReadWrite();
		}
		
		if ( !this.editor ) {
			readOnly && this.makeReadonly();
			return false;	
		}
		
		this.editor.setContent( val );
		readOnly && this.makeReadonly();
		return true;	
	},	

	makeReadonly: function() {
		this.readOnly = true;
		this.editor && this.editor.mode.set( "readonly" );
	},
	
	makeReadWrite: function() {
		this.readOnly = false;
		this.editor && this.editor.mode.set( "design" );
	},
	
	setFocus: function( triggerEvent ) {
		if ( !this.appearOnPage() || !this.showStatus || this.isDisabled() ) {
			return;
		}
		
		var ctrl = this;
		this.onEditrotInitialized( function(e) {
				ctrl.editor.focus();
				
				// to the end of content
				ctrl.editor.selection.select( ctrl.editor.getBody(), true );
				ctrl.editor.selection.collapse( false );
			});
		
		if ( triggerEvent === true ) {
			this.fireEvent("focus");
		}
	},

	focused: function() {
		return this.editor && this.editor.hasFocus();
	},
	
	onEditrotInitialized: function( callback ) {
		if ( !this.editor ) {
			returnl
		}
		
		this.editor.initialized && callback();
		!this.editor.initialized && this.editor.on('init', function(e) {
				callback();
			});
	},
	
	destructor: function() {
		this.editor && this.editor.destroy();
		delete this.editor;
	},
});/**
 * Base abstract class for all file controls. Should not be created directly.
 * @requires Runner.controls.Control
 * @class Runner.controls.FileControl
 */
Runner.controls.FileControl = Runner.extend( Runner.controls.Control, {
	/**
	 * Radio DOM elem id
	 * @type {string}
	 */
	radioElemsName: "",
	/**
	 * Radio jQuery obj
	 * @type {Object}
	 */
	radioElems: null,
	/**
	 * Is control empty or not
	 * @type {Object}
	 */
	notEmptyCntrl: 0,

	/**
	 * Override parent contructor
	 * @constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {
		this.radioElems = {};

		Runner.controls.FileControl.superclass.constructor.call(this, cfg);

		var notEmpty = $("#notempty_" + this.goodFieldName + "_" + this.rowId);
		this.notEmptyCntrl = parseInt( $(notEmpty).val() );
	},

	/**
	* Set the value element
	*/
	assignValueElem: function() {
		Runner.controls.FileControl.superclass.assignValueElem.call(this);

		this.radioElemsName = "type_" + this.goodFieldName + "_" + this.rowId;
		// for ASP version in inline add mode, there are no radios, but one input type hidden
		if ( $('#' + this.radioElemsName).length ) {
			this.getChekedRadio = function() {
				return false;
			}
		}
		// add radio DOM elem ID,
		this.getRadioControls();
	},

	/**
	* Set the DOM elements for the custom listeners usage
	*/
	assignListOfEvents: function() {
		this.addEvent(["change", "click", "edited", "editing"]);
	},

	/**
	 * Add change event base handler
	 * @param {Object} e
	 */
	"change": function(e) {
		// no idea
		if ( e ) {
			this.stopEvent(e);
		}
		// set radio button to update
		this.changeRadio("updateRadio");
	},

	/**
	 * Radio buttons switcher. Call when need change radio
	 * @param {string} radioToCheck Name of radio button.
	 */
	changeRadio: function(radioToCheck) {
		for (var radio in this.radioElems) {
			// if exists radio button
			if (radio == radioToCheck && this.radioElems[radio]!=false) {
				this.radioElems[radio].elem.get(0).checked = true;
				this.radioElems[radio].cheked = true;
			// if not exists return false
			} else if (radio == radioToCheck && this.radioElems[radio]==false) {
				return false;
			// switch other radios
			} else if (this.radioElems[radio]!=false) {
				this.radioElems[radio].elem.get(0).checked = false;
				this.radioElems[radio].cheked = false;
			}
		}
		// in success
		return true;
	},

	/**
	 * Get object which contains radio elems
	 * @method
	 */
	getRadioControls: function() {
		var keepRadio = $('#' + this.radioElemsName + '_keep'),
			deleteRadio = $('#' + this.radioElemsName + '_delete'),
			updateRadio = $('#' + this.radioElemsName + '_update'),
			ctrl = this;

		keepRadio.on('click', function(e) {
			ctrl.changeRadio('keepRadio');
		});
		deleteRadio.on('click', function(e) {
			ctrl.changeRadio('deleteRadio');
		});
		updateRadio.on('click', function(e) {
			ctrl.changeRadio('updateRadio');
		});
		// create radioElems obj
		this.radioElems["keepRadio"] = keepRadio.length ? {elem: keepRadio, cheked: true} : false;
		this.radioElems["deleteRadio"] = deleteRadio.length ? {elem: deleteRadio, cheked: false} : false;
		this.radioElems["updateRadio"] = updateRadio.length ? {elem: updateRadio, cheked: false} : false;
	},

	/**
	 * Return name of cheked radio
	 * @return {string}
	 */
	getChekedRadio: function() {
		for (var radio in this.radioElems) {
			if (this.radioElems[radio]!=false && this.radioElems[radio].cheked === true) {
				return radio;
			}
		}
		return false;
	},

	validate: function(valArr) {

		return Runner.controls.FileControl.superclass.validate.call(this, valArr);
	},

	/**
	 * Returns array of jQuery object for inline submit
	 * @return {array}
	 */
	getForSubmit: function() {
		// array of fileValue, and cheked radio
		var radio = this.getChekedRadio();
		var cloneArr = [];
		// make real clone of radio, to prevent troubles in IE
		if (radio) {
			var radioClone = document.createElement('input');
			$(radioClone).attr('type', 'hidden');
			$(radioClone).attr('id', this.radioElems[radio].elem.attr('id'));
			$(radioClone).attr('name', this.radioElems[radio].elem.attr('name'));
			$(radioClone).val(this.radioElems[radio].elem.val());
			cloneArr.push($(radioClone));
		// for ASP version in inline add mode, there are no radios, but one input type hidden
		} else if ($('#' + this.radioElemsName).length) {
			cloneArr.push($('#' + this.radioElemsName));
		}
		// add real file elem
		var realFile = this.valueElem;
		var clone = this.valueElem.clone(true);
		clone.insertAfter(realFile);
		cloneArr.push(realFile);
		this.valueElem = clone;
		return cloneArr;
	},

	makeReadonly: function() {
		this.readOnly = true;
		
		this.valueElem.attr('disabled',true);
		if (this.radioElems.keepRadio) {
			this.radioElems.keepRadio.elem.attr('disabled',true);
		}
		if (this.radioElems.updateRadio) {
			this.radioElems.updateRadio.elem.attr('disabled',true);
		}
		if (this.radioElems.deleteRadio) {
			this.radioElems.deleteRadio.elem.attr('disabled',true);
		}
		return true;
	},

	makeReadWrite: function() {
		this.readOnly = false;
		
		this.valueElem.attr('disabled', false);
		if (this.radioElems.keepRadio) {
			this.radioElems.keepRadio.elem.attr('disabled',false);
		}
		if (this.radioElems.updateRadio) {
			this.radioElems.updateRadio.elem.attr('disabled',false);
		}
		if (this.radioElems.deleteRadio) {
			this.radioElems.deleteRadio.elem.attr('disabled',false);
		}
		return true;
	},

	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * For files has specific criterias
	 * @override
	 * @method
	 * @return {bool}
	 */
	isEmpty: function() {
		if (this.mode == Runner.pages.constants.PAGE_EDIT || this.mode == Runner.pages.constants.PAGE_INLINE_EDIT) {
			if (this.notEmptyCntrl) {
				return false;
			} else {
				if (typeof(this.fileNameElem) != 'undefined' && this.fileNameElem) {
					if (this.fileNameElem.get(0).value == '') {
						this.fileNameElem.get(0).value = this.fileNameElem.get(0).defaultValue;
					}
				}
				return (this.radioElems["keepRadio"].cheked === true || this.getValue().toString() == "" || this.radioElems["updateRadio"].cheked === false)
			}
		} else {
			if (typeof(this.fileNameElem) != 'undefined' && this.fileNameElem) {
				if (!(this.getValue().toString() == "") && (this.fileNameElem.get(0).value == ''))
					this.fileNameElem.get(0).value = this.fileNameElem.get(0).defaultValue;
			}
			return (this.getValue().toString() == "")
		}
	},	

	/**
	 * Set to radio buttons default value
	 */
	resetRadio: function() {
		for (var radio in this.radioElems) {
			// if exists radio button
			if (this.radioElems[radio]!=false) {
				this.radioElems[radio].elem.get(0).checked = this.radioElems[radio].elem.get(0).defaultChecked;
				this.radioElems[radio].cheked = this.radioElems[radio].elem.get(0).defaultChecked;
			}
		}
	}
});

/**
 * Class for image field controls.
 * @requires Runner.controls.FileControl
 * @class Runner.controls.ImageField
 */
Runner.controls.ImageField = Runner.extend( Runner.controls.FileControl, {

	imgElem: null,
	/**
	 *
	 * @type
	 */
	acceptFileTypes: undefined,
	/**
	 * Override parent contructor
	 * @constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {
		Runner.controls.ImageField.superclass.constructor.call(this, cfg);
		if (cfg.acceptFileTypes) {
			this.addValidation("AcceptFileTypes");
		}
	},

	/**
	* Set the value element
	*/
	assignValueElem: function() {
		Runner.controls.ImageField.superclass.assignValueElem.call(this);

		this.imgElemId = "image_" + this.goodFieldName + "_" + this.rowId;
		this.imgElem = $("#" + this.imgElemId);
	},

	setValue: function(val, triggerEvent, updContext) {
		var src = $(val).attr('src');

		if ( src ) {
			this.imgElem.attr('src', (src + "&rndVal=" + Math.random()) );
			return;
		}

		Runner.controls.ImageField.superclass.setValue.call(this, val, triggerEvent, updContext);
		if ( updContext && updContext.resetHappend ) {
			this.resetRadio();
		}
	}
});

/**
 * Class for file field controls. For images use Runner.controls.ImageField
 * @requires Runner.controls.FileControl
 * @class Runner.controls.FileField
 */
Runner.controls.FileField = Runner.extend( Runner.controls.FileControl, {
	/**
	 * Indicates if need to add timeStamp to fileName
	 * @type {bool}
	 */
	addTimeStamp: false,
	/**
	 * ID of filename elem
	 * @type {string}
	 */
	fileNameElemId: "",
	/**
	 * Filename textfield jQuery object
	 * @param {Object}
	 */
	fileNameElem: null,
	/**
	 * ID of hidden fileName DOM elem
	 * @type String
	 */
	fileHiddElemId: "",
	/**
	 * jQuery object of hidden fileName DOM elem
	 * @type {object}
	 */
	fileHiddElem: null,
	/**
	 *
	 * @type
	 */
	acceptFileTypes: undefined,
	/**
	 * Override parent contructor
	 * @constructor
	 * @param {Object} cfg
	 * @param {bool} cfg.addTimeStamp
	 */
	constructor: function(cfg) {
		Runner.controls.FileField.superclass.constructor.call(this, cfg);

		// add fileName hidden DOM elem
		this.fileHiddElemId = "filenameHidden_" + this.goodFieldName + "_" + this.rowId;
		this.fileHiddElem = $("#" + this.fileHiddElemId).length ? $("#" + this.fileHiddElemId) : null;

		// add radio buttons style switchers
		for (var radio in this.radioElems) {
			// if exists radio
			if (this.radioElems[radio]) {
				// create closure event handler
				var objScope = this;
				// add handler
				this.radioElems[radio].elem.on('click', function(e) {
					// get name of radio object
					var radioTypeStartFrom = this.id.lastIndexOf('_');
					var radioTypeName = this.id.substring(radioTypeStartFrom + 1) + 'Radio';
					// change styles
					objScope.changeControlsStyles(radioTypeName);
				});
			}
		}

		if (cfg.acceptFileTypes) {
			this.addValidation("AcceptFileTypes");
		}
	},

	/**
	* Set the value element
	*/
	assignValueElem: function() {
		Runner.controls.FileField.superclass.assignValueElem.call(this);
		// add fileName DOM elem
		this.fileNameElemId = "filename_" + this.goodFieldName + "_" + this.rowId;
		this.fileNameElem = $("#" + this.fileNameElemId).length ? $("#" + this.fileNameElemId) : null;
		//timeStamp to fileName indicator
		this.addTimeStamp = this.addTimeStamp || Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, 'isUseTimeStamp', this.pageType);
	},

	/**
	 * Override addValidation
	 * @method
	 * @param {string} type
	 */
	addValidation: function(type) {
		// date field can be validated only as isRequired or DenyDuplicated
		if ( type != "IsRequired" && type != "DenyDuplicated" && type != "AcceptFileTypes" || this.mode === Runner.controls.constants.MODE_SEARCH ) {
			return;
		}
		// call parent
		Runner.controls.FileField.superclass.addValidation.call(this, type);
	},
	/**
	 * Cuts name of file from path
	 * @param {string} path
	 * @return {string}
	 */
	getFileNameFromPath: function(path) {
		var wpos=path.lastIndexOf('\\');
		var upos=path.lastIndexOf('/');
		var pos=wpos;
		if (upos>wpos)
			pos=upos;
		return path.substr(pos+1);
	},
	/**
	 * Override setValue function, for files need to change radio control status
	 * @method
	 * @param {file} val
	 */
	setValue: function(val, triggerEvent, updContext) {
		var valWithStamp = "",
			fileName = "",
			readOnly = this.isReadonly();

		if ( readOnly ) {
			this.makeReadWrite();
		}

		// if need to get filename without path
		if (this.fileNameElem != null || this.addTimeStamp) {
			fileName = this.getFileNameFromPath(this.valueElem.val());
		}
		// add timestamp if needed
		if (this.addTimeStamp) {
			valWithStamp = this.addTimestamp(fileName);
		}
		// if name element exists, set new value
		if (this.fileNameElem != null) {
			this.fileNameElem.val(valWithStamp || fileName);
			this.fileNameElem.get(0).defaultValue = (valWithStamp || fileName);
		}
		//check if
		if (updContext && updContext.resetHappend) {
			this.valueElem.val(updContext.values[this.fieldName]);
			if (this.fileNameElem != null) {
				this.fileNameElem.val(this.fileNameElem.get(0).defaultValue);
			}
			this.resetRadio();
		}

		if ( triggerEvent === true ) {
			this.fireEvent("change", updContext);
		}

		if ( readOnly ) {
			this.makeReadonly();
		}
	},

	/**
	 * Add timestap to file name
	 * @param {string} file name
	 * @return {string}
	 */
	addTimestamp: function(filename) {
		var wpos = filename.lastIndexOf('.');
		if (wpos<0) {
			return filename + '-' + this.getTimestamp();
		}
		return filename.substring(0, wpos) + '-' + this.getTimestamp() + filename.substring(wpos);
	},

	/**
	 * Get timestap
	 * @return {string}
	 */
	getTimestamp: function() {
		var ts = "", now = new Date();
		ts += now.getFullYear();
		ts += this.padDateValue(now.getMonth() + 1, false);
		ts += this.padDateValue(now.getDate(), false) + '-';
		ts += this.padDateValue(now.getHours(), false);
		ts += this.padDateValue(now.getMinutes(), false);
		ts += this.padDateValue(now.getSeconds(), false);
		return ts;
	},

	/**
	 * Pad date value
	 * @param {integer} value of date
	 * @param {boolean} use three-digits or not
	 * @return {string}
	 */
	padDateValue: function(value,threedigits) {
		if (!threedigits) {
			if (value>9) {
				return '' + value;
			}
			return '0' + value;
		}

		if (value > 9) {
			if (value > 99) {
				return '' + value;
			}
			return '0' + value;
		}
		return '00' + value;
	},

	/**
	 * Change file value event handler.
	 * Changes radio to update, validates, and change fileName if file pass validation
	 * @method
	 * @param {Object} e
	 */
	"change": function(e) {
		//	no idea
		if ( e ) {
			this.stopEvent(e);
		}

		var vRes = null;
		if (!arguments[1] || !arguments[1].resetHappend) {
			this.changeRadio("updateRadio");
			vRes = this.validate();
			if (vRes.result) {
				var vl = this.getValue();
				this.setValue(vl, false);
			}
		}
		else
			vRes = this.validate();
	},

	/**
	* The custom event "edited" is fired after the control is edited
	* @param {object} nullEvent		Equals to null if function is invoked
	* with the fireEvent method
	* @param {object} updateContext
	*/
	"edited":  function(eNull, updateContext) {
	},

	/**
	 * Override radio buttons switcher, add call change styles method
	 * @param {string} radioToCheck
	 */
	changeRadio: function(radioToCheck) {
		// change styles
		this.changeControlsStyles(radioToCheck);
		// call parent
		Runner.controls.FileField.superclass.changeRadio.call(this, radioToCheck);
	},
	/**
	 * Change styles and set disabled filename field
	 * @param {Object} radioToCheck
	 */
	changeControlsStyles: function(radioToCheck) {
		// if such radio button defined
		if (!this.radioElems[radioToCheck]) {
			return false;
		}
		// if there is filename that need to be changed
		if (this.fileNameElem == null) {
			return false;
		}
		// if choosed delete
		if (radioToCheck == "deleteRadio") {
			//this.fileNameElem.css('backgroundColor','gainsboro');
			this.fileNameElem[0].disabled=true;
			return true;
		// if choosed update or keep
		}
		if (radioToCheck == "updateRadio" || radioToCheck == "keepRadio") {
			//this.fileNameElem.css('backgroundColor','white');
			this.fileNameElem[0].disabled=false;
			return true;
		// in other way return false
		}
		return false;
	},

	/**

	 * Get fileName from fileName type text elem.
	 * @return {string}
	 */
	getFileName: function() {
		if (this.fileHiddElem) {
			return this.fileHiddElem.val();
		} else {
			return false;
		}
	},
	/**
	 * Set fileName to fileName type text elem.
	 * @param {string} fileName
	 * @return {Boolean}
	 */
	setFileName: function(fileName) {
		if (this.fileHiddElem) {
			this.fileHiddElem.val(fileName);
			return true;
		} else {
			return false;
		}
	},
	/**
	 * Returns array of jQuery object for inline submit
	 * @return {array}
	 */
	getForSubmit: function() {
		var cloneArr = Runner.controls.ImageField.superclass.getForSubmit.call(this);
		if (this.fileNameElem) {
			cloneArr.push(this.fileNameElem.clone());
		}
		if (this.fileHiddElem) {
			cloneArr.push(this.fileHiddElem.clone());
		}
		return cloneArr;
	},
	
	/**
	 * A fast changing in a control value may cause 
	 * a race condition with spreadsheet mode submitting
	 * @return boolean
	 */ 
	spEasyChangeable: function() {
		return false;
	},
});
/**
 * Class for multiple file field controls.
 * @requires Runner.controls.Control
 * @class Runner.controls.MultiUploadField
 */
Runner.controls.MultiUploadField = Runner.extend( Runner.controls.Control, {
	/**
	 * From object for files upload
	 * @type {jQuery object}
	 */
	uploadForm: null,
	
	fileArray: null,
	
	maxFileSize: undefined,
	
	maxTotalFilesSize: undefined,
	
	maxNumberOfFiles: undefined,
	
	acceptFileTypes: undefined,

	fileStorageProvider: undefined,
	
	isInline: false,
	
	/**
	 * Indicator whether an error happend during an upload  
	 */
	errorHappened: false,
	
	/**
	 * Indicator whether an error happend during add file  
	 */
	errorHappenedOnAdd: false, 
	
	/**
	 * Count of files which are waiting for upload
	 */
	filesToUploadCount: 0,
	
	$dropZone: null,

	$filesTable: null,

	$progressBar: null,
	$progressLabel: null,

	/**
	 * Override parent contructor
	 * @constructor
	 * @param {Object} cfg
	 */
	constructor: function(cfg) {
		Runner.controls.MultiUploadField.superclass.constructor.call(this, cfg);
		this.initTemplates();
	},

	cloudUpload: function() {
		return ![ Runner.stpDISK, Runner.stpWASABI ].includes( this.fileStorageProvider );
	},

	/**
	* Set the control's events list
	*/
	assignListOfEvents: function() {
		this.addEvent(["click", "change", "edited", "editing"]);
	},
	
	/**
	 *
	 */
	initTemplates: function() {
		var control = this;
		this.uploadForm = $("#fileupload_" + this.goodFieldName + "_" + this.rowId);
		this.$progressBar = this.uploadForm.find('.progress');
		this.$progressLabel = this.uploadForm.find('.progress-extended');
		this.$filesTable = this.uploadForm.find('.mupload-files');
		
		if ( !this.uploadForm.length ) {
			return;
		}
		
		var $buttonAddFiles = $( ".filesUpload", this.uploadForm[0] ),
			$fileInput = $( "input.fileinput-button-input", this.uploadForm[0] ),
			buttonPos = Runner.util.getHiddenDimensions( $buttonAddFiles );
		
		$buttonAddFiles.css("position", "relative");
		$fileInput.css( {
			"left": 0,
			"top": 0,
			"width": buttonPos.outerWidth + "px",
			"height": buttonPos.outerHeight + "px"
		});
		
		this.fileArray = $("#value_" + this.goodFieldName + "_" + this.rowId);
		this.uploadForm[0].action += "?table=" + encodeURIComponent(this.table) 
			+ "&field=" + encodeURIComponent(this.fieldName) 
			+ "&pageType=" + this.pageType
			+ "&page=" + this.pageObj.pageName
			+ "&formStamp=" + $("#formStamp_" + this.goodFieldName + "_" + this.rowId, this.uploadForm).val()
			+ "&newmupload=true";
			
		this.uploadForm.fileupload();
		
		this.initialFileArray = JSON.parse( this.fileArray.val() );
		
		var maxNumberOfFiles = this.maxNumberOfFiles 
				? this.maxNumberOfFiles - this.initialFileArray.length 
				: 0;
		
		var options = {
			autoUpload: true,
			acceptFileTypes: new RegExp(this.acceptFileTypes, "i"),
			dropZone: this.uploadForm,
			maxFileSize: this.maxFileSize * 1024,
			maxTotalFilesSize: this.maxTotalFilesSize,
			maxNumberOfFiles: maxNumberOfFiles || null,
			pasteZone: this.uploadForm, 
			messages: {
				maxNumberOfFiles: maxNumberOfFiles > 1
					? Runner.lang.constants.ERROR_MAX_NUMBER_OF_FILES_MANY.replace('%s', this.maxNumberOfFiles) 
					: Runner.lang.constants.ERROR_MAX_NUMBER_OF_FILES_ONE.replace('%s', this.maxNumberOfFiles) ,
				maxFileSize: Runner.lang.constants.ERROR_MAX_FILE_SIZE.replace('%s', this.maxFileSize),
				minFileSize: Runner.lang.constants.ERROR_MIN_FILE_SIZE.replace('%s', this.maxFileSize),	
				acceptFileTypes: Runner.lang.constants.ERROR_ACCEPT_FILE_TYPES,
			}
		};
		

		var submitFn = function( callbackFn, e, data ) {
			callbackFn();
		};

		if( this.cloudUpload() ) {
			submitFn = function( callbackFn, e, data ) {
				var fileupload = $(this).data("blueimpFileupload");
				
				var fileData = {
					error: data.files[0].error,
					name: data.files[0].name,
					type: data.files[0].type,
					size: data.files[0].size
				}
				
				var form = data.form && data.form.length 
					? data.form[0]
					: fileupload.options.fileInput.prop('form');
				
				$.post( form.action, { file: JSON.stringify( fileData ), _action: "POST" }, function (result) {
					if( result.error ) {
						data.files[0].error = result.error;
						fileupload._trigger('fail', e, data);
						return;
					}
					data.url = result.uploadParams.url;
					data.uploadParams = result.uploadParams.data;
					data.createdFile = result.file;
					callbackFn();
				});
				return false;
			}
		}
		// google drive/onedrive - start uploading
		if ( this.fileStorageProvider == Runner.stpGOOGLEDRIVE
			|| this.fileStorageProvider == Runner.stpONEDRIVE
			|| this.fileStorageProvider == Runner.stpDROPBOX
			|| this.fileStorageProvider == Runner.stpAZUREBLOB )  {

			options.send = function( e, options ) {
				options.type = 'PUT';
				if ( control.fileStorageProvider == Runner.stpDROPBOX ) {
					options.type = 'POST';
					options.contentType = 'application/octet-stream';
					
				}
				var file = options.data.getAll('files[]')[0];
				if ( control.fileStorageProvider == Runner.stpONEDRIVE && file.size ) {
					// fix for OneDrive + Azure Business #16763
					options.headers = options.headers || {};
					options.headers['Content-Range'] = "bytes 0-" + ( file.size - 1 ) + "/" + file.size;
				}
				if( control.fileStorageProvider == Runner.stpAZUREBLOB ) {
					options.headers = options.headers || {};
					options.headers['Content-Range'] = "bytes 0-" + ( file.size - 1 ) + "/" + file.size;
					options.headers['x-ms-blob-type'] = 'BlockBlob';
					delete options.dataType;
				}
				
				
				options.data = file;
			}
		}

		//	amazon s3
		if( this.fileStorageProvider == Runner.stpAMAZON || this.fileStorageProvider == Runner.stpGOOGLECLOUD ) {
			options.send = function( e, options ) {
				var file = options.data.getAll('files[]')[0];
				options.data = new FormData();
				Object.keys( options.uploadParams || {} ).forEach( function( k ) {
					options.data.append( k, options.uploadParams[k] );
				} );
				options.data.append( 'file', file, file.name );

				options.method = 'POST';
			}
		}

		options.submit = function( e, data ) {
			var self = this;
			control
				.resizeImagesRoutine( data.files )
				.then( function( processedFiles ) {
					data.files = processedFiles;
					submitFn.call( self, function() {
						$(self).fileupload('send', data);
					}, e, data );
				
				});

			return false;
		}

		this.uploadForm.fileupload("option", options );

    	var control = this, 
			controlId = this.goodFieldName + "_" + this.rowId;
    	
	    // Load existing files:
    	this.reloadFiles();
    	
    	this.showDropZone();

		this.uploadForm.on('fileuploadprocessalways', function(e, data) {
			if ( data.files ) {
				// show file type image in upload preview
				data.files.forEach( function( file ) {
					if ( file.type !== "image/jpeg" ) {
						var ext = file.name.split('.').pop().toLowerCase();
						file.preview = $(('<image src="' + control._getFileIcon( ext ) + '" />'))
					}
				});
			}
		});
	
    	this.uploadForm.on('fileuploadadded', function(e, data) {
			control.hideDropZone();
   			if (data.files) {
	   			if (!data.files[0].error) {
	   				control.filesToUploadCount++;
				} else {
	   				control.errorHappenedOnAdd = true;
				}
   			}
    		control.uploadForm.fileupload('setUploadAndCancelButtonState', control.filesToUploadCount > 0);
    		control.fireEvent('change');
    	});

		// google drive - finish uploading
		if ( this.cloudUpload() ) {

			this.uploadForm.on( 'fileuploaddone', function ( e, data ) {
				// TODO add upload response handling

				if ( data.createdFile ) {
					data.result = { files: [data.createdFile] };
				}
			} );
		}

		var processSuccessResult = function( fileData ) {
			if (!fileData.error) {
				var fileArray = JSON.parse( control.fileArray.val() ), newFile = fileData;
				newFile.isNew = true; 
				fileArray.push(newFile)
				control.uploadForm.fileupload('setDeleteButtonState', fileArray.length > 0);
				control.fileArray.val(JSON.stringify(fileArray));
				
				if( control.maxNumberOfFiles ) {
					control.uploadForm.fileupload('setFileInputButtonState', 
						fileArray.length < control.uploadForm.fileupload("option").maxNumberOfFiles );
				}
	
			} else {
    			control.errorHappened = true;			
			}
		}
		
		this.uploadForm.on('fileuploadprocessfail', function(e, data) {
			const that = $(this).data("blueimpFileupload");
				
			data.context.replaceWith( that._renderTemplate( that.options.uploadTemplate, data.files )
				.addClass("processfail") );
			
			data.context = that.options.filesContainer.find(".processfail");
			data.context.data('data', data);
			data.context.removeClass("processfail");

			that._forceReflow( data.context );
			that._transition( data.context );
		});
		
		
		this.uploadForm.on('fileuploaddone', function(e, data) {
    		if (control.filesToUploadCount > 0) {
    			control.filesToUploadCount--;
			}
    		control.uploadForm.fileupload('setUploadAndCancelButtonState', control.filesToUploadCount > 0);
    		if ( (data.textStatus != 'success' && data.textStatus != 'nocontent'/*s3*/) 
				|| !data.result || !data.result.files || data.result.files.length == 0) {
    			control.errorHappened = true;
    			return;
    		}
			processSuccessResult( data.result.files[0] );
			control.clearInvalid();
    	});
    	this.uploadForm.on('fileuploadfail', function(e, data) {
    		if ( data.errorThrown && data.jqXHR && data.jqXHR.responseText ) {
				Runner.displayGenericAjaxError( data.jqXHR.responseText, Runner.lang.constants.ERROR_UPLOAD );               	
            }

    		if (control.filesToUploadCount > 0) {
    			control.filesToUploadCount--;
			}
    		control.uploadForm.fileupload('setUploadAndCancelButtonState', control.filesToUploadCount > 0);
		});

		this.uploadForm.on('fileuploadfailed', function(e, data) {
    		control.showDropZone();
    	});

		this.uploadForm.on('fileuploaddestroyed', function(e, data) {
			var fileArray = JSON.parse( control.fileArray.val() ), 
				newFileArray = fileArray.filter( function( element, index ) {
					return element.name != data.name;
				});
			
			control.uploadForm.fileupload( 'setDeleteButtonState', newFileArray.length > 0 );
			control.fileArray.val( JSON.stringify( newFileArray ) );
			control.showDropZone();
			control.fireEvent('change');
		});
	
		this.uploadForm.fileupload({
			destroy: function( e, data ) {
				var $button = $(data.context).find('.btn.delete');
				if ( !$button.is(":visible") ) {
					// to prevent delete request duplication
					return;
				}
				
				const fileupload = $(this).data("blueimpFileupload");
				
				data = {
					context: $button.closest('.template-download'),
					url: fileupload.element[0].action,
					type: 'POST',
					dataType: fileupload.options.dataType,
					name: $button.attr('data-name'),
					data: {
						_action: "DELETE",
						fileName: $button.attr('data-name')
					}
				};
				
				$.ajax( data )
					.done( function() {
						$(this).remove();
						fileupload._trigger('destroyed', e, data);
					})
					.fail(function () {
						fileupload._trigger('destroyfailed', e, data);
					});	
				
				if ( control.maxNumberOfFiles ) {
					control.uploadForm.fileupload('setFileInputButtonState', 
						control.fileArray.length < control.uploadForm.fileupload("option").maxNumberOfFiles );
					
					
					if ( control.initialFileArray ) {
						 if ( control.initialFileArray.find( function(f) { return f.name === data.name } ) !== undefined ) {
							control.uploadForm.fileupload("option", {
								maxNumberOfFiles: control.uploadForm.fileupload("option").maxNumberOfFiles + 1
							});
						 }
					}
				}
			
				fileupload.refreshMaxFilesState();
				
				// to prevent delete request duplication
				data.context.hide();
			}
		});
	},

	_getFileIcon: function( fileExt ) {
		var fileName = '', 
			icons = {
				'7z':'7z',
				'asf':'asf',
				'asp':'code',
				'avi':'mpg',
				'chm':'chm',
				'doc':'doc',
				'docx':'doc',
				'flv':'flv',
				'gz':'gz',
				'html':'html',
				'mdb':'mdb',
				'mdbx':'mdb',
				'mp3':'mp2',
				'mp4':'mp4',
				'mpeg':'mpeg',
				'mpg':'mpg',
				'mov':'mov',
				'pdf':'pdf',
				'php':'code',
				'pps':'pps',
				'ppt':'powerpoint',
				'psd':'psd',
				'rar':'rar',
				'rtf':'rtf',
				'swf':'swf',
				'tif':'tif',
				'ttf':'ttf',
				'txt':'txt',
				'wav':'wav',
				'webm':'mpg',
				'wma':'wma',
				'wmv':'emv',
				'xls':'xls',
				'xlsx':'xls',
				'zip':'zip'
			};
			
		return 'images/icons/' + ( icons[ fileExt ] ||  'text' ) + '.png';
	},
	

	/**
	 * Resize images routine in upload data object 
	 * 
	 * @param {Object} data upload data object 
	 * @return Promise - fires when all files are processed
	 */
	resizeImagesRoutine: function( oldFiles ) {
		var control = this;
		var resizeOnClient = Runner.pages.PageSettings.getGlobalData("resizeImagesOnClient");
		var resizeImage = Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, 'resizeImage', this.pageType),
			resizeImageSize = Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, 'resizeImageSize', this.pageType);
		if( !resizeOnClient || !resizeImage ) {
			return Promise.resolve( oldFiles );
		}
		
		return Promise.all( oldFiles.map( function( file ) {
			if( !control.isFileImage( file.type ) ) {
				return file;
			}
			return new Promise( function( resolve, reject ) {
				var fr = new FileReader();
				fr.onload = function( event ) {
					var src = "data:" + file.type + ";base64," + Runner.util.arrayBufferToBase64( fr.result );
					Runner.util.resizeImage( 
						src, 
						resizeImageSize,
						file.type,
						function( blob ) {
							// create file from resized blob
							resolve( new File( [blob], file.name, { type: file.type, lastModified: new Date().getTime() } ) );
					} );
				}
				fr.readAsArrayBuffer( file );
			} );

		} ) );
		
	},

	isFileImage: function( mimeType ) {
		var acceptedTypes = [ 'image/gif', 'image/jpeg', 'image/png' ];
	
		return mimeType && acceptedTypes.indexOf( mimeType ) != -1;
	},

	/**
	 * Check if the browser supports drag-n-drop and File Api 
	 * It's used as a feature detection to test if It's possible 
	 * to upload files using drag-and-drop
	 * @return {Boolean}
	 */
	isFileApiSupported: function() {
		var div = document.createElement('div');
		return !!window.FileReader && ( 'draggable' in div || ('ondragstart' in div && 'ondrop' in div) );
	},
	
	hideDropZone: function() {
		if ( this.$dropZone ) {
			this.$dropZone.addClass('r-dd-hidden');
		}
		
		this.$progressBar.css('display', '');
		this.$progressLabel.css('display', '');
	},
	
	showDropZone: function() {
		if ( this.$filesTable.children().children().length > 0 || this.errorHappenedOnAdd || JSON.parse( this.fileArray.val() ).length != 0 
			|| this.filesToUploadCount != 0 || !this.isFileApiSupported() ) {
				
    		this.hideDropZone();
			return;
    	}
		this.$progressBar.css('display', 'none');
		this.$progressLabel.css('display', 'none');
		if ( this.$dropZone !== null ) {
			this.$dropZone.removeClass('r-dd-hidden');
			return;
		}
		
		this.$dropZone = $("<div><div>")
			.addClass('rnr-dragndrop-area')
			.appendTo( this.uploadForm.find(".fileupload-buttonbar") )
			.append( $("<div></div>").addClass('rnr-dragtext').text( Runner.lang.constants.UPLOAD_DRAG ) );
		
		$(document)
			.off("drop.multiupload")
			.off("dragover.multiupload")
			.on("drop.multiupload dragover.multiupload", function( e ) {
				e.preventDefault();
			})
			
	},
	
    // Load existing files:
	reloadFiles: function() {
		if ( !!this.initialFileArray.length ) {		
			var dummyEvent = $.Event("done");
			dummyEvent.isDefaultPrevented = function () { return false; };	
			
			this.uploadForm.fileupload('option', 'done')
				.call(this.uploadForm, dummyEvent, {result: { files: this.initialFileArray } });
			
			if ( this.maxNumberOfFiles ) {
				this.uploadForm.fileupload("option", {
					maxNumberOfFiles: this.maxNumberOfFiles - this.initialFileArray.length
				});
			}
		}
		this.uploadForm.fileupload('refreshMaxFilesState');	
		this.uploadForm.fileupload('setDeleteButtonState', this.initialFileArray.length > 0);
		this.uploadForm.fileupload('setUploadAndCancelButtonState', this.filesToUploadCount > 0);
		
		return this.initialFileArray.length;
	},
	
	/**
	 * Set the default value to the control
	 * @param {object} updContext
	 */
	reset: function( updContext ) {
		this.setValue(this.defaultValue, true, updContext);
	
		var fileupload = this.uploadForm.data("blueimpFileupload");
		$.ajax({
			url: fileupload.element[0].action,
			type: 'POST',
			data: {
				_action: "RESET",
				defaultValue: this.defaultValue,
			}
		});
		
		this.clearInvalid();
		this.filesToUploadCount = 0;
		$('table tbody', this.uploadForm).empty();
		this.reloadFiles();
		this.showDropZone();
	},
	
	/**
	 * Add change event base handler
	 * @param {Object} e
	 */
	"change": function(e) {
		// no idea
		if ( e ) {
			this.stopEvent(e);
		}
	},

	/**
	 * Returns array of jQuery object for inline submit
	 * @return {array}
	 */
	getForSubmit: function() {
		var cloneArr = [];
		cloneArr.push(this.valueElem.clone());
		cloneArr.push($("#formStamp_" + this.goodFieldName + "_" + this.rowId, this.uploadForm).clone());
		return cloneArr;
	},
	
	/**
	 * Sets disable attr true
	 * @return {boolean}
	 */
	setDisabled: function() {
		return this.makeReadonly();
	},
	
	/**
	 * Sets disaqble attr false
	 * @return {boolean}
	 */
	setEnabled: function() {
		return this.makeReadWrite();
	},
	
	makeReadonly: function() {
		this.readOnly = true;
		
		this.valueElem.prop('disable', true);
		this.uploadForm.fileupload('setDeleteButtonState', false);
		$(".fileupload-buttonbar", this.uploadForm).hide();
		return true;
	},
	
	makeReadWrite: function() {
		this.readOnly = false;
		
		this.valueElem.prop('disable', false);
		this.uploadForm.fileupload('setDeleteButtonState', true);
		$(".fileupload-buttonbar", this.uploadForm).show();
		return true;
	},	
	
	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * For files has specific criterias
	 * @override
	 * @method
	 * @return {bool}
	 */
	isEmpty: function() {
		return this.getValue().toString() == "" || this.getValue().toString() == "[]";
	},
	
	/**
	 * Sets focus to the element.
	 * Since focus cannot be set properly on valueElem 
	 * It sets focus on the "Add files" button so the contol's value element 
	 * can be visible within it's containing block.
	 * @param {Boolean} triggerEvent
	 * @method
	 * @intellisense
	 */
	setFocus: function( triggerEvent ) {
		var addButtonElem = $(".fileinput-button-input", this.uploadForm);
		
		if ( !this.appearOnPage() || !this.showStatus || this.valueElem.is("disabled") || !addButtonElem.length ) {
			return;
		}
	
		addButtonElem.trigger("focus");
		
		if (triggerEvent === true) {
			this.fireEvent("focus");
		}
	}, 
	
	
	/**
	 * A fast changing in a control value may cause 
	 * a race condition with spreadsheet mode submitting
	 * @return boolean
	 */ 
	spEasyChangeable: function() {
		return false;
	},	
});/**
 * Abstract base class for date fields, should not created directly
 * @class Runner.controls.DateField
 */
Runner.controls.DateField = Runner.extend( Runner.controls.Control, {

	/**
	 * Image and link of datepicker
	 * link element
	 * @type {element}
	 */
	imgCal: null,
	/**
	 * Indicates used datepicker with control or not
	 * @type {bool} cfg
	 */
	useDatePicker: false,
	/**
	 * Id of date format hidden element, which used on serverside
	 * @type {string}
	 */
	dateFormatHiddId: "",
	/**
	 * Indicates date format with control or not
	 * @type {bool} cfg
	 */
	dateFormat: "",
	/**
	 * Indicates show time with control or not
	 * @type {bool} cfg
	 */
	showTime: false,
	/**
	 * jQuery object of date format hidden element, which used on serverside
	 * @type {Object}
	 */
	dateFormatHiddElem: null,


	dateDelimiter: "/",

	startWeekDay: 0,

	is24hoursFormat: false,

	timePmLetter: "PM",

	timeAmLetter: "AM",

	timeDelimiter: ":",

	leadingZero: true,

	intervalStart: false,
	intervalEnd: false,
	intervalErrorMessage: null,
	notAllowedDays: null,

	bsDateTimePicker: null,

	weekdays: "",
	weekdayMessage: "",


	/**
	 * Overrides parent constructor
	 * @param {Object} cfg
	 * @param {bool} cfg.useDatePicker
	 */
	constructor: function( cfg ) {
		Runner.controls.DateField.superclass.constructor.call(this, cfg);

		this.notAllowedDays = {};
		// add hidden field for datepicker usege
		this.useDatePicker = cfg.useDatePicker || false;

		this.dateDelimiter = Runner.pages.PageSettings.getGlobalData("locale")["dateDelimiter"];
		this.startWeekDay = ( parseInt( Runner.pages.PageSettings.getGlobalData("locale").startWeekDay ) + 1 ) % 7;
		this.showTime = Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, "showTime", this.pageType);

		this.timePmLetter = Runner.pages.PageSettings.getGlobalData("locale").timePmLetter;
		this.timeAmLetter = Runner.pages.PageSettings.getGlobalData("locale").timeAmLetter;
		this.timeDelimiter = Runner.pages.PageSettings.getGlobalData("locale").timeDelimiter;

		if ( Runner.pages.PageSettings.getGlobalData("locale").leadingZero == "1" ) {
			this.leadingZero = true;
		}

		if ( Runner.pages.PageSettings.getGlobalData("locale").is24hoursFormat == "1" ) {
			this.is24hoursFormat = true;
		}

		// add hidden field for date format on serverside
		this.dateFormatHiddId = "type" + (cfg.ctrlInd || "") + "_" + this.goodFieldName + "_" + this.rowId;
		this.dateFormatHiddElem = $("#" + this.dateFormatHiddId);


		this.initBSDatepicker();

		this.addValidation("DateValidation");

		this.weekdays = Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, "weekdays", this.pageType);

		if ( this.weekdays && this.weekdays.length ) {
			this.weekdayMessage = Runner.pages.PageSettings.getFieldData(this.table, this.fieldName, "weekdayMessage", this.pageType);

			for ( var day = 0; day < 7; ++day ) {
				if ( this.weekdays.indexOf( day ) == -1 ) {
					this.toggleWeekDay( day, false, this.weekdayMessage );
				}
			}
		}
	},

	/**
	 * Init Bootstrap DatePicker + TimePicker
	 */
	initBSDatepicker: function() {
		if ( !this.useDatePicker ) {
			return;
		}

		var defaultDate = this.getValue();

		var ctrl = this,
			initialYear = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, 'initialYear', this.pageType ),
			lastYear = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, 'lastYear', this.pageType ),
			minDate = this.toJSDate( moment().subtract(initialYear, 'years').startOf("year") ),
			maxDate = this.toJSDate( moment().add(lastYear, 'years').endOf("year") ),
			options = {
				showTodayButton: true,
				showClose: true,
				minDate: defaultDate && minDate > defaultDate ? moment( defaultDate ).startOf("year") : minDate,
				maxDate: defaultDate && maxDate < defaultDate ? moment( defaultDate ).endOf("year") : maxDate,
				tooltips: Runner.datePickerTooltips(),
				keepInvalid: true
			},
			outerContainerForIE;

		this.intervalStart = minDate;
		this.intervalEnd = maxDate;

		if ( this.pageObj.openMode == Runner.pages.constants.OPENMODE_POPUP && ( Runner.isIE || Runner.isIE11 ) ) {
			outerContainerForIE = $("#pickerContainer");
			if ( outerContainerForIE.length === 0 ) {
				outerContainerForIE = $("<div id='pickerContainer' style='position:relative; z-index:9999'></div>");
				outerContainerForIE.appendTo("body");
			}
			options.widgetParent = outerContainerForIE;
		}

		if ( this.dateEditType === Runner.controls.constants.EDIT_DATE_DD_INLINE || this.dateEditType === Runner.controls.constants.EDIT_DATE_DD_DP ) {
			this.bsDateTimePicker = $("#" + this.valContId).parents('.bs-date-control');

			options.format = "YYYY-M-D";
			options.widgetPositioning = {
				horizontal: Runner.isDirRTL() ? 'left' : 'right'
			};
			options.showTime = this.showTime;

			options.datepickerInput = "#" + this.valContId;
			options.useCurrent = false;

			// uncomment to be able to inspect calendar HTML
			// options.debug = true;


			this.bsDateTimePicker.datetimepicker( options );

			ctrl.on("focus", function() {
				ctrl.bsDateTimePicker.data("DateTimePicker").hide();
			});

			this.bsDateTimePicker.on("dp.change", function(e) {
				e.date && ctrl.setValue( e.date.toDate(), true );
			});

			this.imgCal = $('#imgCal_' + this.valContId)
				.on('click', function() {
					Runner.setDatePickerLocale();
					if ( !!Object.keys( ctrl.notAllowedDays ).length ) {
						ctrl.toggleWeekDayInDatepicker();
						ctrl.validate();
					}
					ctrl.bsDateTimePicker.data("DateTimePicker").toggle();
					return false;
				})
				.on("blur", function() {
					if( !options.debug ) {
						ctrl.bsDateTimePicker.data("DateTimePicker").hide();
					}
				});
		} else if ( this.dateEditType == Runner.controls.constants.EDIT_DATE_SIMPLE_INLINE || this.dateEditType == Runner.controls.constants.EDIT_DATE_SIMPLE_DP ) {
			this.bsDateTimePicker = this.valueElem.parents(".input-group");
			Runner.setDatePickerLocale();

			options.format = Runner.getDateFormatString().replace(/yy/g, "YYYY").toUpperCase();
			options.format += this.showTime ? ' ' + Runner.getTimeFormatString( true ) : '';
			options.showTime = this.showTime;
			options.useCurrent = false;

			if ( this.dateEditType == Runner.controls.constants.EDIT_DATE_SIMPLE_INLINE ) {
				options.keyBinds = {
					'delete': function() {}
				};
			}
			
			// uncomment to be able to inspect calendar HTML
			// options.debug = true;

			this.bsDateTimePicker.datetimepicker( options );
			this.setValue( defaultDate );
			this.setDefaultValue();

			this.imgCal = $('#imgCal_' + this.valContId).on('click', function() {
				Runner.setDatePickerLocale();
			});
		}

		if ( !outerContainerForIE && this.bsDateTimePicker && ( this.pageObj.useResize || this.isInline || this.pageObj.openMode != Runner.pages.constants.OPENMODE_PRELOADED ) ) {
			this.bsDateTimePicker.on("dp.show", function(e) {
				Runner.util.absToFixed( $(".bootstrap-datetimepicker-widget", this) );
			});
		}
	},


	/**
	 * Create the Date object from the string representation
	 * basing on the dmy format
	 * @param {string} dmy
	 * @param {string} str
	 * @return {object}
	 */
	parseDateTime: function( str, dmy ) {
		if ( str == null ) {
			return null;
		}

		var dt, i,
			stringDateTime,
			arr = str.match( /\d+/g ),
			amRegular = "(a\.?m\.?)" + ( this.timeAmLetter ? "|(" + this.timeAmLetter + ")" : "" ),
			pmRegular = "(p\.?m\.?)" + ( this.timePmLetter ? "|(" + this.timePmLetter + ")" : "" ),
			amRegExp = new RegExp(amRegular, "gi"),
			pmRegExp = new RegExp(pmRegular, "gi"),
			isAm = str.match(amRegExp),
			isPm = str.match(pmRegExp);

		if ( arr == null || arr.length < 3 ) {
			return null;
		}

		for (i = arr.length; i < 6; i++) {
			arr[i] = 0;
		}

		if ( arr[0] > 100) {
			//	date from database in the YYYY-MM-DD format
			dmy = 2;
		}

		if ( dmy == 1 ) {
			stringDateTime = arr[2] + "/" + arr[1] + "/" + arr[0];
		} else if ( dmy == 0 ) {
			stringDateTime = arr[2] + "/" + arr[0] + "/" + arr[1];
		} else {
			stringDateTime = arr[0] + "/" + arr[1] + "/" + arr[2];
		}

		stringDateTime += " " + arr[3] + ":" + arr[4] + ":" + arr[5];

		if ( arr[3] > 0 && arr[3] <= 12 ) {
			if ( isAm ) {
				stringDateTime += " AM";
			} else if ( isPm ) {
				stringDateTime += " PM";
			}
		}

		dt = new Date(stringDateTime);
		if ( isNaN(dt) ) {
			return null;
		}

		return dt;
	},

	/**
	 * Override addValidation
	 * @param {string} type
	 */
	addValidation: function( type ) {
		// date field can be validated only as isRequired or DenyDuplicated
		if ( type !== "IsRequired" && type !== "DenyDuplicated" && type !== "DateValidation"
			|| this.mode === Runner.controls.constants.MODE_SEARCH ) {
			return;
		}
		Runner.controls.DateField.superclass.addValidation.call( this, type );
	},

	/**
	 *
	 */
	toggleWeekDay: function( days, enable, message ) {
		var ctrl = this;
		if ( !Array.isArray( days ) ) {
			days = [ days ];
		}

		days.forEach( function( day ) {
			if ( !enable ) {
				ctrl.notAllowedDays[ day ] = message;
			} else {
				delete ctrl.notAllowedDays[ day ];
			}
		});

		this.toggleWeekDayInDatepicker();
		this.validate( ["DateValidation"] );
	},

	toggleWeekDayInDatepicker: function() {
		if ( this.useDatePicker ) {
			this.bsDateTimePicker.data('DateTimePicker').daysOfWeekDisabled( Object.keys( this.notAllowedDays ) );
		}
	},

	setAllowedInterval: function( begin, end, message ) {
		this.intervalStart = this.toJSDate( begin );
		this.intervalEnd = this.toJSDate( end );
		this.intervalErrorMessage = message;

		this.validate( ["DateValidation"] );

		if ( this.useDatePicker ) {
			var picker = this.bsDateTimePicker.data('DateTimePicker'),
				useCurrent = picker.useCurrent();

			picker.useCurrent( false );
			picker.minDate( false );
			picker.maxDate( false );

			picker.minDate( this.intervalStart || false );
			picker.maxDate( this.intervalEnd || false );

			picker.useCurrent( useCurrent );
		}
	},

	getAllowedInterval: function() {
		return [ this.intervalStart, this.intervalEnd ];
	},

	copyDateByDay: function( date ) {
		if ( !(date instanceof Date ) ) {
			return new Date();
		}
		return new Date( date.getFullYear(), date.getMonth(), date.getDate() );
	},

	validateDate: function() {
		var value = this.getValue(),
			intervalMessage = false,
			day;

		if ( !value ) {
			return true;
		}

		// compare only dates, ignore hours
		var startDay = this.copyDateByDay( this.intervalStart );
		var endDay = this.copyDateByDay( this.intervalEnd );
		var valueDay = this.copyDateByDay( value );


		if ( this.intervalStart && this.intervalEnd && ( valueDay < startDay || valueDay > endDay) ) {
			intervalMessage = this.intervalErrorMessage || Runner.lang.constants.INLINE_FIELD_DATE_NOT_IN_INTERVAL;
		} else if ( this.intervalStart && valueDay < startDay ) {
			intervalMessage = this.intervalErrorMessage || Runner.lang.constants.INLINE_FIELD_DATE_EARLIER_THAN_START;
		} else if ( this.intervalEnd && valueDay > endDay ) {
			intervalMessage = this.intervalErrorMessage || Runner.lang.constants.INLINE_FIELD_DATE_LATER_THAN_END;
		}

		if ( intervalMessage ) {
			if ( this.intervalStart ) {
				intervalMessage = intervalMessage.replace("start", this.printDateTime(this.intervalStart, this.dateFormat));
			}
			if ( this.intervalEnd ) {
				intervalMessage = intervalMessage.replace("end", this.printDateTime(this.intervalEnd, this.dateFormat));
			}

			return intervalMessage;
		}

		day = value.getDay();
		if ( this.notAllowedDays.hasOwnProperty( day ) ) {
			if ( this.notAllowedDays[ day ] !== undefined ) {
				return this.notAllowedDays[ day ];
			}

			return Runner.lang.constants.INLINE_FIELD_DATE_NOT_ALLOWED_DAY;
		}

		return true;
	},

	toJSDate: function( date ) {
		var dateObj = date;

		if ( typeof date === "string" ) {
			dateObj = this.parseDateTime( date, this.dateFormat );
		} else if ( date instanceof moment ) {
			dateObj = date.toDate();
		}

		return (dateObj instanceof Date) ? dateObj : null;
	},

	/**
	 * @param {mixed} newDate
	 * @return {mixed}
	 */
	setValue: function( newDate ) {
	},

	getMomentValue: function() {
		return moment( this.getValue() );
	},


	getDisplayValue: function() {
		return this.printDateTime( this.getValue(), this.dateFormat );
	},

	/**
	 * format: -1 - native (d-m-y)
				1 - d/m/y
				0 - m/d/y
				2 - y/m/d
	 * @param {Date object} value
	 * @param {string} format
	 * @param {boolean} skipDayMonthZeroes
	 * @return {string}
	 */
	printDateTime: function( value, format, skipDayMonthZeroes ) {
		if ( !value || !(value instanceof Date) ) {
			return "";
		}

		var date, time,
			day = value.getDate(),
			month = value.getMonth() + 1,
			year = value.getFullYear();

		format = parseInt( format );

		if ( !skipDayMonthZeroes ) {
			day = day < 10 ? '0' + day : day;
			month = month < 10 ? '0' + month : month;
		}

		switch ( format ) {
			case -1:
				date = day + '-' + month + '-' + year;
				break;
			case 0:
				date = month + this.dateDelimiter + day + this.dateDelimiter + year;
				break;
			case 1:
				date = day + this.dateDelimiter + month + this.dateDelimiter + year;
				break;
			default:
				date = year + this.dateDelimiter + month + this.dateDelimiter + day;
		}

		if ( !this.showTime ) {
			return date;
		}
		time = this.printTime( value );

		return date + ' ' + time;
	},

	/**
	 * Get the time string
	 * @param {Date object} value
	 * @return {string}
	 */
	printTime: function( value ) {
		return Runner.printTimeFromDate( value, true );
	},
});

/**
 * Class for date fields with textField value editor
 * If there is datePicker, instance of Runner.controls.DateTextField should be passed as target
 * @class Runner.controls.DateTextField
 */
Runner.controls.DateTextField = Runner.extend( Runner.controls.DateField, {
	eventsAreInitialized: false,

	delayedEventsHandlers: null,

	/**
	 * Overrides parent constructor
	 * @param {Object} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.DateTextField.superclass.constructor.call(this, cfg);

		this.delayedEventsHandlers = [];

		if ( this.isSetValidation('DenyDuplicated') ) {
			this.valueElem.attr('autocomplete', 'off');
		}
	},

	/**
	 * Rather ugly fix
	 * Custom events should be run after the datepicker handlers, not before.
	 */
	 initCustomEvents: function() {
		var self = this;
		setTimeout( function() {
			Runner.controls.Control.prototype.initCustomEvents.call( self );
		}, 0);
	},

	on: function( eventName, fn, options, scope ) {
		if ( this.eventsAreInitialized ) {
			this.on = Runner.controls.Control.superclass.on;
			this.on( eventName, fn, options, scope );
			return;
		}

		this.delayedEventsHandlers.push( {eventName: eventName, handler: fn, options: options, scope: scope} );
	},

	/**
	 * The method should be invoked when custom control events are initialized.
	 * It adds to the control custom events handlers
	 */
	addDelayedEventsListeners: function() {
		var i, data;

		for (i = 0; i < this.delayedEventsHandlers.length; i++) {
			data = this.delayedEventsHandlers[i];
			this.on( data.eventName, data.handler, data.options, data.scope );
		}

		this.delayedEventsHandlers = [];
	},

	/**
	 * Set the value element
	 */
	assignValueElem: function() {
		Runner.controls.DateField.superclass.assignValueElem.call(this);
		if ( typeof this.dateFormat === "undefined" || this.dateFormat === "" ) {
			this.dateFormat = Runner.pages.PageSettings.getGlobalData("locale").dateFormat;
		}
	},

	/**
	 * Set the control's events list
	 */
	assignListOfEvents: function() {
		this.addEvent(["click", "blur", "change", "keyup", "edited", "editing"]);
		this.eventsAreInitialized = true;
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		var ctrl = this;

		this.on("change", function(e, updateContext) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			this.fireEvent("edited", updateContext);
			this.fireEvent("editing");
		});

		this.on("keyup", function(e) {
			if ( !e || !Runner.isAcceptableKeyCode(e) ) {
				return false;
			}
			this.fireEvent("editing");
		}, { buffer: 600 });


		if ( this.dateEditType === Runner.controls.constants.EDIT_DATE_DD_INLINE
				|| this.dateEditType === Runner.controls.constants.EDIT_DATE_DD_DP ) {
			this.valueElem.parents(".bs-date-control").on("dp.change", function(e) {
				ctrl.fireEvent("change", null);
			});
		} else if ( this.dateEditType == Runner.controls.constants.EDIT_DATE_SIMPLE_INLINE
				|| this.dateEditType == Runner.controls.constants.EDIT_DATE_SIMPLE_DP ) {

			this.valueElem.parents(".input-group").on("dp.change", function(e) {
				ctrl.fireEvent("change", null);
			});
		}

		this.addDelayedEventsListeners();
	},

	"editing": function(nullEvent, updateContext) {
		// override default handler to skip saving
	},

	getValue: function() {
		var parsedTime = this.parseDateTime( this.valueElem.val(), this.dateFormat );
		return parsedTime == null ? "" : parsedTime;
	},

	getDisplayValue: function() {
		return this.valueElem.val();
	},

	/**
	 * Set value, also change value in hidden field
	 * @method
	 * @param {Object} val
	 * @return {bool} if passed correct Date object, otherwise false
	 */
	setValue: function( newDate, triggerEvent, updContext ) {
		var dt, dateObj = newDate,
			readOnly = this.isReadonly();

		if ( readOnly ) {
			this.makeReadWrite();
		}

		if ( typeof newDate === "string" ) {
			dateObj = this.parseDateTime( newDate, this.dateFormat );
			if ( newDate === '' && dateObj === null ) {
				this.bsDateTimePicker && this.bsDateTimePicker.data("DateTimePicker")
					&& this.bsDateTimePicker.data("DateTimePicker").clear();
			}
		} else if ( newDate instanceof moment ) {
			dateObj = newDate.toDate();
		}

		if ( dateObj instanceof Date ) {
			dt = this.printDateTime(dateObj, this.dateFormat);
			//set value in edit textfield
			this.valueElem.val( dt );

			if ( readOnly ) {
				this.makeReadonly();
			}

			if ( triggerEvent === true ) {
				this.fireEvent("change", updContext);
			}
			return true;
		}

		// set empty value = ""
		this.valueElem.val("");

		if ( readOnly ) {
			this.makeReadonly();
		}

		if ( triggerEvent === true ) {
			this.fireEvent("change", updContext);
		}
		return false;
	},

	/**
	 * Custom function for onblur event
	 * @param {Object} e
	 */
	"blur": function(e) {
		this.stopEvent(e);
		this.focusState = false;
	},

	/**
	 * The method is invoked if the custom event "edited" is fired
	 * It's overrided the base "edited" prop.
	 * @param {object} nullEvent		Equals to null if function is invoked
	 * with the fireEvent method
	 * @param {object} updateContext
	 */
	"edited": function( nullEvent, updateContext ) {
		var valArr = this.getPreparedValidationArray(),
			vRes = this.validate( valArr ),
			newValue = this.getValue();

		// set values to hidden fields
		if ( vRes.result && this.useDatePicker && newValue ) {
			this.setValue( newValue );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );

		this.processResolvedEvent( updateContext );
	},

	/**
	 * Sets disable attr true
	 * Should be overriden for sophisticated controls
	 * @method
	 */
	setDisabled: function() {
		if ( !this.valueElem.length ) {
			return false;
		}

		this.valueElem.prop('disabled', true);
		if ( this.imgCal ) {
			this.imgCal.css('visibility', 'hidden');
		}

		return true;
	},

	/**
	 * Sets disaqble attr false
	 * Should be overriden for sophisticated controls
	 * @method
	 */
	setEnabled: function() {
		if ( !this.valueElem.length ) {
			return false;
		}

		this.valueElem.prop('disabled', false);
		if ( this.imgCal ) {
			this.imgCal.css('visibility', 'visible');
		}

		return true;
	},

	/**
	 * Clone html for iframe submit
	 */
	getForSubmit: function() {
		return [ this.valueElem.clone(), this.dateFormatHiddElem.clone() ];
	},

	makeReadonly: function() {
		Runner.controls.DateTextField.superclass.makeReadonly.call(this);
		if ( this.imgCal ) {
			this.imgCal.css('display','none');
		}
		return true;
	},

	makeReadWrite: function() {
		Runner.controls.DateTextField.superclass.makeReadWrite.call( this );
		if ( this.imgCal ) {
			this.imgCal.css('display','');
		}
		return true;
	},


	getStringValue: function( value ) {
		var momentValue = moment( value || this.getValue() );
		return momentValue ? momentValue.format('YYYY-MM-DD HH:mm:ss') : "";
	},

	getUserValue: function() {
		return this.valueElem.val();
	},


	/**
	 * Get the default control's value
	 * @return {String}
	 */
	getDefaultControlValue: function() {
		return this.defaultValue ?
			this.getStringValue( this.defaultValue ) : "";
	},

	destructor: function() {
		Runner.controls.DateTextField.superclass.destructor.call( this );
	}
});

/**
 * Class for date fields with three dropdowns value editor
 * If there is datePicker, instance of Runner.controls.DateDropDown should be passed as target
 * @class Runner.controls.DateDropDown
 */
Runner.controls.DateDropDown = Runner.extend( Runner.controls.DateField, {
	/**
	 * Hidden element for date value
	 * value for server submit
	 * @type {Object} type
	 */
	hiddValueElem: null,
	/**
	 * Hidden element id
	 * @type {string}
	 */
	hiddElemId: "",

	elemDay: null,
	elemMonth: null,
	elemYear: null,


	/**
	 * Overrides parent constructor
	 * @param {Mixed} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.DateDropDown.superclass.constructor.call(this, cfg);

		// if allready have constants, than fill combos
		if ( Runner.lang.constants.MONTH_JAN ) {
			this.addYearOptions( /*cfg.yearVal*/ );
			this.addMonthOptions( /*cfg.monthVal*/ );
			this.addDayOptions( /*cfg.dayVal*/ );
		}

		if ( this.hiddValueElem.length ) {
			this.setValue( this.parseDateTime(this.hiddValueElem.val(), 2) );
		}

		this.defaultValue = this.getValue();
	},

	/**
	 * @return Boolean
	 */
	focused: function() {
		return this.valueElem
			.filter( function() {
				return this === document.activeElement;
			}).length != 0;
	},

	/**
	 * Set the value element
	 */
	assignValueElem: function() {
		if ( typeof this.dateFormat === "undefined" || this.dateFormat === "" ) {
			this.dateFormat = Runner.pages.PageSettings.getGlobalData("locale").dateFormat;
		}

		this.elemDay = $("#day" + this.valContId);
		this.elemMonth = $("#month" + this.valContId);
		this.elemYear = $("#year" + this.valContId);
		this.valueElem = [this.elemDay, this.elemMonth, this.elemYear ].reduce( $.merge, $() );

		// add hidden elems
		this.hiddElemId = this.valContId;
		this.hiddValueElem = $("#" + this.hiddElemId);
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		this.elemsForEvent = [
			this.elemDay.get(0),
			this.elemMonth.get(0),
			this.elemYear.get(0)
		];
	},

	/**
	 * Set the control's events list
	 */
	assignListOfEvents: function() {
		this.addEvent(["click", "change", "edited", "editing"]);
	},

	/**
	 * DateDropDown control's destructor
	 */
	destructor: function() {
		Runner.controls.DateDropDown.superclass.destructor.call(this);

		this.elemDay.remove();
		this.elemMonth.remove();
		this.elemYear.remove();
	},

	/**
	 * Add year options
	 * @param {integer} year value
	 */
	addYearOptions: function( selectedYear ) {
		var dt = new Date(),
			currentYear = dt.getFullYear(),
			fromYearShift = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "initialYear", this.pageType ),
			toYearShift = Runner.pages.PageSettings.getFieldData( this.table, this.fieldName, "lastYear", this.pageType ),
			startYear = currentYear - fromYearShift,
			endYear = currentYear + toYearShift,
			$opt = $('<option>').val('').html('&nbsp;');

		startYear = Math.min( startYear, selectedYear
			|| parseInt( (this.elemYear.val() || '').toString().trim(), 10 ) || startYear );

		endYear = Math.max( endYear, selectedYear
			|| parseInt( (this.elemYear.val() || '').toString().trim(), 10 ) || endYear );

			this.elemYear.html('')
			.append( $opt );


		for ( var i = startYear; i <= endYear; i++ ) {
			$opt = $('<option>')
				.val(i).html(i);

			if ( i == selectedYear ) {
				$opt.attr( "selected", "selected" );
			}

			this.elemYear.append( $opt );
		};

		this.addYearOptions = Runner.emptyFn;
	},

	/**
	 * Add month options
	 * @param {integer} month value
	 */
	addMonthOptions: function( selectedMonth ) {
		var i,
			opt = $('<option>')
				.val('').html('&nbsp;');

		this.elemMonth.html('').append( opt );

		for (i = 0; i < Runner.monthNames.length; i++) {
			opt = $('<option>')
				.val(i + 1).html( Runner.monthNames[i] );

			if (i + 1 == selectedMonth) {
				opt.attr("selected", "selected");
			}
			this.elemMonth.append( opt );
		}

		if ( !this.elemMonth.css('width') ) {
			this.elemMonth.css('width','90px');
		}

		this.addMonthOptions = Runner.emptyFn;
	},

	/**
	 * Add day options
	 * @param {integer} day value
	 */
	addDayOptions: function( selectedDay ) {
		var i,
			opt = $('<option>')
				.val('').html('&nbsp;');

		this.elemDay.html('').append( opt );

		for (i = 1; i <= 31; i++) {
			opt = $('<option>')
				.val(i).html(i);

			if (i === selectedDay) {
				opt.attr("selected", "selected");
			}
			this.elemDay.append( opt );
		};

		this.addDayOptions = Runner.emptyFn;
	},

	/**
	 * Custom function for onchange event
	 * @param {Object} e
	 */
	"change": function(e) {
		//	no idea
		if ( e ) {
			this.stopEvent(e);
		}
	},

	/**
	 * The method is invoked when the "edited" event is fired
	 * @param {object} nullEvent		Equals to null if function is invoked
	 * with the fireEvent method
	 * @param {object} updateContext
	 */
	"edited": function(nullEvent, updateContext) {
		var name, vRes,
			newValue = this.getValue();

		// if any dd is empty, than we can't start validation
		if( [ this.elemDay, this.elemMonth, this.elemYear ]
				.find( function( $e ) {
					return $e.val() === '';
				} ) !== undefined ) {
			this.hiddValueElem.val('');
			return;
		}

		vRes = this.validate();
		if ( vRes.result ) {
			this.setValue( this.getValue() );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );
	},

	/**
	 * Gets values from dropdowns and returns it in YYYY-mm-dd-hh-ss format
	 * @return {Mixed}
	 */
	getValue: function() {
		var dayVal = (this.elemDay.val() || "").toString().trim(),
			monthVal = (this.elemMonth.val() || "").toString().trim(),
			yearVal = (this.elemYear.val() || "").toString().trim();

		if ( !dayVal || !monthVal || !yearVal ) {
			return false;
		}

		if ( isNaN( parseInt( monthVal, 10 ) ) ) {
			var mIdx = Runner.monthNames.indexOf( monthVal );
			if ( mIdx !== -1 ) {
				monthVal = mIdx + 1;
			}
		}

		return new Date(yearVal, monthVal - 1, dayVal, 0, 0, 0);
	},

	/**
	 * Sets value to dropdowns
	 * @param {Date} newDate
	 * @return {bool}Returns true if success, otherwise false
	 */
	setValue: function( newDate, triggerEvent, updContext ) {
		var day, month, year,
			dateObj = newDate,
			readOnly = this.isReadonly();

		if ( readOnly ) {
			this.makeReadWrite();
		}

		if ( typeof newDate === "string" ) {
			dateObj = this.parseDateTime( newDate, this.dateFormat );
			if ( newDate === '' && dateObj === null ) {
				this.bsDateTimePicker && this.bsDateTimePicker.data("DateTimePicker")
					&& this.bsDateTimePicker.data("DateTimePicker").clear();
			}
		} else if ( newDate instanceof moment ) {
			dateObj = newDate.toDate();
		}

		if ( dateObj instanceof Date ) {
			day = dateObj.getDate();
			month = dateObj.getMonth() + 1;
			year = dateObj.getFullYear();

			this.hiddValueElem.val( year + '-' + month + '-' + day );

			this.elemDay.get(0).selectedIndex = day;
			this.elemMonth.get(0).selectedIndex = month;

			this.elemYear.val( year ).attr('selected', 'selected');

			if ( this.useDatePicker ) {
				this.hiddValueElem.parents(".bs-date-control").data("DateTimePicker").date( dateObj );
			}

			if ( readOnly ) {
				this.makeReadonly();
			}

			if ( triggerEvent === true ) {
				this.fireEvent("change", updContext);
			}

			return true;
		}

		this.hiddValueElem.val('');

		if ( this.elemDay.length ) {
			this.elemDay.get(0).selectedIndex = 0;
		}

		if ( this.elemMonth.length ) {
			this.elemMonth.get(0).selectedIndex = 0;
		}

		if ( this.elemYear.length ) {
			this.elemYear.get(0).selectedIndex = 0;
		}

		if ( readOnly ) {
			this.makeReadonly();
		}

		if ( triggerEvent === true ) {
			this.fireEvent( "change", updContext );
		}

		return false;
	},

	makeReadonly: function() {
		this.readOnly = true;
		this.setDisabled();
		return true;
	},

	makeReadWrite: function() {
		this.readOnly = false;
		this.setEnabled();
		return true;
	},

	/**
	 * Overrides parent function for three element control
	 */
	setDisabled: function() {
		if (!this.elemDay || !this.elemMonth || !this.elemYear) {
			return false;
		}

		this.elemDay.prop('disabled', true);
		this.elemMonth.prop('disabled', true);
		this.elemYear.prop('disabled', true);

		if (this.imgCal) {
			this.imgCal.css('visibility', 'hidden');
		}
		return true;
	},

	/**
	 * Overrides parent function for three element control
	 */
	setEnabled: function() {
		this.elemDay.prop('disabled', false);
		this.elemMonth.prop('disabled', false);
		this.elemYear.prop('disabled', false);

		if ( this.imgCal ) {
			this.imgCal.css('visibility','visible');
		}
		return true;
	},

	/**
	 * Clone html for iframe submit
	 * @method
	 */
	getForSubmit: function() {
		return [ this.hiddValueElem.clone(), this.dateFormatHiddElem.clone() ];
	},

	/**
	 * Set focus to the element, override.
	 * Since focus cannot be set properly on valueElem
	 * It sets focus on the select element within the control.
	 * @method
	 * @param {Boolean} triggerEvent
	 */
	setFocus: function( triggerEvent ) {
		var ctrl = this;

		this.isSetFocus = false;

		if ( !this.showStatus ) {
			return;
		}

		$.each( this.valueElem, function( ind, elem ) {
			var $elem = $(elem);

			if ( $elem.is(':disabled') || !$elem.is(':visible') ) {
				return;
			}

			if ( $elem.parent().find('select:first').attr('id') != $elem.attr('id') ) {
				return;
			}

			elem.focus();
			ctrl.isSetFocus = true;

			if ( triggerEvent === true ) {
				ctrl.fireEvent("focus");
			}
			return false;
		});
	},

	/**
	 * Checks if the date control's value is empty.
	 * @method
	 * @return {boolean}
	 */
	isEmpty: function() {
		return this.elemDay.val() == "" || this.elemMonth.val() == "" || this.elemYear.val() == "";
	},

	isEmptyForSearch: function() {
		return this.isEmpty();
	},

	/**
	 * Return date value as string
	 * @parma {Date object} datePassed (optional)
	 * @return {string}
	 */
	getStringValue: function( datePassed ) {
		var date = datePassed || this.getValue(),
			day, month, year;

		if ( date instanceof Date ) {
			day = date.getDate();
			month = date.getMonth() + 1;
			year = date.getFullYear();

			return year + '-' + month + '-' + day;
		}
		return '';
	},


	/**
	 * Get the initial control's value
	 * @return {string}
	 */
	getDefaultControlValue: function() {
		if ( this.defaultValue === false ) {
			return '';
		}
		return this.getStringValue( this.defaultValue );
	},

	toggleWeekDayInDatepicker: function() {
		if ( !this.useDatePicker ) {
			return;
		}

		this.bsDateTimePicker.data('DateTimePicker').daysOfWeekDisabled( Object.keys( this.notAllowedDays ) );

		if ( this.hiddValueElem.length ) {
			// set value for dd explicitly
			this.setValue( this.parseDateTime( this.hiddValueElem.val(), 2 ) );
		}
	},

});

/**
 * Abstract base class for LookupWizard fields, should not created directly.
 * Contains common functionality for dependent lookup wizard controls
 * @class 
 * @requires Runner.controls.Control
 */
Runner.controls.LookupWizard = Runner.extend( Runner.controls.Control, {	

	/**
	 * An array of main controls 
	 * @type {array}
	 */
	parentCtrls: null,

	/**
	 * An array containing the names of the main fields
	 * for this control
	 * @type {array}
	 */
	parentFieldNames: null,
	
	/**
	 * A flag indicating if multiple selection is allowed
	 * @type {boolean}
	 */
	multiSelectionAllowed: false,
	
	/**
	 * @type {jQuery object}
	 */
	addNew: null,
	editCurrent: null,

	/**
	 *
	 */
	lookupTable: "",
	
	/**
	 *
	 */
	pageId: -1,
	
	/**
	 *
	 */
	dispField: "",
	
	/**
	 *
	 */
	linkField: "",
	
	/**
	 * @type {object}
	 */
	preloadData: null,
	
	/**
	 * @type {array}
	 */	
	autoCompleteFields: null,
	
	/**
	 * Dashboard Search: the table where the original field comes from
	 * @type {string}
	 */
	originalTable: null,
	/**
	 * Dashboard Search: original field name
	 * @type {string}
	 */
	originalField: null,
	
	masterTName: "",
	masterTId: 0,
	mainFields: null,
	mainMasterFields: null,
	
	
	/**
	 * Override parent contructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		// recreate objects
		this.parentCtrls = [];
		this.autoCompleteFields = [];

		Runner.controls.LookupWizard.superclass.constructor.call(this, cfg);
		
		this.initAddNewLink();
		this.initEditLink();
		
		if ( this.pageType === Runner.pages.constants.PAGE_ADD && !this.isEmpty() ) {
			this.doAutoCompleteFields();
		}
		
		if ( Runner.pages.PageSettings.getTableData( this.table, "entityType" ) === Runner.pages.constants.titDASHBOARD ) {
			this.originalTable = this.getFieldSetting( "originalTable" );
			this.originalField = this.getFieldSetting( "originalField" );
			this.shortTableName = Runner.pages.PageSettings.getShortTName( this.originalTable )
		} else {
			this.originalTable = this.table;
			this.originalField = this.fieldName;
		}
		
		if ( this.preloadData ) {
			this.preload( this.preloadData.vals, this.preloadData.fVal );
			this.setDefaultValue();	
		}		
	},
	
	/**
	 * Set the control's events list
	 */	
	assignListOfEvents: function() {
		this.addEvent( ["click", "change", "edited", "editing"] );
	},
	
	/**
	 * Add the click handler to the addNew link 
	 */
	initAddNewLink: function() {
		var control = this;
		
		this.addNew = $("#addnew_" + this.valContId).on("click", function(e) {
			var params = control.getAddPageParams();
			control.pageId = Runner.pages.PageManager.openPage( params );
			return false;
		});	
	},
	
	initEditLink: function() {
		var control = this;
		
		this.editCurrent = $("#edit_lookup_item_" + this.valContId).on("click", function(e) {
			var params = control.getEditPageParams();
			control.pageId = Runner.pages.PageManager.openPage( params );
			return false;
		});

		this.updataEditCurrentButton()
	},
	
	/**
	 * Get the 'add new on the fly' page's params
	 * @return {object}
	 */
	getAddPageParams: function() {
		var pageParams = {
				modal: true,
				lookupCtrl: this,
				tName: this.lookupTable, 
				pageType: Runner.pages.constants.PAGE_ADD, 
				baseParams: {
					page: this.addPageId,
					parId: this.rowId, 
					field: this.fieldName, 
					pageType: this.pageType,
					table: this.shortTableName,
					editType: Runner.pages.constants.ADD_ONTHEFLY
				},
				afterSave: {
					fn: function( respObj, formObj, fieldControls, pageObj ) {
						if ( respObj.success ) {
							// displayed value may be formatted
							this.processNewFlyAdded( respObj.displayValue, respObj.linkValue, respObj.keys );
							return;
						} 
						
						if ( respObj.message ) {
							// respObj contains error message
							pageObj.displayHalfPreparedMessage( respObj.message, true );
							pageObj.showElement("message");
						} 

						pageObj.bsWin.animate( {scrollTop: 0} );
						
						Runner.delDisabledClass( pageObj.saveButton );
						return false;	
					},
					scope: this
				}
		};
		
		if ( !!this.parentCtrls.length ) {
			pageParams.baseParams.parentsExist = true;
			pageParams.baseParams.parentCtrlsData = JSON.stringify( this.getParentControlsData() );
		}
		
		return pageParams;
	},
	
	/**
	 * Get the 'add new on the fly' page's params
	 * @return {object}
	 */
	getEditPageParams: function() {
		var keys = this.getCurrentItemKeys();
		
		var pageParams = {
				modal: true,
				lookupCtrl: this,
				tName: this.lookupTable, 
				pageType: Runner.pages.constants.PAGE_EDIT,
				keys: keys,
				baseParams: {
					page: this.editPageId,
					parId: this.rowId, 
					field: this.fieldName, 
					pageType: Runner.pages.constants.PAGE_EDIT,
					table: this.shortTableName,
					editType: Runner.pages.constants.EDIT_ONTHEFLY
				},
				afterSave: {
					fn: function( respObj, formObj, fieldControls, pageObj ) {
						if ( respObj.success ) {
							// displayed value may be formatted
							this.processFlyEdited( respObj.displayValue, respObj.linkValue, respObj.keys );
							return;
						} 
						
						if ( respObj.message ) {
							// respObj contains error message
							pageObj.displayHalfPreparedMessage( respObj.message, true );
							pageObj.showElement("message");
						} 

						pageObj.bsWin.animate( {scrollTop: 0} );
						
						Runner.delDisabledClass( pageObj.saveButton );
						return false;
					},
					scope: this
				}
		};
		
		if ( !!this.parentCtrls.length ) {
			pageParams.baseParams.parentsExist = true;
			pageParams.baseParams.parentCtrlsData = JSON.stringify( this.getParentControlsData() );
		}
		
		return pageParams;
	},
	
	getCurrentItemKeys: function() {
		return [];
	},
	
	
	/**
	 * Clear links from children to there's parent ctrl	 
	 * @param {boolean} triggerReload pass true to call reload function on children
	 */
	clearChildrenLinks: function( triggerReload ) {
		// reload all children
		for (var i = 0; i < this.dependentCtrls.length; i++) {
			// if children exists
			if ( this.dependentCtrls[i] ) {
				this.dependentCtrls[i].clearParents( triggerReload );
			}
		}
	},
	
	/**
	 * Deletes link to parent ctrl, and optionaly reloads this
	 * @param {boolean} triggerReload pass true to call reload method
	 */
	clearParents: function( triggerReload ) {
		this.paretCtrls = [];

		if ( triggerReload === true ) {
			this.reload();
		}
	},
	
	/**
	 * @param {string} tName
	 * @param {number} pageId
	 * @param {object} parentFData
	 * @param {boolean} triggerReload
	 */
	setDependencies: function( tName, pageId, parentFData, triggerReload ) {
		if ( !this.hasDependencies() ) {
			return;
		}
		
		if ( this.isSearchControl() && parentFData ) {
			this.setSearhcLookupDependencies( tName, parentFData, triggerReload );
			return;
		}
		
		this.setLookupDependencies( tName, pageId );
	},
	
	/**
	 * @param {string} tName
	 * @param {object} parentFData
	 * @param {boolean} triggerReload
	 */
	setSearhcLookupDependencies: function( tName, parentFData, triggerReload ) {
		var i, pfName, rowId, ctrlIndex, parentCtrl, loaded;
		
		for ( i = 0; i < this.parentFieldNames.length; i++ ) {
			pfName = this.parentFieldNames[ i ];
			
			if ( !parentFData[ pfName ] ) {
				continue;
			}
			
			parentCtrl = Runner.controls.ControlManager.getAt( tName, parentFData[ pfName ].rowId, pfName, parentFData[ pfName ].ctrlIndex );
			if ( !parentCtrl || !parentCtrl.showStatus || !parentCtrl.isLookupWizard() ) {
				continue;
			}				
				
			this.parentCtrls.push( parentCtrl );
			parentCtrl.addDependentCtrls( [ this ] );
							
			if ( triggerReload ) {
				parentCtrl.fireEvent('change');
				continue;
			} 
			
			if ( !loaded && this.preloadData ) {
				this.preload( this.preloadData.vals, this.preloadData.fVal );
				loaded = true;
			}
		}
		
		if ( !loaded ) {
			this.reload();
		}
	},
	
	/**
	 * init dependeces and preload
	 * @param {string} tName
	 * @param {string} pageId
	 */
	setLookupDependencies: function( tName, pageId ) {
		var i, ctrl;

		for (i = 0; i < this.parentFieldNames.length; i++) {
			ctrl = Runner.controls.ControlManager.getAt( tName, pageId, this.parentFieldNames[ i ] );
			
			if ( !ctrl ) {
				continue;
			}
			
			this.parentCtrls.push( ctrl );			
			ctrl.addDependentCtrls( [ this ] );			
		}

		for (i = 0; i < this.mainFields.length; i++) {
			ctrl = Runner.controls.ControlManager.getAt( tName, pageId, this.mainFields[ i ] );
			
			if ( ctrl ) {
				ctrl.addDependentCtrls( [ this ] );	
			}			
	
		}		
		
		if ( this.masterTName && this.masterTId ) {
			for (i = 0; i < this.mainMasterFields.length; i++) {
				ctrl = Runner.controls.ControlManager.getAt( this.masterTName, this.masterTId, this.mainMasterFields[ i ] );
				
				if ( ctrl ) {
					ctrl.addDependentCtrls( [ this ] );
				}
			}
			
			if ( this.mainMasterFields.length ) {
				this.reload();
			}
		}
	},
	
	/**
	 * A stub
	 * @param {array} vals 	 				unparsed values for options
	 * @param {string | array} selectValue	unparsed values of selected options
	 */
	preload: function( vals, selectValue ) {	
	},
	
	/**
	 * @return {object}
	 */
	getMasterContolData: function() {
		var masterData = {};
		
		this.mainMasterFields.forEach( function( mField, idx ) {
			var ctrl = Runner.controls.ControlManager.getAt( this.masterTName, this.masterTId, mField );
			if ( ctrl ) {
				masterData[ mField ] = ctrl.getStringValue();
			}
		}, this );
		
		return masterData;
	},
	
	/**
	 * @param {object} updContext (optional)
	 */
	reload: function( updContext ) {
		var ctrl = this, 
			ajaxParams = {
				mode: this.mode,
				field: this.fieldName,
				pageType: this.pageType,
				page: this.pageObj.pageName,
				isExistParent: !!this.parentCtrls.length ? 1 : 0,
				parentCtrlsData: JSON.stringify( this.getParentControlsData() ),
				data: JSON.stringify( this.getControlsData() ),
				masterTable: this.masterTName,
				shortTName: Runner.pages.PageSettings.getShortTName( this.table )
			},
			selectedValue = this.getValue( true );
			
		if ( this.masterTName && this.mainMasterFields.length ) {
			ajaxParams.masterData = JSON.stringify( this.getMasterContolData() );
		}		
		
		if ( !updContext || typeof updContext !== "object" ) {
			updContext = {};
		}
		updContext.reloadsStarted = updContext.reloadsStarted || 0;
		++updContext.reloadsStarted;
		Runner.runnerAJAX( Runner.getPageUrl( "autocomplete" ), ajaxParams, function( respObj ) {
			--updContext.reloadsStarted;
			/*
			( updContext.reloadDependencies || [] ).forEach( function( data ) {
				if ( data.dependentNames.indexOf( ctrl.fieldName ) !== -1 ) {
					data.resolved.push( ctrl.fieldName );
				}
			});
			*/
			
			ctrl.updateLookupData( respObj.data || [], selectedValue, updContext );
			ctrl.processAutofillDataOnReload( updContext );
			
			ctrl.fireEvent("change", updContext);
			ctrl.clearInvalid();
		});
	},

	/**
	 * @param {object} updContext
	 */
	processAutofillDataOnReload: function( updContext ) {
		if ( updContext && updContext.values && updContext.values[ this.fieldName ] ) {
			this.setValue( updContext.values[ this.fieldName ], true, updContext );	
		}	
	},
	
	/**
	 * Complete other form's fields basing on 'Autofill' settings
	 * @param {object} updContext
	 */
	doAutoCompleteFields: function( updContext ) {
		var ctrl = this;
		if ( !updContext || typeof updContext !== "object" ) {
			updContext = {};
		}
		
		if ( !this.autoCompleteFields.length || updContext.resetHappend ) {
			return;
		}
	
		updContext.autofillDependencies = updContext.autofillDependencies || [];
		var autofillStarted = !!updContext.autofillDependencies.filter( function( data ) {
				return data.fName == ctrl.fieldName;
			}).length;
		
		//	do nothing if control has already initiated autofill within the current update operation
		if ( autofillStarted ) {
			return;
		}

		updContext.autofillDependencies.push({ 
			fName: this.fieldName, 
			acFields: this.autoCompleteFields,
			resolved: false 
		});		
		
		var ajaxParams = {
				shortTName: Runner.pages.PageSettings.getShortTName( this.table ),
				mainField: this.fieldName,
				linkFieldVal: this.getValue(),
				pageType: this.pageType,
				page: this.pageObj.pageName,
				data: JSON.stringify( this.getControlsData() ),
				masterTable: this.masterTName				
			};
		
		Runner.runnerAJAX( Runner.getPageUrl('autofillfields'), ajaxParams,
			$.proxy( function( respObj ) {
				if ( !respObj.success ) {
					return false;
				}
				
				var data = respObj.data,
					valToFill, i, ctrl;

				updContext.values = updContext.values || {};
				
				for (i = 0; i < this.autoCompleteFields.length; i++) {
					updContext.values[ this.autoCompleteFields[i].masterField ] = [ data[ this.autoCompleteFields[i].lookupField ] ];
				}
				
				updContext.autofillDependencies.forEach( function( data ) {
					if ( data.fName === this.fieldName ) {
						data.resolved = true;
					}
				}, this );
				 
				for (i = 0; i < this.autoCompleteFields.length; i++) {
					ctrl = Runner.controls.ControlManager.getAt( this.table, this.rowId, this.autoCompleteFields[i].masterField );

					if ( !ctrl || this.autoCompleteFields[i].masterField == this.fieldName ) {
						continue;
					}

					valToFill = data[ this.autoCompleteFields[i].lookupField ];
					valToFill = valToFill === null || valToFill === undefined ? '' : valToFill;
					
					var val = valToFill["value"];
					var dispVal = valToFill["dispValue"];

					ctrl.setValue( val, true, updContext );
					if ( dispVal !== undefined ) {
						ctrl.setDisplayValue( dispVal );
					}
				}
				
			}, this )
		);
	},
	
	/**
	 * Override simple dropDown event,
	 * add reloading for dependent dropDowns
	 * @param {object} e		The event object or null 
	 * if the handler is invoked with the fireEvent method
	 * @param {object} updContext
	 */
	"change": function( e, updContext ) {		
		if ( !updContext || typeof updContext !== "object" ) {
			updContext = {};
		}
		
		this.doAutoCompleteFields( updContext );
		this.reloadDependentCtrls( updContext, e );	

		this.updataEditCurrentButton();

		return false;
	},

	/**
	 * A stub
	 * @return {array}
	 */
	getParentLookupFieldNames: function() {
		return this.parentFieldNames;
	},
	
	/**
	 * Check if a lookup has main controls
	 * @return {boolean}
	 */	
	hasDependencies: function() {
		return this.parentFieldNames && this.parentFieldNames.length 
			|| this.mainFields && this.mainFields.length 
			|| this.mainMasterFields && this.mainMasterFields.length ;
	},
	
	/**
	 * @return {boolean}
	 */
	isLookupWizard: function() {
		return true;
	},
	
	isEmptyForSearch: function() {
		return this.isEmpty();
	},

	updataEditCurrentButton: function() {
		this.isEditCurrentItemAvailable() &&
			this.editCurrent.toggleClass( 'hidden-link', !this.isModifyVisible() );
	},

	hideEditCurrentButton: function() { 
		this.isEditCurrentItemAvailable() &&
			this.editCurrent.toggleClass( 'hidden-link', true );
	},

	/**
	 * Process the edited 'on the fly' item 
	 * @param {string} text
	 * @param {string | number} value
	 * @param {array} keys
	 */
	processFlyEdited: function( text, value, keys ) {
		this.updateOption( text, value, keys );
		this.setValue( value, true );
	},
	
	isModifyVisible: function() {
		// only one option is selected
		return !this.isEmpty() && !!this.getCurrentItemKeys().length;
	},
	 
	isEditCurrentItemAvailable: function() {
		return !!this.editCurrent.length;
	}
});
/**
 * Radio control class
 * @requires Runner.controls.Control
 */
Runner.controls.RadioControl = Runner.extend( Runner.controls.LookupWizard, {
	/**
	 * Radio DOM elem id, starts from + _i 
	 * where i index of element, starts from 0
	 * @type {String} 
	 */
	radioElemsId: "",
	
	/**
	 * Radio jQuery obj
	 * @type {Array} 
	 */
	radioElemsArr: [],
	
	/**
	 * Radio name attr 
	 * @type {String}
	 */
	radioElemsNameAttr: "",
	
	/**
	 * jQuery object which contains all radios
	 * @type {jQuery object}
	 */
	radioElem: null,
	
	/**
	 * Count of radio buttons
	 * @type {Number}
	 */
	radioElemsCount: 0,
	
	/**
	 * Override parent contructor
	 * @constructor
	 * @param {Object} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.RadioControl.superclass.constructor.call( this, cfg );
	},
	
	/**
	 * Set the value element
	 */	
	assignValueElem: function() {
		Runner.controls.RadioControl.superclass.assignValueElem.call(this);
		
		// id starts from
		this.radioElemsId = "radio_" + this.goodFieldName + "_" + this.rowId + "_";
		// radio elems name attr
		this.radioElemsNameAttr = "radio_" + this.goodFieldName + "_" + this.rowId; 

		this.radioElem = $('input[name=' + this.radioElemsNameAttr + ']');
		this.radioElemsCount = this.radioElem.length;
		
		this.radioElemsArr = [];
		for (var i = 0; i < this.radioElemsCount; i++) {
			this.radioElemsArr.push( $("#" + this.radioElemsId + i) );
		}		
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */	
	assignsElemsForEvent: function() {
		for (var i = 0; i < this.radioElemsCount; i++) {
			this.elemsForEvent.push( this.radioElemsArr[i].get(0) );
		}
	},

	/**
	 * Set the control's events list
	 */	
	assignListOfEvents: function() {
		this.addEvent( ["change", "keyup", "edited", "editing"] );
	},
		
	/**
	 * Set value to the control
	 * @param {string} val
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @return {boolean}
	 */
	setValue: function(val, triggerEvent, updContext) {
		var choosen = false,
			readOnly = this.isReadonly(),
			i;

		if ( readOnly ) {
			this.makeReadWrite();
		}
			
		// loop for all radio elements
		for (i = 0; i < this.radioElemsCount; i++) {
			if (this.radioElemsArr[i].val() == val) {
				// set checked radio element
				this.radioElemsArr[i].prop("checked", true);
				//set value in hidden element
				this.valueElem.val( val );
				choosen = true;
			} else {
				this.radioElemsArr[i].prop("checked", false);
			}
		}

		if ( !choosen ) {
			this.valueElem.val('');
		}

		if ( readOnly ) {
			this.makeReadonly();
		}
		
		if ( triggerEvent === true ) { 
			this.fireEvent("change", updContext);
		}
		
		return choosen;
	},
	
	/**
	 * Sets disable radio control
	 * @method
	 */
	setDisabled: function() {
		for (var i = 0; i < this.radioElemsCount; i++) {
			this.radioElemsArr[i].get(0).disabled = true;
		}
		this.hideEditCurrentButton();
		return true;
	},
	/**
	 * Sets disaqble attr false
	 * Should be overriden for sophisticated controls
	 * @method
	 */
	setEnabled: function() {
		for (var i = 0; i < this.radioElemsCount; i++) {
			this.radioElemsArr[i].get(0).disabled = false;
		}
		this.updataEditCurrentButton();
		return true;
	},	

	/**
	 * The change event handler
	 * @param {object} e		The event object or null 
	 * if the handler is invoked with the fireEvent method
	 * @param {object} updContext
	 */
	"change": function( e, updContext ) {
		if ( !updContext || typeof updContext !== "object" ) {
			updContext = {};
		}		
		
		if ( e && e.target.value != this.getValue() ) {
			// set new val to hidden elem for chrome
			this.setValue( e.target.value, false );
			this.doAutoCompleteFields( updContext );
		}
		
		this.reloadDependentCtrls( updContext, e );
		this.updataEditCurrentButton();
	},
	
	makeReadonly: function() {
		this.readOnly = true;
		this.setDisabled();
		return true;
	},
	
	makeReadWrite: function() {
		this.readOnly = false;
		this.setEnabled();
		return true;
	},
	
	/**
	 * Get the displayed field's value
	 * @return {string}
	 */
	getDisplayValue: function() {
		var value = this.valueElem.val(),
			i;
			
		for (i = 0; i < this.radioElemsCount; i++) {
		    if ( this.radioElemsArr[i].val() == value && this.radioElemsArr[i].prop("checked") ) {
				return $( "#label_" + this.radioElemsArr[i].attr("id") ).text();		
			}
		}
		return "";
	},
	
	/**
	 * First loading, without ajax. Should be called directly
	 * @param {string} txt unparsed values for checkboxes
	 * @param {string} selectValue unparsed values of selected checkboxes
	 */
	preload: function(vals, selectValue) {
		// clear all old radios
		this.clearCtrl();
		// adds new checkboxes
		this.addRadioByArr( vals );
		// don't need to use ajax reload call
		this.setValue( selectValue, false );
		this.updataEditCurrentButton();
	},

	/**
	 * @param {array} data
	 * @param {array} selectedValues
	 * @param {object} updContext
	 */ 
	updateLookupData: function( data, selectValue, updContext ) {
		// clear ctrl
		this.clearCtrl();
		// add new radios
		this.addRadioByArr( data );
		
		// restore the previously marked radios
		this.setValue( selectValue, false );		
	},
	
	/**
	 * Deletes all radios from ctrl
	 * @method
	 */
	clearCtrl: function() {
		var divWrapper = this.spanContElem.find('div[data-lookup-options]');
		divWrapper.children().remove();
		
		this.radioElemsArr = [];
		this.radioElemsCount = 0;
	},
	
	/**
	 * Adds radio to ctrl
	 * @param {string} text
	 * @param {string} val
	 */
	addRadio: function(text, value, keys) {
		var $radioButton, $radioLabel,
			delim = '&nbsp;',
			ind = this.radioElemsArr.length,
			$divWrapper, $spanWrapper;

		$divWrapper = $('div[data-lookup-options]', this.spanContElem);

		$spanWrapper = $('<span class="radio"><label></label></span>').appendTo( $divWrapper );
			
		$divWrapper.addClass( this.isHorizontalLookup ? 'rnr-horizontal-lookup' : 'rnr-vertical-lookup' );
		
		$radioButton = $('<input>')
			.attr('id', this.radioElemsId + ind)
			.addClass('rnr-radio-button')
			.attr('type', 'radio')
			.attr('name', this.radioElemsId)
			.val( value );
			
		if ( keys && this.isEditCurrentItemAvailable() ) {
			$radioButton.data('keys', keys);
		}
		
		$radioLabel = $('<span>')
			.attr('id', 'label_' + this.radioElemsId + ind)
			.addClass('rnr-radio-label')
			.html( text );
		
		$('label', $spanWrapper).append( $radioButton ).append( delim ).append( $radioLabel );
		
		this.radioElemsArr.push( $radioButton ); 
		this.radioElemsCount++;
	},
	
	/**
	 * Add radios from array.
	 * Array must have such structure:
	 * array[0] = value, array[1] = text,
	 * array[2] = value, array[3] = text,
	 * 2*i - indexes of values; 2*i+1 - indexes of text. I starts from 0   
	 * @param {array} radioElemsArr
	 */
	addRadioByArr: function( radioElemsArr ) {
		var i;
		for (i = 0; i < radioElemsArr.length; i++) {
			this.addRadio( radioElemsArr[i]["displayValue"], radioElemsArr[i]["linkValue"], radioElemsArr[i]["keys"] );
		}
		this.radioElem = $('input[name=' + this.radioElemsNameAttr + ']');
		// count of elems get from jQuery obj
		this.radioElemsCount = this.radioElemsArr.length;

		//reinit Custom Events
		this.purgeListeners();
		this.initCustomEvents();
	},
		
	/**
	 * Sets focus to the element
	 * Since focus cannot be set properly on the contol's value element 
	 * It sets focus on the first radio button in the list
	 * @method
	 * @intellisense
	 */
	setFocus: function( triggerEvent ) {		
		if ( !this.appearOnPage() || !this.showStatus || !this.radioElemsArr.length || this.radioElemsArr[0].is(':disabled') ) {
			this.isSetFocus = false;
			return;
		}
		
		this.radioElemsArr[0].focus();
		this.isSetFocus = true;
		
		if (triggerEvent === true) {
			this.fireEvent("focus");
		}
	},

	/**
	 * Process the new added 'on the fly' item 
	 * @param {string} text
	 * @param {string | number} value
	 * @param {array} keys
	 */	
	processNewFlyAdded: function(text, value, keys) {
		this.addRadio(text, value, keys);
		this.setValue(value, true);
		
		//reinit Custom Events
		this.purgeListeners();
		this.initCustomEvents();
	},
	
	/**
	 * Clone html for iframe submit
	 * @return {array}
	 * @intellisense
	 */
	getForSubmit: function() {
		if ( !this.appearOnPage() ) {
			return [];
		}
		
		var cloneElem = this.valueElem.clone(),
			val = this.valueElem.val(), 
			i, rButton;
		
		if (!val) {		
			for (i = 0; i < this.radioElemsArr.length; i++) {
				rButton = this.radioElemsArr[i];
				// the second condition is for the elements that were moved in DOM
				if ( rButton.is(":checked") || $("#" + rButton.attr("id")).is(":checked") ) {
					val = rButton.val();
					break;
				}
			}
		}
		
		return [ cloneElem.val( val ) ];
	},

	getCurrentItemKeys: function() {
		var $radioButton = this.radioElemsArr.find( function( $r ) {
				return $r.is(":checked")
			});

		if ( $radioButton && this.isEditCurrentItemAvailable() ) {
			return $radioButton.data('keys') || [];
		}

		return [];
	},

	/**
	 * update lookup option
	 * @param {string} displayedValue
	 * @param {string} val
	 * @param {array} keys
	 */
	updateOption: function( displayedValue, val, keys ) {
		var $radioButton = this.radioElemsArr.find( function( $r ) {
			return $r.is(":checked")
		});
		
		$radioButton
			.attr('value', val)
			.data('keys', keys);
			
		$("#label_" + $radioButton.attr("id")).html( displayedValue );
	},
});/**
 * Select control class. 
 * @requires Runner.controls.LookupWizard
 * @class Runner.controls.DropDownLookup
 */
Runner.controls.DropDownLookup = Runner.extend( Runner.controls.LookupWizard, {
	/**
	 * The number of rows in a select element
	 * @type {number} 
	 */
	selectSize: 1,
	
	/**
	 * DropDown DOM options array
	 * @type {Array}
	 */
	optionsDOM: null,
	
	/**
	 * @type {jQuery object} 
	 */
	simpleSearchBox: {},

	/**
	 * @type {boolean}
	 */
	multiSelectionWithTabsEnabled: false,
	
	/**
	 * Override parent contructor 
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.DropDownLookup.superclass.constructor.call( this, cfg );			
	},
	
	/**
	 * Set the value elements
	 */	
	assignValueElem: function() {
		Runner.controls.DropDownLookup.superclass.assignValueElem.call( this );	

		// add options array property
		this.optionsDOM = this.appearOnPage() ? this.valueElem.get(0).options : [];	
		
		this.initMultiselectionWithTabs();
		
		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") && !this.multiSelectionWithTabsEnabled ) {
			this.simpleSearchBox = $("#" + this.valContId + "_simpleSearchBox");
		}

		this.initChanginAppearance();	
	},
	
	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		Runner.controls.DropDownLookup.superclass.assignsElemsForEvent.call( this );	
		
		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") && !this.multiSelectionWithTabsEnabled ) {
			this.elemsForEvent.push( this.simpleSearchBox.get(0) ); 
		}
	},
	
	/**
	 * Set the control's events list
	 */	
	assignListOfEvents: function() {
		Runner.controls.DropDownLookup.superclass.assignListOfEvents.call( this );

		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") && !this.multiSelectionWithTabsEnabled ) {
			this.addEvent( ["keyup"] ); 
		}		
	},

	/**
	 * Adjust the select element with jQuery.chosen plugin 
	 */
	initMultiselectionWithTabs: function() {
		var self = this;
		if ( this.selectSize != 1 || !this.multiSelectionAllowed || !this.valueElem.length ) {
			return;
		}

		this.valueElem.chosen({
			placeholder_text_multiple: Runner.lang.constants.PLEASE_SELECT,
			width: "100%",
			rtl: Runner.isDirRTL(),
		});

		var chosen = this.valueElem.data("chosen");
		
		if ( !chosen ) {
			return;
		}
		
		if ( this.pageObj.bsScrollBodyInited ) {
			// use default absolute positoning
			this.valueElem
				.on("chosen:showing_dropdown", function( e, data ) {
					data.chosen.dropdown.show();
					
					var $tBody = self.pageObj.gridElem.children('tbody');
					var position = data.chosen.container.position();
					
					if ( !data.chosen.dropdown.height() ) {
						setTimeout( function() {
							var delta = 50;
							if ( position.top + data.chosen.container.outerHeight() + 50 > $tBody.height() ) {
								data.chosen.dropdown.css({
									top: data.chosen.dropdown.position().top - data.chosen.dropdown.height() - data.chosen.container.outerHeight()
								});
							}
						}, 0 );
					}
				})
				.on("chosen:hiding_dropdown", function( e, data ) {
					data.chosen.dropdown.hide();
				});
				
			this.multiSelectionWithTabsEnabled = true;
			return;
		}
		
		// change a chosen container's positioning
		chosen.container.css("position", "static");
		chosen.dropdown.css("top", 0);
		
		// augment showing/hiding results methods 
		this.valueElem
			.on("chosen:showing_dropdown", function( e, data ) {
				var chosen = data.chosen,
					offset = chosen.container.offset();
					
				if ( self.pageObj.openMode ==  Runner.pages.constants.OPENMODE_POPUP ) {						
					var containingBlockOffset = { top: 0, left: 0 };
					var $containingBlock = $(window);
					if ( typeof chosen.dropdown.get(0).closest === "function" ) { //?
						// modal dialog is transform initial container
						$containingBlock = chosen.dropdown.parents(".modal-dialog");
						containingBlockOffset = $containingBlock.offset();
					}
					
					chosen.dropdown.css({
						position: "fixed",
						width: chosen.container.width(),
						top: offset.top - containingBlockOffset.top + chosen.container.outerHeight(),
						left: Runner.isDirRTL() ? null : offset.left - containingBlockOffset.left,
						right: Runner.isDirRTL() ? $containingBlock.innerWidth() - offset.left + containingBlockOffset.left - chosen.container.width() : null,
					}).show();
				} else {
					var offsetParent = chosen.container.offsetParent();
				
					chosen.dropdown.css( {
						width: chosen.container.width(),
						top: offset.top + chosen.container.outerHeight(),
						left: Runner.isDirRTL() ? null : offset.left,
						right: Runner.isDirRTL() ? offsetParent.innerWidth() - offset.left - chosen.container.width() : null
					}).show();						
				}
			})		
			.on("chosen:hiding_dropdown", function( e, data ) {
				data.chosen.dropdown.hide();
			});
		
		this.multiSelectionWithTabsEnabled = true;
	},
	
	/**
	 * Set the control's default value
	 */
	setDefaultValue: function() {
		this.defaultValue = this.getValue( true );		
	},
	
	/**
	 * Sets value to DropDown. Tries to set all values from array if multiselect control
	 * @param {mixed} pvals
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @return {boolean} true if success otherwise false
	 */
	setValue: function( pvals, triggerEvent, updContext ) {
		var vals = Runner.isArray( pvals ) ? pvals : [ pvals ],
			ctrl = this,
			// number of chosen options
			choosen = 0,
			readOnly = this.isReadonly(),
			j;

		if ( readOnly ) {
			this.makeReadWrite();
		}	
			
		this.valueElem.find( 'option' ).each( function( i, option ) {
			for (j = 0; j < vals.length; j++) {
				if ( option.value == vals[j] ) {
					try {
						ctrl.optionsDOM[i].selected = true;
						choosen++;
					} catch(e){
					};
					
					if ( ctrl.multiSelectionAllowed ) {
						break;
					} 
					
					ctrl.valueElem.prop("selectedIndex", i);
					return false;
				}
				
				try {
					ctrl.optionsDOM[i].selected = false;
				} catch(e){
				};	
			}
			
			if ( !vals.length ) {
				ctrl.optionsDOM[i].selected = false;
			}
		});
		
		if ( !!vals.length && !choosen || !this.multiSelectionWithTabsEnabled && choosen > this.selectSize ) {			
			readOnly && this.makeReadonly();
			triggerEvent && this.fireEvent("change", updContext);
			return false;
		}	
		
		if ( this.multiSelectionWithTabsEnabled ) {
			this.valueElem.trigger("chosen:updated");
		}
		
		if ( readOnly ) {
			this.makeReadonly();
		}
		
		if ( triggerEvent === true ) {
			this.fireEvent("change", updContext);
		}			
		
		return true;
	},
	
	/**
	 * Returns values from dropDown. 
	 * @method
	 * @param {boolean} returnArray
	 * @return {mixed}
	 */
	getValue: function( returnArray ) {
		var selVals = [], i;

		if ( !this.optionsDOM ) {
			return "";
		}
		
		// loop for all options
		for (i = 0; i < this.optionsDOM.length; i++) {
			if ( this.optionsDOM[i].selected && selVals.indexOf( this.optionsDOM[i].value ) === -1 ) {
				selVals.push(this.optionsDOM[i].value)
			}
		}
		if ( !this.optionsDOM.length && this.appearOnPage() ) {
			this.valueElem.find( 'option:selected' ).each( function( i, option) {
				selVals.push( option.value );
			});
		}
		
		if ( returnArray === true || selVals.length > 1 ) { 
			return selVals;
		}
		if ( selVals.length === 1 ) {
			return selVals[0];
		}
		return "";
	},
	
	/**
	 * Return an escaped string value
	 * @param {Array} valueArray	 
	 * @return {string}
	 * @intellisense
	 */	
	getStringValue: function( valueArray ) {		
		var valsArr = valueArray || this.getValue( true ), i;
		
		if ( !this.multiSelectionAllowed ) {
			return valsArr[0];
		}
		
		for (i = 0; i < valsArr.length ;i++) {
			if ( valsArr[i].indexOf(',') !== -1 || valsArr[i].indexOf('"') !== -1 ) {
				valsArr[i] = '"' + valsArr[i].replace(/"/g, '""') + '"';
			}
		}
		
		return valsArr.join(",");
	},	
	
	/**
	 * Get the default control's value
	 * @return {String | Number}
	 */
	getDefaultControlValue: function() {
		if ( this.defaultValue === '' ) {
			return '';
		}
		return this.getStringValue( this.defaultValue );
	},
	
	/**
	 * Checks if control value is empty. 
	 * @method
	 * @return {boolean}
	 */
	isEmpty: function() {
		var selVals = this.getValue();
		
		return selVals === "";
	},
	
	/**
	 * Deletes all options from ctrl
	 * @method
	 */
	removeOptions: function() {
		var select = this.valueElem.get(0), i;
		
		for (i = this.optionsDOM.length; i--; ) {
			select.remove(i);
		}
		
		if ( this.multiSelectionWithTabsEnabled ) {
			this.valueElem.trigger("chosen:updated");
		}		
	},
	
	/**
	 * Add a new option to the select element
	 * @param {string} text
	 * @param {string} val
	 */
	addOption: function( text, val, keys ) {
		var $option = $('<option></option>')
			.attr('value', val)
			.html( text )
			.appendTo( this.valueElem );
			
		if ( keys && this.isEditCurrentItemAvailable() ) {
			$option.data('keys', keys);
		}
		
		if ( this.multiSelectionWithTabsEnabled ) {
			this.valueElem.trigger("chosen:updated");
		}
	},
	
	/**
	 * Add options from array.
	 * Array must have the following structure:
	 * 		array[0] = value, array[1] = text,
	 * 		array[2] = value, array[3] = text,
	 * 		2*i - indexes of values; 2*i+1 - indexes of text. I starts from 0 
	 * @param {array} optionsArr
	 */
	addOptionsArr: function( optionsArr ) {
		for (var i = 0; i < optionsArr.length; i++) {
			this.addOption( optionsArr[i]["displayValue"], optionsArr[i]["linkValue"], optionsArr[i]["keys"] );
		}
	},
	
	/**
	 * First loading, without ajax. Should be called directly
	 * @param {array} vals 	 				unparsed values for options
	 * @param {string | array} selectValue	unparsed values of selected options
	 */
	preload: function( vals, selectValue ) {
		// clear all old options
		this.removeOptions();

		if ( !this.multiSelectionAllowed ) {
			// add empty option for non multiselect
			this.addOption( Runner.lang.constants.PLEASE_SELECT, "" );
		}

		// load options
		this.addOptionsArr( vals );
		if ( selectValue !== null && selectValue !== '' ) {
			// don't need to use ajax reload call
			this.setValue( selectValue, false );
			this.updataEditCurrentButton();
			return;	
		}
		
		if ( this.optionsDOM.length > 0  && this.mode !== Runner.controls.constants.MODE_SEARCH ) {
			this.setValue( [ this.optionsDOM[0].value ], false );
			this.updataEditCurrentButton();
		}
	},
	
	
	/**
	 * @param {array} data
	 * @param {array} selectedValues
	 * @param {object} updContext
	 */ 
	updateLookupData: function( data, selectedValues, updContext ) {
		this.removeOptions();	
		// add empty option for non multiple select if it doesn't comes from server data
		if ( !this.multiSelectionAllowed ) {
			// add empty option for non multiselect
			this.addOption( Runner.lang.constants.PLEASE_SELECT, "" );				
		}

		// load options
		this.addOptionsArr( data );
		
		// if only one values except please select, so choose it
		if ( this.optionsDOM.length == 2 && !this.multiSelectionAllowed || this.optionsDOM.length == 1 && this.multiSelectionAllowed ) {
			this.setValue( [ this.optionsDOM[ this.optionsDOM.length - 1 ].value ], false );	
		} else {
			this.setValue( selectedValues, false );
		}			
	},
	
	/**
	 * Initilize the two-states behaviour for the search control with the same displayed and link fields #8538
	 */
	initChanginAppearance: function() {
		if ( this.mode !== Runner.controls.constants.MODE_SEARCH || !this.valueElem.data("usesuggests") || !this.simpleSearchBox.length ) {
			this.updateAppearance = Runner.emptyFn;
			return;
		}
		
		// redifine some basic methods:
		this.getStringValue = function( valueArray ) {
			if ( this.simpleSearchBox.is(":visible") && !valueArray ) {
				return this.simpleSearchBox.val(); 
			}
			return Runner.controls.DropDownLookup.prototype.getStringValue.call( this, valueArray );	
		};
		
		this.isEmpty = function() {
			if ( this.simpleSearchBox.is(":visible") ) {
				return this.simpleSearchBox.val() === "";
			}
			return Runner.controls.DropDownLookup.prototype.isEmpty.call( this );
		};

		this.makeReadonly = function() {
			this.simpleSearchBox.prop({
				'disabled': true,
				'readonly': true
			});	
			this.hideEditCurrentButton();
			return Runner.controls.DropDownLookup.prototype.makeReadonly.call( this );
		};
		
		this.makeReadWrite = function() {
			this.simpleSearchBox.prop({
				'disabled': false,
				'readonly': false
			});
			this.updataEditCurrentButton();
			return Runner.controls.DropDownLookup.prototype.makeReadWrite.call( this );			
		};
		
		this.setDisabled = function() {
			this.simpleSearchBox.prop('disabled', true);
			this.hideEditCurrentButton();
			return Runner.controls.DropDownLookup.prototype.setDisabled.call( this );	
		}; 
		
		this.setEnabled = function() {
			this.simpleSearchBox.prop('disabled', false);
			this.updataEditCurrentButton();
			return Runner.controls.DropDownLookup.prototype.setDisabled.call( this );	
		};

		this.getValue = function( returnArray ) {
			if ( this.simpleSearchBox.is(":visible") ) {
				return this.simpleSearchBox.val();
			}
			return Runner.controls.DropDownLookup.prototype.getValue.call( this, returnArray );	
		};
		
		this.setValue = function( value ) {
			if ( this.simpleSearchBox.is(":visible") ) {
				return this.simpleSearchBox.val( value );
			}
			return Runner.controls.DropDownLookup.prototype.setValue.call( this, value );	
		};
		
		this.preload = function( vals, selectValue ) {
			if ( this.simpleSearchBox.is(":visible") ) {
				return;
			}
			return Runner.controls.DropDownLookup.prototype.preload.call( this, vals, selectValue );
		};

		this.reload = function( updContext ) {
			if ( this.simpleSearchBox.is(":visible") ) {
				return;
			}
			return Runner.controls.DropDownLookup.prototype.reload.call( this, updContext );			
		};

		this.getDispElem = function() {
			if ( this.simpleSearchBox.is(":visible") ) {
				return this.simpleSearchBox;
			}
			return Runner.controls.DropDownLookup.prototype.getDispElem.call( this );
		};

		this.implySuggests = function() {
			return this.simpleSearchBox.is(":visible");
		};		
	},
	
	/**
	 * Change the search control's appearance on the search option change #8538		
	 * @param {string} searchOption
	 */	
	updateAppearance: function( searchOption ) {
		var simpleControlOptions = [ "Contains", "NOT Contains", "Starts with", "NOT Starts with" ];
		if ( $.inArray( searchOption, simpleControlOptions ) === -1 ) {
			( this.isReadonly() ? this.readonlyElem : this.valueElem ).show();
			this.simpleSearchBox.hide();
			return;
		}
		( this.isReadonly() ? this.readonlyElem : this.valueElem ).hide();
		this.simpleSearchBox.show();		
	},	
	
	/**
	 * Overrides parent function for element control
	 * Sets disable attr true
	 * Sets hidden css style true for link add New
	 * @method
	 */
	setDisabled: function() {
		if ( !this.valueElem ) {
			return false;
		}
		
		this.valueElem.prop('disabled', true);
	
		if ( this.addNew ) {
			this.addNew.css('visibility', 'hidden');					
		}			
		
		if ( this.multiSelectionWithTabsEnabled ) {
			$('option', this.valueElem).prop('disabled', true);
			this.valueElem.trigger("chosen:updated");
		}
		
		return true;
	},
	
	/**
	 * Overrides parent function for element control
	 * Sets disable attr false
	 * Sets visible css style true for link add New
	 * @method
	 */
	setEnabled: function() {
		if ( !this.valueElem ) {
			return false;
		}
		
		this.valueElem.prop('disabled', false);
		
		if ( this.addNew ) {
			this.addNew.css('visibility', 'visible');					
		}	

		if ( this.multiSelectionWithTabsEnabled ) {
			$('option', this.valueElem).prop('disabled', false);
			this.valueElem.trigger("chosen:updated");
		}
		
		return true;
	},
	
	/**
	 * Clone html for iframe submit.
	 * jQuery clone method won't clone object with new selected values
	 * that's why we need to set values in clone object separetely
	 * @return {array}
	 */
	getForSubmit: function() {
		if ( !this.appearOnPage() ) {
			return [];
		}
		
		var $clone = this.valueElem.clone(), 
			selVals = this.getValue( true ),
			cloneOpt = $clone.get(0).options,
			i, j;
		
		if ( this.multiSelectionWithTabsEnabled && this.isReadonly() ) {
			$clone.prop("disabled", false);
			$(cloneOpt).prop("disabled", false);
		}
		
		for (i = 0; i < cloneOpt.length; i++) {
			for (j = 0; j < selVals.length; j++) {
				if ( cloneOpt[i].value == selVals[j] ) {
					if ( !this.multiSelectionAllowed ) {
						$clone.get(0).selectedIndex = i;
					}
					cloneOpt[i].selected = true;
					break;
				} 
				cloneOpt[i].selected = false;
			}
			
			if ( !selVals.length ) {
				cloneOpt[i].selected = false;
			}
		}
		
		if ( this.multiSelectionWithTabsEnabled && !cloneOpt.length || !selVals.length ) {
			$clone.append( '<option selected="selected" value=""></option>' ); 
		}
		return [ $clone ];
	},
	
	/**
	 * Make the control 'readonly'
	 */
	makeReadonly: function() {
		this.addNew.hide(); 
	
		if ( this.multiSelectionWithTabsEnabled ) {
			this.readOnly = true;
			this.valueElem.prop("disabled", false);
			$('option', this.valueElem).prop('disabled', true);
			
			this.valueElem.trigger("chosen:updated");
			return;
		}
		
		Runner.controls.DropDownLookup.superclass.makeReadonly.call( this );
	},
	
	/**
	 * Make the control writable
	 */
	makeReadWrite: function() {	
		this.addNew.show();
		
		if ( this.multiSelectionWithTabsEnabled ) {
			this.readOnly = false;
			this.valueElem.prop("disabled", false);
			$('option', this.valueElem).prop("disabled", false);
			
			this.valueElem.trigger("chosen:updated");
			return;
		}

		Runner.controls.DropDownLookup.superclass.makeReadWrite.call( this );	
	},
	
	/**
	 * Get the displayed field's value
	 * @return {string}
	 */
	getDisplayValue: function() {
		var displayValues = [],
			i = this.multiSelectionAllowed ? 0 : 1;
		
		for ( ; i < this.optionsDOM.length; i++) {
			if ( this.optionsDOM[i].selected ) {
				displayValues.push( this.optionsDOM[i].text );			
			}
		}
		return displayValues.join(", ");
	},
	
	/**
	 * Process the new added 'on the fly' item 
	 * @param {string} text
	 * @param {string | number} value
	 * @param {array} keys
	 */		
	processNewFlyAdded: function( text, value, keys ) {
		var values;
		
		if ( this.multiSelectionAllowed  ) {
			values = this.getValue( true ); 
			values.push( value );
		} else {
			values = [ value ];
		}
		
		this.addOption( text, value, keys );
		this.setValue( values, true );
	},
	
	/**
	 * Set an empty value to the control
	 */	
	clear: function() {
		this.clearInvalid();
		if ( !this.multiSelectionWithTabsEnabled ) {
			this.setValue('', false);	
			$("#" + this.valContId + "_simpleSearchBox").val("");
		} else {
			$('option:selected', this.valueElem).prop('selected', false);
			this.valueElem.trigger("chosen:updated");
		}
	},
	
	/**
	 * Sets focus to the control's element.
	 * @param {Boolean} triggerEvent
	 * @method
	 * @intellisense
	 */	
	setFocus: function( triggerEvent ) {
		if ( !this.multiSelectionWithTabsEnabled ) {
			Runner.controls.DropDownLookup.superclass.setFocus.call( this, triggerEvent );		
			return;
		}
		
		var chosenCtrl = this.valueElem.data('chosen');
		if( !chosenCtrl ) {
			return;
		}
		
		if ( !this.appearOnPage() || this.valueElem.prop('disabled') || !this.showStatus || !chosenCtrl.container.is(':visible') ) {
			return;
		}
		
		chosenCtrl.search_field.trigger("focus");
		this.isSetFocus = true;
	},
	
	/**
	 * Check if search suggests need initializing for the control
	 * @return {boolean}
	 */
	useSearchSuggests: function() {
		return this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") && !this.multiSelectionWithTabsEnabled;
	},
	
	deleteOptions: function() {
		var select = this.valueElem.get(0), i;
		
		for (i = this.optionsDOM.length; i--; ) {
			if ( this.optionsDOM[i].value !== Runner.lang.constants.PLEASE_SELECT ) {
				select.remove(i);
			}
		}
		
		if ( this.multiSelectionWithTabsEnabled ) {
			this.valueElem.trigger("chosen:updated");
		}
	},
	
	getCurrentItemKeys: function() {
		return this.valueElem.find( 'option:selected' ).eq(0).data('keys') || [];
	},
	
	/**
	 * Add a new option to the select element
	 * @param {string} displayedValue
	 * @param {string} val
	 * @param {array} keys
	 */
	updateOption: function( displayedValue, val, keys ) {
		this.valueElem.find( 'option:selected' ).eq(0)
			.html( displayedValue )
			.attr('value', val)
			.data('keys', keys);
	},
	
	isModifyVisible: function() {
		return !this.isEmpty() && this.valueElem.find( 'option:selected' ).length === 1 
			&& !!this.getCurrentItemKeys().length;
	}
});
/**
 * @class Runner.controls.CheckBox
 */
Runner.controls.CheckBox = Runner.extend( Runner.controls.Control, {
	/**
	 * type element jQuery obj
	 * @type {object}
	 */
	typeElem: null,
	
	reservedBoolean: null,
	
	/**
	 * @contructor
	 * @param {object} cfg
	 */ 
	constructor: function( cfg ) {
		Runner.controls.CheckBox.superclass.constructor.call(this, cfg);
		
		//	when 'update selected', click on the checkbox shouldn't affect 'update selected' checkbox
		this.valueElem.on( 'click', ( e ) => e.stopPropagation() );
	},

	/**
	 * Set the value element and the corresponding properties
	 */	
	assignValueElem: function() {		
		Runner.controls.CheckBox.superclass.assignValueElem.call(this);
	
		// assign the control's type hidden element		
		this.typeElem = $("#" + "type_" + this.goodFieldName + "_" + this.rowId);
		// to set value correctly for postgre
		this.reservedBoolean = { 
			'true': this.valueElem.data('true'),
			'false': this.valueElem.data('false'),
		};
	},
	
	/**
	 * Sets array of values to checkboxes
	 * @method
	 * @param {mixed} value 			The contorl's value or an array of values
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @return {Boolean} true if success otherwise false
	 */	
	setValue: function( value, triggerEvent, updContext ) {
		var readOnly = this.isReadonly();
		
		if ( readOnly ) {
			this.makeReadWrite();
		}

		this.valueElem.prop( "checked",  value && value !== "0" && value !== this.reservedBoolean['false'] ); 
		
		if ( triggerEvent === true ) {
			this.fireEvent("change", updContext);
		}
		
		if ( readOnly ) {
			this.makeReadonly();
		}
		
		return true;
	},
	
	/**
	 * Returns array of checked values
	 * @return {array}
	 */
	getValue: function() {		
		if ( this.valueElem.is(":checked") ) {
			return this.valueElem.val();
		}
		
		return "";
	},
	
	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * @method
	 * @return {boolean}
	 */
	isEmpty: function() {
		return !this.valueElem.is(":checked");
	},
	
	isEmptyForSearch: function() {
		return this.isEmpty();
	},
	
	/**
	 * Returns array of jQuery object for inline submit
	 * @return {array}
	 */
	getForSubmit: function() {
		var cloneArr = [],
			$cb, $type;
		
		if ( this.valueElem.is(":checked") ) {
			$cb = $('<input>')
				.attr('type', 'hidden')	
				.attr('id', this.valueElem.attr('id'))
				.attr('name', this.valueElem.attr('name'))
				.val( this.valueElem.val() );

			cloneArr.push( $cb.get(0) );			
		}
		
		$type = $('<input>')
				.attr('type', this.typeElem.attr('type'))	
				.attr('id', this.typeElem.attr('id'))
				.attr('name', this.typeElem.attr('name'))
				.val( this.typeElem.val() );
		
		cloneArr.push( $type.get(0) );
		return cloneArr;
	},
	
	/**
	 * Set the field in 'readonly' mode 
	 * hiding the real checkboxes and adding 
	 * their disabled clones to the page
	 */
	makeReadonly: function() {
		if ( !this.appearOnPage() ) {
			return false;
		}
	
		this.readOnly = true;
		
		var $readonlyElem = this.valueElem.clone(),
			elId = this.valueElem.attr("id");
			
		$readonlyElem.prop({
			id: "readonly_" + elId,
			name: "readonly_" + elId,
			disabled: true
		});	
		
		this.valueElem
			.before( $readonlyElem )
			.hide();
		
		return true;
	},
	
	/**
	 * Set the field in 'read/write' mode 
	 * showing the real checkboxes and 
	 * removing their disabled substitutes
	 */	
	makeReadWrite: function() {
		if ( !this.appearOnPage() || !this.isReadonly() ) {
			return false;
		}
		
		this.readOnly = false;
		
		$('#readonly_' + this.valueElem.attr("id")).remove();
		this.valueElem.show();

		return true;
	},
});
/**
 * Multiple select control class
 * @requires Runner.controls.LookupWizard
 * @class Runner.controls.CheckBoxLookup
 * @extends Runner.controls.LookupWizard
 */
Runner.controls.CheckBoxLookup = Runner.extend( Runner.controls.LookupWizard, {
	/**
	 * type hidd element jQuery obj
	 * @type {object}
	 */
	typeElem: null,
	
	/**
	 * Number of checkboxes
	 * @type {Number}
	 */
	checkBoxCount: 0,
	
	/**
	 * Array of checkbox jQuery elements
	 * @type {array}
	 */
	checkBoxesArr: null,
	
	/**
	 * Override parent contructor
	 * @param {object} cfg
	 */ 
	constructor: function( cfg ) {
		// add checkboxes elements
		this.checkBoxesArr = [];

		Runner.controls.CheckBoxLookup.superclass.constructor.call(this, cfg);
	},

	/**
	 * Set the value element and the corresponding properties
	 */	
	assignValueElem: function() {
		var $checkBox, typeElemId,
			control = this;
		
		Runner.controls.CheckBoxLookup.superclass.assignValueElem.call(this);
	
		// assign the control's type hidden element		
		typeElemId = "type_" + this.goodFieldName + "_" + this.rowId;
		this.typeElem = $("#" + typeElemId);

		// checkbox is used as lookup		
		$("[id^='" + this.valContId + "_']").each( function() {
			control.checkBoxesArr.push( $(this) );
		});
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */	
	assignsElemsForEvent: function() {
		for (var i = 0; i < this.checkBoxesArr.length; i++) { 
			this.elemsForEvent.push( this.checkBoxesArr[i].get(0) );
		}
	},
	
	/**
	 * Sets array of values to checkboxes
	 * @method
	 * @param {mixed} value 			The contorl's value or an array of values
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @return {Boolean} true if success otherwise false
	 */	
	setValue: function( value, triggerEvent, updContext ) {
		var readOnly = this.isReadonly(),
			valsArr = [], 
			checkCount = 0,
			i, j;
		
		if ( readOnly ) {
			this.makeReadWrite();
		}
		
		if ( value === true && this.checkBoxesArr.length == 1 ) {
			// set checked
			this.checkBoxesArr[0].get(0).checked = true;
		} else if (!value) {
			for (i = 0; i < this.checkBoxesArr.length; i++) {
				// set all unchecked
				this.checkBoxesArr[i].get(0).checked = false;
			}
		} else {
			valsArr = typeof value === 'string' ? [value] : value;

			//loop for all checkboxes
			for (i = 0; i < this.checkBoxesArr.length; i++) {
				// set unchecked
				this.checkBoxesArr[i].get(0).checked = false;
				// loop for all vals
				for (j = 0; j < valsArr.length; j++) {
					// if check box val same as val in arr to check
					if ( this.checkBoxesArr[i].val() == valsArr[j] ) {
						this.checkBoxesArr[i].get(0).checked = true;
						checkCount++;
						break;
					}
				}
			}
		}
		
		if ( triggerEvent === true ) {
			//this.fireEvent("click", updContext);
			this.fireEvent("change", updContext);
		}
		
		if ( readOnly ) {
			this.makeReadonly();
		}		
		
		// check number of checked boxes
		return checkCount == valsArr.length && checkCount <= this.checkBoxesArr.length;
	},
	
	/**
	 * Returns array of checked values
	 * @return {array}
	 */
	getValue: function() {
		var checkedArr = this.getCheckedBoxes(),
			valsArr = [];
		// get value from each checkbox
		for (var i = 0; i < checkedArr.length; i++) {
			valsArr.push( checkedArr[i].val() );
		}		
		return valsArr;
	},
	
	/**
	 * Return an escaped string value
	 * @param {Array} valueArray
	 * @return {string}
	 * @intellisense
	 */
	getStringValue: function( valueArray ) {
		var valsArr = valueArray || this.getValue(), i;
		
		for (i = 0; i < valsArr.length; i++) {
			if ( valsArr[i].indexOf(',') !== -1 || valsArr[i].indexOf('"') !== -1 ) {
				valsArr[i] = '"' + valsArr[i].replace(/"/g, '""') + '"';
			}
		}
		
		return valsArr.join(",");
	},

	/**
	* Get the default control's value
	* @return {String | Number}
	*/
	getDefaultControlValue: function() {
		return this.getStringValue( this.defaultValue );
	},
	
	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * @method
	 * @return {boolean}
	 */
	isEmpty: function() {
		return !this.getValue().length;
	},
	
	/**
	 * Sets disable attr true
	 * @method
	 */
	setDisabled: function() {
		for (var i = 0; i < this.checkBoxesArr.length; i++) {
			this.checkBoxesArr[i].prop('disabled', true);
		}
		this.hideEditCurrentButton();
		return true;
	},
	
	/**
	 * Sets disaqble attr false
	 * @method
	 */
	setEnabled: function() {
		for (var i = 0; i < this.checkBoxesArr.length; i++) {
			this.checkBoxesArr[i].prop('disabled', false);
		}
		this.updataEditCurrentButton();
		return true;
	},
	
	/**
	 * Returns array of cheked checkBoxes
	 * @return {array}
	 */
	getCheckedBoxes: function() {
		var chekedArr = [], i, checkBox;
		
		// get value from each checkbox
		for (i = 0; i < this.checkBoxesArr.length; i++) {			
			checkBox = this.checkBoxesArr[i];
			// the second condition is for the elements that were moved in DOM
			if ( checkBox.is(":checked") || $("#" + checkBox.attr("id") ).is(":checked") ) {
				chekedArr.push( this.checkBoxesArr[i] );
			}
		}
		
		return chekedArr;
	},
	
	/**
	 * Returns array of jQuery object for inline submit
	 * @return {array}
	 */
	getForSubmit: function() {
		var checkedArr = this.getCheckedBoxes(), 
			cloneArr = [],
			realCb, cbClone, i;
		
		// get clone of each checkbox
		for (i = 0; i < checkedArr.length; i++) {
			realCb = checkedArr[i];	
			cbClone = $('<input>')
				.attr('type', 'hidden')	
				.attr('id', realCb.attr('id'))
				.attr('name', realCb.attr('name'))
				.val( realCb.val() );

			cloneArr.push( cbClone.get(0) );	
		}
		
		cloneArr.push( this.typeElem );
		return cloneArr;
	},

	/**
	 * Deletes all checkBoxes from ctrl
	 * @method
	 */
	removeCheckBoxes: function() {
		var divWrapper = this.spanContElem.find('div[data-lookup-options]');	
		divWrapper.children().remove();
		
		this.checkBoxesArr = [];
		this.checkBoxCount = 0;
	},

	/**
	 * Add checkboxes from array.
	 * Array must have such structure:
	 * array[0] = value, array[1] = text,
	 * array[2] = value, array[3] = text,
	 * 2*i - indexes of values; 2*i+1 - indexes of text. I starts from 0 
	 * @param {array} checkBoxesArr
	 */
	addCheckBoxArr: function( checkBoxesArr ) {
		if ( !checkBoxesArr ) {
			return;
		}

		for (var i = 0; i < checkBoxesArr.length; i++) { 
			this.addCheckBox( checkBoxesArr[i]["displayValue"], checkBoxesArr[i]["linkValue"], checkBoxesArr[i]["keys"] );
		}
		//reinit Custom Events
		this.purgeListeners();
		this.initCustomEvents();
	},
	
	/**
	 * Adds checkbox to ctrl
	 * @param {string} text
	 * @param {string} val
	 */
	addCheckBox: function( text, value, keys ) {
		var $checkBox, $boxLabel,
			delim = '&nbsp;', 
			ind = this.checkBoxesArr.length,
			$divWrapper, $spanWrapper;

		$divWrapper = $('div[data-lookup-options]', this.spanContElem);
	
		$spanWrapper = $('<span class="checkbox"><label></label></span>').appendTo( $divWrapper );

		$divWrapper.addClass( this.isHorizontalLookup ? 'rnr-horizontal-lookup' : 'rnr-vertical-lookup' );
		
		$checkBox = $('<input>')
			.attr('id', this.valContId + "_" + ind)
			.addClass('rnr-checkbox')
			.attr('type', 'checkbox')
			.attr('name', this.valContId + '[]')
			.val(value);
		
		if ( keys && this.isEditCurrentItemAvailable() ) {
			$checkBox.data('keys', keys);
		}

		$boxLabel = $('<span>')
			.attr('id', 'data_' + this.valContId + '_' + ind)
			.addClass('rnr-checkbox-label')
			.html(text);
			
		$('label', $spanWrapper).append( $checkBox ).append( delim ).append( $boxLabel );
		
		this.checkBoxesArr.push( $checkBox );
		this.checkBoxCount++;
	},
	
	/**
	 * First loading, without ajax. Should be called directly
	 * @param {string} vals 		unparsed values for checkboxes
	 * @param {array} selectValue 	unparsed values of selected checkboxes
	 */
	preload: function( vals, selectValue ) {
		// clear all old checkboxes
		this.removeCheckBoxes();
		// load checkboxes
		this.addCheckBoxArr( vals );
		// don't need to use ajax reload call
		this.setValue( selectValue, false );
		this.updataEditCurrentButton();
	},
	
	
	/**
	 * @param {array} data
	 * @param {array} selectedValues
	 * @param {object} updContext
	 */ 
	updateLookupData: function( data, selectedValues, updContext ) {
		// clear all checkboxes
		this.removeCheckBoxes();
		// add checkboxes to ctrl
		this.addCheckBoxArr( data );
		
		// restore the previously marked checkboxes
		this.setValue( selectedValues, false );		
	},
	
	/**
	 * Set the field in 'readonly' mode 
	 * hiding the real checkboxes and adding 
	 * their disabled clones to the page
	 */
	makeReadonly: function() {
		if ( !this.appearOnPage() ) {
			return false;
		}
		
		this.readOnly = true;
	
		for (var i = 0; i < this.checkBoxesArr.length; i++) {
			var elemChk = this.checkBoxesArr[i], 
				readonlyElem = elemChk.clone(),
				elId = elemChk.get(0).id;
				
			readonlyElem.prop({
				id: "readonly_" + elId,
				name: "readonly_" + elId,
				disabled: true
			});	
			elemChk.before( readonlyElem ).hide();
		}
		this.hideEditCurrentButton();
		return true;
	},
	
	/**
	 * Set the field in 'read/write' mode 
	 * showing the real checkboxes and 
	 * removing their disabled substitutes
	 */	
	makeReadWrite: function() {
		if ( !this.appearOnPage() || !this.isReadonly() ) {
			return false;
		}
		
		this.readOnly = false;
	
		for (var i = 0; i < this.checkBoxesArr.length; i++) {
			var elemChk = this.checkBoxesArr[i],
				elId = elemChk.get(0).id;
				
			$('#readonly_' + elId).remove();
			elemChk.show();
		}
		this.updataEditCurrentButton();
		return true;
	},
	
	/**
	 * Get the displayed field's value
	 * @return {string}
	 */
	getDisplayValue: function() {
		var displayValues = [];
		
		for (var i = 0; i < this.checkBoxesArr.length; i++) {
			if ( this.checkBoxesArr[i].prop("checked") ) {
				displayValues.push( $("#data_" + this.checkBoxesArr[i].attr("id")).text() );
			}
		}
		return displayValues.join(", ");		
	},
	
	/**
	 * Checks whether the control exists or not
	 * @method
	 * @return {boolean}
	 * @intellisense
	 */	
	appearOnPage: function() {
		return this.checkBoxesArr.length;
	},
	
	/**
	 * Sets focus to the element.
	 * Since focus cannot be set properly on the contol's value element 
	 * It sets focus on the first checkbox in the list 
	 * @param {Boolean} triggerEvent
	 * @method
	 * @intellisense
	 */
	setFocus: function( triggerEvent ) {
		var firstValueElem = this.checkBoxesArr[0];
		
		this.isSetFocus = false;
		
		if ( !this.appearOnPage() || !this.showStatus || firstValueElem.is(':disabled') ) {
			return;
		}
	
		firstValueElem.focus();
		this.isSetFocus = true;
		
		if (triggerEvent === true) {
			this.fireEvent("focus");
		}	
	},

	/**
	 * Process the new added 'on the fly' item 
	 * @param {string} text
	 * @param {string | number} value
	 */	
	processNewFlyAdded: function( text, value, keys ) {
		var values = this.getValue();
		
		values.push( value );
		this.addCheckBox( text, value, keys );
		this.setValue( values, true );

		//reinit Custom Events
		this.purgeListeners();
		this.initCustomEvents();
	},
	
	getCurrentItemKeys: function() {
		var options = this.getCheckedBoxes();
		if ( options.length === 1 && this.isEditCurrentItemAvailable() ) {
			return options[0].data('keys') || [];
		}
		return [];
	},

	/**
	 * Add a new option to the select element
	 * @param {string} displayedValue
	 * @param {string} val
	 * @param {array} keys
	 */
	updateOption: function( displayedValue, val, keys ) {
		var options = this.getCheckedBoxes()
		if ( options.length === 1 ) {
			options[0]
				.attr('value', val)
				.data('keys', keys);
			
			// update label
			$('#data_' + options[0].attr('id') ).html( displayedValue );
		}
	},
	
	isModifyVisible: function() {
		return !this.isEmpty() && this.getCheckedBoxes().length === 1 
			&& !!this.getCurrentItemKeys().length;
	},
});
/**
 * Base abstract class for lookups with textFields
 * Contains text box editor as display field and hidden field for submit values
 * @class
 * @requires Runner.controls.LookupWizard
 */
Runner.controls.TextFieldLookup = Runner.extend( Runner.controls.LookupWizard, {
	/**
	 * jQuery element that display value
	 * Value element in EditBoxLookup is hidden, and used for submit data
	 * @type {jQuery object}
	 */
	displayElem: null,

	/**
	 * An array containig multiselect control's
	 * default options data (value and text)
	 * @type {array}
	 */
	defaulOptionsData: null,

	/**
	 * Override parent contructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.TextFieldLookup.superclass.constructor.call( this, cfg );
	},

	/**
	 * Set the value element
	 */
	assignValueElem: function() {
		Runner.controls.TextFieldLookup.superclass.assignValueElem.call(this);

		//get the jQuery object representing the displayed element
		this.displayElem = $("#display_" + this.valContId);

		this.initMultiselectionWithTabs();
	},

	/**
	 * Initialize a multiselect control if multiple selection is allowed
	 */
	initMultiselectionWithTabs: function() {
		if ( !this.multiSelectionAllowed ) {
			return;
		}

		var ctrl = this,
			chosenCtrl;

		var placeholderFieldProperty = this.valueElem.attr("placeholder");
		
		this.valueElem.chosen({
			disable_search: false,
			placeholder_text_multiple: placeholderFieldProperty?placeholderFieldProperty:" ",
			width: "100%",
			rtl: Runner.isDirRTL(),
		});

		chosenCtrl = this.valueElem.data("chosen");	

		this.valueElem
			.on("chosen:showing_dropdown", function() {
				if ( chosenCtrl ) {
					chosenCtrl.results_showing = true;
					chosenCtrl.results_hide();
				}
			})
			.on("change", function( e, data ) {
				if ( data && data.deselected ) {
					ctrl.removeMultiselectOption( data.deselected );
				}
			});
	},


	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignListOfEvents: function() {
		this.addEvent( ["blur"] );
		Runner.controls.TextFieldLookup.superclass.assignListOfEvents.call(this);
	},

	/**
	 * Set the control's default value
	 */
	setDefaultValue: function() {
		var ctrl = this;

		if ( !this.multiSelectionAllowed ) {
			this.defaultValue = [ this.getValue(), this.getDisplayValue() ];
			return;
		}

		this.defaultValue = this.getValue( true );

		this.defaulOptionsData = [];
		$('option', this.valueElem).each( function( ind, option ) {
			var $option = $( option );

			ctrl.defaulOptionsData.push( {
				linkVal: $option.val(),
				dispVal: $option.text()
			});
		});
	},

	/**
	 * Set value to display element
	 * @param {mixed} val
	 * @return {bool} true if success otherwise false
	 */
	setDisplayValue: function( val ) {
		if ( this.displayElem ) {
			return this.displayElem.val( val );
		}
		return false;
	},

	/**
	 * Get value from value element.
	 * Should be overriden for sophisticated controls
	 * @method
	 */
	getDisplayValue: function() {
		if ( this.displayElem ) {
			return this.displayElem.val();
		}
		return false;
	},

	/**
	 * Get value from input element.
	 * @return {string}
	 */
	getInputValue: function() {
		if ( this.displayElem ) {
			return this.displayElem.val();
		}
		return "";
	},

	/**
	 * Get display field value by set value (search by link field)
	 * @param {string} value
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 */
	setValue: function( value, triggerEvent, updContext ) {
		if ( this.freeInput ) {
			// link == disp case
			if ( updContext && updContext.setForSpreadsheet && this.multiSelectionAllowed ) {
				this.clearTextLookup();

				// split by comma ignoring commas in quotes
				var values = value.split(/,(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/);
				values.forEach( function( _value ) {
					this.setLookupValue( _value, triggerEvent, _value, updContext );
				}, this );
			} else {
				this.setLookupValue( value, triggerEvent, value, updContext );
			}
			
			return;
		}
		if( value == null || value === '' ) {
			this.setLookupValue( '', triggerEvent, '', updContext );
			return;
		}
		var ctrl = this,
			multiselection = this.multiSelectionAllowed && Runner.isArray( value ),
			ajaxParams = {
				searchByLinkField: true,
				pageType: this.pageType,
				page: this.pageObj.pageName,
				table: this.shortTableName,
				searchField: this.fieldName,
				searchFor: multiselection ? value.join(",") : value,
				isExistParent: !!this.parentCtrls.length ? 1 : 0,
				parentCtrlsData: JSON.stringify( this.getParentControlsData() ),
				multiselection: multiselection,
				data: JSON.stringify( this.getControlsData() ), // values from controls
				masterTable: this.pageObj.masterTable
			};

		Runner.runnerAJAX( Runner.getPageUrl("lookupsuggest"), ajaxParams, function( respObj ) {
			if ( !respObj.success ) {
				return false;
			}

			if ( updContext && updContext.setForSpreadsheet ) {
				ctrl.clearTextLookup();
			}	
			
			respObj.data.forEach( function( suggest ) {
				if ( multiselection && $.inArray( suggest["link"], value ) !== -1 || suggest["link"] == value ) {
					ctrl.setLookupValue( suggest["link"], triggerEvent, suggest["display"], updContext );
				}
			});
		});
	},

	/**
	 * Value in editBoxLookup is pair of display and hidden values
	 * @method
	 * @param {mixed} linkVal
	 * @param {mixed} dispVal
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 * @return {boolean} true if success otherwise false
	 */
	setLookupValue: function( linkVal, triggerEvent, dispVal, updContext ) {
		var readOnly = this.isReadonly(),
			changed, isSetDispVal;

		if ( readOnly ) {
			this.makeReadWrite();
		}

		if ( this.multiSelectionAllowed ) {
			this.addMultiselectOption( linkVal, dispVal, !triggerEvent );
			readOnly && this.makeReadonly();
			return;
		}

		changed = this.valueElem.val() !== linkVal;

		if ( this.displayElem ) {
			changed = changed || this.displayElem.val() !== dispVal;
		}

		if ( changed ) {
			this.valueElem.val( linkVal );
			isSetDispVal = this.setDisplayValue( dispVal );
		}

		if ( isSetDispVal === false ) {
			changed = false;
		}

		if ( readOnly ) {
			this.makeReadonly();
		}

		if ( changed && triggerEvent === true ) {
			this.fireEvent("change", updContext, true);
		}

		return changed;
	},

	/**
	 * @param {boolean} returnArray
	 * @return {mixed}
	 */
	getValue: function( returnArray ) {
		if ( !this.multiSelectionAllowed ) {
			return this.valueElem.val();
		}

		var optionValues = [];

		$('option:selected', this.valueElem).each( function( ind, option ) {
			optionValues.push( option.value );
		})

		if ( returnArray || optionValues.length > 1 ) {
			return optionValues;
		}

		if ( optionValues.length === 1 ) {
			return optionValues[ 0 ];
		}

		return "";
	},

	/**
	 * Set the default value to the control
	 * @param {object} updContext
	 */
	reset: function( updContext ) {
		var ctrl = this;

		this.clearInvalid();

		if ( !this.multiSelectionAllowed ) {
			this.setLookupValue( this.defaultValue[0], true, this.defaultValue[1], updContext );
			return;
		}

		this.removeMultiselectOptions( !!this.defaulOptionsData.length );

		$.each( this.defaulOptionsData, function( idx, optionData ) {
			ctrl.addMultiselectOption( optionData[ 'linkVal' ], optionData[ 'dispVal' ], true );
		});

		this.fireEvent("change", updContext );
	},

	/**
	 * Sets disable attr true
	 * @method
	 */
	setDisabled: function() {
		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).prop('disabled', true);
		}

		if ( this.displayElem ) {
			this.displayElem.prop('disabled', true);
		}

		this.hideEditCurrentButton();

		return this.displayElem || this.multiSelectionAllowed;
	},

	/**
	 * Sets disable attr false
	 * @method
	 */
	setEnabled: function() {
		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).prop('disabled', false);
		}

		if ( this.displayElem ) {
			this.displayElem.prop('disabled', false);
		}

		this.updataEditCurrentButton();

		return this.displayElem || this.multiSelectionAllowed;
	},

	/**
	 * Sets focus to the element
	 * @method
	 * @param {Boolean} triggerEvent
	 */
	setFocus: function( triggerEvent ) {
		this.isSetFocus = false;

		if ( !this.displayElem || !this.displayElem.length || this.displayElem.prop("disabled") ) {
			return;
		}

		this.displayElem.get(0).focus();
		this.isSetFocus = true;

		if ( triggerEvent === true ) {
			this.fireEvent("focus");
		}
	},

	/**
	 * Removes css class to value element
	 * @param {string} className
	 */
	removeCSS: function( className ) {
		this.displayElem.removeClass( className );
	},

	/**
	 * Removes css class to value element
	 * @param {string} className
	 */
	removeClass: function( className ) {
		var $displayElem = this.getDispElem();
		$displayElem.removeClass( className );
	},

	/**
	 * Adds css class to value element
	 * @param {string} className
	 */
	addCSS: function( className ) {
		this.displayElem.addClass( className );
	},

	/**
	 * Adds css class to value element
	 * @param {string} className
	 */
	addClass: function( className ) {
		var $displayElem = this.getDispElem();
		$displayElem.addClass( className );
	},

	/**
	 * Returns specified attribute from value element
	 * @param {string} attrName
	 */
	getAttr: function( attrName ) {
		return this.displayElem.attr( attrName );
	},

	/**
	 * Return element that used as display.
	 * Usefull for suggest div positioning
	 * @return {object}
	 */
	getDispElem: function() {
		if ( this.multiSelectionAllowed && this.valueElem.data('chosen') ) {
			return this.valueElem.data('chosen').container;
		}

		return this.displayElem;
	},

	/**
	 * Checks if control value is empty. Used for isRequired validation
	 * @method
	 * @return {boolean}
	 */
	isEmpty: function() {
		if ( !this.multiSelectionAllowed ) {
			return ( this.getDisplayValue() || "" ).toString() == "";
		}

		return !( this.getValue( true )|| "" ).length;
	},

	/**
	 * First loading, without ajax. Should be called directly
	 * @param {array} vals					txt unparsed values for options
	 * @param {string | array} selectValue 	unparsed values of selected options
	 */
	preload: function( vals, selectValue ) {
		for ( var i = 0; i < vals.length; i++ ) {
			if ( this.multiSelectionAllowed && selectValue.some( s => s == vals[i]["linkValue"] ) ) {
				this.setLookupValue( vals[i]["linkValue"], false, vals[i]["displayValue"] );
				
				if ( vals[i]["keys"] && this.isEditCurrentItemAvailable() ) {
					this.getMultiselectOption( vals[i]["linkValue"] )
						.data('keys', vals[i]["keys"]);
				}
				
			} else if ( !this.multiSelectionAllowed && vals[i]["linkValue"] == selectValue ) {
				this.setLookupValue( vals[i]["linkValue"], false, vals[i]["displayValue"] );
				
				if ( vals[i]["keys"] && this.isEditCurrentItemAvailable() ) {
					this.displayElem
						.data('keys', vals[i]["keys"]);
				}
				break;
			}
		}
		this.updataEditCurrentButton();
	},

	/**
	 * @param {object} updContext
	 */
	processAutofillDataOnReload: function( updContext ) {
		if ( updContext && updContext.values && updContext.values[ this.fieldName ] ) {
			this.removeMultiselectOptions();

			// if value changed, so fire change event
			var triggerEvent = this.getValue() != updContext.values[ this.fieldName ][0];

			if ( updContext.values[ this.fieldName ].length == 2 ) {
				this.setLookupValue( updContext.values[ this.fieldName ][0], triggerEvent, updContext.values[ this.fieldName ][1], updContext );
			} else {
				this.setValue( updContext.values[ this.fieldName ], triggerEvent, updContext );
			}
		}
	},

	/**
	 * @param {array} data
	 * @param {array} selectedValues
	 * @param {object} updContext
	 */
	updateLookupData: function( data, selectedValues, updContext ) {
		this.clearTextLookup();

		if ( data.length == 1 ) {
			this.setLookupValue( data[0]["linkValue"], selectedValues != data[0]["linkValue"], data[0]["displayValue"], updContext );
			this.removeClass("highlight");
			
			if ( this.multiSelectionAllowed ) {
				this.valueElem.find( 'option:selected' ).eq(0)
					.data('keys', data[0]["keys"]);
			} else {
				this.displayElem.data('keys', data[0]["keys"]);
			}
		} else {
			if ( this.multiSelectionAllowed && Array.isArray( selectedValues ) && !selectedValues.length ) {
				this.clearTextLookup();
			} else {
				this.setValue( selectedValues );
		
				if ( this.isEditCurrentItemAvailable() ) {
					if ( this.multiSelectionAllowed ) {
						selectedValues.forEach( function( sv ) {
							var keys = data.find( function( d ) {
								return d.displayValue == sv;
							})?.keys;
							if ( keys ) {
								this.getMultiselectOption( sv ).data('keys', keys);
							}
						}, this);
							
					} else {
						this.displayElem.data('keys', data[0]["keys"]);
					}
				}
			}
		}
		this.updataEditCurrentButton();
	},

	/**
	 * Make the control 'readonly'
	 */
	makeReadonly: function() {
		if ( !this.appearOnPage() ) {
			return false;
		}
		
		this.readOnly = true;

		this.addNew.hide();

		this.displayElem.prop({
			"disabled": true,
			"readonly": true
		});

		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).prop('disabled', true);
		}

		this.hideEditCurrentButton();
	},

	/**
	 * Make the control writable
	 */
	makeReadWrite: function() {
		if ( !this.appearOnPage() ) {
			return false;
		}
		
		this.readOnly = false;

		this.addNew.show();

		this.displayElem.prop({
			"disabled": false,
			"readonly": false
		});

		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).prop('disabled', false);
		}

		this.updataEditCurrentButton();
	},

	/**
	 * Get the default control's value
	 * @return {String | Number}
	 */
	getDefaultControlValue: function() {
		if ( !this.multiSelectionAllowed ) {
			return this.defaultValue[0];
		}

		if ( this.defaultValue[0] === '' ) {
			return '';
		}

		return this.getStringValue( this.defaultValue );
	},

	/**
	 * Get a particular multiselect control option
	 * @param {string} linkVal
	 * @return {jQuery object}
	 */
	getMultiselectOption: function( linkVal ) {
		return $('option', this.valueElem).filter( function() {
				return this.value == linkVal;
			});
	},

	/**
	 * Add an option for the multiselect control
	 * @param {string} linkVal
	 * @param {string} dispVal
	 * @param {boolean} notFireChangeEvent
	 */
	addMultiselectOption: function( linkVal, dispVal, notFireChangeEvent ) {
		if ( !this.multiSelectionAllowed || this.getMultiselectOption( linkVal ).length ) {
			return;
		}

		$('<option selected="selected"></option>')
			.attr('value', linkVal)
			.text( dispVal )
			.appendTo( this.valueElem );

		this.valueElem.trigger("chosen:updated");
		if ( !notFireChangeEvent ) {
			this.fireEvent("change");
		}
	},

	/**
	 * Set an empty value to the control
	 */
	clear: function() {
		this.clearInvalid();
		this.clearTextLookup();
	},

	/**
	 * Set an empty value to the text lookup control
	 */
	clearTextLookup: function() {
		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).remove();
			this.valueElem.trigger("chosen:updated");
			return;
		}
		
		if ( this.displayElem ) {
			this.displayElem.data('keys', null);
		}

		this.setLookupValue( '', false, '' );
	},

	/**
	 * Clear selected options for the multiselect control
	 * @param {boolean} notFireChangeEvent
	 */
	removeMultiselectOptions: function( notFireChangeEvent ) {
		if ( this.multiSelectionAllowed ) {
			$('option', this.valueElem).remove();
			if ( !notFireChangeEvent ) {
				this.valueElem.trigger("chosen:updated");
				this.fireEvent("change");
			}
		}
	},

	/**
	 * Remove a particular multiselect control option
	 * @param {string} linkVal
	 */
	removeMultiselectOption: function( linkVal ) {
		if ( !this.multiSelectionAllowed ) {
			return;
		}

		this.getMultiselectOption( linkVal )
			.remove();

		this.valueElem.trigger("chosen:updated");
		this.fireEvent("change");
	},

	/**
	 * The method is invoked when the "editing" event is fired
	 * @param {object} nullEvent		 Equals to null if function is invoked
	 * 								with the fireEvent method
	 * @param {object} updateContext
	 */
	"editing": function( nullEvent, updateContext ) {
		this.reloadDependentCtrls( updateContext, nullEvent );

		if ( this.freeInput ) {
			this.processResolvedEvent( updateContext );
		}
	},

	/**
	 * The method is invoked when the "edited" event is fired
	 * @param {object} nullEvent		 Equals to null if function is invoked
	 * 								with the fireEvent method
	 * @param {object} updateContext
	 */
	"edited": function( nullEvent, updateContext ) {
		var valArr = this.getPreparedValidationArray();

		if ( valArr.length ) {
			this.validate( valArr );
		}

		// clear invalid state in dependent controls in anyway
		this.clearInvalidOnDependences();
		this.reloadDependentCtrls( updateContext, nullEvent );
		this.processResolvedEvent( updateContext );
	},
	
	isModifyVisible: function() {
		if ( this.multiSelectionAllowed ) {
			return $('option', this.valueElem).length === 1 && !!this.getCurrentItemKeys().length;
		}
		
		return !this.isEmpty() && !!this.getCurrentItemKeys().length;
	},

	getCurrentItemKeys: function() {
		if ( this.multiSelectionAllowed ) {
			return this.valueElem.find( 'option:selected' ).eq(0).data('keys') || [];
		} 
		return this.displayElem.data('keys') || [];
	},
	
	/**
	 * Add a new option to the select element
	 * @param {string} displayedValue
	 * @param {string} val
	 * @param {array} keys
	 */
	updateOption: function( displayedValue, val, keys ) {
		if ( this.multiSelectionAllowed ) {
			this.valueElem.find( 'option:selected' ).eq(0)
				.html( displayedValue )
				.attr('value', val)
				.data('keys', keys);
				
			this.valueElem.trigger("chosen:updated");
		} else {
			this.displayElem.data('keys', keys);
			this.setLookupValue( val, true, displayedValue );
		}
	},

});
/**
 * Edit box with ajax popup class with suggest div handling
 * @requires Runner.controls.TextFieldLookup
 * @class Runner.controls.EditBoxLookup
 */
Runner.controls.EditBoxLookup = Runner.extend( Runner.controls.TextFieldLookup, {
	/**
	 * Focus indicator
	 * @type Boolean
	 */
	focusState: false,

	/**
	 * suggestDiv cursor ind
	 * @type
	 */
	cursor: -1,

	/**
	 * Array of suggest vals
	 * @type {array}
	 */
	suggestValues: null,
	
	
	suggestKeys: null,

	/**
	 * Array of lookup vals
	 * @type {array}
	 */
	lookupValues: null,

	/**
	 * Lookup div id
	 * @type String
	 */
	lookupDivId: "",

	/**
	 * Lookup div jQuery object
	 * @type {object}
	 */
	lookupDiv: null,

	/**
	 * Lookup div jQuery object
	 * @type {object}
	 */
	lookupIframe: null,

	/**
	 * Set suggest value from ajaxsuggest or not
	 * @type {boolean}
	 */
	isSetSuggestVal: false,

	/**
	 * A flag indicating if 'free input' is allowed.
	 * It could be equal to true if the lookup link and
	 * displayed fields are the same only
	 * @type {boolean}
	 */
	freeInput: false,

	/**
	 * @type {boolean}
	 */
	isError: false,

	/**
	 * Reference to timeout set up during the last keyup event
	 */
	submitTimout: null,

	/**
	 * An indicator showing if the search control is in the simple search mode
	 * @type {boolean}
	 */
	simpleSearchControlMode: false,

	/**
	 *
	 */
	ajaxTask: null,

	/**
	 * the DelayedTask to run onchange event handler in case of freeform input
	 */
	onChangeTask: null,


	/**
	 * Override parent contructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		this.suggestValues = [];
		this.lookupValues = [];
		this.suggestKeys = [];

		Runner.controls.EditBoxLookup.superclass.constructor.call( this, cfg );

		this.freeInput = this.freeInput || this.mode == Runner.controls.constants.MODE_SEARCH;
		this.lookupDivId = 'lookupSuggest_' + this.valContId;

		this.delegateSuggestsContainerDestroying();
		this.ajaxTask = new Runner.util.DelayedTask( this.lookupAjax, this );
		this.onChangeTask = new Runner.util.DelayedTask( function() { this.fireEvent("change"); }, this );
	},

	/**
	 * Set the value element
	 */
	assignValueElem: function() {
		Runner.controls.EditBoxLookup.superclass.assignValueElem.call( this );
		this.simpleSearchControlMode = !this.multiSelectionAllowed && this.displayElem.data("simpleSearchMode");
	},

	/**
	 * Initialize a multiselect control if multiple selection is allowed
	 */
	initMultiselectionWithTabs: function() {
		if ( !this.multiSelectionAllowed || !this.valueElem.length ) {
			return;
		}

		Runner.controls.EditBoxLookup.superclass.initMultiselectionWithTabs.call( this );

		var chosenCtrl = this.valueElem.data('chosen');
		if( !chosenCtrl ) {
			return;
		}


		this.displayElem = chosenCtrl.search_field
			.off('keyup.chosen')
			.off('keydown.chosen')
			.width( 100	);
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		this.elemsForEvent = [ this.displayElem.get(0) ];
	},

	/**
	 * Set the control's events list
	 */
	assignListOfEvents: function() {
		this.addEvent( ["focus", "keydown", "input" ] );
		Runner.controls.EditBoxLookup.superclass.assignListOfEvents.call( this );
	},

	/**
	 * Set the events that will fire "edited", "editing" events
	 */
	addEditEditingListeners: function() {
		var ctrl = this;
		this.on("change", function( e, updateContext, lookupValueSet ) {
			updateContext = updateContext && typeof updateContext === "object" ? updateContext : {};
			if ( e === null && ( lookupValueSet || this.multiSelectionAllowed ) ) { //#7845
				this.fireEvent("edited", updateContext);
			}
		});

		this.on("input", function(e) {
			var dispValue = ctrl.getInputValue();
			if ( this.freeInput && this.multiSelectionAllowed &&  !this.lookupDiv && dispValue.trim() !== "" ) {
				this.setSuggestValueToControl( dispValue, dispValue, null );
				this.fireEvent("editing");
			}
			
			if ( this.freeInput && !this.multiSelectionAllowed ) {
				this.fireEvent("editing");
			}

		
			var index = this.suggestValues.findIndex( function( suggest ) {
				return suggest === dispValue;
			});

			if ( index == -1 ) {
				if ( !this.multiSelectionAllowed ) {
					this.displayElem.data('keys', null);
				} 
			} else {
				if ( !this.multiSelectionAllowed ) {
					this.displayElem.data('keys', this.suggestKeys[ index ]);
				} else {
					this.valueElem.find( 'option:selected' ).eq(0)
						.data('keys', this.suggestKeys[ index ]);
				}
			}
			
			this.updataEditCurrentButton();	
		}, { buffer: 1000 } );
	},

	/**
	 * Destroy sugessts container on 'outer' click
	 */
	delegateSuggestsContainerDestroying: function() {
		var ctrl = this,
			destroySuggestDivDelegate = function(e) {
				$target = $( e.target );
				if ( !$target.hasClass( "suggest_link_over" ) && !$target.hasClass( "suggest_link" ) ) {
//					clearTimeout( ctrl.submitTimeout );
					ctrl.destroyDiv();
				}
			};

		$('body, a').on('click', destroySuggestDivDelegate);
		$('body').on('click', 'input', destroySuggestDivDelegate);
	},

	/**
	 * Update the control 'isError' state.
	 * Add/remove the red frame basing on the control's 'isError' state
	 * @param {boolean} errorHappend
	 */
	setFrame: function( errorHappend ) {
		this.isError = errorHappend && this.getDisplayValue() !== "" && !this.freeInput;
		$('[data-signal-error-for=' + this.goodFieldName + this.rowId + ']')
			.toggleClass( 'has-error', this.isError );
	},

	/**
	 * Set the default value to the control
	 * @param {object} updContext
	 */
	reset: function( updContext ) {
		Runner.controls.EditBoxLookup.superclass.reset.call( this, updContext );
		this.setFrame( false );
	},

	/**
	 * Destructor with suggest div remove
	 */
	destructor: function() {
		Runner.controls.EditBoxLookup.superclass.destructor.call(this);

		this.destroyDiv();
		this.ajaxTask.cancel();
		this.onChangeTask.cancel();
	},

	/**
	 * @return {boolean}
	 */
	isEmpty: function() {
		return Runner.controls.EditBoxLookup.superclass.isEmpty.call( this ) || this.isError;
	},

	/**
	 * Keyup event handler, for call lookupsuggest
	 * Do all work after keypressed
	 */
	"input": function( e ) {
		var changed;
		var dispValue = this.getInputValue();

		if ( this.freeInput && !this.multiSelectionAllowed ) {
			changed = dispValue !== this.getValue();

			this.setLookupValue( dispValue, false, dispValue );
			if ( changed ) {
				this.onChangeTask.delay(500);
			}
		}

		if ( dispValue === "" ) {
			this.ajaxTask.cancel();
			this.destroyDiv();
			this.setFrame( false );

			// set empty val and trigger error
			this.setValue("", true);
			return;
		}

		this.isSetSuggestVal = false;
		this.ajaxTask.delay( 500 );
	},

	"keydown": function(e) {

		switch( e.keyCode ) {
			case 38: //up arrow
				this.moveUp();
				break;
			case 40: //down arrow
				this.moveDown();
				break;
			case 13: //enter
				if ( this.multiSelectionAllowed ) {
					this.setSuggestValueToControl( 
						this.lookupValues[ this.cursor ], 
						this.suggestValues[ this.cursor ].replace(/\<(\/b|b)\>/gi, ""),
						this.suggestKeys[ this.cursor ]
					);
				}
				this.destroyDiv();
				this.stopEvent(e);
				return;
		}
	},

	/**
	 * Creates and set position of lookup div.
	 * Also set suggest vals
	 * @param {array} divsArr
	 */
	showSuggests: function( divsArr ) {
		this.destroyDiv();

		if ( !divsArr.length ) {
			return;
		}

		this.lookupDiv = $('<div id="' + this.lookupDivId + '" class="search_suggest"></div>')
			.css("visibility", "visible")
			.appendTo( document.body )
			.on('mouseenter', function(e) {
				$(this).data( "hovered", true );
			})
			.on('mouseleave', function(e) {
				$(this).data( "hovered", false );
			});

		for (var i = 0; i < divsArr.length; i++) {
			this.lookupDiv.append( divsArr[i] );
		}

		this.setDivPos();
	},

	/**
	 * wrapper
	 */
	destroySuggest: function() {
		this.destroyDiv();
	},

	/**
	 * Destroys lookupDiv from DOM
	 */
	destroyDiv: function() {
		if ( this.lookupDiv ) {
			this.lookupDiv.remove();
		}
		this.cursor = -1;
		// clear link
		this.lookupDiv = null;
	},

	/**
	 * Set div coords
	 */
	setDivPos: function() {
		if ( !this.lookupDiv ) {
			return;
		}

		var $displayedElem = this.getDispElem(),
			dElementOffset = $displayedElem.offset();

		if ( Runner.isDirRTL() ) {
			this.lookupDiv.css({
				left: "auto",
				top: dElementOffset.top + $displayedElem.outerHeight() + "px",
				right: $(window).width() - dElementOffset.left - $displayedElem.outerWidth() + "px"
			});
		} else {
			this.lookupDiv.css({
				top: dElementOffset.top + $displayedElem.outerHeight() + "px",
				left: dElementOffset.left + "px"
			});
		}

		this.lookupDiv.addClass( 'dropdown-menu' )
			.css({
				display: "block",
				width: $displayedElem.width() + "px"
			});

		// add highest z index
		this.lookupDiv.css("z-index", 2000);
	},

	/**
	 * On hover suggest div value div handler
	 * @param {object} divHovered
	 */
	suggestOver: function( divHovered ) {
		// set new cursor index
		this.cursor = Number(divHovered.id.substring( 10 ));
		this.dispSuggestVal( true );
	},
	/**
	 * On unhover suggest div value div
	 * @param {object} divValue
	 */
	suggestOut: function( divValue ) {
		divValue.className = 'suggest_link';
		this.isSetSuggestVal = false;
	},

	/**
	 * Function that makes request to server and parse content
	 * @param {boolean} findLinkValue. Get link field value only
	 */
	lookupAjax: function( findLinkValue ) {
		var ctrl = this,
			ajaxParams = {
				table: Runner.pages.PageSettings.getShortTName(this.table),
				pageType: this.pageType,
				page: this.pageObj.pageName,
				searchFor: this.getInputValue(),
				searchField: this.fieldName,
				isExistParent: !!this.parentCtrls.length ? 1 : 0,
				parentCtrlsData: JSON.stringify( this.getParentControlsData() ),
				data: JSON.stringify( this.getControlsData() ), // values from controls
				masterTable: this.pageObj.masterTable
			};
		ctrl.focusState = true;

		Runner.runnerAJAX( Runner.getPageUrl("lookupsuggest"), ajaxParams, function( respObj ) {
			if ( !respObj.success ) {
				// if error add red frame
				ctrl.setFrame( true );
				ctrl.destroyDiv();
				return;
			}

			var dispVal = ctrl.getInputValue(),
				found = !findLinkValue && ctrl.freeInput,
				divsArr = [],
				eventsFlag;

			ctrl.suggestValues = [];
			ctrl.lookupValues = [];
			ctrl.suggestKeys = [];


			respObj.data.forEach( function( suggest, j ) {
				if ( ctrl.focusState ) {
					divsArr.push( ctrl.getSuggestContainer( 
							'suggestDiv' + j,  
							suggest["display"], 
							suggest["link"], 
							suggest["displayEscaped"], 
							suggest["keys"] )
					);
				}

				suggest["display"] = String( suggest["display"] );
				ctrl.suggestValues.push( suggest["display"] );
				ctrl.lookupValues.push(  suggest["link"] );
				ctrl.suggestKeys.push( suggest["keys"] );

				if ( !found && ctrl.suggestValues[ j ] == dispVal ) {
					//	trigger events if value changed
					eventsFlag =  suggest["link"] !== ctrl.getValue();
					ctrl.setLookupValue(  suggest["link"], true, suggest["display"] );
					found = true;
				}
			});

			ctrl.setFrame( !found );

			if ( !findLinkValue && !found && !ctrl.multiSelectionAllowed ) {
				eventsFlag = ctrl.getValue() !== "";
				ctrl.setLookupValue( "", eventsFlag, dispVal );
			}

			if ( !ctrl.focusState ) {
				return;
			}
			if ( !findLinkValue ) {
				ctrl.showSuggests( divsArr );
			}
		});
	},

	/**
	 * Create a suggest div container
	 * @param {string} containerId
	 * @param {string} suggestValue
	 * @param {string} lookupValue
	 * @param {string} escapedSuggestValue  html-escaped suggest
	 * @return {jQuery object}
	 */
	getSuggestContainer: function( containerId, suggestValue, lookupValue, escapedSuggestValue, keys ) {
		var ctrl = this;

		return $('<div id="' + containerId + '" class="suggest_link"></div>')
					.css("cursor", "pointer")
					.html( escapedSuggestValue )
					.on("mouseover", function() {
						ctrl.suggestOver( this );
					})
					.on("mouseout", function() {
						ctrl.suggestOut( this );
					})
					.on("click", function() {
						ctrl.setFrame( false );
						ctrl.setSuggestValueToControl( lookupValue, suggestValue, keys );
						ctrl.destroyDiv();
					});
	},

	/**
	 * Down arrow handler
	 */
	moveDown: function() {
		if ( !this.lookupDiv ) {
			return;
		}

		var $suggests = this.lookupDiv.children();
		if ( !$suggests.length || this.cursor >= $suggests.length ) {
			return;
		}

		// there are any suggest vals and cursor not on last of them
		// add cursor count - same to move down
		this.cursor++;
		// loop for all suggest vals
		this.dispSuggestVal();
		// for cursor loop
		if ( this.cursor == $suggests.length ) {
			this.cursor = -1;
			this.focus();
		}
	},

	/**
	 * Up arrow handler
	 */
	moveUp: function() {
		if ( !this.lookupDiv ) {
			return;
		}

		var $suggests = this.lookupDiv.children();
		// there are any suugest vals and dont't know why check that cursor >= -1
		if ( !$suggests.length || this.cursor < -1 ) {
			return;
		}

		// move up same as make cursor less
		this.cursor--;
		// set cursor on the last values, for loop
		if ( this.cursor == -2 ) {
			this.cursor = $suggests.length - 1;
			this.focus();
		}
		// set styles and values
		this.dispSuggestVal();
	},

	/**
	 * Set the current suggest value for the control
	 * @param {boolean} hover
	 */
	dispSuggestVal: function( hover ) {
		var ctrl = this;

		this.lookupDiv.children().each( function( index, suggest ) {
			var suggestVal, lookupVal;

			suggest.className = index === ctrl.cursor ? "suggest_link_over" : "suggest_link";
			if ( index !== ctrl.cursor ) {
				return;
			}
			if ( hover ) {
				ctrl.isSetSuggestVal = true;
				return;
			}

			suggestVal = ctrl.suggestValues[ ctrl.cursor ].replace(/\<(\/b|b)\>/gi, "");
			lookupVal = ctrl.lookupValues[ ctrl.cursor ];
			// if lookup val changes, than fireEvent
			if ( ctrl.getValue() != lookupVal ) {
				ctrl.setFrame( false );
			}

			if ( !ctrl.multiSelectionAllowed ) {
				ctrl.setLookupValue( lookupVal, true, suggestVal );
			}
		});
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr true
	 * Sets hidden css style true for link add New
	 * @method
	 */
	setDisabled: function() {
		var res = Runner.controls.EditBoxLookup.superclass.setDisabled.call( this );
		if ( res ) {
			this.addNew.css('visibility', 'hidden');
		}

		return res;
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr false
	 * Sets visible css style true for link add New
	 * @method
	 */
	setEnabled: function() {
		var res = Runner.controls.EditBoxLookup.superclass.setEnabled.call( this );
		if ( res ) {
			this.addNew.css('visibility', 'visible');
		}

		return res;
	},

	/**
	 * Blur event handler
	 * @event
	 */
	"blur": function(e) {
		var ctrl = this,
			dispValue = this.getInputValue();

		if ( this.freeInput && this.multiSelectionAllowed && ( !this.lookupDiv || !this.lookupDiv.data('hovered') ) && dispValue.trim() !== "" ) {
			this.setSuggestValueToControl( dispValue, dispValue, null );
		}

		this.focusState = false;
		if ( !this.multiSelectionAllowed && !this.freeInput && !this.isSetSuggestVal && this.suggestValues.length ) {
			this.setFrame( dispValue !== "" && $.inArray( dispValue, this.suggestValues ) === -1 );
		}

		if ( this.multiSelectionAllowed  && !this.freeInput ) {
			this.setFrame( false );
		}

		setTimeout( function() {
			ctrl.destroyDiv();
			if ( ctrl.multiSelectionAllowed	) {
				ctrl.displayElem.width( 100 );
			}
		}, 500);

		this.stopEvent(e);
		this.isSetFocus = false;

		//	if delayed task was started, start it immediately
		if ( this.ajaxTask.started() ) {
			this.ajaxTask.delay(0);
		}

		if ( this.onChangeTask.started() ) {
			this.onChangeTask.delay(0);
		}
	},

	/**
	 * Focus event handler
	 * @event
	 */
	"focus": function(e) {
		if ( !this.multiSelectionAllowed ) {
			this.stopEvent(e);
		}
		this.focusState = true;
	},

	/**
	 * Get the displayed field's value
	 * @return {string}
	 */
	getDisplayValue: function() {
		if ( !this.multiSelectionAllowed ) {
			return this.getInputValue();
		}

		var displayValues = [], i;
		$('option:selected', this.valueElem).each( function( ind, option ) {
			displayValues.push( $( option ).text() );
		})
		return displayValues.join(", ");
	},

	/**
	 * Get a string representation of the control's value
	 * @return {string}
	 */
	getStringValue: function( valueArray ) {
		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.simpleSearchControlMode ) {
			return this.getDisplayValue();
		}

		if ( this.multiSelectionAllowed ) {
			var valsArr = valueArray || this.getValue( true ), i;

			for (i = 0; i < valsArr.length ;i++) {
				if ( valsArr[i].indexOf(',') !== -1 || valsArr[i].indexOf('"') !== -1 ) {
					valsArr[i] = '"' + valsArr[i].replace(/"/g, '""') + '"';
				}
			}

			return valsArr.join(",");
		}

		return this.getValue();
	},

	/**
	 * Change the search control's appearance on the search option change #8538
	 * @param {string} searchOption
	 */
	updateAppearance: function( searchOption ) {
		if ( this.multiSelectionAllowed ) {
			return;
		}

		var simpleSearchMode = [ "Contains", "NOT Contains", "Starts with", "NOT Starts with" ]
			.indexOf( searchOption ) !== -1;

		if ( this.simpleSearchControlMode && !simpleSearchMode && this.linkField !== this.dispField ) {
			// find and set link field value
			this.lookupAjax( true );
		}

		this.simpleSearchControlMode = simpleSearchMode;
	},

	/**
	 * Update the control values' list
	 * @param {array} vals
	 * @param {string | array} selectValue
	 */
	preload: function( vals, selectValue ) {
		if ( this.simpleSearchControlMode ) {
			return;
		}

		if ( this.freeInput && this.multiSelectionAllowed ) {
			for (var i = 0; i < selectValue.length; i++ ) {
				//this.setLookupValue( selectValue[i], false, selectValue[i] );
				var v = vals.find( function(v) {
					return v["linkValue"] === selectValue[i];
				});
				if ( v !== undefined ) {
					this.setLookupValue( v["linkValue"], false, v["displayValue"] );
					if ( v["keys"] && this.isEditCurrentItemAvailable() ) {
						this.getMultiselectOption( v["linkValue"] )
							.data('keys', v["keys"]);
					}
				} else {
					this.setLookupValue( selectValue[i], false, selectValue[i] );
				}
			}
			return;
		}

		Runner.controls.EditBoxLookup.superclass.preload.call( this, vals, selectValue );
	},

	/**
	 * Reload the control basing on its master control's value
	 * @param {object} updContext
	 */
	reload: function( updContext ) {
		if ( this.simpleSearchControlMode ) {
			return;
		}
		Runner.controls.EditBoxLookup.superclass.reload.call( this, updContext );
	},

	/**
	 * Clone html for iframe submit
	 * @return {array}
	 * @intellisense
	 */
	getForSubmit: function() {
		if ( !this.appearOnPage() ) {
			return [];
		}

		var cloneElem = this.valueElem.clone(),
			$options;

		if ( this.multiSelectionAllowed ) {
			$options = $('option', cloneElem).prop('selected', true);
			if ( !$options.length ) {
				cloneElem.append('<option selected="selected" value=""></option>');
			}

			return [ cloneElem ];
		}

		if ( this.freeInput || !this.multiSelectionAllowed && this.getDisplayValue() === "" ) {
			return [ cloneElem.val( this.displayElem.val() ) ];
		}

		return [ cloneElem.val( this.valueElem.val() ) ];
	},

	/**
	 * Process the new added 'on the fly' item
	 * @param {string} text
	 * @param {string | number} value
	 */
	processNewFlyAdded: function( text, value, keys ) {
		if ( !this.suggestValues ) {
			return;
		}

		this.suggestValues.push( text );
		this.lookupValues.push( value );
		this.suggestKeys.push( keys );

		this.setSuggestValueToControl( value, text, keys );

		this.setFrame( false );
	},

	/**
	 * Set a selected suggest value to the control
	 * @param {mixed} linkVal
	 * @param {mixed} dispVal
	 * @param {array} keys
	 */
	setSuggestValueToControl: function( linkVal, dispVal, keys ) {
		if ( !this.multiSelectionAllowed ) {
			this.setLookupValue( linkVal, true, dispVal );
			if ( this.displayElem && this.isEditCurrentItemAvailable() ) {
				this.displayElem.data('keys', keys);
			}
			this.updataEditCurrentButton();
			return;
		}

		var $option = this.getMultiselectOption( linkVal );

		if ( !$option.length ) {
			this.addMultiselectOption( linkVal, dispVal );
		}
		
		if ( this.displayElem && this.isEditCurrentItemAvailable() ) {
			this.getMultiselectOption( linkVal ).data('keys', keys);
		}
		
		this.updataEditCurrentButton();
	},

	/**
	 * Reload dependent lookups
	 * @param {object} updContext
	 * @param {event Object} e
	 */
	reloadDependentCtrls: function( updContext, e ) {
		if ( this.freeInput || e === null ) {
			Runner.controls.EditBoxLookup.superclass.reloadDependentCtrls.call( this, updContext, e );
		}
	},
	
	/**
	 * Set focus
	 * For text nodes set cursor to the end of text
	 */	
	setEndOfTextFocus: function() {
		this.setFocus();
		if ( !( this.displayElem instanceof jQuery ) ) {
			return;
		}
		
		var ctrlElem = this.displayElem.get(0);
		if ( ctrlElem && ctrlElem.setSelectionRange && ctrlElem.type != 'hidden' ) {
			setTimeout( function() {
					var valLength = ctrlElem.value.length;
					ctrlElem.setSelectionRange( valLength, valLength );
				}, 0 );
		}
	},
});
/**
 * List page with search lookup control class
 * @requires Runner.controls.TextFieldLookup
 */
Runner.controls.ListPageLookup = Runner.extend( Runner.controls.TextFieldLookup, {
	/**
	 * The 'Select' link
	 * @type {jQuery object}
	 */
	selectLinkElem: null,

	/**
	 * @type {Array}
	 */
	lookupVals: null,
	

	multiselectionOnMobile: false,	
	clearLookupLink: null,

	/**
	 * Override parent constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.controls.ListPageLookup.superclass.constructor.call( this, cfg );

		this.initClearableInput();
		this.initSelectLink();
		this.initChanginAppearance();
		
		this.initClearLookup();
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignListOfEvents: function() {
		Runner.controls.ListPageLookup.superclass.assignListOfEvents.call( this );
		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") ) {
			this.addEvent( ["keyup"] );
		}
	},

	/**
	 * Set the DOM elements for the custom listeners usage
	 */
	assignsElemsForEvent: function() {
		Runner.controls.ListPageLookup.superclass.assignsElemsForEvent.call( this );
		if ( this.mode === Runner.controls.constants.MODE_SEARCH && this.valueElem.data("usesuggests") ) {
			this.elemsForEvent.push( this.displayElem.get(0) );
		}
	},

	/**
	 * Initialize the 'Select' link
	 */
	initSelectLink: function() {
		var ctrl = this,
			pageParams = {
				tName: this.lookupTable,
				pageType: Runner.pages.constants.PAGE_LIST,
				lookupCtrl: this,
				modal: true,
				baseParams: {
					page: this.listPageId,
					firstTime: 1,
					parId: this.rowId,
					table: this.table,
					field: this.fieldName,
					pageType: this.pageType,
					mode: Runner.pages.constants.MODE_LOOKUP,
					mainRecordMasterTable: this.masterTName // for where expr. :master.field
				},
				afterCreateHn: function() {
					ctrl.lookupVals = this.controlsMap.lookupVals;
					ctrl.dispFieldAlias = this.controlsMap.dispFieldAlias;
				}
			},
			selectLinkId = "open_lookup_" + this.goodFieldName + "_" + this.rowId;

		this.selectLinkElem = $("#" + selectLinkId)
			.on("click", function(e) {
				if ( ctrl.selectLinkElem.hasClass("disabled") ) {
					return false;
				}

				if ( Runner.suggests && $("#search_suggest").length > 0 && typeof Runner.suggests.DestroySuggestDiv != "undefined" ) {
					Runner.suggests.DestroySuggestDiv();
				}

				if ( !!ctrl.parentCtrls.length ) {
					pageParams.baseParams.parentsExist = true;
					pageParams.baseParams.parentCtrlsData = JSON.stringify( ctrl.getParentControlsData() );
				}

				pageParams.baseParams.data = JSON.stringify( ctrl.getControlsData() );
				if ( ctrl.lookupOrderBy ) {
					pageParams.baseParams.orderby = ctrl.lookupDesc ? "d" + Runner.goodFieldName( ctrl.lookupOrderBy ): "a" + Runner.goodFieldName( ctrl.lookupOrderBy );
				}

				ctrl.pageId = Runner.pages.PageManager.openPage( pageParams );
				ctrl.setDisabled();

				return false;
			});
	},

	/**
	 * @param {string} pageId
	 * @return {object}
	 */
	getLookupParams: function( pageId ) {
		var lookupParams = {
				id: pageId,
				table: this.table,
				field: this.fieldName,
				pageType: this.pageType,
				mode: Runner.pages.constants.MODE_LOOKUP,
				data: JSON.stringify( this.getControlsData() ),
				mainRecordMasterTable: this.masterTName // for where expr. :master.field
			};

		if ( !!this.parentCtrls.length ) {
			lookupParams.parentsExist = true;
			lookupParams.parentCtrlsData = JSON.stringify( this.getParentControlsData() );
		}

		return lookupParams;
	},

	/**
	 * Initialize a multiselect control if multiple selection is allowed
	 */
	initMultiselectionWithTabs: function() {
		if ( !this.multiSelectionAllowed || !this.valueElem.length ) {
			return;
		}

		Runner.controls.ListPageLookup.superclass.initMultiselectionWithTabs.call( this );

		var ctrl = this,
			chosenCtrl = this.valueElem.data("chosen");

		if ( chosenCtrl ) {
			chosenCtrl.search_field.prop('readonly', true);
			chosenCtrl.container
				.on('click', function(e) {
					ctrl.selectLinkElem.trigger('click');
				});
		} else {
			// chosen is disabled on mobile devices
			this.multiselectionOnMobile = true;
			this.valueElem.addClass('mobile-multiselection');

			$('option', this.valueElem).prop('disabled', true);
		}
	},
	
	initClearLookup: function() {
		if ( this.multiselectionOnMobile ) {
			var ctrl = this;
			this.clearLookupLink = $("#clearLookup_" + this.goodFieldName + "_" + this.rowId, this.pageCont)
				.on("click", function(e) {
					ctrl.clear();
					ctrl.clearLookupLink.css('visibility', 'hidden');
					return false;
				});
				
			if ( !this.isEmpty() ) {
				this.clearLookupLink.css('visibility', 'visible');
			}

			ctrl.on("change", function() {
				$('option', this.valueElem).prop('selected', true);
				this.clearLookupLink.css('visibility', this.isEmpty() ? 'hidden' : 'visible');
			});	
		}
	},
	

	/**
	 *	Initialize the two-states behaviour for the search control with the same displayed and link fields #8538
	 */
	initChanginAppearance: function() {
		if ( this.mode !== Runner.controls.constants.MODE_SEARCH || this.multiSelectionAllowed ) {
			this.updateAppearance = Runner.emptyFn;
			return;
		}

		var ctrl = this;
		this.displayElem
			.on("change", function(e) {
				var dispVal = $(this).val();
				ctrl.setLookupValue( dispVal, true, dispVal );
			})
			.on("keyup", function(e) {
				if ( !Runner.isAcceptableKeyCode(e) ) {
					return;
				}
				var dispVal = $(this).val();
				ctrl.setLookupValue( dispVal, true, dispVal );
			});
	},

	/**
	 * Change the search control's appearance on the search option change #8538
	 * @param {string} searchOption
	 */
	updateAppearance: function( searchOption ) {
		var simpleControlOptions = [ "Contains", "NOT Contains", "Starts with", "NOT Starts with" ];

		if ( $.inArray( searchOption, simpleControlOptions ) === -1 ) {
			this.displayElem.prop('readonly', true);
			this.selectLinkElem.css('visibility', 'visible');
			return;
		}

		this.displayElem.prop('readonly', false);
		this.selectLinkElem.css('visibility', 'hidden');
	},

	/**
	 * Update the control values' list
	 * @param {array} vals
	 * @param {string | array} selectValue
	 */
	preload: function( vals, selectValue ) {
		if ( !this.multiSelectionAllowed && !this.displayElem.prop('readonly') ) {
			return;
		}
		Runner.controls.ListPageLookup.superclass.preload.call( this, vals, selectValue );
	},

	/**
	 * Reload the control basing on its master control's value
	 * @param {object} updContext
	 */
	reload: function( updContext ) {
		if ( !this.multiSelectionAllowed && !this.displayElem.prop('readonly') ) {
			return;
		}
		Runner.controls.ListPageLookup.superclass.reload.call( this, updContext );
	},

	/**
	 * Check if suggests should be displayed for the control
	 * @return {boolean}
	 */
	implySuggests: function() {
		return !this.multiSelectionAllowed && !this.displayElem.prop('readonly');
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr true
	 * Sets hidden css style true for image selectLinkElem
	 * @method
	 */
	setDisabled: function() {
		var res = Runner.controls.ListPageLookup.superclass.setDisabled.call( this );

		if ( res && this.selectLinkElem ) {
			this.selectLinkElem
				.css('visibility', 'hidden')
				.addClass('disabled');
				
			this.clearLookupLink && this.clearLookupLink
				.css('visibility', 'hidden')
				.addClass('disabled');
		}

		return res;
	},

	/**
	 * Overrides parent function for element control
	 * Sets disable attr false
	 * Sets visible css style true for image selectLinkElem
	 * @method
	 */
	setEnabled: function() {
		var res = Runner.controls.ListPageLookup.superclass.setEnabled.call( this );
		
		if ( this.multiselectionOnMobile ) {
			$("option", this.valueElem).prop('disabled', true);
		}

		if ( res && this.selectLinkElem ) {
			this.selectLinkElem
				.css('visibility', 'visible')
				.removeClass('disabled');
				
			this.clearLookupLink && this.clearLookupLink
				.css('visibility', this.isEmpty() ? 'hidden' : 'visible')
				.removeClass('disabled');
		}

		return res;
	},

	/**
	 * @param {number} valInd
	 */
	setValByInd: function( valInd ) {
		var $option,
			linkVal = this.lookupVals[ valInd ].linkVal,
			dispVal = this.lookupVals[ valInd ].dispVal;

		if ( !this.multiSelectionAllowed ) {
			if ( !!this.lookupVals[ valInd ].keys && this.isEditCurrentItemAvailable() ) {
				this.displayElem.data('keys', this.lookupVals[ valInd ].keys);
			}
			
			this.setLookupValue( linkVal, true, dispVal );
			this.updataEditCurrentButton();
			return;
		}

		$option = this.getMultiselectOption( linkVal );

		if ( !$option.length ) {
			this.addMultiselectOption( linkVal, dispVal );
			
			if ( this.multiselectionOnMobile ) {
				$('option', this.valueElem).prop('disabled', true);
			}
		}
		
		if ( !!this.lookupVals[ valInd ].keys && this.isEditCurrentItemAvailable() ) {
			this.getMultiselectOption( linkVal )
				.data('keys', this.lookupVals[ valInd ].keys);
		}

		this.updataEditCurrentButton();
	},

	/**
	 * Add a new element to the lookupVals array property
	 * @param {string} linkVal
	 * @param {string} dispVal
	 * @return {number}			An index of the new added element
	 */
	addLookupVal: function( linkVal, dispVal, keys ) {
		return this.lookupVals.push({
				linkVal: linkVal, 
				dispVal: dispVal, 
				keys: keys
			}) - 1;
	},

	/**
	 * Make the control 'readonly'
	 */
	makeReadonly: function() {
		Runner.controls.ListPageLookup.superclass.makeReadonly.call( this );
		this.selectLinkElem
			.addClass('disabled')
			.hide();
	},

	/**
	 * Make the control writable
	 */
	makeReadWrite: function() {
		Runner.controls.ListPageLookup.superclass.makeReadWrite.call( this );
		this.selectLinkElem
			.removeClass('disabled')
			.show();
	},

	/**
	 * @param {mixed} value
	 * @param {boolean} triggerEvent
	 * @param {object} updContext
	 */
	setValue: function( value, triggerEvent, updContext ) {
		Runner.controls.ListPageLookup.superclass.setValue.call( this, value, triggerEvent, updContext );

		if ( this.multiSelectionAllowed ) {
			this.valueElem.trigger("chosen:updated");
		}

		return true;
	},

	/**
	 * Clone the value DOM element to submit
	 * @return {array}
	 */
	getForSubmit: function() {
		if ( !this.multiSelectionAllowed ) {
			return Runner.controls.ListPageLookup.superclass.getForSubmit.call(this);
		}

		if ( !this.appearOnPage() ) {
			return [];
		}

		var cloneElem = this.valueElem.clone(),
			$options = $('option', cloneElem).prop('selected', true);

		if ( this.multiselectionOnMobile ) {
			$options.prop('disabled', false);
		}

		if ( !$options.length ) {
			cloneElem.append('<option selected="selected" value=""></option>');
		}

		return [ cloneElem ];
	},

	/**
	 * Get the displayed field's value
	 * @return {string}
	 */
	getDisplayValue: function() {
		if ( !this.multiSelectionAllowed ) {
			return this.getInputValue();
		}

		var displayValues = [], i;
		$('option:selected', this.valueElem).each( function( ind, option ) {
			displayValues.push( $( option ).text() );
		})
		return displayValues.join(", ");
	},

	/**
	 * Return an escaped string value
	 * @param {Array} valueArray
	 * @return {string}
	 */
	getStringValue: function( valueArray ) {
		if ( !this.multiSelectionAllowed ) {
			return Runner.controls.ListPageLookup.superclass.getStringValue.call( this );
		}

		var valsArr = valueArray || this.getValue( true ), i;

		for (i = 0; i < valsArr.length ;i++) {
			if ( valsArr[i].indexOf(',') !== -1 || valsArr[i].indexOf('"') !== -1 ) {
				valsArr[i] = '"' + valsArr[i].replace(/"/g, '""') + '"';
			}
		}

		return valsArr.join(",");
	},

	/**
	 * Check if search suggests need initializing for the control
	 * @return {boolean}
	 */
	useSearchSuggests: function() {
		return this.mode === Runner.controls.constants.MODE_SEARCH 
			&& !this.multiSelectionAllowed && this.valueElem.data("usesuggests");
	},

	/**
	 * Add clear icon inside a control's input field
	 */
	initClearableInput: function() {
		var $valueElem = this.valueElem,
			iconSize = 25,
			ctrl = this;

		this.displayElem
			.on('mousemove', function(e) {
				if ( this.value ) {
					$(this).toggleClass( 'onX', Runner.isDirRTL() ? 
						iconSize > e.clientX - this.getBoundingClientRect().left : 
						this.offsetWidth - iconSize < e.clientX - this.getBoundingClientRect().left );
				}
			})
			.on('touchstart click', function(e) {
				if ( Runner.isDirRTL() ? 
					iconSize > e.clientX - this.getBoundingClientRect().left : 
					this.offsetWidth - iconSize < e.clientX - this.getBoundingClientRect().left && this.value ) {
						
					e.preventDefault();
					$valueElem.val('');
						
					ctrl.fireEvent( "change", {}, true );

					$(this).removeClass('x onX')
						.val('').trigger("change");
				}
			});
	},
});
Runner.namespace('Runner.pages');

Runner.pages.constants = {
	NOT_TABLE_BASED_TNAME: "<global>",
	
	PAGE_LIST: "list",
	PAGE_ADD: "add",
	PAGE_INLINE_ADD: "inline_add",
	PAGE_EDIT: "edit",
	PAGE_INLINE_EDIT: "inline_edit",
	PAGE_VIEW: "view",
	
	PAGE_SEARCH: "search",
	PAGE_REPORT: "report",
	PAGE_CHART: "chart",
	PAGE_DREPORT: "dreport",
	PAGE_DCHART: "dchart",
	PAGE_PRINT: "print",
	PAGE_REPORT_PRINT: "rprint",
	PAGE_EXPORT: "export",
	PAGE_IMPORT: "import",
	PAGE_ADMIN_MEMBERS: "admin_members_list",
	PAGE_ADMIN_ADMEMBERS: "admin_admembers_list",
	PAGE_ADMIN_RIGHTS: "admin_rights_list",
	PAGE_MASTER_INFO_LIST: "masterlist",
	PAGE_MASTER_INFO_PRINT: "masterprint",
	
	PAGE_REGISTER: "register",
	PAGE_MENU: "menu",
	PAGE_REMIND: "remind",
	PAGE_CHANGEPASS: "changepwd",
	PAGE_LOGIN: "login",

	PAGE_DASHBOARD: "dashboard",
	PAGE_DASHMAP: "map",
	
	PAGE_USERINFO: "userinfo",
	
	PAGE_SESSION_EXPIRED: "session_expired",
	PAGE_CALENDAR: "calendar",
	PAGE_GANTT: "gantt",
	
	LIST_SIMPLE: 0,
	LIST_LOOKUP: 1,
	LIST_DETAILS: 3,
	LIST_AJAX: 4,
	RIGHTS_PAGE: 5,
	MEMBERS_PAGE: 6,
	LIST_DASHBOARD: 7,
	LIST_DASHDETAILS: 8,
	MAP_DASHBOARD: 9,
	GRIDBASED_MAP_DASHBOARD: 10,
	

	CHART_SIMPLE: 0,
	CHART_POPUPDETAILS: 1,
	CHART_DASHBOARD: 2,
	CHART_DETAILS: 3,
	CHART_DASHDETAILS: 4,

	CALENDAR_DASHBOARD: 2,
	GANTT_DASHBOARD: 2,


	ADD_SIMPLE: 0,
	ADD_INLINE: 1,
	ADD_ONTHEFLY: 2,
	ADD_MASTER: 3,
	ADD_POPUP: 4,
	ADD_DASHBOARD: 5,
	ADD_MASTER_POPUP: 6,
	ADD_MASTER_DASH: 7,
	
	EDIT_SIMPLE: 0,
	EDIT_INLINE: 1,
	EDIT_ONTHEFLY: 2,
	EDIT_POPUP: 3,
	EDIT_DASHBOARD: 4,
	EDIT_SELECTED_SIMPLE: 5,
	EDIT_SELECTED_POPUP: 6,
	
	SEARCH_DASHBOARD: 1,
	
	MODE_AJAX: "ajax",
	MODE_SIMPLE: "",
	MODE_LOOKUP: "lookup",
	MODE_LIST_DETAILS: "listdetails",
	
	OPENMODE_POPUP: 1,
	OPENMODE_CONTAINER: 2,
	OPENMODE_PRELOADED: 3,
	OPENMODE_FULLSCREEN: 4,
	OPENMODE_DASHCONTAINER: 5,
	OPENMODE_DASHTABS: 6,
	OPENMODE_TABVIEW: 7,
	OPENMODE_PRELOADED_CONTAINER: 8,	//	page initially preloaded, then updated as OPENMODE_CONTAINER ( details on Edit/View pages )
	
	DP_POPUP: 0,
	DP_INLINE: 1,
	DP_NONE: 2,
	
	DL_SINGLE: 0,
	DL_INDIVIDUAL: 1,
	DL_NONE: 2,

	DASHMODE_SNIPPET: 1,

	SEARCH_OPTIONS: {
		"Contains": "contains",
		"Equals": "equals",
		"Starts with": "startswith",
		"More than": "morethan",
		"Less than": "lessthan",
		"Between": "between",
		"Empty": "empty", 
		"NOT Contains": "notcontain", 
		"NOT Equals": "notequal", 
		"NOT Starts with": "notstartwith",
		"NOT More than": "lessequal",
		"NOT Less than": "moreequal", 
		"NOT Between": "notbetween", 
		"NOT Empty": "notempty"
	},
	
	MINUS_GIF: Runner.getFullResourcePath("images/minus.gif"),
	PLUS_GIF: Runner.getFullResourcePath("images/plus.gif"),
	CLOSE_RED_GIF: "remove",
	OK_GIF: "iesave",
	CANCEL_GIF: "iecancel",
	
	HORIZONTAL_LAYOUT: 0,
	VERTICAL_LAYOUT: 1,
	COLUMNS_LAYOUT: 2,
	FLEXIBLE_LAYOUT: 3,

	LOGIN_SEPARATE: 0,
	LOGIN_POPUP: 1,
	LOGIN_EMBEDED: 2,
	
	PORTRAIT_PRINT_ORIENTATION: 0,
	LANDSCAPE_PRINT_ORIENTATION: 1,
	
	STEPPED_LAYOUT: 0,
	BLOCK_LAYOUT: 1,
	OUTLINE1_LAYOUT: 2,
	ALIGN1_LAYOUT: 3,
	TABULAR_LAYOUT: 6,
	
	REPORT_MODE_PRINT_FRIENDLY: 0,
	REPORT_MODE_PRINT_WHOLE_REPORT: 1,
	
	REPORT_PRINT_PAGE_FIT: 0,
	REPORT_PRINT_FIXED_GROUPS: 1,
	
	REPORT_TYPE_STANDART: 0,
	REPORT_TYPE_CROSSTAB: 1,
	
	REPORT_SIMPLE: 0,
	REPORT_POPUPDETAILS: 1,
	REPORT_DASHBOARD: 2,
	REPORT_DETAILS: 3,
	REPORT_DASHDETAILS: 4,
	
	
	VPRINT_PAGE_WIDTH: 700,
	VPRINT_PAGE_HEIGHT: 900,
	
	HPRINT_PAGE_WIDTH: 1000,
	HPRINT_PAGE_HEIGHT: 700,
	
	/** 
	 * Dashboard element types 
	 */
	DASHBOARD_LIST: 0,
	DASHBOARD_CHART: 1,
	DASHBOARD_REPORT: 2,
	DASHBOARD_RECORD: 3,
	DASHBOARD_SEARCH: 4,
	DASHBOARD_DETAILS: 5,
	DASHBOARD_MAP: 6,
	DASHBOARD_SNIPPET: 7,
	DASHBOARD_CALENDAR: 8,
	DASHBOARD_GANTT: 9,
	DASHBOARD_WIDGET: 10,

	
	/**
	 * After record added/edited actions constants
	 */	
	AA_TO_LIST: 0,
	AA_TO_ADD: 1,
	AA_TO_VIEW: 2,
	AA_TO_EDIT: 3,
	AA_TO_DETAIL_ADD: 4,
	AA_TO_DETAIL_LIST: 5,
	
	AE_TO_LIST: 0,
	AE_TO_EDIT: 1,
	AE_TO_VIEW: 2,
	AE_TO_NEXT_EDIT: 3,
	AE_TO_PREV_EDIT: 4,
	AE_TO_DETAIL_LIST: 5,
	
	titTABLE: 0,
	titVIEW: 1,
	titREPORT: 2,
	titCHART: 3,
	titDASHBOARD: 4,
	
	BOOTSTRAP_LAYOUT: 3,
	PD_LAYOUT: 4,
	
	SMALL_MODAL_WIDTH: 700,
	
	// paramsLogger types
	SSEARCH_PARAMS_TYPE: 1,
	CRESIZE_PARAMS_TYPE: 2,
	SHFIELDS_PARAMS_TYPE: 3,
	FORDER_PARAMS_TYPE: 4,
	NEWSHFIELDS_PARAMS_TYPE: 5,
	
	DESKTOP:1,
	TABLET_10_IN: 2,
	TABLET_7_IN: 3,
	SMARTPHONE_LANDSCAPE: 4,
	SMARTPHONE_PORTRAIT: 5,

	TWOFACTOR_EMAIL: 0,
	TWOFACTOR_PHONE: 1,
	TWOFACTOR_APP: 2,
	
	stpDISK: 0,
	stpAMAZON: 1,
	stpGOOGLEDRIVE: 2,
	stpONEDRIVE: 3,
	stpDROPBOX: 4,
	stpWASABI: 5,
	stpGOOGLECLOUD: 6,
	stpAZUREBLOB: 7,

};

Runner.apply(Runner, Runner.pages.constants);// create namespace
Runner.namespace('Runner.util.inlineEditing');

/**
 * Base abstract class for InlineAdd and InlineEdit
 * provides base functionality and event handling
 * @class Runner.util.inlineEditing.InlineEditor
 */
Runner.util.inlineEditing.InlineEditor = Runner.extend( Runner.util.Observable, {
	
	id: -1,
	
	tName: "",
	
	pageType: "",
	
	shortTName: "",
	
	rowPref: "gridRow",
	
	/**
	 * @type {array}
	 */
	rows: null,

	/**
	 * @type {array}
	 */	
	fNames: null,
	
	/**
	 * @type {array}
	 */		
	totalFields: null,

	/**
	 * @type {object}
	 */
	baseParams: null, 
	
	/**
	 * The host list page object
	 * @type {object}
	 */
	pageObj: null,
	
	/**
	 * @type {jQuery object}
	 */
	saveAllButt: null,

	/**
	 * @type {jQuery object}
	 */	
	cancelAllButt: null,
	
	massRecButtEditMode: false,
	
	loadSettings: false,
	
	isEditOwn: false,
	
	isOldLayout: false,
	
	rowFocussed: false,
	
	classNameHidden: "",
	classNameRow: "", 
	classNameBottomRow: "",
	classNameMessage: "",
	classNameInlineError: "",
	classNameLocking: "",
	
	/**
	 * It shows if the object is set up 
	 * for a dashboard grid element
	 */
	dashGridBased: false,
	
	/**
	 * An array storing the grid rows order
	 * @type {array}
	 */
	rowsOrderData: [],
	
	/**
	 * It's properly set for the 'List page 
	 * with search lookup' only 
	 * @type {object}
	 */
	lookupParams: null,
	
	/**
	 * It's properly set for the 'List page 
	 * with search lookup' only 
	 * @type {object}
	 */	
	lookupFieldInfo: null,
	
	spreadsheet: false,	


	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		this.rows = [];
		this.fNames = [];
		
		Runner.apply( this, cfg );		
		Runner.util.inlineEditing.InlineEditor.superclass.constructor.call( this, cfg );	
		
		this.pageObj = Runner.pages.PageManager.getAt( this.tName, this.id );
		
		this.baseParams = Object.assign( this.baseParams || {}, this.pageObj.getStateParams() );
		this.baseParams.masterpagetype = Runner.pages.PageSettings.getTableData(this.tName, "masterPageType") || ""; // Fix it!		
		
		this.isEditOwn = Runner.pages.PageSettings.getTableData( this.tName, "isEditOwn" );
		this.totalFields = Runner.pages.PageSettings.getTableData( this.tName, "totalFields" );
		this.listGridLayout = Runner.pages.PageSettings.getTableData(this.tName, "listGridLayout");
		
		this.assignClassNames();
		
		this.addEvents(
			'rowsEdited',
			'createControls',
			'beforeSubmit',
			'afterSubmit',
			'afterSave',
			'submitFailed',
			'beforeProcessNewRow',
			'revertRow',
			'validationFailed',
			'afterInit',
			'afterPageReady',
			'beforeRequestControls',
			'recalcGridSize',
			'beforeCancel',
			'cancel'
		);
	},

	/**
	 * Assign the class names properties
	 */
	assignClassNames: function() {
		var classPrefix = this.isOldLayout ? 'runner-' : 'rnr-';
			
		this.classNameHidden = classPrefix + "hiddenelem";
		this.classNameRow = classPrefix + "row";
		this.classNameBottomRow = classPrefix + "bottomrow";
		this.classNameMessage = classPrefix + "message";;
		this.classNameInlineError = classPrefix + "inline-error";
		this.classNameLocking = classPrefix + "locking";		
	},	
	
	/**
	 * Get submit URL
	 * @return {string}
	 */
	getSubmitUrl: function() {
		return Runner.getPageUrl( this.shortTName, this.pageType );
	},
	
	init: function() {	
		this.fillGridRowsData();		
		
		this.initButtons();
		this.initEvents();		
	},
	
	/**
	 *
	 */
	fillGridRowsData: function() {
		this.rowsOrderData = [];
		
		for (var i = 0; i < this.rows.length; i++) {
			this.rowsOrderData.push( {
				id: this.rows[i].id, 
				keys: this.rows[i].keys
			} );
		}
	},
	
	/**
	 * Subscribe handlers for default events ("afterInit", "afterPageReady")
	 */
	initEvents: function() {
		var events = Runner.pages.PageSettings.getTableEventsData( this.tName, this.pageType ),
			evName, eventHnArr, i;
			
		for ( evName in events ) {
			if ( evName !== 'afterInit' && evName !== 'afterPageReady' ) {
				continue;
			}
			
			eventHnArr = events[ evName ];
			for (i = 0; i < eventHnArr.length; i++) {
				this.on( evName, eventHnArr[i].hn, eventHnArr[i].scope || this.pageObj || this );
			}	
		}		
		
		this.on('rowsEdited', function( allVals, fields, allKeys, allRowIds, isEdited, singleSaveBtnPressed ) {
			this.toggleMassRecButt();
			this.calcTotals();
		}, this);   
	},

	/**
	 * Reinit inline object properties
	 * Use for example to ajax reload list page
	 * id and parId can be no need
	 * @param {Array} gridRows
	 */
	reInit: function( gridRows ) {
		this.rows = gridRows;	
		this.fillGridRowsData();
		
		if ( this.pageObj.pageMode === Runner.pages.constants.MODE_LIST_DETAILS 
			|| this.pageObj.pageMode === Runner.pages.constants.LIST_DASHBOARD ) {
			this.initButtons();
		}
	},
	
	/**
	 * UnSelect all records
	 */
	resetSelectAll: function() {
		var $selectAllButton = $('#select_all' + this.id);
		
		if ( $selectAllButton.length ) {
			$selectAllButton.get(0).checkAllStatus = false;
			$selectAllButton.html( Runner.lang.constants.SELECT_ALL );
		}
		if ( $('.chooseAll' + this.id).length ) {
			$('input[type="checkbox"][id^="chooseAll_' + this.id + '"]').prop("checked", false);
		}
	},
	
	/**
	 * Initialize 'Save all', 'Cancel' and 'Edit selected' buttons.
	 * Add corresponding props to the inline object and add on click
	 * handlers to 'Save all' and 'Cancel' buttons.
	 * As for the 'Cancel' button's on click handler doesn't depends
	 * only on base class props It's initialised once
	 */
	initButtons: function() {
		var inlineObj = this;
		
		this.editAllButt = $("[id=edit_selected" + this.id + "]");
		this.cancelAllButt = $("[id=revertall_edited" + this.id + "]");
		this.saveAllButt = $("[id=saveall_edited" + this.id + "]").on("click", function(e) {
			inlineObj.saveAll();
			inlineObj.resetSelectAll();
			return false;
		});
		
		//cancelAll and resetSelectAll don't depend on a patricular 'this' object
		if ( !this.pageObj.inlineButtonsIntialized ) {
			this.cancelAllButt.on("click", function(e) {
				inlineObj.cancelAll();
				inlineObj.resetSelectAll();
				return false;
			});
		}
		this.pageObj.inlineButtonsIntialized = true;
	},
	
	/**
	 * Reinitialize the 'Save all', 'Cancel' and 'Edit selected' buttons.
	 */
	reinitButtons: function() {
		this.initButtons();
	},

	/**
	 * @param {string} val
	 * @param {string} format
	 * @return {number}
	 */
	parseForTotals: function( val, format ) {
		if ( format == 'Time' ) {
			var timeArr = val.split(':');
			
			if ( timeArr.length !== 3 ) {
				return '';
			}
					
			return parseInt(timeArr[2], 10) + parseInt(timeArr[1], 10) * 60 + parseInt(timeArr[0], 10) * 3600;
		}

		if ( format == 'Number' ) {
			return parseFloat( ( val || '' ).replace(',', '.') );
		}
		
		return parseFloat( ( val || '' ).toString().trim() ); 
	},
	
	/**
	 * Update the 'Total' value for the field 
	 * @param {object} totalData
	 */		
	calcTotalField: function( totalData ) {
		var totalVal = 0;
		
		this.rows.forEach( function( row, i ) {
			if ( !row.data || row.data[ totalData.fName ] === undefined ) {
				this.getValuesFromSpan( row );
			}
			
			var fVal = this.parseForTotals( this.getValueFromSpan( row, totalData.fName ), totalData.format );	
			
			if ( !isNaN( fVal ) ) { 
				totalVal += fVal;
			}
		}, this );
		
		if ( totalData.format == 'Number' ) {
			totalVal = Runner.util.getNumberFormatted( totalVal, totalData.formatSettings );
		} else if ( totalData.format == 'Currency' ) {
			totalVal = Runner.util.getCurrencyFormatted( totalVal, totalData.formatSettings );
		} else if ( totalData.format == 'Time' ) {
			totalVal =  Runner.util.getTimeForTotal( totalVal );
		} 
		
		this.setTotalValue( totalData.fName, totalVal );
	},
	
	/**
	 * Update 'COUNT' value for the total field (fName)
	 * Set the obtained value as html for the corresponding jQuery element
	 * @paran {string} fName
	 * @paran {string} format	The total field's view format
	 */
	calcCountField: function( fName, format ) {
		var totalVal = 0;
		
		this.rows.forEach( function( row, i ) {
			var fVal, fieldSpan;
			
			if ( row.rowVals && typeof row.rowVals[ fName ] === "string" ) {
				fVal = row.rowVals[ fName ].trim();
			} else {
				fieldSpan = Runner.getFieldSpan( fName, row.id );
				fVal = ( fieldSpan.attr("val") || "" ).trim();
			}
			
			if ( format == Runner.controls.constants.FORMAT_CHECKBOX && (fVal == 0 || fVal == "" || fVal == "False") ) {
				return;
			}
			
			if ( fVal ) {
				totalVal++;
			}
		}, this );
		
		this.setTotalValue( fName, totalVal );
	},
	
	/**
	 * Update the 'Average' value for the field 
	 * @param {object} totalData
	 */		
	calcAverageField: function( totalData ) {
		var totalVal = 0, 
			numRows = 0;
			
		this.rows.forEach( function( row, i ) {
			if ( !row.data || row.data[ totalData.fName ] === undefined ) {
				this.getValuesFromSpan( row );
			}
			
			var fVal = this.parseForTotals( this.getValueFromSpan( row, totalData.fName ), totalData.format );
			
			if ( !isNaN( fVal ) ) {
				totalVal += fVal;
				numRows += 1;
			}
		}, this );
		
		if ( numRows ) {
			if ( totalData.format == 'Time' ) {
				totalVal = Runner.util.getTimeForTotal( Math.round( totalVal / numRows ) );
			} else {
				totalVal = Math.round( ( totalVal / numRows ) * 100 ) / 100;
				if ( totalData.format == 'Number' ) {
					totalVal = Runner.util.getNumberFormatted( totalVal, totalData.formatSettings );
				} else if ( totalData.format == 'Currency') {
					totalVal = Runner.util.getCurrencyFormatted( totalVal, totalData.formatSettings );
				}
			}
		} else {
			totalVal = "";
		}
		
		this.setTotalValue( totalData.fName, totalVal );
	},
	
	/**
	 * Count for total fields the corresponding total function's value.
	 * Show the obtained value in the total's row 
	 */		
	calcTotals: function() {
		if ( this.pageObj.openMode === Runner.pages.constants.OPENMODE_POPUP || !this.totalFields.length ) { 
			return;
		}
		
		if ( this.pageObj.pageData["allDataTotals"] ) {
			var inlineObj = this,
				baseParams = this.pageObj.getRequestTotalsParams();
			
			Runner.runnerAJAX( this.pageObj.requestUrl || Runner.pages.getUrl( this.pageObj.tName, this.pageObj.pageType ), baseParams, function( respObj ) {
				inlineObj.totalFields.forEach( function( totalField, idx ) {
					inlineObj.setTotalValue( totalField.fName, respObj[ totalField.fName ] );
				});
			});	
		} else {
		
			this.totalFields.forEach( function( totalField, idx ) {
				if ( totalField.type == "TOTAL" ) {
					this.calcTotalField( totalField );
				} else if ( totalField.type == "COUNT" ) {
					this.calcCountField( totalField.fName, totalField.format );
				} else if ( totalField.type == "AVERAGE" ) {
					this.calcAverageField( totalField );
				}				
			}, this );
		}
		
		
		$( 'tr.r-grid-footer', this.pageObj.gridElem )
			.removeClass( this.classNameHidden )
			.show();
	},
	
	/**
	 * @param strign fName
	 * @param string value
	*/
	setTotalValue: function( fName, value ) {
		$( "#total" + this.id + "_" + Runner.goodFieldName( fName ) )
			.html( value );
	},
	
	/**
	 * Assign the row's basicForm property with the Runner BasicForm object reference
	 * or if it's alredy assigned update its properties
	 * @param {object} row
	 */		
	initForm: function( row ) {
		row.basicForm = row.basicForm || this.createRowForm( row ); 

		// add/update form properties dependent on the row's state
		row.basicForm.id = row.id;
		row.basicForm.setFocus = !this.rowFocussed;
		row.basicForm.baseParams.id = row.id;
		row.basicForm.fieldControls = Runner.controls.ControlStorage.byId( row.id );
		
		if ( this.dashGridBased ) {
			row.basicForm.baseParams.dashTName = this.pageObj.dashboard.tName;
		}		
	},
	
	/**
	 * Create a form for a particular row
	 * @param {object} row 
	 * @return {object}
	 */
	createRowForm: function( row ) {
		var baseParams = Runner.apply( {
					id: row.id, 
					editType: 'inline',
					a: this.pageType === Runner.pages.constants.PAGE_ADD ? 'added' : 'edited', 
					page: this.pageObj.pageName,
					spreadsheetGrid: this.spreadsheet ? 1 : 0,
					hostPageName: this.pageObj.pageName,
				}, this.baseParams );
			
		return new Runner.form.BasicForm({
			isFileUpload: true,
			submitUrl: this.getSubmitUrl(),
			baseParams: baseParams,
			successSubmit: {
				fn: function( respObj, formObj, fieldControls ) {
					if ( !respObj.success ) {
						if ( respObj.fatalError ) {
							formObj.clearForm();
						}
						this.fireEvent("submitFailed", respObj, this, formObj, fieldControls);											
						this.makeError( respObj.message, row );	
					} else {
						if ( respObj['nonEditable'] ) {
							row.isEditable = false;
							row.canDeleteRecord = false;
							row.canEditRecord = false;
						}
						this.afterSubmit( row, respObj );
						if ( typeof row.onAfterSubmit != "undefined" ) {
							row.onAfterSubmit();
						}
					}

					row.submitting = false;
					
					if (this.spreadsheet) {
						$rowElement = row.row || $('#gridRow' + row.id);
						$rowElement.removeClass('r-ss-saving');
					}	
					if (row.saveLink) {
						row.saveLink.removeClass('disabled');
					}
				},
				scope: this
			},
			submitFailed: {
				fn: function(respObj, formObj, fieldControls) {
					row.submitting = false;
					
					if (respObj.success === false) {
						this.makeError( respObj.message, row );
					} else {
						this.makeError( respObj, row );
					}
					this.fireEvent("submitFailed", {}, this, formObj, fieldControls);
					if (this.spreadsheet) {
						$rowElement = row.row || $('#gridRow' + row.id);
						$rowElement.removeClass('r-ss-saving');
					}	
					if (row.saveLink) {
						row.saveLink.removeClass('disabled');
					}
				},
				scope: this
			},
			beforeSubmit: {
				fn: function( formObj ) {

					if ( row.onBeforeSubmit !== undefined ) {
						var rowBeforeResult = row.onBeforeSubmit();
						if ( rowBeforeResult === false ) {
							row.submitting = false;
							return false;
						}
					}
					var before = this.fireEvent( "beforeSubmit", row, this, formObj );
					if ( !before ) {
						row.submitting = false;
					}
					return before;
				},
				scope: this
			},
			validationFailed: {
				fn: function(formObj, fieldControls) {
					this.fireEvent("validationFailed", formObj, fieldControls);
					this.rowFocussed = true;
					if ( row.saveLink ) {
						row.saveLink.removeClass('disabled');
					}
					row.submitting = false;
				},
				scope: this
			},
			formSent: {
				fn: function(formObj, fieldControls)  {
					if (this.spreadsheet) {
						$rowElement = row.row || $('#gridRow' + row.id);
						$rowElement.addClass('r-ss-saving');
					}
				}, 
				scope: this
			}
		});
	},
	
	/**
	 * Cancel all Inline Add/Edit for all grid's rows
	 */
	cancelAll: function() {
		var canceledAll = true, i;
		
		for (i = 0; i < this.rows.length; i++) {
			if ( !this.rows[i].submitted ) {
				if ( this.rows[i].isAdd ) {
					if ( Runner.util.inlineEditing.InlineAdd.prototype.cancelButtonHn.call(this, this.rows[i], true) ) {	
						i--;
					} else {
						canceledAll = false;
					}
				} else if ( this.pageObj.inlineEdit ) {
					this.pageObj.inlineEdit.cancelButtonHn(this.rows[i], true);
				}
			}
		}
		if ( canceledAll ) {
			this.fireEvent("recalcGridSize");
			this.toggleMassRecButt();
		}
	},
	
	/**
	 * @param {object} row
	 */
	submit: function( row ) {
		row.revertted = false;
		this.initForm( row );
		row.basicForm.submit();	
	},
	
	/**
	 * Save all Inline Add/Edit grid's rows
	 * @param {boolean} modifiedOnly
	 */
	saveAll: function( modifiedOnly ) {
		var allVals = [], allKeys = [], allRowIds = [];
		
		this.rowFocussed = false;
		
		for (var i = 0, l = this.rows.length; i < l; i++) {
			allVals.push( this.rows[i].data );
			allKeys.push( this.rows[i].keys );
			allRowIds.push( this.rows[i].id );
			
			if ( !this.rows[i].skipSubmit && this.rows[i].submitted === false && this.rows[i].cancelLink.length ) {
				return false;
			}
		}
		// fire rowsEdited event if all rows submited
		this.fireEvent('rowsEdited', allVals, this.fNames, allKeys, allRowIds);
		return true;
	},
	
	/**
	 * The default cancel button's click handler
	 */ 
	cancelButtonHn: function() {	
		$('div.shiny_box').hide();
	},
	
	/**
	 * Load and show the row's controls
	 * @param {object} pageObj
	 * @param {object} row
	 * @param {boolean} hideRevertButton
	 * @param {boolean} hideSaveButton
	 * @param {boolean} skipFocus
	 */
	getControls: function( pageObj, row, hideRevertButton, hideSaveButton, skipFocus ) {
		var inlineObject = this,
			reqParams = {
				id: row.id,
				editType: "inline",
				page: pageObj.pageName,
				screenWidth: window.screen.width,
				screenHeight: window.screen.height,
				orientation: Runner.getOrientation(),
				isNeedSettings: this.loadSettings || !Runner.pages.PageSettings.checkSettings(this.tName),
				spreadsheetGrid: inlineObject.spreadsheet ? 1 : 0,
			};
		
		reqParams = Runner.apply( reqParams, this.baseParams );
		
		if ( this.lookupParams ) {
			reqParams = Runner.apply( reqParams, this.lookupParams );
		}
		
		if ( this.dashGridBased && this.pageObj ) {
			reqParams['dashTName'] = this.pageObj.dashboard.tName;
		}
		
		this.fireEvent('beforeRequestControls', this, row, reqParams);
		
		for (var i = 0; i < row.keys.length; i++) {
			reqParams['editid' + (i + 1)] = row.keys[i];
		}
		
		Runner.runnerAJAX( Runner.getPageUrl( this.shortTName, this.pageType ), reqParams, function( ctrlsJSON ) {
			if ( ctrlsJSON.success !== false ) {
				if ( inlineObject.loadSettings && ctrlsJSON.settings ) {
					// add settings
					Runner.pages.PageSettings.addSettings( inlineObject.tName, ctrlsJSON.settings, false );
				}
                var proxy = ctrlsJSON.pagesData[row.id].proxy;
				
				var init = function() {
					inlineObject.makeRowEditable( row, ctrlsJSON );					
					inlineObject.lockRow( row, ctrlsJSON );
					 
					inlineObject.getEditBlock( row, hideRevertButton, hideSaveButton );
					// the inline record's markup is ready
					if ( !skipFocus ) {
						inlineObject.setFocusOntheFirstControl( row );
					}
					
					if ( Runner.pages.PageSettings.getGlobalData("s508") ) {
						inlineObject.pageObj.s508Obj.s508inlineSaveCancel(row.id);
					}

					const $rowElement = $("#gridRow" + row.id );
					Runner.pages.RunnerPage.prototype.initViewLinks.call( inlineObject.pageObj, $rowElement  );					
						
					inlineObject.fireEvent('afterInit', inlineObject.pageObj, proxy, row.id, row, inlineObject);
				};
				
				if ( ctrlsJSON.additionalCSS ) {
					Runner.util.ScriptLoader.loadCSS( ctrlsJSON.additionalCSS );
				}
				
				if ( ctrlsJSON.additionalJS ) {
					for (var jsFile in ctrlsJSON.additionalJS) {
						Runner.util.ScriptLoader.addJS( [jsFile], ctrlsJSON.additionalJS[ jsFile ] );
					}
											
					Runner.util.ScriptLoader.onFilesLoaded( function() { 
						init();
						inlineObject.fireEvent('afterPageReady', inlineObject.pageObj, proxy, row.id, row, inlineObject, new Runner.AjaxRow( inlineObject.pageObj, row ));
					}, inlineObject );	
						
					Runner.util.ScriptLoader.load();
					return;	
				}
				init();
			}
			inlineObject.makeError( ctrlsJSON.message, row );
			inlineObject.showRowButtons( row );
		});	
	},		
	
	/**
	 * Returns LABEL element corresponding to the field.
	 * Can be used in the Vertical or Columns layout only.
	 */
	getFieldLabelElement: function( row, field ) {
		var spanCont;
		
		if ( this.listGridLayout === Runner.pages.constants.VERTICAL_LAYOUT ) {
			spanCont = Runner.getFieldSpan( field, row.id );
			return spanCont.closest( '.bs-viewform-group' ).find( '.bs-view-label' );
		} else if ( this.listGridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			spanCont = Runner.getFieldSpan( field, row.id );
			return spanCont.closest('table').find('[data-field="' + Runner.goodFieldName( field ) +'"] > label');
		}
		
		return $();
	},

	/**
	 * Changes LABEL element to the View mode.
	 * Must be called after Inline Add/Edit is finished
	 */
	revertFieldLabels: function( row ) {		
		if ( !row.labelElements ) {
			return;
		}
		
		if ( this.listGridLayout === Runner.pages.constants.VERTICAL_LAYOUT ) {
			this.fNames.forEach( function( fName ) {
				var $label = row.labelElements[ fName ];
				if ( $label ) {
					$label.attr( "for", "" );
					$label.removeClass( "control-label" );
				}
			}, this );
		}	
	},

	getUniqueOrder: function( seed ) {
		seed = parseInt( seed, 10 );
		while( this.rows.find( function( row ) {
			return row.order === seed;
		} ) !== undefined ) {
			++seed;
		}
		return seed;
	},

	/**
	 * initialize the record's functionality (controls, buttons)
	 * @param {object} row
	 * @param {object} ctrlsJSON
	 */
	makeRowEditable: function( row, ctrlsJSON ) {
		var controls = ctrlsJSON.controlsMap[ this.tName ][ this.pageType ][ row.id ].controls,
			ctrlsArr = [],
			inlineObj = this;
		
		row.labelElements = row.labelElements || {};

		this.fNames.forEach( function( fName ) {
			var spanCont = Runner.getFieldSpan( fName, row.id, this.pageObj.pageCont );
			
			if ( row.isAdd ) {
				spanCont.append( ctrlsJSON['htmlControls'][ fName ] );
			} else {
				spanCont.html( ctrlsJSON['htmlControls'][ fName ] );
			}

			if ( row.isAdd && ctrlsJSON.order ) {
				row.order = inlineObj.getUniqueOrder( ctrlsJSON.order );
			}
		}, this );
		
		this.updateRowLabels( row );

		controls.forEach( function( ctrlMap ) {
			if ( this.fNames.indexOf( ctrlMap.fieldName ) !== -1 ) {
				var ctrl = Runner.controls.ControlFactory( ctrlMap, this.pageType, true, this.pageObj );		
				ctrlsArr.push( ctrl );
				
				row.labelElements[ ctrlMap.fieldName ].attr( 'for', ctrl.getDispId() );
				this.updateItemTypeSpanState( ctrlMap.fieldName, row.id, true );
			}
		}, this );		


		ctrlsArr.forEach( function( ctrl ) {
			ctrl.setDependencies( this.tName, row.id ); 
		}, this );
		
		this.fireEvent( 'createControls', row, ctrlsArr );
	},
	
	updateRowLabels: function( row ) {
		row.labelElements = row.labelElements || {};
		
		this.fNames.forEach( function( fName ) {
			var spanCont = Runner.getFieldSpan( fName, row.id, this.pageObj.pageCont );
			
			//	update label element
			row.labelElements[ fName ] = this.getFieldLabelElement( row, fName );
			
			if ( this.listGridLayout === Runner.pages.constants.VERTICAL_LAYOUT ) {
				row.labelElements[ fName ].addClass("control-label");
				row.labelElements[ fName ].parent()
					.attr('data-signal-error-for', Runner.goodFieldName( fName ) + row.id );
			
			} else if ( this.listGridLayout === Runner.pages.constants.HORIZONTAL_LAYOUT 
				|| this.listGridLayout === Runner.pages.constants.FLEXIBLE_LAYOUT ) {
				
				spanCont.closest('td')
					.attr('data-signal-error-for', Runner.goodFieldName( fName ) + row.id );
			
			} else if ( this.listGridLayout === Runner.pages.constants.COLUMNS_LAYOUT  ) {
				spanCont.closest('table').find('[data-field="' + Runner.goodFieldName( fName ) +'"]')
					.attr('data-signal-error-for', Runner.goodFieldName( fName ) + row.id );

				row.labelElements[ fName ].addClass("control-label");
			}
		}, this );
	},
	
	/**
     * Set/unset data-editcontrol attr for outter field span
	 * @param {string} fName
	 * @param {string} rowId
	 * @param {boolean} toggler
	 */
	updateItemTypeSpanState: function( fName, rowId, toggler ) {
		Runner.getFieldSpan( fName, rowId, this.pageObj.pageCont ).closest('[data-itemtype]')
			.attr( 'data-editcontrol', toggler ? '' : null );		
	},

	updateItemTypeSpanStateForCtrls: function( rowId, toggler ) {
		var ctrls = Runner.controls.ControlStorage.byId( rowId );
		ctrls.forEach( function( ctrl ) {
			this.updateItemTypeSpanState( ctrl.fieldName, rowId, toggler );
		}, this );
	},

	/**
	 * Set focus on the first record's contorl
	 * Used when record is opened for the first time
	 * @param {object} row
	 */
	setFocusOntheFirstControl: function( row ) {
		if ( this.pageObj.pageMode === Runner.pages.constants.MODE_LIST_DETAILS && 
			 Runner.pages.PageManager.getAt( this.pageObj.masterTName, this.pageObj.parId ).pageType === Runner.pages.constants.PAGE_ADD ) {
			return;
		}
		
		Runner.pages.RunnerPage.prototype.setFirstFocus.call( this.pageObj, row.id );
	},
	
	/**
	 * @param {object} row
	 * @param {boolean} hideSaveButton
	 * @return {object}
	 */
	getSaveLink: function( row, hideSaveButton ) {
		var inlineObj = this;
				
		this.pageObj.toggleItemType( 'grid_inline_save', !hideSaveButton, row.id );
		
		if ( row.saveLink ) {
			return row.saveLink;
		}
		
		row.saveLink = $( "#saveLink" + row.id )
			.removeClass('disabled');
		
		return row.saveLink.on( "click", function(e) {
			if ( row.saveLink.hasClass('disabled') ) {
				return false;
			}
			
			$('div.shiny_box').hide();
			
			row.saveLink.addClass('disabled');
			inlineObj.saveRow( row );
			inlineObj.fromSaveBtn = true;
			
			return false;
		});
	},
	
	/**
	 *
	 */
	saveRow: function( row ) {
		// flag for details recount
		row.fileFieldsCount = 0;
		row.upploadErrorHappened = false;

		this.rowFocussed = false;
		this.waitForFilesUpload( row );
	},
	
	
	/**
	 * @param {object} row
	 */
	waitForFilesUpload: function( row ) {
		var inlineObj = this, index,
			controls = Runner.controls.ControlStorage.byId( row.id );

		// mark to prevent a form from multiple saving
		row.submitting = true;
			
		for (index = 0; index < controls.length; index++) {
			if (controls[index].editFormat == Runner.controls.constants.EDIT_FORMAT_FILE
				&& controls[index].filesToUploadCount > 0) {
				row.fileFieldsCount++;
				controls[index].errorHappened = false;
				controls[index].uploadForm.on('fileuploadstopped', { ctrl: controls[index] }, function(e, data) {
					row.fileFieldsCount--;
					$(this).on('fileuploadstopped');
					if (e.data.ctrl.errorHappened) {
						row.upploadErrorHappened = true;
						inlineObj.errorHn(row);
					} else {
						inlineObj.callSaveHn( row );
					}
				});
				$(".btn-primary.start", controls[index].uploadForm).trigger("click");
			} 
		}
		
		if ( row.fileFieldsCount < 1 ) {
			inlineObj.callSaveHn( row );
		}
	},
	
	/**
	 * @param {object} row
	 */
	errorHn:function( row ) {
		if ( row.fileFieldsCount < 1 ) {
			row.saveLink.removeClass('disabled');
			row.submitting = false;
		}
	},
	
	/**
	 * @param {object} row
	 */	
	callSaveHn: function( row ) {
		if ( row.fileFieldsCount < 1 && !row.upploadErrorHappened ) {
			this.submit( row );
			this.toggleMassRecButt();
			this.resetSelectAll();
			
		}
	},
	
	/**
	 * @param {object} row
	 * @param {boolean} hideRevertButton
	 */
	getCancelLink: function( row, hideRevertButton ) {
		var inlineObj = this,
			$link;

		this.pageObj.toggleItemType( 'grid_inline_cancel', !hideRevertButton, row.id );
		if ( row.cancelLink ) {
			return row.cancelLink;
		}
		
		$link = $("#revertLink" + row.id );
		$link.on("click", function(e) {
			inlineObj.cancelButtonHn( row );
			
			if ( inlineObj.rows.length !== 0 ) {
				inlineObj.toggleMassRecButt();
			}
			
			inlineObj.resetSelectAll();
			return false;
		});
		
		return row.cancelLink = $link;
	},	
	
	/**
	 * Show the 'Save' and 'Cancel' button's in the editable row's header
	 * @param {object} row
	 * @param {boolean} hideRevertButton
	 * @param {boolean} hideSaveButton
	 */
	getEditBlock: function( row, hideRevertButton, hideSaveButton ) {		
		this.getCancelLink( row, hideRevertButton );
		this.getSaveLink( row, hideSaveButton );
	},
	
	/**
	 * @param {object} row
	 */
	revertRow: function( row ) {
		this.updateItemTypeSpanStateForCtrls( row.id, false );
		// clear controls
		Runner.controls.ControlStorage.unregisterById( row.id );
		this.fireEvent('revertRow', row);
	},
	
	/**
	 *
	 */
	removeRowData: function( row, ind ) {
		if ( row.basicForm && row.basicForm.destructor ) {
			row.basicForm.destructor();
		}
		
		if ( row.row ) {
			row.row.remove();
		}
		if ( row.srow ) {
			row.srow.remove();
		}
		
		if ( this.rows.length === 1 ) {
			// hide the grid table
			this.pageObj.hideElement("grid");
			$("thead tr, tr.footer, tr." + this.classNameBottomRow, this.pageObj.gridElem).addClass( this.classNameHidden );

			this.toggleRecordControlButtons( false );
		}
		
		this.rows.splice( ind || this.getRowInd( row ), 1 );
		this.removeGridRowData( row.id );
	},
	
	/**
	 * Remove grid row data
	 * @param {number} rowId
	 */
	removeGridRowData: function( rowId ) {		
		for (var i = 0; i < this.rowsOrderData.length; i++) {
			if ( this.rowsOrderData[ i ].id == rowId ) {
				break;
			}
		}
		
		this.rowsOrderData.splice( i, 1 );
	},
	
	/**
	 * Get the row index in the rows array
	 * @param {object} row
	 * @return {number}
	 */
	getRowInd: function( row ) {
		for (var i = 0; i < this.rows.length; i++) {
			if ( this.rows[i].id === row.id ) {
				return i;
			}
		}
		return -1;
	},
	
	/**
	 * @param {number} rowId
	 * @return {object}
	 */
	getRowById: function( rowId ) {
		for (var i = 0; i < this.rows.length; i++) {
			if ( this.rows[i].id === parseInt( rowId, 10 ) ) {
				return this.rows[i]; 
			}
		}
		return null;
	},
	
	/**
	 * Get a row's order index
	 * @param {number}
	 * @return {number}
	 */
	getRowOrderIndex: function( rowId ) {
		for (var i = 0; i < this.rowsOrderData.length ;i++) {
			if ( this.rowsOrderData[i].id === rowId ) {
				return i;
			}
		}
		return -1;
	},
	
	/**
	 * Get a row's keys by its order index
	 * @param {number}
	 * @return {array}
	 */
	getRowKeysByOrderIndex: function( ordInd ) {
		if ( this.rowsOrderData[ ordInd ] !== undefined ) {
			return this.rowsOrderData[ ordInd ].keys;
		}

		return [];		
	},
	
	/**
	 * Get a row's id by its order index	
	 * @param {number}
	 * @return {number}
	 */
	getRowIdByOrderIndex: function( ordInd ) {
		if ( this.rowsOrderData[ ordInd ] !== undefined ) {
			return this.rowsOrderData[ ordInd ].id;
		}

		return -1;		
	},
	
	/**
	 * @return {object}
	 */
	getBottomRow: function() {
		if ( !this.rows.length ) {
			return null;
		}
		
		return this.getRowById( this.rowsOrderData[ this.rows.length - 1 ].id );
	},
	
	/**
	 * @param {string} contextRowId
	 * @return {object}
	 */	
	getRowTrByContextId: function( contextRowId ) {
		return $('#' + this.rowPref + contextRowId ).eq(0);
	},	
	
	/**
	 * Show the row's 'Error ocirred >>' link that 
	 * show the error message on 'mouseover' 
	 * and hide it on 'mouseout' 	
	 * @param {string} msg
	 * @param {object} row
	 */
	makeError: function( msg, row ) {
		row.errorMess = msg;

		if ( !this.pageObj.gridElem ) {
			return;
		}

		if ( !row.errorDiv ) {
			var inlineObj = this,
				//get the first span in a row
				$span = $("span[id^=edit" + row.id + "_]:eq(0)", this.pageObj.gridElem),
				$errorLink = $('<a href="javascript:void(0)" id="error_' + row.id + '" style="white-space:nowrap;"></a>').html( Runner.lang.constants.INLINE_ERROR + ' >>' );
				
			row.errorDiv = $("<div></div>")
				.addClass( this.classNameInlineError )
				.append( $errorLink )
				.appendTo( $span );
						
			
			if ( !this.errCont ) {	
				this.errCont = $("<div></div>");
				this.errCont.addClass( "alert alert-danger" );
				
				this.errCont.appendTo( document.body )
					.hide();
			}
			
			$errorLink
				.on("mouseenter", (e) => {
					var errorPos = $errorLink.offset(),
						topPos = errorPos.top,
						adjustment;
						
					topPos += $errorLink.height();
					
					adjustment = $(document).height() - topPos - inlineObj.errCont.height();
					// check if an error is showing outside of the document
					if ( adjustment < 0 ) {
						// move the error container to a better place to make it possible to read it in full
						errorPos += adjustment;
					}
					this.errCont.html( msg );
					
					inlineObj.errCont.css({
							"top": topPos + "px",
							"left": errorPos.left + $errorLink.width() + "px",
							"z-index": 1100,
							"position": "absolute"
						})
						.show();
				})
				.on("mouseleave", function(e) {
					inlineObj.errCont.hide();
				});
		}
		
	},
	
	/**
	 * Remove the row's error container
	 * @param {object} row
	 */
	clearError: function( row ) {
		delete row.errorMess;
		if ( row.errorDiv ) {
			row.errorDiv.remove();
			delete row.errorDiv;
		}
	},

	/**
	 * Update a table row with new values after the record is added or edited.
	 * ( on the row's form successSubmit event or add popup was saved )
	 * @param {object} row			An inline row object
	 * @param {object} newData
	 */
	updateTableRow: function( row, newData ) {
		var checkBoxVals, i, $rowElement;
		
		row.revertted = false;
		// add new data from server to row object
		row.data = Runner.apply( {}, newData.vals ); //shown data in grid
		
		if ( !newData.fNamesSelected ) {
			row.keys = newData.keys;
			row.keyFields = newData.keyFields;
		}

		row.order = newData.order;
		row.rowVals = newData.rawVals; // data from BD
		 
		// update rows order data
		for ( i = 0; i < this.rowsOrderData.length; i++ ) {
			if ( this.rowsOrderData[i].id === row.id ) {
				this.rowsOrderData[i].keys = newData.keys;
				break;
			}
		}
		// proccess checkbox
		if ( row.checkBox && row.checkBox.length && !newData.noKeys && !newData.fNamesSelected ) {
			checkBoxVals = [];
			
			for ( i = 0; i < newData.keys.length; i++ ) {
				checkBoxVals.push( newData.keys[i] );
			}

			row.checkBox
				.val( checkBoxVals.map( encodeURIComponent ).join( "&" ) )
				.prop("checked", false)
				.show();			
		}
		
		if ( !newData.noKeys && !newData.fNamesSelected  ) {
			this.changeLinksKeys( row );
		}
	
		this.checkRowMasterKeys( row, newData.detKeys );
		
		if ( newData.hrefs && !this.dashGridBased ) {
			for (i = 0; i < newData.hrefs.length; i++) {
				var idLink = "#details_" + row.id + "_" + newData.hrefs[i].id;
				$(idLink).attr("href", newData.hrefs[i]["href"]);
			}
		}		
		
		// add values to lookupVals
		if ( this.lookupParams && this.lookupFieldInfo && newData.linkValue !== undefined ) {
			newData.vals[ this.lookupFieldInfo.linkField ] = newData.linkValue;
			newData.vals[ this.lookupFieldInfo.dispFieldAlias || this.lookupFieldInfo.dispField ] = newData.displayValue;
		}
		
		if ( !this.spreadsheet ) {
			// change row to simple grid row with no editBoxes
			this.setValuesIntoSpans( row, newData.fNamesSelected );
			this.revertFieldLabels( row );
		}

		this.toggleRowEditingButtons( row, newData.noKeys !== true );
		this.hideSaveCancelButtons( row );	
		
		//	highlight edited record
		$rowElement = $("#gridRow" + row.id )
			.removeClass("recently-added")
			.addClass("just-added");

		Runner.pages.RunnerPage.prototype.initViewLinks.call( this.pageObj, $rowElement  );
			
		setTimeout(function() {
			$rowElement
				.removeClass("just-added")
				.addClass("recently-added");
		}, 10 );
				
		// set submitted attr
		row.submitted = true;
		if ( row.isAdd ) {
			row.isAdd = false;
		}
	 },
	

	
	/**
	 * A handler that is invoked after an inline row is submitted
	 * ( on the row's form successSubmit event or add popup was saved )
	 * @param {object} row			An inline row object
	 * @param {object} newData
	 * @param {object} extPageObj		A reference to a pop-up or dash page object (optional). 
	 */
	afterSubmit: function( row, newData, extPageObj ) {		
		this.fireEvent("beforeProcessNewRow", row, newData.vals, newData.fields, newData.keys);
		
		if ( !this.spreadsheet ) {
			this.updateItemTypeSpanStateForCtrls( row.id, false );
			// delete controls
			Runner.controls.ControlStorage.unregisterById( row.id );
		}
		
		this.updateTableRow( row, newData );
		
		this.fireEvent('afterSubmit', newData.vals, newData.fields, newData.keys, row.id, newData, extPageObj, row );
		
		if ( row.basicForm ) {
			setTimeout( function() {
				row.basicForm.destructor();
				row.basicForm = null;
			}, 0);
		}		
		
		if ( this.rows.length === 1 ) {
			this.pageObj.hideElement("message");
		}
		
		if ( Runner.pages.PageSettings.getGlobalData("s508") ) {
			this.pageObj.s508Obj.s508setFocusToRow(row.id);		
		}
	},
	
	/**
	 * @param {Object} row
	 */	
	fireUserEventAfterInline: function( row ) {
		var fieldsData = [], i;
		//make fileds data for user event
		for (i = 0; i < this.fNames.length; i++) {
			fieldsData[i] = {
				name: this.fNames[i],
				value: row.rowVals[ this.fNames[i] ],
				text: row.data[ this.fNames[i] ],
				container: Runner.getFieldSpan( this.fNames[i], row.id )
			};
		}
		//fire after save
		this.fireEvent("afterSave", fieldsData, new Runner.AjaxRow( this.pageObj, row ));
		//set user data to row
		for (i = 0; i < this.fNames.length; i++) {
			row.rowVals[ this.fNames[i] ] = fieldsData[i].value;
			row.data[ this.fNames[i] ] = fieldsData[i].text;
		}
	},
	
	/**
	 * @param {Object} row
	 * @params {array} fNames (optional)
	 * @return {Object}
	 */
	setValuesIntoSpans: function( row, fNames ) {
		var inlineObj = this,
			fName, pos, fSpan, viewFormat, i, j;
		
		fNames = fNames || this.fNames;
		
		this.fireEvent("beforeSetVals", row, this.fNames);
		
		if ( !row.revertted ) {
			this.fireUserEventAfterInline( row );
		}
		
		for (i = 0; i < fNames.length; i++) {
			fName = fNames[ i ];
			
			if ( typeof row.data[ fName ] === "undefined" ) {
				row.data[ fName ] = "";
				pos = $.inArray( fName, row.keyFields );
				if ( pos !== -1 ) {
					row.data[ fName ] = row.keys[ pos ].toString();
				}
			}
			
			fSpan = Runner.getFieldSpan( fName, row.id ) 
				.html( row.data[ fName ] );
			
			for (j = 0; j < this.totalFields.length; j++) {
				if ( this.totalFields[j].fName === fName ) {
					fSpan.attr('val', row.rowVals && row.rowVals[ fName ] ? row.rowVals[ fName ] : "");
					break;
				}
			}
			
			viewFormat = Runner.pages.PageSettings.getFieldData(this.tName, fName, 'viewFormat', this.pageType);
			if ( viewFormat !== Runner.controls.constants.FORMAT_MAP && viewFormat !== Runner.controls.constants.FORMAT_FILE_IMAGE ) {
				fSpan.find('img').each( function( index, image ) {
					var src = $(image).attr( 'src' );
					$(image).attr( 'src', inlineObj.replaceRndVal( src ) );
				});	
				fSpan.find('a').each( function( index, link ) {
					var href = $(link).attr( 'href' );
					$(link).attr( 'href', inlineObj.replaceRndVal( href ) );
				});					
			}
		}
		
		this.initRowViewControls( row );
		
		return row.data;
	},
	
	/**
	 * Init view updated field's view controls
	 * @param {Object} row
	 */
	initRowViewControls: function( row ) {
		if ( this.pageObj.viewControlsMap ) {
			//this.pageObj.setupViewContols( this.pageObj.viewControlsMap.controls, $('#' + this.rowPref + row.contextRowId) );	
			this.pageObj.setupViewContols( this.pageObj.viewControlsMap.controls, this.pageObj.pageCont );	
		}	
	},
	
	/**
	 * @param {string} src
	 * @return {string}
	 */
	replaceRndVal: function( src ) {
		if (!src) {
			return "";	
		}
		
		var rndValPos = src.indexOf('rndVal='),
			endPos;
			
		if (rndValPos === -1) {
			return src + (src.indexOf('?') != -1 ? "&" : "?") + "rndVal=" + Math.random();
		}
		endPos = src.indexOf('&', rndValPos);
		return src.slice(0, rndValPos + 7) + Math.random() + (endPos !== -1 ? src.slice(endPos) : '');
	},
	
	/**
	 * Fill up the row's 'data' object with html content of row fields' spans
	 * @param {object} row
	 */
	getValuesFromSpan: function( row ) {
		var fSpan;
		
		row.data = {};
		
		for (var i = 0; i < this.fNames.length; i++) {
			fSpan = Runner.getFieldSpan( this.fNames[i], row.id );
			if ( !fSpan.length ) {
				continue;
			} 
			
			$('.video-js', fSpan).each( function(j, videoElement) {
				var vElementId = $(videoElement).attr("id");
				var player = videojs( vElementId );
				
				$(fSpan.find('video').get(j)).attr({
					"src": player.currentSrc(),
					"type": player.currentType()
				});
				
				player.pause();
				player.dispose();
			});

			row.data[ this.fNames[i] ] = fSpan.html();
		}
	},
	
	/**
	 * avoid using this
	 * @param {object} row
	 * @param {string} fName
	 * @return {string}
	 */		
	getValueFromSpan: function( row, fName ) {
		if ( row.rowVals && row.rowVals[ fName ] ) {
			return row.rowVals[ fName ];
		}
		
		if ( this.spreadsheet ) {
			var ctrl = Runner.getControl( row.id, fName );
			if ( ctrl ) {
				return ctrl.getStringValue() || "";
			}
		}
		
		var span = Runner.getFieldSpan( fName, row.id ),
			val = span.attr('val');
		
		return !val && row.data[ fName ] != null ? row.data[ fName ] : val;
	},
	
	/**
	 * @param {jQuery object} $link
	 * @param {object} row
	 * @return {jQuery object}
	 */	
	initCopyLink: function( $link, row ) {
		var inlineObj = this;
		$link
			.filter( function( i, elem ) { 
				return inlineObj.pageObj.showAddInPopup( $(elem) );
			})
			.on( "click", function(e) {
				var $link = $(this);
				
				var pageParams = {
					tName: inlineObj.tName, 
					pageType: Runner.pages.constants.PAGE_ADD, 
					modal: true, 
					keys: row.keys,
					keyFields: row.keyFields,
					keyPref: "copyid",
					baseParams: {
						parId: inlineObj.id,
						table: inlineObj.tName,
						editType: Runner.pages.constants.ADD_POPUP
					},
					afterSave: {
						fn: function( respObj, formObj, fieldControls, page ) {
							if ( respObj.success ) {
								var addedRow = this.pageObj.inlineAdd.addRowToGrid( respObj, true );
								this.pageObj.initNewRowButtons && this.pageObj.initNewRowButtons( addedRow );
								return true;		
							} 
							return false;				
						},
						scope: inlineObj
					}
				};
				
				if ( $link.data("page") ) {
					pageParams.baseParams["page"] = $link.data("page");
				}
				
				pageParams.baseParams = Runner.apply( pageParams.baseParams, inlineObj.pageObj.getStateParams() );
				
				pageParams.popupWidth = $link.data("popupwidth");
				pageParams.popupHeight = $link.data("popupheight");
				
				Runner.pages.PageManager.openPage( pageParams );
				return false;
			});

		return $link;
	},
	
	/**
	 * Initialize the row's checkbox
	 * @param {jQuery object} $checkbox
	 * @return {jQuery object}
	 */	
	initCheckBox: function( $checkbox ) {		
		var $checkBoxCell = $checkbox.parents("[data-checkbox-cell]");
		
		if ( !$checkBoxCell.length ) {
			$checkBoxCell = $checkbox.parent()
		}

		$checkBoxCell.on( "click", function( e ) {
			if ( !$checkbox.is( e.target ) && $checkbox.is(":visible") ) {
				$checkbox.trigger( "click" );
			}
			//	prevent triggering ClickAction
			e.stopPropagation();
		});
		
		return $checkbox;
	},
	
	/**
	 * @param {object} row	An inline row object
	 * Update 'href' attributes for a record's links
	 */		
	changeLinksKeys: function( row ) {
		var state = this.pageObj.getStateParams();
		$( row.viewLink ).attr('href', Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_VIEW, row.keys, 'editid', state ) );	
		$( row.editLink ).attr('href', Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_EDIT, row.keys, 'editid', state ) );
		$( row.copyLink ).attr('href', Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_ADD, row.keys, 'copyid', state ) );
	},

	/**
	 * @param {jQuery object} $link
	 * @param {object} row
	 * @return {jQuery object}
	 */	
	initViewLink: function( $link, row ) {
		var self = this;
		$link
			.filter( function( i, elem ) { 
				return self.pageObj.showViewInPopup( $(elem) );
			})
			.on( "click", self.pageObj.getViewLinkHandler( row, 'popup' ) );
			
		return $link;
	},
	
	
	/**
	 * @param {jQuery object} $link
	 * @param {object} row
	 * @return {jQuery object}
	 */	
	initEditLink: function( $link, row ) {
		var self = this;
		$link
			.filter( function( i, elem ) { 
				return self.pageObj.showEditInPopup( $(elem) );
			})
			.on( "click", self.pageObj.getEditLinkHandler( row, 'popup' ) );
			
		return $link;	
	},
	
	/**
	 * @param {jQuery object} $link
	 * @param {object} row
	 * @return {jQuery object}
	 */
	initInlineEditLink: function( $link, row ) {
		var newUrl = Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_EDIT, row.keys, 'editid', this.pageObj.getStateParams() ),
			inlineObj = this;
		
		return $link.attr('href', newUrl)
			.on("click", function(e) {		
				( inlineObj.inlineEditObj || inlineObj ).inlineEdit( row );
				return false;
			});
	},
	
	/**
	 * Toggle the 'Save all' and 'Cancel all' buttons
	 */
	toggleMassRecButt: function() {
		this.pageObj.onAfterChangeCheckboxsState();
		this.massRecButtEditMode = this.isRowsEditing();
		Runner.toggleButton( this.saveAllButt, this.massRecButtEditMode );
		Runner.toggleButton( this.cancelAllButt, this.massRecButtEditMode );
	},
	
	/**
	 * Check if thre are some unsubmitted rows
	 * @return {boolean}
	 */
	isRowsEditing: function() {
		for (var i = 0; i < this.rows.length; i++) {
			if ( this.rows[i].submitted !== undefined && !this.rows[i].submitted && !this.rows[i].skipSubmit ) {
				return true;
			}
		}
		return false;
	},
	
	/** 
	 * Validate the rows controls
	 * Used for inline add/edit records on detail page
	 * @param {boolean} modifiedOnly
	 * @return {boolean}
	 */
	validate: function( modifiedOnly ) {
		var invalidControls = [];
		
		this.rowFocussed = false;
		
		for (var i = 0, l = this.rows.length; i < l; i++) {
			if ( modifiedOnly && !this.rows[i].modified ) {
				continue;
			}
			
			invalidControls = this.pageObj.getInvalidControls( Runner.controls.ControlStorage.byId( this.rows[i].id ) );
			
			if ( invalidControls.length && ( this.rows[i].isAdd || !this.rowFocussed ) ) {				
				if ( this.pageObj.setFirstFocus(this.rows[i].id, invalidControls) ) {
					this.rowFocussed = true;
				}
			}
		}
		
		return !this.rowFocussed;
	},
	
	/**
	 * Add new row to grid
	 * Used after copy record
	 * @param {object} data 
	 * @param {boolean} fireEvents
	 * @param {number} newRowId
	 */
	addRowToGrid: function( data, fireEvents, newRowId ) {
		var newAddRow = this.prepareRow( data.vals, true, newRowId );
		this.getEditBlock( newAddRow );
		
		if ( fireEvents ) {
			this.afterSubmit( newAddRow, data );
		} else {
			this.updateTableRow( newAddRow, data );
		}
		
		return newAddRow;
	},
	
	/**
	 * Initialize the row object
	 * @param {object} row
	 */
	initRow: function( row ) {
		row.submitted = true;
		row.revertted = false;
		row.data = {};
		row.saveLink = false;
		row.cancelLink = false;
		
		row.checkBox = this.initCheckBox( $('[id=check' + this.id + '_' + row.id + ']') );
		row.copyLink = this.initCopyLink( $('[id=copyLink' + row.id + ']'), row );
		row.editLink = this.initEditLink( $('[id=editLink' + row.id + ']'), row );
		row.iEditLink = this.initInlineEditLink( $('[id=iEditLink' + row.id + ']'), row );
		row.viewLink = this.initViewLink( $('[id=viewLink' + row.id + ']'), row );
	},
	
	hideRowButtons: function( row ) {
		row.viewLink.hide();
		row.copyLink.hide();
		row.editLink.hide();
		row.iEditLink.hide();
		row.checkBox.hide();
	},
	
	hideSaveCancelButtons: function( row ) {
		this.pageObj.toggleItemType( 'grid_inline_save', false, row.id );
		this.pageObj.toggleItemType( 'grid_inline_cancel', false, row.id );

		//	don't remove the DOM element
		if ( row.cancelLink ) {
			row.cancelLink.off('click');
			delete row.cancelLink;
		}
		
		if ( row.saveLink ) {
			row.saveLink.off('click');
			delete row.saveLink;
		}
	},	
	
	showRowButtons: function( row ) {
		this.toggleRowEditingButtons( row, true );
		this.hideSaveCancelButtons( row );
	},
	
	toggleRowEditingButtons: function( row, toggler ) {
		row.viewLink && row.viewLink.toggle( toggler );
		row.copyLink && row.copyLink.toggle( toggler );
		row.checkBox && row.checkBox.toggle( toggler );
		
		if ( row.isEditable ) {
			row.editLink && row.editLink.toggle( toggler );
			row.iEditLink && row.iEditLink.toggle( toggler );
		}		
	},
	
	/**
	 * Show or hide recordcontrols briks's buttons 
	 * basing on toggler value
	 * @param {boolean} toggler
	 */
	toggleRecordControlButtons: function( toggler ) {
		Runner.toggleButton( this.editAllButt, toggler );
		Runner.toggleButton( this.saveAllButt, toggler );
		Runner.toggleButton( this.cancelAllButt, toggler );
		
		Runner.toggleButton( $("[id=print_selected" + this.id + "]"), toggler );
		Runner.toggleButton( $("[id=export_selected" + this.id + "]"), toggler );
		Runner.toggleButton( $("[id=delete_selected" + this.id + "]"), toggler );
		Runner.toggleButton( $("[id=update_selected" + this.id + "]"), toggler );
		
		this.pageObj.toggleElement("recordcontrol", toggler);		
	},
	
	/**
	 * Clone the 'gridRowAdd' row and its separator and add them to
	 * the top of the grid element
	 * @param {object} vals
	 * @param {boolean} submitted
	 * @return {object}
	 */
	prepareRow: function( vals, submitted, newRowId ) {
		var grid = this.pageObj.gridElem,
			$addRow,
			$sepAddRow = $(".gridRowSepAdd", grid).eq(0), // support layout version <= 2
			newRowId = newRowId || Runner.genId(),
			pageId = this.id,
			newAddRow;
		
		$addRow = $(".gridRowAdd[data-pageid=" + this.id + "]", grid) // record row and details in horizontal
				.add(grid.children("[data-record-id=add]")); // details in vertical or column

		if ( this.rows.length === 0 ) {
			// show the grid
			this.pageObj.showElement("grid");	
			$("thead tr, tr.footer, tr." + this.classNameBottomRow, grid).removeClass( this.classNameHidden );

			this.toggleRecordControlButtons( true );
		}
		
		newAddRow = {
			row: $addRow.clone( true ),
			srow: false,
			id: newRowId,
			keys: [],
			keyFields: [],
			data: vals,
			isAdd: true,
			isEditable: true,
			contextRowId: newRowId,
			gridLayout: this.listGridLayout
		};
		if ( $sepAddRow.length ) {
			newAddRow.srow = $sepAddRow.clone();
			newAddRow.srow.removeClass('gridRowSepAdd ' + this.classNameHidden);
		}
		
		this.rows.push( newAddRow );
		if ( this.pageObj.addNewRecordsToBottom ) {
			this.rowsOrderData.push( {
				id : newRowId, 
				keys : []
			} );
		} else {
			this.rowsOrderData.unshift( {
				id : newRowId,				
				keys : []
			} );
		}
		
		this.replaceElementsIdForAddedRow( newAddRow, pageId );
		
		newAddRow.row.removeClass('gridRowAdd ' + this.classNameHidden);
		newAddRow.row.attr( 'data-hidden', null );
		$(".gridRowAdd", newAddRow.row).removeClass('gridRowAdd ' + this.classNameHidden);
		
		newAddRow.row.attr('data-record-id', newAddRow.id );
		newAddRow.row.find('[data-record-id=add]').attr('data-record-id', newAddRow.id );
		if ( this.listGridLayout !== Runner.pages.constants.HORIZONTAL_LAYOUT ) {
			newAddRow.row.find('.panel').attr('data-record-id', newAddRow.id );
		}
		
		
		newAddRow.row.attr("id", this.rowPref + newAddRow.id);	
			
		if ( newAddRow.srow ) {
			if ( this.pageObj.addNewRecordsToBottom  ) {
				newAddRow.srow.appendTo( $sepAddRow.parent().siblings().last() );
				newAddRow.row.appendTo( $sepAddRow.parent().siblings().last() );
			} else {
				newAddRow.srow.insertAfter( $sepAddRow );
				newAddRow.row.insertAfter( $sepAddRow );
			}
		} else {	
			if ( this.pageObj.addNewRecordsToBottom  ) {
				newAddRow.row.appendTo( $addRow.last().parent() );
			} else {
				newAddRow.row.insertAfter( $addRow.last() );
			}
		}
	
		// add extra properties to the newAddRow object such as 'checkBox', 'viewLink' and so on
		( this.inlineEditObj || this ).initRow( newAddRow );

		newAddRow.submitted = submitted;
		return newAddRow;
	},
	
	/**
	 * Replace the temp id values for some DOM elements within the added row
	 * @param {object} newAddRow
	 * @param {number} pageId
	 */
	replaceElementsIdForAddedRow: function( newAddRow, pageId ) {

			var suffixLength = ("_add" + pageId).length;
			$("[id$=_add" + pageId + "]", newAddRow.row).each( function () {
				var $elem = $(this);
				var strId = $elem.attr('id');
				var idName = strId.substr(0, strId.length - suffixLength );
				
				if ( idName === 'check' ) {
					$elem.attr('id', "check" + pageId + "_" +  newAddRow.id );
				} else if ( idName === 'details' ) {
					return;
				} else {
					$elem.attr('id', idName +  newAddRow.id );
				}
				/*
				if ( idName !== 'saveLink' && idName !== 'revertLink' )
					$elem.hide(); 
				else
					$elem.parent().css('display', 'inline-block'); 
				*/					
			});

		$("[id^='cntDet']", newAddRow.row)
			.each( function( idx, el ) {
				var $el = $(el);	
				$el.attr("id", $el.attr("id") + newAddRow.id );
			})
			.hide();
			//.filter(":not(.badge)").hide(); remove filter for Issue #12136
		
		$("[id$='_preview" + pageId + "']", newAddRow.row).hide().each( function() {
			var /*cntDet,*/
				reg = new RegExp( pageId + '$' );
			
			this.id = this.id.replace( reg, newAddRow.id );
			/*cntDet = $('span[id^=cntDet]', this);
			if ( cntDet.length ) {
				cntDet.attr('id', cntDet.attr('id') + newAddRow.id);
			}*/
		});
		
		$("[id^='details_add" + pageId + "_']", newAddRow.row).each( function( idx, el ) {
			var $el = $(el);
			$el.attr("id", $el.attr("id").replace( "details_add" + pageId + "_", "details_" + newAddRow.id + "_" ));
		});
		
		$("[id^='details_'][id$='_add" + pageId + "']", newAddRow.row).hide().each( function() {
			var reg = new RegExp( 'add' + pageId + '$' );
			this.id = this.id.replace( reg, newAddRow.id );
		});
		
		$("[id^='add" + pageId + "_']", newAddRow.row).each( function() {
			var reg = new RegExp( '^add' + pageId );
			this.id = this.id.replace( reg, 'edit' + newAddRow.id ); 
		});
	},
	
	/**
	 * checkRowDetailKeys
	 * Check new and existent row masterKeys and update details count and popup links
	 * @param {object} changed row
	 * @param {object} new master keys
	 */
	checkRowMasterKeys: function( row, newMasterKeys ) {
		if ( !this.pageObj.dpObjs || !newMasterKeys || $.isEmptyObject( newMasterKeys ) ) {
			return;
		}
		
		var mKeysAdded = false;
		if ( row.masterKeys == undefined ) {
			row.masterKeys = newMasterKeys;
			mKeysAdded = true;
		}		
		
		$.each( this.pageObj.dpObjs, function( ind, obj ) {
			var isKeysChanged = false;
			
			if ( !mKeysAdded ) {
				$.each( newMasterKeys[ obj.tName ], function( kInd, key ) {
					if ( key != row.masterKeys[ obj.tName ][ kInd ] ) {
						isKeysChanged = true;
						return false;
					}
				});
			}
			
			if ( mKeysAdded || isKeysChanged ) {
				obj.updateChildRecNum( newMasterKeys[ obj.tName ], row.id );
				row.masterKeys[ obj.tName ] = newMasterKeys[ obj.tName ];
				obj.closeDetailsById( row.id );
			} else {
				obj.updateChildRecNum( row.masterKeys[ obj.tName ] );
			}
		});
	},
	
	/**
	 * A stub
	 * @param {object} row
	 * @param {object} ctrlJSON
	 */
	lockRow: function( row, ctrlsJSON ) {
	}	
});

/**
 * @class Runner.util.inlineEditing.InlineAdd
 */
Runner.util.inlineEditing.InlineAdd = Runner.extend( Runner.util.inlineEditing.InlineEditor, {	
	
	pageType: Runner.pages.constants.PAGE_ADD,

	/**
	 * It indicates if to show inline row without the Save button
	 */
	hideSaveButton: false,
	
	hideRevertButton: false,
	
	
	constructor: function( cfg ) {
		Runner.util.inlineEditing.InlineAdd.superclass.constructor.call( this, cfg );				
		this.addEvents("beforeSetVals", "beforeInlineAdd");
	},
	
	init: function() {
		Runner.util.inlineEditing.InlineAdd.superclass.init.call(this);
	},

	/**
	 * Get submit URL
	 * @return {string}
	 */
	getSubmitUrl: function() {
		return Runner.util.inlineEditing.InlineAdd.superclass.getSubmitUrl.call( this ) + "?submit=1&inline=1&";
	},	
	
	initButtons: function() {
		Runner.util.inlineEditing.InlineAdd.superclass.initButtons.call( this );	
		this.initAddButton();
	},
	
	/**
	 * Reinitialize the 'Save all', 'Cancel' buttons.
	 */
	reinitButtons: function() {
		Runner.util.inlineEditing.InlineAdd.superclass.initButtons.call( this );
	},	
	
	/**
	 * Initialize the 'Inline Add' button
	 */
	initAddButton: function() {
		var inlineAddObj = this;
		
		$("[id=inlineAdd" + this.id + "]")
			.off("click")
			.on("click", function(e) {
				inlineAddObj.inlineAdd( inlineAddObj.hideSaveButton, inlineAddObj.hideRevertButton );
				return false;
			});
	},	
	
	cancelButtonHn: function( row, isCancelAll ) {
		row.revokeCancel = false;
		
		if ( typeof row.onBeforeCancel != "undefined" ) {
			row.onBeforeCancel();
		}
		if ( row.revokeCancel ) {
			return false;
		}
		this.removeRowData( row );
		if ( !isCancelAll ) {
			this.fireEvent('recalcGridSize');
		}
		
		Runner.util.inlineEditing.InlineAdd.superclass.cancelButtonHn.call(this);
		
		if ( typeof row.onCancel != "undefined" ) {
			row.onCancel();
		}
			
		this.fireEvent('cancel', row, this.fNames);
		this.pageObj.fireEvent( "afterInlineCancel" );
		
		this.updateItemTypeSpanStateForCtrls( row.id, false );
		Runner.controls.ControlStorage.unregisterById( row.id );
		return true;
	},
	
	revertRow: function( row ) {
		Runner.util.inlineEditing.InlineAdd.superclass.revertRow.call(this, row);
		// clear row data from memory
		this.removeRowData( row );

	},
	
	/**
	 * Save all Inline Add grid's rows
	 * @param {boolean} modifiedOnly	 
	 *
	 * Go in reverse order because:
	 *		all added rows set in the end of array this.rows 
	 *		set focus to invalid control on last added row
	 */	
	saveAll: function( modifiedOnly ) {
		if ( Runner.util.inlineEditing.InlineAdd.superclass.saveAll.call( this, modifiedOnly ) ) {
			return true;
		}
		
		for ( var i = this.rows.length; i--; ) {			
			if ( this.rows[i] && !this.rows[i].submitted && this.rows[i].isAdd && !this.rows[i].submitting ) {
				if ( !modifiedOnly || this.rows[i].modified ) {
					this.rows[i].fileFieldsCount = 0;
					this.rows[i].upploadErrorHappened = 0;
					delete this.rows[i].skipSubmit;
					
					this.waitForFilesUpload( this.rows[i] );
				} else {
					this.rows[i].skipSubmit = true;
				}
			}
		}
		
		// to fire rowsEdited event for detail inline object in case only empty 'InlineAdd' rows were open
		Runner.util.inlineEditing.InlineAdd.superclass.saveAll.call( this, modifiedOnly );
	},
	
	fireUserEventAfterInline: function( row ) {
		if ( this.pageObj.isEventExist('afterInlineAdd') ) {
			Runner.util.inlineEditing.InlineEdit.superclass.fireUserEventAfterInline.call(this, row);
		}
	},
	
	/**
	 * Assign the row's basicForm property with the Runner BasicForm object reference
	 * or if it's alredy assigned update its properties
	 * @param {object} row
	 */		
	initForm: function( row ) {
		Runner.util.inlineEditing.InlineAdd.superclass.initForm.call( this, row );
		
		row.basicForm.baseParams.order = row.order;
		if ( this.lookupParams ) {
			row.basicForm.baseParams["forLookup"] = true;
			row.basicForm.baseParams["table"] = this.lookupParams.table;
			row.basicForm.baseParams["field"] = this.lookupParams.field;			
			row.basicForm.baseParams["pageType"] = this.lookupParams.pageType;
		}		
	},
	
	/**
	 * A handler that is invoked after an inline add row is submitted
	 * ( on the row's form successSubmit event of add popup was saved)
	 * @param {objetc} row
	 * @param {newData} newData
	 * @param {newData} extAddPageObj 	A reference to a pop-up or dash add page (optional)
	 */	 
	afterSubmit: function( row, newData, extAddPageObj ) {
		Runner.util.inlineEditing.InlineAdd.superclass.afterSubmit.call(this, row, newData, extAddPageObj);
		
		if ( row.tempContainer ) {
			row.tempContainer.remove();
			row.tempContainer = null;
		}

		//	for compatibility only, use canEditRecord, canDeleteRecord instead
		row.isEditOwnRow = true;

		if ( !!row.isEditable ) {
			row.canEditRecord = true;
			row.canDeleteRecord = true;
		}
		
		if ( this.pageObj.parId ) {
			Runner.pages.PageManager.getById( this.pageObj.parId ).setRecountFlagForPopup();
		}
		
		// fire rowsEdited event if all rows submited
		var allVals = [],
			allKeys = [],
			allRowIds = [],
			selAllHid = $("#select_all" + this.id + ":hidden");
			
		if ( typeof row.fromSaveBtn == "undefined" && !this.spreadsheet ) {
			for (var i = 0; i < this.rows.length; i++) {
				allVals.push(this.rows[i].data);
				allKeys.push(this.rows[i].keys);
				allRowIds.push(this.rows[i].id);
				if ( this.rows[i].submitted === false && !this.rows[i].skipSubmit ) {
					this.pageObj.isDetailsRecountNeeded = true;
					return;
				}
			}
		}
		
		if ( selAllHid.length ) {
			selAllHid.show();
		}
		
		this.fireEvent('rowsEdited', allVals, newData.fields, allKeys, allRowIds, false, row.fromSaveBtn);
		this.pageObj.initHeaderCheckBox();
		row.fromSaveBtn = undefined;
	},
	
	/**
	 * Toggle the 'Save all', 'Cancel all', 'Edit all' buttons
	 */
	toggleMassRecButt: function() {
		Runner.util.inlineEditing.InlineAdd.superclass.toggleMassRecButt.call( this );
		
		if ( !this.isRowsEditing() && this.rows.length ) {
			Runner.toggleButton( this.editAllButt, !this.massRecButtEditMode );
		}
	},
	
	/**
	 * Create a new 'Inline Add' row and add it to the page's grid element
	 * @param {boolean} hideSaveButton
	 * @param {boolean} hideRevertButton
	 * @param {boolean} skipFocus
	 * @return {number} New record id
	 */	 
	inlineAdd: function( hideSaveButton, hideRevertButton, skipFocus ) {
		this.fireEvent("beforeInlineAdd");
	
		var newAddRow = this.prepareRow( {}, false );
		
		this.toggleMassRecButt();
		this.hideRowButtons( newAddRow );
		this.getControls( this.pageObj, newAddRow, hideRevertButton, hideSaveButton, skipFocus );
		
		this.pageObj.hideElement('message');
		
		return newAddRow.id;
	},
	
	/**
	 * Show the cancel button for the Inline Add row
	 * @param {object} row
	 */
	showCancelButton: function( row ) {
		this.inlineAddChangeContent = true;
		this.getEditBlock( row, false, true );
	}
});

/**
 * @class Runner.util.inlineEditing.InlineEdit
 */
Runner.util.inlineEditing.InlineEdit = Runner.extend( Runner.util.inlineEditing.InlineEditor, {	
	
	pageType: Runner.pages.constants.PAGE_EDIT,
	
	/**
	 * @type {boolean}
	 */
	isUseLocking: false,
	
	
	constructor: function( cfg ) {
		Runner.util.inlineEditing.InlineEdit.superclass.constructor.call(this, cfg);	
		
		this.isUseLocking = Runner.pages.PageSettings.getTableData(this.tName, "locking");
		this.initLocking();
		
		this.addEvents("beforeEditRow");
	},
	
	init: function() {
		Runner.util.inlineEditing.InlineEdit.superclass.init.call(this);
		this.initRows();
	},

	/**
	 * Get submit URL
	 * @return {string}
	 */
	getSubmitUrl: function() {
		return Runner.util.inlineEditing.InlineEdit.superclass.getSubmitUrl.call( this ) + "?submit=1";
	},	
	
	/**
	 * Initialize a locking object
	 */
	initLocking: function() {
		if ( !this.isUseLocking ) {
			return;
		}
		
		this.locking = new Runner.Locking({
			tName: this.tName,
			pageId: this.id,
			hostObj: this,
		});	
	},
	
	/**
	 *
	 */
	disableLockedReckordPage: function( rowId, message ) {
		if ( !this.isUseLocking ) {
			return;
		}		
		
		var ctrl = Runner.controls.ControlStorage.byId( rowId );
		ctrl.forEach( function( ctrl ) {
			ctrl && ctrl.setDisabled();
		});

		this.makeError( message, this.getRowById( rowId ) );
	},
	
	/**
	 *
	 */
	enableLockedReckordPage: function() {
		// on admin unlock
	},

	/**
	 * Lock the row's record
	 * @param {object} row
	 * @param {object} ctrlJSON
	 */
	lockRow: function( row, ctrlsJSON ) {
		this.isUseLocking 
			&& this.locking.startLocking( row.id, this.getKeysForLocking( row ) );
	},
	
	/**
	 * Get the locking keys string for the row object
	 * @param {object} row
	 * @return {string}
	 */
	getKeysForLocking: function( row ) {
		return this.getLockingKeysString( row.keys );
	},
	
	/**
	 * Get the keys string for the 'locking' operations
	 * @param {array} keys
	 * @return {string}
	 */
	getLockingKeysString: function( keys ) {
		if ( !this.isUseLocking ) {
			return "";
		}
		
		var sKeys = [], i;

		keys = keys || [];
		
		for (i = 0; i < keys.length; i++) {
			sKeys.push( escape( keys[i] ) ) ;
		}
		return sKeys.join("&");		
	},
	
	/**
	 * Show error massage for locked deleted records
	 */
	showErrorForLockingRecords: function() {
		var lockingRecords = Runner.pages.PageSettings.getTableData(this.tName, 'lockRecIds');
		if ( !lockingRecords.length ) {
			return;
		}
		for (var i = 0, l = lockingRecords.length; i < l; i++) {
			this.makeError( "Record is being edited by another user", this.getRowById( lockingRecords[i] ) );
			$('input[id=check' + this.id + '_' + lockingRecords[i] + ']', this.pageObj.gridElem).prop('checked', true);
		}
	},
	
	/**
	 * Initialize the row object
	 * @param {object} row
	 */
	initRow: function( row ) {
		Runner.util.inlineEditing.InlineEdit.superclass.initRow.call( this, row );
		row.isEditable = true;
	},
	
	initRows: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.initRow( this.rows[i] );
		}
		if ( this.isUseLocking ) {
			this.showErrorForLockingRecords();
		}
	},
	
	initInlineRowEditors: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.getInlineButtBlock( this.rows[i] );
		}
	},
	
	initButtons: function() {
		Runner.util.inlineEditing.InlineEdit.superclass.initButtons.call(this);
		this.initEditAll();
	},
	
	/**
	 * Initialize the 'Edit selected' button
	 */
	initEditAll: function() {
		var inlineObj = this;
		
		this.editAllButt.off("click").on("click", function(e) {			
			var i, j, row, checkBox, 
				selBoxesArr = [],
				isEdit = false;
				
			$('input[type=checkbox][id^=check' + inlineObj.id + '_]:checked').each( function() {
				var $box = $(this),
					recordId = $box.attr('id').substr( $box.attr('id').lastIndexOf('_') + 1 );
				
				if ( inlineObj.isEditOwn && !$('#iEditLink' + recordId).length ) {
					$box.prop("checked", false);
					return;
				}
				selBoxesArr.push( $box );
			});
				
			for (i = 0; i < selBoxesArr.length; i++) {
				for (j = 0; j < inlineObj.rows.length; j++) {
					row = inlineObj.rows[j];
					checkBox = row.checkBox;
					
					if ($(checkBox[0]).attr('id') !== $(selBoxesArr[i]).attr('id')) {
						continue;
					}
					
					if ( row.keys && row.keys.length > 0 ) {
						inlineObj.inlineEdit( row );
						if ( inlineObj.rows[i] && !inlineObj.rows[i].submitted && !inlineObj.rows[i].isAdd ) {
							isEdit = true;
						}
					}
				}
			}
			if ( isEdit ) {
				inlineObj.toggleMassRecButt();
			}
			
			e.preventDefault();
			//return false;	
		});
	},
	
	/**
	 * Reinitialize the InlineEdit functionality
	 */
	reInit: function( gridRows ) {
		Runner.util.inlineEditing.InlineEdit.superclass.reInit.call(this, gridRows);
		this.initRows();
	},
	
	/**
	 *
	 * @param {Object} row
	 * @param {Boolean} hideRevertButton
	 * @param {Boolean} hideSaveButton
	 */
	inlineEdit: function( row, hideRevertButton, hideSaveButton ) {
		this.pageObj.hideElement('message');
		
		this.fireEvent("beforeEditRow", row);
		row.submitted = false;
		this.clearError( row );
		this.toggleMassRecButt();
		this.getValuesFromSpan( row );
		this.hideRowButtons( row );	
		this.getControls( this.pageObj, row, hideRevertButton, hideSaveButton );
		
		var inlineObj = this;
		setTimeout( function() {
			$("#" + inlineObj.rowPref + row.id).removeClass('hovered');
		});
	},
	
	editRecById: function( rowId ) {
		var row = this.getRowById(rowId);
		if (row) {
			this.inlineEdit(row);
			return true;
		}
		return false;
	},
	
	editAllRecs: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.inlineEdit( this.rows[i] );
		}
	},
	
	getRecsId: function() {
		var recsId = {};
		for (var i = 0; i < this.rows.length; i++) {
			recsId[i + 1] = this.rows[i].id;
		}
		return recsId;
	},
	
	/**
	 * Revert the edit record
	 * @param {object} row
	 */
	revertRow: function( row ) {
		//	nothing to revert
		if ( !row.data ) {
			return;
		}
		
		row.submitted = true;
		row.revertted = true;
		row.errorDiv = false;
		
		if ( row.checkBox ) {
			row.checkBox.prop("checked", false);
		}

		// clear controls
		Runner.util.inlineEditing.InlineEdit.superclass.revertRow.call(this, row);
		// change row buttons
		this.showRowButtons( row );
		// set row data into spans
		this.setValuesIntoSpans( row );
		this.revertFieldLabels( row );
		
		if ( this.isUseLocking && this.locking ) {
			this.locking.unlockRecordInline( this.getKeysForLocking( row ) );
		}
		
		if ( row.checkBox ) {
			row.checkBox.show();
		}
	},
	
	/**
	 * Assign the row's basicForm property with the Runner BasicForm object reference
	 * or if it's alredy assigned update its properties
	 * @param {object} row
	 */		
	initForm: function( row ) {
		Runner.util.inlineEditing.InlineEdit.superclass.initForm.call( this, row );

		for (var i = 0; i < row.keys.length; i++) {
			row.basicForm.baseParams[ "editid" + (i + 1) ] = row.keys[i];
		}
	},
	
	/**
	 * The Cancel button's handler
	 * @param {object} row 
	 * @param {boolean} isCancelAll 
	 */
	cancelButtonHn: function( row, isCancelAll ) {
		row.revokeCancel = false;
		if ( typeof row.onBeforeCancel != "undefined" ) {
			row.onBeforeCancel();
		}
		if ( row.revokeCancel ) {
			return false;
		}
		
		this.revertRow( row );

		if ( typeof row.onCancel != "undefined") {
			row.onCancel();
		}
			
		this.pageObj.fireEvent( "afterInlineCancel" );
		this.fireEvent('cancel', row, this.fNames);	
		
		Runner.util.inlineEditing.InlineEdit.superclass.cancelButtonHn.call(this);

		if ( !isCancelAll ) {
			this.fireEvent('recalcGridSize');
		}
		return true;
	},
	
	/**
	 * Toggle the 'Save all', 'Cancel all', 'Edit all' buttons
	 */
	toggleMassRecButt: function() {
		Runner.util.inlineEditing.InlineEdit.superclass.toggleMassRecButt.call( this );
		if ( this.rows.length ) {
			Runner.toggleButton( this.editAllButt, !this.massRecButtEditMode )
		}
	},
	
	fireUserEventAfterInline: function( row ) {
		if ( this.pageObj.isEventExist('afterInlineEdit') ) {
			Runner.util.inlineEditing.InlineEdit.superclass.fireUserEventAfterInline.call(this, row);
		}	
	},
	
	/**
	 * A handler that is invoked after an Inline Edit row is submitted
	 * ( on the row's form successSubmit event or when the edit popup is saved )
	 * @param {object} row
	 * @param {object} newData
	 * @param {object} editPageObj		A reference to a pop-up or dash edit page (optional)
	 */
	afterSubmit: function( row, newData, editPageObj ) {
		var oldKeysForLocking = this.getKeysForLocking( row ), 
			isEdit;
		
		Runner.util.inlineEditing.InlineEdit.superclass.afterSubmit.call( this, row, newData, editPageObj );

		if ( this.isUseLocking && this.locking ) {
			// reset confirming for the record with the old keys
			this.locking.resetSetInterval( oldKeysForLocking ); 
			
			if ( editPageObj && !editPageObj.moveNext ) {
				// start confirming with new keys for the popup edit page without prev/next buttons
				editPageObj.sKeys = this.getLockingKeysString( newData.keys );
				this.locking.startLocking( row.id, editPageObj.sKeys );
			}
		}
		
		// fire the 'rowsEdited' event if all rows were submited
		var allVals = [],
			allKeys = [],
			allRowIds = [];
			
		for (var i = 0; i < this.rows.length; i++) {
			if ( this.rows[i].submitted === false 
				&& ( !this.spreadsheet || !this.rows[i].isAdd ) ) {
				return;
			}
			allVals.push( this.rows[i].data );
			allKeys.push( this.rows[i].keys );
			allRowIds.push( this.rows[i].id );
		}
		
		//the following function code is invoked after the all edited row are submitted only	
		isEdit = !this.pageObj.isDetailsRecountNeeded;
		delete this.pageObj.isDetailsRecountNeeded;
		
		this.fireEvent('rowsEdited', allVals, newData.fields, allKeys, allRowIds, isEdit);
	},
	
	/**
	 * Save all Inline Edit grid's rows
	 * @param {boolean} modifiedOnly	 
	 */
	saveAll: function( modifiedOnly ) {
		if ( Runner.util.inlineEditing.InlineEdit.superclass.saveAll.call( this, modifiedOnly ) ) {
			return true;
		}
		
		for (var i = 0; i < this.rows.length; i++) {
			if ( !this.rows[i].submitted && !this.rows[i].isAdd && !this.rows[i].submitting ) {
				this.rows[i].fileFieldsCount = 0;
				this.waitForFilesUpload( this.rows[i] );
			}
		}
	},
	
	/**
	 * @param {object} row
	 * @return Boolean
	 */
	isRowEditing: function( row ) {
		return row.submitted !== undefined && !row.submitted;
	}
});

/**
 * Runner.pages.Defaults
 * Class wich contains default settings (global, tables, fileds, validation)
 * Insert new settings - only in alphabetical order 
 */
Runner.pages.Defaults = function() {
	this.globalSettings = {
		debugMode: false,
		pwdStrong: false,
		webRootPath: "",
		s508: false,
		isFB: false,
		FBappId: "",
		FBLoginUrl: "",
		loginFormType: 0
	};
	this.tableSettings = {
		ajaxSuggest: true,
		confirmTime: 15,
		copy: false,
		defaultMembership: [],
		defaultRights: [],
		detailTables: {},
		dpParams: [],
		events: {},
		firstTime: 0,
		columnOrder: [],
		fieldSettings: {},
		hasEvents: false,
		hideColumns: [],
		isAllowFieldsReordering: false,
		isAllowShowHideFields: false,
		isEditOwn: false,
		isInlineAdd: false,
		isInlineEdit: false,
		isShowDetails: false,
		isUseCK: false,
		simpleSearchActive: false,
		isUsePopUp: false,
		isUseResize: false,
		entityType: 0,
		listGridLayout: 0,
		keys: {},
		listFields: [],
		locking: false,
		lockRecIds: [],
		masterPageType: "",
		masterTable: "",
		masterKeys: [],
		maxPages: 1,
		nextKeys: {},
		pageMode: 0,
		pageNumber: 0,
		passFieldName: 'password',
		permissions: {},
		prevKeys: {},
		proxy: {},
		recsPerRowList: 1,
		msg_emailError: "",
		msg_passwordError: "",
		msg_userError: "",
		rightsGroups: [],
		rightsTables: [],
		shortTNames: [],
		inlineEditAvailable: false,
		showRows: false,
		sKeys: "",
		strCaption: "",
		strKey: [],
		totalFields: [],
		usersList: [],
		warnOnLeaving: false,
		view: false,
		scrollGridBody: false,
		searchSaving: false,
		importFieldsLabels: {},
		printerPageOrientation: Runner.pages.constants.PORTRAIT_PRINT_ORIENTATION,
		printGridLayout: Runner.pages.constants.HORIZONTAL_LAYOUT,
		reportPrintLayout: Runner.pages.constants.STEPPED_LAYOUT,
		reportType: Runner.pages.constants.REPORT_TYPE_STANDART,
		reportPrintMode: Runner.pages.constants.REPORT_MODE_PRINT_FRIENDLY,
		reportPrintPartitionType: Runner.pages.constants.REPORT_PRINT_PAGE_FIT,
		reportPrintGroupsPerPage: 3,
		pageWidth: 700,
		pageHeight: 900,
		createPdf: 0,
		pdfFitToPage: 1,
		printerPagePDF: false,
		crossTable: false,
		isRTL: false,
		isDashSearchPage: false,
		afterAddAction: 0,
		afterAddActionDetTable: "",
		afterEditAction: 0,
		afterEditActionDetTable: "",
		extraSearchPageParams: null,
		crossParams: null
	};
	this.fieldSettings = {
		acceptFileTypes: ".+$",
		autoCompleteFields: [],
		parentFields: [],
		compatibilityMode: false,
		dateEditType: Runner.controls.constants.EDIT_DATE_SIMPLE,
		dispField: "",
		editFormat: Runner.controls.constants.EDIT_FORMAT_NONE,
		freeInput: false,
		initialYear: 100,
		isDisabled: false,
		isHidden: false,
		isUseTimeStamp: false,
		lastYear: 10,
		lcType: Runner.controls.constants.LCT_DROPDOWN,
		linkField: "",
		lookupTable: "",
		mask: "",
		maxFileSize: undefined,
		maxNumberOfFiles: 1,
		maxTotalFileSize: undefined,
		nHeight: 200,
		RTEType: "",
		Multiselect: false,
		selectSize: 1,
		showTime: false,
		strName: "",
		timePick: null,
		validation: null,
		viewFormat: Runner.controls.constants.FORMAT_NONE
	};
	
	var self = this, 
		SOURCE_GLOBAL = 'g', 
		SOURCE_TABLE = 't', 
		SOURCE_FIELD = 'f', 
		getSettings = function(source, settingName) {
			var sourceName = source == SOURCE_GLOBAL ? 'globalSettings' 
					: source == SOURCE_TABLE ? 'tableSettings' : 'fieldSettings';
			if ( self[ sourceName ][ settingName ] != undefined ) {
				return self[ sourceName ][ settingName ];
			}
			return null;
		};
		
	this.getGlobalSettings = function(settingName) {
		return getSettings(SOURCE_GLOBAL, settingName);
	};
	
	this.getTableSettings = function(settingName) {
		return getSettings(SOURCE_TABLE, settingName);
	};
	
	this.getFieldSettings = function(settingName) {
		return getSettings(SOURCE_FIELD, settingName);
	};
	
	this.getValidationSettings = function() {
		return { 
			regExp: null, 
			validationArr: [],
			customMessages: []	
		};
	};
};

Runner.pages.Defaults = new Runner.pages.Defaults();/**
 * The global page manager object.
 * Should not be created directly, only one instance per page.
 * @singleton
 */
Runner.pages.PageManager = function() {
	// table managers collection
	var tables = {},
		// window 'onunload' handlers data array
		beforeUnloadPool = [];

	if (window.onunload) {
		beforeUnloadPool.push(window.onBeforeUnload, window, []);
	}
	window.onunload = function() {
		window.Runner.pages.PageManager.callUnload();
	}

	Runner.onMediaChange(function(media) {
		Runner.pages.PageManager.updateMedia(media);
	});

	return {
		/**
		 * Register a page controller
		 * @param {object} pageController
		 */
		register: function(pageController) {
			if (!pageController) {
				return;
			}
			var pageTable = pageController.tName,
				pageId = pageController.pageId;

			// if table not exists, create new one
			tables[pageTable] = tables[pageTable] || {};
			tables[pageTable][pageId] = pageController;
		},

		/**
		 * Ge the page's controller by the table name and id.
		 * @param {string} tName
		 * @param {number} pageId
		 * @return {mixed} return control, array of controls or false
		 */
		getAt: function(tName, pageId) {
			if (!tables[tName] || !tables[tName][pageId]) {
				return false;
			}

			return tables[tName][pageId];
		},

		/**
		 * Ge the page's controller by the table's id.
		 * @param {number} pageId
		 * @return {mixed}
		 */
		getById: function(pageId) {
			if (typeof pageId == "undefined") {
				return false;
			}
			for (var tName in tables) {
				if (typeof tables[tName][pageId] != "undefined") {
					return tables[tName][pageId];
				}
			}
			return false;
		},

		/**
		 * Unregister a page controller
		 * @param {string} tName
		 * @param {number} pageId
		 * @return {boolean} true if success, otherwise false
		 */
		unregister: function(tName, pageId) {
			// if no table name passed, return false
			if (!tables[tName]) {
				return false;
			}
			// recursively destroy pageObjects
			if (!pageId) {
				for (var id in tables[tName]) {
					this.unregister(tName, id);
				}

				delete tables[tName];
			} else if (tables[tName][pageId]) {
				if (typeof tables[tName][pageId].destructor === "function") {
					tables[tName][pageId].destructor();
				}

				if (tables[tName] !== undefined && tables[tName][pageId] !== undefined) {
					Runner.pages.PageControlsMap.removeMap(tName, tables[tName][pageId].pageType, pageId);
					Runner.pages.PageViewControlsMap.removeMap(tName, tables[tName][pageId].pageType, pageId);
					delete tables[tName][pageId];
				}
			}

			return true;
		},

		/**
		 * Initialize all preloaded pages
		 */
		initPages: function() {
			var controlsMap = Runner.pages.PageControlsMap.getMap(),
				viewControlsMap = Runner.pages.PageViewControlsMap.getMap(),
				cfg, tName, pageType, pageId;

			for (tName in controlsMap) {
				for (pageType in controlsMap[tName]) {
					for (pageId in controlsMap[tName][pageType]) {

						cfg = {
							tName: tName,
							pageType: pageType,
							pageId: pageId,
							controlsMap: controlsMap[tName][pageType][pageId] || {},
							viewControlsMap: viewControlsMap[tName][pageType][pageId] || {},
							pageMode: Runner.pages.PageSettings.getTableData(tName, "pageMode"),
							openMode: Runner.pages.constants.OPENMODE_PRELOADED
						};

						this.initPage(cfg);
					}
				}
			}

			// init only once
			this.initPages = Runner.emptyFn;
			Runner.stopLoading(); //?
		},

		/**
		 * Create a page based popup window.
		 * This function is run as the part of initialization routine.
		 * The context is newly-created Page object
		 */
		createPopup: function() {
			var args = {
				centered: true,
				closeByButton: true,
				modal: this.modal,
				bodyContent: this.bodyCont,
				headerContent: this.headerCont,
				footerContent: this.footerCont,
				hidden: this.hiddenPopup,
				noHeader: this.noHeader,
				noFooter: this.noFooter
			};

			var itemSelector = '[data-itemid]:not(li)';

			args.headerContent = $(args.headerContent).find(itemSelector).addBack(itemSelector);
			args.footerContent = $(args.footerContent).find(itemSelector).addBack(itemSelector);

			if (!Runner.smallScreen) {
				args.width = this.popupWidth;
				args.height = this.popupHeight;
			}

			Runner.pages.PageManager.createBSWin.call(this, args, true, null, this.afterCloseFlyWinHandler.bind(this));
		},



		/**
		 * This function is run as the part of initialization routine.
		 * The context is newly-created Page object
		 * @param {object} Server response JSON onbject
		 */
		putToContainer: function(obj, container) {
			obj = obj || this;
			container = container || this.openContainer;

			this.openContainer = this.openContainer || container;

			$(container).html(obj.bodyCont)
				.prepend(obj.headerCont)
				.append(obj.footerCont);
		},


		/**
		 * Creates and retuens Bootstrap panel element with page header, footer and body inserted
		 * The context is newly-created Page object
		 * @param {object} obj
		 * @return Object - jQuery object .panel
		 */
		createPagePanel: function(obj, style ) {
			var $panel = Runner.pages.PageManager.createPanel( !!obj.footerCont, style );
			Runner.pages.PageManager.insertPageIntoPanel.call( this, $panel, obj );
			return $panel;
		},

		createPanel: function( addFooter, style ) {
			style = style || 'primary';
			var $panel = $('<div class="panel panel-' + ( style || 'primary' ) + '"></div>');

			$("<div></div>")
				.addClass("panel-heading")
				.appendTo($panel);

			$("<div></div>")
				.addClass("panel-body")
				.appendTo($panel);

			if ( addFooter ) {
				$("<div></div>")
					.addClass("panel-footer")
					.appendTo($panel);
			}
			return $panel;
		},

		/**
		 * Update button style in header panel
		 */
		updateHeaderButtons: function( $header ) {
			//	update header buttons
			$header.find('.btn')
				.removeClass('btn-primary')
				.addClass('btn-default')
				.addClass('btn-xs');

		},

		insertPageIntoPanel: function( $panel, obj ) {
			var $body, $header, $footer;

			$header = $panel.children( '.panel-heading' );
			$header.html( Runner.pages.PageManager.setPanelHeader( obj.headerCont, obj.iconHtml ) );

			$body = $panel.children( '.panel-body' );
			$body.html( obj.bodyCont );

			$footer = $panel.children( '.panel-footer' );
			$footer.html( obj.footerCont );

			//	update header buttons
			Runner.pages.PageManager.updateHeaderButtons( $header );

			if ( obj.dashElementSettings && obj.dashElementSettings.type == Runner.pages.constants.DASHBOARD_MAP) {
				$body.addClass('bs-dashmap');
			}

			this.$headerContainer = $header;
			this.$footerContainer = $footer;
			return $panel;
		},

		makeDashPanelFoldable: function( $panel ) {
			const $containerItem = this.dashboard.findItem( this.dashElementSettings.containerItemId );
			const randomId = 'dashcollapse' + Math.floor( Math.random() * 100000 );
			const $header = $panel.children( '.panel-heading' );

			const iconRestore = 'fa-angle-down';
			const iconCollapse = 'fa-angle-up';
			
			//	create toggler
			if( !this.dashElementSettings.collapseToggler ) {
				$header
					.attr( {
						'data-toggle': 'collapse',
						'data-target': '#' + randomId
					} )
					.addClass( 'foldable' );
			} else {
				let $dbBrick = $header.children('.rnr-dberight');
				if( $dbBrick.length == 0 ) {
					$dbBrick = $('<div></div>' ).addClass( 'rnr-dbebrick rnr-dberight' );
					$header.append( $dbBrick );
				}
				$('<a></a>')
					.attr( {
						'data-toggle': 'collapse',
						'data-target': '#' + randomId
					} )
					.append( $('<span></span>').addClass( 'fas ' + iconCollapse ) )
					.appendTo( $dbBrick );
			}


			const $tabPanel = this.tabControl?.children( '.tab-content' ) ?? $([]);

			const $panelCollapse = $('<div></div>')
				.addClass( 'panel-collapse collapse' )
				.attr( 'id', randomId );
			if( $containerItem.data( 'collapsed' ) === undefined ) {
				//	initial state
				if( this.dashElementSettings.initiallyClosed ) {
					$header.find( '[data-toggle] > span' ).addClass( iconRestore ).removeClass( iconCollapse );
					$containerItem
						.css( { 
							'max-height': this.dashElementSettings.height, 
							'height': 'auto',
							'overflow': 'hidden'
						} )
						.data( 'collapsed', true );
				}
			}
			if( !$containerItem.data( 'collapsed' ) ) {
				$panelCollapse.addClass( 'in' );
			}
			$panelCollapse.append( $header.siblings() );
			$panelCollapse.insertAfter( $header );

			//	events 
			$panelCollapse.on( 'shown.bs.collapse', () => {
				// ensure smooth height transition
				$panelCollapse.children().css( 'min-height', '' );

				$containerItem
					.css( { 'height': '' } )
					.data( 'collapsed', false );
				
				//	update the rest of elements
				$tabPanel.find('> .tab-pane > .panel > .panel-collapse').addClass( 'in' );
				//	code snippet min-height handling
				$tabPanel.find( '> .tab-pane > .panel' ).removeClass( 'r-collapsed-panel' );
			} );

			$panelCollapse.on( 'show.bs.collapse', () => {
				$tabPanel.find( '> .tab-pane > .panel > .panel-heading [data-toggle] > span' ).removeClass( iconRestore ).addClass( iconCollapse );
			} );

			$panelCollapse.on( 'hide.bs.collapse', () => {
				// ensure smooth height transition
				$panelCollapse.children().each( (idx, el) => $(el).css( 'min-height', $(el).outerHeight() + 'px' ) );
				$panelCollapse.height( $panelCollapse.height() );

				$containerItem
					.css( { 
						'max-height': this.dashElementSettings.height, 
						'height': 'auto',
						'overflow': 'hidden'
					} )
					.data( 'collapsed', true );
				$tabPanel.find( '> .tab-pane > .panel > .panel-heading [data-toggle] > span' ).removeClass( iconCollapse ).addClass( iconRestore );
				//	code snippet min-height handling
				$tabPanel.find( '> .tab-pane > .panel' ).addClass( 'r-collapsed-panel' );
			} );
			
			$panelCollapse.on( 'hidden.bs.collapse', () => {
				//	update the rest of elements
				$tabPanel.find('> .tab-pane > .panel > .panel-collapse').removeClass( 'in' );

			});
		},

		/**
		 * This function is run as the part of initialization routine.
		 * The context is newly-created Page object
		 * Called for both dashboards and details preview on List page!
		 * this.dashElementSettings might be undefined
		 * @param {object} obj - Object with new page contents
		 * @param {jQuery obj} $container
		 */
		putToDashContainer: function(obj, $container) {
			obj = obj || this;
			var $panel = Runner.pages.PageManager.createPagePanel.call(this, obj, this.dashElementSettings?.panelStyle );
			$panel.addClass( 'r-dash-panel' );
			
			//	dashboard element and group folding 
			if( this.dashElementSettings?.foldable ) {
				Runner.pages.PageManager.makeDashPanelFoldable.call(this, $panel );
			}
			//	new types of dashboard snippet
			if( this.dashElementSettings?.type == Runner.pages.constants.DASHBOARD_SNIPPET ) {
				const snippetStyle = this.dashElementSettings.item.snippetStyle;
				if( snippetStyle == 'left' || snippetStyle == 'right' ) {
					$panel = $panel.find( '.r-box-left, .r-box-right' );
				}
			}

			Runner.pages.PageManager.insertPanelIntoContainer.call(this, $panel, $container || $(this.openContainer), obj);

		},

		/**
		 * This function is run as the part of initialization routine.
		 * Inserts prepared bootstrap panel into dashboard
		 * The context is newly-created Page object
		 * @param {jQuery obj} $panel
		 * @param {jQuery obj} $container
		 */
		insertPanelIntoContainer: function($panel, $container ) {
			$container
				.html('')
				.append($panel);

			this.pageCont = $container.get(0);
		},


		/**
		 *	The sequence of the page initialization:
		 *	1. Creating HTML structure - openPopup, putToContainer or other
		 *	2. Loading libraries required for the page initialization - page.loadFiles
		 *	3. Run caller-defined After-Create step: cfg.afterCreateHn
		 *	4. Run page-defined initialization steps: page.init and other
		 *	5. Run caller-defined After-Init step: cfg.afterInitHn
		 *	6. Correct window position
		 *	7. Run user-defined initialization step
		 * @param {object} cfg
		 * @return {object}
		 */
		initPage: function(cfg) {
			var page = Runner.pages.PageFactory(cfg),
				tasksToPrepend = [],
				tasksToAppend = [];

			if (cfg.beforeCreateHn) {
				tasksToPrepend.push(cfg.beforeCreateHn);
			}

			//	modify page initialization routine. Add container-related steps to the head of the queue.
			if ( cfg.openMode === Runner.pages.constants.OPENMODE_POPUP) {
				tasksToPrepend.push(this.createPopup);
			} else if (cfg.openMode === Runner.pages.constants.OPENMODE_CONTAINER) {
				tasksToPrepend.push(this.putToContainer);
			} else if (cfg.openMode === Runner.pages.constants.OPENMODE_DASHCONTAINER) {
				tasksToPrepend.push(this.putToDashContainer);
			} else if (cfg.openMode === Runner.pages.constants.OPENMODE_DASHTABS) {
				tasksToPrepend.push(this.putToDashTab);
			} else if (cfg.openMode === Runner.pages.constants.OPENMODE_TABVIEW) {
				tasksToPrepend.push(this.putToTab);
			}

			tasksToPrepend.push(page.loadFiles);
			tasksToPrepend.push(page.createCharts);

			//	add caller-defined afterCreate step
			if (cfg.afterCreateHn) {
				tasksToPrepend.push(cfg.afterCreateHn);
			}
			//	add caller-defined afterInit step
			if (cfg.afterInitHn) {
				tasksToAppend.push(cfg.afterInitHn);
			}
			//	update popup position and size
			if ( cfg.openMode === Runner.pages.constants.OPENMODE_POPUP) {
				tasksToAppend.push(function() {
					Runner.pages.PageManager.correctBSWindowSize(page.bsWin, page);
				});
			}

			//	add user-defined initialization
			tasksToAppend.push(function() {
				page.fireEvent('afterPageReady', page, page.proxy, page.id);
			});

			page.prependInitTask(tasksToPrepend);
			page.addInitTask(tasksToAppend);

			//	proceed with the initialization
			page.goInitQueue();

			return page;
		},

		/**
		 * For dynamic page opening
		 * @param {object} pageParams
		 * @return {number} 			The page's id
		 *
		 * All the parameters will be passed to the new page constructor as cfg param.
		 * Besides, some parameters have special meaning. They are:
		 *
		 *	 tName {string} 		- name of the table (chart/report etc). The tName will be used to make the request URL
		 *	 pageType {string} 		- one of the PAGE_ constants, used to make the request URL.
		 *	 baseParams {object} 	- these parameters will be added to the request and will be passed to the server.
		 *	 pageMode {integer} 	- one of the LIST_ or other constants. This parameter is used by PageFactory
		 *								function to select the proper constructor for the page object.
		 *	 multiRecordPage {bool} - the page may use multiple ids (records etc). Increase id counter by 10000;
		 *	 openMode {integer}		- one of the OPENMODE_... constants. Determines whether the page should be displayed in popup, in a container or any other way.
		 *	 openContainer {element}	- used with openMode==OPENMODE_CONTAINER. The HTML element the page should be put into.
		 *	 tabsSettings {object}	- an object containing the following data
		 *		tabControlName {string}	- used with openMode==OPENMODE_DASHTABS. The unique identifier of the tab view control
		 *	 	tabLabel {string}			- the label displayed in the tab.
		 *	 	activeTab {boolean}		- the flag indicating if a tab should be active.
		 *	 	tabIndex {number}			- the tab index within the tabs control
		 *	 	tabId {string}				- the unique identifier of the tab within the single tab control
		 *	 popupCentered {boolean}	- create popup centered or not
		 *	 modal {boolean}			- create modal popup
		 *
		 * 	 beforeCreateHn	{function}	- the function to be run before the page created.
		 *	 afterCreateHn	{function}	- the function to be run after the page created but not initialized - before the page.init call.
		 *	 afterInitHn	{function}	- the function to be run after the page created and initialized - right before the after afterPageReady event.
		 *			All the handlers are called with the context of the newly created Page object.
		 *	 skipTrackingActivity {boolean} do not track user activity while requesting a page
		 */
		openPage: function(pageParams) {
			var pageManager = this,
				ajaxRequestUrl = pageParams.requestUrl || Runner.pages.getUrl(pageParams.tName, pageParams.pageType),
				reqParams, i;

			pageParams.pageId = Runner.genId();
			if (pageParams.multiRecordPage) {
				Runner.setIdCounter(pageParams.pageId + 10000);
			}
			//	TODO! Delete this line:
			pageParams.openMode = pageParams.openMode || Runner.pages.constants.OPENMODE_POPUP;

			//	prepare request parameters
			reqParams = {
				id: pageParams.pageId,
				onFly: 1,
				isNeedSettings: true
			};

			if (pageParams.keys) {
				for (i = 0; i < pageParams.keys.length; i++) {
					reqParams[(pageParams.keyPref || "editid") + (i + 1)] = pageParams.keys[i];
				}
			}

			reqParams = Runner.apply(reqParams, pageParams.baseParams);

			// make the request
			Runner.runnerAJAX(ajaxRequestUrl, reqParams, function(ctrlsJSON) {
				// add loaded settings to global storage
				Runner.pages.PageSettings.addSettings('', ctrlsJSON.settings);
				Runner.applyPagesData(ctrlsJSON.pagesData);

				// load additional css files
				Runner.util.ScriptLoader.loadCSS(ctrlsJSON.CSSFiles);
				Runner.util.ScriptLoader.loadCSS(ctrlsJSON.additionalCSS);


				// load additional JS files
				if (ctrlsJSON.additionalJS) {
					$.each(ctrlsJSON.additionalJS, function(jsFile, jsFileReq) {
						Runner.util.ScriptLoader.addJS([jsFile], jsFileReq);
					});
				}

				Runner.util.ScriptLoader.onFilesLoaded(function() {
					// add map
					Runner.pages.PageControlsMap.addMap(pageParams.tName, pageParams.pageType, pageParams.pageId, ctrlsJSON.controlsMap);
					Runner.pages.PageViewControlsMap.addMap(pageParams.tName, pageParams.pageType, pageParams.pageId, ctrlsJSON.viewControlsMap);

					// callback analyze request data and call initPage of this class with cfg as param
					var cfg = {
						submitUrl: ajaxRequestUrl,
						editType: pageParams.editType || pageParams.baseParams && pageParams.baseParams.editType,
						controlsMap: ctrlsJSON.controlsMap ? ctrlsJSON.controlsMap[pageParams.tName][pageParams.pageType][pageParams.pageId] : null,
						viewControlsMap: ctrlsJSON.viewControlsMap ? ctrlsJSON.viewControlsMap[pageParams.tName][pageParams.pageType][pageParams.pageId] : null,
						headerCont: ctrlsJSON.headerCont || "",
						iconHtml: ctrlsJSON.iconHtml,
						bodyCont: pageManager.wrapPageBody(ctrlsJSON.html, pageParams.tName, pageParams.pageType),
						footerCont: ctrlsJSON.footerCont || "",
						message: ctrlsJSON.message || ""

					};
					Runner.apply(cfg, pageParams);

					Runner.setIdCounter(ctrlsJSON.idStartFrom);
					pageManager.initPage(cfg)

				}, pageManager);

				Runner.util.ScriptLoader.load();
			}, pageParams.skipTrackingActivity);

			return pageParams.pageId;
		},

		/**
		 *	This functon is called in the context of the details page ( this == details page object )
		 */
		replaceDashboardGridTabs: function() {
			if (!this.$gridTabControl) {
				return;
			}

			var $replacementTabs = $(this.pageData.gridTabs),
				$originalTab = $('[data-target="#' + this.tabSettings.tabPaneId + '"]:not([data-pageid])').closest('li'),
				$originalLink = $originalTab.children('a');

			var showHandler = $originalLink.data( 'showHandler' );
			var hideHandler = $originalLink.data( 'hideHandler' );

			$originalTab.hide();

			var attrs = {
				'data-target': $originalLink.attr('data-target')
			}

			var $newLinks = $replacementTabs.children('a');
			$newLinks.attr( attrs );
			//	reuse show/hide handlers from the replaced tab
			if( showHandler ) {
				$newLinks.on('shown.bs.tab', showHandler );
			}
			if( hideHandler ) {
				$newLinks.on('hidden.bs.tab', hideHandler );
			}

			$replacementTabs.attr('data-tabidx', $originalTab.attr('data-tabidx'));

			if (!$originalTab.hasClass('active')) {
				$replacementTabs.removeClass('active');
			}

			this.$gridTabControl.find('[data-target="#' + this.tabSettings.tabPaneId + '"][data-pageid]').parent().remove();
			$originalTab.after( $replacementTabs );

			this.tabControl.attr('data-empty', this.tabSettings.tabEmpty ? '' : null );			

			this.$originalTab = $originalTab;
		},


		/**
		 * @param {string} body
		 * @param {string} tName
		 * @param {string} pageType
		 * @return {string}
		 */
		wrapPageBody: function(body, tName, pageType) {
			if (!body) {
				return '&nbsp;';
			}
			return '<div class="rnr-pagewrapper ">' + body + '</div>';
		},

		/**
		 * Add add page content to the tabs controller in a dashboard
		 * The context is newly-created Page object
		 * @param this - RunnerPage
		 * @param {object} obj		The page's data object
		 */

		putToDashTab: function(obj) {
			obj = obj || this;
			if( this.dashElementSettings.group ) {
				return Runner.pages.PageManager.putToDashTabGroup.call( this, obj );
			}
			if( this.dashElementSettings.tabLocation == 'above' ) {
				return Runner.pages.PageManager.putToDashTabAbove.call( this, obj );
			}
			if( this.dashElementSettings.tabLocation == 'body' ) {
				return Runner.pages.PageManager.putToDashTabBody.call( this, obj );
			}
		},

		/**
		 *
		 */
		putToDashTabBody: function(obj) {
			obj = obj || this;

			this.tabSettings.tabLabel = this.pageData['dashTabLabel'] || this.tabSettings.tabLabel;

			var $openContainer = $(this.openContainer);
			var panelId = 'panel_' + this.dashElementSettings.elementName + this.id;

			//	create or find tab control and panel
			var $panel;
			this.tabControl = this.tabControl 
				|| Runner.pages.PageManager.findTabControl( $openContainer, this.tabSettings.tabControlName );
			
			if ( !this.tabControl || !this.tabControl.length ) {
				this.tabControl = Runner.pages.PageManager.findOrCreateTabControl( 
						$openContainer, 
						obj.tabSettings.tabControlName, 
						obj.tabSettings.hiddenNav && !this.pageData.gridTabs
					);
					
				$panel = Runner.pages.PageManager.createPanel.call(this, obj, this.dashElementSettings.panelStyle );
				$panel.attr('id', panelId );
				//	insert tab control into panel
				$panel.children('.panel-body').append( this.tabControl );
				//	create panel header switch
				var $headerSwitcher = $('<div></div>')
					.addClass('r-header-switcher')
					.appendTo( $panel.children('.panel-heading') );
					
				//	create panel footer switch
				var $footerSwitcher = $('<div></div>')
					.addClass('r-header-switcher')
					.appendTo( $panel.children('.panel-footer') );
				//	insert panel into container
				Runner.pages.PageManager.insertPanelIntoContainer.call( this, $panel, $openContainer, obj );
				
				this.tabControl.prop('headerSwitcher', $headerSwitcher );
				this.tabControl.prop('footerSwitcher', $footerSwitcher );
				
			} else {
				//	find panel
				$panel = $( '#' + panelId );
				$headerSwitcher = $panel.children('.panel-heading').children( '.r-header-switcher' );
			}

			if( this.dashElementSettings?.foldable ) {
				Runner.pages.PageManager.makeDashPanelFoldable.call(this, $panel );
			}
			
			var tabPaneId = "tab" + this.tabSettings.tabControlName + "_" + this.tabSettings.tabId;

			if ( this.pageData.gridTabs ) {
				this.$gridTabControl = this.tabControl.children('ul');
				this.tabSettings.tabPaneId = tabPaneId;
				this.replaceGridTabs = Runner.pages.PageManager.replaceDashboardGridTabs;
			}	
			
			//	insert page body into tab control
			var content = {
				body: $( obj.bodyCont ),
				header: Runner.pages.PageManager.setPanelHeader( obj.headerCont, obj.iconHtml ),
				footer: $( obj.footerCont )
			}
			
			Runner.pages.PageManager.setTabWithinTabsControl.call( 
					this, 
					this.tabControl, 
					content, 
					tabPaneId, 
					this.tabSettings.tabLabel, 
					this.tabSettings.activeTab, 
					this.tabSettings.tabIndex, 
					this.tabSettings.hideTabsContent
				);

/*

			$footer = $panel.children( '.panel-footer' );
			$footer.html( obj.footerCont );
*/

			//	insert page header into header container
		},
		
		/**
		 * 
		 * @param {string} headerCont 
		 * @param {string} iconHtml 
		 * @returns string
		 */
		setPanelHeader: function( headerCont, iconHtml ) {
			
			var $header = $("<div></div>");
			// prepare header and footer PD ITEMS
			// separate left and right-aligned items
			var itemSelector = '[data-itemid]:not(li)';
			var $headerCont = $(headerCont);

			//	take all dbe-bricks first
			/*
			const leftBrickSelector = '.rnr-dbebrick:not(.rnr-dberight)';
			const rightBrickSelector = '.rnr-dbebrick.rnr-dberight';
			var $leftBricks = $headerCont.filter( leftBrickSelector );
			var $rightBricks = $headerCont.filter( rightBrickSelector );
			var $rest = $headerCont.not( leftBrickSelector ).not( rightBrickSelector );
			*/
			
			//	find items inside the form
			const leftCellSelector = '[data-cellid]:not(.r-align-right)';
//			var $leftItems = $rest.find( leftCellSelector ).addBack( leftCellSelector ).find( itemSelector );
			let $leftItems = $headerCont.find( leftCellSelector ).addBack( leftCellSelector ).find( itemSelector );
			const rightCellSelector = '[data-cellid].r-align-right';
//			var $rightItems = $rest.find( rightCellSelector ).addBack( rightCellSelector ).find( itemSelector );
			let $rightItems = $headerCont.find( rightCellSelector ).addBack( rightCellSelector ).find( itemSelector );
			
			//	dashboard snippet, no cells or items, raw HTML text
			if( !$leftItems.length && !$rightItems.length && !$headerCont.data('location') ) {
				$leftItems = $('<span></span>').html( headerCont );
			}


			//$header.html( iconHtml ).append( $leftBricks );
			$header.html( iconHtml );
			$leftItems.appendTo( $header ).wrap( '<div class="rnr-dbebrick"></div>' );

			//$header.append( $rightBricks );
			$rightItems.appendTo( $header ).wrap( '<div class="rnr-dbebrick rnr-dberight"></div>' );

			return $header.html();
		},		

		putToDashTabAbove: function(obj) {
			this.tabSettings = this.tabSettings ||
				{
					tabControlName: 'emptytabs' + this.id,
					tabLabel: '',
					tabId: '',
					tabEmpty: !this.pageData.gridTabs
				};
			this.tabSettings.tabLabel = this.pageData['dashTabLabel'] || this.tabSettings.tabLabel;

			var $contentContainer = $("<div/>"),
				$openContainer = $(this.openContainer),
				tabPaneId = "tab" + this.tabSettings.tabControlName + "_" + this.tabSettings.tabId;

			obj = obj || this;

			if (!this.tabControl || !this.tabControl.length) {
				this.tabControl = Runner.pages.PageManager.findOrCreateTabControl($openContainer, obj.tabSettings.tabControlName, obj.tabSettings.hiddenNav && !this.pageData.gridTabs);
				this.tabControl.attr('data-empty', this.tabSettings.tabEmpty ? '' : null );
			}

			// show dash element
			this.dashboard.markElementEmpty( this.dashElement, false );			

			if (this.pageData.gridTabs) {
				this.$gridTabControl = this.tabControl.children('ul');
				this.tabSettings.tabPaneId = tabPaneId;
				this.replaceGridTabs = Runner.pages.PageManager.replaceDashboardGridTabs;
			}


			Runner.pages.PageManager.putToDashContainer.call(this, obj, $contentContainer);
			let $panel = $contentContainer.children('.panel, .r-box-left, .r-box-right');
			//	new snippet styles
			if( !$panel.length ) {
				$panel = $contentContainer;
			}			
			Runner.pages.PageManager.setTabWithinTabsControl.call( this, this.tabControl, $panel, tabPaneId, this.tabSettings.tabLabel, this.tabSettings.activeTab, this.tabSettings.tabIndex, this.tabSettings.hideTabsContent);
		},


		/**
		 * @param this - RunnerPage
		 * @param {*} obj 
		 */
		putToDashTabGroup: function( obj ) {
			this.tabSettings = this.tabSettings ||
				{
					tabLabel: this.pageData['pageTitle'],
					tabId: '',
					tabEmpty: !this.pageData.gridTabs
				};
			this.tabSettings.tabControlName = 'grouptabs' + this.dashElementSettings.group + this.dashboard.pageId;
			this.tabSettings.tabLabel = this.pageData['dashTabLabel'] || this.tabSettings.tabLabel;


			// group container
			var $openContainer = $('#dashgroup_' + this.dashElementSettings.group + this.dashboard.pageId );
			var $contentContainer = $("<div/>");
			var tabPaneId = [ "tab" + this.tabSettings.tabControlName, this.dashElementSettings.elementName, this.tabSettings.tabId ].join('-');

			obj = obj || this;

			if (!this.tabControl || !this.tabControl.length) {
				this.tabControl = Runner.pages.PageManager.findOrCreateTabControl($openContainer, obj.tabSettings.tabControlName, obj.tabSettings.hiddenNav && !this.pageData.gridTabs);
				//this.tabControl.attr('data-empty', this.tabSettings.tabEmpty ? '' : null );
			}

			// show dash element
			this.dashboard.markElementEmpty( this.dashElement, false );			

			if (this.pageData.gridTabs) {
				this.$gridTabControl = this.tabControl.children('ul');
				this.tabSettings.tabPaneId = tabPaneId;
				this.replaceGridTabs = Runner.pages.PageManager.replaceDashboardGridTabs;
			}

			const groupData = {
				element: this.dashElement,
				elements: $openContainer.data( 'elements' ) || []
			};


			Runner.pages.PageManager.putToDashContainer.call(this, obj, $contentContainer);
			let $panel = $contentContainer.children('.panel, .r-box-left, .r-box-right');
			//	new snippet styles
			if( !$panel.length ) {
				$panel = $contentContainer;
			}			
			Runner.pages.PageManager.setTabWithinTabsControl.call( this, this.tabControl, $panel, tabPaneId, this.tabSettings.tabLabel, this.tabSettings.activeTab, this.tabSettings.tabIndex, this.tabSettings.hideTabsContent, groupData );
		},


		/**
		 * Add add page content to the Bootstrap tabs controller
		 * The context is newly-created Page object
		 * @param {object} obj		The page's data object
		 * 	obj should include tabsettings: {
					tabControlName: <string>
					tabId: <string>
				}
		 */
		putToTab: function(obj) {
			obj = obj || this;

			var $contentContainer = $("<div/>"),
				tabId = "tab" + this.tabSettings.tabControlName + "_" + this.tabSettings.tabId;

			if (!this.tabControl || !this.tabControl.length) {
				this.tabControl = Runner.pages.PageManager.findOrCreateTabControl(this.openContainer, this.tabSettings.tabControlName, this.tabSettings.hiddenNav);
			}


			Runner.pages.PageManager.putToDashContainer.call(this, obj, $contentContainer);	//?
			Runner.pages.PageManager.setTabWithinTabsControl.call( this, this.tabControl, $contentContainer.children('.panel'), tabId, this.tabSettings.tabLabel, this.tabSettings.activeTab, this.tabSettings.tabIndex, this.tabSettings.hideTabsContent);

			this.detCont = this.tabControl.children( '.tab-content' ).children('[id='+tabId+']');

		},

		/**
		 * @param {object} obj
		 * @param {function} callback
		 */
		onTabShown: function(obj, callback) {
			if (!obj.tabControl || !obj.tabControl.length) {
				return;
			}

			var panelId = "tab" + obj.tabSettings.tabControlName + "_" + obj.tabSettings.tabId;

			obj.tabControl.find("a[data-target='#" + panelId + "'][data-toggle='tab']").on("shown.bs.tab", function(e) {
				callback();
			});
		},

		/**
		 * Initialize Bootstrap tabs controller within the $container elem passed
		 * @param {jQuery object} $container
		 * @param {string} name
		 * @param {boolean} hiddenNav
		 * @return {object}
		 */
		 findOrCreateTabControl: function($container, name, hiddenNav) {
			var $tabsControl = Runner.pages.PageManager.findTabControl( $container, name ),
				hiddenClass = hiddenNav ? " hidden" : "";

			if ($tabsControl.length) {
				return $tabsControl;
			}
			//	creating new tab control
			//	clear container first as there exist the previous tab control
			$container.html('');
			return $('<div id="tabs-' + name + '"><ul class="nav nav-tabs' + hiddenClass + '" role="tablist" ></ul> <div class="tab-content"></div></div>')
				.appendTo($container);
		},

		findTabControl: function( $container, name ) {
			return $("#tabs-" + name, $container);
		},


		/**
		 * @param {jQuery object} $container
		 * @param {string} name
		 */
		getTabControl: function($container, name) {
			return $("#tabs-" + name, $container);
		},

		/**
		 * Get a dash element tabs control by elements name
		 * @param {string} dashElementName
		 * @param {string} dashId
		 * @return {jQuery UI object}
		 */
		getDashElementTabControl: function(dashElementName, dashId) {
			return $("#tabs-" + dashElementName, $('#dashelement_' + dashElementName + dashId).get(0));
		},

		getDashGroupTabControl: function( groupName, dashId ) {
			return $( `#tabs-grouptabs${ groupName }${ dashId }` );
		},

		/**
		 * Add a new tab to the Bootstrap tabs controller ($tabsControl)
		 * or reload the content of existing one
 		 * The context is newly-created Page object
		 * @param {jQuery object} $tabControl
		 * @param {jQuery object} $tabContents
		 * @param {string} tabElementId
		 * @param {string} tabLabel
		 * @param {boolean} activeTab
		 * @param {number} rnrTabIndex
		 * @param {boolean} hideTabsContent
		 * @param {Object} groupData { elements: string[], element: string } - 
		 * 		when tab control belongs to dashboard group, 'elements' contains all group memebrs and 'element' - this element name
		 */
		setTabWithinTabsControl: function( $tabsControl, $tabContents, tabElementId, tabLabel, activeTab, rnrTabIndex, hideTabsContent, groupData ) {
			if ( !$tabsControl.length ) {
				return;
			}
			var contents = $tabContents;
			if ( !$tabContents.body ) {
				//	plain jQuery or string
				contents = {
					body: $tabContents
				};
			}
			var activeIdx, $activeLi,
				$tab = $("#" + tabElementId, $tabsControl);

			if ( $tab.length ) {
				$tabsControl.find("li[data-tabidx=" + rnrTabIndex + "]")
					.show();

				$tab
					.html('')
					.append( contents.body );
					
				$headerPane = $tab.data('headerPane' );
				$footerPane = $tab.data('footerPane' );
				if ( $headerPane && $headerPane ) {
					$headerPane.html( contents.header );
					$footerPane.html( contents.footer );
				}
				
				this.pageCont = $tab[0];

				activeIdx = $tabsControl.data("activeIdx");
				if (activeIdx !== undefined) {
					$activeLi = $tabsControl.children("ul").children("li").eq(activeIdx);
					if ($activeLi.data("tabidx") == rnrTabIndex) {
						$("a[data-toggle='tab']", $activeLi).tab("show");
					}
				}
				
				this.$headerContainer = $headerPane || this.$headerContainer;
				this.$footerContainer = $footerPane || this.$footerContainer;
				return;
			}

			Runner.pages.PageManager.addTabToTabsControl.call( this, $tabsControl, $tabContents, tabElementId, tabLabel, activeTab, rnrTabIndex, hideTabsContent, undefined, groupData );
		},

		/**
		 * Add a new tab to the Bootstrap tabs controller ($tabsControl)
		 * @param {jQuery object} $tabControl
		 * @param {jQuery object} $tabContents
		 * @param {string} tabElementId
		 * @param {string} tabLabel
		 * @param {boolean} activeTab
		 * @param {number} rnrTabIndex
		 * @param {boolean} hideTabsContent
		 * @param {string} tabHref (optional)
		 * @param {Object} groupData { elements: string[], element: string } - 
		 * 		when tab control belongs to dashboard group, 'elements' contains all group memebrs and 'element' - this element name
		 */
		addTabToTabsControl: function( $tabsControl, $tabContents, tabElementId, tabLabel, activeTab, tabidx, hideTabsContent, tabHref, groupData ) {
			if (!$tabsControl.length) {
				return;
			}
			
			//	create new structural elements, tab and pane
			var $newTabLink = $('<a data-target="#' + tabElementId + '" role="tab" data-toggle="tab">' + tabLabel + '</a>');
			if (tabHref) {
				$newTabLink.attr("href", tabHref);
			}
			var $newPane = $('<div role="tabpanel" class="tab-pane" id="' + tabElementId + '"/>');
			var $newTab = $( '<li role="presentation"></li>' )
				.attr( 'data-tabidx', tabidx )
				.append( $newTabLink );

			if( groupData ) {
				$newTab.attr( 'data-element', groupData.element );
				//	assign item attributes to the pane so that styles can be assigned and basic item API work
				$newPane.attr({
					'data-itemtype': 'dashboard-item',
					'data-itemid': this.dashElementSettings.itemId,
					'data-pageid': this.dashboard.pageId,
					'data-page': `${ this.dashboard.shortTName }_${ this.dashboard.pageName }`,
					'data-dashtype': this.dashElementSettings.type,
				});

			}

			//	insert new structural elements, tab and pane
			var $navigation = $tabsControl.children().first();
			var added = false;
			if( !groupData ) {
				if ( typeof tabidx === 'number' ) {
					$('li', $navigation).each(function(idx, li) {
						var $li = $(li),
							rnrCurrentTabIdx = $li.data('tabidx');

						if (rnrCurrentTabIdx === undefined || rnrCurrentTabIdx > tabidx) {
							
							$newTab.insertBefore($li);

							var target = $li.find('a').data("target") || $li.find('a').attr('href');
							$newPane.insertBefore( $(target, $tabsControl) );
							added = true;
							return false;
						}
					});
				}

			} else {
				$('li', $navigation).each(function(idx, li) {
					var $li = $(li);
					var rnrCurrentTabIdx = $li.data('tabidx');
					var element = $li.data('element');
					var nextElement = groupData.elements[ groupData.elements.indexOf( groupData.element ) + 1 ];

					if( element == nextElement || element == groupData.element && ( rnrCurrentTabIdx === undefined || rnrCurrentTabIdx > tabidx ) ) {
						$newTab.insertBefore($li);
						var target = $li.find('a').data("target") || $li.find('a').attr('href');
						$newPane.insertBefore( $(target, $tabsControl) );
						added = true;
						return false;
					}
				});
			}
			if ( !added ) {
				$newTab.appendTo( $navigation );
				$newPane.appendTo($(".tab-content", $tabsControl));
			}

			// insert header and footer panes
			var $headerSwitcher = $tabsControl.prop('headerSwitcher'),
				$footerSwitcher = $tabsControl.prop('footerSwitcher');
				
			var $headerPane, $footerPane;
			
			if ( $headerSwitcher && $footerSwitcher ) {
				$headerPane = $('<div></div>')
					.appendTo( $headerSwitcher )
					.hide();
				$footerPane = $('<div></div>')
					.appendTo( $footerSwitcher )
					.hide();

				$newPane
					.data('headerPane', $headerPane )
					.data('footerPane', $footerPane );
				
				var showHandler = function(e) {
					$headerPane.show();
					$footerPane.show();
				};
				var hideHandler = function(e) {
					$headerPane.hide();
					$footerPane.hide();
				};

				$newTabLink
					.on('shown.bs.tab', showHandler )
					.on('hidden.bs.tab', hideHandler );
				//	save handlers in the element in order for being reused when the tabs are replaced
				$newTabLink
					.data( 'showHandler', showHandler )
					.data( 'hideHandler', hideHandler );

				this.$headerContainer = $headerPane;
				this.$footerContainer = $footerPane;
			}


			//	insert tab contents
			var contents = $tabContents;
			if( !$tabContents.body ) {
				//	plain jQuery or string
				contents = {
					body: $tabContents
				};
			}
			$newPane.append( contents.body )
			if ( $headerPane ) {
				$headerPane.append( contents.header );
			}
			if ( $footerPane ) {
				$footerPane.append( contents.footer );
			}
			this.pageCont = $newPane[0];

			//	initialize new elements, update visibility
			let show = activeTab || $('li', $navigation).length === 1;
			if( groupData ) {
				//	show only the first element
				if( groupData.elements.indexOf( groupData.element ) == 0 ) {
					show = activeTab || $( `li[data-element= ${ groupData.element }]`, $navigation).length === 1;
				} else {
					show = false;
				}
			}
			$newTabLink.tab( show ? 'show' : undefined );

			if ( hideTabsContent ) {
				$tabContents.hide();
				if ( $headerPane ) {
					$headerPane.hide();
				}
				if ( $footerPane ) {
					$footerPane.hide();
				}
			}
		},

		/**
		 * @return {strign}
		 */
		getBSModalMarkup: function() {
			return '<div class="bs-popup modal" tabindex="-1" role="dialog">'
				+ '<div class="modal-dialog">'
				+ '<div class="modal-content">'
				+ '<div class="modal-header"></div>'
				+ '<div class="modal-body"></div>'
				+ '<div class="modal-footer"></div>'
				+ '</div>'
				+ '</div>'
				+ '</div>';
		},

		/**
		 * @param ???
		 * @return {object}
		 */
		createBSWin: function(args, pageBased, afterCreateHandler, afterCloseHandler, beforeCloseHandler) {
			var $modal = $(Runner.pages.PageManager.getBSModalMarkup.call(this)),
				$header = $(".modal-header", $modal),
				$body = $(".modal-body", $modal),
				$footer = $(".modal-footer", $modal),
				pageObj = this,
				mobileMode = $(window).width() < 768,
				cssSizeToPx = function(cssProp, cssSize) {
					var pxVal = 0,
						el = $('<div style="position:absolute;left:-9999px;top:-9999px;margin:0px;padding:0px;border:0px;"></div>');
					$(document.body).append(el);
					if (cssProp == 'width') {
						el.width(cssSize);
						pxVal = el.width();
					} else if (cssProp == 'height') {
						el.height(cssSize);
						pxVal = el.height();
					}
					el.remove();
					return pxVal;
				},
				modalWrapper = { // user api #12145
					_obj: $modal,
					close: function() {
						$modal.modal("hide");
					},
					destroy: function() {
						$modal.modal("hide");
					},
					setWidth: function(width) {
						width = cssSizeToPx('width', Runner.cssSize(width));
						$(".modal-dialog", $modal).width(width);
						$(".modal-content", $modal).width( width );
						$body.css( "width", width );
						//Runner.pages.PageManager.correctBSWindowSize.call( this, $modal, pageObj);
					},
					// user API
					setHeight: function(height) {
						height = cssSizeToPx('height', Runner.cssSize(height));
						if ( height > window.innerHeight - 60 ) {
							$(".modal-dialog", $modal).css('margin', '0px auto');
						}

						// 2 this border top + border bottom
						$(".modal-content", $modal).height( height ? height - 2 : 0 );
						if ( height - ( $footer.outerHeight() + $header.outerHeight() ) > 0 ) {
							$body.css( "height", height - ( $footer.outerHeight() + $header.outerHeight() ) );
						}
					},
					setPos: function(x, y) {
						if (x) {
							$(".modal-dialog", $modal).css("margin-left", "0px");
							$(".modal-dialog", $modal).css("left", x + "px");
						}

						if (y) {
							$(".modal-dialog", $modal).css("margin-top", "0px");
							$(".modal-dialog", $modal).css("top", y + "px");
						}
					},
					body: function() {
						return $body;
					},
					header: function() {
						return $header;
					},
					footer: function() {
						return $footer;
					},
					getContextNode: function() {
						return $modal;
					},
					getContentNode: function() {
						return $(".modal-content", $modal);
					},
					show: function() {
						$modal.modal("show");
					},
				};

			if (pageBased) {
				this.pageCont = $modal;
				this.bsWin = $modal;
				this.winObject = modalWrapper;
			}

			// for normal empty header
			if (args.headerContent == "") {
				args.headerContent = '<h2></h2>';
			}


			$header.append(args.headerContent);
			$header.append($('<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>'));
			$header.children( '.close' ).on( 'touchend', function() {
				$(this).click();
				return false;
			} )
			$body.html(args.bodyContent);
			$footer.append(args.footerContent);

			if (!args.width) {
				args.width = 800;
			}

			var pxHeight = cssSizeToPx('height', args.height);
			var pxWidth = cssSizeToPx('width', args.width);

			if ( pxHeight && !mobileMode ) {
				if ( pxHeight > window.innerHeight - 60 ) {
					$(".modal-dialog", $modal).css('margin', '0px auto');
				}

				// 2 this border top + border bottom
				$(".modal-content", $modal).height( pxHeight - 2 );
			}

			if ( pxWidth && !mobileMode ) {
				$modal.css('padding-left', '0px');
				$modal.css('padding-right', '0px');

				$(".modal-dialog", $modal).width( pxWidth );
				$(".modal-content", $modal).width( pxWidth );

				$body.css( "width", pxWidth );
			}

			if (args.x) {
				$(".modal-dialog", $modal).css("margin-left", "0px");
				$(".modal-dialog", $modal).css("left", args.x + "px");
			}

			if (args.y) {
				$(".modal-dialog", $modal).css("margin-top", "0px");
				$(".modal-dialog", $modal).css("top", args.y + "px");
			} else if ( args.centered && pxHeight && !mobileMode ) {
				var topOffset = ( window.innerHeight - pxHeight ) / 2;
				if (topOffset > 0) {
					$(".modal-dialog", $modal).css("margin-top", "0px");
					$(".modal-dialog", $modal).css("top", topOffset + "px");
				}
			}

			$modal.on("shown.bs.modal", function() {
				if ( pxHeight &&  pxHeight - ( $footer.outerHeight() + $header.outerHeight() ) > 0 ) {
					$body.css( "height", pxHeight - ( $footer.outerHeight() + $header.outerHeight() ) );
				}

				if (afterCreateHandler) {
					afterCreateHandler(modalWrapper);
				}

				if (pageBased) {
					pageObj.fireEvent("windowSizeCorrected");
				}
			});

			if (args.closeByButton) {
				$modal.modal({
					backdrop: 'static',
					keyboard: false,
					show: !args.hidden,
				});
			}

			$modal.appendTo(document.body);

			if (!args.hidden) {
				$modal.modal("show");
			}

			if (args.noHeader) {
				$header.hide();
			}

			if (args.noFooter) {
				$footer.hide();
			}

			Runner.setZindexMax(parseInt($modal.css('z-index'), 10));


			$modal.on("hide.bs.modal", function(e) {
				if (beforeCloseHandler && !beforeCloseHandler(modalWrapper)) {
					e.preventDefault();
					e.stopImmediatePropagation();
					return false;
				}
			});

			$modal.on("hidden.bs.modal", function() {
				$modal.data("bs.modal", null);

				if (pageBased) {
					$('#shiny_box').not("._hintBox").hide();

					if (typeof pageObj.hideSearchControllerContainers === "function") {
						pageObj.hideSearchControllerContainers();
					}
				}

				if (afterCloseHandler) {
					afterCloseHandler(modalWrapper);
				}

				$modal.remove();

				// not to interfere with other open windows
				$(".bs-popup").each(function(ind, el) {
					var omData = $(el).data("bs.modal");

					if (!omData) {
						return;
					}

					omData.checkScrollbar();
					omData.setScrollbar();
					omData.$body.addClass('modal-open');
				});
			});

			$(".modal-dialog", $modal)
				.draggable({
					handle: ".modal-header",
					start: function(event, ui) {
						Runner.trackUserActivity();
					}
				});

			$(".modal-content", $modal)
				//.css("overflow", "hidden")
				.resizable({
					minWidth: 200,
					minHeight: 150,
					resize: function(event, ui) {
						$header.css("width", ui.element.innerWidth() + "px");
						$footer.css("width", ui.element.innerWidth() + "px");
						$body.css("width", ui.element.innerWidth() + "px");
						$body.css("height", (ui.element.innerHeight() - $header.outerHeight() - $footer.outerHeight()) + "px");
						$modal.toggleClass("bs-smallpopup", ui.element.innerWidth() < Runner.pages.constants.SMALL_MODAL_WIDTH);
					},
					handles: "n, e, s, w, ne, nw, se, sw",
					start: function(event, ui) {
						Runner.trackUserActivity();
					}
				});

			if (Runner.isDirRTL()) {
				var rchange = $(".modal-content", $modal).data("uiResizable")._change;
				rchange.e = function(event, dx) {
					var cs = this.originalSize, sp = this.originalPosition;
					return { left: sp.left + dx, width: cs.width + dx };
				};
				rchange.w = function(event, dx) {
					return { width: this.originalSize.width - dx };
				};
			}

			$body.on("scroll", function(e) {
				if (typeof pageObj.hideSearchControllerContainers === "function") {
					pageObj.hideSearchControllerContainers();
				}
			});

			return modalWrapper;
		},

		/**
		 *
		 */
		correctBSWindowSize: function(bsWin, pageObj) {
			if (pageObj && pageObj.popupSizeIsCorrected) {
				return;
			}

			if (pageObj && pageObj.controlsMap && pageObj.controlsMap.multistep ) {
				pageObj.adjustHeightAndWidth();
				return;
			}

			var winDim = Runner.getWindowDimensions(),
				halfWinWidth, halfWinHeight,
				$modalContentBox = $(".modal-content", bsWin),
				$modalBody = $(".modal-body", bsWin),
				mWidth, mHeight, mWidthDelta;

			//	don't correct window sizes on small screens.
			if (winDim.width < 768) {
				if ($modalContentBox.height() < winDim.height) {
					$modalBody.height($modalBody.height() - $modalContentBox.height() + winDim.height - 2); // 2 for borders
					$modalContentBox.height(winDim.height - 2); // 2 for borders
				}

				bsWin.css("paddingLeft", "");

				if ($modalContentBox.width() < winDim.width) {
					$modalBody.width(winDim.width - 2 - $modalBody.innerWidth() + $modalBody.width()); // 2 for borders
					$modalContentBox.width(winDim.width - 2); // 2 for borders
				}
				return;
			}

			halfWinWidth = Math.floor(winDim.width * 0.9); 	
			halfWinHeight = Math.floor(winDim.height * 0.9 ); 

			mWidth = $modalContentBox.width();
			mWidthDelta = mWidth - $modalBody.width();
			mHeight = $modalContentBox.height();

			if (halfWinWidth > 100 && mWidth > halfWinWidth) {
				$modalContentBox.width(halfWinWidth);
				$modalBody.width(halfWinWidth - mWidthDelta);
			}

			if (halfWinHeight > 100 && mHeight > halfWinHeight) {
				$modalBody.height($modalBody.height() - mHeight + halfWinHeight);
				$modalContentBox.height(halfWinHeight);
			}

			// update width for scroll
			$modalBody.width($modalContentBox.width() - $modalBody.outerWidth() + $modalBody.width());

			// aligning by center
			$modalContentBox.parent().width($modalContentBox.width());

			bsWin.toggleClass("bs-smallpopup", $modalContentBox.width() < Runner.pages.constants.SMALL_MODAL_WIDTH);
		},

		/**
		 * Add a window 'onunload' event handler
		 * @param {function} hn
		 * @param {object} scope
		 * @param {object} args
		 */
		addUnloadHn: function(hn, scope, args) {
			if (typeof hn !== 'function') {
				return;
			}
			// beforeUnloadPool is a scope variable
			beforeUnloadPool.push({
				hn: hn,
				scope: scope || window,
				args: args || []
			});
		},

		/**
		 * The window 'onunload' event handler
		 * unregistering the loaded pages
		 */
		callUnload: function() {
			// beforeUnloadPool and tables are scope variables
			var i, handlerData, tName;

			for (i = 0; i < beforeUnloadPool.length; i++) {
				handlerData = beforeUnloadPool[i];
				handlerData.hn.apply(handlerData.scope, handlerData.args);
			}

			for (tName in tables) {
				this.unregister(tName);
			}
		},

		updateMedia: function(media) {
			Object.keys(tables).forEach(function(table) {
				Object.keys(tables[table]).forEach(function(pageId) {
					tables[table][pageId].updateMedia(media);
				});
			})
		}
	}
}();
// Create namespace
Runner.namespace('Runner.pages');


// Assign extra Runner object's properties
(function() {
	if ( !window.settings || !window.settings.global || typeof window.settings.global !== 'object' ) {
		return;
	}
	
	Runner.debugMode = window.settings.global.debugMode;

	Runner.setIdCounter( window.settings.global.idStartFrom );
	Runner.charSet = window.settings.global.charSet;	
})();


Runner.pages.PageControlsMap = function() {
	window.controlsMap = window.controlsMap || {};

	return {	
		addMap: function( tName, pageType, pageId, map ) {
			if (!tName || !pageType || typeof pageId == 'undefined') {
				return false;
			}
			if ( !controlsMap[ tName ] ) {
				controlsMap[ tName ] = {};
			}
			if ( !controlsMap[ tName ][ pageType ] ) {
				controlsMap[ tName ][ pageType ] = {};
			}
			
			if ( map ) {
				controlsMap[ tName ][ pageType ][ pageId ] = map[ tName ][ pageType ][ pageId ];
			}
		},
		
		getMap: function( tName, pageType, pageId ) {
			if ( !tName ) {
				return controlsMap;
			}
			if ( !pageType ) {
				return controlsMap[ tName ];
			}
			if (typeof pageId == 'undefined') {
				return controlsMap[ tName ][ pageType ];
			}
			return controlsMap[ tName ][ pageType ][ pageId ];
		},
		
		removeMap: function( tName, pageType, pageId ) {
			if ( !tName || !pageType || typeof pageId == "undefined" ) {
				return false;
			}
			if ( !controlsMap[ tName ] ) {
				return false;
			}
			if ( !controlsMap[ tName ][ pageType ] ) {
				return false;
			}
			delete controlsMap[ tName ][ pageType ][ pageId ];
			return true;
		}
	};
}();

Runner.pages.PageViewControlsMap = function() {
	window.viewControlsMap = window.viewControlsMap || {};
	
	return {		
		addMap: function( tName, pageType, pageId, map ) {
			if ( !tName || !pageType || typeof pageId == 'undefined' ) {
				return false;
			}
			if ( !viewControlsMap[ tName ] ) {
				viewControlsMap[ tName ] = {};
			}
			if ( !viewControlsMap[ tName ][ pageType ] ) {
				viewControlsMap[ tName ][ pageType ] = {};
			}
			
			if ( map ) {
				viewControlsMap[ tName ][ pageType ][ pageId ] = map[ tName ][ pageType ][ pageId ];
			}
		},
		
		getMap: function( tName, pageType, pageId ) {
			if ( !tName ) {
				return viewControlsMap;
			}
			if ( !pageType ) {
				return viewControlsMap[ tName ];
			}
			if ( typeof pageId == 'undefined' ) {
				return viewControlsMap[ tName ][ pageType ];
			}
			return viewControlsMap[ tName ][ pageType ][ pageId ];
		},
		
		removeMap: function( tName, pageType, pageId ) {
			if ( !tName || !pageType || typeof pageId == "undefined" ) {
				return false;
			}
			if ( !viewControlsMap[ tName ] ) {
				return false;
			}
			if ( !viewControlsMap[ tName ][ pageType ] ) {
				return false;
			}
			delete viewControlsMap[ tName ][ pageType ][ pageId ];
			return true;
		}
	};
}();

/**
 * Page settings store base constructor
 * It gets page's settings from the window.settings object
 * @singletone
 */
Runner.pages.PageSettings = Runner.extend( Runner.emptyFn, {
	/**
	 * Get the page's settings	
	 * @param {string} tName	 The table's name
	 * @param {string} fName 	 The field's name
	 * @param {string} pageType	 The page's type (list, add, edit etc.)
	 */
	getSettings: function( tName, fName, pageType ) {
		if ( !tName ) {
			return settings;
		}
		
		if ( !fName ) {
			return settings.tableSettings[ tName ];
		}
		
		return settings.tableSettings[ tName ][ "fieldSettings" ][ fName ][ pageType ];
	},	
	
	/**
	 * Checks setting if they are already exists
	 * @param {string} tName
	 * @param {string} fName 
	 * @param {string} pageType
	 * @return {boolean}
	 */
	checkSettings: function( tName, fName, pageType ) {
		return !!this.getSettings( tName, fName, pageType );
	},
	
	/**
	 * 
	 */
	fieldSettingsExist: function( tName, fName, key, pageType ) {
		if ( typeof settings.tableSettings[ tName ] !== 'undefined' &&
			 typeof settings.tableSettings[ tName ][ "fieldSettings" ][ fName ] !== 'undefined' &&
			 typeof settings.tableSettings[ tName ][ "fieldSettings" ][ fName ][ pageType ] !== 'undefined' &&
			 typeof settings.tableSettings[ tName ][ "fieldSettings" ][ fName ][ pageType ][ key ] !== 'undefined' ) {
			
			return true;
		}	
		return false;
	},
	
	/**
	 * Add the page's events ("afterPageReady", "afterInit")
	 * @param {string} tName
	 * @param {string} pType
	 * @param {string} evName
	 * @param {function} evHn
	 */
	addPageEvent: function( tName, pType, evName, evHn ) {
		if ( !settings.tableSettings[ tName ] ) {
			settings.tableSettings[ tName ] = {};
		}
		if ( !settings.tableSettings[ tName ]["events"] ) {
			settings.tableSettings[ tName ]["events"] = {};
		}
		if ( !settings.tableSettings[ tName ]["events"][ pType ] ) {
			settings.tableSettings[ tName ]["events"][ pType ] = {};
		}
		if ( !settings.tableSettings[ tName ]["events"][ pType ][ evName ] ) {
			settings.tableSettings[ tName ]["events"][ pType ][ evName ] = [];
		}
		settings.tableSettings[ tName ]["events"][ pType ][ evName ].push( {hn: evHn} );
	},
	
	/**
	 * Add settings from data
	 * @param {string} tName
	 * @param {object} addSettings
	 * @param {boolean} forceRewrite
	 */
	addSettings: function( tName, addSettings, forceRewrite ) {
		if ( !addSettings ) {
			return;
		}
		
		var settings = this.getSettings();
		//Runner.setIdCounter( settings.global.idStartFrom );
		// add short table names
		Runner.apply( settings.global.shortTNames, addSettings.global.shortTNames );

		if ( forceRewrite === true ) {
			// just replace
			settings.tableSettings[ tName ] = addSettings.tableSettings[ tName ];
			return;
		}
				
		// recursively apply if settings
		if ( !tName ) {
			Runner.deepCopy( settings.tableSettings, addSettings.tableSettings );
		} else {
			settings.tableSettings[ tName ] = settings.tableSettings[ tName ] || {};
			Runner.deepCopy( settings.tableSettings[ tName ], addSettings.tableSettings[ tName ] );
		}				
	},
	
	getViewType: function( tName, fName, pageType ) {
		return this.getFieldData(tName, fName, "viewFormat", pageType);
	},
	
	getEditFormat: function( tName, fName, pageType ) {
		return this.getFieldData(tName, fName, "editFormat", pageType);
	},
	
	getShortTName: function( tName ) {
		if ( !tName ) {
			return "";
		}
		return settings.global.shortTNames[ tName ] || ( settings.tableSettings[ tName ] && settings.tableSettings[ tName ].shortTName ) || "";
	},
	
	getValidations: function( tName, fName, pageType ) {
		if ( this.fieldSettingsExist(tName, fName, "validation", pageType) ) {
			return this.getFieldData(tName, fName, "validation", pageType);
		}
		return  Runner.pages.Defaults.getValidationSettings();	 
	},
	
	getLookupTable: function( tName, fName, pageType ) {
		return this.getFieldData(tName, fName, "lookupTable", pageType);
	},
	
	getLookupSize: function( tName, fName, pageType ) {
		return this.getFieldData(tName, fName, "selectSize", pageType);
	},
	
	getLCT: function( tName, fName, pageType ) {
		return this.getFieldData(tName, fName, "lcType", pageType);
	},
	
	getImageWidth: function( tName, fName, pageType ) {
		var nReturn = this.getFieldData(tName, fName, "imageWidth", pageType);
		return nReturn || null;
	},
	
	getImageHeight: function( tName, fName, pageType ) {
		var nReturn = this.getFieldData(tName, fName, "imageHeight", pageType);
		return nReturn || null;
	},

	/**
	 * Get the table's events data
	 * @param {string} tName
	 * @param {string} pageType
	 * @return {object}
	 */
	getTableEventsData: function( tName, pageType ) {
		var events = this.getTableData(tName, "events");
		return events[ pageType ] || {};	
	},
	
	/**
	 * Get the table's settings by the key value
	 * @param {string} tName
	 * @param {string} fName
	 * @return {mixed}
	 */
	getTableData: function( tName, key ) {
		if (typeof settings.tableSettings[ tName ] != 'undefined' && typeof settings.tableSettings[ tName ][ key ] != 'undefined') {
			return this.returnData( settings.tableSettings[ tName ][ key ] );
		}
		return this.returnData( Runner.pages.Defaults.getTableSettings( key ) );
	},
	
	/**
	 * Get the field's settings by the key value
	 * @param {string} tName
	 * @param {string} fName
	 * @param {string} key
	 * @param {string} pageType
	 * @return {mixed}
	 */
	getFieldData: function( tName, fName, key, pageType ) {
		if ( this.fieldSettingsExist( tName, fName, key, pageType ) ) {
			return this.returnData( settings.tableSettings[ tName ]["fieldSettings"][ fName ][ pageType ][ key ] );
		}
		return  this.returnData( Runner.pages.Defaults.getFieldSettings( key ) );
	},
	
	/**
	 * Get the global settings by the key value
	 * @param {string} key
	 * @return {mixed}
	 */
	getGlobalData: function( key ) {
		if ( settings.global[ key ] != undefined ) {
			return this.returnData( settings.global[ key ] );
		}
		return this.returnData( Runner.pages.Defaults.getGlobalSettings( key ) );
	},
	
	/**
	 * It returns the variable itself or a corresponding 
	 * object-copy basing on the type of the variable passed
	 * @param {mixed} data
	 */
	returnData: function( data ) {
		if ( Runner.isArray( data ) ) {
			// return the array's shallow copy
			return data.slice(0);
		}
		if ( data && typeof data === "object" ) {
			// return the new empty object with all 'data' object properties 
			// accessible through the prototype chain
			var F = function() {};
			F.prototype = data;
			return new F();
		}
		return data;
	}
});

Runner.pages.PageSettings = new Runner.pages.PageSettings();

Runner.pages.ajaxEvents = {};
Runner.pages.fieldsEvents = {};

Runner.registerAjaxEvent = function( gid, before, after ) {
	Runner.pages.ajaxEvents[ gid ] = function( pageObj, proxy, pageid, inlineRow, inlineObject ) { 
		var params = {}, 
			result = {}, 
			ret, reqParams, fieldsData = {},
			row = new Runner.AjaxRow( pageObj, inlineRow ),
			ajax = new Runner.form.Button({ id: gid, btnName: gid }),
			submit = function() {
				params["table"] = pageObj.tName;
				params["masterTable"] = pageObj.masterTable;
				params.page = pageObj.pageName;
			
				params["fieldsData"] = {};
				( Runner.controls.ControlStorage.byId( inlineRow.id ) || [] ).forEach( function( ctrl ) {
					if ( !( ctrl instanceof  Runner.controls.MultiUploadField ) && !( ctrl instanceof Runner.controls.FileControl ) ) {
						params["fieldsData"][ ctrl.fieldName ] = ctrl.getValue();
					}
				});	
				
				reqParams = {
					params: JSON.stringify( params ), 
					keys: JSON.stringify( inlineRow.keys ), 
					pageType: pageObj.pageType,
					event: gid	
				};
				
				if ( pageObj.masterTable ) {
					reqParams.masterTable = pageObj.masterTable;
					reqParams = Runner.apply( reqParams, pageObj.masterKeys );
				}			
				
				$.post( Runner.getPageUrl("buttonhandler"), reqParams, function( rawResult ) {
					try {
						result = JSON.parse( rawResult );
					} catch(e) {				
						Runner.displayGenericAjaxError( rawResult );				
					}
				
					// execute EventAfter code
					after.call( pageObj, pageObj, params, result, ajax, row, pageid );
				})
				.fail( function( jqXHR, textStatus, errorThrown ) {
					Runner.displayGenericAjaxError( jqXHR.responseText || textStatus + ' ' + errorThrown  );
				});
			};
			
		ajax.submitHandler = submit;
		ajax.submit = submit;
		ret = before.call( pageObj, pageObj, params, ajax, row, pageid );
		if ( ret === false ) {
			return;
		}
		submit();
	
	}
}

Runner.registerFieldEvent = function( gid, before, after ) {
	Runner.pages.fieldsEvents[ gid ] = function( pageObj, pageid, row ) {
		if ( row ) {
			row = new Runner.AjaxRow( pageObj, row );
		}
		var ret, reqParams,
			ctrl = this,
			fieldsData = {},
			params = {},
			ajax = new Runner.form.Button({ id: gid, btnName: gid }),
			submit = function() {
				params["table"] = pageObj.tName;
				if( pageObj.tName !== pageObj.pageTable ) {
					params["pageTable"] = pageObj.pageTable;
				}
				params["field"] = ctrl.fieldName;
				params.page = pageObj.pageName;
				
				( Runner.controls.ControlStorage.byId( pageid ) || [] ).forEach( function( ctrl ) {
					if ( !( ctrl instanceof  Runner.controls.MultiUploadField ) && !( ctrl instanceof Runner.controls.FileControl ) ) {
						fieldsData[ ctrl.fieldName ] = ctrl.getStringValue();
					}
				});
				
				reqParams = {
					params: JSON.stringify( params ), 
					event: gid,
					pageType: pageObj.pageType,
					keys: JSON.stringify( pageObj.keys ? pageObj.keys : ( row && row.getKeys() ) ),
					fieldsData: JSON.stringify( fieldsData )
				};
				
				if ( pageObj.masterTable ) {
					reqParams.masterTable = pageObj.masterTable;
					reqParams = Runner.apply( reqParams, pageObj.masterKeys );
				}			
				
				$.post( Runner.getPageUrl("buttonhandler"), reqParams, function( result ) {
					var _result;
					try {
						_result = JSON.parse( result );
					} catch(e) {				
						Runner.displayGenericAjaxError( result );				
					}
					// execute EventAfter code
					after.call( ctrl, _result, ctrl, pageObj, ajax,  params, pageid );				
				})
				.fail( function( jqXHR, textStatus, errorThrown ) {
					Runner.displayGenericAjaxError( jqXHR.responseText || textStatus + ' ' + errorThrown  );
				});			
	
				submit = function(){};
			};
		
		ajax.submitHandler = submit;
		ajax.submit = submit;
		// execute EventBefore code	
		ret = before.call( this, params, ctrl, pageObj, ajax, pageid );
		if ( ret === false ) {
			return;
		}
	
		submit();
	};
}

		
/**
 * @ignore
 * @param {object} baseCfg
 * @return {object}
 */
Runner.pages.PageFactory = function( baseCfg ) {
	var cfg = Runner.apply( {}, baseCfg );

	switch ( cfg.pageType ) {
		case Runner.pages.constants.PAGE_ADMIN_RIGHTS:
			return new Runner.pages.RightsPage( cfg );
		case Runner.pages.constants.PAGE_ADMIN_MEMBERS:
			return new Runner.pages.MembersPage( cfg );
		case Runner.pages.constants.PAGE_ADMIN_ADMEMBERS:
			return new Runner.pages.MembersPageAD( cfg );
		case Runner.pages.constants.PAGE_LIST:
			if ( cfg.pageMode === Runner.pages.constants.LIST_DASHBOARD || cfg.pageMode === Runner.pages.constants.LIST_DASHDETAILS ) {
				return new Runner.pages.ListPageDash( cfg );
			}
			if ( cfg.pageMode === Runner.pages.constants.MODE_LIST_DETAILS ) {
				return new Runner.pages.ListPageDP( cfg );
			}
			if ( cfg.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
				return new Runner.pages.ListPageFly( cfg );
			}
			if ( cfg.pageMode === Runner.pages.constants.LIST_AJAX ) {
				cfg.openMode = Runner.pages.constants.OPENMODE_PRELOADED_CONTAINER;
				return new Runner.pages.ListPageAjax( cfg );
			}

			return new Runner.pages.ListPage( cfg );

		case Runner.pages.constants.PAGE_ADD:
			if ( cfg.editType === Runner.pages.constants.ADD_ONTHEFLY ) {
				return new Runner.pages.AddPageFly( cfg );
			}
			if ( cfg.pageMode === Runner.pages.constants.EDIT_DASHBOARD ) { //TODO: change the dash add page's mode constant
				return new Runner.pages.AddPageDash( cfg );
			}
			if( cfg.baseParams?.page == 'add_calendar' ) {
				return new Runner.pages.AddPageCalendar( cfg );
			}
			return new Runner.pages.AddPage( cfg );

		case Runner.pages.constants.PAGE_EDIT:
			if ( cfg.editType === Runner.pages.constants.EDIT_ONTHEFLY ) {
				return new Runner.pages.EditPageFly( cfg );
			}
			if ( cfg.pageMode === Runner.pages.constants.EDIT_DASHBOARD ) {
				return new Runner.pages.EditPageDash( cfg );
			}
			if ( cfg.pageMode == Runner.pages.constants.EDIT_SELECTED_SIMPLE || cfg.pageMode == Runner.pages.constants.EDIT_SELECTED_POPUP ) {
				return new Runner.pages.EditSelectedPage( cfg );
			}
			if( cfg.baseParams?.page == 'edit_calendar' ) {
				return new Runner.pages.EditPageCalendar( cfg );
			}
			if( cfg.baseParams?.gantt ) {
				return new Runner.pages.EditPageGantt( cfg );
			}
			return new Runner.pages.EditPage( cfg );

		case Runner.pages.constants.PAGE_LOGIN:
			return new Runner.pages.LoginPage( cfg );

		case Runner.pages.constants.PAGE_REMIND:
			return new Runner.pages.RemindPage( cfg );

		case Runner.pages.constants.PAGE_VIEW:
			if( cfg.baseParams?.gantt ) {
				return new Runner.pages.ViewPageGantt( cfg );
			}
			if( cfg.baseParams?.page == 'view_calendar' ) {
				return new Runner.pages.ViewPageCalendar( cfg );
			}
			return new Runner.pages.ViewPage( cfg );

		case Runner.pages.constants.PAGE_SEARCH:
			return new Runner.pages.SearchPage( cfg );

		case Runner.pages.constants.PAGE_REPORT:
			if ( cfg.pageMode === Runner.pages.constants.MODE_LIST_DETAILS ) {
				return new Runner.pages.ReportPageDP( cfg );
			}
			return new Runner.pages.ReportPage( cfg );

		case Runner.pages.constants.PAGE_CHART:
			if ( cfg.pageMode === Runner.pages.constants.MODE_LIST_DETAILS ) {
				return new Runner.pages.ChartPageDP( cfg );
			}
			
			if ( cfg.openMode === Runner.pages.constants.OPENMODE_DASHCONTAINER ) {
				return new Runner.pages.ChartPageDash( cfg );
			}

			if ( cfg.openMode === Runner.pages.constants.OPENMODE_DASHTABS ) {
				return new Runner.pages.ChartPageDPDash( cfg );
			}

			return new Runner.pages.ChartPage( cfg );
		case Runner.pages.constants.PAGE_CALENDAR:
			return new Runner.pages.CalendarPage( cfg );
		case Runner.pages.constants.PAGE_GANTT:
			return new Runner.pages.GanttPage( cfg );
		
		case Runner.pages.constants.PAGE_REGISTER:
			return new Runner.pages.RegisterPage( cfg );

		case Runner.pages.constants.PAGE_CHANGEPASS:
			return new Runner.pages.ChangePwdPage( cfg );

		case Runner.pages.constants.PAGE_EXPORT:
			return new Runner.pages.ExportPage( cfg );

		case Runner.pages.constants.PAGE_IMPORT:
			return new Runner.pages.ImportPage( cfg );

		case Runner.pages.constants.PAGE_PRINT:
			return new Runner.pages.PrintPage( cfg );

		case Runner.pages.constants.PAGE_REPORT_PRINT:
			return new Runner.pages.ReportPrintPage( cfg );

		case Runner.pages.constants.PAGE_DASHBOARD:
			if( cfg.pageMode === Runner.pages.constants.DASHMODE_SNIPPET ) {
				cfg.pageType = 'snippet';
				return new Runner.pages.RunnerPage( cfg );
			}
			return new Runner.pages.DashboardPage( cfg );

		case Runner.pages.constants.PAGE_DASHMAP:
			if ( cfg.pageMode === Runner.pages.constants.GRIDBASED_MAP_DASHBOARD ) {
				return new Runner.pages.DashboardGridBasedMap( cfg );
			}
			return new Runner.pages.DashboardLeadingMap( cfg );
			
		case Runner.pages.constants.PAGE_USERINFO:
			return new Runner.pages.UserInfoPage( cfg );
			
		case Runner.pages.constants.PAGE_SESSION_EXPIRED:
			return new Runner.pages.SessionExpiredPage( cfg );

		default:
			return new Runner.pages.RunnerPage( cfg );
	}
};

/**
 * Get the generated page's url
 * @param {string} tName
 * @param {string} pageType
 * @param {array} keys
 * @param {string} keyPref
 * @param {object} paramsData
 * @return {string}
 */
Runner.pages.getUrl = function( tName, pageType, keys, keyPref, paramsData ) {	
	var url = Runner.pages.getBaseUrl( tName, pageType ),
		params = [];
			
	keys = keys || [];
	paramsData = paramsData || {};
	if ( !keys.length && !Object.keys( paramsData ).length ) {
		return url;
	}

	keyPref = keyPref || "editid";
	keys.forEach( function( key, i ) {
		params.push( keyPref + ( i + 1 ) + "=" + encodeURIComponent( key ) );
	});
	
	Object.keys( paramsData || {} ).map( function( name ) {
		params.push( name + "=" + encodeURIComponent( paramsData[ name ] ) );
	});
	
	return url + "?" + params.join( "&" );
};

Runner.pages.getBaseUrl = function( tName, pageType ) {
	pageType =  pageType === 'rprint' ? 'print' : pageType; 
	if( pageType === 'snippet' ) {
		pageType = 'dashboard';
	}
	
	var tableBased = tName !== Runner.pages.constants.NOT_TABLE_BASED_TNAME
			&& pageType !== Runner.pages.constants.PAGE_REGISTER;
			
	return tableBased
			? Runner.getPageUrl( Runner.pages.PageSettings.getShortTName( tName ) || tName, pageType )
			: Runner.getPageUrl( pageType );	
}

/**
 * Base abstract class for all pages
 * @abstract
 * @class Runner.pages.RunnerPage
 * @base Runner.util.Observable
 * @intellisense
 */
Runner.pages.RunnerPage = Runner.extend( Runner.util.Observable, {
	/** 
	 * Table name 
	 * @intellisense
	 */
	tName: '',

	/**
	 * Page Type
	 * @intellisense
	 */
	pageType: '',

	/**
	 * Page name from Page Designer
	 * @intellisense
	 */
	pageName: '',

	/**
	 * current page - default
	 * @intellisense
	 */
	isDefaultPage: true,

	/**
	 * Default pages for each types
	 * @intellisense
	 */
	defaultPages: [],

	/**
	 * 
	 */
	modal: false,

	/**
	 * Page id
	 * @intellisense
	 */
	pageId: -1,

	controlsMap: null,

	viewControlsMap: null,

	/**
	 * Not a preloaded page's html header content
	 * @intellisense
	 */
	headerCont: "",

	/**
	 * Not a preloaded page's html body content 
	 * @intellisense
	 */
	bodyCont: "",

	/**
	 * Not a preloaded page's html footer content
	 * @intellisense
	 */
	footerCont: "",

	/**
	 * The flag showing if the popup window's
	 * position and size don't need the correction
	 * @type {boolean}
	 */
	popupSizeIsCorrected: false,

	/**
	 * Page container DOM element
	 *
	 * For simple page - body
	 * For details - td or div
	 * For fly page - div (use it instead of winDiv)
	 * @intellisense
	 */
	pageCont: null,

	/**
	 * The list of bricks, which must be reloaded after ajax
	 * @intellisense
	 */
	bricksForReload: [],

	proxy: null,

	useAsGlobal: false,

	shortTName: "",

	useLocking: false,

	useAudio: false,

	useResize: false,

	bricksForMobileFly: [],

	buttonNames: [],

	buttonEventBefore: {},

	buttonEventAfter: {},

	validateTimer: null,

	pageModified: false,

	oldLayout: false,

	layoutName: "",

	/**
	 * The flaf indicating if there are some Tabs on the page
	 * @type {boolean}
	 */
	tabsPresented: false,

	/**
	 * The flag indicating if ajax search suggests should be used
	 * @type {boolean}
	 */
	useSearchSuggests: false,

	/**
	 * The flag indicating if to warn the user 
	 * on leaving the pages with unsaved data 
	 * @type {boolean}
	 */
	warnOnLeaving: false,

	/**
	 * The array of functions need to be run in order to initialize the page.
	 * Use addInitTask() to and runInitQueue() to add and run tasks.
	 * @type {array}
	 */
	initTaskQueue: null,

	/** 
	 * Index of the current running task from the initTaskQueue array
	 * @type {number}
	 */
	currentInitTaskIndex: -1,

	/** 
	 * The variable is used to keep track of any asynchronous calls made during the current task execution.
	 * @type {number}
	 */
	initSubtasksCreated: 0,
	initSubtasksFinished: 0,

	/**
	 * @type {number}
	 */
	openMode: null,

	/**
	 *
	 * @type {string}
	 */
	message: "",

	/**
	 * @type {number}
	 */
	currentStep: 0,

	reloadTimerId: null,

	chartObj: null,

	silentReloading: false,

	masterTable: "",
	masterKeys: null,

	/**
	 *	Page-related data passed from server code
	 */
	pageData: {},

	layoutHelper: {},

	fieldItems: {},

	/**
	 *	$gridTabControl property is used with Grid Tabs feature only. Contains UL element with grid tabs.
	 */
	$gridTabControl: null,

	/**
	 * Array of itemIds in the leftbar form
	 */
	leftBarItems: null,
	
	baseParams: null,
	
	/**
	 * Session expire page id
	 */
	expirePageId: null,

	/**
	 * Runner.Notifications object when NotificationsAPI is enabled
	 */
	notifications: null,

	multipleHint: null,

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.apply( this, cfg );
		Runner.pages.PageManager.register( this );

		this.addEvents( 'beforeInit', 'afterInit', 'afterClose' );

		this.id = this.pageId;
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		this.pageName = this.pageData.pageName;

		this.layoutHelper = new Runner.PDLayout( this, {
			formMap: this.pageData.helperFormItems,
			itemsByType: this.pageData.helperItemsByType,
			gridCellMap: this.pageData.cellMaps,
			itemVisiblity: this.pageData?.helperFormItems?.itemVisiblity || {},
			fieldItems: this.pageData.fieldItems,
		} );
		//	server returns desktop-prepared page
		if ( this.pageData.renderedMediaType != Runner.getMediaType() ) {
			if ( this.pageData.mobileSub 
				&& this.openMode == Runner.pages.constants.OPENMODE_PRELOADED ) {
				location.reload();
			}

			this.updateMedia( Runner.getMediaType() );
		}
		set_cookie( 'mediaType', Runner.getMediaType(),'', Runner.projectRoot() );

		this.fieldItems = this.pageData.helperFieldItems || {};

		this.useResize = Runner.pages.PageSettings.getTableData( this.tName, "isUseResize" );
		this.useLocking = Runner.pages.PageSettings.getTableData( this.tName, "locking" );
		this.warnOnLeaving = Runner.pages.PageSettings.getTableData( this.tName, "warnOnLeaving" );
		this.shortTName = Runner.pages.PageSettings.getShortTName( this.tName );
		
		this.masterTable = this.pageData["masterTable"];
		this.masterKeys = this.pageData["masterKeys"];

		/* .NET returns null instead of {} here */
		this.defaultPages = Runner.pages.PageSettings.getTableData( this.tName, "defaultPages" ) || {};		
		this.isDefaultPage = this.defaultPages[this.pageType] == this.pageName;

		this.oldLayout = this.controlsMap && this.controlsMap.oldLayout;
		this.layoutName = this.controlsMap ? this.controlsMap.layoutName : "";
		this.tabsPresented = !!this.getTabsCtrlsCount();
		// set default page container
		this.pageCont = document.body;

		this.pageTable = this.controlsMap && this.controlsMap.pageTable 
			? this.controlsMap.pageTable
			: this.tName;

		this.multipleHint = $(".rnr-sorthint", document.body);

		if ( this.afterInit ) {
			this.on( { 'afterInit': this.afterInit } );
		}

		if( this.pageData.notifications && this.isPageToControlSession() ) {
			this.notifications = new Runner.Notifications( this, this.pageData.notifications );
		}

		// preapre initialization queue
		this.initTaskQueue = [];
		this.addInitTask( this.init );

	},

	initCustomButtons: function() {
		( this.pageData.buttons || [] ).forEach( function( buttonId ) {
			Runner.buttonEvents[ buttonId ].call( this, this, this.proxy, this.id );
		}, this );
	},
	
	/**
	 * Initialize custom button for reloaded page
	 */
	reinitGridCustomButtons: function() {
		var gridElem = this.findForm( 'grid' );
		( this.pageData.buttons || [] ).forEach( function ( buttonId ) {
			if ( !$('[data-event=' + buttonId + ']', gridElem ).length ) {
				return;
			}
			var idx = this.buttonNames.indexOf( buttonId );
			if ( idx !== -1 ) {
				this.buttonNames.splice( idx, 1 );
			}
			
			Runner.buttonEvents[ buttonId ].call( this, this, this.proxy, this.id );
		}, this );	
	},	

	/**
	 * The page's destructor
	 */
	destructor: function() {
		// unregister page Object, clean memory
		this.destroyWin();

		this.purgeListeners();
		Runner.controls.ControlStorage.unregisterById( this.pageId );

		if ( this.reloadTimerId ) {
			clearTimeout( this.reloadTimerId );
		}

		if ( this.chartObj && this.chartObj.reloadIntervalId ) {
			clearInterval( this.chartObj.reloadIntervalId );
		}
	},

	/**
	 * Add a function(s) to the tail of the page initialization queue.
	 * @param {function|array} task	- the function or an array of functions to add
	 * The task will be called with this context. 
	 * Asynchronous calls within tasks should be made to this.asyncInitCall( func ) instead of just func.
	 * 
	 * Example:	 
	 * addInitTask( function() {
	 *		if (this.noAjaxNeeded) {
	 *			return;
	 *		}
	 *		$.post(page, params, this.asyncInitCall( function( response ) {
	 *			if ( response.finished ) {
	 *				return;
	 *			}
	 *			$.post(page, otherparams, this.asyncInitCall( function( response ) {
	 *			}
	 *		})
	 *	});
	 */
	addInitTask: function( task ) {
		if ( Runner.isArray( task ) ) {
			this.initTaskQueue = this.initTaskQueue.concat( task );
		} else {
			this.initTaskQueue.push( task );
		}
	},

	/**
	 * Add a function(s) to the head of the page initialization queue.
	 * @param {function|array} task	- the function or an array of functions to add
	 * See addInitTask
	 */
	prependInitTask: function( task ) {
		if ( Runner.isArray( task ) ) {
			this.initTaskQueue = task.concat( this.initTaskQueue );
		} else {
			this.initTaskQueue.splice( 0, 0, task );
		}
	},

	/**
	 * Auxiliary function for the initialization routine. 
	 * See the addInitTask function.
	 * @param {function} func
 	 */
	asyncInitCall: function( func ) {
		var pageObj = this;

		++this.initSubtasksCreated;
		return function() {
			if ( typeof func === "function" ) {
				func.apply( pageObj, arguments );
			}

			++pageObj.initSubtasksFinished;

			//	the task has been finished
			if ( pageObj.initSubtasksFinished == pageObj.initSubtasksCreated ) {
				pageObj.goInitQueue();
			}
		}
	},

	/**
	 * Run or continue the initialization queue. 
 	 */
	goInitQueue: function() {
		// run tasks while there are any
		while ( ++this.currentInitTaskIndex < this.initTaskQueue.length ) {
			this.initSubtasksFinished = 0;
			this.initSubtasksCreated = 1;

			this.initTaskQueue[this.currentInitTaskIndex].call( this );
			++this.initSubtasksFinished;

			if ( this.initSubtasksFinished !== this.initSubtasksCreated ) {
				// do nothing for now. Continue with initialization when the async subtask is finished
				return;
			}
			// task finished, do the next task
		}
	},
	
	/**
	 * Insert necessary parameters
	 */
	updatePageUrl: function() {
		const url = new URL( window.location.href );
		if( !this.isDefaultPage && !url.searchParams.has( 'page' ) ) {
			url.searchParams.set( 'page', this.pageName );
			window.history.replaceState( null, '', url );
		}

		if( this.pageData.tabId && !url.searchParams.has( 'tab' ) ) {
			url.searchParams.set( 'tab', this.pageData.tabId );
			window.history.replaceState( null, '', url );
		}
	},

	/**
	 * Initialize the page's basic functionality
	 */
	init: function() {
		var pageObj = this;

		this.updatePageUrl();

		this.fireEvent( 'beforeInit', this );
		
		this.initSessionControl();

		//	init buttons
		this.initCustomButtons();

		this.initMenu();
		this.initEvents();
		this.initRunnerButtons();
		this.initLoginButtons();
		this.hideFieldsHiddenByUser();

		this.initLanguageSelector();
		this.initCollapseButtons();

		//	initTabs must be called before initForTabs
		this.initTabsSections();
		this.initForTabs();

		this.createCharts();

		// init if reCaptcha control defined
		this.initReCaptcha();

		this.initPdfMake();

		this.initSearch();

		if ( !this.useResize ) {
			this.initForGrid();
		}
		
		this.initMasterInfoGrid();
		this.initMoreText();

		if ( this.checkPageBarsToInit() ) {
			this.initCoockieConsentBanner();
			
			this.initLeftScrollBar();
			this.initCollapseButton();
		}

		if ( this.warnOnLeaving
			&& this.openMode !== Runner.pages.constants.OPENMODE_POPUP ) {
			//warn users about leaving the page with changes unsaved
			$( window ).on( 'beforeunload', function() {
				return pageObj.beforeUnloadHn();
			});
		}
		this.replaceGridTabs();
		this.initGridTabs();
		this.updateDropdownButtons();
		this.initSnippetReload();
		this.initViewLinks();

	},

	initSnippetReload() {
		if( !this.dashboard || this.pageType !== 'snippet' ) {
			return;
		}
		if ( this.dashboard.getElementByName( this.dashElement )?.reload ) {

			if ( this.reloadTimerId ) {
				window.clearTimeout( this.reloadTimerId );
			}
			
			this.reloadTimerId = window.setTimeout( 
				() =>  this.dashboard.doTimerReloadAction( this.dashElement ),
				1000 * this.dashboard.getElementByName( this.dashElement ).reload
			);
		}

	},
	
	/**
	 * Send requests to control logged user session 
	 */
	initSessionControl: function() {
		var sessionControl = Runner.pages.PageSettings.getGlobalData("sessionControl");
		if ( !sessionControl || !this.isPageToControlSession() ) {
			return;
		}
		
		if ( sessionControl["clickTracking"] && sessionControl["forceExpire"] ) {
			
			["click", "touchstart"].forEach( function( eventName ) {
				$( this.pageCont ).get(0).addEventListener( eventName, function(e) {
						var $target = $( e.target );
						// trace only a/button(s) captured events
						if ( $target.closest("a").length || $target.closest("button").length ) {
							//ignore session expired page click events? 
							Runner.trackUserActivity();
						}
					}, true ); // useCapture = true 
			}, this );
		}

		// 60 seconds interval delay for sesion control requests
		var requestDelay = sessionControl["requestDelay"] * 1000,
			pageObj = this;	
		
		if ( sessionControl["keepAlive"] ) {
			window.setInterval( function() {
					Runner.runnerAJAX( Runner.getPageUrl( "", "sessioncontrol" ), {
							sessionControl: "keepAlive",
						}, function( resp ) {
							// process response ...
						});	
				}, requestDelay );
			
		} else if ( sessionControl["forceExpire"] ) {
			Runner.lastUserActivity = Date.now();
			this.sendForceExpiredRequest();
			
			window.setInterval( function() {
					pageObj.sendForceExpiredRequest();
				}, requestDelay );
		}
	},

	/**
	 * Time in ms before session expiration
	 */
	msTimeLeftUntilSessionExpired: function() {
		var sessionControl = Runner.pages.PageSettings.getGlobalData("sessionControl");
//		return Runner.lastUserActivity + sessionControl["warnBeforeSeconds"] * 1000 - Date.now();
		return Date.now() - Runner.lastUserActivity + sessionControl["warnBeforeSeconds"] * 1000 ;
	},
	
	/**
	 * Send a request to control inactivity time
	 */
	sendForceExpiredRequest: function() {
		var pageObj = this;
		var sessionControl = Runner.pages.PageSettings.getGlobalData("sessionControl");
		
		Runner.runnerAJAX( Runner.getPageUrl("sessioncontrol"), {
				sessionControl: "forceExpire",
				lastUserActivity: Math.floor( Runner.lastUserActivity / 1000 ),
				warnBeforeSeconds: sessionControl["warnBeforeSeconds"],
			}, function( resp ) {
				if ( resp.showExpirePopup ) {
					if ( !Runner.pages.PageManager.getById( pageObj.expirePageId ) ) {
						pageObj.expirePageId = pageObj.openSessionExpiredPopup( Date.now() + 1000 * resp.secondsLeft );
					}
				} else if ( resp.logout ) {
					pageObj.sessionControlLogout( true );
				}
			}, true );	
	},
	
	/**
	 * @param {boolean} showExpiredMessage
	 */
	sessionControlLogout: function( showExpiredMessage ) {
		this.sessionExpired = true;
		window.location.href = this.getLogoutUrl( showExpiredMessage );		
	},
	
	/**
	 * Check if page is allowed to control user session
	 * @return {boolean}
	 */
	isPageToControlSession: function() {
		return this.openMode === Runner.pages.constants.OPENMODE_PRELOADED;
	},
	
	/**
	 * Open expired session page in popup
	 * @param {number} logoutOn
	 * @return {stirng}
	 */
	openSessionExpiredPopup: function( logoutOn ) {
		var pageParams = {
			pageType: Runner.pages.constants.PAGE_SESSION_EXPIRED,
			tName: Runner.NOT_TABLE_BASED_TNAME,
			pageId: -1,
			modal: true,
			hostPageObj: this,
			noHeader: true,
			noFooter: true,
			baseParams: {
				parId: this.id,
				mode: "popup"
			},
			requestUrl: Runner.getPageUrl( "session_expired" ),
			// do not track user activity 
			skipTrackingActivity: true,
			logoutOn: logoutOn
		};

		return Runner.pages.PageManager.openPage( pageParams );		
	},

	/**
	 * Сheck whether to initialize left scrollbar and coockie banner
	 * @return {boolean}
	 */
	checkPageBarsToInit: function() {
		return this.openMode === Runner.pages.constants.OPENMODE_PRELOADED;
	},
	
	/**
	 * Add cookie consent
	 */	
	initCoockieConsentBanner: function() {
		if ( settings.global["useCookieBanner"] && !window.localStorage.getItem( "cookie_consent" ) ) {
			var $banner = $( '<div class="r-cookie-banner"></div>' );
			$banner.html( settings.global["cookieBanner"] );
			var $cookieButton = $( '<button class="btn btn-default btn-sm"></button>' )
				.html( Runner.lang.constants.OK );
			
			$banner.append( ' ' );
			$banner.append( $cookieButton );
			$( 'body' ).append( $banner );
			
			window.setTimeout( function() { $banner.animate( { bottom: 0 }, 250 ); }, 300 );
			
			$cookieButton.click( function() {
				window.localStorage.setItem( 'cookie_consent', true );
				$banner.remove();
			});
		}	
	},
	
	/**
	 * init left scrollbar
	 */
	initLeftScrollBar: function() {
		var pageObj = this;
		
		// init left scrollbar if needed
		$( "[data-fixed][data-resize-name]" ).each( function() {
			var $mainElem = $( this );
			var resizeName = $mainElem.data( 'resize-name' );
			var topMarginElements = $( '[data-resize-top="' + resizeName + '"]' );
			var leftMarginElements = $( '[data-resize-left="' + resizeName + '"]' );
			ResizeSensor( this, function() {
				if ( Runner.mobileScreen() ) {
					topMarginElements.css( 'margin-top', "" );
					leftMarginElements.css( 'margin-' + Runner.left, "" );
				} else {
//							var height = $mainElem.children().height();
					var height = $mainElem.height();
					var width = $mainElem.width();
					topMarginElements.css( 'margin-top', height + 'px' );
					leftMarginElements.css( 'margin-' + Runner.left, width + 'px' );
				}
			} );
		} );

		$( ".r-vbar-page > .r-left" ).mCustomScrollbar({
			theme: "minimal-dark",
			scrollInertia: 0,
			axis: "y"
		});
		
		$( window ).on("scroll", function(e) {
			pageObj.hideSearchControllerContainers();
		});
		
		Runner.setZindexMax( parseInt( $( ".r-vbar-page > .r-left" ).css("z-index"), 10 ) );		
	},
	
	/**
	 * @return {boolean}
	 */
	isPageInTab: function() {
		return this.openMode === Runner.pages.constants.OPENMODE_DASHTABS || this.openMode === Runner.pages.constants.OPENMODE_TABVIEW;
	},

	/**
	 * Hide rows if user already hide any field
	 * Set 'hiddenByField' to true for the corresponding controls
	 */
	hideFieldsHiddenByUser: function() {
		var pageObj = this;

		$( '.' + this.makeClassName( 'hiddenfield' ) ).each( function( idx, elem ) {
			if ( pageObj.oldLayout ) {
				pageObj.toggleParentRow( elem );
			}

			var fieldName = $( elem ).data( 'fieldname' ),
				ctrl = Runner.getControl( pageObj.pageId, fieldName );

		});
	},

	/**
	 * Toggle parent row display setting for element 
	 * @param {DOMElement} element
	 * @intellisense
	 */
	toggleParentRow: function( element ) {
		if ( !this.oldLayout ) {
			return;
		}

		$( element ).parents().each( function() {
			var $row = $( this );

			if ( $row.hasClass( 'runner-toprow' ) || $row.hasClass( 'runner-bottomrow' ) ) {
				return false;
			}
			if ( $row.hasClass( 'runner-row' ) ) {
				$row.addClass( 'runner-hiddenfield' );
				return false;
			}
		});
	},

	/**
	 * Initialize language selector elements
	 */
	initLanguageSelector: function() {
		var pageType = this.pageType,
			$languageLink = $( ".rnr-langlink", this.pageCont ),
			$langSelector = $( "#languageSelector", this.pageCont ),
			prefix = $langSelector.data( "prefix" ),
			extraparams = $langSelector.data( "params" ) || "",
			pageUrl, pageParam;

		if ( prefix === "dchart" || prefix === "dreport" ) {
			pageType = prefix;
		}

		pageUrl = Runner.pages.getUrl( this.pageTable, pageType );
		pageParam = this.pageData.pageName ? "&page=" + this.pageData.pageName : "";
		
		$languageLink.on( "click", function( e ) {
			window.location.href = pageUrl + "?language=" + $( this ).data( "lang" ) + pageParam;
			return false;
		} );


		$langSelector.on( "change", function( e ) {
			window.location.href = pageUrl + "?" + extraparams + "language=" + this.options[this.selectedIndex].value + pageParam;
		} );
	},

	/**
	 * Initialize the page's functionality that need initializing after tabs are loaded.
	 * If there are no tabs on the page it's invoked from the page's init method 
	 * otherwise from the initTabs method
	 * @intellisense
	 */
	initForTabs: function() {
		this.initControls();
		this.initViewControls();
		this.initLookups();
		this.initChangeCtrlEvents();
	},

	/**
	 * Init functionality for grid
	 * @intellisense
	 */
	initForGrid: function() {
		if ( Object.getPrototypeOf( this ) === Runner.pages.RunnerPage.prototype ) {
			this.fireEvent( 'afterInit', this, this.proxy, this.id );
		}
	},
	
	/**
	 *
	 */
	getMasterGrid: function() {
		return  this.findItemType( "master_info" );
	},
	
	/**
	 * Init master_info grid basic js functionality 
	 */
	initMasterInfoGrid: function() {
		// optional check
		if ( this.pageData.hasMasterList ) {
			this.initMoreText( this.getMasterGrid() );
			this.initMasterViewControls();
		}	
	},
	
	/**
	 *  @param {jQuery object} container
	 *  	To delegate click processing
	 */
	initMoreText: function( $container ) {
		if ( !$container || !$container.length ) {
			$container = this.getPageGrid();
		}
		
		$container
			.off("click.opentext")
			.on("click.opentext", this.largeTextOpenerDelegate.bind( this ) );		
	},

	/**
	 * @return {jQeury object}
	 */
	getPageGrid: function() {
		return $( this.findForm( 'grid' ) );
	},
	
	/**
	 *	init functionality for reCaptcha 
	 */
	initReCaptcha: function() {
		if ( !this.hasReCaptcha() || !Runner.globalReCaptchaLoader ) {
			return;
		}

		Runner.globalReCaptchaLoader.onLoad( function() {
			this.reCaptcha = new Runner.controls.ReCaptcha( this.controlsMap.reCaptcha );
			this.reCaptcha.init();
		}, this );
	},

	/**
	 * @return boolean
	 */
	hasReCaptcha: function() {
		return this.controlsMap && !!this.controlsMap.reCaptcha;
	},

	/**
	 *	init PrintFriendly
	 */
	initPrintFriendly: function() {
		var pageObj = this,
			$toggler = $( "[id=print_friendly_togger" + this.id + "]" ),
			$panelBlock = $( "#printpanelblock" + pageObj.id );

		$toggler.off( "click" ).on( "click", function( e ) {
			$toggler.attr( "data-icon", $toggler.attr( "data-icon" ) == "printdown" ? "printup" : "printdown" );
			$panelBlock.toggleClass( 'open' );
		} );

		$( "#printpanelblock" + this.id + " > .dropdown-menu" ).off( "click" ).on( "click", function( e ) {
			e.stopPropagation();
		} );

		$( "[id=print_friendly_button" + this.id + "]" ).off( "click" ).on( "click", function( e ) {
			var pageName = $( this ).data( "page" ) || "";
			$toggler.attr( "data-icon", $toggler.attr( "data-icon" ) == "printdown" ? "printup" : "printdown" );
			$panelBlock.toggleClass( 'open' );

			pageObj.printerFriendlyBtnHandler( pageName );
			return false;
		} );

		$( "#printDetailsToggler" + this.id ).off( "click" ).on( "click", function( e ) {
			$( "#printdetailstables" + pageObj.id ).toggle();
		} );
	},

	printPanelParams: function() {
		var $scope = $( "#print_friendly_scope" + this.id + " input[type='radio'][name='printScope']:checked" ),
			scopeId = $scope.attr( "id" ),
			$detailsBox = $( '#printDetailsToggler' + this.id ),
			$records = $( '#printPerPage' + this.id ),
			$detailsTablesBoxes = $( "input[type=checkbox]", "#printdetailstables" + this.id ),
			details = [],
			params = {};

		if ( $records.length ) {
			params.records = $records.val()
		}

		//	details
		if ( $detailsBox.is( ':checked' ) ) {
			$detailsTablesBoxes.each( function( n, box ) {
				if ( box.checked ) {
					details.push( box.value );
				}
			} );
			params["details[]"] = details;
		}

		//	selection
		if ( scopeId == "printSelection" + this.id ) {
			params["selection[]"] = this.getSelectedRecordKeys().map( function( keys ) {
				if ( keys instanceof Array) {
					return keys.map( function(k) {
						return encodeURIComponent( k );
					})
					.join( '&' );
				} else {
					return keys;
				}
			} );
		}

		if ( scopeId == "printAll" + this.id ) {
			params.all = 1;
		}
		return params;
	},

	getSelectedRecordKeys: function() {
		var selection = [];
		this.getSelBoxes( this.id ).filter( ':checked' ).each( function() {
			selection.push( $( this ).val() );
		} );
		return selection;
	},
	
	/**
	 *
	 */
	printerFriendlyBtnHandler: function( pageName ) {
		var requestParams = {
			standardSubmit: true,
			submitUrl: Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_PRINT, "", "", this.getStateParams() ),
			target: '_blank',
			method: 'POST',
			id: this.id,
			baseParams: this.printPanelParams()
		};	
		requestParams.baseParams["tab"] = this.pageData.tabId;
		requestParams.baseParams["a"] = "print";
		if ( pageName ) {
			requestParams.baseParams["page"] = pageName;
		}

		var form = new Runner.form.BasicForm( requestParams );
		form.submit();
		form.destructor();
	},
	
	/**
	 * The PageSettings.getSettings method's wrapper
	 * @param {string} tName
	 * @param {string} fName
	 * @intellisense
	 */
	getSettings: function( tName, fName ) {
		return Runner.pages.PageSettings.getSettings( tName, fName, this.pageType );
	},

	/**
	 * The PageSettings.checkSettings method's wrapper
	 * @param {string} tName
	 * @param {string} fName	 
	 * @intellisense
	 */
	checkSettings: function( tName, fName ) {
		return Runner.pages.PageSettings.checkSettings( tName, fName, this.pageType );
	},

	/**
	 * Reset the inline display style in order to 
	 * ':hover' display css rules will be applied 
	 * @param {jQuery object} $buttonGroup
	 */
	resetButtonGroupDisplayStyle: function( $buttonGroup ) {
		setTimeout( function() {
			$buttonGroup.css( 'display', '' );
		}, 500 );
	},

	/**
	 * Update runner buttons 'disabled' status
	 * @intellisense
	 */
	initRunnerButtons: function() {
		$( "." + this.makeClassName( 'button' ) ).each( function() {
			if ( $( this ).attr( 'type' ) === 'disabled' ) {
				Runner.addDisabledClass( this );
			}
		} );

		$( "." + this.makeClassName( 'button-img' ) ).each( function() {
			if ( $( this ).attr( 'type' ) === 'disabled' ) {
				Runner.addDisabledClass( this );
			}
		} );

		// for lock all panels
		/*$(".dropdown-menu").on("click", function(e) {
			//e.stopPropagation();
		});*/
	},

	/**
	 * Initialize the page's 'login' buttons
	 */
	initLoginButtons: function() {
		var $loginButton,
			pageObj = this,
			loginFormType = Runner.pages.PageSettings.getGlobalData( "loginFormType" );

		$loginButton = $( "[id=loginButton" + this.id + "]" ).on( "click", function(e) {
			if ( loginFormType === Runner.pages.constants.LOGIN_POPUP ) {
				pageObj.openLoginPage();
			} else if ( loginFormType === Runner.pages.constants.LOGIN_EMBEDED ) {
				pageObj.submitEmbedLoginForm();
			} else if ( loginFormType === Runner.pages.constants.LOGIN_SEPARATE ) {
				window.location.href = Runner.pages.getUrl( Runner.pages.constants.NOT_TABLE_BASED_TNAME, "login" ) + "?return=true";
			}
			return false;
		} );

		$( 'input[name="password"]', "#login_block" + this.id ).on( "keydown", function(e) {
			if ( e.keyCode == 13 ) {
				$loginButton.trigger( "click" );
				return false;
			}
		} );
		
		$( "[id=logoutButton" + this.id + "]" ).on( "click", function(e) {
			window.location.href = pageObj.getLogoutUrl();
			return false;
		});

		if ( $( "ul.dropdown-menu > li", "#login_block" + this.id ).length < 1 ) {
			$( "[data-toggle=dropdown]", "#login_block" + this.id ).removeAttr( "data-toggle" );
		}

		$( "[id=loginButtonContinue" + this.id + "]" ).on( "click", function(e) {
			pageObj.openLoginPage( '', '', false, true );
			return false;
		} );
		
		
		$("[id=changePasswordButton" + this.id + "]" ).on( "click", function(e) {
			var pageParams = Runner.pages.ChangePwdPage.prototype.getOpenPageParams.call( pageObj );	
			Runner.pages.PageManager.openPage( pageParams );			
			
			e.preventDefault();
		});		
		
	},
	
	/**
	 * @param {boolean} expired
	 * @return {string}
	 */ 
	getLogoutUrl: function( expired ) {
		return Runner.pages.getUrl( this.pageTable, this.pageType ) 
			+ "?a=logout" + ( expired ? "&reason=expired" : "" );
	},

	/**
	 * Submit the embedded login form's data
	 */
	submitEmbedLoginForm: function() {
		var passwordValue, usernameValue,
			$loginBlock = $( "#login_block" + this.id );
			
		if ( $loginBlock.length ) {
			passwordValue = $( 'input[name="password"]', $loginBlock ).val();
			usernameValue = $( 'input[name="username"]', $loginBlock ).val();
		} else {
			passwordValue = $( '#password' + this.id ).val();
			usernameValue = $( '#username' + this.id ).val();
		}

		this.submitAndOpenLoginPopup( usernameValue, passwordValue );
	},	
	
	/**
	 * Submit username/password 
	 * and open popup on errors or for 2f auth
	 * @param {string} usernameValue
	 * @param {string} passwordValue
	 * @param {string} restore ( to restore session on add/edit )
	 * @param {object} hostPage
	 */
	submitAndOpenLoginPopup: function( usernameValue, passwordValue, restore, hostPage ) {
		var pageObj = this;

		var pageParams = {
			pageType: Runner.pages.constants.PAGE_LOGIN,
			tName: Runner.NOT_TABLE_BASED_TNAME,
			pageId: -1,
			modal: true,
			userval: usernameValue,
			passval: passwordValue,
			hostPageObj: hostPage || this,
			hiddenPopup: true,
			baseParams: {
				a: "Login",
				mode: "popup",
				username: usernameValue,
				password: passwordValue,
			},
			requestUrl: Runner.getPageUrl( "login" )
		};
		
		if ( restore ) {
			pageParams.restore = restore;
			pageParams.baseParams.restore = restore;
		}

		pageParams.beforeCreateHn = function() {
			// hook before create popup
			if ( this.pageData.redirectUrl ) {
				window.location.href = this.pageData.redirectUrl;
			} else {
				this.hiddenPopup = false;
			}
		}
		
		Runner.pages.PageManager.openPage( pageParams );		
	},
	
	/**
	 * Initialize the page's login link
	 */
	initLoginLink: function() {
		var $loginLink = $( "[id=loginButtonContinue" + this.id + "]", this.pageCont ),
			pageObj = this;

		if ( !$loginLink.length ) {
			$loginLink = $( "#loginButtonContinue", this.pageCont );
		}

		$loginLink.on( "click", function( e ) {
			pageObj.openLoginPage( '', '', false, true );
			return false;
		} );
	},

	/**
	 * Open the login page in a popup window
	 * @param {string} user
	 * @param {string} pass
	 * @param {funtion} afterCreateHn
	 * @param {boolean} restore ( to restore session on add/edit )
	 * @param {object} hostPage
	 */
	openLoginPage: function( user, pass, afterCreateHn, restore, hostPage ) {
		var pageParams = {
			pageType: Runner.pages.constants.PAGE_LOGIN,
			tName: Runner.NOT_TABLE_BASED_TNAME,
			pageId: -1,
			modal: true,
			userval: user,
			passval: pass,
			hostPageObj: hostPage || this,
			baseParams: {
				parId: this.id,
				mode: "popup"
			},
			requestUrl: Runner.getPageUrl( "login" )
		};
		
		if ( restore ) {
			pageParams.baseParams.restore = "session";
			pageParams.restore = "session";
		}

		if ( typeof afterCreateHn === "function" ) {
			pageParams.afterCreateHn = afterCreateHn;
		}

		Runner.pages.PageManager.openPage( pageParams );
	},

	/**
	 * Initialize the page's menu blocks
	 */
	initMenu: function() {
		var pageObj = this,
			menuSelector = '.r-menu.menu-treelike',
			toggleSelector = '[data-toggle="menu-collapse"]',
			iconSelector = Runner.smallScreen ? '.r-menu-mobile-expand-icon' : '.r-menu-expand-icon',
			$menus = $( menuSelector + ':not([data-initialized])', this.pageCont );
		
		$menus.on( 'click', toggleSelector, function(e) {
				//	add triangle direction handler
				var $triangle = $( iconSelector, this ),
					$link = $(this),
					$target = $( $link.attr('data-target') ), 
					expand = $link.attr( 'aria-expanded' ) !== 'true',
					$menu = $link.closest('.r-menu');
					
				var collapsedBar = $link.closest('.r-left-collapsed').length != 0;
				if ( collapsedBar ) {
					expand = true;
				}
				
				// hide submenus in collapsed left bar mode
				$menu.find( 'ul' ).removeClass('r-most-recent');
				$target.addClass('r-most-recent');
				$target.parents('ul').addClass('r-most-recent');

				$link.toggleClass( 'r-menu-opened', expand );

				$link.attr('aria-expanded', expand );

				if ( !collapsedBar ) {
					$target.collapse( expand ? 'show' : 'hide' );
				} else {
					$target
						.toggleClass('in', expand )
						.css( 'height', '' );
						
					$link.attr( 'aria-expanded', expand ? 'true' : 'false' );
				}
				e.preventDefault();
				e.stopPropagation();
			})
			.find( '[data-toggle="menu-collapse"]' ).each( function() {
				//	set initial triangle direction
				var	$link = $(this),
					$triangle = $( iconSelector, this ),
					$target = $( $( this ).data( 'target' ) ),
					collapsed = !$target.hasClass( 'in' );

				$link.toggleClass( 'r-menu-opened', !collapsed );
				$(this).attr( 'aria-expanded', collapsed ? 'false' : 'true' );

			} )
			.attr( 'data-initialized', 'true' );
		/**
		 * close treelike submenus on document click in 
		 */
		$( document ).off('click.collapsed.treemenu').on( 'click.collapsed.treemenu' , function(e) {
			if ( $('.r-left-collapsed').length != 0 &&  $(e.target).closest('[data-itemtype="menu"]').length == 0 ) {
				pageObj.closeTreeSubmenus();
			}
		});
		
		/**
		 * close subtree menus on loading
		 */
		if ( $('.r-left-collapsed').length != 0  ) {
			pageObj.closeTreeSubmenus();
		}


	},

	/**
	 * 
	 */
	destroyVideo: function() {
		if ( typeof videojs === "undefined" ) {
			return;
		}

		var hasMasterList = this.pageData.hasMasterList ,
			pageObj = this;

		$( '.video-js', this.pageCont ).each( function() {
			if ( !hasMasterList || !$( this ).parents( '.' + pageObj.makeClassName( 'c-master' ) ).length ) {
				var player = videojs( this.id );
				player.pause();
				player.dispose();
			}
		} );
	},

	/**
	 * @return {boolean}
	 */
	pageHasCharts: function() {
		return !!$( '[data-runner-chart-params]', this.pageCont ).length;
	},

	/**
	 * Create page's charts
	 */
	createCharts: function() {
		$( '[data-runner-chart-params]', this.pageCont )
			.each( function() {
				Runner.Charts.createChart( $( this ).data( 'runner-chart-params' ) );
			} )
			.remove();
	},

	/**
	 * Load extra files
	 */
	loadFiles: function() {
		if ( window.Promise === undefined ) {
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/polyfill.min.js'] );
		}
		
		if ( this.hasReCaptcha() ) {
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/runnerJS/ReCaptcha.js'] );	
		}
		
		if ( this.pageHasCharts() ) {
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'libs/js/anychart.min.js'] );
			
			Runner.util.ScriptLoader.loadCSS( [settings.global["webRootPath"] + 'libs/js/anychart-ui.min.css'] );
			Runner.util.ScriptLoader.loadCSS( [settings.global["webRootPath"] + 'libs/js/anychart-font.min.css'] );
		}

		if ( Runner.pages.PageSettings.getTableData( this.tName, "isUseCK" ) ) {
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'plugins/ckeditor/ckeditor.js'] );
		}

		if ( Runner.pages.PageSettings.getTableData( this.tName, "isUseResize" ) ) {
			Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/colresizable.js'] );
		}

		if ( Runner.pages.PageSettings.getTableData( this.tName, "isUseGoogleMap" ) &&  !Runner.pages.PageSettings.getGlobalData( "staticMapsOnly" ) ) {
			switch ( Runner.pages.PageSettings.getGlobalData( "mapProvider" ) ) {
				case Runner.controls.constants.GOOGLE_MAPS:
					Runner.util.ScriptLoader.addJS([
						settings.global["webRootPath"] + 'include/runnerJS/MapManager.js', 
						settings.global["webRootPath"] + 'include/runnerJS/gmap.js'
					]);
					break;
				case Runner.controls.constants.OPEN_STREET_MAPS:
					Runner.util.ScriptLoader.addJS([
						settings.global["webRootPath"] + 'include/runnerJS/MapManager.js', 
						settings.global["webRootPath"] + 'include/runnerJS/osmap.js', 
						settings.global["webRootPath"] + 'plugins/OpenLayers.js'
					]);
					break;
				case Runner.controls.constants.BING_MAPS:
					Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/runnerJS/MapManager.js', settings.global["webRootPath"] + 'include/runnerJS/bingmap.js'] );
					break;
				case Runner.controls.constants.HERE_MAPS:
					Runner.util.ScriptLoader.addJS([
						settings.global["webRootPath"] + 'include/runnerJS/MapManager.js', 
						settings.global["webRootPath"] + 'include/runnerJS/heremap.js'
					]);
					break;
				case Runner.controls.constants.MAPQUEST_MAPS:
					Runner.util.ScriptLoader.addJS([
						settings.global["webRootPath"] + 'include/runnerJS/MapManager.js', 
						settings.global["webRootPath"] + 'include/runnerJS/mapquest.js'
					]);
					break;
			}
		}

		Runner.util.ScriptLoader.onFilesLoaded( this.asyncInitCall() );
		Runner.util.ScriptLoader.load();
	},

	/**
	 * see Runner.pages.PageManager.createPopup
	 * It's invoked on popup close
	 * @param {object} modalWrapper
	 */
	afterCloseFlyWinHandler: function( modalWrapper ) {
		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP ) {
			return;
		}

		this.destroyVideo();
		this.fireEvent( 'afterClose' );

		this.unregisterDetails();
		Runner.pages.PageManager.unregister( this.tName, this.pageId );
	},

	/**
	 * Unregister details pages
	 */
	unregisterDetails: function() {
		if ( !this.dpObjs ) {
			return;
		}

		var tName, detailsPageObj;

		for ( tName in this.dpObjs ) {
			detailsPageObj = this.dpObjs[tName].detailsPageObj;
			if ( detailsPageObj ) {
				Runner.pages.PageManager.unregister( detailsPageObj.tName, detailsPageObj.pageId );
			}
		}
	},

	/**
	 * Resize and move the popup window basing on
	 * the old popup window params.
	 * @param {object} oldPageObj 	Runner Old Popup page object
	 * @param {boolean} moveOnly
	 */
	resizeAndMoveWindow: function( oldPageObj, moveOnly ) {
		// see #17031
		/*if ( this.pageName != oldPageObj.pageName ) {
			return;
		}*/
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP && this.bsWin && oldPageObj.bsWin ) {
			if ( $( window ).width() < 768 ) { /*Runner.smallScreen*/
				return;
			}

			var $oldDialog = $( ".modal-dialog", oldPageObj.bsWin ),
				$oldModalBody = $( ".modal-body", oldPageObj.bsWin ),
				$oldModalContentBox = $( ".modal-content", oldPageObj.bsWin ),
				$modalContentBox = $( ".modal-content", this.bsWin ),
				$modalBody = $( ".modal-body", this.bsWin );

			if ( !moveOnly ) {	
				$modalContentBox.width( $oldModalContentBox.width() );
				$modalContentBox.height( $oldModalContentBox.height() );

				$modalBody
					.width( $oldModalBody.width() )
					.height( $oldModalBody.height() );

				// update width for scroll
				$modalBody.width( $modalContentBox.width() - $modalBody.outerWidth() + $modalBody.width() );
			}
			
			$( ".modal-dialog", this.bsWin ).css( {
				top: $oldDialog.css( "top" ),
				left: $oldDialog.css( "left" )
			} );

			this.bsWin.toggleClass( "bs-smallpopup", $modalContentBox.width() < Runner.pages.constants.SMALL_MODAL_WIDTH );
			this.popupSizeIsCorrected = true;
		}
	},

	/**
	 * Destroy popup panel
	 * @param {object} bsWin
	 * @intellisense
	 */
	destroyWin: function( bsWin ) {
		bsWin = bsWin || this.bsWin;
		bsWin && bsWin.modal( "hide" );
	},

	/**
	 * Close the page
	 * @intellisense
	 */
	close: function() {
		this.destroyWin();
	},

	/** 
	 * Dummy for function which setting flag for popup editing.
	 * This dummy overrides in EditPage.
	 */
	setRecountFlagForPopup: Runner.emptyFn, //?

	/**
	 * Subscribe handlers for default events (eg "afterPageReady")
	 */
	initEvents: function() {
		var events = Runner.pages.PageSettings.getTableEventsData( this.pageTable, this.pageType ),
			eventHnArr, evName, i;

		for ( evName in events ) {
			eventHnArr = events[evName];
			for ( i = 0; i < eventHnArr.length; i++ ) {
				this.on( evName, eventHnArr[i].hn, eventHnArr[i].scope || this );
			}
		}
	},

	/**
	 * Initialize edit controls 	
	 * @intellisense
	 */
	initControls: function( controlsMap ) {
		controlsMap = controlsMap || this.controlsMap;
		if ( !controlsMap ) {
			return;
		}

		for ( var i = 0; i < ( controlsMap.controls || [] ).length; i++ ) {
			// register page's controls
			Runner.controls.ControlFactory( controlsMap.controls[i], this.pageType, false, this );
		}
	},

	/**
	 * Initialize view controls 
	 * @intellisense
	 */
	initViewControls: function() {
		if ( !this.viewControlsMap || typeof this.viewControlsMap !== "object" ) {
			return;
		}

		this.setupViewContols( this.viewControlsMap.controls, this.pageCont );
	},

	initMasterViewControls: function() {
		if ( !this.pageData.hasMasterList || !this.viewControlsMap.mViewControlsMap
			|| typeof this.viewControlsMap.mViewControlsMap !== "object" ) {
			return;
		}
		
		var context = this.findItemType( "master_info" );
		var pageId = Runner.pages.PageSettings.getTableData( this.tName, "masterPageId" );
		
		this.setupViewContols( this.viewControlsMap.mViewControlsMap.controls, context, pageId );		
	},
	
	/**
	 * @param {array} viewContols
	 * @param {jQuery object | DOM Node Element} pageCont
	 */
	setupViewContols: function( viewContols, pageCont, pageId ) {
		for ( var i = 0; i < ( viewContols || [] ).length; i++ ) {
			( Runner.viewControls.ViewControlFactory( viewContols[i], this.pageType, pageCont, this, pageId ) ).init();
		}
	},

	/**
	 * Initialize lookup wizard controls that has dependencies
	 */
	initLookups: function() {
		var pageCtrls = Runner.controls.ControlManager.getAt( this.tName, this.pageId ), i;

		if ( !pageCtrls ) {
			return;
		}

		for ( i = 0; i < pageCtrls.length; i++ ) {
			pageCtrls[i].setDependencies( this.tName, this.pageId );
		}
	},

	/**
	 * @return Boolean
	 */
	hasMaps: function() {
		return this.controlsMap && this.controlsMap.gMaps && this.controlsMap.gMaps.isUseGoogleMap && Runner.globalGmapLoader;
	},

	/**
	 * @return {number}
	 */
	getTabsCtrlsCount: function() {
		return $( ".panel.with-tabs", this.pageCont ).length;
	},

	/**
	 * @return {array}
	 */
	getTabsCtrls: function() {
		var tabCtrls = [];

		for ( var idx = 0; idx < this.getTabsCtrlsCount(); idx++ ) {
			tabCtrls.push( this.getTabs( idx ) );
		}

		return tabCtrls;
	},

	/**
	 * @param {number} tabControlIdx
	 * @return {object}
	 */
	getTabs: function( tabControlIdx ) {
		tabControlIdx = tabControlIdx || 0;

		var $tabContext = $( ".panel.with-tabs", this.pageCont ).eq( tabControlIdx );
		if ( !$tabContext.length ) {
			return null;
		}

		return new Runner.Tabs( $tabContext );
	},

	/**
	 * Initialize the Tabs and Sections
	 * @intellisense
	 */
	initTabsSections: function() {
		this.initTabs();
		this.initSections();
		this.initSteps();
	},

	/**
	 * Init page's tab-elements
	 */
	initTabs: function() {
		if ( !this.getTabsCtrlsCount() ) {
			return;
		}

		var pageObj = this;

		this.getTabsCtrls().forEach( function( tabCtrl ) {
			for ( var i = 0; i < tabCtrl.count(); i++ ) {
				pageObj.initMoreText( tabCtrl.bodyElement( i ) );
				
				tabCtrl.headerElement( i ).on( "shown.bs.tab", function( e ) {
					pageObj.mapManager && pageObj.mapManager.updateLittleMaps();
				});
			}
		});
	},

	/**
	 * Check if there are sections on the page
	 * @return {boolean}
	 */
	checkIfSectionPresented: function() {
		return !!this.getSectionCount();
	},

	/**
	 * @return {number}
	 */
	getSectionCount: function() {
		return $( ".panel.form-section", this.pageCont ).length;
	},

	/**
	 * @param {number} sectionIdx
	 * @return {object}
	 */
	getSection: function( sectionIdx ) {
		sectionIdx = sectionIdx || 0;

		var $context = $( ".panel.form-section", this.pageCont ).eq( sectionIdx );
		if ( !$context.length ) {
			return null;
		}

		return new Runner.Section( $context );
	},

	/**
	 * bs only
	 */
	getSections: function() {
		var sections = [],
			i, section,
			sc = this.getSectionCount();

		for ( i = 0; i < sc; i++ ) {
			section = this.getSection( i );
			if ( section ) {
				sections.push( section );
			}
		}

		return sections;
	},


	/**
	 * Initilize the page's sections
	 */
	initSections: function() {
		if ( !this.checkIfSectionPresented() ) {
			return;
		}

		this.getSections().forEach( function( section, idx ) {
			this.initMoreText( section.bodyElement() );	

			var pageObj = this;
			section.get().on( "shown.bs.collapse", function( e ) {
				pageObj.mapManager && pageObj.mapManager.updateLittleMaps();
			});
		}, this );
	},

	/**
	 * Initialize multistep page
	 */
	initSteps: function() {
		if ( !this.controlsMap || !this.controlsMap.multistep ) {
			return;
		}

		var $stepnav = $( '#stepnav' + this.id ),
			stepsNumber = $stepnav.children().length,
			pageObj = this, hashStep,
			initialStep = this.controlsMap.initialStep;

		$stepnav.on( 'click', 'li', function() {
			var step = $( this ).data( 'step' );

			if ( step <= pageObj.currentStep ) {
				pageObj.setCurrentStep( step );
				return false;
			}

			pageObj.validateStep( step );
			return false;
		} );

		$( '[id=nextstep' + this.id + ']' ).on( 'click', function() {
			if ( pageObj.currentStep < stepsNumber - 1 ) {
				pageObj.validateStep( pageObj.currentStep + 1 );
			}

			return false;
		} );

		if ( this.openMode == Runner.pages.constants.OPENMODE_PRELOADED ) {
			hashStep = parseInt( location.hash.replace( '#', '' ), 10 );
			if ( hashStep > 1 && hashStep < stepsNumber ) {
				initialStep = hashStep - 1;
			}
		}

		this.setCurrentStep( initialStep );
	},

	/**
	 * @param {number} destStep
	 */
	validateStep: function( destStep ) {
		var stepFields = this.getStepFields( this.currentStep ),
			ddStepFields = [],
			pageObj = this,
			counter, onDuplicatesResolved;

		this.getForm();

		$.each( this.form.fieldControls, function( idx, control ) {
			if ( $.inArray( control.fieldName, stepFields ) !== -1 && control.isSetValidation( "DenyDuplicated" ) ) {
				ddStepFields.push( control.fieldName );
			}
		} );

		if ( !ddStepFields.length ) {
			if ( this.form.validate( stepFields ) ) {
				this.setCurrentStep( destStep );
			}

			return;
		}

		counter = ddStepFields.length;

		onDuplicatesResolved = function( cfName, hasDuplicates ) {
			counter = counter - 1;
			if ( hasDuplicates ) {
				// has Duplications
				pageObj.un( "ddValidationResolved", onDuplicatesResolved );
				return;
			}

			if ( !!counter ) {
				return;
			}

			pageObj.un( "ddValidationResolved", onDuplicatesResolved );

			if ( pageObj.form.validate( stepFields ) ) {
				pageObj.setCurrentStep( destStep );
			}
		};

		this.on( "ddValidationResolved", onDuplicatesResolved );
		this.form.validate( ddStepFields );
	},

	/**
	 * @param {number} stepIdx
	 */
	getStepFields: function( stepIdx ) {
		var stepFields = [];

		//	bad code, we shouldn't rely on DOM for this
		$( "#step_" + stepIdx + "_" + this.id ).find( "[data-fieldname]" )
			.each( function( idx, el ) {
				//	there might be fully numeric field names
				stepFields.push( String( $( el ).data( "fieldname" ) ) );
			} );

		return stepFields;
	},

	/**
	 * @param {number} index
	 */
	setCurrentStep: function( index ) {
		index = parseInt( index );

		if ( this.openMode == Runner.pages.constants.OPENMODE_PRELOADED ) {
			location.hash = "#" + ( index + 1 );
		}

		var $stepNavLinks = $( '#stepnav' + this.id ).children();

		$stepNavLinks.removeClass( 'active' );
		if ( $stepNavLinks.length > index ) {
			$stepNavLinks.eq( index ).addClass( 'active' );
		}

		$( '[id=nextstep' + this.id + ']' ).toggle( index < $stepNavLinks.length - 1 );
		$( '[id=saveButton' + this.id + ']' ).toggle( index >= $stepNavLinks.length - 1 );

		$( ".stepFrame[id^='step_'][id$='_" + this.id + "']" ).hide();
		$( "#step_" + index + "_" + this.id ).show();

		if ( this.mapManager ) {
			this.mapManager.updateLittleMaps();
		}

		this.currentStep = index;
	},

	/**
	 * @param {Array} fNames
	 */
	openFieldTabsSections: function( fNames ) {
		var pageObj = this;

		if ( this.controlsMap && this.controlsMap.multistep ) {
			$( '#stepnav' + this.id ).children().each( function( idx, li ) {
				var stepIdx = $( li ).data( 'step' ),
					stepFields = pageObj.getStepFields( stepIdx );

				for ( var i = 0; i < fNames.length; i++ ) {
					if ( $.inArray( fNames[i], stepFields ) !== -1 ) {
						pageObj.setCurrentStep( stepIdx );
						return false;
					}
				}
			} );
		}

		// open sections containing failed controls
		this.getSections().forEach( function( section ) {
			for ( var i = 0; i < fNames.length; i++ ) {
				if ( section.bodyElement().find( pageObj.getFieldWrapperElement( fNames[i] ) ).length ) {
					section.expand();
					break;
				}
			}
		} );

		// open tab containing failed controls
		this.getTabsCtrls().forEach( function( tabCtrl ) {
			var selected = false;

			for ( var j = 0; j < tabCtrl.count(); j++ ) {
				for ( var i = 0; i < fNames.length; i++ ) {
					if ( tabCtrl.bodyElement( j ).find( pageObj.getFieldWrapperElement( fNames[i] ) ).length ) {
						// select tab
						tabCtrl.activate( j );

						selected = true;
						break;
					}
				}

				if ( selected ) {
					break;
				}
			}
		} );
	},

	/**
	 *
	 */
	adjustHeightAndWidth: function() {
		if ( !this.bsWin || this.popupSizeIsCorrected ) {
			return;
		}

		if ( !this.controlsMap.multistep ) {
			this.adjustPopWindowHeightByContent();
			this.adjustPopWindowWidthByContent();
			return;
		}

		var $allTabs = $( '[id^=step_][id$=_' + this.id + ']' ),
			$visibleTab = $( '[id^=step_][id$=_' + this.id + ']:visible' ),
			maxHeight = 0,
			maxWidth = 0,
			$bigger, $wides;

		if ( !$allTabs.length ) {
			return;
		}

		$allTabs.css( "visibility", "hidden" );

		$allTabs.each( function( idx, elem ) {
			var $elem = $( elem );

			$elem.show();
			if ( maxHeight < $elem.height() ) {
				maxHeight = $elem.height();
				$bigger = $elem;
			}

			if ( maxWidth < /*$elem.width()*/ $elem.get( 0 ).scrollWidth ) {
				maxWidth = $elem.get( 0 ).scrollWidth;
				$wides = $elem;
			}

			$elem.hide();
		} );

		$bigger.show();
		this.adjustPopWindowHeightByContent();
		$bigger.hide();

		$wides.show();
		this.adjustPopWindowWidthByContent();

		$allTabs.css( "visibility", 'visible' ).hide();
		$visibleTab.show();
	},

	/**
	 * Set the popup window height so the all its content will be displayed
	 * @type {string} contentDivClass
	 */
	adjustPopWindowHeightByContent: function( contentDivClass ) {
		if ( !this.bsWin ) {
			return;
		}

		contentDivClass = contentDivClass || "rnr-pagewrapper";

		var $bodyNode, $widgetNode,
			defaultMaxWinHeight = Math.floor( 4 * $( window ).height() / 5 ),
			contentHeight, heightDelta, newWinHeight;

		$bodyNode = $( ".modal-body", this.bsWin );
		$widgetNode = $( ".modal-content", this.bsWin );
		heightDelta = $widgetNode.height() - $bodyNode.height();

		contentHeight = $bodyNode.children( "div." + contentDivClass ).outerHeight( true );
		// never shrink the popup
		newWinHeight = Math.min( contentHeight + heightDelta, defaultMaxWinHeight );

		if ( newWinHeight >= $widgetNode.height() ) {
			$widgetNode.height( newWinHeight );
			$bodyNode.height( newWinHeight - heightDelta );
		}

		this.bsWin.toggleClass( "bs-smallpopup", $widgetNode.width() < Runner.pages.constants.SMALL_MODAL_WIDTH );
	},

	/**
	 * Set the popup window height so the all its content will be displayed
	 * @type {string} contentDivClass
	 */
	adjustPopWindowWidthByContent: function( contentDivClass ) {
		if ( !this.bsWin ) {
			return;
		}

		contentDivClass = contentDivClass || "rnr-pagewrapper";

		var $bodyNode, $widgetNode, $dialog, contentWidth,
			defaultMaxWinWidth = Math.floor( 4 * $( window ).width() / 5 );

		$dialog = $( ".modal-dialog", this.bsWin );
		$bodyNode = $( ".modal-body", this.bsWin );
		$widgetNode = $( ".modal-content", this.bsWin );
		contentWidth = /*this.controlsMap.multistep ? $bodyNode.get(0).scrollWidth :*/ $bodyNode.innerWidth();

		if ( contentWidth > defaultMaxWinWidth ) {
			$bodyNode.width( defaultMaxWinWidth );
			$widgetNode.width( defaultMaxWinWidth );
		} else {
			/*if ( this.controlsMap.multistep ) {
				$bodyNode.width( contentWidth );
			}*/

			$widgetNode.width( contentWidth );
			$dialog.width( contentWidth );
		}
		this.bsWin.toggleClass( "bs-smallpopup", $widgetNode.width() < Runner.pages.constants.SMALL_MODAL_WIDTH );

		if ( $dialog.offset().left + $dialog.width() > $( window ).width() ) {
			$dialog.css({
				left: Math.max( 0, ( $bodyNode.width() - $( window ).width() ) / 2 )
			});
		}
	},

	/**
	 * Set focus to the first visible control
	 * Used when need set focus to the first invalid visible control
	 * @param {number} pageId (optional) 	the page's id
	 * @param {object} ctrls (optional)		invalid controls
	 * @intellisense
	 */
	setFirstFocus: function( pageId, ctrls ) {
		ctrls = ctrls || Runner.controls.ControlManager.getAt( this.tName );
		if ( !ctrls || !ctrls.length ) {
			return false;
		}

		var i, fNamesArr = [],
			pageObj = this,
			spanContIds = [],
			currentStep,
			currnetIndxCtrl,
			firstInvalidIndx = false;

		for ( i = 0; i < ctrls.length; i++ ) {
			if ( ctrls[i] && ctrls[i].isFocusAllowed() ) {
				spanContIds[i] = ctrls[i].spanContId;
			}
		}

		if ( this.controlsMap && this.controlsMap.multistep ) { //TODO: refine code
			var steps = $( '#stepnav' + this.id ).children()
				.map( function( idx, li ) {
					return $( li ).data( 'step' );
				} )
				.toArray();

			for ( var i = 0; i < steps.length; i++ ) {
				$( "span[id^='edit" + ( pageId || this.id ) + "_']" ).each( function() {
					var idx = $.inArray( this.id, spanContIds );

					if ( currentStep === undefined && idx > -1 && $.inArray( ctrls[idx].fieldName, pageObj.getStepFields( steps[i] ) ) > -1 ) {
						if ( ctrls[idx].isInvalid ) {
							currentStep = steps[i];
							currnetIndxCtrl = idx;
							firstInvalidIndx = idx;
						}
					}
				} );

				if ( currentStep !== undefined ) {
					this.setCurrentStep( currentStep );
					ctrls[currnetIndxCtrl].setFocus();
					return true;
				}
			}
		} else if ( this.tabsPresented || this.checkIfSectionPresented ) {
			for ( i = 0; i < ctrls.length; i++ ) {
				if ( ctrls[i].isInvalid ) {
					firstInvalidIndx = firstInvalidIndx || i;
					fNamesArr.push( ctrls[i].fieldName );
				}
			}

			this.openFieldTabsSections( fNamesArr );
		}

		$( "span[id^='edit" + ( pageId || this.id ) + "_']:visible" ).each( function() {
			var indx = firstInvalidIndx !== false ? firstInvalidIndx : $.inArray( this.id, spanContIds );
			if ( indx > -1 ) {
				ctrls[indx].setFocus();
				return false;
			}
		} );

		return true;
	},

	/**
	 * Check if a control passed is the first page's invalid control
	 * @param {object} ctrl
	 * @return {boolean}
	 */
	isFirstInvalidControl: function( ctrl ) {
		return ctrl.spanContId === $( "span[id^='edit" + ctrl.id + "_']:visible:has(div.rnr-error-text:visible):first" ).attr( "id" );
	},

	/**
	 * Check validation for controls
	 * @param {object} ctrls (optional)
	 * @return {object} invalid controls
	 * @intellisense
	 */
	getInvalidControls: function( ctrls ) {
		ctrls = ctrls || Runner.controls.ControlManager.getAt( this.tName );
		if ( !ctrls || !ctrls.length ) {
			return [];
		}

		var indx, i, validateAction,
			invalidCtrls = [];

		for ( i = 0; i < ctrls.length; i++ ) {
			validateAction = ctrls[i].validate();
			if ( ctrls[i].isInvalid || !validateAction.result ) {
				invalidCtrls.push( ctrls[i] );
			}
		}
		return invalidCtrls;
	},

	getFieldWrapperElement: function( fieldName ) {
		var $result = $( "[data-fieldname='" + fieldName + "']" );

		if ( $result.length ) {
			return $result;
		}

		// one more try for bad names	
		return $( "[data-fieldname]" ).filter( function( i, e ) {
			return $( e ).data( "fieldname" ).toString() === fieldName;
		} );
	},

	/**
	 * Show field by name on current page
	 * @param {string} name of field
	 * @intellisense
	 */
	showField: function( fieldName ) {
		var pageObj = this;
		var ctrl = Runner.getControl( this.pageId, fieldName );

		( this.fieldItems[fieldName] || [] ).forEach( function( item ) {
			pageObj.toggleItem( item, true );
		} );
	},

	/**
	 * Hide field by name on current page
	 * @param {string} name of field
	 * @intellisense
	 */
	hideField: function( fieldName ) {
		var pageObj = this;
		var ctrl = Runner.getControl( this.pageId, fieldName );

		( this.fieldItems[fieldName] || [] ).forEach( function( item ) {
			pageObj.toggleItem( item, false );
		} );

	},

	/**
	 * Hide array of bricks by name on current page
	 * @param {array} names of bricks (without runner-b-)
	 * @intellisense
	 */
	hideBricks: function( arrOfNames ) {
		for ( var i = arrOfNames.length; i--; ) {
			this.hideBrick( arrOfNames[i] );
		}
	},

	/**
	 * Hide bricks by name on current page
	 * @param {string} name of brick (without runner-b-)
	 * @intellisense
	 */
	hideBrick: function( name ) {
		var brickObjs = this.getBrickObjs( name ), i;
		for ( i = brickObjs.length; i--; ) {
			brickObjs[i].hide();
		}
	},

	/**
	 * Show array of bricks by name on current page
	 * @param {array} names of bricks (without runner-b-)
	 * @intellisense
	 */
	showBricks: function( arrOfNames ) {
		for ( var i = arrOfNames.length; i--; ) {
			this.showBrick( arrOfNames[i] );
		}
	},

	/**
	 * Show bricks by name on current page
	 * @param {string} name of brick (without runner-b-)
	 * @intellisense
	 */
	showBrick: function( name ) {
		var brickObjs = this.getBrickObjs( name ), i;
		for ( i = brickObjs.length; i--; ) {
			brickObjs[i].show();
		}
	},

	/**
	 * Toggle the current page's bricks with a particular name
	 * @param {string} name
	 * @param {boolean} toggler
	 */
	toggleBrick: function( name, toggler ) {
		toggler ? this.showBrick( name ) : this.hideBrick( name );
	},

	/**
	 * Toggle item on the page
	 * @param {string} itemId
	 * @param {boolean} show
	 */
	toggleItem: function( itemId, show, recordId ) {
		return this.layoutHelper.toggleItem( itemId, show, recordId );
	},

	toggleItems: function( itemIds, show ) {
		return this.layoutHelper.toggleItems( itemIds.map( function( id ) {
			return {
				itemId: id, 
				show: show
			};
		}) );
	},

	toggleItemType: function( itemType, show, recordId ) {
		return this.layoutHelper.toggleItemType( itemType, show, recordId );
	},

	hasItemType: function( itemType ) {
		return this.layoutHelper.hasItemType( itemType );
	},

	/**
	 * @return jQuery object of the item's DOM element
	 */
	findItemType: function( itemType, recordId ) {
		return this.layoutHelper.findItemType( itemType, recordId );
	},

	/**
	 * @return jQuery object of the item's DOM element
	 */
	findItem: function( itemId, recordId ) {
		return this.layoutHelper.findItem( itemId, recordId );
	},

	getItemButton: function( itemId, recordId ) {
		return this.layoutHelper.findItem( itemId, recordId ).children('a');
	},


	/**
	 * @return jQuery object of the form's DOM element
	 */
	findForm: function( location ) {
		return this.layoutHelper.findForm( location );
	},

	/**
	 * Show bricks by name on current page for Mobile version
	 * @param {string} name of brick (without runner-b-)
	 * @param {string} name of container (without runner-c-)
	 * @intellisense
	 */
	showBrickMobile: function( name, contName ) {
		var brickObjs = this.getBrickObjs( name, contName ), i;
		for ( i = brickObjs.length; i--; ) {
			brickObjs[i].show();
		}
	},

	/**
	 * Get objects for array of bricks by name on current page
	 * @param {array} names of bricks (without runner-b-)
	 * @intellisense
	 */
	getBricksObjs: function( arrOfNames ) {
		var bricksObjs = {}, i;

		for ( i = 0; i < arrOfNames.length; i++ ) {
			bricksObjs[arrOfNames[i]] = this.getBrickObjs( arrOfNames[i] );
		}
		return bricksObjs;
	},

	/**
	 * Get all brick objects by name on current page
	 * @param {string} name of brick (without runner-b-)
	 * @param {object} page container (if need another) 
	 * @param {boolean} get all bricks objs in pageCont or not 
	 *
	 * @return {array} of brick objects
	 * @intellisense
	 */
	getBrickObjs: function( name, pageCont, all ) {
		return Runner.bricks.Brick.prototype.getBrickObjs( name || '', pageCont || this.pageCont, all, 4, this.id );
	},


	/**
	 * Display a message
	 * @param {string} text
	 * @param {boolean} styled 		apply message style
	 * @param {boolean} error 		apply error style
	 * @intellisense
	 */
	displayMessage: function( text, styled, error ) {
		var messageText = styled ? 
			this.getMessageText( text, error ) : text;

		this.findItemType( this.messageItemType() )
			.html( messageText );
	},

	/**
	 * Display half of prepared message to show
	 * @param {string} text
	 * @intellisense
	 */
	displayHalfPreparedMessage: function( text, error ) {
		var messageText = this.getMessageText( text, error ),
			messType = this.messageItemType();

		var $message = this.messageItemType() === 'grid_message' ?
			this.getGridMessageElem() : this.findItemType( messType );
	
		$message
			.html( messageText )
			.addClass( error ? "alert-danger" : " alert-success" );
	},

	/**
	 * @param {string} text 
	 */
	showPageMessageWithErrorText: function( text ) {
		this.displayHalfPreparedMessage( text, true );
		this.showElement( "message" );
	},

	/**
	 * Get prepared message text
	 * @param {string} text
	 * @intellisense
	 */
	getMessageText: function( text, error ) {
		return "<h5>" + text + '</h5>';
	},

	/**
	 * Get search controller object (Help, JavaScrit API)
	 * @return {object}
	 * @intellisense
	 */
	getSearchController: function() {
		return this.searchController;
	},

	/** 
	 * Initialize the Search controller functionality on the page
	 */
	initSearch: function() {
		if ( !this.controlsMap || !this.controlsMap.searchPanelActivated || !this.controlsMap.search || !this.controlsMap.search.searchBlocks ) {
			return false;
		}

		this.useSearchSuggests = Runner.pages.PageSettings.getTableData( this.tName, "ajaxSuggest" );

		this.searchController = new Runner.search.SearchController( {
			id: this.pageId,
			tName: this.tName,
			pageType: this.pageType,
			useSuggest: this.useSearchSuggests,
			usedSrch: this.controlsMap.search.usedSrch,
			fNamesArr: this.controlsMap.search.allSearchFields,
			searchFieldsLabels: this.controlsMap.search.allSearchFieldsLabels,
			searchTableName: this.controlsMap.search.searchTableName,
			shortTName: this.controlsMap.search.shortSearchTableName,
			panelSearchFields: this.controlsMap.search.panelSearchFields,
			isSearchPanelInflexible: this.controlsMap.search.inflexSearchPanel,
			isSearchRequired: this.controlsMap.search.isSearchRequired,
			requiredSearchFields: this.controlsMap.search.requiredSearchFields,
			searchPanelRunByUserApi: this.controlsMap.search.searchPanelRunByUserApi
		} );

		this.searchController.init( this.controlsMap.search.searchBlocks );
	},

	getAdvancedSearchUrl: function() {
		return Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_SEARCH );
	},
	
	/**
	 * Initialize the 'Advanced search' button
	 */
	initAdvSearch: function() {
		var pageObj = this;

		$("[id=advButton" + this.pageId + "][data-popup]").on("click", function(e) {
			var pageParams = {
					requestUrl: pageObj.getAdvancedSearchUrl(),
					baseParams: pageObj.getStateParams(),
					tName: pageObj.tName, 
					pageType: Runner.pages.constants.PAGE_SEARCH, 
					modal: true,
				},
				$button = $(this),
				page = $button.data("page");
			
			if ( page ) {
				pageParams.baseParams.page = page;
			}
							
			pageParams.popupWidth = $button.data("popupwidth");
			pageParams.popupHeight = $button.data("popupheight");

			Runner.pages.PageManager.openPage( pageParams );
			return false;
		});
	},	
	
	/**
	 * Hide search suggests containers
	 */
	hideSearchControllerContainers: function() {
		if ( this.useSearchSuggests 
			|| this.searchController && this.searchController.useSuggest && Runner.suggests
				&& typeof Runner.suggests.DestroySuggestDiv === "function" ) {
			Runner.suggests.DestroySuggestDiv();
		}
		
		this.searchController && this.searchController.hideSearchFieldsSuggests();
		
		var ctrls = Runner.controls.ControlManager.getAt( this.tName, this.pageId ), i;

		for ( i = 0; i < ctrls.length; i++ ) {
			if ( ctrls[i] instanceof Runner.controls.EditBoxLookup ) {
				ctrls[i].destroySuggest();
			}
		}				
	},

	/**
	 * Add the 'changePageHn' method as a handler to the each control's 'editing' event
	 */
	initChangeCtrlEvents: function() {
		var ctrls = Runner.controls.ControlManager.getAt( this.tName, this.pageId ),
			i;

		if ( !ctrls ) {
			return;
		}

		for ( i = 0; i < ctrls.length; i++ ) {
			ctrls[i].on( 'editing', this.changePageHn, {}, this );
		}
	},

	/**
	 * The handler for the controls' 'editing' custom event on the add/edit/register pages.
	 * It allows to track controls's values changes on the page.
	 * @param {object} e			It could be both an Event object and null (if the event is triggered with "fireEvent")
	 * @param {object} updContext	If the event is triggered with "fireEvent" it conains an update context
	 */
	changePageHn: function( e, updContext ) {
		if ( e && ( e.type == 'keyup' || e.type == 'keypress' || e.type == 'keydown' ) && !Runner.isAcceptableKeyCode( e ) ) {
			// skip arrows, tab keys
			return;
		}

		updContext = updContext || {};

		if ( !updContext.resetHappend ) {
			this.setPageModified( true );
		}
	},

	/**
	 * Check if the page is modified
	 * @return {boolean}
	 */
	isPageModified: function() {
		return this.pageModified;
	},

	/**
	 * Update the page modification state
	 * @param {boolean}
	 */
	setPageModified: function( state ) {
		this.pageModified = state;
	},

	/**
	 * Handler onbeforeunload for window object.
	 * @return {string}
	 */
	beforeUnloadHn: function() {
		if ( this.isPageModified() ) {
			var sessionControl = Runner.pages.PageSettings.getGlobalData("sessionControl");
			if ( !sessionControl || !sessionControl["forceExpire"] || !this.sessionExpired ) {
				return Runner.lang.constants.QUESTION_UNSAVED_CHANGES;
			}
		}
	},

	/**
	 * Get the class name
	 * @param {string} name
	 * @return {string}
	 */
	makeClassName: function( name ) {
		return ( this.oldLayout ? "runner-" : "rnr-" ) + name;
	},

	/**
	 * @return {jQeury object}
	 */
	getActiveWhereTab: function() {
		return $('[data-pageid=' + this.id + '][data-tabid]').filter( function(e) {
			return $(this).parent().hasClass('active'); 
		});
	},	
	
	/**
	 * Reload the page.
	 * @param {object} baseParams - page parameters to be sent to the server
	 * @param {object} reloadParams
	 * @param {object} requestParams - object with additional parameters:
	 * 	{
	 * 		method: 'POST', 'GET' etc
	 * 	}
	 */
	reload: function( baseParams, reloadParams, requestParams = {}) {
		var form,
			pageObj = this;

		baseParams = Object.assign( {}, this.getStateParams(), baseParams );
		
		if ( !this.isDefaultPage ) {
			baseParams.page = baseParams.page || this.pageName;
		}
		
        if ( baseParams["tab"] === undefined ) {
			var $activeWhereTab = pageObj.getActiveWhereTab();
			if ($activeWhereTab.length) {
				baseParams["tab"] = $activeWhereTab.data("tabid");
			}
		}
		
		if ( this.openMode == Runner.pages.constants.OPENMODE_PRELOADED ) {
			form = new Runner.form.BasicForm({
				addRndVal: false,
				initImmediately: true,
				submitUrl: Runner.pages.getUrl( this.pageTable, this.pageType ),
				standardSubmit: true,
				baseParams: baseParams,
				method: requestParams.method || this.defaultReloadMethod()
			});
			form.submit();
			return;
		}

		if ( this.containerBased() ) {
			this.preparePageForReloading();

			if ( this.multiRecordPage ) {
				baseParams.recordId = Runner.genId();
				Runner.setIdCounter( this.id + 10000 );
			}
		}

		baseParams = Object.assign( {}, this.getBaseParams(), baseParams );
		baseParams.id = this.id;
		baseParams.__method__ = requestParams.method || baseParams.__method__;

		Runner.runnerAJAX( this.requestUrl || Runner.pages.getUrl( this.tName, this.pageType ), baseParams, function( respObj ) {
			Runner.apply( pageObj, reloadParams || {} );
			pageObj.pageReloadHn( respObj, reloadParams );
		}, reloadParams && reloadParams.autoReload );
	},

	defaultReloadMethod: function() {
		return "GET";
	},

	/**
	 * 
	 */
	cleanupBeforeReload: function() {
		this.destroyVideo();
	},

	/**
	 * A stub
	 */
	preparePageForReloading: function() {
	},

	/**
	 * @param {object} respObj
	 */
	pageReloadHn: function( respObj ) {
		var $tabs;

		if ( this.containerBased() ) {
			Runner.stopLoading( this.getGrayedElement() );

			if ( !respObj.success ) {
				$( "#message_block" + this.id ).html( "Submit failed!" );
			}

			this.cleanupBeforeReload();

			Runner.applyPagesData( respObj.pagesData );
			this.pageData = Runner.getPageData( this.id );
			this.proxy = this.pageData.proxy;

			Runner.setIdCounter( respObj.idStartFrom );

			respObj.bodyCont = Runner.pages.PageManager.wrapPageBody( respObj.html, this.tName, this.pageType );
			respObj.headerCont = respObj.headerCont || "";
			respObj.footerCont = respObj.footerCont || "";

			if ( this.openMode == Runner.pages.constants.OPENMODE_CONTAINER
				|| this.openMode == Runner.OPENMODE_PRELOADED_CONTAINER ) {
				Runner.pages.PageManager.putToContainer.call( this, respObj );
			} else if ( this.openMode == Runner.pages.constants.OPENMODE_DASHCONTAINER ) {
				Runner.pages.PageManager.putToDashContainer.call( this, respObj );
			} else if ( this.openMode == Runner.pages.constants.OPENMODE_DASHTABS ) {
				Runner.pages.PageManager.putToDashTab.call( this, respObj );
			} else if ( this.openMode == Runner.pages.constants.OPENMODE_TABVIEW ) {
				Runner.pages.PageManager.putToTab.call( this, respObj );
			}
			$( this.openContainer ).children( ".rnr-pagewrapper" ).first().html( respObj.html );

			// set controlsMap
			if( respObj["controlsMap"] ) {
				this.controlsMap = respObj["controlsMap"][this.tName][this.pageType][this.id];
			}


			//	reinit page
			this.init();
		}
	},

	/**
	 *
	 */
	replaceGridTabs: function() {
		if ( !this.$gridTabControl ) {
			return;
		}

		var $tabs = this.$gridTabControl.find( '[data-tabid][data-pageid=' + this.id + ']' );

		if ( $tabs.length ) {
			$tabs.filter( ":not(:first)" ).parent().remove();
			$tabs.first().parent().replaceWith( this.pageData.gridTabs );
		} else {
			this.$gridTabControl.append( this.pageData.gridTabs );
		}
	},

	/**
	 * Tells if the page open mode is container-based
	 * @return {boolean}
	 */
	containerBased: function() {
		return this.openMode == Runner.pages.constants.OPENMODE_CONTAINER
			|| this.openMode == Runner.pages.constants.OPENMODE_PRELOADED_CONTAINER
			|| this.openMode == Runner.pages.constants.OPENMODE_DASHCONTAINER
			|| this.openMode == Runner.pages.constants.OPENMODE_DASHTABS
			|| this.openMode == Runner.pages.constants.OPENMODE_TABVIEW;
	},


	/**
	 *
	 */
	finishDashElementActions: function( updContext, dashAction ) {
		updContext = updContext || this.updContext;
		dashAction = dashAction || this.dashAction;
		if ( !this.dashboard || !updContext || !dashAction ) {
			return;
		}

		updContext.continueActions( this.dashboard.getElementByName( this.dashElement ), dashAction );
		this.dashAction = null;
		this.updContext = null;
	},

	setChart: function( chartObj ) {
		this.chartObj = chartObj;
	},

	/**
	 * hide excess separators in setting menu
	 */
	updateDropdownButtons: function() {

		$( 'ul.dropdown-menu' ).each( function() {
			var $dropdown = $( this ),
				followsNormalVisibleItems = false,
				emptyDropdown = true;

			$dropdown.children( 'li' ).each( function( i, li ) {
				var $li = $( li );
				var separator = $li.attr( 'role' ) == "separator";

				if ( !separator && $li.css('display') != 'none' ) {
					followsNormalVisibleItems = true;
					emptyDropdown = false;
				} else if ( separator ) {
					if ( !followsNormalVisibleItems ) {
						$li.hide();
					} else {
						$li.css('display', '');
					}
					followsNormalVisibleItems = false;
				}
			});
			//	hide last visible elements that are separators
			var $ddChildren = [ ...$dropdown.children( 'li:not([data-hidden])' ) ].reverse();
			for( const ch of $ddChildren ) {
				const $ch = $(ch);
				if( $ch.is( '[role="separator"]' ) ) {
					$ch.hide();
				} else {
					break;
				}
			}

			var $dropdownItem = $dropdown.closest( '[data-itemtype]' );
			var itemType = $dropdownItem.data('itemtype');

			//	E.g. menu should not be affected
			if ( !$dropdownItem.is('[data-button-item]') && [ 'username_button', 'search_panel'].indexOf( itemType ) == -1 ) {
				return;
			}

			if ( !emptyDropdown ) {
				//	search panel visibility is controlled elsewhere.
				if ( itemType != 'search_panel' ) {
					//	show non-empty dropdown button
					$dropdownItem.attr('data-hidden', null );
				}
			} else {
				if ( itemType == 'username_button') {
					//	remove dropdown from the username button 
					$dropdown.attr('data-hidden', '' );
					$dropdownItem.find('.caret').hide();
				} else if ( $dropdownItem.is('[data-button-item]') ) {
					$dropdownItem.attr('data-hidden', '' );
				} else {
					//	do nothing ( search panel etc )
				}
			}
		});
	},

	mobileCollpased: function( $elem ) {
		return $elem.attr( 'data-mobile-hidden' ) !== undefined;
	},

	/**
	 * Toggle single elememnt visiblity in mobile mode
	 */
	toggleMobileCollapse: function( $elem, hide ) {
		if ( hide === undefined ) {
			hide = !this.mobileCollpased( $elem );
		}
		$elem.attr( 'data-mobile-hidden', hide ? '' : null );
		$elem.attr( 'aria-expanded', hide ? 'true' : 'false' );
	},
	/**
	 *
	 */
	initCollapseButtons: function() {
		var pageObj = this;

		$( "[data-runner-toggle=collapse]", this.pageCont )
			.on( "click", function( e ) {
				Runner.toggleMoreButtonState();
				pageObj.layoutHelper.updateMedia( Runner.getMediaType() );
				return false;
			});
	},

	/**
	 * Show/hide all elements controlled by mobile 'more' (collapse) button
	 */
	togglePageMobileCollapse: function( target, hide ) {
		var pageObj = this;
		target = target || '[data-mobile-controlled]';
		$( target, pageObj.pageCont ).each( function( idx, element ) {
			pageObj.toggleMobileCollapse( $( element ), hide );
		} );
	},

	/**
	 * A stub
	 */
	clearSelection: Runner.emptyFn,

	getFormControls: function() {
		return [];
	},

	getExtraPageReloadParams: function() {
		return {};
	},

	initGridTabs: function() {
		$( '[data-pageid=' + this.id + '][data-tabid]' ).off( "click" ).on( "click", function( e ) {

			//	just do nothing on clicking the active tab
			if ( $( this ).parent().hasClass( 'active' ) ) {
				e.preventDefault();
			}
		} );
	},

	/**
	 * 	Returns Control object for a given field
	 *	@param String fieldName Case-insensitive field name
	 *	@return Object RunnerControl-derived object
	 *
	 */
	getControl: function( fieldName ) {
		return Runner.controls.ControlStorage.byIdField( this.id, fieldName );
	},

	showAddInPopup: function( $link ) {
		return $link.attr( 'data-popup' ) !== undefined;
	},

	showEditInPopup: function( $link ) {
		if ( $link === undefined ) {
			return $( '[id^=editLink][data-popup]' ).length > 0;
		}
		return $link.attr( 'data-popup' ) !== undefined;
	},

	showViewInPopup: function( $link ) {
		if ( $link === undefined ) {
			return $( '[id^=viewLink][data-popup]' ).length > 0;
		}
		return $link.attr( 'data-popup' ) !== undefined;
	},

	getInlineRow: Runner.emptyFn,

	messageItemType: function() {
		return 'message';
	},

	showElement: function( name ) {
		this.toggleElement( name, true );
	},

	hideElement: function( name ) {
		this.toggleElement( name, false );
	},

	toggleElement: function( name, show ) {
		var _self = this;
		if ( Runner.isArray( name ) ) {
			name.forEach( function( eName ) {
				_self.toggleElement( eName );
			} );
		}
		this.element2Item( name ).forEach( function( itemType ) {
			if ( itemType === 'grid_message' ) {
				_self.toggleGridMessage( show );
			} else if ( itemType === 'grid' ) {
				_self.layoutHelper.toggleForm( 'grid', show );
			} else {
				_self.toggleItemType( itemType, show );
			}
		} );
	},

	/**
	 * @returns {Array}
	 */
	element2Item: function( name ) {
		if ( name === 'message' ) {
			return [this.messageItemType()];
		} else if ( name === 'filterpanel' ) {
			return ['filter_panel'];
		} else if ( name === 'loginfields' || name === 'grid' ) {
			return ['grid'];
		} else if ( name === 'searchpanel' ) {
			return ['search_panel'];
		} else if ( name === 'printpanel' ) {
			return ['print_panel'];
		} else if ( name === 'recordcontrol' ) {
			return ["inline_save_all", "inline_cancel_all", "delete", "update_selected", "export_selected", "delete_selected"];
		} else if ( name === 'bsfieldhidepanel' ) {
			return ["columns_control"];
		}

		return [];
	},

	element2Brick: function( name ) {
		return name;
	},

	toggleGridMessage: function( show ) {
		$( '[data-grid-message][data-pageid=' + this.id + ']' ).attr( 'data-hidden', show ? null : '' );
	},
	
	getGridMessageElem: function( context ) {
		return $( '[data-grid-message][data-pageid=' + this.id + ']', context );
	},

	/**
	 * User API
	 * @param {number} rowId
	 */
	openDetailsPreview: function( rowId ) {
		if ( !this.dpObjs ) {
			return;
		}

		var tNames = [],
			pageObj = this,
			row, tName, dp;

		for ( tName in this.dpObjs ) {
			//Runner.util.details.NewListPreview inst
			dp = this.dpObjs[tName];

			row = row || dp.getRowById( rowId );
			if ( !row ) {
				// wrong row id
				return;
			}

			if ( dp.dpShowType === Runner.pages.constants.DP_INLINE && dp.isTabToShow( tName, row ) ) {
				tNames.push( tName );
			}
		}

		if ( !tNames.length ) {
			// nothing to show
			return;
		}

		row.row.addClass( "bs-details-opened" );
		row.isShown = true;

		tNames.forEach( function( tName ) {
			pageObj.dpObjs[tName].prepareTabControl( row );
		} );

		this.hideElement( "message" );
	},

	/**
	 * User API
	 * @param {number} rowId	 
	 */
	closeDetailsPreview: function( rowId ) {
		if ( !this.dpObjs ) {
			return;
		}

		var row, tName, dp;

		for ( tName in this.dpObjs ) {
			//Runner.util.details.NewListPreview inst
			dp = this.dpObjs[tName];

			row = row || dp.getRowById( rowId );
			if ( !row ) {
				// wrong row id
				return;
			}

			if ( dp.dpShowType === Runner.pages.constants.DP_INLINE ) {
				dp.closeDetails( row );
			}
		}
	},

	/**
	 * Unimportant purely visual thing. Gray out the page and show 'loading' box.
	 * 
	 */
	showLoadingBox: function( text ) {
		if ( this.silentReloading ) {
			return;
		}

		var grayed = this.getGrayedElement();

		if ( !grayed ) {
			return;
		}

		Runner.runLoading( text || "", grayed, this.openMode === Runner.pages.constants.OPENMODE_POPUP );
	},

	/**
	 * Returns element that needs to be grayed out
	 * Purely visual thing
	 * 
	 * @returns jQuery object
	 */
	getGrayedElement: function() {
		if ( this.containerBased() ) {
			return this.openContainer;
		}
		return this.gridElem;
	},

	initPagination: function() {
		var pageObj = this;
		var $paginationElem = this.findItemType( 'pagination' );

		$paginationElem.on( "click", function( e ) {
			Runner.Event.prototype.stopEvent( e );

			var $target = $( e.target ),
				pageNum;

			if ( !$target.is( "a" ) ) {
				return;
			}
			pageNum = $target.attr( "pageNum" );

			pageObj.preparePageForReloading();
			pageObj.reload( { goto: pageNum }, pageObj.getExtraPageReloadParams() );
		});
	},

	getBaseParams: function() {
		if ( this.openMode === Runner.pages.constants.OPENMODE_PRELOADED ) {
			return this.baseParams || Runner.getUrlParams();
		}
		return this.baseParams || {};
	},

	updateMedia: function( media ) {
		this.layoutHelper && this.layoutHelper.updateMedia && this.layoutHelper.updateMedia( media );
	},

	getRowById: function( rowId ) {
		if ( !this.inlineEdit ) {
			return null;
		}
		return this.inlineEdit.getRowById( rowId );
	},

	initPdfMake: function() {
		var pageObj = this;
		this.findItemType("view_pdf")
		.add( this.findItemType("print_pdf") )
		.add( this.findItemType("create_pdf") )
		.add( this.findItemType("export_report_pdf") )
		.find("a")
		.on("click", function(){
			pageObj.createButtonPDF( $( this ) );
		});
	},

	createButtonPDF: function( $item ) {
		var params = {
			orientation: $item.data( 'orientation' ),
			scale: $item.data( 'scale' ),
			page: $item.data( 'pdfpage' ),
			pageType: $item.data( 'pagetype' ),
			scope: $item.data( 'scope' ),
			split: $item.data( 'split' ),
			records: $item.data( 'records' ),
			filename: $item.data( 'filename' ),
			backgroundImage: $item.data( 'bg-image' ),
			backgroundOpacity: $item.data( 'bg-opacity' ),
			beforeCreate: $item.data('before-create'),
			pageSize: $item.data( 'pagesize' ),
		};
		
		Runner.PDF.create( params, this, function( pdf ) { 
			pdf.download( params.filename );
		});
	},

	showHiddenColumn: function( column ) {},
	
	/**
	 * @return Boolean
	 */
	leftBarLayout: function() {
		return $('.r-vbar-page').length > 0;
	},

	initCollapseButton: function() {
		var mobileScreen = Runner.mobileScreen(),
			leftBarLayout = this.leftBarLayout(),
			rtl = Runner.isDirRTL();
		if ( mobileScreen && !leftBarLayout ) {
			if ( this.leftBarCollapsed() ) {
				this.toggleItemType( 'logo', true );
				$('.r-left-collapsed').removeClass('r-left-collapsed');
				
				this.toggleItemType( 'collapse_button', false );
				this.toggleItemType( 'expand_button', false );

				delete_cookie('collapse_leftbar', Runner.projectRoot());
			}
			return;
		}
		//	left bar layout is always collapsed initially on mobiles
		if ( mobileScreen && leftBarLayout ) {
			if ( !this.leftBarCollapsed() ) {
				$('.r-left').addClass('r-left-collapsed');
				
				this.toggleItemType( 'collapse_button', false );
				this.toggleItemType( 'expand_button', false );

				set_cookie('collapse_leftbar', true, '', Runner.projectRoot() );
			} else {
				this.toggleItemType( 'logo', true );
			}
		}

		
		this.leftBarItems = [];
		
		var animateTime = 300;
		var pageObj = this;
		var $logoCell = $('[data-logo-cell]');
		var $leftBar = $('.r-left'),
			$collapseBtn = this.findItemType('collapse_button').find('a'),
			$expandBtn = this.findItemType('expand_button').find('a'),
			$expand_menu_button = this.findItemType('expand_menu_button').find('a');
		
		var $content = $('.r-left[data-fixed] + .r-content-col')
			.add('.r-topheader[data-fixed]');

		var barWidth = $leftBar.attr('data-width') || '300px';
		
		//	appearance in the leftbar layout 
		var topbarHeight = $('.r-topheader > .navbar').outerHeight();
		
		/*
		if ( $expandBtn.closest( '.r-left').length ) {
			$expandBtn.css( { height:  ( topbarHeight ) + 'px' } );
		}
		*/
		var logoBorder = $logoCell.css('border-bottom-width') || '0px';
		//$logoCell.css( { height:  'calc( ' + topbarHeight + 'px + ' + logoBorder +' )' } );
		
		// load leftbar item list
		$logoCell
			.add( this.layoutHelper.findForm('left') )
			.find('[data-itemtype][data-itemid]') 
			.each( function() {
				var $item = $(this);
				var itemType = $item.attr('data-itemtype');
				if ( itemType != 'expand_button' && itemType !== 'menu' && itemType !== 'menu2') {
					pageObj.leftBarItems.push( $item.attr('data-itemid') );
				}
			});
			
		var visibleItems = this.layoutHelper.getVisibleItems( this.leftBarItems );

		var doCollapse = mobileScreen
			? function() {
			// collapse leftbar 
				var startStyle = {}, targetStyle = {}, endStyle = {},
					side = rtl ? 'right' : 'left';
				startStyle[ side ] = 0;
				targetStyle[ side ] = '-' + barWidth;
				endStyle[ side ] = '';
				$leftBar
					.css( startStyle )
					.animate( targetStyle, animateTime, function() {
						$leftBar.addClass('r-left-collapsed');
						$leftBar.css( endStyle )
					});
				pageObj.closeTreeSubmenus();
				set_cookie('collapse_leftbar', true, '', Runner.projectRoot() );
			}
			: function() {
			//	doCollapse desktop
				visibleItems = pageObj.layoutHelper.getVisibleItems( pageObj.leftBarItems );
				// hide all items in the logo cell except 'collapse-button' and 'menu'
				pageObj.toggleItems( pageObj.leftBarItems, false );

				pageObj.toggleItemType( 'collapse_button', false );
				pageObj.toggleItemType( 'expand_button', true );
				// collapse leftbar 
				$leftBar
					.css({
						'overflow': 'hidden',
						'white-space': 'nowrap'
					})
					.animate({ 'width': '60px' }, animateTime, function() {
						$('.r-menu-collapsed').attr('data-hidden', null);
						$('.r-menu-expanded').attr('data-hidden', '');
						$leftBar.addClass('r-left-collapsed');
						$leftBar.css({
							'width': '',
							'overflow': '',
							'white-space': ''
						})
					});

				// update margin and width of the rest of the page				
				// IE fix
				var targetCss = {
						'max-width': ( window.innerWidth - 60 ) + 'px'
					};
		
				targetCss[ 'margin-' + Runner.left ] = '60px';

				$content
					.css( { 'max-width': 'calc( ' + window.innerWidth + 'px - ' + barWidth + '  )' } )
					.animate( targetCss, animateTime, function() {
						$content
							.css( 'margin-' + Runner.left, '')
							.css( 'max-width', '' );
					});
				pageObj.closeTreeSubmenus();
				set_cookie('collapse_leftbar', true, '', Runner.projectRoot() );
	
			};
		
		var doExpand = mobileScreen 
		? function() {
			//	doExpand mobile
			var startStyle = {}, targetStyle = {}, endStyle = {},
			side = rtl ? 'right' : 'left';
			startStyle[ side ] = '-' + barWidth;
			targetStyle[ side ] = 0;
			endStyle[ side ] = '';
			$leftBar
				.removeClass('r-left-collapsed')
				.css( startStyle );
			$leftBar.animate( targetStyle, 
				animateTime, 
				function() {
					$leftBar.css( endStyle );
				});
				
			delete_cookie('collapse_leftbar', Runner.projectRoot() );
		}
		: function() {
			//	doExpand desktop
			$leftBar.animate( {
					'flex-basis': barWidth,
					'width': barWidth
				}, 
				animateTime, 
				function() {
					$leftBar.css( {
						'flex-basis': '',
						'width': '',
						'overflow': ''
					});
					
					$('.r-menu-collapsed').attr('data-hidden', '');
					$('.r-menu-expanded').attr('data-hidden', null);
					
					pageObj.toggleItems( visibleItems, true );
					pageObj.toggleItemType( 'logo', true );
					$leftBar.removeClass('r-left-collapsed');
					
					pageObj.toggleItemType( 'collapse_button', true );
					pageObj.toggleItemType( 'expand_button', false );
				}
			);
				
			// IE fix
			var targetCss = {
					'max-width': 'calc( ' + window.innerWidth + 'px - ' + barWidth + '  )'
				};
			targetCss[ 'margin-' + Runner.left ] = barWidth;

			$content.animate( targetCss, animateTime, function() {
				$content.css( 'margin-' + Runner.left, '' );
			});
			delete_cookie('collapse_leftbar', Runner.projectRoot() );
		}

		$collapseBtn
			.add('.r-body-shadow')
			.on( 'click', function() { doCollapse(); return false; } );
		$expandBtn
			.add( $expand_menu_button )
			.on( 'click', function() { doExpand(); return false; } );
		
		this.toggleLeftBar = function( show ) {
			show ? doExpand() : doCollapse();
		}
	},

	closeTreeSubmenus: function() {
		var $menu = this.findItemType('menu');
		$menu.find( 'ul' ).removeClass('r-most-recent');
		
		$menu = this.findItemType('menu2');
		$menu.find( 'ul' ).removeClass('r-most-recent');
	},

	leftBarCollapsed: function() {
		return $('.r-left-collapsed').length > 0;
	},
	getSelectedRecordKeys: function() {
		return [];
	},
	getSelectedRecordIds: function() {
		return [];
	},
	getSelectedRecords: function() {
		return [];
	},
	getAllRecords: function() {
		return [];
	},
	getRowObject: function( id ) {
		return null;
	},

	/**
	 * @returns { table: RunnerPage }
	 */
	getDetailsPages: function( rowId ) {
		var ret = {};
		var pageObj = this;
		Object.keys( this.dpObjs || {} ).forEach( function( dtable ) {
			if ( pageObj.dpObjs[dtable].detailsPageObj ) {
				ret[ dtable ] = pageObj.dpObjs[dtable].detailsPageObj;
			}
		});
		return ret;
	},

	/**
	 * @returns RunnerPage object || null
	 */
	getDetailsPage: function( table, rowId ) {
		return this.dpObjs[table]
			? this.dpObjs[table].detailsPageObj
			: null;
	},
	
	getMasterPage: function() {
		return this.masterPageObj || null;
	},
	
	/**
	 * A 'Click' delegated handler that
	 * opens the the full field's content in a popup window
	 * @param {jQuery Event object} e
	 */
	largeTextOpenerDelegate: function(e) {
		var self = this,
			$target = $( e.target ),
			query = $target.data("query"),
			fieldLabel = $target.data("fieldlabel"),
			afterCreateTextWin, afterCloseTextWin;
		
		if ( !$target.is("a") || !query ) {
			return;
		}

		afterCreateTextWin = function( win ) {		
			$("[data-role='closeText']", win.footer() ).on("click", function( e ) {
				win.destroy();
				return false;
			});
			$("[data-role='copyText']", win.footer() ).on("click", function( e ) {
				navigator.clipboard.writeText( win.body().text() );
				return false;
			});
			Runner.pages.PageManager.correctBSWindowSize( win._obj, self );
		};			
		
		afterCloseTextWin = function( win ) {
			var $content = win.getContentNode(),
				newWinWidth = $content.width(),
				newWinHeight = $content.height();
				
			set_cookie( 'textWinWidth', newWinWidth, '', Runner.getCookieRoot() );
			set_cookie( 'textWinHeight', newWinHeight, '', Runner.getCookieRoot() );
		};
		
		Runner.runnerAJAX( query, {}, function( respObj ) {	
			var args = {
					bodyContent: respObj.success ? respObj.textCont : respObj.error || "Server error",
					width: parseInt( get_cookie("textWinWidth"), 10 ) || 550, 
					height: parseInt( get_cookie("textWinHeight"), 10 ) || 400,
					footerContent: [
						'<a href="#" data-role="copyText" class="btn btn-default btn-sm">'
						+ Runner.lang.constants.COPY + '</a>',
						'<a href="#" data-role="closeText" class="btn btn-primary btn-sm">'
						+ Runner.lang.constants.CLOSE + '</a>',

					].join(' '),
					headerContent: '<span>' +  fieldLabel + '</span>'
				};

			Runner.pages.PageManager.createBSWin.call( self, args, false, afterCreateTextWin, afterCloseTextWin );
		});
		
		Runner.Event.prototype.stopEvent(e);	
	},
	
	/**
	 * #14869
	 */
	getStateParams: function() {
		var params = {};
		if ( this.masterTable ) {
			params.mastertable = this.masterTable;
			params = Runner.apply( params, this.masterKeys );
		}
		if ( this.pageData.tabId !== undefined ) {
			params.tab = this.pageData.tabId;
		}
		var mainPageTypes = ['list', 'calendar', 'gantt', 'chart', 'report'];
		if( mainPageTypes.includes( this.pageType ) ) {
			if( this.pageName != this.defaultPages[ Runner.pages.PageSettings.getTableData( this.tName, "defaultPageType" ) ] ) {
				params.returnPage = this.pageName;
			}
		}
		return params;
	},
	
	backToListUrl: function( pageName ) {		
		var params = this.getStateParams();
		
		params["a"] = "return";
		if ( pageName ) {
			params["page"] = pageName;
		}
		
		return Runner.pages.getUrl( this.tName, this.defaultListPageType(), "", null, params );
	},

	defaultListPageType: function() {
		const pageTypes = [ 'list', 'calendar', 'gantt' ];
		for( const type of pageTypes ) {
			if( this.defaultPages[ type ] ) {
				return type;
			}
		}
	},

	/**
	 * Add hint for multiple sorting
	 * @param {object} event
	 */	
	 addHint: function(e) {
		this.existMultipleHint = true;
		this.showHint(e);
	},	
	
	/**
	 * Set coordinates for sorting hint
	 * @param {object} event
	 */
	setCoordinatesHint: function(e) {
		var winDim = Runner.getWindowDimensions(),
			scrollX = $(window).scrollLeft(),
			scrollY = $(window).scrollTop(),
			hintY = e.pageY + 20,
			hintX = e.pageX;
					
		if ( hintX + this.multipleHint.width() > winDim.width + scrollX ) {
			hintX -= this.multipleHint.width();
		}
		if ( hintY + this.multipleHint.height() > winDim.height + scrollY ) {
			hintY -= this.multipleHint.height();
		}
		this.multipleHint.css({
			left: hintX + "px",
			top: hintY + "px"
		});
	},
	
	/**
	 * Show hint, it may take some time
	 * @param {object} event
	 */	
	showHint: function(e) {
		if ( !this.multipleHint.length ) {
			this.multipleHint = $('<span class="rnr-sorthint"><b></b></span>')
				.appendTo("body");
		}
		if ( this.existMultipleHint ) { 
			this.setCoordinatesHint(e);
			this.multipleHint.show();
		}
	},
	
	/**
	 * Delete hint
	 */	
	delHint: function() {
		if ( this.multipleHint.length ) {
			this.multipleHint.hide();
			this.existMultipleHint = false;
		}	
	},
	
	/**
	 * Moving hints
	 * @param {object} event
	 */	
	moveHint: function(e) {
		if ( this.multipleHint.length && this.multipleHint.is(":visible") ) {
			this.setCoordinatesHint(e);
		}
	},

	setHintText: function( hintText ) {
		var $textContainer = this.multipleHint.find( "b" );
		$textContainer.text( hintText );
	},

	initElementHint: function( $element, text ) {
		var pageObj = this;
		
		$element
			.on( "mouseout", pageObj.delHint.bind( pageObj ) )
			.on( "mousemove", pageObj.moveHint.bind( pageObj ) )
			.on( "mouseover", function() {
				pageObj.addHint( pageObj );
				pageObj.setHintText( text );
			});
	},

	/**
	 * Initialize the 'Add new' button
	 */
	initAddButton: function() {
		var pageObj = this;
		$("[id=addButton" + this.id + "]")
			.off("click")
			.on("click", function( e ) { // don't replace 'function' with '=>'. We need 'this' inside
				var $button = $(this);
				var pageName = $button.data("page");
				var inPopup = pageObj.showAddInPopup( $button );
				pageObj.showAddPage( inPopup, pageName, {
					popupWidth: $button.data("popupwidth"),
					popupHeight: $button.data("popupheight"),
				} );
				return false;
			});
	},

	/**
	 * 
	 * @param {boolean} inPopup 
	 * @param {string} pageName 
	 * @param {Object} pageParams - popupWidth, popuHeight etc
	 * @param {Object} baseParams - those added to request payload
	 */
	showAddPage: function( inPopup, pageName, addtlPageParams, baseParams ) {
		if( inPopup ) {
			var pageParams = this.getAddPopupParams();
			if ( pageName ) {
				pageParams.baseParams["page"] = pageName;
			}
			Object.assign( pageParams, addtlPageParams || {} );
			Object.assign( pageParams.baseParams, baseParams || {} );

			this.hideElement('message');
			Runner.pages.PageManager.openPage( pageParams );
		} else {
			var params = this.getStateParams();
			if ( pageName ) {
				params["page"] = pageName;
			}
			
			if ( !this.isDefaultPage ) {
				params["listPage"] = this.pageName;
			}
			
			Object.assign( params, baseParams || {} );
			window.location.href = Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_ADD, "", "", params );
		}
	},
	
	getAddPopupParams: function() {
		return Runner.pages.AddPage.prototype.getOpenPageParams.call( this, this.id );
	},

	/**
	 * Can be called from Inline Add/Edit for new records
	 * @param {*} container - new row from Inline object
	 */
	initViewLinks: function( container ) {
		const self = this;
		container = container || this.pageCont;
		$( '[data-viewlink-table]', container ).on( 'click', function( e ) {
			try {
				const $link = $(this);
				var pageParams = Runner.pages.ViewPage.prototype.staticPageParams.call( null, {
					hostPage: self,
					tName: $link.data( 'viewlink-table' ),
					keys: $link.data('viewlink-keys')
				} ); 
				pageParams.baseParams["page"] = $link.data('viewlink-page');
				Runner.pages.PageManager.openPage( pageParams );
			} finally {
				e.preventDefault();
				e.stopPropagation();
			}
		} );
	}

});
/**
 * Advanced Search page class
 */
Runner.pages.SearchPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * The dashboard page object
	 */
	dashboard: null,
	
	/**
	 * If this: true - all dashboard search, false - element dashboard search
	 */
	isDashSearchPage: false,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function(cfg) {
		Runner.pages.SearchPage.superclass.constructor.call( this, cfg );	
	
		this.isDashSearchPage = this.pageData.isDashSearchPage;
		if ( this.dashboard ) {
			this.warnOnLeaving = false;
		}
		
		if ( this.baseParams ) {
			// no to add page param to a search form
			delete this.baseParams.page;
		}
	},
	
	init: function() {
		Runner.pages.SearchPage.superclass.init.call(this);
		this.initButtons();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	/**
	 * Initialize the 'Advanced search' page's buttons
	 */
	initButtons: function() {
		var pageObj = this;

		$("[id=searchButton" + this.pageId + "]").on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				pageObj.submitAdvancedSearch( pageObj.getSubmitBtnParams( $(this) ) );
			}
			return false;			
		});
		
		$("[id=resetButton" + this.pageId + "]").on("click", function(e) {
			pageObj.searchController.clearCtrls();
			pageObj.searchController.initSearchButtonStatus();
			return false;
		});
		
		$("[id=backButton" + this.pageId + "]").on("click", function(e) {
			pageObj.searchController.returnSubmit( $(this).data("page") );
			return false;
		});
		
		$("[id=cancelButton" + this.pageId + "]").on("click", function(e) {
			pageObj.close();
			return false;
		});	
	},

	/**
	 * Ininitialize the page's search functionality
	 */	
	initSearch: function() {
		var pageType,
			tName = this.tName,
			baseParams = this.controlsMap.search.baseParams || this.baseParams || {},
			pageExtraPrams = Runner.pages.PageSettings.getTableData( this.tName, "extraSearchPageParams" );
		
		if ( this.isDashSearchPage ) {
			pageType = this.dashboard.pageType;
			tName = this.dashboard.tName;
		} else {
			pageType = this.controlsMap.search.submitPageType;
		}

		baseParams = Runner.apply( baseParams, this.getStateParams() );
		if( this.controlsMap.search.submitPageName ) {
			baseParams.page = this.controlsMap.search.submitPageName;
		}
		
		this.searchController = new Runner.search.SearchForm({
			id: this.pageId,
			tName: tName,
			pageType: pageType,
			searchType: 'advanced',
			baseParams: baseParams,
			usedSrch: this.controlsMap.search.usedSrch,
			fNamesArr: Runner.pages.PageSettings.getTableData( this.tName, "searchPageFields"),
			isSearchRequired: this.controlsMap.search.isSearchRequired,
			shortTName: Runner.pages.PageSettings.getShortTName( this.tName ),
			requiredSearchFields: this.controlsMap.search.requiredSearchFields,
			ajaxSubmit: this.pageMode === Runner.pages.constants.SEARCH_DASHBOARD,
			useSuggest: Runner.pages.PageSettings.getTableData( this.tName, "ajaxSuggest" ),
			formBaseParams: Runner.apply( pageExtraPrams, baseParams ),
			delegatedSubmit: this.submitAdvancedSearch.bind( this ),
		});
		
		this.searchController.init( this.controlsMap.search.searchBlocks );

		if ( this.isDashSearchPage ) {
			this.searchController.on('afterSearch', function( respObj, srchController, srchForm ) {				
				this.dashboard.doLoadAction();
				Runner.toggleButton( $("[id=showAll" + this.dashboard.id + "]"), respObj.show_all );
			}, this);
		}
	},
	
	/**
	 * Submit the page's search form
	 *  @param {object} params - object with additional parameters
	 */
	submitAdvancedSearch: function( params = {} ) {
		var showAllMode = this.searchController.usedSrch && !this.searchController.hasNotEmptyControl();
	
		if ( this.pageMode === Runner.pages.constants.SEARCH_DASHBOARD && !this.isDashSearchPage ) {
			if ( showAllMode ) {
				this.searchController.prepareForShowAllSubmit();
			} else {
				this.searchController.prepareForSearchSubmit();
			}
			
			this.dashboard.doSearchAction( this.dashElement, this.searchController.getDashSearchParams( showAllMode ) );
			return;
		}		
	
		if ( showAllMode ) {
			this.searchController.showAllSubmit();
			return;
		}
		
		this.searchController.submitSearchWithParams( params );
	},

	getSubmitBtnParams: function( $btn ) {
		var params = {};
		var page = $btn.data( "page" );
		if ( page ) {
			params.page = page;
		}
		return params;
}
});Runner.pages.ViewPage = Runner.extend( Runner.pages.RunnerPage, {	
	keys: null,
	
	keyFields: null,
	
	prevKeys: null,
	
	nextKeys: null,
	
	pageType: Runner.pages.constants.PAGE_VIEW,
	
	/**
	 * The jQuery object representing the 'Next record' button
	 * @type {jQuery object}
	 */ 
	nextButton: null,
	
	/**
	 * The jQuery object representing the 'Previous record' button	
	 * @type {jQuery object}
	 */ 	
	prevButton: null,
	
	/**
	 * The jQuery object representing the 'Edit page' button	
	 * @type {jQuery object}
	 */ 	
	editPageButton: null,
	
	/**
	 * The object supplying the popup mode view page
	 * with list page's inline object data
	 */
	inlineObjectParams: null,
	
	/** 
	 *
	 */
	isShowDetails: false,
	
	/**
	 * It shows if the page is popup window 
	 * for a dash grid element	
	 * @type {boolean}
	 */
	dashGridBased: false,

	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.ViewPage.superclass.constructor.call(this, cfg);

		this.detailsMasterKeys = this.pageData['detailsMasterKeys'] || {};
		
		this.keys = cfg.keys || Runner.pages.PageSettings.getTableData(this.tName, 'keys');
		this.keyFields = cfg.keyFields || Runner.pages.PageSettings.getTableData(this.tName, 'keyFields');
		this.prevKeys = Runner.pages.PageSettings.getTableData(this.tName, 'prevKeys');
		this.nextKeys = Runner.pages.PageSettings.getTableData(this.tName, 'nextKeys');
		
		this.isShowDetails = Runner.pages.PageSettings.getTableData(this.tName, "isShowDetails");
		
		this.dpTablesParams = this.controlsMap && this.controlsMap.dpTablesParams;
	},
	
	/**
	 * Initialize the page
	 */
	init: function() {
		var dbElem, pageObj = this;
		
		Runner.pages.ViewPage.superclass.init.call(this);
		this.initButtons();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
		
		if ( this.dashboard ) {
			dbElem = this.dashboard.getElementByName( this.dashElement );
			//	do major actions only if no Edit page exists in the element
			if ( dbElem.major && jQuery.inArray( Runner.pages.constants.PAGE_EDIT, dbElem.tabsPageTypes ) === -1 ) {
				//	don't move record if the page reloaded as a part of update action
				if ( !this.updContext || !this.updContext.hasUpdateRecordAction() ) {
					this.dashboard.doMoveAction( this.dashElement, {
						keys: this.keys, 
						masterKeys: this.detailsMasterKeys
					}, this.updContext );
				}
				
				this.dashboard.doReportFirstLastAction( this.dashElement, !this.prevKeys, !this.nextKeys, this.updContext );
			}
			
			this.finishDashElementActions();
			
			if ( this.dashboard.getElementByName( this.dashElement ).reload ) {
				if ( this.reloadTimerId ) {
					window.clearTimeout( this.reloadTimerId );
				}
				
				this.reloadTimerId = window.setTimeout( function() {
					pageObj.dashboard.doTimerReloadAction( pageObj.dashElement, { keys: this.keys } );
				}, 1000 * this.dashboard.getElementByName( this.dashElement ).reload );
			}
		}
	},

	/**
	 * Initialize controls
	 * @intellisense
	 */
	initControls: function( controlsMap ) {
		controlsMap = controlsMap || this.controlsMap;
		if ( !controlsMap ) {
			return;
		}
		
		for ( var i = 0; i < ( controlsMap.controls || [] ).length; i++ ) {
			Runner.controls.MockControlFactory( controlsMap.controls[i], this.pageType, this.tName, this );
		}
	},

	initLookups: Runner.emptyFn,

	/**
	 * Initialize the page's functionality that need initializing after tabs are loaded.
	 * If there are no tabs on the page it's invoked from the page's init method 
	 * otherwise from the initTabs method
	 */	
	initForTabs: function() {
		Runner.pages.ViewPage.superclass.initForTabs.call( this );
		this.initMap();
		this.initDetails();
	},
	
	/**
	 * Initialize the view page's buttons
	 */
	initButtons: function() {
		var pageObj = this;
		
		$( "[id=backButton" + this.id + "]").on("click", function(e) {
			window.location.href = pageObj.backToListUrl( $(this).data("page") );
			return false;
		});
	
		$( "[id=backToMenuButton" + this.id + "]" ).on("click", function(e) {
			window.location.href = Runner.getPageUrl("menu");
			return false;
		});				
		
		$( "[id=closeButton" + this.id + "]" ).on("click", function(e) {
			pageObj.close();			
			return false;
		});
		
		this.initNavigationButtons();
	},
	
	/**
	 * Initialize navigation (the 'prev/next' buttons) to the previous and next records.
	 * Initialize the 'Edit' buttons
	 */
	initNavigationButtons: function() {
		var pageObj = this;
					
		this.nextButton = $( "[id=nextButton" + this.id + "]" );
		this.prevButton = $( "[id=prevButton" + this.id + "]" );
		
		this.updateNavigationVisibility();

		this.nextButton.on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				pageObj.openPrevNextRecordPage( pageObj.nextKeys, 1 );	
			}
			return false;	
		});
		
		this.prevButton.on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				pageObj.openPrevNextRecordPage( pageObj.prevKeys, -1 );	
			}
			return false;	
		});
		
		this.editPageButton = $( "#editPageButton" + this.id );
		
		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP ) {
			return;
		}
		
		this.editPageButton.on("click", function(e) {
			if ( Runner.isDisabledButton( this ) ) {
				return false;
			}
				
			
			var pageName = $(this).data("page") || "";
			var	pageParams = Runner.pages.EditPage.prototype.getOpenPageParams.call( pageObj, pageObj.keys, pageObj.inlineObjectParams.rowId );
			
			pageParams.popupCentered = true;
			if ( pageName ) {
				pageParams.baseParams["page"] = pageName;
			}
			
			pageObj.disablePageButtons();
			Runner.pages.PageManager.openPage( pageParams );				
		
			return false;
		});
	},
	
	/**
	 * Update the navigation buttons visibility
	 * for a dash-view page that has a related dash-grid element
	 */
	updateNavigationVisibility: function() {	
		if ( !this.dashboard || this.dashboard.getElementByName( this.dashElement ).major ) {
			return;
		}

		if ( ( !this.dashAction || !this.dashAction.data.firstRecord ) && ( this.dashAction || this.prevKeys && this.prevKeys.length ) ) {
			this.prevButton.removeClass("rnr-invisible-button");
		}

		if ( ( !this.dashAction || !this.dashAction.data.lastRecord ) && ( this.dashAction || this.nextKeys && this.nextKeys.length ) ) {
			this.nextButton.removeClass("rnr-invisible-button");
		}
	},
	
	/**
	 * Open the page corresponding to the next/previous record
	 * @param {Array} keys
	 * @param {Number} step
	 */
	openPrevNextRecordPage: function( keys, step ) {		
		if ( this.openMode === Runner.pages.constants.OPENMODE_PRELOADED ) {
			var params = this.getStateParams();
			if ( this.pageName ) {
				params["page"] = this.pageName;
			}
			
			window.location.href = Runner.pages.getUrl( this.tName, this.pageType, keys, "editid", params );
			return;
		} 
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.disablePageButtons();
			
			var pageParams = this.getOpenPageParams( keys, this.inlineObjectParams.rowId + step );
			if ( this.pageName ) {
				pageParams.baseParams.page = this.pageName;
			}
			
			Runner.pages.PageManager.openPage( pageParams );
			return;
		} 
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_DASHTABS ) {
			
			if ( !this.dashboard.getElementByName( this.dashElement ).major ) {
				this.dashboard.doMoveAdjacentAction( this.dashElement, step > 0 );
				return;
			}
			
			var pageParams = { 
				baseParams: {} 
			};
			
			if ( this.pageName ) {
				pageParams.baseParams.page = this.pageName;
			}			
				
			keys.forEach( function( key, i ) {
				pageParams.baseParams[ 'editid' + (i + 1) ] = key;
			});
			
			pageParams.baseParams = Runner.apply( pageParams.baseParams, this.getStateParams() );
			
			this.dashboard.loadSingleRecord( this.dashboard.getElementByName( this.dashElement ), 
				[ Runner.pages.constants.PAGE_EDIT, Runner.pages.constants.PAGE_VIEW ], pageParams );
		}
	},
	
	/**
	 * Disable the 'Next record', 'Prev record' and 'Edit page' 
	 * navigation buttons
	 */
	disablePageButtons: function() {
		Runner.addDisabledClass( this.nextButton );
		Runner.addDisabledClass( this.prevButton );
		Runner.addDisabledClass( this.editPageButton );
	},

	/**
	 * Enable the 'Next record', 'Prev record' 
	 * and 'View page' navigation buttons
	 */	
	enablePageButtons: function() {
		Runner.delDisabledClass( this.nextButton );
		Runner.delDisabledClass( this.prevButton );
		Runner.delDisabledClass( this.editPageButton );		
	},
	
	/**
	 * Get an object containing params for the Runner page's openPage method
	 * @param {Array} keys
	 * @param {Number} rowId 
	 * @return {Object}
	 */
	getOpenPageParams: function( keys, rowId, inlineObj, afterCreateHn ) {
		
		inlineObj = inlineObj || this.inlineObjectParams?.getInlineObject();
		const row = inlineObj?.getRowById( rowId );
		const input = {
		 	hostPage: this,
		 	tName: inlineObj?.tName ?? this.tName,
		 	keys,
		 	keyFields: this.keyFields || ( row && row.keyFields ),
		 	rowId,
		 	inlineObj,
		 	afterCreateHn,
		 	stateParams: this.getStateParams()

		};
		return Runner.pages.ViewPage.prototype.staticPageParams.call( null, input );

	},

	/**
	 * version that doesn't rely on this
	 * @param input {
	 *		hostPage: RunnerPage,
	 * 		tName: string,
	 * 		keys: string[],
	 * 		keyFields?: string[],
	 * 		rowId?: number,
	 * 		inlineObj?: inline object
	 * 		afterCreateHn?: function
	 * 		stateParams?: {}
	 * }
	 * 
	 * @returns 
	 */
	staticPageParams: function( input ) {
		const inlineObj = input.inlineObj;
		const hostPage = input.hostPage;
		var params = {
			hostPage,
			tName: input.tName,
			pageType: Runner.pages.constants.PAGE_VIEW, 
			keys: input.keys,
			keyFields: input.keyFields,
			modal: true,
			dashGridBased: inlineObj?.dashGridBased,
			baseParams: {
				parId: inlineObj?.id,
				table: inlineObj?.tName,
			},
			inlineObjectParams: {
				getInlineObject: function() {
					return inlineObj;
				},
				rowId: input.rowId
			}
		};

		params.baseParams = Runner.apply( params.baseParams, input.stateParams ?? {} );			
			
		if ( typeof afterCreateHn === "function" ) {
			params.afterCreateHn = afterCreateHn;
		} else {
			params.afterCreateHn = function() {	
				if ( !this.controlsMap ) {
					// the session was expired; show the message in the old window
					if ( this.message ) {
						hostPage.displayHalfPreparedMessage( this.message );
						hostPage.showElement('message');
					}
					
					hostPage.initLoginLink();
					hostPage.enablePageButtons();
					
					if ( this.bsWin ) {
						this.bsWin.modal('hide');
					}						
					
					return;
				}
							
				this.resizeAndMoveWindow( hostPage );
				this.adjustPopWindowHeightByContent();
				
				if ( hostPage.bsWin ) {
					hostPage.bsWin.modal('hide');
				}				
			}
		}
		if ( inlineObj?.dashGridBased && inlineObj?.pageObj ) { 
			params.baseParams.dashTName = inlineObj.pageObj.dashboard.tName;
			params.baseParams.dashPage = inlineObj.pageObj.dashboard.pageName;
			params.baseParams.dashelement = inlineObj.pageObj.dashElement;
		}

		return params;	
	},

	
	/**
	 * Init the Google maps functionality using the Runner.GmapLoader instance
	 */
	initMap: function() {
		if ( !this.hasMaps() ) {
			return;
		}
				
		Runner.globalGmapLoader.onLoad( function() {		
			this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps );
			this.mapManager.init();
	
			if ( this.isPageInTab() ) {
				Runner.pages.PageManager.onTabShown( this, this.mapManager.updateLittleMaps.bind( this.mapManager ) );
			}
		}, this );
	},
	
	/**
	 * Initialize details list page
	 */
	initDetails: function() {
		if ( !this.isShowDetails || !this.dpTablesParams ) {
			return;
		}
			
		this.dpObjs = {};
		this.dpTablesParams.forEach( function( dpTableData, i, array ) {
			var dtName = dpTableData.tName;	
			var dParams = {
				tName: dtName,
				parId: this.id,
				masterTName: this.tName,
				id : dpTableData.id,
				pType: dpTableData.pType,
				masterPageObj: this,
			}
			
			if ( !this.dashboard ) {
				dParams.controlsMap = this.controlsMap.dControlsMap[ dtName ];
				dParams.viewControlsMap = this.viewControlsMap.dViewControlsMap[ dtName ];
				
				this.dpObjs[ dtName ] = new Runner.util.details.ViewDP( dParams );
			} else {
				dParams.masterPageType = this.pageType;
				dParams.pageType = dpTableData.pType;
				dParams.masterKeys = this.detailsMasterKeys[ dtName ];
				dParams.dashTName = this.dashboard.tName;
				dParams.dashPage = this.dashboard.pageName;
				dParams.dashElement = this.dashElement;
				
				this.dpObjs[ dtName ] = new Runner.util.details.ViewEditDashDP( dParams );
			}
			
			this.dpObjs[ dtName ].init();			
		}, this );
	},
	
	/**
	 *
	 */
	showNextPrevButtons: function( next, prev ) {
		this.prevButton.toggleClass("rnr-invisible-button", !prev);
		this.nextButton.toggleClass("rnr-invisible-button", !next);
	},

	getBaseParams: function () {
		var params = Runner.pages.ViewPage.superclass.getBaseParams.call( this );
		for ( var i = 0; i < this.keys.length; ++i ) {
			params[ 'editid' + (i + 1) ] = this.keys[ i ];	
		}
		return params;
	},
});/**
 * Base abstract class for all pages with editing content, add, edit etc.
 */
Runner.pages.EditorPage = Runner.extend( Runner.pages.RunnerPage, {
	
	form: null,
	
	submitUrl: '',
	
	baseParams: null,
	
	isShowDetails: false,
	
	/**
	 * Indicator whether an error happend during files upload 
	 * @type {boolean}	 
	 */
	upploadErrorHappened: false, 
	
	/**
	 * Count of FileFields with multiple uploads which need to be saved.
	 * Dynamic value. Do not edit manualy. 
	 */
	fileFieldsCount: 0,
	
	/**
	* The jQuery object representing the save button
	* @type {object}
	*/
	saveButton: null,

	/**
	* The Indicator showing if it's necessary 
	* to keep the locking for the locked record
	* @type {boolean}
	*/	
	keepLocking: false,
	
	/**
	 * After Successful save page action
	 * @type {number}
	 */
	afterSaveAction: -1,
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.EditorPage.superclass.constructor.call(this, cfg);
		
		this.submitUrl = Runner.getPageUrl( Runner.pages.PageSettings.getShortTName(this.tName), this.pageType );
		this.submitUrl += "?page=" + this.pageName + "&submit=1&";
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.submitUrl += "fly=1&";
		}

		this.pageControlsMode = this.pageType;

		this.isShowDetails = this.checkIfShowDetails();
		
		this.setBaseParams();
		
		this.addEvents("beforeSave", "afterSave");	
	},
	
	/**
	 * @return {boolean}
	 */
	checkIfShowDetails: function() {
		return Runner.pages.PageSettings.getTableData(this.tName, "isShowDetails");
	},
	
	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		this.baseParams = this.baseParams || {};
		
		this.baseParams["id"] = this.id;
		this.baseParams["editType"] = this.baseParams["editType"] || this.editType;
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.baseParams["onFly"] = 1;
		}

		this.baseParams = Runner.apply( this.baseParams, this.getStateParams() );
	},	
	
	/**
	 * @destructor
	 */
	destructor: function() {
		Runner.pages.EditorPage.superclass.destructor.call(this);
		if ( this.form ) {
			this.form.destructor();
			this.form = null;
		}
	},
	
	init: function() {
		Runner.pages.EditorPage.superclass.init.call(this);	

		if ( this.beforeSave ) {
			this.on( {'beforeSave': this.beforeSave} );
		}
		if ( this.afterSave ) {
			this.on( {'afterSave': this.afterSave} );
		}

		this.initButtons();
	},
	
	initForGrid: function() {
		Runner.pages.EditorPage.superclass.initForGrid.call( this );
		this.initMaps();
	},
	
	initMaps: function() {
		if ( this.getMasterGrid() && this.hasMaps() ) {
			Runner.globalGmapLoader.onLoad( function() {
				this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps ); 
				this.mapManager.init();
			}, this );
		}	
	},
	
	/**
	 * Initialize the page's functionality that need initializing after tabs are loaded.
	 * If there are no tabs on the page it's invoked from the page's init method 
	 * otherwise from the initTabs method
	 */
	initForTabs: function() {
		Runner.pages.EditorPage.superclass.initForTabs.call( this );
		
		this.setFirstFocus();
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.on('windowSizeCorrected', this.setFirstFocus);
		}

		this.initDetails(); 		
	},
	
	/**
	 * stub
	 */
	initDetails: Runner.emptyFn,
	
	/**
	 * Init the 'Save' and the popup window's 'close' buttons
	 */	 
	initButtons: function() {
		var pageObj = this;
		
		$( "[id=resetButton" + this.id +"]" ).on("click", function(e) {
			pageObj.resetPage();
			e.preventDefault();
		});
		
		$( "[id=backToMenuButton" + this.id + "]" ).on("click", function(e) {
			window.location.href = Runner.getPageUrl("menu");
			return false;
		});			
		
		this.saveButton = $("[id=saveButton" + this.id + "]").on("click", function() {
			if ( Runner.isDisabledButton( this ) ) {
				return false;
			}
			
			var $button = $(this),
				controls, i;
			
			pageObj.disablePageButtonsWhileSaving();
			
			pageObj.fileFieldsCount = 0;
			pageObj.upploadErrorHappened = false;
			
			controls = Runner.controls.ControlManager.getAt(pageObj.tName, pageObj.pageId);
			for (i = 0; i < controls.length; i++) {
				if (controls[i].editFormat == Runner.controls.constants.EDIT_FORMAT_FILE && controls[i].filesToUploadCount > 0) {
					controls[i].errorHappened = false;
					pageObj.fileFieldsCount++;
					
					controls[i].uploadForm.bind('fileuploadstopped', { ctrl: controls[i] }, function(e) {
						pageObj.fileFieldsCount--;
						$button.on('fileuploadstopped');
						if (e.data.ctrl.errorHappened) {
							pageObj.upploadErrorHappened = true;
							pageObj.errorHn();
						} else {
							pageObj.callSaveHn();
						}
					});
					$(".btn-primary.start", controls[i].uploadForm).trigger("click");
				} 
			}
			if (pageObj.fileFieldsCount < 1) {
				pageObj.saveHn();
			}
			return false;
		});
	},

	
	/**
	 * see Runner.pages.PageManager.createPopup
	 * It's invoked on popup close
	 * @param {object} modalWrapper
	 */
	afterCloseFlyWinHandler: function ( modalWrapper ) {
		if ( this.locking && !this.keepLocking ) {
			this.locking.unlockRecordInline( this.sKeys );
		}
		Runner.pages.EditorPage.superclass.afterCloseFlyWinHandler.call( this, modalWrapper );
	},	
	
	/**
	 * reset button handler's functionality
	 */	
	resetPage: function() {
		this.resetControls();
		clearTimeout( this.validateTimer );
		
		if ( this.controlsMap.reCaptcha !== undefined ) {
			var reCaptchaClientId = $( '#reCaptchaFor' + this.controlsMap.reCaptcha.inputCaptchaId ).data('client');
			grecaptcha.reset( reCaptchaClientId );
		}		
		
		this.setPageModified( false );
	},

	/**
	 * @param {Object} row
	 */
	errorHn: function( row ) {
		if ( this.fileFieldsCount < 1 ) {
			this.enablePageButtonsAfterSaving();
		}
	},
	
	/**
	 * Call the page's data-save handler
	 */
	callSaveHn: function() {
		if ( this.fileFieldsCount < 1 && !this.upploadErrorHappened ) {
			this.saveHn();
		}
	},
	
	/**
	 * @return Array
	 */
	getVisibleControls: function() {
		var controls = Runner.controls.ControlStorage.byId( this.pageId ),
			visibleControls = [], i;
		
		for( i = 0; i < controls.length; ++i ) {
			if ( !controls[i].hiddenField() ) {
				visibleControls.push( controls[i] );
			}
		}
		
		return visibleControls;
	},

	getFormControls: function() {
		return Runner.controls.ControlStorage.byId( this.pageId );
	},
	
	/**
	 * Assign the 'form' property with the new 
	 * Runner.form.BasicForm object reference.
	 */
	getForm: function() {
		this.form = new Runner.form.BasicForm({
			submitUrl: this.submitUrl,	
			standardSubmit: this.isSubmitStandard(),
			isFileUpload: true,
			method: 'POST',
			id: this.pageId,
			baseParams: this.baseParams,
			fieldControls: this.getFormControls(),
			useMultipart: true,
			submitFailed: {
				fn: function( response, formObj, fieldControls ) {
					if ( response === -1 ) { //#4511 ? 
						formObj.clearForm();
						this.showError("<< " + Runner.lang.constants.INLINE_ERROR + " >>");
					} else {
						Runner.displayGenericAjaxError( response );
					}

					this.fireEvent("afterSave", {success: false, html: response}, formObj, fieldControls, this);
					
					this.enablePageButtonsAfterSaving();
				},
				scope: this
			},
			beforeSubmit: {
				fn: function( formObj ) {
					return this.fireEvent("beforeSave", formObj, formObj.fieldControls, this);
				},
				scope: this
			},
			validationFailed: {
				fn: function( formObj, fieldControls, failedControlsArr ) {
					var fNamesArr = [], i;
					
					for (i = 0; i < failedControlsArr.length; i++) {
						fNamesArr.push( failedControlsArr[i].fieldName );
					}
					
					this.openFieldTabsSections( fNamesArr );
					
					// scroll to the firt field, failed its validation, to make it visible once tab/sec is open
					setTimeout(function() { failedControlsArr[0].setFocus(); }, 4);
					
					this.enablePageButtonsAfterSaving();
					return false;
				},
				scope: this
			}
		});
	},

	/**
	 * Check if the page's form must use the standard submit process
	 * @return Boolean
	 */
	isSubmitStandard: function() {
		return this.openMode === Runner.pages.constants.OPENMODE_PRELOADED;
	},
	
	/**
	 * The page's data-save handler
	 */
	saveHn: function() {
		this.getForm();
		this.setPageModified( false );
		this.form.submit();	
	},
	
	/**
	 * Show the error messages
	 * @param {string | array} txt
	 * @param {boolean} isArr
	 */
	showError: function(txt, isArr) {
		if ( !txt.length ) {
			return;
		}
		
		if ( !isArr ) {
			this.displayHalfPreparedMessage( txt, true );
		} else {
			var $messageElem = this.findItemType( this.messageItemType() );
			$messageElem.html('');
			for (var i = 0; i < txt.length; i++) {
				$messageElem.append( this.getMessageText( txt[i], true ) + ( i < txt.length - 1 ? '<br>' : '' ) );
				$messageElem.toggleClass('alert-danger', true);
			}
		}
		
		this.showElement("message");
	},
	
	/**
	 * scroll to the details
	 */ 
	scrollToDetails: function( dpId ) {
		var detailsGridPosition = $( "#detailPreview" + dpId ).position();
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			var $modalBody = $(".modal-body", this.bsWin);
			$modalBody.animate( { scrollTop: detailsGridPosition.top + $modalBody.scrollTop() } );
		} else {
			$(window).scrollTop( detailsGridPosition.top );
		}	
	},
	
	/**
	 * Disable some page's buttons along with the 'Save' button
	 */
	disablePageButtonsWhileSaving: function() {
		Runner.addDisabledClass( this.saveButton );
	},
	
	/**
	 * Enable some page's buttons along with the 'Save' button
	 */
	enablePageButtonsAfterSaving: function() {
		Runner.delDisabledClass( this.saveButton );
	},
	
	/**
	 * A stub
	 */
	enablePageButtons: function() {
	},
	
	/**
	 *
	 */
	cleanupBeforeReload: function() {
		Runner.pages.EditorPage.superclass.cleanupBeforeReload.call( this );
		Runner.controls.ControlStorage.unregisterById( this.pageId );
		
		if ( this.form ) {
			this.form.destructor();
			this.form = null;
		}
		
		this.upploadErrorHappened = false;
	},
	
	resetControls: function() {
		var cntrls = Runner.controls.ControlStorage.byId( this.id ),
				updContext, i;
			
		if ( !cntrls || !cntrls.length ) {
			return;
		}
			
		updContext = {
			enableNextButtons: false,
			resetHappend: true,
			values: {}
		};
			
		for (i = 0; i < cntrls.length; i++) {
			if ( cntrls[i].mode === this.pageControlsMode && cntrls[i].editFormat != "Readonly" ) {
				updContext.values[ cntrls[i].fieldName ] = cntrls[i].defaultValue;
			}
		}
		
		for (i = 0; i < cntrls.length; i++) {
			if ( cntrls[i].mode === this.pageControlsMode && cntrls[i].editFormat != "Readonly" ) {
				cntrls[i].reset( updContext );
			}
		}
	},
	
	/**
	 * focus on invalid capthca
	 * @param {string} captchaFieldName
	 */
	onCaptchaFailed: function( captchaFieldName ) {
		if ( captchaFieldName !== undefined ) {	
			this.openFieldTabsSections( [ captchaFieldName ] );
			Runner.getControl( this.pageId, captchaFieldName ).setFocus();
		}
	},
});Runner.pages.AddPageFly = Runner.extend( Runner.pages.EditorPage, {
	
	pageType: Runner.pages.constants.PAGE_ADD,
	
	editType: Runner.pages.constants.ADD_ONTHEFLY,
	
	/**
	 * The main lookup control
	 * @type {object}
	 */
	lookupCtrl: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.AddPageFly.superclass.constructor.call(this, cfg);
	},	

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.AddPage.superclass.setBaseParams.call( this );

		this.baseParams['a'] = "added";		
	},
	
	init: function() {
		Runner.pages.AddPageFly.superclass.init.call( this );
		this.fireEvent("afterInit", this, this.proxy, this.id);
	}, 
	
	/**
	 * Initialize the page's buttons
	 */
	initButtons: function() {
		Runner.pages.AddPage.superclass.initButtons.call( this );	
		
		var pageObj = this;
		
		$("[id='cancelButton" + this.id + "']").on("click", function(e) {
			pageObj.close();
			return false;
		});
	},
	
	/**
	 * Assign the "form" property with the new 
	 * Runner.form.BasicForm object reference.
	 * Add the form's "successSubmit" event handler	 
	 */	
	getForm: function() {
		if ( this.form ) {
			return this.form;
		}
		
		Runner.pages.AddPageFly.superclass.getForm.call( this );
		
		this.form.on("successSubmit", function( respObj, basicForm, fieldControls ) {
			var evRes = this.fireEvent("afterSave", respObj, basicForm, fieldControls, this);
			
			if ( evRes !== false ) {
				this.close();
			}
		}, this);
	},
	
	messageItemType: function() {
		return 'add_message';
	}	
});Runner.pages.AddPage = Runner.extend( Runner.pages.EditorPage, {

	pageType: Runner.pages.constants.PAGE_ADD,

	masterSaved: false,

	/**
	 * The object supplying the popup mode add page
	 * with list page's inline object data
	 */
	inlineObjectParams: null,

	/**
	 * The added record keys
	 * @type {array}
	 */
	keys: null,

	/**
	 * The added record master keys	data
	 * @type {object}
	 */
	mKeys: null,

	/**
	 * @type {string}
	 */
	afterAddId: "",

	/**
	 * @type {object}
	 */
	detKeys: null,

	/**
	 * The jQuery object representing the 'View page' button
	 * @type {jQuery object}
	 */
	viewPageButton: null,

	/**
	 * The jQuery object representing the 'Edit page' button
	 * @type {jQuery object}
	 */
	editPageButton: null,

	/**
	 * @type {boolean}
	 */
	recordAdded: false,

	/**
	 * flag to prevent starting multiple redirects
	 */
	redirectStarted: false,


	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.AddPage.superclass.constructor.call(this, cfg);

		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP || this.isShowDetails ) {
			this.afterSaveAction = Runner.pages.PageSettings.getTableData(this.tName, "afterAddAction");
		}

		if ( this.proxy[ this.shortTName + "_recordAdded" ] ) {
			this.recordAdded = true;
		}
	},

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.AddPage.superclass.setBaseParams.call( this );

		this.baseParams["a"] = "added";

		if ( this.dashboard ) {
			this.baseParams.dashelement = this.dashElement;
			this.baseParams.dashTName = this.dashboard.tName;
			this.baseParams.dashPage = this.dashboard.pageName;
		}

		if ( this.isShowDetails ) {
			this.baseParams["editType"] = this.openMode === Runner.pages.constants.OPENMODE_POPUP ? Runner.pages.constants.ADD_MASTER_POPUP : Runner.pages.constants.ADD_MASTER;
		}
		
		if ( this.pageData["listPage"] ) {
			this.baseParams["listPage"] = this.pageData["listPage"];
		}
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		Runner.pages.AddPage.superclass.init.call( this );
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},

	/**
	 * Initialize the page's buttons
	 */
	initButtons: function() {
		Runner.pages.AddPage.superclass.initButtons.call( this );

		var pageObj = this;

		$( "[id=backButton" + this.id + "], #extraBackButton" + this.id ).on("click", function(e) {
			window.location.href = pageObj.backToListUrl( $(this).data("page") );
			return false;
		});

		$( "[id=cancelButton" + this.id + "]" ).on("click", function(e) {
			pageObj.close();
			return false;
		});

		this.initNavigationButtons();
	},

	/**
	 * Initialize the 'View' and 'Edit' buttons in success message
	 */
	initNavigationButtons: function() {
		var pageObj = this;

		this.viewPageButton	= $("[id='viewPageButton" + this.id + "']").on("click", function(e) {
			var keys = $(this).data("keys");

			if ( Runner.isDisabledButton( this ) || !keys ) {
				return false;
			}

			Runner.addDisabledClass( this.viewPageButton );
			pageObj.openEditViewPage("view", keys );
			return false;
		});

		this.editPageButton	= $("[id='editPageButton" + this.id + "']").on("click", function(e) {
			var keys = $(this).data("keys");
			if ( Runner.isDisabledButton( this ) || !keys ) {
				return false;
			}

			Runner.addDisabledClass( this.editPageButton );
			pageObj.openEditViewPage("edit", keys );
			return false;
		});
	},

	/**
	 * Open the corresponding view page
	 */
	openEditViewPage: function( pageType, keys ) {
		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP ) {
			var type = pageType === "view" ? Runner.pages.constants.PAGE_VIEW : Runner.pages.constants.PAGE_EDIT;
			window.location.href = Runner.pages.getUrl( this.tName, type, keys, "editid", this.getStateParams() );
			return;
		}

		var pageParams = pageType === "view"
			? Runner.pages.ViewPage.prototype.getOpenPageParams.call( this, keys, this.baseParams.rowId )
			: Runner.pages.EditPage.prototype.getOpenPageParams.call( this, keys, this.baseParams.rowId );

		Runner.pages.PageManager.openPage( pageParams );
	},

	/**
	 *
	 * @param {String} tName
	 * @return {Boolean}
	 */
	isInlineAddContentChanged: function( tName ) {
		if ( !this.dpObjs || !this.dpObjs[ tName ] ) {
			return false;
		}

		var inlineObj = this.dpObjs[ tName ].detailsPageObj.inlineAdd;
		return inlineObj && inlineObj.inlineAddChangeContent;
	},

	/**
	 * Initialize details list page
	 */
	initDetails: function() {
		if ( !this.isShowDetails ) {
			return;
		}

		var dpTablesParams = this.controlsMap.dpTablesParams,
			dtName, dParams, i;

		if ( !dpTablesParams ) {
			return;
		}

		this.dpObjs = {};

		for (i = 0; i < dpTablesParams.length; i++) {
			dtName = dpTablesParams[i].tName;

			dParams = {
				tName: dtName,
				parId: this.id,
				masterTName:this.tName,
				id : dpTablesParams[i].id,
				pType: dpTablesParams[i].pType,
				controlsMap: this.controlsMap.dControlsMap[ dtName ],
				useChildCount: this.openMode === Runner.pages.constants.OPENMODE_POPUP,
				masterPageObj: this,
				saveFailed: {
					fn: this.onDetailsSavingFailed,
					scope: this
				},
				detailsSaved: {
					fn: this.onDetailsSaved,
					scope: this
				}
			};

			this.dpObjs[ dtName ] = new Runner.util.details.AddDP( dParams );
			this.dpObjs[ dtName ].init();
		}
	},

	/**
	 * The 'detailsSaved' event's handler
	 * @param {object} detObj
	 * @param {array} allVals
	 * @param {array} fields
	 * @param {array} allKeys
	 * @param {array} allRowIds
	 */
	onDetailsSaved: function( detObj, allVals, fields, allKeys, allRowIds ) {
		var detailsSaved = true,
			tName, hostListPage;

		for ( tName in this.dpObjs ) {
			if ( this.isInlineAddContentChanged( tName ) && this.dpObjs[ tName ].detailsPageObj.inlineAdd
				&& ( !this.dpObjs[ tName ].submitMade || !this.dpObjs[ tName ].submitSucceded ) ) {
				detailsSaved = false;
			} else {
				$("#_" + this.dpObjs[ tName ].id).parent().remove();
			}
		}

		if ( !detailsSaved ) {
			return;
		}

		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP && this.afterAddId ) {
			var params = this.getStateParams();
			params['afteradd'] = this.afterAddId;

			if( this.redirectStarted ) {
				return;
			}
			this.redirectStarted = true;
			window.location.href = Runner.pages.getUrl( this.tName, this.pageType, "", "", params );
			return;
		}

		if ( !!allRowIds.length || this.masterSaved ) {
			// several details were added
			hostListPage = Runner.pages.PageManager.getAt( this.tName, this.baseParams.parId )

			for ( tName in hostListPage.dpObjs ) {
				hostListPage.dpObjs[ tName ].updateChildRecNum( this.detKeys[ tName ] );
			}

			this.runAfterAddAction();
		}

		this.noDetailsToSave = true;
	},

	/**
	 * The detail 'saveFailed' event's handler
	 * @param {object} detObj
	 * @param {object} respObj
	 * @param {object} formObj
	 */
	onDetailsSavingFailed: function( detObj, respObj, formObj ) {
		var messArr = [],
			pageObj = this,
			tName, msg, txt,
			dpId;

		for (tName in this.dpObjs) {
			if ( this.isInlineAddContentChanged( tName ) ) {
				if ( this.dpObjs[ tName ].submitMade && !this.dpObjs[ tName ].submitSucceded ) {
					msg = Runner.lang.constants.DETAIL_NOT_SAVED.replace('%s', "<b>" + tName + "</b>");

					txt = "<div id='_" + this.dpObjs[ tName ].id + "' class='message mes_not'> " + msg
							+ " <a class='goToDetails' href='#' data-dpid='" + this.dpObjs[ tName ].id + "'>"
								+ Runner.lang.constants.DETAIL_GOTO + " " + tName + "</a>"
							+ " </div>";

					dpId = dpId || this.dpObjs[ tName ].id;

					messArr.push( txt );
				} else if ( !this.dpObjs[ tName ].submitMade ) {
					return;
				}
			}
		}

		//make saved master's fields readonly
		this.form.makeReadonly();

		this.findItemType( this.messageItemType() ).on( "click", ".goToDetails", function(e) {
			pageObj.scrollToDetails( $(this).data("dpid") );
			return false;
		});

		this.showError( messArr, true );
		this.scrollToDetails( dpId );

		Runner.delDisabledClass( this.saveButton );
	},

	/**
	 *
	 */
	addFormEventsForDetails: function() {
		if ( !this.isShowDetails ) {
			return;
		}

		this.form.on( 'beforeSubmit', function( basicForm ) {
			var valRes, tName;

			for (tName in this.dpObjs) {
				if ( this.isInlineAddContentChanged( tName ) && !this.dpObjs[ tName ].validate() ) {
					valRes = false;
				}
			}

			if ( valRes === false ) {
				basicForm.validate();
				this.enablePageButtonsAfterSaving();
			}

			return valRes;
		}, this );

		this.form.on( 'successSubmit', function( respObj, basicForm, fieldControls ) {
			if ( respObj.success ) {
				this.keys = respObj.keys;
				this.mKeys = respObj.mKeys;
				this.message = respObj.message;
				this.detKeys = respObj.detKeys;

				this.afterAddId = respObj.afterAddId || "";
				this.noDetailsToSave = false;
				for (var tName in this.dpObjs) {
					this.dpObjs[ tName ].saveDetails( respObj.mKeys[ tName ] );
				}
			}
		}, this );
	},

	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object
	 * reference. Add the form's 'beforeSubmit', 'successSubmit', 'submitFailed'
	 * events handlers basing on the page's type and settings
	 */
	getForm: function() {
		if ( this.form ) {
			return;
		}

		Runner.pages.AddPage.superclass.getForm.call( this );

		this.addFormEventsForDetails();

		this.form.on( 'successSubmit', function( respObj, basicForm, fieldControls ) {
			var evRes;

			if ( respObj.hideCaptcha ) {
				$('.captcha_block').remove();
			}

			if ( respObj.message ) {
				this.displayHalfPreparedMessage( respObj.message, !respObj.success);
				this.showElement("message");
				this.adjustHeightAndWidth();
			}

			this.message = respObj.message;
			this.keys = this.keys || respObj.keys;
			this.detKeys = this.detKeys || respObj.detKeys;


			this.onCaptchaFailed( respObj.wrongCaptchaFieldName );
			if ( respObj.wrongCaptchaFieldName === undefined ) {
				this.setFirstFocus();
			}

			this.initLoginLink();

			if ( !respObj.success ) {
				this.enablePageButtonsAfterSaving();
				return;
			}

			this.masterSaved = true;

			evRes = this.fireEvent("afterSave", respObj, basicForm, fieldControls, this);

			if ( evRes !== false && ( !this.isShowDetails || this.noDetailsToSave ) ) {
				this.runAfterAddAction();
				return;
			}

		}, this );
	},

	/**
	 * @return {boolean}
	 */
	checkIfNoDetailsWereAdded: function() {
		for (var tName in this.dpObjs) {
			if ( !!this.dpObjs[ tName ].detailsPageObj.inlineAdd.rows.length ) {
				return false;
			}
		}
		return true;
	},

	/**
	 * Stay on the page, close it or open another page
	 * basing on add page settings
	 */
	runAfterAddAction: function() {
		var pageParams, inlineObj, addedRowId, dTableName, keyParams, dKey,
			popupMode = this.openMode === Runner.pages.constants.OPENMODE_POPUP,
			pageObj = this;

		switch( this.afterSaveAction ) {
			case Runner.pages.constants.AA_TO_ADD:
				if ( !popupMode ) {
					window.location.href = Runner.pages.getUrl( this.tName, this.pageType, "", "", this.getStateParams() );
					break;
				}

				pageParams = this.getOpenPageParams( null, function() {
					if ( pageObj.message ) {
						this.displayHalfPreparedMessage( pageObj.message, false );
						this.showElement("message");
					}

					this.resizeAndMoveWindow( pageObj );
					pageObj.close();
				});

				if ( this.pageName ) {
					pageParams.baseParams["page"] = this.pageName;
				}

				Runner.pages.PageManager.openPage( pageParams );
				break;

			case Runner.pages.constants.AA_TO_VIEW:
				if ( !popupMode ) {
					window.location.href = Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_VIEW, this.keys, "editid", this.getStateParams() );
					break;
				}

				inlineObj = this.inlineObjectParams.getInlineObject();
				addedRowId = inlineObj.rows[ inlineObj.rows.length - 1 ].id;

				pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( this, this.keys, addedRowId );
				Runner.pages.PageManager.openPage( pageParams );
				break;

			case Runner.pages.constants.AA_TO_EDIT:
				if ( !popupMode ) {
					window.location.href = Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_EDIT, this.keys, "editid", this.getStateParams() );
					break;
				}

				inlineObj = this.inlineObjectParams.getInlineObject();

				addedRowId = inlineObj.rows[ inlineObj.rows.length - 1 ].id;

				pageParams = Runner.pages.EditPage.prototype.getOpenPageParams.call( this, this.keys, addedRowId, pageObj.message );
				pageParams.popupCentered = true;
				Runner.pages.PageManager.openPage( pageParams );
				break;

			case Runner.pages.constants.AA_TO_DETAIL_LIST:
				dTableName = Runner.pages.PageSettings.getTableData(this.tName, "afterAddActionDetTable");
				keyParams = [];
				for ( dKey in this.detKeys[ dTableName ] ) {
					if ( this.detKeys[ dTableName ].hasOwnProperty( dKey ) ) {
						keyParams.push( dKey + "=" + encodeURIComponent( this.detKeys[ dTableName ][ dKey ] ) );
					}
				}

				window.location.href = Runner.getPageUrl( dTableName, Runner.pages.constants.PAGE_LIST )
					+ "?" + keyParams.join("&") + "&mastertable=" + encodeURIComponent( this.tName );
				break;

			case Runner.pages.constants.AA_TO_DETAIL_ADD:
				dTableName = Runner.pages.PageSettings.getTableData(this.tName, "afterAddActionDetTable");

				keyParams = [];
				for ( dKey in this.detKeys[ dTableName ] ) {
					if ( this.detKeys[ dTableName ].hasOwnProperty( dKey ) ) {
						keyParams.push( dKey + "=" + encodeURIComponent( this.detKeys[ dTableName ][ dKey ] ) );
					}
				}

				window.location.href = Runner.getPageUrl( dTableName, Runner.pages.constants.PAGE_ADD )
					+ "?" + keyParams.join("&") + "&mastertable=" + encodeURIComponent( this.tName );
				break;

			case Runner.pages.constants.AA_TO_LIST:
				if ( !popupMode ) {
					window.location.href = Runner.pages.getUrl( this.tName, Runner.pages.constants.PAGE_LIST, "", "", this.getStateParams() );
					break;
				}
				this.close();
				break;
			default:
				this.close();
		}
	},

	/**
	 * Check if the page's form must use the standard submit process
	 * @return Boolean
	 */
	isSubmitStandard: function() {
		return !this.isShowDetails && this.openMode !== Runner.pages.constants.OPENMODE_POPUP ;
	},

	/**
	 * The page's 'save data' handler
	 */
	saveHn: function() {
		if ( !this.masterSaved ) {
			Runner.pages.AddPage.superclass.saveHn.call( this );
			return;
		}

		if ( !this.isShowDetails ) {
			return;
		}

		for (var tName in this.dpObjs) {
			this.dpObjs[ tName ].saveDetails( this.mKeys[ tName ] );
		}
	},

	/**
	 * Get configuration params to open a pop-up add page
	 * @param {string} parId
	 * @param {function} afterCreateHn
	 * @return {object}
	 */
	getOpenPageParams: function( parId, afterCreateHn ) {
		var hostListPage = Runner.pages.PageManager.getAt( this.tName, parId || this.baseParams.parId ),
			inlineObj =	hostListPage.inlineAdd || this.inlineObjectParams?.getInlineObject(),
			newRowId,
			pageParams = {
				tName: this.tName,
				pageType: Runner.pages.constants.PAGE_ADD,
				modal: true,
				editType: Runner.pages.constants.ADD_POPUP,
				baseParams: {
					parId: hostListPage.id || this.baseParams.parId,
					table: this.tName,
					editType: Runner.pages.constants.ADD_POPUP,
					spreadsheetGrid: hostListPage.spreadsheet ? 1 : 0,
					hostPageName: hostListPage.pageName,
				},
				inlineObjectParams: {
					getInlineObject: function() {
						return inlineObj;
					}
				},
				afterSave: {
					fn: function( respObj, formObj, fieldControls, addPageObj ) {
						if ( respObj.success ) {
							if( inlineObj ) {
								var addedRow = inlineObj.addRowToGrid( respObj, true, newRowId );
								hostListPage.initNewRowButtons( addedRow );
							}
							if( hostListPage.pageType == 'calendar' ) {
								//	add new event
								hostListPage.insertAddedEvent( respObj.event );
							}
							if( hostListPage.pageType == 'gantt' ) {
								//	add new event
								hostListPage.insertAddedTask( respObj.task );
							}
							return;
						}

						addPageObj.bsWin.animate( {scrollTop: 0} );
						return false;
					},
					scope: hostListPage
				},
				beforeSave: {
					fn: function( formObj, fieldControls, addPageObj ) {
						newRowId = Runner.genId();
						formObj.baseParams[ "newRowId" ] = newRowId;
					},
					scope: this
				},

			};

		if( hostListPage.pageType == 'gantt' ) {
			pageParams.baseParams['gantt'] = 1;
		}
		if( hostListPage.pageType == 'calendar' ) {
			pageParams.baseParams['page'] = 'add_calendar';
		}
		pageParams.baseParams = Runner.apply( pageParams.baseParams, hostListPage.getStateParams() );

		if ( this.dashboard ) {
			pageParams.baseParams.dashelement = this.dashElement;
			pageParams.baseParams.dashTName = this.dashboard.tName;
			pageParams.baseParams.dashPage = this.dashboard.pageName;

		}

		if ( hostListPage.pageMode == Runner.pages.constants.LIST_DASHBOARD || hostListPage.pageMode == Runner.pages.constants.LIST_DASHDETAILS ) {
			pageParams.baseParams.dashTName = hostListPage.dashboard.tName;
			pageParams.baseParams.dashPage = hostListPage.dashboard.pageName;
			pageParams.baseParams.dashelement = hostListPage.dashElement;
		}

		if ( typeof afterCreateHn === "function" ) {
			pageParams.afterCreateHn = afterCreateHn;
		}

		return pageParams;
	},

	messageItemType: function() {
		return 'add_message';
	}
});
Runner.pages.EditPage = Runner.extend( Runner.pages.EditorPage, {
	
	keys: null,
	
	keyFields: null,
	
	prevKeys: null,
	
	nextKeys: null,
	
	pageType: Runner.pages.constants.PAGE_EDIT,
	
	details: null,
	
	/**
	 * The jQuery object representing the 'Previous record' button
	 * @typs {jQuery object}
	 */ 
	prevButton: {},
	
	/**
	 * The jQuery object representing the 'Next record' button
	 * @typs {jQuery object}
	 */ 
	nextButton: {},
	
	/**
	 * The jQuery object representing the 'View page' button	
	 * @type {jQuery object}
	 */ 	
	viewPageButton: null,
	
	/**
	 * The object supplying the popup mode edit page
	 * with list page's inline object data
	 */
	inlineObjectParams: null,

	/**
	 * It shows if the page is popup window 
	 * for a dash grid element
	 * @type {boolean}
	 */
	dashGridBased: false,
	
	/**
	 * @type {boolean}
	 */
	recordUpdated: false, 

	/**
	 * @type {array}
	 */
	 dpTablesParams: null,

	 hostPage: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.EditPage.superclass.constructor.call(this, cfg);
		
		this.detailsMasterKeys = this.pageData['detailsMasterKeys'] || {};
		
		
		this.keys = cfg.keys || Runner.pages.PageSettings.getTableData(this.tName, 'keys');
		this.keyFields = cfg.keyFields || Runner.pages.PageSettings.getTableData(this.tName, 'keyFields');
		//	sKeys - keys of current record use for locking
		this.sKeys = Runner.pages.PageSettings.getTableData(this.tName, "sKeys", "");
		this.prevKeys = Runner.pages.PageSettings.getTableData(this.tName, 'prevKeys');
		this.nextKeys = Runner.pages.PageSettings.getTableData(this.tName, 'nextKeys');
		
		this.shortTName = Runner.pages.PageSettings.getShortTName(this.tName);
		
		this.submitUrl += this.getKeysUrlParams();

		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.afterSaveAction = Runner.pages.PageSettings.getTableData(this.tName, "afterEditAction");
		}

		if ( this.proxy[ this.shortTName + "_recordUpdated" ] ) {
			this.recordUpdated = true;
		}
		
		this.dpTablesParams = this.controlsMap && this.controlsMap.dpTablesParams;		
	},

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.EditPage.superclass.setBaseParams.call( this );

		this.baseParams["a"] = "edited";

		if ( this.dashboard ) {
			this.baseParams.dashelement = this.dashElement;
			this.baseParams.dashTName = this.dashboard.tName;
			this.baseParams.dashPage = this.dashboard.pageName;
		}

		if ( this.pageData["listPage"] ) {
			this.baseParams["listPage"] = this.pageData["listPage"];
		}		
	},
	
	/**
	 * Initialize the page
	 */	
	init: function() {
		Runner.pages.EditPage.superclass.init.call(this);
		this.initLocking();
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},

	/**
	 * reset button handler's functionality
	 */
	resetPage: function() {
		Runner.pages.EditPage.superclass.resetPage.call( this );
		this.enablePageButtons();
	},

	/**
	 * Initialize the edit page's buttons
	 */
	initButtons: function() {
		Runner.pages.EditPage.superclass.initButtons.call( this );
		
		var pageObj = this;
		
		$( "[id='backButton" + this.id + "'], [id=extraBackButton" + this.id + "]").on("click", function(e) {
			pageObj.unlockPage( pageObj.backButtonHref.bind( pageObj, $(this).data("page") ) );
			return false;
		});
		
		$( "[id=closeButton" + this.id + "]" ).on("click", function(e) {
			pageObj.hideSearchControllerContainers();
			pageObj.unlockPage( function() {
				pageObj.close();
			});
			return false;
		});
		
		this.initNavigationButtons();
	},

	/**
	 * Initialize navigation (the 'prev/next' buttons) to the previous and next records.
	 * Initialize the 'View' buttons
	 */
	initNavigationButtons: function() {
		var pageObj = this;
		
		this.viewPageButton	= $("[id='viewPageButton" + this.id + "']");
		
		if ( !this.viewPageButton.attr("href") ) {
			this.viewPageButton
				.attr("href", Runner.pages.getUrl( this.tName, this.pageType, this.keys, this.getStateParams() ) );
		}
			
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.viewPageButton.on( "click",  function(e) {
				if ( Runner.isDisabledButton( this ) ) {
					return false;
				}

				var pageName = $(this).data("page") || "";
				var pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( pageObj, pageObj.keys, pageObj.baseParams.rowId );
				if ( pageName ) {
					pageParams.baseParams["page"] = pageName;
				}
				
				Runner.pages.PageManager.openPage( pageParams );
				return false;
				
			});
		} else {
			this.viewPageButton.on( "click",  function(e) {
				if ( Runner.isDisabledButton( this ) ) {	
					return false;
				}				
				// unlock before leave the page
				pageObj.unlockPage();
			});
		}
		
		this.nextButton = $( "[id=nextButton" + this.id + "]" );
		this.prevButton = $( "[id=prevButton" + this.id + "]" );
		
		this.updateNavigationVisibility();

		if ( !this.nextButton.hasClass("rnr-invisible-button") ) {		
			this.nextButton.on("click", function(e) {
				if ( Runner.isDisabledButton( this ) ) {	
					return false;
				}
				
				pageObj.disablePageButtons();			
				pageObj.unlockPage( function() {
					pageObj.openPrevNextRecordPage( pageObj.nextKeys, 1 );
				});
				return false;			
			});
		}
		
		if ( !this.prevButton.hasClass("rnr-invisible-button") ) {
			this.prevButton.on("click", function(e) {
				if ( Runner.isDisabledButton( this ) ) {
					return false;
				}
				
				pageObj.disablePageButtons();
				pageObj.unlockPage( function() {
					pageObj.openPrevNextRecordPage( pageObj.prevKeys, -1 ); 
				});
				return false;
			});	
		}
	},
	
	/**
	 * A stub
	 */
	updateNavigationVisibility: function() {						
	},	
	
	/**
	 * Run the handler immediately or unlock the page's records 
	 * basing on the 'useLocking' property value. In the second case 
	 * the handler is passed as a callback to the 'UnlockRecord' method 
	 * 
	 * @param {Function} handler
	 */
	unlockPage: function( handler ) {
		if ( this.useLocking && this.locking ) {
			this.locking.unlockRecord( this.sKeys, '', handler );
			return;
		}

		if ( typeof handler === 'function' ) {
			handler();
		}
	},
	
	/**
	 * Disable the 'Next record', 'Prev record' 
	 * and 'View page' navigation buttons
	 */
	disablePageButtons: function() {
		Runner.addDisabledClass( this.nextButton );
		Runner.addDisabledClass( this.prevButton );
		Runner.addDisabledClass( this.viewPageButton );
	},
	
	/**
	 * Enable the 'Next record', 'Prev record' 
	 * and 'View page' navigation buttons
	 */	
	enablePageButtons: function() {
		Runner.delDisabledClass( this.nextButton );
		Runner.delDisabledClass( this.prevButton );
		Runner.delDisabledClass( this.viewPageButton );		
	},
	
	/**
	 * Open the page corresponding to the next/previous record
	 * @param {Array} keys
	 * @param {Number} step
	 */
	openPrevNextRecordPage: function( keys, step ) {
		if ( this.openMode == Runner.pages.constants.OPENMODE_PRELOADED ) {
			var params = this.getStateParams();
			if ( this.pageName ) {
				params["page"] = this.pageName;
			}
			
			window.location.href = Runner.pages.getUrl( this.tName, this.pageType, keys, "editid", params );
			return;	
		}
		
		var pageParams = this.getOpenPageParams( keys, this.baseParams.rowId + step );
		if ( this.pageName ) {
			pageParams.baseParams.page = this.pageName;
		}
		
		Runner.pages.PageManager.openPage( pageParams );
	},
	
	/**
	 * Get an object containing params for the Runner page's openPage method
	 * @param {Array} keys
	 * @param {Number} rowId
	 * @param {String} message (optional)
	 * @return {Object}
	 */	
	getOpenPageParams: function( keys, rowId, message, inlineObj, afterCreateHn ) {
		var inlineObj = inlineObj || this.inlineObjectParams?.getInlineObject(),
			row = inlineObj?.getRowById( rowId ),
			hostPage = this,
			params = {			
				tName: inlineObj?.tName ?? this.tName,
				hostPage: this,
				pageType: Runner.pages.constants.PAGE_EDIT, 
				keys: keys,
				keyFields: this.keyFields || row?.keyFields,
				modal: true,
				dashGridBased: inlineObj?.dashGridBased,
				baseParams: {
					parId: inlineObj?.id,
					rowId: rowId,
					table: inlineObj?.tName ?? this.tName,
					editType: Runner.pages.constants.EDIT_POPUP,
					spreadsheetGrid: inlineObj?.spreadsheet ? 1 : 0,
					hostPageName: ( inlineObj?.pageObj || hostPage ).pageName,
				},
				inlineObjectParams: {
					getInlineObject: function() {
						return inlineObj;
					},
					rowId: rowId
				},
				afterSave: {
					fn: function( respObj, formObj, fieldControls, editPageObj ) {
						if ( respObj.success ) {
							if ( row ) {
								this.afterSubmit( row, respObj, editPageObj );
							}
							if( hostPage?.pageType == 'calendar' ) {
								//	update the event
								hostPage.updateEditedEvent( respObj.oldKeys, respObj.event );
							}
							if( hostPage?.pageType == 'gantt' ) {
								//	update the event
								hostPage.updateEditedTask( respObj.oldKeys, respObj.task );
							}
							return true;
						}
						
						if ( respObj.lockMessage ) {
							$('.rnr-locking').html( respObj.lockMessage ).css('display', 'block');
							return false;
						}
						
						if ( respObj.hideCaptcha ) {
							$('.captcha_block').remove();
						}				

						editPageObj.displayHalfPreparedMessage( respObj.message || "", true);
						editPageObj.showElement("message");
						
						editPageObj.bsWin 
							&& editPageObj.bsWin.animate( {scrollTop: 0} );
						return false;			
					},
					scope: inlineObj
				}
			};
			
		params.baseParams = Runner.apply( params.baseParams, this.getStateParams() );	
				
		if ( typeof afterCreateHn === "function" ) {
			params.afterCreateHn = afterCreateHn;
		} else {							
			params.afterCreateHn = function() {				
				var reloadParams;
			
				if ( !this.controlsMap ) {				
					// the session was expired; show the message in the old window
					if ( this.message ) {
						hostPage.displayHalfPreparedMessage( this.message );
						hostPage.showElement("message");						
					}
					
					hostPage.initLoginLink();
					hostPage.enablePageButtons();
					
					this.close();						
					return;
				}					

				if ( message ) {
					this.displayHalfPreparedMessage( message );
					this.showElement( "message" );
				}
				
				// see #17031, #16468
				this.resizeAndMoveWindow( hostPage, hostPage.pageType === Runner.pages.constants.PAGE_ADD );
				this.adjustHeightAndWidth();
				
				hostPage.keepLocking = true;	
				hostPage.close();
			}		
		};
			
		if ( inlineObj?.dashGridBased && inlineObj?.pageObj ) { 
			params.baseParams.dashTName = inlineObj.pageObj.dashboard.tName;
			params.baseParams.dashPage = inlineObj.pageObj.dashboard.pageName;
			params.baseParams.dashelement = inlineObj.pageObj.dashElement;
		}
	
		return params;
	},

	/**
	 * Open the corresponding list page
	 */
	backButtonHref: function( pageName ) {
		window.location.href = this.backToListUrl( pageName );
	},
	
	/**
	 * The change page's data handler
	 */
	changePageHn: function() {
		Runner.pages.EditPage.superclass.changePageHn.apply(this, arguments);
		if ( this.isPageModified() ) {
			this.disablePageButtons( );
		}
	},
	
	/**
	 * Init the page's locking object
	 */
	initLocking: function() {
		if ( !this.useLocking ) {
			return;
		}

		var pageObj = this;

		this.locking = new Runner.Locking({
			tName: this.tName,
			pageId: this.id,
			hostObj: this,
		});

		//	show it only now to avoid page jumping
		$('.rnr-locking:not([data-locked])').show();
		
		$('.unlock', '.rnr-locking').on('click', function(e) {	
			pageObj.locking.unlockAdmin( pageObj.sKeys, 'no' );
			return false;
		});
		
		$('.edit', '.rnr-locking').on('click', function(e) {	
			pageObj.locking.unlockAdmin( pageObj.sKeys, 'yes' );
			return false;
		});		

		this.on("afterClose", function(e) {
			pageObj.locking.unlockRecord( pageObj.sKeys, '' );
		}, this);
		
		if ( this.pageData.lockedByOther ) {
			this.disableLockedReckordPage();
		} else {
			this.locking.startLocking( this.id, this.sKeys );
		}
	},
	
	/**
	 *
	 */
	disableLockedReckordPage: function( rowId, message ) {
		Runner.addDisabledClass( this.saveButton );
		
		var ctrl = Runner.controls.ControlStorage.byId( this.id );
		ctrl.forEach( function( ctrl ) {
			ctrl && ctrl.setDisabled();
		});		
	},
	
	/**
	 *
	 */
	enableLockedReckordPage: function() {
		Runner.delDisabledClass( this.saveButton );
		
		var ctrl = Runner.controls.ControlStorage.byId( this.id );
		ctrl.forEach( function( ctrl ) {
			ctrl && ctrl.setEnabled();
		});		
	},
	
	/**
	 * Initialize details list page
	 */	
	initDetails: function() {
		if ( !this.isShowDetails || !this.dpTablesParams ) {
			return;
		}	
		
		var dtName, dParams, i;
		
		this.dpObjs = {};		
		
		for (i = 0; i < this.dpTablesParams.length; i++) {
			dtName = this.dpTablesParams[i].tName;
			
			dParams = {
				tName: dtName,			
				parId: this.id,
				masterTName: this.tName,
				id : this.dpTablesParams[i].id,
				pType: this.dpTablesParams[i].pType,
				masterPageObj: this,
				saveFailed: {
					fn: this.onDetailsSavingFailed,
					scope: this
				},
				detailsSaved: {
					fn: this.onDetailsSaved,
					scope: this
				},
				afterDeleteDetails: {
					fn: this.onDetailsDeleted,
					scope: this				
				},
				// a handler for a details row's form 'validationFailed' event
				validationFailed: $.proxy( function( formObj, fieldControls ) {
					this.enablePageButtonsAfterSaving();
				}, this )
			};
	
			if ( !this.dashboard ) {
				dParams.childRecNum = 0;
				dParams.useChildCount = this.openMode === Runner.pages.constants.OPENMODE_POPUP;
				dParams.controlsMap = this.controlsMap.dControlsMap[ dtName ];
				dParams.viewControlsMap = this.viewControlsMap && this.viewControlsMap.dViewControlsMap ? this.viewControlsMap.dViewControlsMap[ dtName ] : null;
				
				this.dpObjs[ dtName ] = new Runner.util.details.EditDP( dParams );
			} else {
				dParams.masterPageType = this.pageType;
				dParams.pageType = this.dpTablesParams[i].pType;
				dParams.masterKeys = this.detailsMasterKeys[ dtName ];
				dParams.dashTName = this.dashboard.tName;
				dParams.dashPage = this.dashboard.pageName;
				dParams.dashElement = this.dashElement;
				
				this.dpObjs[ dtName ] = new Runner.util.details.ViewEditDashDP( dParams );
			}
	
			this.dpObjs[ dtName ].init();
		}		
	},
	
	/**
	 * Update relative dash elements
	 */
	updateRelativeDashElements: function() {
		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP ) {
			this.updateRelativeDashElements = Runner.emptyFn;
			return;
		}
		// page is eidt popup for dash grid record. 
		// popup's details preview was edited by adding/deleting records 
		var inlineObj = this.inlineObjectParams.getInlineObject();
		if ( inlineObj.dashGridBased && inlineObj.pageObj && inlineObj.pageObj instanceof Runner.pages.ListPageDash ) {
			// update grid dependent eg sr edit with the same details preview
			inlineObj.pageObj.sendMoveAction( inlineObj.pageObj.updContext );
			return;
		}

		this.updateRelativeDashElements = Runner.emptyFn;		
	},
	
	/**
	 * The 'detailsSaved' event's handler
	 * @param {object} detObj
	 * @param {array} allVals
	 * @param {array} fields
	 * @param {array} allKeys
	 * @param {array} allRowIds
	 */
	onDetailsSaved: function( detObj, allVals, fields, allKeys, allRowIds ) {
		if ( !this.form || !Runner.isDisabledButton( this.saveButton ) ) {
			// the details submit wasn't initiated by the save button of the main record
			this.updateRelativeDashElements();
			return;
		}
		
		var detailsSaved = true,
			tName;
		
		for (tName in this.dpObjs) {
			var dp = this.dpObjs[ tName ];
			if ( dp.submitMade !== undefined 
				&& !dp.detailsPageObj.spreadsheet
				&& ( dp.detailsPageObj.inlineAdd || dp.detailsPageObj.inlineEdit )
				&& ( !dp.submitMade || !dp.submitSucceded ) ) {
				detailsSaved = false;
			} else {
				$("#_" + this.dpObjs[tName].id).parent().remove();
			}
		}
		
		if ( !detailsSaved || this.formAlreadySubmitted ) {
			return;
		}
		
		this.formAlreadySubmitted = true;
		Runner.pages.EditPage.superclass.saveHn.call(this);	
	},

	/**
	 * The detail 'saveFailed' event's handler
	 * @param {object} detObj	 
	 * @param {object} respObj	 
	 * @param {object} formObj	 
	 */
	onDetailsSavingFailed: function( detObj, respObj, formObj ) {
		var messArr = [],
			pageObj = this,
			tName, msg, txt, dpId;
			
		for ( tName in this.dpObjs ) {
			var preview = this.dpObjs[ tName ];
			if ( preview.detailsPageObj && preview.detailsPageObj.spreadsheet ) {
				continue;
			}
			
			if ( preview.submitMade && !preview.submitSucceded ) {
				msg = Runner.lang.constants.DETAIL_NOT_SAVED.replace('%s', "<b>" + tName + "</b>"); 
				
				txt = "<div id='_" + preview.id + "' class='message mes_not'> " + msg + " <br>"
						+ "<a class='goToDetails' href='#' data-dpid='" + preview.id + "' >" 
							+ Runner.lang.constants.DETAIL_GOTO + " " + tName + "</a>"
						+ "</div>";
				
				dpId = dpId || preview.id;
				
				messArr.push( txt );
			} else if ( !preview.submitMade ) {
				return;
			}
		}
		
		this.enablePageButtonsAfterSaving(); 
		
		if ( dpId === undefined ) {
			return;
		}
		
		// scroll to details on message click
		this.findItemType( this.messageItemType() ).on( "click", ".goToDetails", function(e) {
			pageObj.scrollToDetails( $(this).data("dpid") );
			return false;
		})
		
		this.showError( messArr, true );
		this.scrollToDetails( dpId );
	},

	/**
	 * The detail 'afterDeleteDetails' event's handler
	 */
	onDetailsDeleted: function() {
		this.updateRelativeDashElements();
	},
	
	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object 
	 * reference. Add the form's 'validationFailed', 'successSubmit' events 
	 * handlers basing on the page's type and settings
	 */
	getForm: function() {
		if ( this.form ) {
			return;
		}
		
		Runner.pages.EditPage.superclass.getForm.call( this );
		
		// the following event could be fired if the form has the falsy 'standardSubmit' property
		this.form.on('successSubmit', function( respObj, basicForm, fieldControls ) {
			var pageParams,
				evRes = this.fireEvent("afterSave", respObj, basicForm, fieldControls, this);
				
			this.setFirstFocus();
			if ( evRes === false ) {
				// the record wasn't saved
				this.initLoginLink();
				this.enablePageButtonsAfterSaving();

				this.onCaptchaFailed( respObj.wrongCaptchaFieldName );
				this.adjustHeightAndWidth();
				
				return;
			}
			
			this.runAfterEditAction( respObj );
		}, this);		
	},
	
	/**
	 * The record is successfully updated. 	
	 * Stay on the page, close it or open another page
	 * basing on eidt page settings
	 * @param {object} respObj
	 */	
	runAfterEditAction: function( respObj ) {
		var pageParams, dTableName;	
			
		switch( this.afterSaveAction ) {						
			case Runner.pages.constants.AE_TO_EDIT:
				pageParams = this.getOpenPageParams( respObj.keys, this.baseParams.rowId, respObj.message );
				if ( this.pageName ) {
					pageParams.baseParams["page"] = this.pageName;
				}
				Runner.pages.PageManager.openPage( pageParams );				
				break;
				
			case Runner.pages.constants.AE_TO_VIEW:
				pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( this, respObj.keys, this.baseParams.rowId );
				Runner.pages.PageManager.openPage( pageParams );				
				break;
			
			case Runner.pages.constants.AE_TO_NEXT_EDIT:	
				pageParams = this.getOpenPageParams( this.nextKeys, this.baseParams.rowId + 1, respObj.message );
				Runner.pages.PageManager.openPage( pageParams );
				break;
				
			case Runner.pages.constants.AE_TO_PREV_EDIT:			
				pageParams = this.getOpenPageParams( this.prevKeys, this.baseParams.rowId - 1, respObj.message );
				Runner.pages.PageManager.openPage( pageParams );			
				break;
			
			case Runner.pages.constants.AE_TO_DETAIL_LIST:
				dTableName = Runner.pages.PageSettings.getTableData(this.tName, "afterEditActionDetTable");
				
				var keyParams = [];
				for ( dKey in this.detailsMasterKeys[ dTableName ] ) {
					if ( this.detailsMasterKeys[ dTableName ].hasOwnProperty( dKey ) ) {
						keyParams.push( dKey + "=" + encodeURIComponent( this.detailsMasterKeys[ dTableName ][ dKey ] ) );
					}
				}				
				
				window.location.href = Runner.getPageUrl( dTableName, Runner.pages.constants.PAGE_LIST )
					+ "?" + keyParams.join("&") + "&mastertable=" + encodeURIComponent( this.tName );				
				break;
				
			case Runner.pages.constants.AE_TO_LIST:	
			default:
				this.close();
		}	
	},
	
	/**
	 * The page's 'save data' handler
	 */
	saveHn: function() {
		if ( !this.isShowDetails ) {
			Runner.pages.EditPage.superclass.saveHn.call(this);
			return;
		}

		this.getForm();
		
		// validate master and if ok save details		
		if ( !this.form.validate() ) {
			return;
		}
	
		this.formAlreadySubmitted = false;

		var saveDpInlines = false,
			tName, sDp;

		for ( tName in this.dpObjs ) {
			var preview = this.dpObjs[ tName ];
			
			if ( preview.detailsPageObj && !preview.detailsPageObj.spreadsheet ) {
				sDp = this.dpObjs[ tName ].saveDetails();
				saveDpInlines = saveDpInlines || sDp;
			}
		}
		
		if ( !saveDpInlines ) {
			Runner.pages.EditPage.superclass.saveHn.call(this);
		}	
	},
	
	/**
	 *
	 */
	setRecountFlagForPopup: function() {
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.needToRecountChildAfterPopup = true;
		}
	},

	/**
	* Get the keys URL params string
	* @return {string}
	*/	
	getKeysUrlParams: function() {
		var keysString = "", i;
		
		for (i = 0; i < this.keys.length; i++) {
			this.baseParams["editid" + (i + 1)] = this.keys[i];
			keysString += "editid" + (i + 1) + "=" + this.keys[i] + '&';
		}
		return keysString;
	},
	
	/**
	 * Recalculate details in the following setup:
	 * 1. Edit page is opened in popup
	 * 2. The table has details tables on both List and Edit pages
	 * 3. Changes were made to the details list on the Edit page
	 * 4. The main edited record itself was not saved, window was simply closed instead.
	 */
	initDetailsRecalculation: function() {
		if ( this.openMode !== Runner.pages.constants.OPENMODE_POPUP || !this.baseParams.parId ) {
			return;
		}
		
		var parentPage = Runner.pages.PageManager.getById( this.baseParams.parId );
		
		if ( !parentPage ) {
			return;
		}
		// we need to recount children if popup closed by cross-button (didn't saved) and details count changed 
		this.on('afterClose', function() { 
			var tName, masterKeys;
			
			if ( !pageObj.needToRecountChildAfterPopup ) {
				return;
			}
			for (tName in parentPage.dpObjs) {
				masterKeys = parentPage.dpObjs[ tName ].getRowById( pageObj.baseParams.rowId ).masterKeys;
				parentPage.dpObjs[ tName ].updateChildRecNum( masterKeys, pageObj.baseParams.rowId );
			}
		});		
		
		if ( parentPage.pageType == Runner.pages.constants.PAGE_LIST ) {
			for (tName in parentPage.dpObjs) {
				parentPage.dpObjs[ dpObj ].closeDetails( parentPage.dpObjs[ dpObj ].getRowById( this.baseParams.rowId ) );
			}
		}
	},
	
	/**
	 * Disable some page's buttons along with the 'Save' button
	 */
	disablePageButtonsWhileSaving: function() {
		Runner.pages.EditPage.superclass.disablePageButtonsWhileSaving.call( this );
		
		Runner.addDisabledClass( this.closeButton );
		this.disablePageButtons();
	},

	/**
	 * Enable some page's buttons along with the 'Save' button
	 */
	enablePageButtonsAfterSaving: function() {
		Runner.pages.EditPage.superclass.enablePageButtonsAfterSaving.call( this );
		
		Runner.delDisabledClass( this.closeButton );
		if ( !this.isPageModified() ) {
			this.enablePageButtons();
		}
	},

	messageItemType: function() {
		return 'edit_message';
	},
});Runner.pages.EditPageFly = Runner.extend( Runner.pages.EditorPage, {
	
	pageType: Runner.pages.constants.PAGE_EDIT,
	
	editType: Runner.pages.constants.EDIT_ONTHEFLY,
	
	/**
	 * The main lookup control
	 * @type {object}
	 */
	lookupCtrl: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.EditPageFly.superclass.constructor.call(this, cfg);
		
		this.keys = cfg.keys || Runner.pages.PageSettings.getTableData(this.tName, 'keys');
		this.keyFields = cfg.keyFields || Runner.pages.PageSettings.getTableData(this.tName, 'keyFields');
		
		this.shortTName = Runner.pages.PageSettings.getShortTName(this.tName);	
		this.submitUrl += this.getKeysUrlParams();

		if ( this.proxy[ this.shortTName + "_recordUpdated" ] ) {
			this.recordUpdated = true;
		}			
	},	

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.EditPage.superclass.setBaseParams.call( this );
		this.baseParams['a'] = "edited";		
	},
	
	init: function() {
		Runner.pages.EditPageFly.superclass.init.call( this );
		this.fireEvent("afterInit", this, this.proxy, this.id);
	}, 
	
	/**
	 * Initialize the page's buttons
	 */
	initButtons: function() {
		Runner.pages.AddPage.superclass.initButtons.call( this );	
		
		var pageObj = this;	
		$("[id='cancelButton" + this.id + "']").on("click", function(e) {
			pageObj.close();
			return false;
		});
	},
	
	/**
	 * Assign the "form" property with the new 
	 * Runner.form.BasicForm object reference.
	 * Add the form's "successSubmit" event handler	 
	 */	
	getForm: function() {
		if ( this.form ) {
			return this.form;
		}
		
		Runner.pages.EditPageFly.superclass.getForm.call( this );
		
		this.form.on("successSubmit", function( respObj, basicForm, fieldControls ) {
			var evRes = this.fireEvent("afterSave", respObj, basicForm, fieldControls, this);
			
			if ( evRes !== false ) {
				this.close();
			}
		}, this);
	},
	
	messageItemType: function() {
		return 'edit_message';
	},
	
	/**
	* Get the keys URL params string
	* @return {string}
	*/	
	getKeysUrlParams: function() {
		var keysString = "", i;
		
		for (i = 0; i < this.keys.length; i++) {
			this.baseParams["editid" + (i + 1)] = this.keys[i];
			keysString += "editid" + (i + 1) + "=" + this.keys[i] + '&';
		}
		return keysString;
	},	
});Runner.pages.EditSelectedPage = Runner.extend( Runner.pages.EditPage, {
	/**
	 * ?
	 */
	defaultUncheckedBoxes: [],

	/**
	 *
	 */
	selection: null,
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.EditSelectedPage.superclass.constructor.call( this, cfg );
		
		this.selection = this.selection || Runner.pages.PageSettings.getTableData( this.tName, "selection" );
		this.baseParams[ "selection[]" ] = this.selection;
	},
	
	/**
	 * Initialize the page
	 */	
	init: function() {
		Runner.pages.EditSelectedPage.superclass.init.call( this );
		this.initCheckboxes();
	},
	
	/**
	 *
	 */
	updateSaveButton: function() {
		this.saveButton.prop( "disabled", !$("input[type=checkbox][id^=updsel_][data-field]:checked" ).length );
	},
	
	/**
	 *
	 */
	initCheckboxes: function() {
		var pageObj = this; 
		var $boxes = $("input[type=checkbox][id^=updsel_][data-field]", this.pageCont);
		
		$boxes.parent('label').on( "click", function( e ) {
			$("input[type=checkbox][id^=updsel_][data-field]", this ).trigger("click");
			
			// click should not affect the checkbox that is 'Edit as Checkbox'
			e.preventDefault();
		});

		//	disable initially unchecked fields
		$("input[type=checkbox][id^=updsel_][data-field]:not(:checked)").each( function( i, cb ) {
			var field = $(cb).data('field'),
				ctrl = Runner.getControl( pageObj.id, field );
				
			if ( ctrl ) {
				pageObj.defaultUncheckedBoxes.push( field );
				ctrl.makeReadonly();
			}
		});
		
		//	enable/disable control upon checkbox click
		$boxes
			.on("change", function( event ) {
				var $cb = $(this);
				var ctrl = Runner.getControl( pageObj.id, $cb.data('field') );
				
				if ( !ctrl ) {
					return;
				}
				
				if ( $cb.prop('checked') ) {
					ctrl.makeReadWrite();
				} else {
					ctrl.makeReadonly();
				}
				
				pageObj.updateSaveButton();
			}).on( "click", function( event ) {
				event.stopPropagation();
			});
			
		this.updateSaveButton();
	},
	
	/**
	 * reset button handler's functionality
	 */
	resetEditPage: function() {
		var pageObj = this;
		
		Runner.pages.EditSelectedPage.superclass.resetEditPage.call( this );
		
		$("input[type=checkbox][id^=updsel_][data-field]").each( function( i, cb ) {
			var $cb = $(cb),
				checked = $cb.prop("checked");
			
			if ( $.inArray($cb.data("field"), pageObj.defaultUncheckedBoxes) !== -1 ) {
				if ( checked ) {
					$cb.trigger("click");
				}
			} else if ( !checked ) {
				$cb.trigger("click");
			}
		});		
	},

	/**
	 * A stub
	 */
	initNavigationButtons: Runner.emptyFn,
	
	/**
	 * A stub
	 */
	updateNavigationVisibility: Runner.emptyFn,
	
	/**
	 * Get an object containing params for the Runner page's openPage method
	 * @param {string} parId
	 * @param {array} selection
	 * @param {array} rowIds
	 * @return {Object}
	 */	
	getOpenPageParams: function( parId, selection, rowIds, message ) {
		var hostListPage = Runner.pages.PageManager.getAt( this.tName, parId || this.baseParams.parId ),		
			inlineObj = this.inlineObjectParams ? this.inlineObjectParams.getInlineObject() : hostListPage.inlineEdit ,
			pageObj = this,
			params = {			
				tName: inlineObj.tName,
				openContainer: this.openContainer, //?
				pageType: Runner.pages.constants.PAGE_EDIT, 
				selection: selection,
				modal: true,
				pageMode: Runner.pages.constants.EDIT_SELECTED_POPUP,
				dashGridBased: inlineObj.dashGridBased,
				spreadsheetGrid: inlineObj.spreadsheet ? 1 : 0,
				baseParams: {
					table: escape( inlineObj.tName ),
					parId: hostListPage.id || this.baseParams.parId,
					editType: Runner.pages.constants.EDIT_POPUP,
					"selection[]": selection,
					rowIds: JSON.stringify( rowIds )
				},
				inlineObjectParams: {
					getInlineObject: function() {
						return inlineObj;
					}
				},
				afterSave: {
					fn: function( respObj, formObj, fieldControls, editPageObj ) {
						var inlineObj = this;
						
						editPageObj.displayHalfPreparedMessage( respObj.message || "", !respObj.success);
						editPageObj.showElement('message');

						if ( !respObj.success ) {
							editPageObj.displayHalfPreparedMessage( respObj.message || "", true);
							editPageObj.showElement('message');
							return false;
						}
						
						rowIds.forEach( function( rowId ) {
							var row = inlineObj.getRowById( rowId );
							
							if ( row && respObj[ rowId ] ) {
								inlineObj.afterSubmit( row, respObj[ rowId ], editPageObj );
							}
						});
						
						hostListPage && hostListPage.clearSelection();
						
						return true;
					},
					scope: inlineObj
				},					
				afterCreateHn: function() {				
					var reloadParams;
				
					if ( !this.controlsMap ) {				
						// the session was expired; show the message in the old window
						if ( this.message ) {
							pageObj.displayHalfPreparedMessage( this.message );
							pageObj.showElement('message');
						}
						
						pageObj.initLoginLink();
						pageObj.enablePageButtons();
						
						this.close();						
						return;
					}					

					if ( message ) {
						this.displayHalfPreparedMessage( message );
						this.showElement('message');
					}
					
					this.resizeAndMoveWindow( pageObj );			
					this.adjustHeightAndWidth();
					
					pageObj.keepLocking = true;	
					pageObj.close();
				}
			};	
			
		if ( inlineObj.dashGridBased && inlineObj.pageObj ) { 
			params.baseParams.dashTName = inlineObj.pageObj.dashboard.tName;
			params.baseParams.dashPage = inlineObj.pageObj.dashboard.pageName;
			params.baseParams.dashelement = inlineObj.pageObj.dashElement;
		}
		
		return params;
	},
	
	
	/**
	 * Init the page's locking object
	 */
	initLocking: Runner.emptyFn,
	
	/**
	 * Initialize details list page
	 */	
	initDetails: Runner.emptyFn,

	/**
	 * @return Array
	 */
	getFormControls: function() {
		var ctrls = this.getVisibleControls(),
			controlsNames = this.getCheckedFieldsNames(),
			captchaEditFieldName = Runner.pages.PageSettings.getTableData( this.tName, "captchaEditFieldName" );

		controlsNames.push( captchaEditFieldName );
		
		return ctrls.filter( function( ctrl, idx ) {
			return $.inArray( ctrl.fieldName, controlsNames ) !== -1;
		});
	},
	
	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object 
	 * reference. Add the form's 'validationFailed', 'successSubmit' events 
	 * handlers basing on the page's type and settings
	 */
	getForm: function() {
		if ( this.form ) {
			// update
			this.form.fieldControls = this.getFormControls();
			this.baseParams[ "fields[]" ] = this.getCheckedFieldsNames();
			return;
		}
		
		this.baseParams[ "fields[]" ] = this.getCheckedFieldsNames();	
		Runner.pages.EditSelectedPage.superclass.getForm.call( this );
	},
	
	/**
	 * @return {array}
	 */
	getCheckedFieldsNames: function() {
		var checkedFields = [];
		
		$("input[type=checkbox][id^=updsel_][data-field]:checked").each( function( i, cb ) {
			checkedFields.push( $(cb).data("field") );
		});
		
		return checkedFields;
	},
	
	/**
	 * The records are successfully updated. 
	 * Stay on the page, close it or open another page
	 * basing on eidt page settings
	 * @param {object} respObj
	 */	
	runAfterEditAction: function( respObj ) {			
		switch( this.afterSaveAction ) {						
			case Runner.pages.constants.AE_TO_EDIT:
				this.enablePageButtonsAfterSaving();
				
				var pageParams = this.getOpenPageParams( this.baseParams.parId, this.selection, 
						JSON.parse( this.baseParams.rowIds ), respObj.message );
				
				if ( this.pageName ) {
					pageParams.baseParams["page"] = this.pageName;
				}
				
				Runner.pages.PageManager.openPage( pageParams );			
				break;
				
			case Runner.pages.constants.AE_TO_LIST:	
			default:
				this.close();
		}	
	}
});/**
 * Constructor
 * @param {object} cfg
 */
Runner.SecurityPlugin = function( cfg ) {
	this.appId = cfg.appId;

	this.onsuccess = cfg.onsuccess;
	this.onfailure = cfg.onfailure;
}

Runner.SecurityPlugin.prototype.getPlugins = function( cfg ) {
	var plugins = [],
		pageData = cfg.pageObj.pageData;

	if ( pageData["isFB"] ) {
		cfg.appId = pageData["FBappId"];
		plugins.push( new Runner.SecurityPluginFB( cfg ) );
	}

	/*
	if ( pageData["isGoogleSignIn"] ) {
		cfg.appId = pageData['GoogleClientId'];
		plugins.push( new Runner.SecurityPluginGoogle( cfg ) );
	}
	*/
	return plugins;
};

/**
 * Constructor
 * @param {object} cfg
 */
Runner.SecurityPluginFB = function( cfg ) {
	Runner.SecurityPlugin.call( this, cfg );


	this.getButtonElem = function() {
		return $(".bs-facebook", cfg.pageObj.pageCont ).get(0);
	}
}

Runner.SecurityPluginFB.prototype = Object.create( Runner.SecurityPlugin.prototype );

/**
 * Initialize the Facebook login method
 */
Runner.SecurityPluginFB.prototype.init = function() {
	var pl = this;

	if ( typeof FB === "object" && typeof FB.XFBML === "object" && typeof FB.XFBML.parse === "function" ) {
		//reload continue_with button
		FB.XFBML.parse( this.getButtonElem() );
		return;
	}

	window.fbAsyncInit = function() {
		FB.init({
			appId: pl.appId,
			autoLogAppEvents: true,
			xfbml: true,
			version: "v2.9",
			//status: false,
			cookie: true // enable cookies to allow the server to access the session
		});

		FB.Event.subscribe("auth.statusChange", function( response ) {
			if ( response.authResponse && response.status == "connected" ) {
				pl.onsuccess({
					a: "pluginLogin",
					plugin: "fb",
					plugin_token: response.authResponse.signedRequest,
					//	facebook library requires this
					signed_request: response.authResponse.signedRequest
				});
			} else {
				pl.onfailure();
			}
		});

		FB.AppEvents.logPageView();
	};

	// add bf api
	( function(d, s, id) {
		var js, fjs = d.getElementsByTagName(s)[0];
		if ( d.getElementById( id ) ) {
			return;
		}
		js = d.createElement(s);
		js.id = id;
		js.src = "//connect.facebook.net/en_US/sdk.js";
		fjs.parentNode.insertBefore(js, fjs);
	}( document, 'script', 'facebook-jssdk' ) );
};

Runner.SecurityPluginFB.constructor = Runner.SecurityPlugin;


/**
 * Constructor
 * @param {object} cfg
 */
Runner.SecurityPluginGoogle = function( cfg ) {
	Runner.SecurityPlugin.call( this, cfg );

	this.getButtonId = function() {
		return 'signinButton' + cfg.pageObj.id;
	}
};

Runner.SecurityPluginGoogle.prototype = Object.create( Runner.SecurityPlugin.prototype );

/**
 * Load google api
 */
Runner.SecurityPluginGoogle.prototype.init = function() {
	var tag = document.createElement('script');
	var firstScriptTag = document.getElementsByTagName('script')[0];

	window.gAPILoaded = this.gAPILoaded.bind( this );

	tag.src = "https://apis.google.com/js/client:platform.js?onload=gAPILoaded";
	tag.setAttribute('defer', '');
	tag.setAttribute('async', '');

	firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
};

/**
 * Initialize sign in button
 */
Runner.SecurityPluginGoogle.prototype.gAPILoaded = function() {
	var pl = this,
		onSignIn, auth2;

	gapi.load('auth2', function() {
		auth2 = gapi.auth2.init({
			client_id: pl.appId,
		});

		var buttonId = pl.getButtonId(),
			$button = $("#" + buttonId );

		gapi.signin2.render( buttonId, {
				scope: 'profile email',
				width: $button.data('width'),
				height: $button.data('height'),
				longtitle: true,
				theme: 'dark',
				onsuccess: onSignIn,
				onfailure: pl.onfailure
			});
	});

	onSignIn = function( googleUser ) {
		// to prevent next auto sign in on api load
		auth2.disconnect();

		// send it with https request only
		pl.onsuccess({
			a: 'pluginLogin',
			plugin: 'go',
			plugin_token: googleUser.getAuthResponse().id_token,
		});
	};
};

Runner.SecurityPluginGoogle.constructor = Runner.SecurityPlugin;

Runner.pages.LoginPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * Is login page open in popup window
	 * @type {boolean}
	 */
	isPopUp: false,

	/**
	 * User name value
	 * @type {Object}
	 */
	userval: null,

	/**
	 * Password value
	 * @type {Object}
	 */
	passval: null,

	/**
	 * Captcha input element
	 * @type {Object}
	 */
	captcha: null,

	/**
	 * User name input element
	 * @type {Object}
	 */
	userName: null,

	/**
	 * Password input element
	 * @type {Object}
	 */
	password: null,

	/**
	 * The host page object for a popup login page
	 * @type {Object}
	 */
	hostPageObj: null,


	/**
	 * Remember password input element
	 * @type {Object}
	 */
	rememberPassword: null,
	rememberMachine: null,
	twoFactorAuth: false,

	twoFactorMethod: null,
	
	userCode: null,
	verifyCode: null,

	/**
	 * login form
	 * @type {object} Runner.form.BasicForm
	 */
	form: null,

	/**
	 * True for login link pages
	 * @boolean 
	 */
	restore: '',

	twoFactorInterval: null,
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.LoginPage.superclass.constructor.call( this, cfg );

		//this.twoFactorAuth = Runner.pages.PageSettings.getGlobalData("twoFactorAuth");
		this.isPopUp = this.openMode === Runner.pages.constants.OPENMODE_POPUP;
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		Runner.pages.LoginPage.superclass.init.call( this );
		
		this.initLoginControls();		
		this.initSecurityPlugins();
		
		this.initButtons();
		this.initUserCodeControls();

		this.on( 'windowSizeCorrected', this.setFirstFocus.bind( this ) );
		this.setFirstFocus();
		
		this.twoFactorMethod = this.pageData.twoFactorMethod;
		this.initTwoFactorTimer();

		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},
	
	/**
	 *
	 */
	initLoginControls: function() {
		var pageObj = this;
		var submitOnEnter = function(e) {
			if ( e.which == 13 ) {
				$('#submitLogin' + pageObj.id, pageObj.pageCont).trigger('click');
				return false;
			}			
		}		

		this.userName = $('input[name="username"]', this.pageCont).val( this.userval );
		this.password = $('input[name="password"]', this.pageCont).val( this.passval );
		this.userName.add( this.password ).on( "keydown", submitOnEnter );
		
		Runner.controls.ControlStorage.byId( this.id ).forEach( function( ctrl ) {
			ctrl.on( "keydown", submitOnEnter );
		});
		
		this.userCode = $('#userCode' + this.id).on( "keydown", function(e) {
			if ( e.which == 13 ) {
				$('#verifyCode' + pageObj.id, pageObj.pageCont).trigger('click');
				return false;
			}			
		});
		
		this.captcha = $('#value_captcha_' + this.id);
		
		this.rememberPassword = $('input[name="remember_password"]', this.pageCont);
		this.rememberMachine = $('input[name="remember_machine"]', this.pageCont);
	},
	

	/**
	 * Set focus to the first visible control
	 * Used when need set focus to the first invalid visible control
	 * @param {number} pageId (optional)	the page's id
	 * @param {array} ctrls (optional)		invalid controls
	 * @intellisense
	 */
	setFirstFocus: function( pageId, ctrls ) {
		if ( ctrls && ctrls.length ) {
			Runner.pages.LoginPage.superclass.setFirstFocus.call( this, pageId, ctrls );
			return;
		}
		
		var $activeControls = $(".form-control", this.pageCont ).filter( function( idx, el ) {
				return !$(el).prop("disabled") && $(el).is(":visible");
			});
			
		$activeControls.eq(0).trigger("focus");
	},	
	
	/**
	 * Check it
	 * Initialize third-party authentication plugins
	 */
	initSecurityPlugins: function() {
		var pageObj = this;

		var plugins = Runner.SecurityPlugin.prototype.getPlugins({
			pageObj: this,
			onsuccess: function( params ) {
				if ( pageObj.isPopUp ) {
					params.mode = "popup";
					params.id = pageObj.id;
					params.page = pageObj.pageName;
				}

				var pluginForm = new Runner.form.BasicForm({
					submitUrl: Runner.getPageUrl("login"),
					standardSubmit: !pageObj.isPopUp,
					method: 'POST',
					id: pageObj.pageId,
					baseParams: params,
					successSubmit: pageObj.pageReloadHn.bind( pageObj ),
				});

				pluginForm.submit();
				pluginForm.destructor();
				pluginForm = null;
			},
			onfailure: function( data ) {
				if ( data ) {
					pageObj.showMessage( data.details || data.error, true );
					return;
				}

				pageObj.showMessage( 'User cancelled login or did not fully authorize.', true );
			}
		});

		plugins.forEach( function( pl ) {
			pl.init();
		});
	},

	/**
	 * Initialize language selector elements
	 */
	initLanguageSelector: function() {
		$(".rnr-langlink", this.pageCont).on( "click", function(e) {
			window.location.href = Runner.getPageUrl("login") + "?" + "language=" + $(this).data("lang");
			return false;
		});
	},

	submitLoginForm: function( additionalParams ) {
		additionalParams = additionalParams || {};
		if ( !this.isPopUp ) {
			// simple submit
			this.submitSimpleLoginForm( additionalParams );
		} else {
			this.reload( Object.assign( this.getAJAXLikeLoginSubmitParams(), additionalParams ), true );
		}
	},
	
	/**
	 * Initialize the login page's buttons
	 */
	initButtons: function() {
		var pageObj = this;

		$( "#submitLogin" + this.id, this.pageCont ).on("click", function(e) {
			pageObj.submitLoginForm();
			return false;
		});

		this.initPopupButtons();
		
		$( "#resendActivation" + this.id, this.pageCont ).on( "click", function(e) {
			var params = {
				a: "resendActivation",
				restore: pageObj.restore ?  "sessionLogin" : "",
			}

			pageObj.reload( params );
			return false;
		});

		//	init 'login with provider' button
		this.layoutHelper.getItemIds( 'login_external' ).forEach( function( id ) {
			var $button = pageObj.getItemButton( id );
			$button.on( 'click', function() { 
				window.location.href = Runner.getPageUrl('login', '', 'a=providerlogin&provider=' + $button.data('provider'));
				return false;
			});
		})

		this.layoutHelper.getItemIds( 'login_provider' ).forEach( function( id ) {
			var $button = pageObj.getItemButton( id );
			$button.on( 'click', function() { 
				pageObj.submitLoginForm( {provider: $button.data('provider') } );
				return false;
			});
		})
	},

	initPopupButtons: function() {
		var pageObj = this;

		if ( !this.isPopUp ) {
			return;
		}

		$('a[name="RegisterPage"]', this.pageCont).on("click", function(e) {
			pageObj.registerPage( $(this).data("table") );
			return false;
		});

		$('a[name="ForgotPasswordPage"]', this.pageCont).on("click", function(e) {
			pageObj.forgotPasswordPage();
			return false;
		});

		$( "#continueButton", this.pageCont ).on("click", function(e) {
			if ( pageObj.hostPageObj ) {
				pageObj.hostPageObj.hideElement( 'message' );
				pageObj.hostPageObj.adjustPopWindowHeightByContent();
			}			
			
			pageObj.close();
			return false;
		});
	},

	/**
	 * Submit native form on simple login page
	 */
	submitSimpleLoginForm: function( additionalParams ) {
		// validate controls
		var controlsToValidate = Runner.controls.ControlStorage.byId( this.pageId ).slice(),
			invalidControls = this.getInvalidControls( controlsToValidate );

		if ( !invalidControls.length ) {
			var $form = $('#form' + this.id, this.pageCont);
			var $addedElems = $();
			Object.keys( additionalParams || {} ).forEach( function( key ) {
				$addedElems.add( 
					$('<input type="hidden">')
						.attr( 'name', key )
						.val( additionalParams[key] )
						.appendTo( $form ) 
				);
			} );
			$form.trigger("submit");
			$addedElems.remove();
		} else {
			this.setFirstFocus( this.pageId, invalidControls );
		}
	},
	
	/**
	 *
	 */
	reload: function( baseParams, withControls ) {
		if ( !withControls ) {
			Runner.pages.LoginPage.superclass.reload.call( this, baseParams ); 
			return;
		}
		
		// REDO with supreclass
		
		baseParam = baseParams || {};
		if ( !this.isDefaultPage ) {
			baseParams.page = baseParams.page || this.pageName;
		}
		
		baseParams = Object.assign( {}, this.getStateParams(), baseParams );
		
		// add params like parent
		this.form = this.form || new Runner.form.BasicForm({
			submitUrl: Runner.getPageUrl("login"),
			standardSubmit: false,
			isFileUpload: true,
			method: 'POST',
			id: this.pageId,
			successSubmit: this.pageReloadHn.bind( this ),
		});

		this.form.fieldControls = Runner.controls.ControlStorage.byId( this.pageId ).slice();
		
		this.form.baseParams = baseParams;
		this.form.submit();
	},

	/**
	 *
	 */
	pageReloadHn: function( respObj ) {	
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
	
		if ( this.pageData.redirectUrl ) {
			window.location.href = this.pageData.redirectUrl;
			return;
		}
	
		Runner.controls.ControlStorage.unregisterById( this.pageId );	
		// destroy smth else??
		
		this.controlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.id ];
		this.proxy = this.pageData.proxy;
		
		var $content = this.winObject.getContentNode(),
			$header = this.winObject.header(),
			$footer = this.winObject.footer(),
			$body = this.winObject.body();
			
		$body.height( $body.height() );		
		$body.html( respObj['html'] );
		
		var itemSelector = '[data-itemid]:not(li)',
			footerContent = $(respObj.footerCont).find( itemSelector ).addBack( itemSelector );
		
		$footer.html( footerContent );
		$body.height( $content.innerHeight() - $header.outerHeight() - $footer.outerHeight() - $body.outerHeight() + $body.height());
		
		// adjustPopWindowHeightByContent ?
		
		this.init();
	},	
	

	/**
	 * @return {object}
	 */
	getAJAXLikeLoginSubmitParams: function() {
		var params = {
			a: "Login",
			id: this.id,
			username: this.userName.val(),
			password: this.password.val(),
			value_captcha_1: this.captcha.val(),
			remember_password: this.rememberPassword.is(':checked') ? 1 : 0,
			mode: this.isPopUp ? "popup" : ""
		};
		
		if ( this.restore ) {
			params.restore = "sessionLogin";
		}
		
		return params;
	},

	/**
	 * @param {string} message
	 * @param {boolean} warning
	 */
	showMessage: function( message, warning ) {
		this.showElement( 'message' );

		this.findItemType( this.messageItemType() )
			.html( message )
			.toggleClass("alert-danger", warning)
			.toggleClass("alert-success", !warning);
	},

	/**
	 *
	 */
	initUserCodeControls: function() {
		var pageObj = this;

		this.verifyCode = $("#verifyCode" + this.id).off("click").on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				var params = {
					a: "verifyCode",
					'2fcode': $( "#userCode" + pageObj.id ).val(),
					method: pageObj.twoFactorMethod,
					remember_machine: pageObj.rememberMachine.is(':checked') ? 1 : 0,
					restore: pageObj.restore ?  "sessionLogin" : "",
				}

				pageObj.reload( params );
			}

			return false;
		});

		$("#resendCode" + this.id).off("click").on("click", function(e) {
			if ( pageObj.twoFactorInterval ) {
				clearInterval( pageObj.twoFactorInterval );
			}
			var params = {
				a: "resendCode",
				restore: pageObj.restore ?  "sessionLogin" : "",
			}

			pageObj.reload( params );
			return false;
		});
		
		$("[data-action=change2f]").off("click").on("click", function(e) {
			if ( pageObj.twoFactorInterval ) {
				clearInterval( pageObj.twoFactorInterval );
			}
			var params = {
				a: "change2f",
				method: $(this).attr('data-method'),
				restore: pageObj.restore ?  "sessionLogin" : "",
			}

			pageObj.reload( params );
			return false;
		});
	},
	
	/**
	 * Open a 'forgot password' page in a popup window
	 */
	forgotPasswordPage: function() {
		var pageObj = this,
			pageParams = {
				pageType: Runner.pages.constants.PAGE_REMIND,
				tName: Runner.pages.PageSettings.getGlobalData( "loginTName" ),
				requestUrl: Runner.getPageUrl("remind"),
				pageId: -1,
				modal: true,
				baseParams: {
					parId: this.id,
					mode: "popup"
				},
				afterCreateHn: function() {
					var forgotPageObj = this;

					if ( this.bsWin ) {
						this.bsWin.find("button.close").detach( "click" ).on( "click", function() {
							forgotPageObj.bsWin.modal('hide');
							forgotPageObj.openLoginPageOnPopUpClose( pageObj.restore, pageObj.hostPageObj );
						});
					}

					pageObj.close();
				}
			};

		if ( this.bsWin ) {
			this.bsWin.modal('hide');
		}

		Runner.pages.PageManager.openPage( pageParams );
	},

	/**
	 * Open a register page in a popup window
	 * @param {string} regTable
	 */
	registerPage: function( regTable ) {
		var pageObj = this,
			pageParams = {
				pageType: Runner.pages.constants.PAGE_REGISTER,
				tName: regTable,
				pageId: -1,
				modal: true,
				baseParams: {
					parId: this.id
				},
				afterCreateHn: function() {
					var regPageObj = this;

					pageObj.close();

					if ( this.bsWin ) {
						this.bsWin.find("button.close").detach( "click" ).on( "click", function() {
							regPageObj.bsWin.modal('hide');
							regPageObj.openLoginPageOnPopUpClose( pageObj.restore, pageObj.hostPageObj );
						});
					}
				}
			};

		if ( this.bsWin ) {
			this.bsWin.modal('hide');
		}

		Runner.pages.PageManager.openPage( pageParams );
	},

	messageItemType: function() {
		return 'login_message';
	},
	
	defaultReloadMethod: function() {
		return "POST";
	},
	
	/**
	 * Check if page is allowed to control user session
	 * @return {boolean}
	 */
	isPageToControlSession: function() {
		return false;
	},
	
	expireTimeFormat: function( seconds ) {
		const m = Math.floor( seconds / 60 );
		const s = seconds % 60;
		return m + ':' + ( s < 10 ? '0' : '' ) + s;	
	},

	initTwoFactorTimer: function() {
		if ( this.twoFactorInterval ) {
			clearInterval( this.twoFactorInterval );
			this.twoFactorInterval = null;
		}
		
		let remaining = this.pageData.twoFactorRemainingSeconds;
		const $timer = $('.twofactor-timer', this.pageCont);
		const expireText = $timer.attr('data-expires-text');

		if ( this.twoFactorMethod == Runner.pages.constants.TWOFACTOR_APP || !remaining || !$timer.length ) {
			return;
		}
		
		const updateTimer = () => {
			if ( remaining > 0 ) {
				const message = expireText.replace( '%seconds%', this.expireTimeFormat(remaining) );
				$timer.text( message );
				$timer.show();
				remaining--;
			} else {
				$timer.hide();
				this.disableTwoFactorInput();
				clearInterval( this.twoFactorInterval );
			}
		};

		updateTimer();
		this.twoFactorInterval = setInterval( updateTimer, 1000 );
	},

	disableTwoFactorInput: function() {
		this.userCode.prop('disabled', true);
		this.verifyCode.hide();
		
		this.rememberMachine.prop('disabled', true);
		this.rememberMachine.closest('[data-itemtype]').addClass('text-muted');
	},	
});Runner.pages.RemindPage = Runner.extend(Runner.pages.RunnerPage, {
	/**
	 *
	 */
	init: function() {
		Runner.pages.RemindPage.superclass.init.call(this);
		this.initButtons();
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
 
	/**
	 *
	 */
	initButtons: function() {
		var pageObj = this;

		$("[id=submitRemind]", this.pageCont).on("click", function(e) {
			var	pageParams = {
				mode: "popup", 
				onFly: 2, //remove #12116
				btnSubmit: 'Remind',
				username_email: $('input[name="username_email"]').val(),
				value_captcha_1: $('input[id="value_captcha_' + pageObj.id + '"]').val()
			};

			Runner.runnerAJAX( Runner.getPageUrl("remind"), pageParams, function( respObj ) {
				if ( respObj.message ) {
					pageObj.showElement('message');
					
					setTimeout(	function() {
						pageObj.adjustPopWindowHeightByContent()
					}, 300);
					
					pageObj.displayHalfPreparedMessage( respObj.message, true);
				}
				
				if ( respObj.html ) {
					if ( pageObj.bsWin ) {
						$(".modal-body", pageObj.bsWin ).html( respObj.html );
						$(".modal-footer", pageObj.bsWin ).html( respObj.footerCont );
					}
					
					$("[id=closeWindowRemind]")
						.on("click", function(e) {
							pageObj.openLoginPageOnPopUpClose();
							return false;
						})
						.show();
					
					$("[id=ProceedToLogin]").hide();
					$("[id=submitRemind]").hide();
				}

				if ( respObj.additionalJS ) { // for Custom button support
					$.each(respObj.additionalJS, function( jsFile, jsFileReq ) {
						Runner.util.ScriptLoader.addJS( [jsFile], jsFileReq );
					});

					Runner.util.ScriptLoader.onFilesLoaded( function() {						
						pageObj.pageData.buttons = respObj.pagesData[1]["buttons"];						
						pageObj.initCustomButtons();							
					}, pageObj );	
					Runner.util.ScriptLoader.load();
				}
			});
			
			return false;
		});	
	},
	
	/**
	 * @param {boolean} restore ( to restore session on add/edit)
	 * @param {object} hostPage
	 */
	openLoginPageOnPopUpClose: function( restore, hostPage ) {
		var	pageObj = this;
		
		this.openLoginPage( '', '', function() {
				pageObj.close();
			}, restore, hostPage );
	},

	/**
	 * Check if page is allowed to control user session
	 * @return {boolean}
	 */
	isPageToControlSession: function() {
		return false;
	},	
});Runner.pages.UserInfoPage = Runner.extend( Runner.pages.EditorPage, {
	/**
	 * @type {string}
	 */
	pageType: Runner.pages.constants.PAGE_USERINFO,

	twofMessageContainer: null,

	/**
	 * Current Two factor data
	 */
	twoFactorData: {},

	redirectUrl: null,

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.UserInfoPage.superclass.constructor.call( this, cfg );

		this.pageControlsMode = Runner.pages.constants.PAGE_EDIT;

		this.submitUrl = Runner.getPageUrl( this.pageType )
			+ "?page=" + this.pageName + "&submit=1&";
	},

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.UserInfoPage.superclass.setBaseParams.call( this );
		this.baseParams["a"] = "edited";
	},

	/**
	 * set up ajax-like submit for fields block
	 */
	isSubmitStandard: function() {
		return false;
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		this.twoFactorData = this.pageData[ 'twoFactorData' ];
		this.redirectUrl = this.pageData[ 'redirectUrl' ];
		this.twoFactorSettings = this.pageData[ 'twoFactorSettings' ];
		this.update2fControls();

		this.twofMessageContainer = $(".r-2fa-message");
		Runner.pages.UserInfoPage.superclass.init.call( this );
		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},

	/**
	 * Initialize page's buttons
	 */
	initButtons: function() {
		Runner.pages.UserInfoPage.superclass.initButtons.call( this );
		/* here reset and save have initialized */

		this.init2fDisable();
		this.init2fEnable();
		this.init2fPrefer();
		this.init2fContinue();

	},

	selected2fMethod: function() {
		return $("input[name=twof_method]:checked").val();
	},

	initTwofControls: function() {
		var pageObj = this;
		var onModified = function() {
			pageObj.twofMessageContainer.html( '' );
			pageObj.findItemType( 'twofactor_settings' ).removeClass('error');

			//	disable/enable when 2FA disabled/enabled
			$('.r-2fa-method').find('input, button').attr('disabled',
				pageObj.selected2fEnableValue()
					? null
					: ''
			);
		}
		
		var radio = $('[name=twof_method]');
		//	init radio box
		radio.on( "change", function() {
			$('.r-2fa-method').removeClass('active');
			$(this).closest( '.r-2fa-method' ).addClass('active');
			onModified();
		});
		$('input[name=twofactor_enable]' )
			.add( "#twof_email" + pageObj.id )
			.add( "#twof_phone" + pageObj.id )
			.on( "change keyup", onModified );

		if ( radio.filter(':checked').length === 0 ) {
			radio.first().prop('checked', true);
		}
		radio.filter(':checked').closest( '.r-2fa-method' ).addClass('active');
		radio.filter(':checked').triggerHandler('change');

		onModified();
	},

	ask2fAddress: function( method, callback ) {
		var $email;
		var pageObj = this;
		var $okButton = $();
		var fieldLabel = method == Runner.pages.constants.TWOFACTOR_EMAIL 
			? Runner.lang.constants.TWO_FACTOR_PARAM_EMAIL 
			: Runner.lang.constants.TWO_FACTOR_PARAM_PHONE;
		var fieldValue  = method == Runner.pages.constants.TWOFACTOR_EMAIL 
			? this.twoFactorData.email 
			: this.twoFactorData.phone;
		Runner.Dialog({
			ok: Runner.lang.constants.OK,
			cancel: Runner.lang.constants.CANCEL,
			title: Runner.lang.constants.TWOFACTOR_VERIFICATION,
			header: '<div id="confirm2f' + this.id + '"></div>',
			bottom: '<div class="text-danger" id="wrongEmail' + this.id + '"></div>',
			beforeCancel: function( win ) {
				//	revert everything
			},
			beforeOK: function( win, controls ) {
				var $userCode = controls[0];
				//	send the code
				$okButton.prop('disabled', true );
				pageObj.update2fData( method, $email.val(),
					function( respObj ) {
						win.close();
						callback( respObj );
					},
					function( respObj ) {
						$('#wrongEmail' + pageObj.id ).html( respObj.message );
						// set focus to user code control explicitly
						$email.trigger("focus");
						$okButton.prop('disabled', false );

						// back to $ collection
						//controls.reduce($.merge).val('');
					});
				return false;
			},
			afterCreate: function( win, controls, prefix ) {
				// click 'ok' dialog button  on user code enter
				$okButton = $('#' + prefix + '_button_ok', win.getContentNode() );
				$email = controls[0].on( "keydown", function(e) {
					if ( e.which == 13 && !$okButton.prop('disabled' ) ) {
						$okButton.trigger('click');
						return false;
					}
				});
			},
			width: '450px',
			height: '400px',
			fields: [{ name: 'email', label: fieldLabel, value: fieldValue }]
		});

	},
	
	/**
	 * Save 2factor settings
	 */
	 init2fEnable: function() {
		var pageObj = this;
		$('[data-action=twof_enable], [data-action=twof_update]').click( function() {
			var method = $(this).data('method');
			if( method == Runner.pages.constants.TWOFACTOR_EMAIL || method == Runner.pages.constants.TWOFACTOR_PHONE ) {
				pageObj.ask2fAddress( method, function( respObj ) {
					pageObj.process2fConfirm( method, respObj );
				});
			} else if( method == Runner.pages.constants.TWOFACTOR_APP ) {
				
				pageObj.update2fData( method, "",
					function( respObj ) {
						pageObj.process2fConfirm( method, respObj );
					}
				);
			}
		});
	},

	/**
	 * Save 2factor settings
	 */
	 init2fDisable: function() {
		var pageObj = this;
		$('[data-action=twof_disable]').click( function() {
			var method = $(this).data('method');
			var data = {
				a: 'disable2f',
				method: method,
			};
			Runner.runnerAJAX( Runner.getPageUrl("userinfo"), data, function( respObj ) {
				if ( respObj.status == 'saved' ) {
					pageObj.process2fSuccess( respObj );
				} 
			});
		});
	},

	init2fPrefer: function() {
		var pageObj = this;
		$('[data-action=twof_makepref]').click( function() {
			var method = $(this).data('method');
			var data = {
				a: 'prefer2f',
				method: method,
			};
			Runner.runnerAJAX( Runner.getPageUrl("userinfo"), data, function( respObj ) {
				if ( respObj.status == 'saved' ) {
					pageObj.process2fSuccess( respObj );
				} 
			});
		});
	},


	/**
	 * Process successful saving of 2f data
	 * Update relevant user settings controls
	 * Mark 2FA info as saved
	 */
	process2fSuccess: function( respObj ) {
		
		this.twoFactorData = respObj.twoFactorData;
		this.update2fControls();
	},

	update2fControls: function() {
		var pageObj = this;
		var enabledCount = Object.keys( this.twoFactorData.methods ).length;
		[ 0, 1 , 2 ].forEach( function( m ) {
			$elem = $(".r-2fa-method[data-method=" + m + "]");
			$elem.attr('data-enabled', pageObj.twoFactorData.methods[ m ] || null );
			$elem.attr('data-preferred', pageObj.twoFactorData.preferred == m || null );
			$elem.attr('data-required', (pageObj.twoFactorData.required && enabledCount == 1 ) || null );
		} ); 
		$(".r-2fa-method[data-method=0] input[type=text]").val( this.twoFactorData.email );
		$(".r-2fa-method[data-method=1] input[type=text]").val( this.twoFactorData.phone );

		this.toggleItemType( 'twofactor_continue', enabledCount > 0 );
		this.toggleItemType( 'twofactor_skip', enabledCount == 0 );

		//	update values in email and phone controls
		var ctlEmail = Runner.getControl( this.id, this.twoFactorSettings.emailField );
		if( ctlEmail && ctlEmail.getValue() != this.twoFactorData.email ) {
			ctlEmail.setValue( this.twoFactorData.email );
		}
		var ctlPhone = Runner.getControl( this.id, this.twoFactorSettings.phoneField );
		if( ctlPhone && ctlPhone.getValue() != this.twoFactorData.phone ) {
			ctlPhone.setValue( this.twoFactorData.phone );
		}

	},

	init2fContinue: function() {
		var pageObj = this;
		$('[data-action=twof_continue]').click( function() {
			location.href = pageObj.redirectUrl;
		});
		$('[data-action=twof_skip]').click( function() {
			var data = {
				a: 'skip2f',
			};
			Runner.runnerAJAX( Runner.getPageUrl("userinfo"), data, function( respObj ) {
				location.href = pageObj.redirectUrl;
			});
		});
	},


	/**
	 * @return Booolean
	 */
	selected2fEnableValue: function() {
		//	either control is hidden or checked
		return !$("input[name=twofactor_enable]:not(:checked)").length;
	},

	/**
	 * Process unsuccessful saving of 2f data
	 * Display error message
	 * Revert 2FA controls to previous values
	 */
	process2fError: function( respObj ) {
		$('<b></b>')
			.addClass('text-danger')
			.text( respObj.message )
			.appendTo( this.twofMessageContainer );
		this.findItemType( 'twofactor_settings' ).addClass('error');

	},

	/**
	 * Test new 2FA settings.
	 * Prompt user to enter delivered code
	 */
	process2fConfirm: function( method, respObj ) {
		var pageObj = this;

		//	show dialog
		Runner.Dialog({
			ok: Runner.lang.constants.OK,
			cancel: Runner.lang.constants.CANCEL,
			title: Runner.lang.constants.TWOFACTOR_VERIFICATION,
			header: '<div id="confirm2f' + this.id + '"></div>',
			bottom: '<div class="text-danger" id="wrong2f' + this.id + '"></div>',
			beforeCancel: function( win ) {
				//	revert everything
			},
			beforeOK: function( win, controls ) {
				var $userCode = controls[0];
				//	send the code
				pageObj.send2fCode( method, $userCode.val(),
					function( respObj ) {
						win.close();
						pageObj.process2fSuccess( respObj );
					},
					function( respObj ) {
						$('#wrong2f' + pageObj.id ).html( respObj.message );
						// set focus to user code control explicitly
						$userCode.trigger("focus");

						// back to $ collection
						controls.reduce($.merge).val('');
					});
				return false;
			},
			afterCreate: function( win, controls, prefix ) {
				// click 'ok' dialog button  on user code enter
				var $userCode = controls[0].on( "keydown", function(e) {
					if ( e.which == 13 ) {
						$('#' + prefix + '_button_ok', win.getContentNode() ).trigger('click');
						return false;
					}
				});
			},
			width: '450px',
			height: method === Runner.pages.constants.TWOFACTOR_APP ? '700px' : '400px',
			fields: [{ name: 'code', label: '' }]
		});

		var $area = $('#confirm2f' + this.id );
		if (method === Runner.pages.constants.TWOFACTOR_EMAIL) {
			$area.html((Runner.lang.constants.USERINFO_SENT_EMAIL).replace(/%email%/g, '<b>' + respObj.email + '</b>')
				+ '<br>' + Runner.lang.constants.USERINFO_ENTER_CODE);
		} else if (method === Runner.pages.constants.TWOFACTOR_PHONE ) {
			$area.html((Runner.lang.constants.USERINFO_SENT_TEXT).replace(/%phone%/g, '<b>' + respObj.phone + '</b>')
				+ '<br>' + Runner.lang.constants.USERINFO_ENTER_CODE);
		} else if (method === Runner.pages.constants.TWOFACTOR_APP ) {
			$area.html(Runner.lang.constants.USERINFO_INSTALL_APP1
				+ '<br>' + Runner.lang.constants.USERINFO_INSTALL_APP2 + '<br>'
				+ '<div class="r-2fa-qr"></div>'
				+ Runner.lang.constants.USERINFO_INSTALL_APP3 + '<br>'
				+ '<div class="r-2fa-secret">' + respObj.secret + '</div><br>'
				+ Runner.lang.constants.USERINFO_INSTALL_APP4);
		}

		$('.r-2fa-qr' ).qrcode({
			width: 128,
			height: 128,
			text: respObj.totpUrl
		});
	},

	update2fData: function( method, value, successHandler, errorHandler ) {
		var data = {
			a: 'enable2f',
			method: method,
			value: value
		};
		Runner.runnerAJAX( Runner.getPageUrl("userinfo"), data, function( respObj ) {
			if ( respObj.status == 'saved' || respObj.status == 'confirm' ) {
				successHandler( respObj );
			} else if ( respObj.status == 'wrong' || respObj.status == 'error' ) {
				errorHandler( respObj );
			}
		});
	},
	send2fCode: function( method, code, successHandler, errorHandler ) {
		var data = {
			a: 'confirm2f',
			code: code,
			method: method
		};
		Runner.runnerAJAX( Runner.getPageUrl("userinfo"), data, function( respObj ) {
			if ( respObj.status == 'saved' ) {
				successHandler( respObj );
			} else if ( respObj.status == 'wrong' || respObj.status == 'error' ) {
				errorHandler( respObj );
			}
		});

	},


	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object
	 * reference. Add the form's 'validationFailed', 'successSubmit' events
	 * handlers basing on the page's type and settings
	 */
	getForm: function() {
		if ( this.form ) {
			return;
		}
		var pageObj = this;
		Runner.pages.UserInfoPage.superclass.getForm.call( this );

		this.form.on('successSubmit', function( respObj, basicForm, fieldControls ) {
			if ( !respObj.success ) {
				if ( respObj.message ) {
					this.displayHalfPreparedMessage( respObj.message, true );
					this.showElement( 'message' );
				}

				this.initLoginLink();
				this.onCaptchaFailed( respObj.wrongCaptchaFieldName );

			} else {
				pageObj.reload();
				return;
				this.hideElement( 'message' );

				// update controls
				fieldControls.forEach( function( ctrl ) {
					// set server values without event triggering
					respObj.controlValues.hasOwnProperty( ctrl.fieldName ) &&
						ctrl.setValue( respObj.controlValues[ ctrl.fieldName ], false );
				});

				// fire user event
				this.fireEvent( "afterSave", respObj, basicForm, fieldControls, this );
			}

			this.enablePageButtonsAfterSaving();
		}, this );
	},


	messageItemType: function() {
		return 'fields_message';
	},

	//	prevent scrolling the page down to the first control
	setFirstFocus: function() {}
});
/**
 * Base abstract class for all pages with showing content, list, report etc.
 */
Runner.pages.DataPageWithSearch = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * Table short name
	 * @type {string}
	 */
	shortTName: "",
	
	/**
	 * Search Controller 
	 * @type {object} 
	 */
	searchController: null,
	
	/**
	 * Indicator showing if the 'Search saving' is enabled
	 * @type {boolean}
	 */
	searchSavingEnabled: false,
	
	/**
	 * The searchParamsLogger object reference
	 * @type {object} 
	 */
	searchParamsLogger: null,
	
	/**
	 * Tab index for the Details preview objects
	 * Each object must have uniqeu Tab ID
	 */
	nextDetailsTabId: 0,
	
	detailsOrder: [],

	constructor: function( cfg ) {
		Runner.pages.DataPageWithSearch.superclass.constructor.call( this, cfg );
		
		this.shortTName = Runner.pages.PageSettings.getShortTName( this.tName );
		Runner.setZindexMax( $(".sidebar").css("z-index") );		
	},
		
	init: function() {		
		Runner.pages.DataPageWithSearch.superclass.init.call( this );	
	},
	
	initForGrid: function() {
		Runner.pages.DataPageWithSearch.superclass.initForGrid.call( this );
		this.initPagination();
	},
	
	/**
	 * @return {object}
	 */
	getSearchControllerConfig: function() {
		var baseParams = {};
		if ( !this.isDefaultPage ) {
			baseParams.page = this.pageName;
		}
		
		baseParams = Runner.apply( baseParams, this.getStateParams() );

		return {
			id: this.pageId,
			tName: this.tName,
			pageType: this.pageType,
			baseParams: baseParams,
			searchTableName: this.tName,
			shortTName: this.shortTName,
			usedSrch: this.controlsMap.search.usedSrch,
			fNamesArr: this.controlsMap.search.allSearchFields,
			searchFieldsLabels: this.controlsMap.search.allSearchFieldsLabels,			
			panelSearchFields: this.controlsMap.search.panelSearchFields,
			isSearchPanelInflexible: this.controlsMap.search.inflexSearchPanel,
			isSearchRequired: this.controlsMap.search.isSearchRequired,
			requiredSearchFields: this.controlsMap.search.requiredSearchFields,
			useSuggest: Runner.pages.PageSettings.getTableData(this.tName, "ajaxSuggest"),
			filterContolsData: this.controlsMap.filters? this.controlsMap.filters.controls : [], 
			searchPanelRunByUserApi: this.controlsMap.search.searchPanelRunByUserApi
		};
	},
	
	/**
	 * Ininitialize the page's search functionality
	 */
	initSearch: function() {
		if ( !this.controlsMap.search ) {
			return false;
		}
		
		this.searchController = new Runner.search.SearchController( this.getSearchControllerConfig() );	
		this.searchController.init( this.controlsMap.search.searchBlocks );
		
		this.searchSavingEnabled = Runner.pages.PageSettings.getTableData(this.tName, "searchSaving");		
		if ( !this.searchSavingEnabled ) {
			return;
		}
		
		
		//Create the SearchParamsLogger instance to provide this search saving functionality
		this.searchParamsLogger = new Runner.search.SearchParamsLogger({
				id: this.pageId,
				tName: this.tName,
				pageType: this.pageType,
				savedSearches: this.controlsMap.search.savedSearches || {},
			})
			.init( this.searchController );
	},
	

	getExtraPageReloadParams: Runner.emptyFn,
	
	/**
	 * REDO: use mouseenter - mouseleave events
	 * Initialize popup link to details pages
	 */
	initPopupLinks: function() {
		if ( typeof this.dpObjs === "undefined" ) {
			return;
		}
		
		var pageObj = this, 
			detTables = pageObj.pageData.detailTables,
			masterMouseOver = function() {
				var linkId = $(this).attr('id'),
					shortTName, 
					rowId, 
					masterKeys = {},
					page, tName, row, detTName;				

				rowId = $(this).parent().attr('data-record-id');
				shortTName = $(this).attr('data-short-details');
									
				for ( detTName in detTables ) {
					if ( Runner.pages.PageSettings.getShortTName( detTName ) == shortTName ) {
						page = detTables[ detTName ].pageType;
						tName = detTName;
						break;
					}
				}	

				if ( !tName ) {
					return;
				}
				
				$.each( pageObj.dpObjs[ tName ].rows, function( index, tRow ) {
					if (tRow.id == rowId) {
						row = tRow;
						return false;
					}
				});
				
				if ( !row) {
					return;
				}
				
				$.each( row.masterKeys, function( mKey, mKeyValue ) {
					masterKeys[ mKey ] = mKeyValue || "";
				});
				
				pageObj.dpObjs[ tName ].showPopup( this, pageObj.dpObjs[ tName ].getUrl( page ), pageObj.tName, masterKeys[tName] );
			};
			
		
		$.each( this.dpObjs, function( tName, tObj ) {			
			if ( detTables[ tName ].listShowType != Runner.pages.constants.DP_POPUP ) {
				return;
			}
			
			var $detailLink;
			$detailLink = $("[data-itemtype=grid_details_link][data-pageid=" + pageObj.id + "] [data-short-details=" + tObj.shortTName + "]" ); 
			
			if ( Runner.isDirRTL() ) {
				$detailLink.attr("dir", $("html").attr("dir") );
			}

			$detailLink
				.off('mouseenter')
				.off('mouseleave')
				.on('mouseenter', masterMouseOver )
				.on('mouseleave', tObj.hidePopup.bind( tObj ) ); 
		});
	},
	
	getDetailsOrders: function() {
		var self = this,
			firstRecordId,
			$previeItems;
		
		if ( !this.detailsOrder.length ) {
			firstRecordId = $("[data-itemtype=details_preview]:first", this.gridElem).attr("data-record-id");
			$previeItems = this.findItemType("details_preview", firstRecordId);
			if ( $previeItems.length ) {
				$previeItems.each(function(){
					var tables = $(this).attr("data-orders").split(",");
					self.detailsOrder = self.detailsOrder.concat(tables);	
				});
			}
		}

		return this.detailsOrder;
	},
	
	getDetailsTabId: function( tName ) {
		var shortTName = Runner.pages.PageSettings.getShortTName( tName ),
			detOrders = this.getDetailsOrders();

		if ( detOrders.indexOf( shortTName ) !== -1 ) {
			return detOrders.indexOf( shortTName );
		}

		return this.nextDetailsTabId++;
	},

	/**
	 * Initialize details previews for master list/report pages
	 */
	initDetails: function() {
		var	tName,
			detTables = this.pageData.detailTables,
			linksOnList = Runner.pages.constants.DL_SINGLE;

		if ( linksOnList == Runner.pages.constants.DL_NONE ) {
			return;
		}
			
		this.dpObjs = {};

		for (tName in detTables) {
			this.dpObjs[ tName ] = this.createDetailsObject( tName, detTables[tName], this.getDetailsTabId(tName) );
		}
	},
	
	/**
	 *
	 */
	createDetailsObject: function( table, dSettings, tabIdx, listShowType ) {
		var dpParams = {
			id : this.id,
			masterTName: this.tName,
			oldLayout: this.oldLayout,
			parId: this.id,
			linksType: Runner.pages.constants.DL_SINGLE,
			pageContext: this.pageCont
		}, dpObj;
		
		if ( typeof listShowType == 'undefined' ) {
			listShowType = dSettings.listShowType;
		}

		dpParams.tName = table;
		dpParams.pageType = dSettings.pageType;
		dpParams.hideChild = dSettings.hideChild;
		dpParams.dispChildCount = dSettings.dispChildCount;
		
		if ( this.controlsMap.gridRows ) {
			dpParams.rows = this.controlsMap.gridRows.slice(0);
		}
		
		if ( listShowType == Runner.pages.constants.DP_NONE ) {
			return new Runner.util.details.ListWithoutPreview( dpParams );
		}

		dpParams.proceedLink = dSettings.proceedLink;
		dpParams.previewPageId = dSettings.pageId;
		dpParams.detailsHrefAvailable = dSettings.detailsHrefAvailable;
		
		
		if ( listShowType == Runner.pages.constants.DP_POPUP ) {
			dpObj = new Runner.util.details.PDPopupDetails( dpParams );

			dpObj.init();
			return dpObj;
		} 

		dpParams.tabSettings = { 
			tabLabel: dSettings.title,
			tabIndex: tabIdx
		};
		

		dpParams.masterPageObj = this;
		dpParams.hideEmptyPreview = dSettings.hideEmptyPreview;
		dpObj = new Runner.util.details.NewListPreview( dpParams );

		dpObj.init();
		
		if ( typeof this.onBeforeShowDetails === "function" ) {
			dpObj.on('beforeShowDetails', this.onBeforeShowDetails, this);
		}
		
		if ( typeof this.onAfterShowDetails === "function" ) {			
			dpObj.on('afterShowDetails', this.onAfterShowDetails, this);
		}
		return dpObj;
	},
	
	/**
	 * The details object 'beforeShowDetails' event object
	 * @param {object} dpObj
	 * @param {object} row
	 */
	onBeforeShowDetails: function( dpObj, row) {
		var shownDPObj, tName;
			
		for (tName in this.dpObjs) {
			if ( this.dpObjs[ tName ].getRowByInd( row.rowInd ).isShown ) { 
				shownDPObj = this.dpObjs[ tName ];
				break;
			}
		}
		
		if ( !shownDPObj ) {
			dpObj.closeDetailsByInd( row.rowInd );
			return;
		} 	
	},
	
	
	/**
	 * Initialize the 'Records per page' select element
	 */
	initRecordBlock: function() {
		var submitUrl = Runner.pages.getUrl( this.tName, this.pageType );
		var pageObj = this;
		
		$("[id='recordspp" + this.id + "']").on("change", function(e) {
			pageObj.reload( {pagesize: this.options[ this.selectedIndex ].value});
		});
	},

	messageItemType: function() {
		return 'grid_message';
	},

});/**
 * Base abstract class for all pages with showing content, list, view etc.
 */
Runner.pages.ListPageCommon = Runner.extend( Runner.pages.DataPageWithSearch, {
	/**
	 * Grid element 
	 * Get with method findForm only after resize
	 * @type {object}
	 */
	gridElem: null,
	
	/**
	 * An indicator showing if to initialize
	 * the 'Scroll table data' functionality
	 * @type {boolean}
	 */
	isScrollGridBody: false,
	
	/**
	 * The number of records per row
	 * @type {number}
	 */
	recsPerRowList: false,
	
	/**
	 * The 'Srollable table data' height. 
	 * It could be updated by user 
	 * in the "afterPageReady" event
	 * @type {number}
	 */
	heightScrollGridBody: 0,
	
	
	constructor: function( cfg ) {
		Runner.pages.ListPageCommon.superclass.constructor.call( this, cfg );
		
		this.isScrollGridBody = this.isScrollingGrid();
		this.permis = Runner.pages.PageSettings.getTableData( this.tName, "permissions" );	
		this.recsPerRowList = Runner.pages.PageSettings.getTableData( this.tName, "recsPerRowList" );
	},
		
	init: function() {
		this.initGridElem();
		Runner.pages.ListPageCommon.superclass.init.call( this );	
		this.initResize();
		this.initSelectAll();
		this.listenHashTags();
		
		this.setUrlParams();
	},

	initSection508: function() {
		if ( Runner.pages.PageSettings.getGlobalData("s508") ) {
			this.s508Obj = new Runner.s508({
				pageId: this.id,
				gridObj: this.gridElem,
				pageURL: Runner.pages.getUrl( this.tName, this.pageType ),
				maxPages: Runner.pages.PageSettings.getTableData( this.tName, "maxPages" ),
				isUseInlineEdit: this.isUseInlineEdit && this.permis['edit']
			});
			this.s508Obj.init();
		}
	},	
	
	/**
	 *
	 */
	setUrlParams: Runner.emptyFn,
	
	/**
	 *
	 */
	listenHashTags: function() {
		if ( !this.ajaxBaseParams ) {
			return;
		}

		var hash = location.hash.replace('#', ''),
			pageObj = this;

		Runner.Hash.changeUrlListener( function( newUrl ) {		
			pageObj.preparePageForReloading();

			var ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType ) + newUrl;
			
			Runner.runnerAJAX( ajaxURL, pageObj.ajaxBaseParams, function( respObj ) {
				pageObj.pageReloadHn( respObj, true );
			});		
		});
			
		if ( !hash ) {
			return;
		}
		
		this.preparePageForReloading();

		Runner.runnerAJAX( Runner.pages.getUrl( this.tName, this.pageType ) + "?" + hash, this.ajaxBaseParams, function( respObj ) {
			pageObj.pageReloadHn( respObj )
		});
	},

	initForGrid: function() {
		Runner.pages.ListPageCommon.superclass.initForGrid.call( this );
		this.initHeaderCheckBox();
		this.initInline();
		this.initMaps();
		this.initFieldFilter();
	},
	
	initGridElem: function() {
		this.gridElem = this.findForm( 'grid' );
		this.initMoreText( $( this.gridElem ) );
	},
	
	/**
	 * Initialize the page's functionality that need initializing after tabs are loaded.
	 * If there are no tabs on the page it's invoked from the page's init method 
	 * otherwise from the initTabs method
	 * @intellisense
	 */
	initForTabs: function() {
		this.initControls();
		
		if ( !this.useResize ) {
			this.initViewControls();
		}
	
		this.initLookups();
	
	},
	
	/**
	 * Cover of inlineEdit object function
	 * If inlineEdit is turn off, then get recsId by another way
	 */	
	getRecsId: function() {
		if ( this.inlineEdit ) {
			return this.inlineEdit.getRecsId();
		}
		var recsId = {};
		$('.rnr-row:not(.gridRowAdd)', this.gridElem).not( this.inDetailPreviewContainer )
			.each( function(i, row) {
				var attrId = $(row).attr('id'),
					lastInd = attrId.lastIndexOf('w');
					
				recsId[++i] = parseInt( attrId.substr(lastInd + 1) );
			});	
		return recsId;
	},
	
	/**
	 * @return {object}
	 */
	getResizableColumnsData: function() {
		if ( !this.bsResizeInitialized ) {
			return null;
		}
		
		var data = {
			gridWidth: $( this.gridElem ).width(),
			thWidths: {}
		}
		
		$( "th", this.gridElem ).each( function( i, th ) {
			var $th = $(th),
				cellId = $th.data("cellid");
			
			if ( cellId ) {
				data.thWidths[ cellId ] = $th.width();
			}
		});
		
		return data;
	},
	
	/**
	 * POST params for Resize columns Logger
	 * @return {array}
	 */
	getResizeColumnsLoggerParams: function() {
		return {
			saveParam: Runner.pages.constants.CRESIZE_PARAMS_TYPE, 
			data: JSON.stringify(this.getResizableColumnsData())
		};
	},
	

	/**
	 * @return {mixed}
	 */
	getServerResizableColumnsData: function() {
		return this.pageData.resizableColumnsData;
	},

	
	/**
	 *
	 */
	initResize: function() {
		if ( !this.useResize ) {
			return;
		}
		
		if ( Runner.smallScreen ) {
			this.initForGrid();
			this.initViewControls();
			return;
		}		
		
		var pageObj = this,
			$grid = $( this.gridElem ),
			pageUrl = Runner.pages.getUrl( this.tName, this.pageType ),
			disabledColumns, resizableColumnsData, 
			delta = 0;
		

		$grid.colResizable({ 
			disable: true
		});
		
		resizableColumnsData = this.resizableColumnsData || this.getServerResizableColumnsData();
		
		$grid.css( 'width', 'auto' );
		$( "th", this.gridElem ).each( function( i, th ) {
			var $th = $(th),
				cellId = $th.data("cellid");
			if ( resizableColumnsData && resizableColumnsData.thWidths[ cellId ] ) {
				$th.width( resizableColumnsData.thWidths[ cellId ] );
				if ( parseInt( resizableColumnsData.thWidths[ cellId ], 10 ) > $th.width() ) {
					delta = delta + parseInt( resizableColumnsData.thWidths[ cellId ], 10 ) - $th.width();
				}
			}
		});			

		if ( resizableColumnsData ) {
			if ( $grid.width() < resizableColumnsData.gridWidth ) {
				$grid.width( resizableColumnsData.gridWidth );
				$grid.css( 'min-width', resizableColumnsData.gridWidth + 'px' );
			} else {
				$grid.width( $grid.width() + delta );
			}
		} else {
			$grid.css('width', $grid.width() + 'px' );
		}
		
		$grid.colResizable({ 
			resizeMode: 'overflow',
			//disabledColumns: disabledColumns,
			onResize: function() {
				if ( pageObj.recsPerRowList <= 1 ) { 
					$.post( pageUrl, pageObj.getResizeColumnsLoggerParams() );
				}
			}
		});
		
		this.bsResizeInitialized = true;
		
		this.initForGrid();	
		this.initViewControls();
	},

	/**
	 * TODO: avoid duplicating code
	 */
	onReoderColumnsStop: function() {				
		if ( !this.bsResizeInitialized ) {
			return;
		}
		
		var $grid = $( this.gridElem ),
			pageUrl = Runner.pages.getUrl( this.tName, this.pageType ),
		//	disabledColumns = [],
			pageObj = this;
		
		$grid.colResizable({ 
			disable: true
		});
		
		/*
		$( "th", this.gridElem ).each( function( i, th ) {
			if ( !$(th).data("field") ) {
				disabledColumns.push( i );
			}
		});	*/		
		
		$grid.colResizable({ 
			resizeMode: 'overflow',
			//disabledColumns: disabledColumns,
			onResize: function() {
				$.post( pageUrl, pageObj.getResizeColumnsLoggerParams() );
			}
		});
	},	
	
	/**
	 * Initialize the 'Choose all' checkboxes
	 */
	initHeaderCheckBox: function() {
		var pageObj = this,
			$checkBoxHeadCell = $( '.chooseAll' + this.id ).parents("[data-checkbox-cell]");
		
		if ( !$checkBoxHeadCell.length ) {
			$checkBoxHeadCell = $( '.chooseAll' + this.id ).parent();
		}
		
		$checkBoxHeadCell.off( "click" ).on( "click", function(e) {
			var $chooseAll = $('.chooseAll' + pageObj.id , this),
				checked = $chooseAll.prop("checked");
				
			if ( !$chooseAll.is( e.target ) ) {
				checked = !checked;				
			}
			
			//set checked/unchecked for all checkbox in grid rows
			$('input[type=checkbox][id^=check' + pageObj.id + '_]:visible')
				.prop( "checked", checked );

			//set checked/unchecked for all chooseAll checkboxes
			$('input[type=checkbox][id^=chooseAll_' + pageObj.id + ']')
				.prop( "checked", checked );

			pageObj.onAfterChangeCheckboxsState();
		});

		// init click on record checkbox
		$('input[type=checkbox][id^=check' + this.id + '_]').off( "click" ).on( "click", function(e) {
			pageObj.onAfterChangeCheckboxsState();
			
		});
		
		this.onAfterChangeCheckboxsState();
	},

	/**
	 * call after checked/unchecked checkboxes in grid
	 */
	onAfterChangeCheckboxsState: function() {		
		var pageObj = this,
			$selBoxes = this.getSelBoxes( this.id ),
			$printSelection = $("#printSelection" + this.id),
			$printPage = $("#printPage" + this.id),
			$labelPrintSelection = $printSelection.parent(),
			$expSelBtns = $("[id=export_selected" + this.id + "]");
			
		$('.chooseAll' + this.id)
			.prop("checked", $selBoxes.length && $selBoxes.length >= $(':not([data-hidden]) > input[type=checkbox][id^=check' + this.id + '_]').length );
			
					
		/* update buttons state */
		var $deleteButtons = $("[id=delete_selected" + this.id + "]");
		var $editButtons = $("[id=edit_selected" + this.id + "]")
			.add("[id=update_selected" + this.id + "]");

		var toggleButtons = function( $buttons, enable ) {
			$buttons.each( function() {
				if ( $(this).parent().is("li") ) {
					$(this).parent().toggleClass("disabled", !enable );
				} else {
					$(this).attr('disabled', enable ? null : "" );
				}

			})
		}

		toggleButtons( $editButtons, this.getOwnedSelBoxes( true, true ).length );
		toggleButtons( $deleteButtons, this.getOwnedSelBoxes( true, false ).length );

		// Disabled/Enabled export_selected buttonS on Page

		$expSelBtns.each( function() {				
			if ( $selBoxes.length ) {
				if ( $(this).parent().is("li") ) {
					$(this).parent().removeClass('disabled')
				} else {
					$(this).removeAttr('disabled');
				}
			} else {
				if ( $(this).parent().is("li") ) {
					$(this).parent().addClass('disabled')
				} else {
					$(this).attr('disabled', true);
				}
			}
		});
		

		// Disabled/Enabled print_selected button
		if ( $selBoxes.length ) {
			$printSelection
				.prop("disabled", false)
				.prop("checked", true);

			$labelPrintSelection.css("color", "inherit");
		} else {
			$printSelection
				.prop("checked", false)
				.prop("disabled", true);

			$printPage.prop("checked", true);
			$labelPrintSelection.css("color", "gray");
		}
	},
	
	/**
	 * Get checked checkboxes
	 * @return {jQuery object}
	 * @intellisense
	 */
	getSelBoxes: function( ) {		
		return 	$("input[type='checkbox'][id^='check" + this.id + "_']:checked");
	},	

	
	getSelectedRecordIds: function() {
		var ret = [];
		this.getSelBoxes().each( function() {
			ret.push( $( this ).closest('[data-record-id]').data('record-id') );
		});
		return ret;
	},
	/**
	 * Get selected record keys
	 * @return [ [key field values] ]
	 * @intellisense
	 */
	getSelectedRecordKeys: function( ) {		
		var recordKeys = [];
		var rows = this.getInlineEditObj().rows;
		var pageObj = this;
		
		this.getSelectedRecordIds().forEach( function( recordId ) { 
			var row = pageObj.getInlineRow( recordId );
			if ( row ) {
				recordKeys.push( row.keys.slice() );
			}
		});
		return recordKeys;
	},	

	/**
	 * Returns array of AjaxRow objects
	 */
	getSelectedRecords: function() {
		var recordObjects = [];
		var pageObj = this;
		var rows = this.getInlineEditObj().rows;
		
		this.getSelectedRecordIds().forEach( function( recordId ) { 
			var row = pageObj.getInlineRow( recordId );
			if ( row ) {
				recordObjects.push( new Runner.AjaxRow( pageObj, row ) );
			}
		});
		return recordObjects;
	},

	/**
	 * Returns corresponding AjaxRow object or null
	 * @param {*} id 
	 */
	getRowObject: function( id ) {
		var row = this.getInlineEditObj().rows.find( function( r ) { 
			return r.id == id;
		});
		return row 
			? new Runner.AjaxRow( this, row )
			: null;
	},


	/**
	 * Init the 'Select/Unselect all' button or checkbox
	 */
	initSelectAll: function() {
		var pageObj = this,
			selectAll = $('#select_all' + this.id),
			useCaption;
			
		if ( !selectAll.length ) {
			return false;
		}
		
		useCaption = Runner.pages.PageSettings.getTableData(this.tName, "listGridLayout") !== Runner.pages.constants.HORIZONTAL_LAYOUT;
		selectAll[0].checkAllStatus = false;
		
		selectAll.on("click", function(e) {
			this.checkAllStatus = !this.checkAllStatus;
			$('input[type=checkbox][id^=check' + pageObj.id + '_]').prop("checked", this.checkAllStatus);
			
			if ( useCaption ) {
				$(this).html( this.checkAllStatus ? Runner.lang.constants.UNSELECT_ALL : Runner.lang.constants.SELECT_ALL );
			}

			pageObj.onAfterChangeCheckboxsState();
			
			return false;
		});
	},

	/**
	 * returns AjaxRow objects for all records
	 */
	getAllRecords: function() {
		var pageObj = this,
			inlineObj = this.getInlineEditObj();
			
		if ( !inlineObj ) {
			return [];
		}
		
		return inlineObj.rows.map( function( row ) {
			return new Runner.AjaxRow( pageObj, row );
		});
	},
	
	/**
	 * Init the Google maps functionality using the Runner.GmapLoader instance
	 */
	initMaps: function() {
		if ( !this.hasMaps() ) {
			return;
		}
		
		Runner.globalGmapLoader.onLoad( function() {		
			var cfg = this.controlsMap.gMaps;
			
			if ( this.mapManager ) {
				this.mapManager.reinit( cfg.mapsData, cfg.fieldMapsIds );
				return;
			}
			
			this.mapManager = new Runner.controls.MapManager( cfg ); 
			this.mapManager.init();
			
			if ( this.inlineEdit ) {				
				this.inlineEdit.on('cancel', this.mapManager.updateMapsOnEditCancel, this.mapManager);
				this.inlineEdit.on('afterSubmit', this.mapManager.updateMapsOnAfterEditSubmit, this.mapManager);
			}
			
			if ( this.inlineAdd ) {
				this.inlineAdd.on('afterSubmit', this.mapManager.updateMapsOnAfterAddSubmit, this.mapManager);
			}
			
			if ( this.isPageInTab() ) {
				Runner.pages.PageManager.onTabShown( this, this.mapManager.updateLittleMaps.bind( this.mapManager ) );
			}
		}, this );
	},
	
	/**
	 * @param {object} row
	 */
	initNewRowButtons: function( row ) {
		var i, newId, $buttonElem, button;	
		
		for (i = 0; i < this.buttonNames.length; i++) {
			$buttonElem = $("[data-event=" + this.buttonNames[i] + ']', row.row);
			if ( !$buttonElem.length ) {
				continue;
			}
			newId = this.buttonNames[i] + "_" + Runner.genId();
			$buttonElem.attr('data-event', newId);
			// create object
			button = new Runner.form.Button({
				id: newId,
				btnName: this.buttonNames[i]
			});
			// init
			button.init( { args: [ this, this.proxy, this.id, true, row ] } );	
		}	
	},	
	
	/**
	 * @param {object} row
	 */
	afterInitInlineAdd: function( row ) {
		this.initNewRowButtons( row );
	},

	/**
	 * Remove all excessive containers
	 * Disable the page's grid and show the 'loading' message
	 * @param {DOM element} gridElem
	 */
	preparePageForReloading: function( gridElem ) {
		if ( this.inlineEdit || this.inlineAdd ) {
			( this.inlineEdit || this.inlineAdd ).cancelAll();
		}
		if ( Runner.pages.PageSettings.getTableData(this.tName, "displayLoading") ) {
			this.showLoadingBox();
		}
	},
	
	/**
	 * Set 'afterInit' event's handler settings the scrollable grid
	 * @param {object} pageObj
	 * @param {object} proxy
	 * @param {number} pageid
	 */
	afterInit: function( pageObj, proxy, pageid ) {
		this.heightScrollGridBody = proxy['gridHeight'];
		this.initScrollGridBody();
	},
	
	/**
	 * Is grid scrolling or not
	 * @return {boolean}
	 */	
	isScrollingGrid: function() {
		if ( Runner.pages.PageSettings.getTableData(this.tName, "isUseResize") ) {
			return false;
		}
		
		return Runner.pages.PageSettings.getTableData(this.tName, "scrollGridBody");
	},
	
	getBelowGridHeight: function() {			
		var $belowGrid = this.layoutHelper.getFormElement("below-grid");
		if ( !$belowGrid.length ) {
			return 0;
		}	
		return $belowGrid.outerHeight( true ) + parseInt( $belowGrid.css("margin-top"), 10 ) + parseInt( $belowGrid.css("margin-bottom"), 10 );
	},
	
	/**
	 *
	 */
	initScrollGridBody: function() {
		if ( !this.isScrollGridBody || this.bsScrollBodyInited || $(window).width() < 768 ) {
			return;
		}

		var pageObj = this,
			maxExtraHeight = 80,
			scrollElementWidth = 16,
			viewPortHeight = $(window).height(),
			gridHeight = this.gridElem.height(),
			gridPosition = this.gridElem.position(),
			fullGridHeight = gridHeight + gridPosition.top + this.getBelowGridHeight(),
			gridWidth = this.gridElem.width(),
			scrollGridHeight = this.heightScrollGridBody,
			$thead = this.gridElem.children( 'thead'),
			$header = $thead.children(),
			$tbody = this.gridElem.children( 'tbody'),
			$tfoot = this.gridElem.children( 'tfoot'),
			theadHeight, bottomHeight, extraDocumentHeight;	

		if ( !$tbody.data("resizeHandlerSet") )	{
			$( window ).on("resize", function() {
				pageObj.adjustBSGridHeight();				
			});
			$tbody.data("resizeHandlerSet", true);
		}
			
		if ( fullGridHeight < viewPortHeight ) {
			// the all grid data is visible in the view port
			return;
		}			

		$('th', $thead).css({
			'border-bottom-width': '1px',
			'border-top-width': '1px',
			'border-top-style': 'solid',
			'border-top-color': this.gridElem.css('border-top-color'),
		});
		
		this.gridElem.css({
			position: 'relative',
			border: 'none'
		});
		
		// set cells' width values explicitly
		this.setEdgeRowsWidthStyles( $header, $(null), $('tfoot tr', this.gridElem) );
		
		theadHeight = $header.innerHeight() || 32;	
		//bottomHeight = $bottomRow.innerHeight();
		bottomHeight = 0;
	
		if ( !scrollGridHeight ) {
			var delta = 5;
			scrollGridHeight = viewPortHeight - gridPosition.top - this.getBelowGridHeight() - delta;
			
			extraDocumentHeight = $(document).height() - gridPosition.top - gridHeight;
			scrollGridHeight = extraDocumentHeight < maxExtraHeight ? scrollGridHeight - extraDocumentHeight : scrollGridHeight;
			
			scrollGridHeight = scrollGridHeight < 0 ? scrollGridHeight * (-1) : scrollGridHeight;
			if ( scrollGridHeight - bottomHeight - theadHeight < 32 ) {
				//set the scrollable grid height so It can show the scroll element correctly
				scrollGridHeight += theadHeight + 32;
			}
		}

		this.gridElem.width( gridWidth );

		$thead.css({
			position: "absolute",
			height: $thead.height() + 'px',
			top: 0 + 'px',
			'z-index': 1,
			// 'background-color': "#FFFFFF"
		})
		.addClass( "r-fixedhead");
		
		$tbody.css({
			height: scrollGridHeight + 'px',
			width: gridWidth + scrollElementWidth,
			position: "absolute",
			"overflow-y": "scroll",
			top: 0 + 'px',
			'padding-top': $thead.height() + 'px',
			'padding-bottom': $tfoot.height() + 'px',
			'overflow-x': 'hidden'
		});
			
		$tfoot.css({
			position: "absolute",
			height: $tfoot.height() + 'px',
			top: scrollGridHeight - $tfoot.height() + 'px',
			'z-index': 1,
			'background-color': "#FFFFFF"
		});
		
		this.gridElem.height( scrollGridHeight );
		this.gridElem.css("margin-bottom", 0);

		$tbody.children(":not(.gridRowAdd)").first().children().each(function(i, el) {
			var lastRecalc = Date.now();
			var recalcCount = 0;

			ResizeSensor(el, function() {
				//	temporary fix to avoid infinte recalculations
				//	will go away when completely refactored
				if ( Date.now() - lastRecalc < 500 ) {
					++recalcCount;
					if ( recalcCount > 3 ) {
						return;
					}
				} else {
					recalcCount = 0;
				}
				lastRecalc = Date.now();

				pageObj.recalculationBSGridSizeByCell(i, el);
			});
		});

		this.bsScrollBodyInited = true;	
	},
	
	/**
	 *
	 */
	adjustBSGridHeight: function() {
		if ( !this.isScrollGridBody ) {
			return;
		}
		
		if ( !this.bsScrollBodyInited ) {
			this.initScrollGridBody();
			return;
		}
		
		var viewPortHeight = $(window).height(),
			$tbody = $('tbody', this.gridElem).not( this.inGridCell ),
			tbodyOffset = $tbody.offset(),
			fullGridHeight = $tbody.height() + tbodyOffset.top + this.getBelowGridHeight(),
			$thead = $('thead', this.gridElem).not( this.inGridCell ),
			$tfoot = $('tfoot', this.gridElem).not( this.inGridCell ),
			theadHeight, bottomHeight, scrollGridHeight, contentHeight;

		if ( fullGridHeight < viewPortHeight ) {
			contentHeight = $tbody.get(0).scrollHeight;
		}
		
		var delta = 5;
		
		scrollGridHeight = viewPortHeight - tbodyOffset.top - this.getBelowGridHeight() - $thead.height() - $tfoot.height() - delta;
		if ( scrollGridHeight < 32 ) {
			scrollGridHeight = 32;
		}

		if ( contentHeight && contentHeight < scrollGridHeight + $thead.height() + $tfoot.height() ) {
			scrollGridHeight = contentHeight - $thead.height() - $tfoot.height();
		}
		
		$tbody.height( scrollGridHeight );
		
		this.gridElem.height( scrollGridHeight + $thead.height() + $tfoot.height() );
			
		$tfoot.css("top", $tbody.innerHeight() - $tfoot.height() + 'px');	
	},
	
	/**
	 * @param {number} ind
	 * @param {DOM Node elem} el
	 */
	recalculationBSGridSizeByCell: function( ind, el ) {
		var scrollElementWidth = 16,
			$tbody = $('tbody', this.gridElem).not( this.inGridCell ),
			$thead = $('thead', this.gridElem).not( this.inGridCell ),
			$tfoot = $('tfoot', this.gridElem).not( this.inGridCell ),
			$header = $('thead tr', this.gridElem).not( this.inGridCell ),	
			$gridCell = $(el),
			$headerCell = $header.children( 'th' ).eq( ind ),
			$footerCell = $('tr', $tfoot ).children( 'td' ).eq( ind ),
			beforeGWidth = $gridCell.data("beforeGWidth"), 
			gWidth, fWidth, hWidth, mWidth,
			oldTbodyWidth = $tbody.data("oldTbodyWidth") || $tbody.width();
		
		gWidth = $gridCell.width();
		fWidth = $footerCell.width();
		hWidth = $headerCell.width();
		
		mWidth = Math.max( gWidth, fWidth, hWidth );
		
		if ( beforeGWidth !== undefined && mWidth < beforeGWidth ) {
			return;
		}
		
		$gridCell
			.width( mWidth )
			.data("beforeGWidth", mWidth);
			
		$footerCell.width( mWidth );
		$headerCell.width( mWidth );
	
		if ( beforeGWidth !== undefined ) {
			$tbody.width( oldTbodyWidth + mWidth - beforeGWidth );
		}

		$tbody.data("oldTbodyWidth", $tbody.width());	
	
		$thead.width( $tbody.width() - scrollElementWidth );
		$tfoot.width( $tbody.width() - scrollElementWidth );
	},
	
	/** 
	 * Set style:width values for the header, foter and bottom rows' cells 
	 * @param {jQuery object} $header
	 * @param {jQuery object} $footer
	 * @param {jQuery object} $bottomRow
	 */	 
	setEdgeRowsWidthStyles: function( $header, $footer, $bottomRow, first ) {
		var cellsWidths = [],
			baseClassPref = this.oldLayout ? 'runner-' : 'rnr-',		
			cellsSelector = this.oldLayout ? '.' + baseClassPref + 'cc' : 'th, td',
			rowClass = 'r-gridrow',
			$gridRow, $gridRowCells;
		
		$gridRow = $( 'tbody > .' + rowClass + ':not(.gridRowAdd):first', this.gridElem )
			.not( this.inGridCell );
			
		$gridRowCells = $gridRow.children( cellsSelector );

		$gridRowCells.each( function( ind, cell ) {
			var $cell = $(cell);
			cellsWidths.push( $cell.width() );
			$cell.width( $cell.width() );			
		});

		if ( first) {
			return;
		}
		$header.children( cellsSelector ).each( function( ind, cell ) {
			var $cell = $(cell); 
			$cell.width( cellsWidths[ ind ] );	
		});			

		$bottomRow.children( cellsSelector ).each( function( ind, cell ) {
			var $cell = $(cell); 
			$cell.width( cellsWidths[ ind ] );	
		});
	},
	
	/**
	 * Set the grid's outer element cells' styles
	 * @param {String} cellsSelector
	 * @param {jQuery object} $element
	 * @param {Array} cellsWidths
	 */
	setOuterElementCellsWidthStyles: function( cellsSelector, $element, cellsWidths ) {
		$element.children( cellsSelector ).each( function( ind, cell ) {			
			$( cell ).css('width', cellsWidths[ ind ] + 'px');
		});	
	},
	
	/** 
	 *	Set style:width 'auto' for the header, foter and bottom rows' cells 
	 */
	setEdgeRowWidthAuto: function() {
		var baseClassPref = this.oldLayout ? 'runner-' : 'rnr-',		
			cellsSelector = this.oldLayout ? '.' + baseClassPref + 'cc' : 'th, td',
			rowClass = baseClassPref + 'row',
			$edgeRows;
			
		$edgeRows = $('.' + baseClassPref + 'toprow, .' + rowClass + ':not(.gridRowAdd):first, .footer, .' + baseClassPref + 'bottomrow', this.gridElem )
			.not( this.inGridCell )
			.css('position', 'static');
		
		$edgeRows.children( cellsSelector )
			.css('min-width', 'auto')
			.css('width', 'auto');	
	},
	
	/**
	 * jQuery callback function checking if the current 
	 * DOM element belongs to the DetailPreview container
	 * @return {Boolean}
	 */
	inDetailPreviewContainer: function() {
		return $(this).closest('td.dpinline').length;
	},

	/**
	 * @return {Boolean}
	 */	
	inGridCell: function() {
		return $(this).closest('td').length;
	},
	
	
	/**
	 * Adjust scrollable grid container width after the frid size recalculation
	 * basing on the grid element width 
	 * @param {Number} scrollElementWidth
	 * @param {jQuery object} $container
	 */ 
	adjustScrollableGridContainer: function( scrollElementWidth, $container ) {
		$container.width( this.gridElem.width() + scrollElementWidth );		
	},

	/**
	 * @param {object} row
	 * @param {string} method
	 */
	getEditLinkHandler: function( row, method ) {
		var pageObj = this;
	
		if ( method == 'popup' ) {
			return function() {
				var $button = $(this), pageParams;
				
				pageObj.hideElement('message');
				pageParams = Runner.pages.EditPage.prototype.getOpenPageParams.call( pageObj, row.keys, row.id, "", pageObj.inlineEdit, Runner.emptyFn );
				
				if ( $button.data("page") ) {
					pageParams.baseParams["page"] = $button.data("page");
				}
				
				pageParams.popupWidth = $button.data("popupwidth");
				pageParams.popupHeight = $button.data("popupheight");			
				
				Runner.pages.PageManager.openPage( pageParams );
				return false;
			};
		}
		
		var params = this.getStateParams();
		
		if ( method == 'new' ) {
			return function() {
				window.open( Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_EDIT, row.keys, 'editid', params ), '_blank' );
			}
		}

		return function() {
			location.href = Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_EDIT, row.keys, 'editid', params );
		}
	},
	

	/**
	 * @return {object}
	 */
	getInlineEditObj: function( ) {
		if ( !this.inlineEdit ) {

			this.inlineEdit = new Runner.util.inlineEditing.InlineEdit({
				id: this.pageId,
				tName: this.tName,
				fNames: this.listFields || [],
				rows: this.controlsMap && this.controlsMap.gridRows || []
			});
		}
	
		return this.inlineEdit;	
	},


	/**
	 * @param {object} row
	 * @param {string} method
	 */
	getViewLinkHandler: function( row, method ) {
		var pageObj = this;
		
		if ( method == 'popup' ) {
			return function() {
				var $button = $(this), pageParams;
				
				pageObj.hideElement('message');
				pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( pageObj, row.keys, row.id, pageObj.getInlineEditObj(), Runner.emptyFn );
				
				if ( $button.data("page") ) {
					pageParams.baseParams["page"] = $button.data("page");
				}
				
				pageParams.popupWidth = $button.data("popupwidth");
				pageParams.popupHeight = $button.data("popupheight");
				
				Runner.pages.PageManager.openPage( pageParams );
				return false;
			};
		}
		
		var params = this.getStateParams();
		
		if ( method == 'new' ) {
			return function() {
				window.open( Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_VIEW, row.keys, 'editid', params ), '_blank' );
			}
		}

		return function() {
			location.href = Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_VIEW, row.keys, 'editid', params );
		}
	},

	/**
	 * @param {object} row
	 * @param {string} method
	 */
	getPrintLinkHandler: function( row, method ) {
		var pageObj = this;
		
		/*if ( method == 'popup' ) {
			return function() {
				pageObj.hideElement('message');
				var pageParams = Runner.pages.PrintPage.prototype.getOpenPageParams.call( pageObj, row.keys, row.id, pageObj.inlineEdit, Runner.emptyFn );
				Runner.pages.PageManager.openPage( pageParams );
				return false;
			};
		}*/
		
		var params = this.getStateParams();
		
		if ( method == 'new' ) {
			return function() {
				window.open( Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_PRINT, row.keys, 'editid', params ), '_blank' );
			}
		}

		return function() {
			location.href = Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_PRINT, row.keys, 'editid', params );
		}
	},

	/**
	 * @param {object} row
	 * @param {string} method
	 */
	getAddLinkHandler: function( row, method ) {
		var pageObj = this;
	
		if ( method == 'popup' ) {
			return function() {
				pageObj.showAddPage( true );
				/*
				pageObj.hideElement('message');
				var pageParams = Runner.pages.AddPage.prototype.getOpenPageParams.call( pageObj, pageObj.id );
				Runner.pages.PageManager.openPage( pageParams );
				*/
				return false;
			};
		}
		
		var params = this.getStateParams();
		
		if ( method == 'new' ) {
			return function() {
				window.open( Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_ADD, null, '', params ), '_blank' );
			}
		}

		return function() {
			location.href = Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_ADD, null, '', params );
		}
	},

	getDetailsLinkHandler: function( row, table, method ) {
		var mKeys = [],
			key,
			link;
		
		for ( key in row.masterKeys[ table ] ) {
			mKeys.push( row.masterKeys[ table ][ key ] );
		}		
		
		link = Runner.pages.getUrl( table, this.dpObjs[ table ].pageType , mKeys, "masterkey" ) + "&mastertable=" + encodeURIComponent( this.tName );

		if ( method == 'new' ) {
			return function() {
				window.open( link, '_blank' );
			}
		}

		return function() {
			location.href = link;
		}
	},

	/**
	 * @param {string} url
	 * @param {string} method
	 */
	getUrlLinkHandler: function( url, method ) {
		var pageObj = this;
		
		if ( method == 'popup' ) {
			return function() {
				var params = {
					url: url
				};
				Runner.displayPopup( params );
				return false;
			};
		}
		
		if ( method == 'new' ) {
			return function() {
				window.open( url , '_blank' );
			}
		}

		return function() {
			location.href = url;
		}
	},		
	
	/**
	 * Return checked boxes from the records that the user can edit/delete
	 *	This function is used for Delete and Edit operations only.
	 * For Print selected and Export selected all records will do.
	 * @param Boolean leaveChecked - when true, leave checkboxes as is.
	 * 		  When false, clear checkboxes in records where the operation is not permitted
	 * @param Boolean edit - when true, check Edit permissions on the records, when false - check Delete permissions
	 * @returns {Object}
	 */
	getOwnedSelBoxes: function( leaveChecked, edit ) {
		var pageObj = this,
		leaveChecked = leaveChecked || false,
			$selBoxes = this.getSelBoxes( this.id );
		
		//	return checkboxes from owned records only 
		return $selBoxes.filter( function( index ) {
			var $checkBox = $(this),
				boxId = $checkBox.attr('id'),
				rowId = boxId.substr( boxId.lastIndexOf('_') + 1 );
			
			var rowObject = pageObj.findRow( rowId );
			if ( edit && !rowObject.canEditRecord || !edit && !rowObject.canDeleteRecord ) {
				if ( !leaveChecked ) {
					$checkBox.prop("checked", false);	
				}
				return false;
			}
			return true;
		});	
	},

	/**
	 * @returns { table: RunnerPage }
	 */
	getDetailsPages: function( rowId ) {
		var rowObj = this.getRowObject( rowId );
		return rowObj && rowObj.getDetails();
	},

	/**
	 * @returns RunnerPage object || null
	 */
	getDetailsPage: function( table, rowId ) {
		var detailsPages = this.getDetailsPages( rowId );
		return detailsPages && detailsPages[ table ];
	},

	initFieldFilterHeader: function( $filter ) {
		var pageObj = this;
		var LOADING_CLASS = 'loading';
		var SEARCH_ACTIVE_CLASS = 'search-active';

		var fieldName = $filter.data('fieldname');
		var $searchMenu = $filter.find('.menu');
		var $okBtn = $searchMenu.find('.field-filter-submit');
		var $cancelBtn = $searchMenu.find('.field-filter-cancel');
		var $dropdown = $filter.find('.dropdown');
		var $valuesContainer = $filter.find('.values-list');
		var $searchInput = $filter.find('.search');
		var $select = $filter.find('.select');
		var $addToExisting = $filter.find('.add-to-existing');
		var $addToExistingWrapper = $filter.find('.add-to-existing-wrapper');
		var $clearAllBtn = $filter.find('.clear-all');
		var $clearFilter = $filter.find('.clear-filter');

		// 'enabled' || 'disabled'
		var fieldFiltered = $filter.attr('data-fieldfilter-state') == 'enabled';

		// JQuery objects of FieldFilter *value* wrappers
		// initialized after fetching values 
		var $values = $();
		// values matched with search input
		var $availableValues = $();
		// available and checked values
		var $checkedAvailableValues = $();

		var updateAvailableValues = function() {
			var searchInputValue = $searchInput.val().toLowerCase();
			$values = $valuesContainer.find('.value');

			$availableValues = $values.filter( function(index) {
				var $value = $(this);
				var searchValues = $value.data('search');
				for (var i = 0; i < searchValues.length; i++) {
					// case insensitive search
					var lCaseSearchValue = searchValues[i].toLowerCase();
					if (lCaseSearchValue.indexOf(searchInputValue) != -1) {
						return true;
					}
				}

				return false;
			});
		}

		var updateCheckedAvailableValues = function() {
			$checkedAvailableValues = $availableValues.filter(function(index) {
				var $value = $(this);
				var $checkbox = $value.find('input[type=checkbox]');
				return $checkbox.prop('checked');
			});
		};

		var updateValues = function() {
			updateAvailableValues();
			updateCheckedAvailableValues();
		};

		var updateSelect = function() {
			$select.prop('checked', $checkedAvailableValues.length == $availableValues.length);
		};

		// update *values* on checkboxes state change
		var initValues = function() {
			$values.each(function(index) {
				var $value = $(this);
				var $checkbox = $value.find('input[type=checkbox]');
				$checkbox.on('change', function() {
					updateValues();
					updateSelect();
				});
			});
		};

		var setValuesChecked = function($values, checked) {
			$values.each(function(index) {
				var $value = $(this);
				var $checkbox = $value.find('input[type=checkbox]');
				$checkbox.prop('checked', checked);
			});
		};

		var searchUpdatehHandler = function() {
			updateAvailableValues();
			setValuesChecked($availableValues, true);
			updateCheckedAvailableValues();

			updateSelect();

			if (searchSuccessfull()) {
				if (fieldFiltered) {
					$addToExistingWrapper.show();
				}
				$filter.addClass(SEARCH_ACTIVE_CLASS);
			} else {
				$addToExistingWrapper.hide();
				$filter.removeClass(SEARCH_ACTIVE_CLASS);
			}

			$values.hide();
			$availableValues.show();
		};

		// *values* filtered && not empty
		var searchSuccessfull = function() {
			return $values.length != $availableValues.length && $availableValues.length != 0;
		};
		
		var $ddmenu = $dropdown.find(".dropdown-menu"),
			$selectArea = $filter.find(".select-area");
		

		var ffr = new Runner.util.FieldFilterResizable( fieldName, $dropdown, this );	
		ffr.makeResizable();
		ffr.updateSize();

		$dropdown.on('show.bs.dropdown', function( event ) {
			var url = Runner.pages.getUrl( pageObj.tName, pageObj.pageType );
			var ajaxParams = {
					fieldFilter: JSON.stringify({
						filterValues: true,
						field: fieldName
					})
				};

			var params = Object.assign( {}, Object.assign( {}, pageObj.getBaseParams(), pageObj.getStateParams() ), ajaxParams );

			$filter.addClass(LOADING_CLASS);
			Runner.runnerAJAX(url, params, function(params) {
				// clear all elements
				$valuesContainer.empty();

				// group entries by display value
				var displayValues = {};
				var diplayOrder = [];
				params.values.forEach(function(entry) {
					if( displayValues[entry.display] === undefined ) {
						diplayOrder.push( entry.display );
						displayValues[entry.display] = [];
					}
					displayValues[entry.display].push(entry);
				});

				// create values entries
				diplayOrder.forEach(function(display) {
					var values = displayValues[display].map(function(e) { return e.value; });
					var entry = displayValues[display][0];
					var $valueElement = pageObj.makeFieldFilterValueEntry(values, entry.display, entry.selected, entry.search);
					$valuesContainer.append($valueElement);
				});

				$filter.removeClass(LOADING_CLASS);
				updateValues();
				updateSelect();
				initValues();
			});

			$addToExistingWrapper.hide();
			$clearAllBtn.hide();

			// show if any filter applied
			if ($('[data-fieldfilter-state="enabled"][data-pageid=' + pageObj.pageId + ']').length != 0) {
				$clearAllBtn.show();
			}
			
			$clearFilter.toggle( fieldFiltered )
				.on( "click", function(e) {
					var params = Object.assign( 
							{}, 
							pageObj.getBaseParams(),
							// reset page
							{ goto: undefined },
							{
								fieldFilter: JSON.stringify({
									clearFilter: true,
									field: fieldName,
									addToExisting: searchSuccessfull() && $addToExisting.prop('checked')
								}),
							} 
						);
					
					pageObj.reload( params, pageObj.getExtraPageReloadParams(), { method: 'POST' } );
				});;
			

			setTimeout( function() {
				if ( !Runner.util.isInViewport( $ddmenu.get(0), true )
					/* || pageObj.gridElem && !Runner.util.isInContainer( $ddmenu.get(0), $(pageObj.gridElem).get(0), true ) */ ) {
					
					// reset width to default and move into the viewport
					var width = 300;
					$ddmenu.children('.resizable').css('width', width + 'px' );
					$ddmenu.css( Runner.isDirRTL() 
						? { right: '-' + width + 'px', left: 'auto' } 
						: { left: '-' + width + 'px', right: 'auto' } );
				}
				ffr.makeResizable();
				$searchInput.last().trigger('focus');
			});

			// clear search value
			$searchInput.val("");
			searchUpdatehHandler();
		});

		$searchInput.on('input', function(event) {
			searchUpdatehHandler();
		});

		$okBtn.on('click', function( event ) {
			var allChecked = $checkedAvailableValues.length == $availableValues.length && !searchSuccessfull();			
			if ( allChecked && !fieldFiltered ) {
				return;
			}
			
			var values = $checkedAvailableValues.toArray()
				.reduce( function( arr, entry ) {
					return arr.concat( $(entry).data('value') || [] );
				}, [] );

			var params = Object.assign(
					{},
					pageObj.getBaseParams(),
					// reset page
					{ goto: undefined },
					{
						fieldFilter: JSON.stringify({
							updateFieldFilterValues: !allChecked || !fieldFiltered,
							clearFilter: allChecked && fieldFiltered,
							field: fieldName,
							values: JSON.stringify( allChecked && fieldFiltered ? [] : values ),
							addToExisting: searchSuccessfull() && $addToExisting.prop('checked')
						}),
					} 
				);
				
			pageObj.reload( params, pageObj.getExtraPageReloadParams(), { method: 'POST' } );
		});

		$select.on('click', function(event) {
			var selectValue = $select.prop('checked');
			setValuesChecked($availableValues, selectValue);
			updateCheckedAvailableValues();
		});

		// prevent menu closing
		$searchMenu.on('click', function(event) {
			var $target = $(event.target);
			// close button
			if ($target.is($cancelBtn) || $target.is($okBtn)) {
				return;
			}

			event.stopPropagation();
		});

		$clearAllBtn.on('click', function(event) {
			var params = Object.assign(
				{}, 
				pageObj.getBaseParams(),
				// reset page
				{ goto: undefined },
				{
					fieldFilter: JSON.stringify({
						clearFieldFilterValues: true
					}),
				} 
			);

			pageObj.reload( params, pageObj.getExtraPageReloadParams(), { method: 'POST' } );
		});

		$dropdown.on('hide.bs.dropdown', function(event) {
			$filter.removeClass(LOADING_CLASS);
		});
	},
	
	
	initFieldFilter: function() {
		var pageObj = this;
		var $fieldFilters = $( '[data-fieldfilter][data-pageid=' + this.pageId +']' );
		
		// init each FieldFilter header
		$fieldFilters.each( function() {
			var $filter = $( this );
			pageObj.initFieldFilterHeader( $filter );
		})
	},

	makeFieldFilterValueEntry: function( value, displayValue, selected, searchValues ) {
		var $labelContainer = $( '<label class="value"></label>' );
		var $checkbox = $( '<input type="checkbox">' );
		var $span = $( '<span></span>' );

		$span.html( displayValue );
		$checkbox.prop( 'checked', selected );
		$labelContainer.data( 'value', value );
		$labelContainer.data( 'search', searchValues );;

		$labelContainer.append( $checkbox );
		$labelContainer.append(' ').append( $span );

		return $labelContainer;
	},


	initPagination: function( ) {
		Runner.pages.ListPageCommon.superclass.initPagination.call( this );
		
		// initialize next n records
		var pageObj = this;
		this.findItemType("next_page").find("a[data-function='nextpage']").on("click", function(e) {
			Runner.Event.prototype.stopEvent( e );
			
			var url = new URL( this.href );
			
			pageObj.preparePageForReloading();
			pageObj.reload( { goto: url.searchParams.get('goto') }, pageObj.getExtraPageReloadParams() );	
		});	
	},
});


	
Runner.pages.ListPageFly = Runner.extend( Runner.pages.ListPageCommon, {
	/**
	 * The main lookup control
	 * @type {object}
	 */	
	lookupCtrl: null,
	
	/**
	 * @type {object}
	 */
	lookupBaseParams: null,
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {	
		Runner.pages.ListPageFly.superclass.constructor.call(this, cfg);
		
		this.useResize = false;
		
		this.listFields = Runner.pages.PageSettings.getTableData(this.tName, 'listFields');
		this.bricksForReload = ["grid", "pagination", "message", "reorder_records"];
		
		this.lookupBaseParams = this.lookupCtrl.getLookupParams( this.pageId );
		this.lookupBaseParams.page = this.pageName;
	},
	
	/**
	 *
	 */
	destructor: function() {
		Runner.pages.ListPageFly.superclass.destructor.call(this);
		
		this.pageCont = null;
		this.lookupCtrl.pageId = -1;
	},
	
	/**
	 *
	 */
	init: function() {
		var flyContainer;
		Runner.pages.ListPageFly.superclass.init.call(this);
		
		this.initLinks();
		this.initSorting();
		this.initSortByDropdown();
		this.initAddButton();

		this.initSection508();	
		
		//	show inline Add button on narrow small screens
		/* everything should be visible on ,pobile screen in  popup, no 'more' ( collapse ) button */
		this.togglePageMobileCollapse( undefined, false );
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	/**
	 * Initialize lookupSelect links
	 */
	initLinks: function() {		
		this.lookupCtrl.setEnabled();
		
		this.initLink( $("[type='lookupSelect" + this.pageId + "']") )
	},

	/**
	 * @param {object} $link
	 */
	initLink: function( $link ) {
		var pageObj = this;
		
		$link.on("click", function(e) {
			var ind = $(this).data("ind");
			
			pageObj.close();
			
			pageObj.lookupCtrl.setValByInd( ind );
			pageObj.lookupCtrl.setFocus();
			// for detail table on master add page
			pageObj.lookupCtrl.fireEvent("keyup");
			
			return false;
		});	
	},
	
	/**
	 *
	 */
	initSearch: function() {
		this.searchController = new Runner.search.SearchController({
			id: this.pageId,
			tName: this.tName,
			fNamesArr: this.controlsMap.search.allSearchFields,
			shortTName: this.shortTName,
			usedSrch: this.controlsMap.search.usedSrch,
			panelSearchFields: this.controlsMap.search.panelSearchFields,
			ajaxSubmit: true,
			useSuggest: Runner.pages.PageSettings.getTableData(this.tName, "ajaxSuggest"),
			pageType: this.pageType,
			formBaseParams: this.lookupBaseParams,
			filterContolsData: this.controlsMap.filters? this.controlsMap.filters.controls : [],
			delegatedExtraSuggestParams: this.extraSearchSuggestParams.bind( this ),
			searchFieldsLabels: this.controlsMap.search.allSearchFieldsLabels,
			isSearchPanelInflexible: this.controlsMap.search.inflexSearchPanel,
			isSearchRequired: this.controlsMap.search.isSearchRequired,
			requiredSearchFields: this.controlsMap.search.requiredSearchFields,	
			searchPanelRunByUserApi: this.controlsMap.search.searchPanelRunByUserApi			
		});
		
		this.searchController.init( this.controlsMap.search.searchBlocks );
		
		this.searchController.srchForm.on("beforeSubmit", function() {
			this.preparePageForReloading();
		}, this);
		this.searchController.srchForm.on("submitFailed", function() {
			Runner.stopLoading();
		}, this);
		
		this.searchController.on("afterSearch", function(respObj, srchController, srchForm) {
			Runner.stopLoading();
			this.pageReloadHn( respObj );
			this.searchController.toggleShowAll( this.searchController.usedSrch );
		}, this);		
	},
	
	/**
	 *
	 */
	extraSearchSuggestParams: function() {
		return {
			forLookup: 1,
			mainTable: this.lookupCtrl.table,
			mainField: this.lookupCtrl.fieldName,
			mainPageType: this.lookupCtrl.pageType,
			parentCtrlsData: JSON.stringify( this.lookupCtrl.getParentControlsData() ),
		};
	},

	showAddInPopup: function( $link ) {
		return true;
	},
	
	getAddPopupParams: function() {
		var pageParams = Runner.pages.ListPageFly.superclass.getAddPopupParams.call( this );

		pageParams.baseParams["forLookup"] = true;
		pageParams.baseParams["table"] = this.lookupCtrl.shortTableName;
		pageParams.baseParams["field"] = this.lookupCtrl.fieldName;			
		pageParams.baseParams["pageType"] = this.lookupBaseParams.pageType;
		pageParams.baseParams["page"] = this.lookupCtrl.addPageId;

		if ( !!this.lookupCtrl.parentCtrls.length ) { 
			pageParams.baseParams["parentsExist"] = true;
			pageParams.baseParams["parentCtrlsData"] = JSON.stringify( this.lookupCtrl.getParentControlsData() );			
		}
	
		return pageParams;
	},

	/**
	 * Initialize 'Inline Add' functionality
	 */
	initInline: function() {
		if ( !this.permis['add'] ) {
			return;
		}
		
		this.inlineAdd = new Runner.util.inlineEditing.InlineAdd( this.getInlineAddParams() );
		this.inlineAdd.init();

		this.inlineAdd.on("beforeSetVals", function( row, fields ) {
			if ( row.data[ this.lookupCtrl.dispFieldAlias ] ) {
				row.data[this.lookupCtrl.dispFieldAlias] = '<a href="#" type="lookupSelect' + this.searchController.srchForm.baseParams.id + '">'
					+ row.data[ this.lookupCtrl.dispFieldAlias ] + '</a>';
			} else if ( row.data[ this.lookupCtrl.dispField ] ) {
				row.data[ this.lookupCtrl.dispField ] = '<a href="#" type="lookupSelect' + this.searchController.srchForm.baseParams.id + '">' 
					+ row.data[ this.lookupCtrl.dispField ] + '</a>';
			} else {
				for ( field in row.data ) {
					row.data[ field ] = '<a href="#" type="lookupSelect' + this.searchController.srchForm.baseParams.id + '">' + row.data[ field ] + '</a>';
					break;
				}
			}
		}, this);
		
		this.inlineAdd.on("afterSubmit", function( vals, fields, keys, rowId, newData ) {
			var	$link = $('tr[id="gridRow' + rowId + '"] a[type="lookupSelect' + this.pageId + '"]').eq(0),
				newInd;
				
			newInd = this.lookupCtrl.addLookupVal( newData.linkValue, newData.displayValue, newData.keys );
			$link.data("ind", newInd);

			this.initLink( $link );
		}, this);
		
		this.inlineAdd.on("afterInit", function( pageObj, proxy, pageid, row ) {
			this.afterInitInlineAdd( row );
		}, this);
	},
	
	/**
	 * Get InlineAdd params
	 * @return {object}
	 */
	getInlineAddParams: function() {
		var params = {
			id: this.pageId,
			tName: this.tName,
			fNames: this.listFields,
			shortTName: this.shortTName,
			rows: this.controlsMap.gridRows,
			inlineEditObj: this.inlineEdit,
			isOldLayout: this.oldLayout,
			loadSettings: true,
			lookupParams: {
				forLookup: true,
				field: this.lookupCtrl.fieldName, 
				table: this.lookupCtrl.shortTableName
			},
			lookupFieldInfo: {
				linkField: this.lookupCtrl.linkField,
				dispField: this.lookupCtrl.dispField,
				dispFieldAlias: this.lookupCtrl.dispFieldAlias
			}
		};
			
		if ( !!this.lookupCtrl.parentCtrls.length ) { 
			params.lookupParams.parentsExist = true;
			params.lookupParams.parentCtrlsData = JSON.stringify( this.lookupCtrl.getParentControlsData() );			
		}
		
		return params;
	},
	
	/**
	 * Initialize Grid Tabs Where functionality
	 */
	initGridTabs: function() {
		var pageObj = this;
				
		$( pageObj.pageCont )
			.off( 'click.tabs' )
			.on( 'click.tabs', '.bsgrid_tabs li > a[data-pageid=' + this.id + '][data-tabid]', function(e) {
			var	url = $(this).attr('href'),
				tabs = $(this).parents('ul').find('li'),
				$currentTab = $(this).parent();
			
			$.each(tabs, function(e) {
				$(this).removeClass('active');
			});
			$currentTab.addClass('active');
			
			pageObj.preparePageForReloading();
			delete pageObj.lookupBaseParams.tab;
			delete pageObj.lookupBaseParams.goto;
			Runner.runnerAJAX( url, pageObj.lookupBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
			
			return false;
		});
	},
	
	/**
	 * Initialize sorting functionality
	 */
	initSorting: function() {
		var pageObj = this;
		
		this.listFields.forEach( function( listField ) {
			var $header = $( '[id="order_' + Runner.goodFieldName( listField ) + '_' + pageObj.id + '"]' );
			var $headerLabel = $header.find( '.text-label' );
			$headerLabel.on("click", function(e) {				
				pageObj.preparePageForReloading();
				Runner.runnerAJAX( $header.data("href"), pageObj.lookupBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
				return false;
			});
		});
	},

	/**
	 * Initialize 'sort by' drop
	 */
	initSortByDropdown: function() {
		var pageObj = this;
		
		$("[id='sortBy" + this.id + "']")
			.off("change.sortby")
			.on("change.sortby", function(e) {
				var ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType ) 
					+ "?sortby=" + $(this).val();
			
				pageObj.preparePageForReloading();
				Runner.runnerAJAX( ajaxURL, pageObj.lookupBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
		});
	},
	
	/**
	 * The fly list page's reload handler
	 * @param {Object} respObj
	 */	
	pageReloadHn: function( respObj ) {
		Runner.stopLoading();
		
		if ( !respObj.success ) {
			this.winObject 
				&& this.winObject.body().html("REQUEST FAILED");
			return;
		}
		
		this.destroyVideo();
		Runner.setIdCounter( respObj.idStartFrom );
				
		// set controlsMap
		this.controlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.pageId ];
		
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		
		var $content = this.winObject.getContentNode(),
			$header = this.winObject.header(),
			$footer = this.winObject.footer(),
			$body = this.winObject.body();
			
		// fix body height before
		$body.height( $body.height() );		
		$body./*find('.rnr-pagewrapper').*/html( respObj['html'] );
		
		var itemSelector = '[data-itemid]:not(li)',
			footerContent = $(respObj.footerCont).find( itemSelector ).addBack( itemSelector );
		
		$footer.html( footerContent );
		$body.height(  $content.innerHeight() - $header.outerHeight() - $footer.outerHeight() - $body.outerHeight() + $body.height());
		
		this.initSearch();
		this.initPagination();
		
		this.inlineButtonsIntialized = false
		this.initInline();
		
		this.togglePageMobileCollapse( undefined, false );

		
		// set new vals
		this.lookupCtrl.lookupVals = this.controlsMap.lookupVals;
		this.initLinks();
		
		this.initGridElem();
		this.initSorting();
		this.initSortByDropdown();
		this.initAddButton();
		
		this.reinitGridCustomButtons();
		
		if ( this.hasMaps() ) {
			this.mapManager.init( this.controlsMap.gMaps.mapsData );
		}
		
		if ( this.inlineAdd ) {
			this.inlineAdd.reInit( this.controlsMap.gridRows );
		}
		
		this.searchController.usedSrch = this.controlsMap.search.usedSrch;	
		this.searchController.toggleShowAll( this.searchController.usedSrch );	
		
		if ( typeof respObj.viewControlsMap === "object" && respObj.viewControlsMap[ this.tName ][ this.pageType ][ this.pageId ].controls ) {
			this.reInitViewControls( respObj.viewControlsMap[ this.tName ][ this.pageType ][ this.pageId ].controls );
		}

		this.initGridTabs();
		this.initFieldFilter();
		
		this.fireEvent('afterPageReady', this, this.proxy, this.pageId);
	},
	
	/**
	 * Reinitialize view controls
	 * @param {array} viewControls
	 */ 
	reInitViewControls: function( viewControls ) {
		this.setupViewContols( viewControls, this.pageCont );
	},

	
	/**
	 * Resize don't use on the list lookup
	 */
	initResize: Runner.emptyFn,
	
	/**
	 * A stub
	 */
	initScrollGridBody: Runner.emptyFn,

	/**
	 * A stub
	 */
	setUrlParams: Runner.emptyFn,

	getGrayedElement: function() {
		return this.bsWin ? $(".modal-content", this.bsWin) : this.gridElem;
	},

	getBaseParams: function() {
		return this.lookupBaseParams;
	},
});		
/**
 * Grid row data wrapper
 */
Runner.gridRow = Runner.extend( Runner.util.Observable, {
	constructor: function ( cfg ) {
		Runner.apply( this, cfg );
		
		Runner.gridRow.superclass.constructor.call( this, cfg );
		this.addEvents( "edited" );
	}
});

/**
 *
 */
Runner.pages.ListPage = Runner.extend( Runner.pages.ListPageCommon, {
		
	inlineEdit: null,
	
	inlineAdd: null,
	
	pageType: Runner.pages.constants.PAGE_LIST,
		
	mapManager: null,
		
	
	existMultipleHint: false,

	hideFieldPanelSorted: false,
	
	spreadsheet: false,
	
	// spreadsheet mode auto add id
	autoAddRecordId: -1,
	
	autoAddNewRecord: false,
	

	constructor: function( cfg ) {
		Runner.pages.ListPage.superclass.constructor.call(this, cfg);
		
		this.listFields = Runner.pages.PageSettings.getTableData( this.tName, "listFields" );
		this.isUseInlineEdit = Runner.pages.PageSettings.getTableData( this.tName, "isInlineEdit" );
		
		this.spreadsheet = this.pageData['spreadsheet'];
		this.autoAddNewRecord = Runner.pages.PageSettings.getTableData( this.tName, "autoAddNewRecord" );
		this.addNewRecordsToBottom = Runner.pages.PageSettings.getTableData(this.tName, "addToBottom");
		
		this.addEvents("beforeDelete", "afterInlineAdd", "afterInlineEdit");
	},
	
	
	init: function() {
		Runner.pages.ListPage.superclass.init.call(this);
		this.initButtons();
		
		this.initSortByDropdown();
		
		this.initSection508();
		
		this.initUnlockingOnPageUnload();
		this.initGridClickActions();
	},
	
	initForGrid: function() {
		Runner.pages.ListPage.superclass.initForGrid.call(this);
		
		this.initSorting();
		this.initDetails();
		this.initPopupLinks();
		this.initShowHideColumnToggle();
		this.initColumnReordering();
		this.initRowReordering();
		this.initPrintFriendly();
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},

	/**
	 *	show Hidden Column
	 */
	showHiddenColumn: function(fieldName) {
		var pageObj = this,
			$panelBlock = $("#fildhidepanel" + pageObj.id),
			box = $panelBlock.find('input[data-field="' + fieldName + '"]');

		if ( !box.prop('checked') ) {
			box.trigger("click");
		}
	},

	/**
	 *	init Grid Column Toggle
	 */
	initShowHideColumnToggle: function() {
		if ( !Runner.pages.PageSettings.getTableData(this.tName, "isAllowShowHideFields") ) {
			this.initReorderingInSHPanel();
			return;
		}

		var pageObj = this,
			$panelBlock = $("#fildhidepanel" + pageObj.id),
			ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType ),
			hideColumns = Runner.pages.PageSettings.getTableData(this.tName, "hideColumns") || [],
			colsToHide = hideColumns[ Runner.util.getDeviceClass() ] || [],
			$grid = $('[data-location="grid"][data-pageid=' + this.id + ']'),
			columns = $("table.bs-fieldsgrid", $grid).length > 0,
			vertical = !columns && $grid.hasClass('bs-grid-vertical'),
			i;

		if ( !$panelBlock.length ) {
			return;
		}
		
		// apply checkbox setting
		for ( i = 0; i < colsToHide.length; ++i ) {
			$panelBlock.find('input[data-field="' + colsToHide[i] + '"]')
				.prop('checked', false);
		}	

		$panelBlock.find('a').on( 'click', function() {
			$(this).find('input').trigger("click");
			return false;
		});
		
		$panelBlock.find('input').off('click').on('click', function(e) {
			var hideFields = {".dummyField": false },
				field = $(e.toElement || e.target).data('field'),
				fieldName = $(e.toElement || e.target).data('fieldname'),
				show = $(e.toElement || e.target).prop('checked');
			
			pageObj.layoutHelper.toggleField( fieldName, show );
			
			
			//	save to the server
			$panelBlock.find('input').each( function( n, box ) {
				hideFields[ $(box).data("field") ] = box.checked;
				/*
				if ( !box.checked ) {
					hideFields.push( $(box).data("field") );
				}
				*/
			});

			pageObj.onReoderColumnsStop();
			
			$.post( ajaxURL, {
				saveParam: Runner.pages.constants.NEWSHFIELDS_PARAMS_TYPE, 
				data: JSON.stringify(hideFields),
				deviceClass: Runner.util.getDeviceClass()
			});
			
			e.stopPropagation();
		});

		this.initReorderingInSHPanel();
	},

	/**
	 *	Re init Grid Column Reordering (use in AJAX reload)
	 */
	reInitColumnReordering: function() {
		var columnOrder = Runner.pages.PageSettings.getTableData(this.tName, "columnOrder");
		this.initColumnReordering(columnOrder, true);
	},

	/**
	 * @return Array of number - record-ids in the order as they appear on the page
	 */
	getRowOrder: function() {
		var rowOrder = [];	
		this.gridElem.find('[data-sortable] [data-record-id]').each( function() {
			var recordId = parseInt( $(this).attr( 'data-record-id' ), 10 );
			if ( recordId && rowOrder.indexOf( recordId ) === -1 ) {
				rowOrder.push( recordId );
			}
		} );
		if ( !this.addNewRecordsToBottom ) {
			rowOrder.reverse();
		}
		return rowOrder;
	},

	orderByRecId: function( recordId ) {
		return ( this.getInlineRow( recordId ) || {} ).order;
	},

	initRowReordering: function( reInitColumnOrder, ajaxReload ) {
		var pageObj = this;
		if ( !Runner.pages.PageSettings.getTableData(this.tName, "reorderRows") ) {
			return;
		}
		
		if ( Runner.smallScreen ) {
			// conflicts with swipe
			return;
		}

		var getRowrecordId = function( $elem ) {
			return parseInt( $elem
				.add( $elem.find('*') )
				.filter('[data-record-id]')
				.first()
				.attr( 'data-record-id' ), 
			10 );
		}
		
		var rowOrder, draggedRecordId, draggedRows;
		var sortable = this.gridElem.is('table') 
			? $( " > tbody", this.gridElem ) //	horizontal layout
			: this.gridElem;		//	vertical layout
	
		this.bodyHeight = parseInt( this.gridElem.data("body-height"), 10 );
		if ( !this.bodyHeight ) {
			var sortableChildren = sortable.children( '[data-record-id]' );
			var firstId = sortableChildren.first().attr( 'data-record-id' );
			this.bodyHeight = sortableChildren.filter( '[data-record-id="' + firstId + '"]' ).length;
		}
		$( " > *:nth-child(" + this.bodyHeight.toString() + "n + 1)", sortable ).attr('data-sortable', '')

		var sortParams = {
			items: '[data-sortable]',
			start: function( e, ui ) {
				rowOrder = pageObj.getRowOrder();
				draggedRecordId = getRowrecordId( ui.item );
				draggedRows = ui.item.add( ui.item.siblings().filter( '[data-record-id="'+ draggedRecordId +'"], :has([data-record-id="'+ draggedRecordId +'"])') );
			},
			update: function( e, ui ) {
				var newRowOrder = pageObj.getRowOrder();
				var oldPos = rowOrder.indexOf( draggedRecordId );
				var newPos = newRowOrder.indexOf( draggedRecordId );
				if ( oldPos == newPos || oldPos == -1 || newPos == -1 ) {
					sortable.sortable("cancel");
					return;
				}
				//	correct item position in case it was dropped inside other record
				var $next = ui.item.next();
				var $prev = ui.item.prev();
				if ( $next.length && $prev.length ) {
					var nextId = getRowrecordId( $next );
					var prevId = getRowrecordId( $prev );
					if ( prevId === nextId ) {
						//	shift the dragged row to after the last row or prevId record
						var $lastChild = sortable.children().filter( '[data-record-id="'+ nextId + '"], :has( [data-record-id="'+ nextId + '"])' ).last();
						ui.item.insertAfter( $lastChild );
					}
				}

				//	move the rest of the rows after the dragged item
				if ( draggedRows.length > 1 ) {
					draggedRows.not( ui.item ).insertAfter( ui.item );
				}

				var forward = newPos > oldPos;
				var row = pageObj.getInlineRow( draggedRecordId );
				if ( !row.order ) {
				}
				var updateData = {
					keys: row.keys,
					recordId: draggedRecordId,
					newOrder: pageObj.orderByRecId( rowOrder[ newPos ] ),
					oldOrder: pageObj.orderByRecId( rowOrder[ oldPos ] ),
					allRecordKeys: pageObj.getAllRecords().map( function( ajaxRow ) {
						return { 
							recordId: ajaxRow.recordId(), 
							keys: ajaxRow.getKeys(),
							order: ajaxRow.getRowObject().order
						};
					})
				};
				if ( updateData.newOrder === undefined || updateData.oldOrder === undefined ) {
					return;
				}
				var ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType );
				Runner.runnerAJAX( ajaxURL, {
						a: 'saveRowOrder', 
						data: JSON.stringify( updateData ),
						page: pageObj.pageName
					},
					function( respObj ) {
						if ( !respObj.orders ) {
							return;
						}
						//	update 'order' parameter in row objects
						//	ensure no 
						var prevOrder = null;
						var prevOrderId = 0;
						
						//	traverse all records and update order correcting gaps caused by inline add. 
						//	When record was moved forward, traverse from the end to beginning, and vice versa
						var sign = forward ? -1 : 1;
						(forward ? newRowOrder.reverse() : newRowOrder ).forEach( function( recordId, idx ) {
							var row = pageObj.getInlineRow( recordId ) || {};
							var newOrder = parseInt( respObj.orders[ recordId ], 10 );
							if ( newOrder ) {
								if ( prevOrder !== null && sign * ( newOrder - prevOrder ) <= 0 ) {
									//	this shouldn't happen unless inline addd involved
									newOrder = prevOrder + sign;
								}
							} else {
								//	inline-added record
								if ( sign * ( row.order - prevOrder ) > 0 ) {
									newOrder = row.order;
								} else {
									newOrder = prevOrder + sign;
								}
							}
							row.order = newOrder;
							prevOrder = newOrder;
							prevOrderId = recordId;
						});
					}
				); 

			},
		};
		
		sortable.sortable( sortParams );
	},


	/**
	 *	init Grid Column Reordering
	 */
	initColumnReordering: function( reInitColumnOrder, ajaxReload ) {
		if ( !Runner.pages.PageSettings.getTableData(this.tName, "isAllowFieldsReordering") ) {
			return;
		}

		var pageObj = this,
			ajaxURL = Runner.pages.getUrl( this.tName, this.pageType ),
			columnOrder = reInitColumnOrder || Runner.pages.PageSettings.getTableData(this.tName, "columnOrder"),
			$gridHorizonatal = $('table[data-location="grid"][data-pageid=' + this.id + ']'),
			columnNormalOrder = {},
			needClickAfterReorder = [],
			dragged, ctrlKey, firstTarget, skipFirstDrag;

		if ( $gridHorizonatal.length > 0 ) {
			columnNormalOrder = this.refineColumnsOrderData( columnOrder, $gridHorizonatal );
						
			$gridHorizonatal.find('th[data-field]')
				.on('mousedown', function(e) {
					ctrlKey = e.ctrlKey;
					firstTarget = e.target;
				});		
			
			if ( $gridHorizonatal.data('dragtable') ) {
				$gridHorizonatal.dragtable( "destroy" );
			}
			
			$gridHorizonatal.dragtable({
				dragaccept:'[data-field]',
				persistState: function( table ) {
					var stateOrder = [];
					
					table.el.find('> thead > tr > th[data-field]').each( function(i) {
						var fieldName = $(this).data('field');
					
						table.sortOrder[ fieldName ] = i;
						stateOrder[ i ] = fieldName;
					});
					
					pageObj.hideFieldPanelSorted = false;
					pageObj.hideFieldPanelOrder( stateOrder );
					
					$.post( ajaxURL, {
						saveParam: Runner.pages.constants.FORDER_PARAMS_TYPE, 
						data: JSON.stringify( stateOrder )
					}); 
				},
				clickDelay: 150,
				restoreState: columnNormalOrder,
				beforeStart: function( table ) {
					// not to interfer with resizable elements
					if ( $(".ui-resizable", $gridHorizonatal).is(":visible") ) {
						return false;
					}
					
					var $visibleDPreviewRows = $gridHorizonatal.find(".r-gridrow.bs-details-opened");

					$visibleDPreviewRows.each( function( i, item ) {
						var $dpButton = $(item).find("a[id^=details_]:first");
	
						if ( $dpButton.length ) {
							$dpButton.trigger("click");
							needClickAfterReorder.push( $dpButton );
						}					
					});
				},
				beforeStop: function( table ) {
					$.each( needClickAfterReorder, function( i, $dpButton ) {
						$dpButton.trigger("click");
					});
					
					needClickAfterReorder = [];
					
					pageObj.onReoderColumnsStop();
					
					var $widget = $gridHorizonatal.data("akottrDragtable");
					$widget.sortableTable.el.off( "mousemove.realdrag");
					
					if ( !dragged ) {
						/*var firstColumnIndex = $widget.originalTable.el.find('th[data-field]').first().index();
						
						$widget.originalTable.el.find('th[data-field]').eq( $widget.originalTable.startIndex - firstColumnIndex - 1 )
							.children().trigger( e );*/
							
						// fire click for initial mousedown target if delay doesn't help
						$( firstTarget ).trigger( jQuery.Event( "click", { ctrlKey: ctrlKey } ) );					
					}
					
					firstTarget	= undefined;
					ctrlKey = undefined;

					dragged = false;
					skipFirstDrag = false;
				},
				beforeMoving: function( table, sortableTable ) {
					sortableTable.el.on( "mousemove.realdrag", function(e) {
						if ( skipFirstDrag ) {
							dragged = true;
						} else {
							// IE may generate one mousemove on ul appearing
							skipFirstDrag = true;
						}
					});
				}
			});
			
			$gridHorizonatal.data('dragtable', true);
			
			this.onReoderColumnsStop();
			
			if ( !reInitColumnOrder ) {
				pageObj.hideFieldPanelOrder( columnOrder );
			}
		} 
	},
	
	refineColumnsOrderData: function( columnsOrder, $grid ) {
		if ( !Array.isArray( columnsOrder ) ) {
			return {};
		}
		
		var columnsNormalOrder = {},
			newColumns = [],
			currentColumnsOrder = [],
			refinedColumnsOrder = [],
			firstColumnIndex = $grid.find('th[data-field]').first().index();
			
		$grid.find('th[data-field]').each( function( idx, el ) {
			var fieldName = $(el).data('field');
			currentColumnsOrder.push( fieldName );
			
			if ( columnsOrder.indexOf( fieldName ) === -1 ) {
				newColumns.push( fieldName );
			}
		});
		
		columnsOrder.forEach( function( fieldName, pos ) {
			if ( currentColumnsOrder.indexOf( fieldName ) !== -1 ) {
				refinedColumnsOrder.push( fieldName );
			}
		})
			
		refinedColumnsOrder.concat( newColumns ).forEach( function( fieldName, pos ) {
			columnsNormalOrder[ fieldName ] = pos + firstColumnIndex;
		})
		
		return columnsNormalOrder;
	},
	
	/**
	 * init Columnt Reordering in Show/Hide field Panel
	 */
	initReorderingInSHPanel: function() {
		if ( !Runner.pages.PageSettings.getTableData(this.tName, "isAllowFieldsReordering") ) {
			return;
		}

		var pageObj = this,
			$list = $("#fildhidepanel" + this.id).find("ul.dropdown-menu"),
			ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType );
			
		$list.sortable({
			update: function(event, ui) {
				var newOrderData = [];

				$list.find('.ui-sortable-handle').each( function(i, el) {
					var fieldName = $(this).find("[data-field]").data('field');
					
					if ( typeof fieldName != "undefined" ) {
						newOrderData.push(fieldName);
					} 
				});
				
				$.post( ajaxURL, {
					saveParam: Runner.pages.constants.FORDER_PARAMS_TYPE, 
					data: JSON.stringify(newOrderData)
				});
				pageObj.initColumnReordering(newOrderData);
			},
			helper : 'clone',
		});
	},

	/**
	 * @param {?} columnOrder
	 */
	hideFieldPanelOrder: function(columnNormalOrder) {
		var $panelHideFieldBlock = $("#fildhidepanel" + this.id);

		if ( !this.hideFieldPanelSorted && $panelHideFieldBlock.length ) {
			$.each(columnNormalOrder, function(pos, fieldName) {
				$panelHideFieldBlock.find('input[data-field="' + fieldName + '"]').parents("li")
					.appendTo( $panelHideFieldBlock.find(".dropdown-menu") );
			});
		}

		this.hideFieldPanelSorted = true;
	},
	
	/**
	 * Inint Inline Add/Edit basing on page's settings
	 */
	initInline: function() {	
		if ( this.controlsMap.gridRows ) {
			this.initInlineEdit();
			this.initInlineAdd();
		}
	},

	/**
	 * @return {object}
	 */
	getInlineEditParams: function() {
		return {
			id: this.pageId,
			tName: this.tName,
			shortTName: this.shortTName,
			fNames: this.listFields,
			rows: this.controlsMap.gridRows,
			loadSettings: true,
			isOldLayout: this.oldLayout,
			spreadsheet: this.spreadsheet,
		}
	},
	
	/**
	 * Init Inline Edit
	 */
	initInlineEdit: function() {
		this.inlineEdit = new Runner.util.inlineEditing.InlineEdit( this.getInlineEditParams() );
		this.inlineEdit.init();
		
		this.inlineEdit.on( "afterSave", function( fieldsData, row ) {
			this.fireEvent( "afterInlineEdit", fieldsData, row );
		}, this );
		
		
		if ( this.spreadsheet ) {
			var gridRows = this.inlineEdit && this.inlineEdit.rows || this.controlsMap.gridRows;
			gridRows.forEach( this.initSpreadsheetEidtRow, this );

			this.inlineEdit.on( "afterSubmit", function( vals, fields, keys, id, resp, editPageObj, initialRow ) {
				// process edit page form submit
				var row = this.inlineEdit.getRowById( id );
				if ( row !== initialRow ) {
					// Inline edit rows were updated, eg by reload
					return;
				}
				
				resp.editFields = resp.editFields || [];
				
				resp.editFields.forEach( function( efName ) {
					var ctrl = Runner.controls.ControlStorage.byIdField( row.id, efName );
					if ( !ctrl ) {
						return;
					}
					
					var updContext = { setForSpreadsheet: true };
					
					// set server values without event triggering
					resp.controlValues.hasOwnProperty( efName ) 
						&& ctrl.safeToSetValue()
						&& ctrl.setValue( resp.controlValues[ efName ], false, updContext );

					ctrl.setDefaultValue();
					
					if ( ctrl.editFormat === Runner.controls.constants.EDIT_FORMAT_READONLY ) {
						ctrl.setDisplayValue( row.data[ efName ] );
					}
				});
				
				var fNames = this.listFields.filter( function( fName ) {
						return resp.editFields.indexOf( fName ) === -1;
					});
				
				this.inlineEdit.setValuesIntoSpans( row, fNames );
			}, this );
		}
	},
	
	/**
	 * @param {object} row
	 */
	initSpreadsheetEidtRow: function( row ) {
		var gridRow = new Runner.gridRow( row );
		
		this.inlineEdit.fireEvent( 
			'afterPageReady', 
			this, 
			this.proxy, 
			row.id, 
			row, 
			this.inlineEdit, 
			new Runner.AjaxRow( this, row ) 
		);
		
		// get row controls 
		var rowCtrls = Runner.controls.ControlStorage.byId( row.id );
		// set up behavior
		rowCtrls.forEach( function( ctrl ) {
			this.inlineEdit.updateItemTypeSpanState( ctrl.fieldName, row.id, true );

			ctrl.on( "editingresolved", function() {
					gridRow.fireEvent( "edited", this );
				}, this );
				
			ctrl.setDependencies( this.tName, row.id );
		}, this );
		
		gridRow.on( "edited", function( ctrl ) {
				this.inlineEdit.clearError( row );
				this.inlineEdit.saveRow( row );
			}, 
			this, { 
				buffer: 200
			});
			
		this.inlineEdit.updateRowLabels( row );	
	},
	
	/**
	 * @return {object}
	 */
	getInlineAddParams: function() {
		return {
			id: this.pageId,
			tName: this.tName,
			shortTName: this.shortTName,
			fNames: this.listFields,
			rows: this.controlsMap.gridRows,
			inlineEditObj: this.inlineEdit,
			loadSettings: true,
			isOldLayout: this.oldLayout,
			hideSaveButton: this.spreadsheet,
			hideRevertButton: this.spreadsheet,
			spreadsheet: this.spreadsheet,
		}
	},
	
	/**
	 * Init Inline Add
	 */
	initInlineAdd: function() {
		this.inlineAdd = new Runner.util.inlineEditing.InlineAdd( this.getInlineAddParams() );
		this.inlineAdd.init();
		
		this.inlineAdd.on( "afterSubmit", function( vals, fields, keys, id, resp ) {
			var detailsBricks = this.getBrickObjs('details_found'),
				tName, detTables, newRowData;
			
			if ( !detailsBricks.length ) {
				detailsBricks = this.getBrickObjs('vdetails_found');
			}
			
			$.each( detailsBricks, function(ind, brick) {
				$('.rnr-details_found_count', brick.elem[0]).each( function(jnd, detail) {
					var df = parseInt( $(detail).html(), 10 );
					$(detail).html( df ? df + 1 : '1' );
				});
				if ( !brick.visible() ) {
					brick.show();
				}
			});
			
			if ( !Runner.pages.PageSettings.getTableData(this.tName, "showRows") ) {
				var pageOfBricks = this.getBrickObjs('page_of');
				
				if ( !pageOfBricks.length ) {
					pageOfBricks = this.getBrickObjs('vpage_of');
				}
				
				$.each( pageOfBricks, function(ind, brick) {
					if ( !brick.visible()) {
						var bPgs = $('b', brick.elem[0]); 
						if ( $(bPgs).length ) {
							$.each( bPgs, function(jnd, page) {
								$(page).html('1');
							});
						} else {
							$('span[name^=pageOf]',brick.elem[0])
								.html(Runner.lang.constants.PAGE + ' 1 ' + Runner.lang.constants.PAGEMAX + ' 1');
						}	
						brick.show();
					}	
				});
				
				this.showBricks(['recsperpage', 'vrecsperpage', 'toplinks_print', 'toplinks_export', 'printpanel', 'bsfieldhidepanel']);
				$("#print_" + this.id).show();
				$("#export_" + this.id).show();
			}
			
			if (resp.noKeys === true) {
				return;
			}
			
			newRowData = {
				id: id,
				masterKeys: resp.detKeys,
				keys: keys,
			};
			
			for (tName in this.dpObjs) {
				this.dpObjs[ tName ].addRow( newRowData );
			}
			
			detTables = this.pageData.detailTables;
			
			for (tName in detTables) {
				$("#master_" + Runner.pages.PageSettings.getShortTName(tName) + "_" + id).show();
			}	
		}, this);
		
		
		this.inlineAdd.on( "afterSave", function( fieldsData, row ) {
			this.fireEvent( "afterInlineAdd", fieldsData, row );
		}, this );
		
		
		this.inlineAdd.on( "afterInit", function( pageObj, proxy, pageid, row ) {
			this.afterInitInlineAdd( row );
		}, this );
		
		if ( this.spreadsheet ) {
			this.inlineAdd.on( "createControls", function( row, rowControls ) {
				var gridRow = new Runner.gridRow( row );
				
				rowControls.forEach( function( ctrl ) {
					ctrl.on( "editingresolved", function() {
							gridRow.fireEvent( "edited", this );
						}, this );
				}, this );
				
				gridRow.on( "edited", function( ctrl ) {
						if ( row.submitting ) {
							// the row is being added
							row.editedAfterSubmit = row.editedAfterSubmit || {};
							// mark ctrl edited to save its value later
							row.editedAfterSubmit[ ctrl.fieldName ] = true;
							return;
						}
						
						row.fileFieldsCount = 0;
						
						row.tempSPValues = rowControls.reduce( function( obj, ctrl ) {
							obj[ ctrl.fieldName ] = ctrl.getValue();
							return obj;
						}, {} );

						this.inlineAdd.saveRow( row );
					}, 
					this, { 
						buffer: 200
					});
					
				this.inlineAdd.hideSaveCancelButtons( row );
			}, this );
			
			
			this.inlineAdd.on( "afterSubmit", function( vals, fields, keys, id, resp ) {
				var row = this.inlineAdd.getRowById( id ),
					ctrlValues = {}, 
					focusedName;
				
				Runner.controls.ControlStorage.byId( id ).forEach( function( ctrl ) {
					ctrlValues[ ctrl.fieldName ] = ctrl.getValue();
					if ( ctrl.spanContElem.find( document.activeElement ).length ) {
						focusedName = ctrl.fieldName;
					}
				});

				Runner.controls.ControlStorage.unregisterById( id );
				
				resp.editFields = resp.editFields || [];

				var fNames = this.listFields.filter( function( fName ) {
					return resp.editFields.indexOf( fName ) === -1;
				});
				this.inlineAdd.setValuesIntoSpans( row, fNames );

				if ( resp['nonEditable'] ) {
					row.isEditable = false;
					row.canDeleteRecord = false;
					row.canEditRecord = false;
					return;
				}
				
				// prep markup
				resp.editFields.forEach( function( efName ) {
					Runner.getFieldSpan( efName, id ) 
						.html( resp.htmlControls[ efName ] );
				});
				
				this.initControls( resp.spreadControlsMap[ this.tName ][ Runner.pages.constants.PAGE_EDIT ][ resp.pageId ] );
				
				this.initSpreadsheetEidtRow( row );
				
				resp.editFields.forEach( function( fName ) {
					var ctrl = Runner.getControl( row.id, fName );
					if ( ctrl.spEasyChangeable() && row.tempSPValues && row.tempSPValues[ fName ] !== ctrlValues[ fName ] ) {
						// trigger change event to save a value for add-cotrols edited after row adding submit
						ctrl.setValue( ctrlValues[ fName ], row.editedAfterSubmit && row.editedAfterSubmit[ ctrl.fieldName ] );
					}
				});
				
				delete row.editedAfterSubmit;
				
				
				if ( focusedName && resp.editFields.indexOf( focusedName ) !== -1 ) {
					var ctrl = Runner.getControl( row.id, focusedName );
					ctrl.setEndOfTextFocus();
				}
			}, this );
			
			
			if ( this.autoAddNewRecord ) {
				this.autoAddRecordId = this.inlineAdd.inlineAdd( true, true, !this.isBottomRowVisible() );
				
				this.inlineAdd.on( "afterSubmit", function( vals, fields, keys, id, resp ) {
					if ( id === this.autoAddRecordId ) {
						this.autoAddRecordId = this.inlineAdd.inlineAdd( true, true, true );
					}
				}, this );
			}
		}
	},
	
	/**
	 * Check if a bottom row is visible in view port 
	 * @return boolean
	 */
	isBottomRowVisible: function() {
		if ( !this.inlineAdd || !this.inlineAdd.rows.length ) {
			return true;
		}
		
		var bottomRow = this.inlineAdd.getBottomRow(),
			bottomTr = this.inlineAdd.getRowTrByContextId( bottomRow.contextRowId ).get(0);
			
		return Runner.util.isInViewport( bottomTr );
	},
	
	afterInlineAdd: function( fn ) {
		this.on('afterInlineAdd', fn, this);
	},
	
	afterInlineEdit: function( fn ) {
		this.on('afterInlineEdit', fn, this);
	},
	
	/**
	 * Reinintialize InlineAdd/InlineEdit functionality
	 */	
	reInitInline: function() {
		this.inlineButtonsIntialized = false;
		
		if ( this.inlineEdit ) {
			// destroy old controls
			this.inlineEdit.rows.forEach( function( row ) {
				// not to interfere with search controls
				Runner.controls.ControlStorage.byId( row.id )
					.forEach( function( ctrl ) {
						if ( ctrl.mode !== Runner.controls.constants.MODE_SEARCH ) {
							ctrl.unregister();
						}
					});
			});
			
			this.inlineEdit.reInit( this.controlsMap.gridRows );
			
			if ( this.spreadsheet ) {
				this.initControls( this.controlsMap );
				this.inlineEdit.rows.forEach( this.initSpreadsheetEidtRow, this );
			}
			
			this.inlineEdit.reinitButtons();
		}
		
		if ( this.inlineAdd ) {
			this.inlineAdd.reInit( this.controlsMap.gridRows );
			this.inlineAdd.reinitButtons();
			
			if ( this.autoAddNewRecord ) {
				this.autoAddRecordId = this.inlineAdd.inlineAdd( true, true, !this.isBottomRowVisible() );
			}
		}
		
		if ( this.inlineAdd || this.inlineEdit ) {
			( this.inlineAdd || this.inlineEdit ).toggleMassRecButt();
		}
	},	
	
	/**
	 * The details object 'afterShowDetails' event object
	 */	
	onAfterShowDetails: function() {
	},
	
	/**
	 * Initialize the page's buttons
	 */
	initButtons: function() {
		this.initDeleteButton();
		this.initUpdateSelectedButton();
		this.initAddButton();
		
		this.initRecordBlock();
		this.initExportLinks();
		this.initPrintLink();
		
		this.initAdvSearch();
		
		this.initAdminButton();
		
		this.initButtonGroupTogglers();
	},
	
	/**
	 * @param {Stirng} rowId
	 * @return {Boolean}
	 */
	findRow: function( rowId ) {
		return this.controlsMap.gridRows.find( function( row ) {
			return row.id == rowId;
		} );
	},


	/**
	 * Clear the current page's selection
	 */
	clearSelection: function() {
		var $selBoxes = this.getSelBoxes( this.id );
		
		$selBoxes.prop("checked", false);
		$( ".chooseAll" + this.id ).prop("checked", false);
		
		this.onAfterChangeCheckboxsState();
	},
	
	/**
	 * Initialize the 'Delete selected' button
	 */
	initDeleteButton: function() {
		$("[id=delete_selected" + this.id + "]")
			.prop("disabled", this.getSelectedRecords().length == 0 )
			.off("click")
			.on("click", this.deleteClickHandler.bind( this ));
	},
	
	deleteClickHandler: function( e ) {
		var pageObj = this;
		
		var deleteParams = {
			baseParams: Runner.apply( { a: 'delete' }, this.getStateParams() ),
			rows: this.getSelectedRecords(),
			message: Runner.lang.constants.DELETE_CONFIRM
		};
		
		if ( this.ajaxReloadOnDelete() ) {
			deleteParams.baseParams.id = this.pageId;
			deleteParams.baseParams = Runner.apply( deleteParams.baseParams, this.getBaseParams() );
		}
		
		var beforeDeletedRes = this.fireEvent( 'beforeDelete', deleteParams );
		if ( beforeDeletedRes === false || !deleteParams.rows.length ) {
			return false;
		}
	
		Runner.util.swalConfirm( deleteParams.message, this.deleteConfirmAction.bind( this, deleteParams ) );
		return false;
	},
	
	/**
	 * on confirm 'delete selected' handler
	 */
	deleteConfirmAction: function( deleteParams ) {
		if ( !this.isDefaultPage ) {
			deleteParams.baseParams.page = this.pageName;
		}
		var $selBoxes = $();
		deleteParams.rows.forEach( function( row, index ) {
			$selBoxes = $selBoxes.add( row.row.checkBox.get( 0 ) );
		});
		
		this.onSuccessDeleteSelected.$selBoxes = $selBoxes;

		var form = new Runner.form.BasicForm({
			standardSubmit: !this.ajaxReloadOnDelete(),
			submitUrl: Runner.getPageUrl( this.shortTName, this.pageType ) 
				+ ( this.ajaxReloadOnDelete() ? '?' + $selBoxes.serialize() : '' ),
			method: 'POST',
			id: this.id,
			baseParams: deleteParams.baseParams,
			addElems: this.ajaxReloadOnDelete() 
				? []
				: this.cloneFormElements( $selBoxes ),
			successSubmit: this.onSuccessDeleteSelected.bind( this ),
		});

		form.submit();
		form.destructor();	
	},
	
	ajaxReloadOnDelete: function() {
		return false;
	},
		
	onSuccessDeleteSelected: Runner.emptyFn,
	
	/**
	 * Initialize the 'Export selected' button
	 */
	initExportLinks: function() {
		var pageObj = this;
			
		$("[id=export_selected" + this.id + "]").off("click.export").on("click.export", function( e ) {
			var $selBoxes = pageObj.getSelBoxes( pageObj.id ),
				selection = $selBoxes.map( function( i, s ) { return s.value } ).get(),
				pageParams;
			
			if ( !selection.length ) {
				return false;
			}
			
			pageParams = Runner.pages.ExportPage.prototype.getOpenPageParams.call( pageObj, selection );
			if ( $(this).data("page") ) {
				pageParams.baseParams["page"] = $(this).data("page");
			}
			Runner.pages.PageManager.openPage( pageParams );
			return false;
		});
		
		$("[id=export_" + this.id + "]").off("click.export").on("click.export", function( e ) {
			var pageParams = Runner.pages.ExportPage.prototype.getOpenPageParams.call( pageObj, [] );
			if ( $(this).data("page") ) {
				pageParams.baseParams["page"] = $(this).data("page");
			}
			Runner.pages.PageManager.openPage( pageParams );
			return false;
		});
	},

	/**
	 * Initialize the 'Print selected' button
	 */	
	initPrintLink: function() {
		var pageObj = this,
			submitUrl = Runner.getPageUrl(this.shortTName, Runner.pages.constants.PAGE_PRINT);
			
		$("[id=print_selected" + this.id + "]").off("click").on("click", function(e) {
			var $selBoxes = pageObj.getSelBoxes( pageObj.id ),
				form;
			
			if ( !$selBoxes.length ) {
				return false;
			}
			
			form = new Runner.form.BasicForm({
				standardSubmit: true,
				submitUrl: submitUrl,
				target: '_blank',
				method: 'POST',
				id: pageObj.id,
				addElems: pageObj.cloneFormElements( $selBoxes ),
				baseParams: {a: "print"}
			});
			
			form.submit();
			form.destructor();
		});
	},

	/**
	 * Initialize the 'With selected...' and 'More...' buttons 
	 * that toggle the corresponding buttons groups
	 */
	initButtonGroupTogglers: function() {
		var pageObj = this,
			zIndex = Runner.genZIndexMax(),
			buttonClass = this.makeClassName( "button" ), 
			$selectButtonsGroup = $("#selectButtonsGroup" + this.id),
			$extraButtonsGroup = $("#extraButtonsGroup" + this.id);
		
		$("#extraButtonsToggler" + this.id).add( "#selectButtonsToggler" + this.id ).on('click', function(e) {
			return false;
		});
		
		$selectButtonsGroup.add( $extraButtonsGroup ).css("z-index", zIndex);
		
		$("." + buttonClass, $selectButtonsGroup).on("click", function(e) {
			$selectButtonsGroup.hide();
			pageObj.resetButtonGroupDisplayStyle( $selectButtonsGroup );
		});	

		$("." + buttonClass, $extraButtonsGroup).on("click", function(e) {
			$extraButtonsGroup.hide();
			pageObj.resetButtonGroupDisplayStyle( $extraButtonsGroup );
		});
	},
	
	/**
	 * Initialize 'sort by' drop
	 */
	initSortByDropdown: function() {
		var pageObj = this;
		
		$("[id='sortBy" + this.id + "']").off("change.sortby").on("change.sortby", function(e) {
			var reloadParams = pageObj.getSortingReloadParams();
					
			pageObj.reload( { 
				sortby: this.options[ this.selectedIndex ].value
			}, reloadParams );
		});
	},
	
	/**
	 * Initialize the 'Update Selected' button
	 */
	 initUpdateSelectedButton: function() {
		var pageObj = this,
		rowid,
		$selBoxes = this.getSelBoxes( this.id );
		
		if ( !this.hasItemType("update_selected") ) {
			return;
		}
		
		$("[id=update_selected" + this.id + "]").prop("disabled", !$selBoxes.length);
		
		$("[id=update_selected" + this.id + "]").off("click").on("click", function( e ) {
			var $selBoxes = pageObj.getOwnedSelBoxes( false, true ),
				selection = [],
				rowIds = [],
				form, pageParams,
				page = $(this).data("page");

			$selBoxes.each( function( i, s ) {
				selection.push( s.value );
				rowIds.push( $(s).closest("[data-record-id]").data("record-id") );
			});
			
			if ( !selection.length ) {
				return false;
			}
			
			if ( !pageObj.showEditInPopup( $(this) ) ) {
				var baseParams = { "selection[]": selection };
				
				if ( page ) {
					baseParams.page = page;
				}
				
				//	go to the Edit page
				form = new Runner.form.BasicForm({
					standardSubmit: true,
					submitUrl: Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_EDIT, "", "", pageObj.getStateParams() ),
					method: 'POST',
					baseParams: baseParams
				});
				
				form.submit();
			} else {
				//	open Edit page in popup
				pageObj.hideElement('message');
				
				pageParams = Runner.pages.EditSelectedPage.prototype.getOpenPageParams.call( pageObj, pageObj.id, selection, rowIds );
				if ( page ) {
					pageParams.baseParams.page = page;
				}
				
				Runner.pages.PageManager.openPage( pageParams );
			}
			
			return false;
		});
	},
	
	/**
	 * Initialize the 'Admin area' and 'Exit Admin area' buttons 
	 */
	initAdminButton: function() {
		$("[id=exitAdminArea" + this.id + "]").on("click", function(e) {
			window.location.href = Runner.getPageUrl("menu");
		});
		
		$("[id=adminArea" + this.id + "]").on("click", function(e) {
			window.location.href = Runner.getPageUrl("admin_rights_list");
		});
	},

	/**
	 * Clone form elements
	 * Now using only for clone elements from grid
	 * @param {jQuery object} elems
	 * @return {array}
	 * @intellisense
	 */
	cloneFormElements: function( $elems ) {
		var cloneElems = [];
		
		$elems.each( function(i, n) {
			if (n.type == "checkbox" && !n.checked) {
				return;
			}
			var cln = $('<input type=hidden>')
				.attr( 'name', $(n).attr('name') )
				.val( $(n).val() );
			
			cloneElems.push( cln );
		});
		
		return cloneElems;
	},	
	
	/**
	 * Remove all excessive containers
	 * Disable the page's grid and show the 'loading' message
	 * @param {DOM element} gridElem
	 */
	preparePageForReloading: function( gridElem ) {
		this.delHint();
		if ( Runner.pages.PageSettings.getTableData(this.tName, "displayLoading") ) {
			this.showLoadingBox();
		}
	},

	initGridTabs: Runner.emptyFn,

	/**
	 * Initialize sorting functionality
	 */
	initSorting: function() {
		var pageObj = this;
		
		this.listFields.forEach( function( listField ) {
			var $header = $( '[id="order_' + Runner.goodFieldName( listField ) + '_' + pageObj.id + '"]' );
			var $headerLabel = $header.find( '.text-label' );
			pageObj.initElementHint( $headerLabel, Runner.lang.constants.CTRL_CLICK );
			$headerLabel.on( "click", function( e ) {
				pageObj.reload( {
					orderby: e.ctrlKey ? 
						$header.data("multisort") + ';' + $header.data("order") : 
						$header.data("order")
				}, pageObj.getSortingReloadParams() );
			});
		});
	},
	
	/**
	 *
	 */
	getSortingReloadParams: function() {
		return this.getExtraPageReloadParams();
	},
	
	/**
	 *
	 */
	getExtraPageReloadParams: function() {
		var reloadParams;
		
		if ( this.bsResizeInitialized ) {
			reloadParams = { 
				resizableColumnsData: this.getResizableColumnsData() 
			};
		}
		
		return reloadParams;
	},
	
	/**
	 * Get sorting url 
	 * @param {object} event
	 * @param {string} pageUrl
	 * @return {string}
	 */	
	getSortUrl: function( event, $elem ) {
		return event.ctrlKey ? $elem.data('href') + ';' + $elem.data('multisort') : $elem.data('href');
	},
	
	
	/**
	 * For the List page with activated Inline edit functionality
	 * add the 'onbeforeunload' handler that will unlock all inline records
	 * if the locking is turned on for the table
	 */	
	initUnlockingOnPageUnload: function() {
		if ( !this.inlineEdit || !this.inlineEdit.isUseLocking ) {
			return;
		}
		
		Runner.pages.PageManager.addUnloadHn( function() {
			var rows = this.rows,
				i;
			
			for (i = 0; i < rows.length; i++) {
				if ( !rows[i].submitted ) {
					this.locking.unlockRecordInline( this.getKeysForLocking( rows[i] ), true );
				}
			}
		}, this.inlineEdit);
	},
	
	/**
	 * Bind handlers set up in the Click Actions feature
	 */	
	initGridClickActions: function() {
		if ( ["ADMIN_MEMBERS", "ADMIN_ADMEMBERS"].indexOf( this.tName.toUpperCase() ) !== -1 ) {
			return;
		}
		
		var layout = Runner.pages.PageSettings.getTableData( this.tName, "listGridLayout" ),
			clickActions = Runner.normalizeClickActionSettings( Runner.pages.PageSettings.getTableData( this.tName, 'clickActions' ) ),
			rowSelector = layout === Runner.pages.constants.HORIZONTAL_LAYOUT ? '[id^=gridRow]' : '.panel[data-record-id]';
		
		this.setClickActionHandler( clickActions.row, rowSelector );

		var fieldSelectors = [];
		for ( var f in clickActions.fields ) {
			var fieldSelector = rowSelector + ( layout === Runner.pages.constants.VERTICAL_LAYOUT ? ' ' : ' td' ) 
				+ '[data-field=' + Runner.goodFieldName( f ) + ']';
			
			if ( clickActions.fields[ f ].action !== 'noaction' ) {
				fieldSelectors.push( ( layout === Runner.pages.constants.VERTICAL_LAYOUT ? ' ' : ' td' ) 
					+ '[data-field=' + Runner.goodFieldName( f ) + ']' );
			}
			
			this.setClickActionHandler( clickActions.fields[ f ], fieldSelector );
		}
		
		if ( clickActions.row.action !== 'noaction' || !!fieldSelectors.length ) {	
			this.inlineAdd && this.inlineAdd.on( "afterSubmit", function( vals, fields, keys, id, resp ) {
				var row = this.inlineAdd.getRowById( id );
	
				if ( clickActions.row.action !== 'noaction' ) {
					if ( this.inlineAdd.listGridLayout !== Runner.pages.constants.HORIZONTAL_LAYOUT ) {
						row.row.find('.panel').addClass( 'bs-clickable' );
					} else {
						row.row.addClass( 'bs-clickable' );
					}
				}
				
				fieldSelectors.forEach( function( selector ) {
					row.row.find( selector ).addClass( 'bs-clickable' );
				});
			}, this );
		}
	},

	/**
	 *
	 */
	setClickActionHandler: function( settings, selector ) {
		if ( settings.action == 'open' ) {
			this.setOpenClickActionHandler( settings.openData, selector );
			return;
		}
		
		if ( settings.action == 'grid' ) {
			if ( settings.gridData.action == 'details' ) {
				this.setDetailsClickActionHandler( settings.gridData.table, selector );
			} else if ( settings.gridData.action == 'checkbox' ) {
				this.setRowCheckingActionHandler( selector );
			} else if ( settings.gridData.action == 'inlineedit' ) {
				this.setRowInlineEditActionHandler( selector );
			}
			
			return;
		}
		
		if ( settings.action == 'code' ) {
			this.setAjaxActionHandler( settings.codeData.snippet, selector );
		}
	},

	/**
	 * @param {number} rowId
	 * @return {object}
	 */
	getInlineRow: function( rowId ) {
		return this.getInlineEditObj().getRowById( rowId );
	},
	
	/**
	 * @param {function} snippet
	 * @param {string} selector
	 */
	setAjaxActionHandler: function( snippet, selector ) {
		var pageObj = this;
			
		this.gridElem.on('click', selector, function( e ) {
			var rowId = pageObj.getRowIdByClickElementMarkup( $( e.target ) ),
				row = pageObj.getInlineRow( rowId );
				
			if ( row && pageObj.inlineEdit && pageObj.inlineEdit.isRowEditing( row ) )
				return;

			if ( row && typeof Runner.pages.ajaxEvents[ snippet ] === 'function' ) {
				Runner.pages.ajaxEvents[ snippet ].call( pageObj, pageObj, pageObj.proxy, pageObj.id, row, pageObj.getInlineEditObj() );
			}
			
			if ( !$(this).is( '[id^=gridRow]' ) ) {
				e.stopPropagation();
			}
		});	
		
		this.gridElem.find( selector ).addClass( 'bs-clickable' );
	},

	/**
	 *
	 */
	setDetailsClickActionHandler: function( table, selector ) {
		var pageObj = this,
			dpObj = this.dpObjs ? this.dpObjs[ table ] : null,
			detTables = this.pageData.detailTables;
		
		if ( this.dashboard ) {
			return;
		}
		
		if ( !dpObj || !( dpObj instanceof Runner.util.details.ListTabbedPreview ) ) {
			if ( !detTables[ table ] ) {
				// there is no data to set the handler
				return;
			}
			//	always created tabbed preview
			dpObj = this.createDetailsObject( table, detTables[ table ], this.getDetailsTabId(table), Runner.pages.constants.DP_INLINE );
		}
		
		this.gridElem.on('click', selector, function( e ) {
			var rowId = pageObj.getRowIdByClickElementMarkup( $( e.target ) ),
				row = dpObj.getRowById( rowId );
			
			if ( !row || pageObj.inlineEdit && pageObj.inlineEdit.isRowEditing( row ) ) {
				return;
			}
			
			dpObj.linkClickHandler( row, e );
			
			if ( !$(this).is( '[id^=gridRow]' ) ) {
				e.stopPropagation();
			}
		});
		
		this.gridElem.find( selector ).addClass( 'bs-clickable' );
	},
	
	/**
	 * @param {jQuery object}
	 * @return {number}
	 */
	getRowIdByClickElementMarkup: function( $elem ) {
		if ( $elem.is("[data-gridlink]") || $elem.parents("[data-gridlink]").length ) {
			return -1;
		}

		/* don't do anything if click comes from the cell where there are details preview items */
		var $cell = $elem.closest( '[data-cellid][data-pageid=' + this.id + ']' );
		if ( $cell.has('[data-itemtype=details_preview]').length ) {
			return -1;
		}

		if ( !$elem.is("[id^='gridRow']") ) {	
			var $row = $elem.closest("[data-record-id]");
			
			if ( $row.length ) {
				return $row.data("record-id");
			}
		}
		
		$row = $elem.closest("[id^='gridRow']");
		
		if ( $row.length ) {
			return $row.attr("id").substr(7);
		}
		
		return -1;
	},
	
	/**
	 * @param {string} selector
	 */
	setRowCheckingActionHandler: function( selector ) {
		var pageObj = this;
		
		this.gridElem.on( "click", selector, function( e ) {
			var rowId = pageObj.getRowIdByClickElementMarkup( $( e.target ) ),
				$checkbox, checkbox;
				
			if ( rowId === -1 ) {
				return;
			}

			row = pageObj.getInlineRow( rowId );
			if ( row && pageObj.inlineEdit && pageObj.inlineEdit.isRowEditing( row ) ) {
				return;
			}
			
			$checkbox = $("input[type='checkbox'][id='check" + pageObj.id + "_" + rowId + "']", pageObj.pageCont);
			$checkbox.prop( "checked", !$checkbox.prop("checked") );
			
			pageObj.onAfterChangeCheckboxsState();
			
			if ( !$(this).is( '[id^=gridRow]' ) ) {
				e.stopPropagation();
			}
		});
	},
	
	/**
	 * @return {boolean}
	 */
	hasInlineEdit: function() {
		return !!this.inlineEdit;
	},
	
	/**
	 * @param {string} selector
	 */	
	setRowInlineEditActionHandler: function( selector ) {
		var pageObj = this;
		
		if ( !this.hasInlineEdit() ) {
			return;
		}
		
		this.gridElem.on( "click", selector, function( e ) {
			var rowId = pageObj.getRowIdByClickElementMarkup( $( e.target ) ),
				row;
				
			if ( rowId === -1 ) {
				return;
			}
	
			row = pageObj.getInlineRow( rowId );
			if ( row && !pageObj.inlineEdit.isRowEditing( row ) ) {
				pageObj.inlineEdit.inlineEdit( row );
			}

			if ( !$(this).is( '[id^=gridRow]' ) ) {
				e.stopPropagation();
			}
		});
	},
	
	/**
	 * @param {object} openData
	 * @param {string} selector
	 */
	setOpenClickActionHandler: function( openData, selector ) {
		var handlerFunc, 
			pageObj = this;
			
		if ( openData.page == 'view' ) {
			handlerFunc = this.getViewLinkHandler;
		} else if ( openData.page == 'edit' ) {
			handlerFunc = this.getEditLinkHandler;
		} else if ( openData.page == 'add' ) {
			handlerFunc = this.getAddLinkHandler;
		} else if ( openData.page == 'print' ) {
			handlerFunc = this.getPrintLinkHandler;
		} else if ( openData.page == 'details' ) {
			handlerFunc = this.getDetailsLinkHandler;
		} else if ( openData.page == 'url' ) {
			handlerFunc = this.getUrlLinkHandler;
		} else { 
			return;
		}
		
		this.gridElem.on('click', selector, function( e ) {
			var rowId = pageObj.getRowIdByClickElementMarkup( $( e.target ) ),
				row = pageObj.getInlineRow( rowId );
				
			if ( !row || openData.page == "edit" && !row.canEditRecord ) {
				return;
			}

			if ( row && pageObj.inlineEdit && pageObj.inlineEdit.isRowEditing( row ) ) {
				return;
			}
			
			if ( openData.page == 'url' ) {
				handlerFunc.call( pageObj, openData.url, openData.how ).call();	
			} else if ( openData.page == 'details' ) {
				if ( typeof pageObj.dpObjs[ openData.table ] === "undefined" ) {
					return;
				}
				
				handlerFunc.call( pageObj, row, openData.table, openData.how ).call();
			} else {
				handlerFunc.call( pageObj, row, openData.how ).call();	
			}
			
			if ( !$(this).is( '[id^=gridRow]' ) ) {
				e.stopPropagation();
			}
		});

		this.gridElem.find( selector )
			.filter( function( i ) {
				if ( openData.page == "edit" ) {
					var rowId = pageObj.getRowIdByClickElementMarkup( $( this ) ),
						row = pageObj.getInlineRow( rowId );
					
					return row && row.canEditRecord;
				} 
				
				if ( openData.page == 'details' ) {
					return typeof pageObj.dpObjs[ openData.table ] !== "undefined";
				}

				return true;
			})
			.addClass( 'bs-clickable' );
	},
	
	getRequestTotalsParams: function() {
		var baseParams = Object.assign( {a: 'calctotals'}, this.getStateParams() );	
		baseParams = Object.assign( {}, this.getBaseParams(), baseParams );
		
		if ( !this.isDefaultPage ) {
			baseParams.page = baseParams.page || this.pageName;
		}
		baseParams.id = this.id;
		
		return baseParams;
	},
});Runner.pages.ListPageAjax = Runner.extend( Runner.pages.ListPage, {
	/**
	 * Base params for ajax reload
	 * @type {object}
	 */
	ajaxBaseParams: null,
	
	
	constructor: function(cfg) {	
		Runner.pages.ListPageAjax.superclass.constructor.call( this, cfg );
		
		this.listFields = Runner.pages.PageSettings.getTableData( this.tName, 'listFields' );
		
		this.ajaxBaseParams = {
			mode: "ajax",
			id: this.pageId
		};
	},
	
	/**
	 * Check if page is allowed to control user session
	 * @return {boolean}
	 */
	isPageToControlSession: function() {
		return true;
	},

	/**
	 * @return {boolean}
	 */
	checkPageBarsToInit: function() {
		return true;
	},	
	
	setUrlParams: function() {
		if ( this.dashElement === undefined && this.pageData.urlParams && !Runner.Hash.oldbrowser() ) {
			Runner.Hash.set( this.pageData.urlParams, true );
		}	
	},
	
	initSearch: function() {
		Runner.pages.ListPageAjax.superclass.initSearch.call( this );
		
		this.searchController.ajaxSubmit = true;
		this.searchController.srchForm.standardSubmit = false; 
		this.searchController.srchForm.baseParams = this.ajaxBaseParams;
		
		this.searchController.srchForm.on("beforeSubmit", function( form ) {
			// delete page number
			delete this.searchController.srchForm.baseParams.goto;
			this.preparePageForReloading();
		}, this);

		this.searchController.on('afterSearch', function( respObj, srchController, srchForm ) {
			Runner.stopLoading( this.getGrayedElement() );
			this.pageReloadHn( respObj );
		}, this);
		
		this.searchController.srchForm.on('submitFailed', function() {
			Runner.stopLoading();
		}, this);
	},
	
	/**
	 * Init the inlineAdd/inlineEdit functionality
	 * The initialization code is invoked once when initial page is loaded	
	 */	
	initInline: function() {
		Runner.pages.ListPageAjax.superclass.initInline.call( this );
		this.initInline = Runner.emptyFn;
	},

	/**
	 * Initialize the 'Records Per Page' select element
	 */	
	initRecordBlock: function() {
		var pageObj = this;
		
		$("[id='recordspp" + this.id + "']").on("change", function(e) {
			var ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType ) + "?pagesize=" + $(this).val();
	
			pageObj.preparePageForReloading();
			Runner.runnerAJAX( ajaxURL, pageObj.ajaxBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
		});	
	},

	/**
	 * Initialize 'sort by' drop
	 */
	initSortByDropdown: function() {
		var pageObj = this;
		
		$("[id='sortBy" + this.id + "']")
			.off("change.sortby")
			.on("change.sortby", function(e) {
				var ajaxURL = Runner.pages.getUrl( pageObj.tName, pageObj.pageType ) 
					+ "?sortby=" + $(this).val();
				
				pageObj.preparePageForReloading();
				Runner.runnerAJAX( ajaxURL, pageObj.ajaxBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
			});
	},

	/**
	 * Initialize Grid Tabs Where functionality
	 */
	initGridTabs: function() {
		var pageObj = this;		
		var tabClickHandler = function( e ) {
			var $tab = $(this),
				$tabs = $tab.parents('ul').find('li'),
				$currentTab = $(this).parent();
				
			$tabs.removeClass('active');
			$currentTab.addClass('active');
			
			pageObj.preparePageForReloading();
			Runner.runnerAJAX( $tab.attr('href'), pageObj.ajaxBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
			return false;				
		};
		

		$( '.bsgrid_tabs [data-tabid][data-pageid=' + this.id + ']', this.pageCont )
			.on( 'click', tabClickHandler );
	},
	

	/**
	 * Initialize sorting functionality
	 */
	initSorting: function() {
		var pageObj = this;
		
		this.listFields.forEach( function( listField ) {
			var $header = $( '[id="order_' + Runner.goodFieldName( listField ) + '_' + pageObj.id + '"]' );
			var $headerLabel = $header.find( '.text-label' );
			pageObj.initElementHint( $headerLabel, Runner.lang.constants.CTRL_CLICK );
			$headerLabel.on("mousedown", function(e) {					
				var	url = pageObj.getSortUrl( e, $header );
					
				pageObj.preparePageForReloading();
				Runner.runnerAJAX( url, pageObj.ajaxBaseParams, pageObj.pageReloadHn.bind( pageObj ) );
				e.stopPropagation();
			});
		});
	},
	
	ajaxReloadOnDelete: function() {
		return true;
	},
	
	onSuccessDeleteSelected: function( respObj, formObj, fieldControls ) {
		this.pageReloadHn( respObj );
	},
	
	/**
	 * Add user-defined custom grid styles
	 * @param {string} styles
	 */
	addCustomCss: function( cssRules ) {
		var customCellsStyle = this.gridElem.parent().find(".rnr-cells-css");
		
		if ( customCellsStyle.length ) {
			customCellsStyle.html( cssRules );
			return;
		} 
		customCellsStyle = $('<style class="rnr-cells-css" type="text/css"> ' + cssRules + ' </style>');
		this.gridElem.parent().prepend( customCellsStyle );
	},
	
	/**
	 * @param {number} numberOfRecs
	 * @param {number} recPerPage
	 */
	updateMoreButtonsVisibility: function( numberOfRecs, recPerPage ) {		
		this.toggleElement("toplinks_print", numberOfRecs > 0);
		this.toggleElement("toplinks_export", numberOfRecs > 0);
		
		$("#print_" + this.id).toggle( numberOfRecs > 0 );
		$("#export_" + this.id).toggle( numberOfRecs > 0 );
		$("#printAll_" + this.id).toggle( numberOfRecs > recPerPage && recPerPage > 0 );

		$("#edit_selected" + this.id).toggle( numberOfRecs > 0 );
		$("#delete_selected" + this.id).toggle( numberOfRecs > 0 );
		$("#export_selected" + this.id).toggle( numberOfRecs > 0 );
	},
	
	/**
	 * @param {number} numberOfRecs
	 * @param {number} recPerPage
	 */
	updatePrintPanel: function( numberOfRecs, recPerPage ) {		
		this.toggleElement("printpanel", numberOfRecs > 0);
	},

	/**
	 * @param {number} numberOfRecs
	 */
	updateFieldHidePanel: function( numberOfRecs ) {		
		this.toggleElement("bsfieldhidepanel", numberOfRecs > 0);
	},

	/**
	 * @param string newPageContext
	 */
	updatePageMarkup: function( newPageContext ) {
		var itemTypes = [ 
			"details_found",
			"details_found_unknown",
			"page_size",
			"pagination",
			"sort_dropdown",
			"filter_panel",
			"save_search",
			"saved_searches",
			"update_selected", 
			"export_selected", 
			"delete_selected",
			"inline_save_all", 
			"inline_cancel_all", 
			"delete",
			"next_page",
		];
		
		itemTypes.forEach( function( itemType ) {
			var $currItems = this.layoutHelper.findItemType( itemType );
			var pageObj = this;
			
			$currItems.filter("[data-itemtype]").each( function( i, el ) {
				var $el = $(el);
				var itemId = $el.data("itemid");
				var $newItem = pageObj.layoutHelper.findItem( itemId, undefined, newPageContext )
					.filter("[data-itemtype]");
				
				if ( $newItem.length ) {
					pageObj.layoutHelper.toggleItem( itemId, pageObj.layoutHelper.elementVisible( $newItem ) );
					$el.replaceWith( $newItem );
				} else {
					pageObj.layoutHelper.toggleItem( itemId, false );
					$el.empty();
				}
			})
		}, this );
		
		this.getGridMessageElem()
			.replaceWith( this.getGridMessageElem( newPageContext ) );
		
		// replace grid
		this.layoutHelper.findForm("grid")
			.replaceWith( this.layoutHelper.findForm("grid", newPageContext) );
		
		// replace grid tabs
		if ( this.pageData.gridTabs ) {
			$( ".bsgrid_tabs.nav-tabs", this.pageCont ).html( this.pageData.gridTabs );
		}
	},
	
	/**
	 * The page's ajax reload handler
	 * @param {Object} respObj
	 */
	pageReloadHn: function( respObj, isNaved ) {
		Runner.stopLoading();
				
		if ( !respObj || !respObj.success ) {
			this.displayMessage('Submit failed', true, true);
			return;	
		}
		
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		
		var respControlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.id ];
		
		isNaved = isNaved === true || false;
				
		Runner.setIdCounter( respObj.idStartFrom );
		Runner.pages.PageSettings.addSettings( this.tName, respObj.settings, false );
		this.controlsMap.gridRows = respControlsMap.gridRows;
		this.controlsMap.controls = respControlsMap.controls;

		if ( this.bsResizeInitialized ) {
			this.resizableColumnsData = this.getResizableColumnsData();
		}
		
		this.destroyVideo();
		
		this.updatePageMarkup( respObj.html );
		
		this.toggleElement( "message", !this.controlsMap.gridRows.length || respObj.usermessage );
		
		this.updateMoreButtonsVisibility( respObj["numberOfRecs"], respObj["recPerPage"] );
		this.updatePrintPanel( respObj["numberOfRecs"], respObj["recPerPage"] );
		this.updateFieldHidePanel( respObj["numberOfRecs"] );

		this.onAfterChangeCheckboxsState();
		
		this.initGridElem();
		
		this.reinitGridCustomButtons();

		this.nextDetailsTabId = 0;
		
		if ( !this.useResize ) {
			this.initDetails();
			this.initPopupLinks();
			this.initSorting();
			this.reInitColumnReordering();
			this.initPagination();
			this.initFieldFilter();
		} else {
			// initResize invokes initForGrid -> initDetails, initPopupLinks, initSorting, reInitColumnReordering
			this.initResize(); 
		}	
		
		this.initHeaderCheckBox();	
		
		this.initDeleteButton();
		this.initExportLinks();
		this.initPrintLink();
		this.initSelectAll();
		this.initRecordBlock();
		this.initUpdateSelectedButton();

		this.initGridClickActions();
		this.initSortByDropdown();
		this.initGridTabs( true );

		this.initViewLinks();
		
		
		
		this.initRowReordering();
		
		this.searchController.usedSrch = respControlsMap.search && respControlsMap.search.usedSrch;
		if ( this.searchController.usedSrch ) {
			this.searchController.simpleSearchActive = Runner.pages.PageSettings.getTableData( this.tName, "simpleSearchActive" );
		}	
		
		// toggle simple search shwow_All|Reset_search button
		respControlsMap.search 
			&& this.searchController.toggleShowAll( respControlsMap.search.usedSrch );

		
		if ( respControlsMap.filters ) {
			this.searchController.filterContolsData = respControlsMap.filters.controls;
			this.searchController.initFilters();
		}
		
		if ( this.searchSavingEnabled ) {
			this.searchParamsLogger.initSearchSavingButtons();
			this.searchParamsLogger.initSavedSearchesList();
		}

		if ( !isNaved && this.pageData.urlParams ) {
			Runner.Hash.set(this.pageData.urlParams, false);
		}
				
		this.reInitInline();

		//add custom cells css rules
		this.addCustomCss( respObj['cellStyles'] );
		
		if ( this.mapManager && respControlsMap.gMaps && respControlsMap.gMaps.isUseGoogleMap ) {
			this.mapManager.init( respControlsMap.gMaps.mapsData );
		}
		
		if ( typeof respObj.viewControlsMap === "object" && respObj.viewControlsMap.controls ) {
			this.reInitViewControls( respObj.viewControlsMap.controls );
		}

		this.fireEvent( 'afterInit', this, this.proxy, this.pageId );
		this.fireEvent( 'afterPageReady', this, this.proxy, this.pageId );
	},
	
	/**
	 * Reinitialize view controls
	 * @param {array} viewControls
	 */ 
	reInitViewControls: function( viewControls ) {
		this.setupViewContols( viewControls, this.pageCont );	
	},

	getBaseParams: function() {
		return Object.assign( {}, this.ajaxBaseParams );
	},
	
	initScrollGridBody: Runner.emptyFn,
	
	initGridElem: function() {
		Runner.pages.ListPageAjax.superclass.initGridElem.call( this );
		// OPENMODE_PRELOADED_CONTAINER
		this.openContainer = this.gridElem;
	},
	
});
Runner.pages.ListPageDP = Runner.extend( Runner.pages.ListPage, {
	detCont: null,
	
	useChildCount: true,
	
	hideSaveButton: false,
	
	constructor: function( cfg ) {
		Runner.pages.ListPageDP.superclass.constructor.call(this, cfg);
		
		// page container must be DOM element!
		this.pageCont = cfg.pageCont || $('#detailPreview' + this.id)[0];

		this.addEvents("beforeSaveDetails", "afterSaveDetails", "afterDeleteDetails", "afterPageReady");
		this.baseParams = cfg.baseParams || { id: this.pageId };
		this.baseParams = Runner.apply( this.baseParams, this.getStateParams() );
		
		if ( !this.isDefaultPage ) {
			this.baseParams.page = this.baseParams.page || this.pageName;
		}

		this.spreadsheetOnList = !this.spreadsheet 
			&& this.pageData["spreadsheetOnList"];	
	},
	
	init: function() {
		Runner.pages.ListPageDP.superclass.init.call(this);
	
		if (this.beforeSaveDetails) {
			this.on({'beforeSaveDetails': this.beforeSaveDetails});
		}
		if (this.afterSaveDetails) {
			this.on({'afterSaveDetails': this.afterSaveDetails});
		}
		if (this.afterDeleteDetails) {
			this.on({'afterDeleteDetails': this.afterDeleteDetails});
		}
		if (this.saveFailed) {
			this.on({'saveFailed': this.saveFailed});
		}	
	},
	
	
	/**
	 *
	 */
	initSearch: function() {
		Runner.pages.ListPageDP.superclass.initSearch.call( this );
		
		this.searchController.srchForm.on("beforeSubmit", function( form ) {
			// delete page number
			delete this.searchController.srchForm.baseParams.goto;
			this.preparePageForReloading();
		}, this );		
		
		this.searchController.on( "afterSearch", function( respObj, srchController, srchForm ) {
			Runner.stopLoading();
			this.pageReloadHn( respObj );
			this.searchController.toggleShowAll( this.searchController.usedSrch );
			
			if ( Runner.suggests && $("#search_suggest").length > 0 && typeof Runner.suggests.DestroySuggestDiv != "undefined" ) {
				Runner.suggests.DestroySuggestDiv();
			}
		}, this );

		this.searchController.srchForm.on('submitFailed', function() {
			Runner.stopLoading();
		}, this );		

	},
	
	getSearchControllerConfig: function() {
		var cfg = Runner.pages.ListPageDP.superclass.getSearchControllerConfig.call( this );
		cfg.ajaxSubmit = true; //?
		cfg.formBaseParams = this.baseParams;
		return cfg;
	},
	
	initScrollGridBody: Runner.emptyFn,
	
	afterPageReady: Runner.emptyFn,
	
	setUrlParams: Runner.emptyFn,
	
	ajaxReloadOnDelete: function() {
		return true;
	},

	onSuccessDeleteSelected: function( respObj, formObj, fieldControls ) {
		this.fireEvent("afterDeleteDetails");
		if ( this.parId ) {
			Runner.pages.PageManager.getById( this.parId ).setRecountFlagForPopup();
		}
		this.pageReloadHn( respObj );
	},	
	
	/**
	 * Inintialize Inline Add/Edit basing on page's settings
	 */
	initInline: function() {
		Runner.pages.ListPageDP.superclass.initInline.call( this );
		this.initInline = Runner.emptyFn;
	},
	
	/**
	 * Init Inline Edit
	 */
	initInlineEdit: function() {
		if ( this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_EDIT 
				&& this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_VIEW
				&& this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_LIST ) {
			return;
		}
		
		Runner.pages.ListPageDP.superclass.initInlineEdit.call( this );	
		if ( !this.inlineEdit ) {
			return;
		}
		
		this.inlineEdit.on("beforeSubmit", function( row, inlineObj, formObj ) {
			return this.fireEvent("beforeSave", row, inlineObj, formObj);
		}, this);
		
		this.inlineEdit.on("rowsEdited", function( allVals, fields, allKeys, allRowIds, isEdited ) {
			if ( !this.inlineAdd || !this.inlineAdd.isRowsEditing() ) {
				this.fireEvent("afterSaveDetails", allVals, fields, allKeys, allRowIds, isEdited);
			}
		}, this);
		
		this.inlineEdit.on("submitFailed", function( respObj, hnScope, formObj, fieldControls ) {
			this.fireEvent("saveFailed", respObj, formObj, fieldControls);
		}, this);
		

		this.inlineEdit.on("validationFailed", function( formObj, fieldControls ) {
			this.validationFailed();
		}, this);	
	},

	/**
	 * @return {object}
	 */
	getInlineAddParams: function() {
		var params = Runner.pages.ListPageDP.superclass.getInlineAddParams.call( this );
		params.hideSaveButton = this.hideSaveButton;
		return params;
	},
	
	/**
	 * Init Inline Add
	 */
	initInlineAdd: function() {
		if ( this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_ADD 
			&& this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_EDIT 
			&& this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_LIST ) {
			return;
		}		
		
		Runner.pages.ListPageDP.superclass.initInlineAdd.call( this );
		
		if ( !this.inlineAdd ) {
			return;
		}
		
		this.inlineAdd.on("beforeSubmit", function( row, inlineObj, formObj ) {
			return this.fireEvent("beforeSave", row, inlineObj, formObj);
		}, this);
		
		this.inlineAdd.on("rowsEdited", function( allVals, fields, allKeys, allRowIds, isEdited, singleSaveBtnPressed ) {
			if ( !this.inlineEdit || !this.inlineEdit.isRowsEditing() || singleSaveBtnPressed ) {
				this.fireEvent("afterSaveDetails", allVals, fields, allKeys, allRowIds, isEdited);
			}
		}, this);
		
		this.inlineAdd.on("submitFailed", function( respObj, hnScope, formObj, fieldControls ) {
			this.fireEvent("saveFailed", respObj, formObj, fieldControls);
		}, this);
		
		this.inlineAdd.on("validationFailed", function( formObj, fieldControls ) {
			this.validationFailed();
		}, this);
		
		if ( this.baseParams.masterpagetype === Runner.pages.constants.PAGE_ADD 
			&& this.spreadsheetOnList && this.autoAddNewRecord ) {
			// detail list page spreadsheet mode + autoAddNewRecord are turned
			// skip sp mode on master add
			this.inlineAdd.on( "createControls", function( row, rowControls ) {
				var gridRow = new Runner.gridRow( row );
								
				rowControls.forEach( function( ctrl ) {
					ctrl.on( "editing", function() {
						gridRow.fireEvent( "edited" );
					}, { single: true }, true );
				}, this );
				
				gridRow.on( "edited", function( ctrl ) {
						row.modified = true;
						
						if ( this.autoAddRecordId === row.id ) {
							this.autoAddRecordId = this.inlineAdd.inlineAdd( true, true );
						}
						
						this.inlineAdd.showCancelButton( row );
					}, this, { 
						single: true
					});	
			}, this );
		}
	},

	/**
	 * A stub
	 * Details inlineAdd/InlineEdit rows' validationFailed event handler
	 */
	validationFailed: Runner.emptyFn,
	
	/**
	 * Save all detail records
	 */
	saveAll: function( mKeys ) {
		var saveAllInlines = false;
		
		if ( this.inlineEdit ) {
			if ( mKeys ) {
				Runner.apply( this.inlineEdit.baseParams, mKeys );
				this.inlineEdit.baseParams['mastertable'] = this.masterTName;
			}
			saveAllInlines = true;
			this.inlineEdit.saveAll();
		}
		
		if ( this.inlineAdd ) {
			if ( mKeys ) {
				Runner.apply( this.inlineAdd.baseParams, mKeys );
				this.inlineAdd.baseParams['mastertable'] = this.masterTName;
			}
			
			saveAllInlines = true;
			
			var modifiedOnly = this.baseParams.masterpagetype === Runner.pages.constants.PAGE_ADD 
				&& this.spreadsheetOnList && this.autoAddNewRecord;
			
			this.inlineAdd.saveAll( modifiedOnly );
		}
		
		return saveAllInlines;
	},
	
	/**
	 * Initialize sorting functionality
	 */	
	initSorting: function() {
		var pageObj = this;
		
		this.listFields.forEach( function( listField ) {
			var $header = $( '[id="order_' + Runner.goodFieldName( listField ) + '_' + pageObj.id + '"]' );
			var $headerLabel = $header.find( '.text-label' );
			$headerLabel.on("click", function(e) {
				pageObj.preparePageForReloading();
				
				Runner.runnerAJAX( pageObj.getSortUrl( e, $header ), pageObj.baseParams, function( respObj ) {
					pageObj.pageReloadHn( respObj );
				});
			});
		});
	},

	/**
	 * Initialize 'sort by' drop
	 */
	initSortByDropdown: function() {
		var pageObj = this,
			submitUrl = Runner.pages.getUrl( this.tName, this.pageType );
		
		$("[id='sortBy" + this.id + "']").off("change.sortby").on("change.sortby", function(e) {
			var ajaxURL = submitUrl + "?sortby=" + $(this).val();
			
			Runner.runnerAJAX( ajaxURL, pageObj.baseParams, function( respObj ) {
				pageObj.pageReloadHn.call( pageObj, respObj )
			});	
		});		
	},
	
	
	/**
	 *
	 */
	pageReloadHn: function( respObj ) {
		var parPageObj = Runner.pages.PageManager.getAt( this.masterTable, this.parId );
		Runner.stopLoading( this.getGrayedElement() );
		
		if ( !respObj.success ) {
			$("#message_block" + this.id).html("Submit failed!");	
		}
		
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		
		
		this.destroyVideo();
		Runner.setIdCounter( respObj.idStartFrom );
		Runner.pages.PageSettings.addSettings( this.tName, respObj.settings, false );
		
		respObj.bodyCont = respObj.html;
		if ( parPageObj.pageType == Runner.pages.constants.PAGE_EDIT 
			|| parPageObj.pageType == Runner.pages.constants.PAGE_ADD 
			|| parPageObj.pageType == Runner.pages.constants.PAGE_VIEW ) {
			Runner.pages.PageManager.putToContainer.call( this, respObj, this.detCont );
		} else {
			parPageObj.dpObjs[ this.tName ].revertTmpAboveGrid(this);
	
			Runner.pages.PageManager.putToDashContainer.call( this, respObj, this.detCont );
			
			parPageObj.dpObjs[ this.tName ].makeTmpAboveGrid(this);	
		}

		// set controlsMap
		if ( respObj.controlsMap ) {
			this.controlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.pageId ];
		}
		
		this.toggleElement( "message", !this.controlsMap.gridRows.length || respObj.delMess );
		
		this.onAfterChangeCheckboxsState();

		
		this.initGridElem();
		this.initHeaderCheckBox();
		
		if ( !this.useResize ) {
			this.initDetails();
			this.initPopupLinks();
			this.initSorting();
			this.initPagination();
			this.initFieldFilter();
			this.reInitColumnReordering();
			this.initShowHideColumnToggle();
		}
		this.initResize();
		
		this.initRowReordering();

		this.createCharts();

		if ( this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_VIEW
			&& this.baseParams.masterpagetype !== Runner.pages.constants.PAGE_EDIT ) {
			this.initSearch();
		} else {
			
			this.searchController.usedSrch = this.controlsMap.search && this.controlsMap.search.usedSrch;
			if ( this.searchController.usedSrch ) {
				this.searchController.simpleSearchActive = Runner.pages.PageSettings.getTableData( this.tName, "simpleSearchActive" );
			}	
						
			this.controlsMap.search &&
				this.searchController.toggleShowAll( this.searchController.usedSrch );
		}
		

		this.initSelectAll();
		this.reInitInline();
		this.initRunnerButtons();
		this.initGridTabs();
		this.initDeleteButton();
		this.initAddButton();
		this.initUpdateSelectedButton();
		
		this.initGridClickActions();
		
		this.initSortByDropdown();
					
		this.initCustomButtons();
		
		if ( this.hasMaps() ) {
			this.mapManager.init( this.controlsMap.gMaps.mapsData );
		}
	
		if ( respObj.viewControlsMap ) {
			this.setupViewContols( respObj.viewControlsMap[ this.tName ][ this.pageType ][ this.pageId ].controls, this.pageCont );
		}

		this.initViewLinks();
		
		this.fireEvent('afterPageReady', this, this.proxy, this.pageId);			
	},
	
	/**
	 * Details list page's destructor
	 */
	destructor: function() {
		if ( this.inlineEdit || this.inlineAdd ) {
			( this.inlineEdit || this.inlineAdd ).cancelAll();
		}
		Runner.pages.ListPageDP.superclass.destructor.call( this );
	},
	
	showAddInPopup: function( $link ) {
		return true;
	},
	
	showEditInPopup: function( $link ) {
		return true;
	},
	
	showViewInPopup: function( $link ) {
		return true;
	},	

	getGrayedElement: function() {
		return this.detCont[0];
	},
	
	/**
	 * validate detail records on master save
	 */
	validateRecords: function() {
		if ( this.baseParams.masterpagetype === Runner.pages.constants.PAGE_ADD 
			&& this.spreadsheetOnList && this.autoAddNewRecord ) {
			return this.inlineAdd.validate( true );
		}

		return ( this.inlineEdit ? this.inlineEdit.validate() : true ) 
			&& ( this.inlineAdd ? this.inlineAdd.validate() : true );		
	},
});
/**
 * The report page class
 */
Runner.pages.ReportPage = Runner.extend( Runner.pages.DataPageWithSearch, {
	
	pageType: Runner.pages.constants.PAGE_REPORT,

	/**
	 * The corresponding dashboard element's name
	 * It's contains reference to Runner.pages.DashboardPage object
	 * when the page is open on a dashboard
	 * @type {string}
	 */
	dashElement: null,	
	
	crossTable: false,
	
	reportFileName: "",
	
	$crossReportTable: null,
	
	colorTrTdHighlight: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.ReportPage.superclass.constructor.call( this, cfg );
		
		this.crossTable = Runner.pages.PageSettings.getTableData( this.tName, "crossTable" );
		this.reportFileName = Runner.getPageUrl( this.shortTName, this.pageType );
	},
	
	/**
	 * Initialize the page
	 */
	init: function() {
		if ( this.pageMode == Runner.pages.constants.REPORT_DASHBOARD ) {
			var dashElementData = this.dashboard.getElementByName( this.dashElement );
			if ( dashElementData.tabLocation == 'body'	) {
				this.$gridTabControl = $( ".bsgrid_tabs", this.pageCont );
			}				
			
			this.initGridTabs = Runner.pages.ListPageDash.prototype.initGridTabs;
		} else if ( this.pageMode == Runner.pages.constants.REPORT_DASHDETAILS ) {
			this.initGridTabs = Runner.pages.ChartPageDPDash.prototype.initGridTabs;
		}
		
		Runner.pages.ReportPage.superclass.init.call( this );
		this.fireEvent('afterInit', this, this.proxy, this.id);
		
		if ( this.pageMode !== Runner.pages.constants.REPORT_DASHBOARD ) {
			this.initDetails();
			this.initPopupLinks();
		}	
		
		this.initRecordBlock();
		
		this.initPageServiceLinks();
		this.initMaps();
		
		this.initCrossControls();
		
		this.processDashElemntActions();
	},
	
	/**
	 * Initialize the 'Records Per Page' select element
	 */	
	initRecordBlock: function() {
		if ( !this.dashboard ) {
			Runner.pages.ReportPage.superclass.initRecordBlock.call( this );
			return;
		}
		
		var pageObj = this;
		
		$("[id='recordspp" + this.id + "']").on("change", function(e) {
			var ajaxURL = pageObj.reportFileName  + "?pagesize=" + $(this).val(),
				params = Runner.apply( {id: pageObj.pageId}, pageObj.baseParams );
	
			params =  Runner.apply( params, pageObj.getStateParams() );
			pageObj.preparePageForReloading();
		
			Runner.runnerAJAX( ajaxURL, params, function( respObj ) {
				pageObj.pageReloadHn( respObj );
			});
		});			
	},
	
	/**
	 * Process dash-report actions
	 */
	processDashElemntActions: function() {
		var pageObj = this;
		
		if ( !this.dashboard ) {
			return;
		}
		
		if ( this.dashboard.getElementByName( this.dashElement ).major || !this.dashAction ) {
			this.updContext = this.dashboard.doReloadListAction( this.dashElement, {}, this.updContext );
		}
		
		this.finishDashElementActions();
		
		if ( this.dashboard.getElementByName( this.dashElement ).reload ) {
			if ( this.reloadTimerId ) {
				window.clearTimeout( this.reloadTimerId );
			}
			
			this.silentReloading = true;
			
			this.reloadTimerId = window.setTimeout( function() {
				pageObj.dashboard.doTimerReloadAction( pageObj.dashElement );
			}, 1000 * this.dashboard.getElementByName( this.dashElement ).reload );
		}	
	},
	
	/**
	 * @return {object}
	 */
	getSearchControllerConfig: function() {
		var cfg = Runner.pages.ReportPage.superclass.getSearchControllerConfig.call( this );
		if ( !this.crossTable ) {
			return cfg;
		}
		
		var crossParams = this.getCrosstableParams() || Runner.pages.PageSettings.getTableData( this.tName, "crossParams" );
		if ( crossParams ) {
			cfg.baseParams = Runner.apply( cfg.baseParams, crossParams );
		}
		
		return cfg;
	},
	
	/**
	 *
	 */
	initPageServiceLinks: function() {
		if ( this.dashboard ) {
			return;
		}
		
		this.initAdvSearch();
		this.initPrintFrLink();
		this.initPrintFriendly();
		this.initExcelLink();
		this.initWordLink();		
		this.initAdminButton();	
	},
	
	getPageGrid: function() {
		return this.findForm( 'grid' );
	},
	
	getAdvancedSearchUrl: function() {
		return Runner.getPageUrl( this.shortTName, "search", this.getParamsString() );
	},

	
	/**
	 * Old templates. Printer-friendly button
	 */
	initPrintFrLink: function() { 
		var pageObj = this;
		
		$("[id=print_" + this.pageId + "]").on("click", function(e) {
			//#14869
			window.open( Runner.pages.getUrl( pageObj.tName, "print", "", "", Runner.apply( pageObj.getStateParams(), pageObj.getCrosstableParams() ) ),
				'wPrint' );
			
			return false;
		});
	},
	
	/**
	 * Printer-friendly panel's handler
	 */
	printerFriendlyBtnHandler: function( pageName ) {
		if ( this.crossTable ) {
			//#14869
			window.open( Runner.pages.getUrl( this.tName, "print", "", "", Runner.apply( this.getStateParams(), this.getCrosstableParams() ) ),
				"_blank" );

			return;
		} 
		
		Runner.pages.ReportPage.superclass.printerFriendlyBtnHandler.call( this, pageName );
	},
	
	/**
	 * Initialize excel-export button
	 */
	initExcelLink: function() {
		if ( !this.crossTable ) {
			return;
		}
		
		var pageObj = this;
		
		$("[id=export_to_excel" + this.pageId + "]").on("click", function(e) {
			// #14869
			window.location.href = Runner.pages.getUrl( pageObj.tName, "print", "", "", 
				Runner.apply( pageObj.getStateParams(), pageObj.getCrosstableParams() ) ) + "&all=1&format=excel";
			return false;
		});
	},
	
	/**
	 * Initialize word-export button
	 * @deprecated ?	 
	 */	
	initWordLink: function() {
		if ( !this.crossTable ) {
			return;
		}
		
		var pageObj = this;
		
		$("[id=export_to_word" + this.pageId + "]").on("click", function(e) {
			// #14869
			window.location.href = Runner.pages.getUrl( pageObj.tName, "print", "", "", 
				Runner.apply( pageObj.getStateParams(), pageObj.getCrosstableParams() ) ) + "&all=1&format=word";
			return false;
		});
	},
	

	/**
	 * Initialize the 'Admin area' button 
	 */
	initAdminButton: function() {		
		$("[id=adminArea" + this.id + "]").on("click", function(e) {
			window.location.href = Runner.getPageUrl("admin_rights_list");
		});
	},
	
	/**
	 * Init the Google maps functionality using the Runner.GmapLoader instance
	 */
	initMaps: function() {
		if ( !this.hasMaps() ) {
			return false;
		}
		
		Runner.globalGmapLoader.onLoad( function() {		
			this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps );		
			this.mapManager.init();
			
			if ( this.isPageInTab() ) {
				Runner.pages.PageManager.onTabShown( this, this.mapManager.updateLittleMaps.bind( this.mapManager ) );
			}			
		}, this );
	},
	
	/**
	 * Initialize crosstable controls
	 */
	initCrossControls: function() {
		if ( !this.crossTable  ) {
			return;
		}
		
		this.$crossReportTable = $( '#crossTable' + this.id );
		
		this.initHighlightColor();
		this.initCrossTableGroupSelection();
		this.initCrossTableOperationSelection();
		this.initCrossDataFieldSelection();		
		this.initCrossHiglighting();		
		this.initCrossHover();
		this.initCrossHeaders();
	},
	
	initHighlightColor: function () {
		var $tempElem = $('<span class="highlightColor"></span>');

		$tempElem.insertBefore( this.$crossReportTable );
		this.colorTrTdHighlight = $tempElem.css("background-color");
		$tempElem.remove();
	},

	/**
	 * Reload the crosstab report page when a group field is changed
	 */
	initCrossTableGroupSelection: function() {
		var pageObj = this;
		
		if ( this.dashboard
			&& ( this.pageMode === Runner.pages.constants.REPORT_DASHBOARD || this.pageMode === Runner.pages.constants.REPORT_DASHDETAILS ) ) {
			this.initDashCrossTableGroupSelection();
			return;
		}
		
		var stateParams = this.getStateParams();
		var stateParamsString = Object.keys( stateParams ).map( function( key ) {
				return key + '=' + encodeURIComponent( stateParams[ key ] );
			}).join('&');		
		
		stateParamsString = stateParamsString ? "&" + stateParamsString : "";
		
		$("#select_group_x" + this.id).on("change", function(e) {
			window.location.href = pageObj.reportFileName + "?" + pageObj.getParamsString( { axis:'x' } ) + stateParamsString;
			return false;
		});
		
		$("#select_group_y" + this.id).on("change", function(e) {
			window.location.href = pageObj.reportFileName + "?" + pageObj.getParamsString( { axis:'y' } ) + stateParamsString;
			return false;
		});			
	},
	
	/**
	 * Reload the crosstab report page on the dash when a group field is changed
	 */	
	initDashCrossTableGroupSelection: function() {
		if ( !this.crossTable || !this.dashboard 
			|| ( this.pageMode !== Runner.pages.constants.REPORT_DASHBOARD
				&&  this.pageMode !== Runner.pages.constants.REPORT_DASHDETAILS ) ) {
			return;
		}
		
		var pageObj = this,
			reportDashElems = this.dashboard.getDashElements( this.tName, Runner.pages.constants.DASHBOARD_REPORT, this.dashElement );
		
		$("#select_group_x" + this.id).on("change", function(e) {
			var additionalParams = {
					baseParams: {
						criteria: pageObj.baseParams.criteria,
						q: pageObj.baseParams.q
					}
				};
				
			additionalParams.baseParams = Runner.apply( additionalParams.baseParams, pageObj.getCrosstableParams() );
			additionalParams.baseParams = Runner.apply( additionalParams.baseParams, pageObj.getStateParams() );
			
			pageObj.dashboard.loadPage( reportDashElems[0], additionalParams );			
		});
		
		$("#select_group_y" + this.id).on("change", function(e) {
			var additionalParams = {
					baseParams: {
						criteria: pageObj.baseParams.criteria,
						q: pageObj.baseParams.q
					}
				};
			
			additionalParams.baseParams = Runner.apply( additionalParams.baseParams, pageObj.getCrosstableParams() );
			additionalParams.baseParams = Runner.apply( additionalParams.baseParams, pageObj.getStateParams() );
			
			pageObj.dashboard.loadPage( reportDashElems[0], additionalParams );
		});	
	},

	/**
	 * Initialize group function selector 
	 */
	initCrossTableOperationSelection: function() {
		var pageObj = this,
			selector = "#group_func" + this.id;
	
		$( selector ).on("change", function(e) {
			pageObj.refreshCrosstable();
			return false;			
		});		
	},
	
	/**
	 * Initialize data field selector
	 */
	initCrossDataFieldSelection: function() {		
		var pageObj = this;
		
		$( "#select_data" + this.id ).on("change", function(e) {
			pageObj.refreshCrosstable();
			return false;
		}); 
	},

	/**
	 * AJAX-like update of a crosstable data-grid 
	 */
	 refreshCrosstable: function() {
		if ( !this.crossTable ) {
			return;
		}
		
		var pageObj = this,
			postParams = {
				id: this.id,
				crosstable_refresh: true,
				rndVal: Math.random()
			};
			
		postParams = Runner.apply( postParams, this.getCrosstableParams() );	
		postParams = Runner.apply( postParams, this.getStateParams() );	
		pageObj.searchController.srchForm.baseParams = this.getCrosstableParams(); // ? update search report params 
			
		if ( this.dashboard ) {
			postParams.mode = "dashreport";
			postParams.table = this.dashboard.tName;
		}	
	
		Runner.runnerAJAX( this.reportFileName, postParams, function( response ) {
			var colsSummary = response["columnSummary"]["data"];
		
			$.each( response["rowsInfo"], function( idx, rowInfo ) {
				if ( isNaN(idx) ) {
					return;
				}
				
				var record = rowInfo["row_record"]["data"], j;
				
				for (j = 0; j < record.length; j++) {
					$("#" + record[j]["id_data"] + "_" + pageObj.id)
						.html( record[j]["row_value"] )
						.removeClass( "r-field-text r-field-number rnr-field-crossdate" )
						.addClass( response["dataClass"] );
				}
				
				$("#" + rowInfo["id_row_summary"] + "_" + pageObj.id )
					.html( rowInfo["row_summary"] )
					.removeClass( "r-field-text r-field-number rnr-field-crossdate" )
					.addClass( response["dataClass"] );						
			});
			
			$.each( colsSummary, function( idx, colSummary ) {
				$("#" + colSummary["id_col_summary"] + "_" + pageObj.id )
					.html( colSummary["col_summary"] )
					.removeClass( "r-field-text r-field-number rnr-field-crossdate" )
					.addClass( response["dataClass"] );					
			});
			
			$("#id_total_summary_" + pageObj.id)
				.html( response["totalSummary"] )
				.removeClass( "r-field-text r-field-number rnr-field-crossdate" )
				.addClass( response["dataClass"] );					;
			
			$("#totals1_" + pageObj.id).html( response["totalsName"] );
			$("#totals2_" + pageObj.id).html( response["totalsName"] );
			
			$("#group_func" + pageObj.id).replaceWith( response["groupFuncCtrl"] );
			
			pageObj.initCrossTableOperationSelection();
			pageObj.reinitCrossHeaders();
			
			if ( !pageObj.dashboard && pageObj.pageMode !== Runner.pages.constants.MODE_LIST_DETAILS ) {				
				var crossParams = pageObj.urlEncodeParams( pageObj.getCrosstableParams() );
				Runner.Hash.set( crossParams, true );	
			}
		});
	},
	
	/**
	 * @return String
	 */
	getParamsString: function( additionalParams ) {
		if ( !this.crossTable ) {
			return "";
		}
	
		var prms = this.getCrosstableParams( additionalParams );
		if ( !prms ) {
			return  "";
		}
		
		return Object.keys( prms ).map( function( key ) {
				return key + '=' + encodeURIComponent( prms[ key ] );
			}).join('&');		
	},
	
	/**
	 * Get a current crosstable controls' state
	 * @return {object}
	 */
	getCrosstableParams: function( additionalParams ) {
		var $groupX = $("#select_group_x" + this.id), 
			$groupY = $("#select_group_y" + this.id),
			params, xType, yType, grfunc_value, $grfunc;
			
		if ( !this.crossTable || !$groupX.length || !$groupY.length ) {
			return null;
		}	
	
		params = {
			x: $groupX.val(),
			y: $groupY.val(),
			data: $("#select_data" + this.id).val()				
		};
		
		Object.keys( additionalParams || {} ).forEach( function(k) {
			params[k] = additionalParams[k];
		});
		
		xType = $groupX.find(":selected").data("xtype");
		yType = $groupY.find(":selected").data("ytype");
		
		if ( xType ) {
			params.xtype = xType;
		}
		
		if ( yType ) {
			params.ytype = yType;
		}
		

		params.op = $("#group_func" + this.id).val();
		return params;
	},
	
	/**
	 * 
	 */
	initCrossHiglighting: function() {
		var pageObj = this,
			controls = [
				{ selector: '#select_data' + this.id, classname: 'bs-cross-highligtdata' }, 
				{ selector: '#group_func' + this.id, classname: 'bs-cross-highligtdata' }, 
				{ selector: '#select_group_y' + this.id, classname: 'bs-cross-highligtrowheaders' }, 
				{ selector: '#select_group_x' + this.id, classname: 'bs-cross-highligtcolheaders' }, 
			];
		
		jQuery.each( controls, function( idx, item ) {		
			var $itemEl = $( item.selector )
				.on('click', function(e) {
					$itemEl.data( 'opened', !$itemEl.data('opened') );
				})
				.on('blur', function(e) {
					$itemEl.data( 'opened', false );
					pageObj.$crossReportTable.removeClass( item.classname );
				})
				.hover(function() {
				    	pageObj.$crossReportTable.addClass( item.classname );
					}, function() {
				    	pageObj.$crossReportTable.toggleClass( item.classname, $itemEl.data('opened') );
					}
				);
		});
	},
	
	/**
	 * 
	 */
	initCrossHover: function() {
		var pageObj = this,
			styleElemId = "crossHighlightcol" + this.id;

		if ( !$("style#" + styleElemId).length ) {
			$("<style id='" + styleElemId + "'></style>").insertBefore( this.$crossReportTable );
		}

		this.$crossReportTable.find("td:not(.bs-cross-rowheadercell),th:not(.bs-cross-xsel):not(:first-child)")
			.on('mouseenter', function(e) {
				var curColIndex = e.target.cellIndex + 1;
				
				$("style#" + styleElemId)
					.html('.bs-crosstab[data-highlightcol="' + curColIndex + '"] > * > tr > *:not(.bs-cross-xsel):nth-child(' + curColIndex + '){background: ' + pageObj.colorTrTdHighlight + ';}');				
				
				pageObj.$crossReportTable.attr('data-highlightcol', curColIndex);
			})
			.on('mouseleave', function(e) {
				pageObj.$crossReportTable.removeAttr('data-highlightcol');
			});
	},
	
	/**
	 * @param Boolean headStabilize
	 */
	stabilizeCrossDimensions: function( headStabilize ) {
		var pageObj = this,
			colwidths = [], colheights = [], headheights=[];
			tWidth = this.$crossReportTable.width();
			
		headStabilize = headStabilize === undefined ? true : headStabilize;
			
		//	set cell widths and heights where needed
		this.$crossReportTable.find('tbody > tr:first-child > td').each( function( i, el ) {
			colwidths.push( $(el).width() );
		});
		
		this.$crossReportTable.find('tbody > tr > td:first-child').each( function( i, el ) {
			colheights.push( $(el).height() );
		});
		
		this.$crossReportTable.find('thead > tr > th:first-child').each( function( i, el ) {
			headheights.push( $(el).height() );
		});

		this.$crossReportTable.find('tbody > tr:first-child > td').each( function( i, el ) {
			$(el).width( colwidths[i] );
		});
		
		this.$crossReportTable.find('thead > tr:nth-child(2) > th').each( function( i, el  ) {
			$(el).width( colwidths[i] );
			if ( headStabilize ) {
				$(el).height( headheights[1] );
			}
		});
		
		this.$crossReportTable.find('thead > tr:first-child > th:first-child').width( colwidths[0] );
		if ( headStabilize ) {
			this.$crossReportTable.find('thead > tr:first-child > *').height( headheights[0] );
		}

		this.$crossReportTable.find('tbody > tr > td:first-child').each( function( i, el  ) {
			$(el).height( colheights[i] );
			$(el).width( colwidths[0] );
		});
		
		this.$crossReportTable.find('tbody > tr > td:nth-child(2)').each( function( i, el  ) {
			$(el).height( colheights[i] );
		});
		
		this.$crossReportTable.width( tWidth );
		this.$crossReportTable.find('tbody, thead').width( tWidth );
	},
	
	/**
	 *
	 */
	reinitCrossHeaders: function() {},

	/**
	 * 
	 */
	initCrossHeaders: function() {	
		if ( this.dashElement || !this.$crossReportTable || !this.$crossReportTable.length ) {
			return;
		}
		
		var pageObj = this, 
			$thead = this.$crossReportTable.find('thead'),
			attachThead, detachThead, attachCol, detachCol,updateColPosition, updateHeadPosition,
			theadAttached = true,
			colAttached = true,
			$dummy,
			$win = $(window),
			$firstCol = this.$crossReportTable.find('tbody > tr > td:first-child'),
			$testCell = $($firstCol[0]),
			headBottom,
			lastScrollPos = { top:0, left:0 },
			$theadFirst = this.$crossReportTable.find( 'thead > tr > th:first-child' ),
			$xSelectCell = this.$crossReportTable.find( 'thead > tr:first-child > th:not(:first-child)' ),
			oldHeadFirstWidth,
			$firstDataRow = this.$crossReportTable.find('tbody > tr:first-child > td:not(:first-child)'),
			$colHeaders = this.$crossReportTable.find('.bs-cross-colheadercell'),
			$firstDataCell = this.$crossReportTable.find('tbody > tr:first-child > td:nth-child(2)'),
			$headDummy, $headDummy2, attachHeadDummy, detachHeadDummy,
			bgColor,
			bgColor2  = $(".table-striped > tbody > tr:nth-of-type(2n+1):first-child").css('background-color');
			
		//	find underlying color and create a style
		$thead.parents().each( function( i, el ) {
			bgColor = $(el).css('background-color');
			
			if ( bgColor != '' && bgColor != 'transparent' && bgColor!='none' && bgColor!='rgba(0, 0, 0, 0)' ) {
				return false;
			}
		});		
		
		$('<style>.bs-detachedcolumn.bs-detachedcolumn > tbody > tr:nth-of-type(2n+1) > td.backDummy { background-color:' + bgColor2 + ';}.bs-detachedcolumn.bs-detachedcolumn > tbody > tr:nth-of-type(2n) >  td.backDummy { background-color:' + bgColor + ';}</style>')
			.insertBefore( this.$crossReportTable );
			
		this.stabilizeCrossDimensions();
		
		$headDummy = $("<th></th>")
			.css( 'border-right-width', '0px')
			.css( 'width', $firstCol.eq(0).css('width') )
			.css( 'height', $xSelectCell.outerHeight() + 'px' );

		$headDummy2 = $("<th><select class='form-control'></select></th>")
			.css( 'display', 'block')
			.css( 'width', $theadFirst.eq(1).css('width') )

		$theadFirst.eq(1).height($theadFirst.eq(1).height() + 1 );

		attachHeadDummy = function() {
			$theadFirst.eq(0).before( $headDummy );
			$firstCol.each(function() {
				$("<td class='backDummy'></td>").css('height', $(this).outerHeight()).css('width', $(this).outerWidth()).insertAfter($(this));
			});			
			$headDummy2.insertAfter($('.bs-cross-colheaders th:first-child',pageObj.$crossReportTable));
		};
		
		detachHeadDummy = function() {
			$headDummy.detach();
			$headDummy2.detach();
			$(".backDummy", this.$crossReportTable).remove();
		};
		
		headBottom = $thead.offset().top + $thead.height();
		
		attachThead = function() {
			$thead.css('position', 'static' );
			theadAttached = true;			
			$dummy.remove();
			headBottom = $thead.offset().top + $thead.height();
		};
		
		detachThead = function() {
			var height = $thead.height();
			$dummy = $('<div></div>')
				.height( height )
				.insertBefore( pageObj.$crossReportTable );
			$thead.css( 'position', 'fixed' )
				.css( 'background', bgColor )
				.css( 'z-index', 10 )
				.css( 'top', 0 );
			theadAttached = false;
			updateHeadPosition();
			headBottom = $dummy.offset().top + $dummy.height();
		};
		
		updateColPosition = function() {
			var scrollTop = $win.scrollTop();
			$firstCol.each( function( i, el ) {
				$td = $(el);
				var topPos = ($td.next().next().offset().top - scrollTop) + 'px'
				$td.css('top', topPos);
				$('.backDummy', $td.parent()).css('top', topPos );
			});
		};

		updateHeadPosition = function() {		
			if ( theadAttached && !colAttached ) {
				//	horizontal position
				$thead.css('left', ( -$win.scrollLeft() ) + 'px' );
				$theadFirst.css( 'left', '0' );
				$xSelectCell.css('left', ( $theadFirst.eq(0).outerWidth() ) + 'px');
				
				//	vertical position
				if ( pageObj.$crossReportTable.offset().top <= $win.scrollTop() ) {
					$theadFirst.eq(0).css( 'top', '0' );
					$xSelectCell.css( 'top', '0' );
					$theadFirst.eq(1).css( 'top', ($colHeaders.eq(0).offset().top - $win.scrollTop()) + 'px' );
				} else {
					$theadFirst.eq(0).css( 'top', ( pageObj.$crossReportTable.offset().top - $win.scrollTop() ) + 'px' );
					$xSelectCell.css( 'top', ( pageObj.$crossReportTable.offset().top - $win.scrollTop() ) + 'px' );
					$theadFirst.eq(1).css( 'top', ( $colHeaders.eq(0).offset().top - $win.scrollTop() ) + 'px' );
				}				
			} else if (!theadAttached ) {
				$thead.css('left', ( pageObj.$crossReportTable.offset().left - $win.scrollLeft() ) + 'px' );
				//	horizontal position
				if ( pageObj.$crossReportTable.offset().left <= $win.scrollLeft() ) {
					$theadFirst.css( 'left', '0' );
					$xSelectCell.css('left', ( $theadFirst.eq(0).outerWidth() ) + 'px');
				} else {
					$theadFirst.css( ( pageObj.$crossReportTable.offset().left - $win.scrollLeft() ) + 'px' );
					$xSelectCell.css('left', ( $theadFirst.eq(0).outerWidth() ) + 'px');
				}
				//	vertical position
				$theadFirst.eq(0).css( 'top', '0' );
				$xSelectCell.css( 'top', '0' );
				$theadFirst.eq(1).css( 'top', ($colHeaders.eq(0).offset().top - $win.scrollTop()) + 'px' );			
			}
		};
		
		attachCol = function() {			
			detachHeadDummy();

			pageObj.$crossReportTable.removeClass('bs-detachedcolumn');
			$firstCol.css('position', 'static');
			$theadFirst.css('position', 'static').css( 'left', null );
			$xSelectCell.css('position', 'static').css( 'left', null );
			
			$theadFirst.css('width', oldHeadFirstWidth );
			$thead.css('left', pageObj.$crossReportTable.offset().left + 'px' );

			colAttached = true;
		};

		detachCol = function() {		
			$firstCol.css('position', 'fixed')
				.css('left', 0 );

			pageObj.$crossReportTable.addClass('bs-detachedcolumn');
			oldHeadFirstWidth = $theadFirst.eq(0).css('width');
			
			$theadFirst.css('position', 'fixed');
			attachHeadDummy();
			$theadFirst.css('width', $firstCol.css('width') );
			$theadFirst.css('background', bgColor );
			$xSelectCell.css('position', 'fixed');
			updateHeadPosition();
			updateColPosition();
			colAttached = false;
		};
		
		this.reinitCrossHeaders = function() {
			if ( !theadAttached )
				attachThead();
			if ( !colAttached )
				attachCol();

			pageObj.stabilizeCrossDimensions(false);
			$(window).trigger("scroll");
		}
	
		$win.on('scroll', function() {
			var top = $win.scrollTop(), 
				left = $win.scrollLeft();
				
			//	attach or detach
			if ( theadAttached ) {
				if ( top >= pageObj.$crossReportTable.offset().top ) {
					detachThead();
				}
			} else {
				if ( top < $dummy.offset().top ) {
					attachThead();
				}
			}
			
			if ( colAttached ) {
				if ( left >= pageObj.$crossReportTable.offset().left ) {
					detachCol();
				}
			} else {
				if ( left < pageObj.$crossReportTable.offset().left ) {
					attachCol();
				}
			}
			
			if ( !colAttached && lastScrollPos.top != top ) {
				updateColPosition();
				updateHeadPosition();
			}
			
			if ( lastScrollPos.left != left ) {
				updateHeadPosition();
			}
			
			lastScrollPos = { 
				top: top, 
				left: left
			};
		}).trigger("scroll");	
	},
	
	urlEncodeParams: function( params ) {
		Object.keys( params ).forEach( function( key ) { 
			params[ key ] = encodeURIComponent( params[ key ] );
		});
		
		return params;
	}
});/**
 * The chart page class
 */
Runner.pages.ChartPage = Runner.extend( Runner.pages.DataPageWithSearch, {
	
	pageType: Runner.pages.constants.PAGE_CHART,
	
	
	/**
	 * @constructor
	 * @param {object} cfg 
	 */
	constructor: function(cfg) {
		Runner.pages.ChartPage.superclass.constructor.call(this, cfg);
	},
	
	init: function() {
		Runner.pages.ChartPage.superclass.init.call(this);
		
		this.initMaps();
		this.initAdvSearch();
		
		//	recreate charts when reloading in AJAX mode
		this.createCharts();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	initMaps: function() {
		if ( this.hasMaps() ) {
			Runner.globalGmapLoader.onLoad( function() {
				this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps ); 
				this.mapManager.init();
			}, this );
		}
	},
	
	initDetails: Runner.emptyFn,

	initGridTabs: Runner.emptyFn,
});Runner.pages.ChartPageDP = Runner.extend( Runner.pages.ChartPage, {
	
	constructor: function( cfg ) {
		Runner.pages.ChartPageDP.superclass.constructor.call(this, cfg);
		this.pageCont = $('#detailPreview' + this.id).get(0);
		this.addEvents("afterPageReady");
	},
	
	/**
	 * Initialize a detail chart
	 * Fix it
	 */
	init: function() {
		Runner.pages.ChartPageDP.superclass.init.call(this);
	},

	preparePageForReloading: function() {
		if ( Runner.pages.PageSettings.getTableData(this.tName, "displayLoading") ) {
			this.showLoadingBox();
		}
	},
});
/**
 * The chart page class
 */
Runner.pages.CalendarPage = Runner.extend( Runner.pages.DataPageWithSearch, {

	pageType: Runner.pages.constants.PAGE_CALENDAR,

	/**
	 * Calendar object
	 */
	calendar: null,
	
	
	/**
	 * Double dialog opening prevention
	 */
	lastClick: new Date(),
	clickPause: 500,


	/**
	 * @constructor
	 * @param {object} cfg 
	 */
	constructor: function ( cfg ) {
		Runner.pages.CalendarPage.superclass.constructor.call( this, cfg );
	},

	init: function () {
		Runner.pages.CalendarPage.superclass.init.call( this );

		this.initMaps();
		this.initAdvSearch();
		this.initAddButton();

		//	recreate charts when reloading in AJAX mode
		this.createCharts();

		this.createCalendar();
		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},

	initMaps: function () {
		if ( this.hasMaps() ) {
			Runner.globalGmapLoader.onLoad( function () {
				this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps );
				this.mapManager.init();
			}, this );
		}
	},

	viewLabelToName: {
	 	year: 'multiMonthYear',
		month: 'dayGridMonth',
		week: 'timeGridWeek',
		list: 'listYear',
	},
	viewNameToLabel: {
	 	multiMonthYear: 'year',
		dayGridMonth: 'month',
		timeGridWeek: 'week',
		listYear: 'list',
	},

	updateViewparameter: function( type ) {
		const urlParams = URL.parse( window.location.href );
		urlParams.searchParams.set( 'view', this.viewNameToLabel[ type ] );
		history.replaceState({}, "", urlParams.toString() );
	},

	createCalendar: function () {
		var calendarEl = $( `[data-calendar${ this.id }]`)[0];
		const data = this.pageData.calendarData;
		if( !data || !calendarEl ) {
			//	hide data until search
			return;
		}

		if( data.canAdd ) {
			calendarEl?.setAttribute('data-can-add', '' );
		}

		const settings = {
			height: 'auto',
			viewDidMount: ( arg ) => this.updateViewparameter( arg.view.type ),
			initialView: this.viewLabelToName[ data.initialView ],
			events: data.events,
			dateClick: data.canAdd ? ( info ) => this.addEvent( info ) : null,
			eventClick:  ( info ) => this.viewEvent( info ),
			editable: data.canEdit,
			eventStartEditable: data.canEdit,
			eventDurationEditable: data.canEdit && data.hasEndDate,
			firstDay: data.firstWeekDay,
			locale: data.locale,
			headerToolbar: {
				start: 'title', // will normally be on the left. if RTL, will be on the right
				center: 'multiMonthYear,dayGridMonth,timeGridWeek,listYear',
				end: 'today prev,next' // will normally be on the right. if RTL, will be on the left
			},
			eventDrop: data.canEdit ? ( { event } ) => this.saveEventDate( event ) : null,
			eventResize: data.canEdit ? ( { event } ) => this.saveEventDate( event ) : null,
		};
		
		var pageEvents = Runner.pages.PageSettings.getTableEventsData( this.pageTable, this.pageType );
		if( pageEvents.EVENT_JS_CREATE_CALENDAR ) {
			this.calendar = pageEvents.EVENT_JS_CREATE_CALENDAR[0].hn.call( this, calendarEl, settings, this.proxy, this );
		} else {
			this.calendar = new FullCalendar.Calendar( calendarEl, settings );
		}


		this.calendar.render();
	},

	addEvent: function( info ) {
		if( new Date() - this.lastClick < this.clickPause ) {
			return;
		}
		this.lastClick = new Date();
		var date = Runner.util.timeString( info.date );
		if( !this.calendar.view.type?.startsWith( 'timeGrid' ) ) {
			//	change time to the beginning of the hour
			newDate = new Date( info.date );
			newDate.setHours( ( new Date() ).getHours() );
			date = Runner.util.timeString( newDate ).substring( 0, 13 ) + ':00:00';
		}
		this.showAddPage( true, 'add_calendar', null, { date } );
	},


	saveEventDate: function( event ) {
		var baseParams = {
			a: 'updateEvent',
			page: 'edit_calendar'
		}
		const keys = event.id.split( '~' );
		for ( var i = 0; i < keys.length; ++i ) {
			baseParams["editid" + ( i + 1 )] = keys[i];
		}
		baseParams.start = Runner.util.timeString( event.start );
		baseParams.end = Runner.util.timeString( event.end > event.start ? event.end : event.start );
		baseParams.allDay = event.allDay ? 1 : 0;

		var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );
		Runner.runnerAJAX( url, baseParams, ( respObj ) => {
			if ( !respObj.success ) {
				Runner.displayGenericAjaxError( respObj.error );
			}
		});
	},

	editEvent: function( keys ) {
		var pageParams = Runner.pages.EditPage.prototype.getOpenPageParams.call( this, keys );
		pageParams.baseParams["page"] = 'edit_calendar';
		Runner.pages.PageManager.openPage( pageParams );
	},

	viewEvent: function( info ) {
		if( new Date() - this.lastClick < this.clickPause ) {
			return;
		}
		this.lastClick = new Date();
		var pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( this, info.event.id.split( '~' ) );
		pageParams.baseParams["page"] = 'view_calendar';
		Runner.pages.PageManager.openPage( pageParams );
	},

	showAddInPopup: function( $link ) {
		return true;
	},

	insertAddedEvent: function( event ) {
		if( !event ) {
			return;
		}
		this.calendar.addEvent( event );
	},

	updateEditedEvent: function( oldKeys, event ) {
		var eventId = oldKeys.join( '~' );
		const oldEvent = this.calendar.getEventById( eventId );
		if( !oldEvent ) {
			return;
		}
		if( !event ) {
			oldEvent.remove();
			return;
		}
		oldEvent.setStart( event.start );
		oldEvent.setEnd( event.end );
		oldEvent.setAllDay( event.allDay );
		oldEvent.setProp( 'id', event.id );
		oldEvent.setProp( 'title', event.title );
		oldEvent.setProp( 'backgroundColor', event.backgroundColor );
		oldEvent.setProp( 'textColor', event.textColor );
	},


	initDetails: Runner.emptyFn,

	initGridTabs: Runner.emptyFn,
} );/**
 * The chart page class
 */
Runner.pages.GanttPage = Runner.extend( Runner.pages.DataPageWithSearch, {
	
	pageType: Runner.pages.constants.PAGE_GANTT,
	gantt: null,
	
	
	/**
	 * @constructor
	 * @param {object} cfg 
	 */
	constructor: function(cfg) {
		Runner.pages.GanttPage.superclass.constructor.call(this, cfg);
	},
	
	init: function() {
		Runner.pages.GanttPage.superclass.init.call(this);
		
		this.initMaps();
		this.initAdvSearch();
		this.initAddButton();
		
		//	recreate charts when reloading in AJAX mode
		this.createCharts();
		
		this.createGantt( true );
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	initMaps: function() {
		if ( this.hasMaps() ) {
			Runner.globalGmapLoader.onLoad( function() {
				this.mapManager = new Runner.controls.MapManager( this.controlsMap.gMaps ); 
				this.mapManager.init();
			}, this );
		}
	},

	sortTasks: function () {
		const oldTasks = this.pageData.ganttData.tasks;
		
		let newTasks = oldTasks.filter( t => !t.dependencies || !oldTasks.some( p => p.id == t.dependencies ) );

		const addChildren = ( parentIdx ) => {
			const parent = newTasks[ parentIdx ];
			const children = oldTasks.filter( t => t.dependencies == parent.id );
			newTasks.splice( parentIdx + 1, 0, ...children );
			//	handle children form the end to keep indices intact
			for( let childIdx = parentIdx + children.length; childIdx > parentIdx; --childIdx ) {
				addChildren( childIdx );
			}
		}
		
		for( let topIdx = newTasks.length - 1; topIdx >= 0; --topIdx ) {
			addChildren( topIdx );
		}
		this.pageData.ganttData.tasks = newTasks;
	},

	tasks: function() {
		return this.pageData.ganttData.tasks;
	},

	/**
	 * Make sure the child date is on or after parent's date
	 * Replaces date in place, returns the argument
	 * @param {*} task 
	 */
	fixChildDate: function( task ) {
		const parent = this.tasks().find( t => t.id == task.dependencies );
		if( !parent || parent.start <= task.start ) {
			return task;
		}
		const length = task.end.valueOf() - task.start.valueOf();
		task.start = parent.start;
		task.end = new Date( task.start.valueOf() + length );
		return task;
	},

	fixDates: function( task ) {
		task.start = Runner.util.localDateFromDbString( task.start );
		task.end = Runner.util.localDateFromDbString( task.end );
		return task;
	},

	createGantt: function( animate ) {
		if( !this.pageData.ganttData ) {
			//	hide data until search
			return;
		}
		var $ganttEl = $( `[data-gantt${ this.id }]`);
		$ganttEl.empty();
		$ganttEl.off();

		//	fix task data so it can be displayed properly 
		this.sortTasks();
		this.pageData.ganttData.tasks = this.tasks().map( t => this.fixDates( t ) );
		this.pageData.ganttData.tasks = this.tasks().map( t => this.fixChildDate( t ) );

		var dateChangeDelayers = {};
		var progressChangeDelayers = {};

		const settings = {
			language: this.pageData.ganttData.language,
			infinite_padding: false,
			container_height: this.pageData.ganttData.tasks.length < 6 ? 480 : 'auto',
			animate: false,
			readonly_dates: !this.pageData.ganttData.canEdit,
			readonly_progress: !this.pageData.ganttData.canEditProgress,
			popup: ( ctx ) => {
				
				if( ctx.task.canEdit ) {
					this.editTask( ctx.task );
					return false;
				}
				if( this.pageData.ganttData.canView ) {
					this.viewTask( ctx.task );
					return false;
				}
				return '';
			},
			on_date_change: ( task, startDate, endDate ) => {
				
				const originalTask = this.pageData.ganttData.tasks.find( t => t.id == task.id );
				if( !originalTask?.canEdit ) {
					return;
				}
				if( originalTask ) {
					originalTask.start = startDate;
					originalTask.end = endDate;
				}
				
				var delayer = dateChangeDelayers[ task.id ];
				if( !delayer ) {
					delayer = new Runner.util.DelayedTask();
					dateChangeDelayers[ task.id ] = delayer;
				}
				startDate = Runner.util.timeString(  startDate );
				endDate = Runner.util.timeString( endDate );
				delayer.delay( 500,  this.saveTask, this, [ task, { startDate, endDate }] );
			},
			on_progress_change: ( task, progress ) => {
				const originalTask = this.pageData.ganttData.tasks.find( t => t.id == task.id );
				if( !originalTask?.canEdit ) {
					return;
				}
				if( originalTask ) {
					originalTask.progress = progress;
				}
				var delayer = progressChangeDelayers[ task.id ];
				if( !delayer ) {
					delayer = new Runner.util.DelayedTask();
					progressChangeDelayers[ task.id ] = delayer;
				}
				delayer.delay( 500, this.saveTask, this, [ task, { progress } ] );
			},
		};

		var pageEvents = Runner.pages.PageSettings.getTableEventsData( this.pageTable, this.pageType );
		if( pageEvents.EVENT_JS_CREATE_GANTT ) {
			this.gantt = pageEvents.EVENT_JS_CREATE_GANTT[0].hn.call( this, $ganttEl[0], this.pageData.ganttData.tasks, settings, this.proxy, this );
		} else {
			this.gantt = new Gantt( $ganttEl[0], this.pageData.ganttData.tasks, settings );	
		}
	},

	saveTask: function( task, data ) {
			var baseParams = {
				a: 'updateTask',
				gantt: 1,
			}
			const keys = task.id.split( '~' );
			for ( var i = 0; i < keys.length; ++i ) {
				baseParams["editid" + ( i + 1 )] = keys[i];
			}
			Object.assign( baseParams, data );

			var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );
			Runner.runnerAJAX( url, baseParams, ( respObj ) => {
				if ( respObj.success ) {

				} else {
					Runner.displayGenericAjaxError( respObj.error );
				}
			});

	},

	editTask: function( task ) {
		var pageParams = Runner.pages.EditPage.prototype.getOpenPageParams.call( this, task.keys );
		pageParams.baseParams["gantt"] = 1;
		Runner.pages.PageManager.openPage( pageParams );
	},

	addChildTask: function( parent ) {
		var pageParams = Runner.pages.AddPage.prototype.getOpenPageParams.call( this, this.id );
		pageParams.baseParams["gantt"] = 1;
		pageParams.baseParams["parent"] = parent;
		const parentTask = this.tasks().find( t => t.id == parent );
		if( parentTask ) {
			pageParams.baseParams["start"] = Runner.util.timeString( parentTask.start );
		}
		Runner.pages.PageManager.openPage( pageParams );
	},

	viewTask: function( task, bar ) {
		var pageParams = Runner.pages.ViewPage.prototype.getOpenPageParams.call( this, task.keys );
		pageParams.baseParams["gantt"] = 1;
		Runner.pages.PageManager.openPage( pageParams );
	},

	updateEditedTask: function( oldKeys, task ) {
		var taskId = oldKeys.join( '~' );
		if( task ) {
			task = this.fixDates( task );
			task = this.fixChildDate( task );
			const oldTask = this.pageData.ganttData.tasks.find( t => t.id === taskId );
			if( oldTask ) {
				Object.assign( oldTask, task );
				this.createGantt();
			}
		} else {
			this.pageData.ganttData.tasks = this.pageData.ganttData.tasks.filter( t => t.id != taskId );
			this.createGantt();
		}
	},

	

	insertAddedTask: function( task ) {
		if( !task ) {
			return;
		}
		this.pageData.ganttData.tasks.push( task );
		this.createGantt();
		
	},

	showAddInPopup: function( $link ) {
		return true;
	},
	
	initDetails: Runner.emptyFn,

	initGridTabs: Runner.emptyFn,
});
Runner.pages.ReportPageDP = Runner.extend( Runner.pages.ReportPage, {
	
	baseParams: null,
	
	constructor: function( cfg ) {
		Runner.pages.ReportPageDP.superclass.constructor.call( this, cfg );
		this.addEvents("afterPageReady");
		
		this.pageCont = cfg.pageCont || $('#detailPreview' + this.id)[0];
		this.baseParams = this.baseParams || { id: this.pageId };
	},
	
	init: function() {
		Runner.pages.ReportPageDP.superclass.init.call(this);
		this.loadFiles();
	},

	/**
	 * Reload the crosstab report page when a group field is changed
	 */
	initCrossTableGroupSelection: function() {
		var pageObj = this;
		
		$("#select_group_x" + this.id).on("change", function(e) {							
			pageObj.preparePageForReloading();
			//CHECK: destroy previous page?
			Runner.runnerAJAX( pageObj.reportFileName + "?" + pageObj.getParamsString(), pageObj.baseParams, function( respObj ) {
				pageObj.pageReloadHn.call( pageObj, respObj )
			});			
			return false;
		});
		
		$("#select_group_y" + this.id).on("change", function(e) {
			pageObj.preparePageForReloading();
			//CHECK: destroy previous page?
			Runner.runnerAJAX( pageObj.reportFileName + "?" + pageObj.getParamsString(), pageObj.baseParams, function( respObj ) {
				pageObj.pageReloadHn.call( pageObj, respObj )
			});
			return false;
		});			
	},	
	
	initSearch: Runner.emptyFn,

	/**
	 * @param {object} respObj
	 */	
	pageReloadHn: function( respObj ) {
		var parPageObj = Runner.pages.PageManager.getAt( this.masterTName, this.parId );
		Runner.stopLoading( this.getGrayedElement() );
		Runner.setIdCounter( respObj.idStartFrom );
		
		respObj.bodyCont = respObj.html;

		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;

		if ( parPageObj.pageType == Runner.pages.constants.PAGE_EDIT || parPageObj.pageType == Runner.pages.constants.PAGE_ADD 
			|| parPageObj.pageType == Runner.pages.constants.PAGE_VIEW ) {
				
			Runner.pages.PageManager.putToContainer.call( this, respObj, this.detCont );
		} else {
			Runner.pages.PageManager.putToDashContainer.call( this, respObj, this.detCont );
		}
		
		this.controlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.pageId ];
		this.initPagination();
		this.initGridTabs();

		if ( this.hasMaps() ) {
			this.mapManager.init( this.controlsMap.gMaps.mapsData );
		}
	
		if ( respObj.viewControlsMap ) {			
			this.setupViewContols( respObj.viewControlsMap[ this.tName ][ this.pageType ][ this.pageId ].controls, this.pageCont );
		}
		
		this.initCrossControls();
	},

	/**
	 *
	 */
	destructor: function() {
		Runner.pages.ReportPageDP.superclass.destructor.call( this );
	},

	getGrayedElement: function() {
		return this.detCont[0];
	}

});
Runner.pages.PrintPageCommon = Runner.extend( Runner.pages.RunnerPage, {
		
	pageType: Runner.pages.constants.PAGE_PRINT,
	
	pageHeight: Runner.pages.constants.VPRINT_PAGE_HEIGHT,
	
	pageWidth: Runner.pages.constants.VPRINT_PAGE_WIDTH,
	
	repeatClass: "rp-repeat",
	
	hideClass: "rp-hide",
	
	tableClass: "rp-maintable",
	
	pageNoClass: "rp-pageno",
	
	noPrintClass: "rp-noprint",
	
	pageCountClass: "rp-pagecount",
	
	loadingPrintClass: "rnr-loading-print",
	
	zoom: 100,

	preSplit: false,

	autoZoom: false,
	
	edgeClass: "rnr-edge",
	
	contentWidth: Runner.pages.constants.VPRINT_PAGE_WIDTH,
	
	/**
	 * The storage of pages' objects
	 * @type {array}
	 */
	pages: [],
	
	/**
	 * The print page's layout type
	 * @type {number}
	 */
	layout: 0,
	
	/**
	 * The flag indicating that 
	 * RTL is turned on for the page
	 * @type {boolean}
	 */
	isRTL: false,
	
	$noPrintElements: null,
	
	getContentWidth: function() {
		return $(".rp-page").width() || document.body.clientWidth;
	},
	
	init: function() {
		Runner.pages.PrintPageCommon.superclass.init.call( this );

		var pageObj = this;
		
		//	set the same zoom for all pages
		
		var zoom = 120;
		this.contentWidth = this.getContentWidth();
		if ( this.autoZoom ) {
			//	set zoom to each page individually
			$(".rp-page").each( function(i, page) {
				var wZoom = 100.0 * pageObj.pageWidth / $(page).width();
				
				if ( wZoom > 120 ) {
					wZoom = 120;
				}
				zoom = zoom > wZoom ? wZoom: zoom;
			});	
			this.zoom = zoom;
		} 
		this.pageWidth *= ( 100.0 / this.zoom );
		this.pageHeight *= ( 100.0 / this.zoom );
		$("<style>@media print { body { zoom:" + this.zoom + "%;} }</style>").appendTo( document.head );		
		
		this.setPrintOrientation();
	},
	
	constructor: function(cfg) {
		Runner.pages.PrintPageCommon.superclass.constructor.call( this, cfg );	
		this.createPdf = this.proxy.createPdf;
		
		if ( this.createPdf ) {
			this.zoom = Runner.pages.PageSettings.getTableData( this.tName, "pdfPrinterPageScale" );
			this.autoZoom = Runner.pages.PageSettings.getTableData( this.tName, "pdfFitToPage" );
			this.landscapeOrientation = Runner.pages.PageSettings.getTableData( this.tName, "pdfPrinterPageOrientation" );
			//	the page would not be called in the in createPdf mode if presplit
			this.preSplit = false;
		} else {
			this.zoom = Runner.pages.PageSettings.getTableData( this.tName, "printerPageScale" );
			this.autoZoom = Runner.pages.PageSettings.getTableData( this.tName, "isPrinterPageFitToPage" );
			this.landscapeOrientation = Runner.pages.PageSettings.getTableData( this.tName, "printerPageOrientation" );
			this.preSplit = Runner.pages.PageSettings.getTableData( this.tName, "printerSplitRecords" ) != 0;
		}
		
		this.layout = Runner.pages.PageSettings.getTableData( this.tName, "printGridLayout" );
		
		if ( this.landscapeOrientation ) {
			this.pageWidth = Runner.pages.constants.HPRINT_PAGE_WIDTH;
			this.pageHeight = Runner.pages.constants.HPRINT_PAGE_HEIGHT;
		}
		
		this.pdfFitToPage = Runner.pages.PageSettings.getTableData( this.tName, "pdfFitToPage" );

		this.hasPdf = Runner.pages.PageSettings.getTableData( this.tName, "printerPagePDF" );
		this.isRTL = Runner.pages.PageSettings.getTableData( this.tName, "isRTL" );
	},
	
	/**
	 * creates a new print page
	 * @return {object}
	 */
	newPage: function() {
		var tableClass = this.tableClass;
		
		return {
			$page: $( "<div class=rp-page></div>" ),
			height: 0,
			$tbody: function() {
				return this.$page.find( "." + tableClass + " > tbody" ).first();
			}
		};
	},
	
	/**
	 * Add page after idx page
	 * @param {number} insertAfterIdx
	 * @param {boolean} cloneTableOnly
	 * @return {number}					An index of the new page object in this.pages
	 */
	addPage: function( insertAfterIdx, cloneTableOnly ) {
		var newPage = this.newPage();
		
		if ( typeof insertAfterIdx == "undefined" ) {
			insertAfterIdx = this.pages.length - 1;
			newPage.$page.appendTo( document.body );
		} else {
			this.pages[ insertAfterIdx ].$page.after( newPage.$page );
		}
		
		insertAfterIdx++;
		
		this.pages.splice( insertAfterIdx, 0, newPage );
		this.cloneBranch( this.pages[0].$page, newPage.$page, cloneTableOnly );
		newPage.height = newPage.$page.height();
		
		return insertAfterIdx;
	},
	
	/**
	 * Copy elements to the new page
	 * @param {jQuery object} $from
	 * @param {jQuery object} $to
	 * @param {boolean} skipRepeat
	 */
	cloneBranch: function( $from, $to, skipRepeat ) {
		var pageObj = this;
		
		$from.children().each( function( i, elem ) {
			var $elem = $( elem ),
				$cloned = null;
			
			if ( $elem.hasClass( pageObj.hideClass ) ) {
				return;
			}
			
			if ( $elem.hasClass( pageObj.tableClass ) ) {
				$cloned = $( Runner.domShallowCopy( elem ) )
					.appendTo( $to )
					.append( !skipRepeat ? $elem.children("THEAD").clone() : $( "<thead></thead>" ) )
					.append( $("<tbody></tbody>") );
				return;
			} 			
			
			if ( $elem.hasClass( pageObj.repeatClass ) ) {
				$elem.clone().appendTo( $to );
			} else if ( $elem.find( "." + pageObj.repeatClass ).length || $elem.find( "." + pageObj.tableClass ).length ) {
				$cloned = $( Runner.domShallowCopy( elem ) ).appendTo( $to );
				pageObj.cloneBranch( $elem, $cloned, skipRepeat );
			}
		});
	},
	
	/**
	 * Copy table columns to the $dstTable jQuery table object passed
	 * @param {jQuery object} $srcTable
	 * @param {jQuery object} $dstTable		
	 * @param {number} startCol
	 */
	copyTableColumns: function( $srcTable, $dstTable, startCol ) {
		var pageObj = this;
		
		$.each( ["thead", "tbody"], function( _i, tag ) {
			var $rows = $srcTable.children( tag ).children(),
				$rowContainer = null;
			
			if ( 0 == $rows.length ) {
				return;
			}
			
			// create new row container (THEAD or TBODY)
			$rowContainer = $( Runner.domShallowCopy( $srcTable.children( tag ).get(0) ) )
				.appendTo( $dstTable );
			
			$rows.each( function( _i, row ) {
				// create new row
				var $dstRow = $( Runner.domShallowCopy( row ) ).appendTo( $rowContainer );
				// move cells
				$( row ).children().slice( startCol ).appendTo( $dstRow );
				$( row ).children().last().addClass( pageObj.edgeClass );
			});
		});
	},
	
	/**
	 * Set the print page's orientation
	 */
	setPrintOrientation: function() {
		if ( Runner.pages.PageSettings.getTableData( this.tName, "printerPageOrientation" ) !== Runner.pages.constants.PORTRAIT_PRINT_ORIENTATION ) {
			$( "link:first" ).before( '<style type="text/css" media="print">@page { size: landscape; }</style>' );
		}
	},

});/**
 * Runner.pages.PrintPage
 */
Runner.pages.PrintPage = Runner.extend( Runner.pages.PrintPageCommon, {
	/**
	 * @type {object}
	 */
	dpObjs: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.PrintPage.superclass.constructor.call( this, cfg );
	},
	
	init: function() {
		Runner.pages.PrintPage.superclass.init.call( this );
		this.baseParams = this.baseParams || {};
		this.baseParams.selection = this.pageData.printSelection;
		this.baseParams.all = this.pageData.printAll;
		this.baseParams.details = this.pageData.printDetails;
		this.baseParams.records = this.pageData.printRecords;
		
		this.initColumnReordering();
		this.initDetails();
		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},
	
	initDetails: function() {
		if ( !this.viewControlsMap || !this.viewControlsMap.dViewControlsMap ) {
			return;
		}
		
		this.dpObjs = {};
		
		for ( var tName in this.viewControlsMap.dViewControlsMap ) {
			var dParams = {
				tName: tName,
				masterTName: this.tName,
				pageId : this.viewControlsMap.dViewControlsMap[ tName ].id,
				viewControlsMap: this.viewControlsMap.dViewControlsMap[ tName ],
				masterPageObj: this,
			};
			
			this.dpObjs[ tName ] = new Runner.pages.PrintPageDetails( dParams );
			this.dpObjs[ tName ].init();
		}
	},	
	
	/**
	 * @return {mixed}
	 */
	getPrintGrid: function() {
		return this.layoutHelper.getFormElement("grid");

	},
	
	/**
	 * Reorder grid columns according to the user's settings
	 */
	initColumnReordering: function() {
		if ( !Runner.pages.PageSettings.getTableData( this.tName, "isAllowFieldsReordering" ) ) {
			return;
		}
		
		var columnOrder = Runner.pages.PageSettings.getTableData( this.tName, "columnOrder" ),
			$grid = this.getPrintGrid(),
			pageObj = this;
		
		if ( !$grid || !$grid.length || !columnOrder|| !Array.isArray( columnOrder ) || !columnOrder.length ) {
			return;
		}
		
		var $pageGrids = $("[id='" + $grid.attr("id") + "']");
		
		$pageGrids.each( function( _ind, grid ) {
			var indices = [],
				tail = [],
				orderedIndices = [];
			
			$("th", grid).not( pageObj.inDetailPreviewContainer ).each( function( ind, th ) {
				var gfName = $(th).data("field"),
					colInd = $.inArray( gfName, columnOrder );
				
				if ( colInd !== -1 ) {
					indices[ colInd ] = ind;
				} else {
					tail.push( ind );
				}
			});
			
			orderedIndices = indices.filter( function( value ) {
				return  value !== undefined;
			});
			
			if ( !orderedIndices.length ) {
				return;
			}
			
			orderedIndices = orderedIndices.concat( tail );
			
			$("tr", grid).not( pageObj.isDetailContainer ).each( function( ind, tr ) {
				var $tr = $(tr), 
					$tds = $tr.children("th, td");
				
				for ( var i = 0; i < orderedIndices.length; i++ ) {
					$tr.append( $tds.eq( orderedIndices[ i ] ) );
				}			
			});
		});
	},
	
	/**
	 * jQuery callback function checking if the current 
	 * DOM element is the print details container
	 * @return {Boolean}
	 */	
	isDetailContainer: function() {
		return !!$(this).find(".details-grid").length;
	},
	
	/**
	 * jQuery callback function checking if the current 
	 * DOM element belongs to the print details container
	 * @return {Boolean}
	 */
	inDetailPreviewContainer: function() {
		return !!$(this).closest(".details-grid").length;
	},
});

/**
 * Detail print page obj 
 */
Runner.pages.PrintPageDetails = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.PrintPageDetails.superclass.constructor.call( this, cfg );
	},
	
	init: function() {
		Runner.pages.PrintPageDetails.superclass.init.call( this );
	},
	
	initPdfMake: Runner.emptyFn,
});
Runner.pages.ReportPrintPage = Runner.extend( Runner.pages.PrintPageCommon, {
		
	pageType: Runner.pages.constants.PAGE_REPORT_PRINT,
	
	tableClass: "rnr-report",
	
	$summaryHeader: null,
	
	$summaryPage: null,
	
	$summaryGlobal: null,
	
	summaryPageHeight: 0,
	
	/**
	 * The print page's mode PRINT FRIENDLY or PRINT WHOLE
	 * @type {number}
	 */
	reportPrintMode: 0,
	
	groupsPerPage: 3,
	
	reportLayout: 0,
	
	/**
	 * The number of report's group
	 * @type {number}
	 */
	
	reportType: 0,
	
	
	constructor: function(cfg) {
		Runner.pages.ReportPrintPage.superclass.constructor.call(this, cfg);
		
		this.reportLayout = Runner.pages.PageSettings.getTableData( this.tName, "reportPrintLayout" );
		this.reportPrintMode = Runner.pages.PageSettings.getTableData( this.tName, "reportPrintMode" );
		this.groupsPerPage = Runner.pages.PageSettings.getTableData( this.tName, "reportPrintGroupsPerPage" );
		this.reportType = Runner.pages.PageSettings.getTableData( this.tName, "reportType" );
		
		if ( this.reportLayout == Runner.pages.constants.TABULAR_LAYOUT ) {
			this.tableClass = "rnr-b-report";
		}
		if ( this.reportType == Runner.pages.constants.REPORT_TYPE_CROSSTAB ) {
			this.tableClass = "rnr-cw-grid";
		}
	},

	isCrossTable: function() {
		return this.reportType == Runner.pages.constants.REPORT_TYPE_CROSSTAB;
	},
	
	init: function() {
		Runner.pages.ReportPrintPage.superclass.init.call( this );
		this.baseParams = this.baseParams || {};
		this.baseParams.all = this.pageData.printAll;
		this.baseParams.records = this.pageData.printRecords;
		
		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},
	

	getContentWidth: function() {
		return $(".rp-page").width() || document.body.clientWidth;
	},
	
	getBaseParams: function() {
		var baseParams = Runner.pages.ReportPrintPage.superclass.getBaseParams.call( this );
		
		if ( this.isCrossTable() && this.pageData.crossParams ) {
			return Runner.apply( baseParams, this.pageData.crossParams );
		}
		
		return baseParams;
	},	
});Runner.pages.DashboardPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * @type {string}
	 */
	pageType: Runner.pages.constants.PAGE_DASHBOARD,
	
	/** 
	 *  Array of dashboard elements in the form of {elementName, table, type}
	 * @type {array}
	 */
	elements: null,
	
	/** 
	 *  Reference object with various info specific to the element type
	 * @type {object}
	 */
	elementTypeInfo: null,
	
	/**
	 *
	 */
	dynamicPagesDataByElement: {},
	
	dynamicSnippetsData: {},
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.DashboardPage.superclass.constructor.call(this, cfg);	
		Runner.dashboard = this;
		
		this.elements = Runner.pages.PageSettings.getTableData(this.tName, "dashElements");
		this.fillElementTypeInfo();
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		Runner.pages.DashboardPage.superclass.init.call( this );

		this.initAdvSearch()
		
		this.doLoadAction();

		this.fireEvent('afterInit', this, this.proxy, this.id);
	},

	/**
	 *
	 */
	initSearch: function() {	
		this.searchController = new Runner.search.SearchController({
			id: this.pageId,
			tName: this.tName,
			pageType: this.pageType,
			ajaxSubmit: true,
			formBaseParams: {
				id: this.pageId,
				mode: "dashsearch",
				page: this.pageName
			},
			useSuggest: true
		});

		this.searchController.init( this.controlsMap.search.searchBlocks );

		this.searchController.on('afterSearch', function( respObj, srchController, srchForm ) {
			srchController.toggleShowAll(respObj.show_all);
			this.doLoadAction( respObj.show_all ? {} : {a: "showall"} );
		}, this);
	},	
	
	/**
	 * Fills in elementTypeInfo
	 */
	fillElementTypeInfo: function() {
		var constants = Runner.pages.constants;
		if ( this.elementTypeInfo ) {
			return;
		}
		
		this.elementTypeInfo = {};
		this.elementTypeInfo[ constants.DASHBOARD_LIST ] = {pageType: constants.PAGE_LIST, pageRequestMode: "dashlist", pageMode: constants.LIST_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_CHART ] = {pageType: constants.PAGE_CHART, pageRequestMode: "dashchart", pageMode: constants.CHART_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_REPORT ] = {pageType: constants.PAGE_REPORT, pageRequestMode: "dashreport", pageMode: constants.REPORT_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_RECORD ] = {pageType: constants.PAGE_EDIT, pageRequestMode: "dashrecord", pageMode: constants.EDIT_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_SEARCH ] = {pageType: constants.PAGE_SEARCH, pageRequestMode: "dashsearch", pageMode: constants.SEARCH_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_DETAILS ] = {pageType: constants.PAGE_LIST, pageRequestMode: "dashdetails", pageMode: constants.LIST_DASHDETAILS};
		this.elementTypeInfo[ constants.DASHBOARD_MAP ] = {pageType: constants.PAGE_DASHMAP, pageRequestMode: "dashmap", pageMode: constants.MAP_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_CALENDAR ] = {pageType: constants.PAGE_CALENDAR, pageRequestMode: "dashcalendar", pageMode: constants.CALENDAR_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_GANTT ] = {pageType: constants.PAGE_GANTT, pageRequestMode: "dashgantt", pageMode: constants.GANTT_DASHBOARD};
		this.elementTypeInfo[ constants.DASHBOARD_SNIPPET ] = {pageType: constants.PAGE_DASHBOARD, pageRequestMode: "dashsnippet", pageMode: constants.DASHMODE_SNIPPET};
		this.elementTypeInfo[ constants.DASHBOARD_WIDGET ] = {pageType: constants.PAGE_DASHBOARD, pageRequestMode: "dashwidget", pageMode: constants.DASHMODE_SNIPPET};
	},
	
	
	/**
	 * fix it
	 * @param {object} dbelem
	 * @param {array} pageTypes
	 * @param {object} additionalPageParams
	 * @param {?} action
	 * @return {number} number of pages reloaded
	 */
	loadSingleRecord: function( dbelem, pageTypes, additionalPageParams, action ) {
		var pageObj = this, 
			ret = 0;
			
		additionalPageParams = additionalPageParams || {};
		additionalPageParams.openMode = Runner.pages.constants.OPENMODE_DASHTABS;
		
		pageTypes = pageTypes.filter( p => dbelem.tabsPageTypes.includes(p) );
		
		// hide edit/view tabs for sr filtered by master or for sr without grid with search running
		if (dbelem.masterTable 
			|| dbelem.major && ( this.searchController.usedSrch || action instanceof Runner.Dashboard.SearchAction )) {
			this.toggleViewEditSingleRecordTabs( dbelem, false, pageTypes );
			//then they will be shown in PageManager setTabWithinTabsControl or putToDashTabAbove
		}
		
		$.each( pageTypes, function( idx, pageType ) {
			additionalPageParams.pageType = pageType;
			
			additionalPageParams.tabSettings = {
				tabIndex: $.inArray( pageType, dbelem.tabsPageTypes ),
				tabId: pageType,
				tabLabel: dbelem.tabLabels && dbelem.tabLabels[ pageType ] || pageObj.getTabLabel( pageType ),
				tabControlName: dbelem.elementName,
				activeTab: pageType === dbelem.initialTabPageType
			}
			
			additionalPageParams.baseParams = additionalPageParams.baseParams || {};
			additionalPageParams.baseParams.page = dbelem.pageNames[ pageType ];
			if ( pageType !== Runner.pages.constants.PAGE_VIEW ) {
				additionalPageParams.baseParams.hostPageName = dbelem.hostListPage;
				additionalPageParams.baseParams.spreadsheetGrid = dbelem.spreadsheetGrid ? 1 : 0;
			}
			
			pageObj.loadPage( dbelem, additionalPageParams, action );
			++ret;
		});
		
		return ret;
	},
	
	/**
	 *
	 * @param {string} pageType
	 * @return {string} 
	 */
	getTabLabel: function( pageType ) {
		var _map = {};
		
		_map[ Runner.pages.constants.PAGE_VIEW ] = Runner.lang.constants.VIEW;
		_map[ Runner.pages.constants.PAGE_ADD ] = Runner.lang.constants.AA_P_ADD;
		_map[ Runner.pages.constants.PAGE_EDIT ] = Runner.lang.constants.EDIT;
		
		this.getTabLabel = function( pageType ) {
			return _map[ pageType ];
		};
		
		return this.getTabLabel( pageType );
	},
	
	/**
	 *
	 * @param {object} dbelem
	 * @param {object} additionalPageParams
	 * @param {object} detailKeys
	 * @param {?} action
	 */
	loadDetails: function( dbelem, additionalPageParams, detailKeys, action ) {
		var pageObj = this, 
			tables, dkeyParams, params;
		
		this.markElementEmpty( dbelem.elementName, false );
		
		additionalPageParams = additionalPageParams || {};
		additionalPageParams.openMode = Runner.pages.constants.OPENMODE_DASHTABS;
		
		additionalPageParams.baseParams = additionalPageParams.baseParams || {};
		additionalPageParams.baseParams["goto"] = additionalPageParams.baseParams["goto"] || 1;
		
		$.each( dbelem.details , function( ind, details ) {
			additionalPageParams.tabSettings = {
				tabIndex: parseInt( ind ),
				tabId: details.dShortTable,
				tabLabel: details.dCaptionTable, 
				tabControlName: dbelem.elementName,
				hideTabsContent: false,
				activeTab: dbelem.initialTabDetailTable === details.dDataSourceTable,
				hiddenNav: dbelem.details.length === 1
			};
			
			additionalPageParams.baseParams.page = details.pageName;
			
			additionalPageParams.pageType = details.dType;
			additionalPageParams.tName = details.dDataSourceTable;
			params = jQuery.extend( true, {}, additionalPageParams );
			
			if ( detailKeys ) {
				jQuery.extend( params.baseParams, detailKeys[ details.dDataSourceTable ] )
				params.baseParams.mastertable = dbelem.table;
			}
			pageObj.loadPage( dbelem, params, action );
		});
		
		return dbelem.details.length;
	},
	
	/**
	 * @param {object} dbelem
	 * @param {object} additionalPageParams
	 * @param {?} action
	 */
	loadPage: function( dbelem, additionalPageParams, action ) {	
		var pageParams = this.getDefaultPageParams( dbelem, additionalPageParams.pageType ),
			pageId;		

		pageParams = jQuery.extend( true, pageParams, additionalPageParams || {} );
		//	don't duplicate the update context
		pageParams.updContext = additionalPageParams.updContext;
		pageParams.dashAction = action;
		
		pageId = Runner.pages.PageManager.openPage( pageParams );
		this.setDynamicPageData( dbelem, pageParams.tName, pageParams.pageType, pageId );
	},

	/**
	 * @param {object} dbelem	
	 * @return {object}
	 */
	getDefaultPageParams: function( dbelem, pageType ) {		
		var openMode = Runner.pages.constants.OPENMODE_DASHCONTAINER;
		if( dbelem.tabLocation == 'above' ) {
			openMode = Runner.pages.constants.OPENMODE_DASHTABS;
		}
		if( dbelem.group ) {
			openMode = Runner.pages.constants.OPENMODE_DASHTABS;
		}
		const tableLessElements = [ Runner.pages.constants.DASHBOARD_SNIPPET, Runner.pages.constants.DASHBOARD_WIDGET ];
		var params = {
				dashboard: this,
				parentId: this.id,
				pageType: this.getElementPageType( dbelem.type ), 
				pageMode: this.getElementPageMode( dbelem.type, pageType ), 
				tName: tableLessElements.includes( dbelem.type ) ? this.tName : dbelem.table,
				pageName: tableLessElements.includes( dbelem.type ) ? this.pageName : undefined,
				
				openMode: openMode, //
				openContainer: $('#dashelement_' + dbelem.elementName + this.id)[0],
				dashElement: dbelem.elementName,
				dashElementSettings: jQuery.extend(true, {}, dbelem ),
				
				baseParams: {
					parId: this.id,
					table: this.tName,
					dashPage: this.pageName,
					dashelement: dbelem.elementName,
					mode: this.getElementPageRequestMode( dbelem.type ),
					page: tableLessElements.includes( dbelem.type ) ? this.pageName : dbelem.pageName,
				}
			};
		
		if ( dbelem.type == Runner.pages.constants.DASHBOARD_LIST || dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ) {
			params.multiRecordPage = true;
		}
		
		if ( dbelem.type == Runner.pages.constants.DASHBOARD_MAP ) {
			params.requestUrl = Runner.pages.getUrl( dbelem.table, Runner.pages.constants.PAGE_LIST );
			params.pageMode = dbelem.updateMoved ? Runner.pages.constants.MAP_DASHBOARD : Runner.pages.constants.GRIDBASED_MAP_DASHBOARD;
		}
		
		return params;
	},
	
	/**
	 * @param {string} type
	 * @return {string}
	 */
	getElementPageType: function( type ) {
		return this.elementTypeInfo[ type ].pageType;
	},
	
	/**
	 * @param {string} type
	 * @return {string}
	 */	
	getElementPageMode: function( type, pageType ) {
		
		if ( type == Runner.DASHBOARD_DETAILS ) {
			if ( !this.detailsElementInfo ) {
				this.detailsElementInfo = {};
				this.detailsElementInfo[ Runner.PAGE_CHART ] = Runner.CHART_DASHDETAILS;
				this.detailsElementInfo[ Runner.PAGE_LIST ] = Runner.LIST_DASHDETAILS;
				this.detailsElementInfo[ Runner.PAGE_REPORT ] = Runner.REPORT_DASHDETAILS;
			}
			return this.detailsElementInfo[ pageType ];
		}
		return this.elementTypeInfo[ type ].pageMode;
	},
	
	/**
	 * @param {string} type
	 * @return {string}
	 */	
	getElementPageRequestMode: function( type ) {
		return this.elementTypeInfo[ type ].pageRequestMode;
	},

	/**
	 * @param {string} name
	 * @return {object}
	 */	
	getElementByName: function( name ) {
		var elementMap = {}, i;
		
		for ( i in this.elements ) {
			elementMap[ this.elements[i].elementName ] = this.elements[ i ];
		}
		
		this.getElementByName = function( _name ) {
			return elementMap[ _name ];
		}
		
		return this.getElementByName( name );
	},
	
	/**
	 * Add a dashboard dynamic page's data 
	 * @param {string} tName
	 * @param {string} pageType
	 * @param {number} pageId
	 */
	setDynamicPageData: function( dbelem, tName, pageType, pageId ) {
		var elementName = dbelem.elementName;
		
		if ( dbelem.type == Runner.pages.constants.DASHBOARD_RECORD ) {	
			this.dynamicPagesDataByElement[ elementName ] = this.dynamicPagesDataByElement[ elementName ] || {};
			
			this.unregisterPageIfExists( tName, this.dynamicPagesDataByElement[ elementName ][ pageType ] );
			this.dynamicPagesDataByElement[ elementName ][ pageType ] = pageId;
		} else if ( dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ) {
			this.dynamicPagesDataByElement[ elementName ] = this.dynamicPagesDataByElement[ elementName ] || {};
			
			this.unregisterPageIfExists( tName, this.dynamicPagesDataByElement[ elementName ][ tName ] );
			this.dynamicPagesDataByElement[ elementName ][ tName ] = pageId;
		} else {
			this.unregisterPageIfExists( tName, this.dynamicPagesDataByElement[ elementName ] );
			this.dynamicPagesDataByElement[ elementName ] = pageId;
		}
	},
	
	/**
	 *
	 */
	unregisterPageIfExists: function( tName, pageId ) {
		if ( pageId ) {
			Runner.pages.PageManager.unregister( tName, pageId )
		}
	},

	/**
	 * Get element page object by element name and page type or table name ( for RECORD and DETAILS respectively )
	 * @param {string} elementName
	 * @param {string} second - page type for single record or table name for details
	 * @return {object}	 
	 */
	getDynamicPageObject: function( elementName, second ) {
		var elementRecord =  this.dynamicPagesDataByElement[ elementName ] ,
			dbelem = this.getElementByName( elementName ),
			pageId, tName;
			
		if ( !elementRecord || !dbelem ) {
			return null;
		}

		if ( dbelem.type == Runner.pages.constants.DASHBOARD_RECORD || dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ) {
			pageId = elementRecord[ second ];
		} else {
			pageId = elementRecord;
		}
		
		if ( !pageId ) {
			return null;
		}
		
		tName =  dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ? second : dbelem.table;
		if( dbelem.type == Runner.pages.constants.DASHBOARD_SNIPPET || dbelem.type == Runner.pages.constants.DASHBOARD_WIDGET ) {
			tName = this.tName;
		}
		
		return Runner.pages.PageManager.getAt( tName, pageId );
	},

	
	/**
	 *  Get an element's page id
	 * @param {string} tName
	 * @param {string} pageType
	 * @param {string} elementName (optional)
	 * @return {number}	 
	 */
	getDynamicPageId: function( dbelem, tName, pageType ) {
		var pageId;	
			
		if ( !this.dynamicPagesDataByElement[ dbelem.elementName ] ) {
			return -1;
		}
			
		if ( dbelem.type == Runner.pages.constants.DASHBOARD_RECORD ) {
			pageId = this.dynamicPagesDataByElement[ dbelem.elementName ][ pageType ];
		} else if ( dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ) {			
			pageId = this.dynamicPagesDataByElement[ dbelem.elementName ][ tName ];
		} else {
			pageId = this.dynamicPagesDataByElement[ dbelem.elementName ];
		}
		
		return pageId || -1;
	},
	
	/**
	 * Unregister a dashboard dynamic page
	 * @param {string} tName
	 * @param {string} pageType
	 * @param {string} elementName
	 */
	unregisterDashPage: function( delem, tName, pageType ) {
		var pageId = this.getDynamicPageId( delem, tName, pageType );
		if ( pageId !== -1 ) {
			Runner.pages.PageManager.unregister( tName, pageId );
		}
	},
	
	/**
	 * @param {string} table
	 * @param {array} dashTypes
	 * @param {string} elementName (optional)
	 */
	getDashElements: function( table, dashTypes, elementName ) {
		var delements = [];
	
		if ( !this.elements ) {
			return delements;
		}
		
		if ( !Runner.isArray( dashTypes ) ) {
			dashTypes = [ dashTypes ];
		}
		
		$.each( this.elements, function( ind, dbelem ) {
			if ( $.inArray( dbelem.type, dashTypes) !== -1 && dbelem.table === table && (!elementName || dbelem.elementName === elementName) ) {
				delements.push( dbelem );
			}
		});
		
		return delements;
	},
	
	markElementEmpty: function( elementName, empty ) {
		const dbElem = this.getElementByName( elementName );
		if( !dbElem.group ) {
			$( '#dashelement_' + elementName + this.id).toggleClass('r-empty-dash-element', empty );
		} else {
			//	show/hide element tabs in the group
			const $group = $( `#dashgroup_${dbElem.group}${this.id}` );
			const $tabsNav = Runner.pages.PageManager.getDashGroupTabControl( dbElem.group, this.id ).children('.nav');
			const elementTabs = $tabsNav.children( `[data-element="${elementName}"]`);
			const switchToOther = empty && elementTabs.filter('.active').length;
			elementTabs.toggleClass('r-empty-dash-element', empty );
			//	show/hide element itself
			$( '#dashelement_' + elementName + this.id).toggleClass('r-empty-dash-element', empty );
			if( switchToOther ) {
				const otherTab = $tabsNav.children( `:not([data-element="${elementName}"]):not(.r-empty-dash-element)`).eq(0);
				if( otherTab.length ) {
					otherTab.tab( 'show' );
				} else {
					//	hide the whole group
					$group.toggleClass('r-empty-dash-element', true );
				}
			}
			if( !empty ) {
				$group.toggleClass('r-empty-dash-element', false );
			}

		}
	},
	
	/**
	 * Page Manager ? 
	 * Show/Hide 'view' and 'edit' single record tabs
	 * @param {object} dElement
	 * @param {boolean} toggler
	 * @param {array} tabsTypes (optional)
	 */
	toggleViewEditSingleRecordTabs: function( dElement, toggler, tabsTypes ) {
		if( dElement.group ) {
			return this.toggleGroupEditViewRecordTabs( dElement, toggler, tabsTypes );
		}
		const $tabsControl = Runner.pages.PageManager.getDashElementTabControl( dElement.elementName, this.id );
		const { PAGE_VIEW, PAGE_EDIT, PAGE_ADD } = Runner.pages.constants;

		tabsTypes = tabsTypes || dElement.tabsPageTypes;
		if ( !$tabsControl.length || (!tabsTypes.includes(PAGE_VIEW) && !tabsTypes.includes(PAGE_EDIT)) ) {
			return;
		}
		
		const addIdx = dElement.tabsPageTypes.indexOf(PAGE_ADD);
		if ( addIdx === -1 ) {
			// there is no add tab, hide the dash-element
			this.markElementEmpty( dElement.elementName, !toggler);
			return;
		}
		
		const $tabNav = $("ul.nav-tabs", $tabsControl);
		// save current active idx
		$tabsControl.data( "activeIdx", $tabNav.children("li").index( $tabNav.children("li.active") ) );
		// make an 'add' tab active
		$("a[data-toggle='tab']", $tabNav.children("li").eq( addIdx )).tab("show"); 
		
		// hide/show tabs
		if ( !toggler ) {
			// save current active idx
			$tabsControl.data( "activeIdx", $tabNav.children("li").index( $tabNav.children("li.active") ) );
			// make an 'add' tab active
			$("a[data-toggle='tab']", $tabNav.children("li").eq( addIdx )).tab("show"); 
		} else  {
			const activeIdx = $tabsControl.data( "activeIdx");
			if ( activeIdx !== undefined ) {
				// restore activeIdx, make this tab active
				$("a[data-toggle='tab']", $tabNav.children("li").eq( activeIdx )).tab("show");
			}
		}
		
		// hide/show tabs
		dElement.tabsPageTypes.forEach( (type, idx) => {
			if ( type !== PAGE_ADD && tabsTypes.includes(type) ) {
				$tabsControl.find(`li[data-tabidx='${idx}']`).toggle( toggler );
			}
		});		
	},

	toggleGroupEditViewRecordTabs( dElement, toggler, tabsTypes ) {
		const $tabsControl = Runner.pages.PageManager.getDashGroupTabControl( dElement.group, this.id );
		const { PAGE_VIEW, PAGE_EDIT, PAGE_ADD } = Runner.pages.constants;

		tabsTypes = tabsTypes || dElement.tabsPageTypes;
		if ( !$tabsControl.length || (!tabsTypes.includes(PAGE_VIEW) && !tabsTypes.includes(PAGE_EDIT)) ) {
			return;
		}
		
		const addIdx = dElement.tabsPageTypes.indexOf(PAGE_ADD);
		if ( addIdx === -1 ) {
			// there is no add tab, hide the dash-element
			this.markElementEmpty( dElement.elementName, !toggler);
			return;
		}
		
		const $tabNav = $("ul.nav-tabs", $tabsControl);
		// save current active idx
		$tabsControl.data( "activeIdx", $tabNav.children("li").index( $tabNav.children("li.active") ) );
		
		
		// make an 'add' tab active
		//$("a[data-toggle='tab']", $tabNav.children("li").eq( addIdx )).tab("show"); 
		
		if ( !toggler ) {
			// save current active idx
			$tabsControl.data( "activeIdx", $tabNav.children("li").index( $tabNav.children("li.active") ) );
			//	one of element's tab is active
			if ( $tabNav.children(`li.active[data-element="${ dElement.elementName}"]`).length ) {
				//switch to Add
				$tabNav
					.children( `li[data-element="${ dElement.elementName}"][data-tabidx="${addIdx}"]`)
					.children( "a[data-toggle='tab']" )
					.tab("show"); 
			}
				
		} else  {
			//	not implemented, we never get here
		}
		
		dElement.tabsPageTypes.forEach( (type, idx) => {
			if ( type !== PAGE_ADD && tabsTypes.includes(type) ) {
				$tabsControl.find(`li[data-tabidx="${idx}"][data-element="${ dElement.elementName}"]`).toggle( toggler );
			}
		});
		
	},

	/**
	 * @param {?} initiatorName
	 * @param {?} firstRecord
	 * @param {?} lastRecord
	 * @param {?} updContext
	 * @return {?}
	 */
	doReportFirstLastAction: function( initiatorName, firstRecord, lastRecord, updContext ) {
		var cfg = this.getActionBaseConfig( initiatorName, {
				firstRecord: firstRecord,
				lastRecord: lastRecord
			} ),
			action = new Runner.Dashboard.ReportFirstLastAction( cfg );

		return this.doAction( action, updContext );
	},


	doChangeTabAction: function( initiatorName, tabId, updContext ) {
		var cfg = this.getActionBaseConfig( initiatorName, {
				tabId: tabId
			} ),
			action = new Runner.Dashboard.ChangeTabAction( cfg );

		return this.doAction( action, updContext );
	},
	
	/**
	 * @param {string} initiatorName
	 * @param {object} data
	 * @param {object} updContext
	 * @return {?}
	 */	
	doMoveAction: function( initiatorName, data, updContext ) {
		var	masterData, t, masterAction,
			moveAction = new Runner.Dashboard.MoveAction( this.getActionBaseConfig( initiatorName, data ) );
			
		updContext =  this.doAction( moveAction, updContext );
		
		//	prepare MoveMasterActions
		for ( t in data.masterKeys ) {
			masterData = {
				detailsTable: t,
				masterKeys: data.masterKeys[t]
			};
			
			masterAction = new Runner.Dashboard.MoveMasterAction( this.getActionBaseConfig( initiatorName, masterData ) );
			this.doAction( masterAction, updContext );
		}
			
		return updContext;
	},
	
	/**
	 * @param {?} initiatorName
	 * @param {?} goForward
	 * @param {?} updContext
	 * @return {?}
	 */	
	doMoveAdjacentAction: function( initiatorName, goForward, updContext ) {
		var cfg = this.getActionBaseConfig( initiatorName, {
				goForward: goForward
			} ),
			moveAction = new Runner.Dashboard.MoveAdjacentAction( cfg );
		
		return this.doAction( moveAction, updContext );
	},

	/**
	 * @param {string} initiatorName
	 * @param {object} data
	 * @param {object} updContext
	 * @return {?}
	 */
	doReloadListAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.ReloadListAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action, updContext );
	},

	doTimerReloadAction: function( initiatorName, data ) {
		var action = new Runner.Dashboard.TimerReloadAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action );
	},

	/**
	 * @param {?} data
	 * @param {?} initiatorName
	 * @param {?} updContext
	 * @return {?}
	 */
	doLoadAction: function( data, initiatorName, updContext ) {
		data = data || {};
		var action = new Runner.Dashboard.LoadAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action, updContext );
	},
	
	/**
	 * @param {?} initiatorName
	 * @param {?} searchParams
	 * @param {?} updContext
	 * @return {?}
	 */
	doSearchAction: function( initiatorName, searchParams, updContext ) {
		var data = {
				searchParams: searchParams
			},
			action = new Runner.Dashboard.SearchAction( this.getActionBaseConfig( initiatorName, data ) );		
		
		return this.doAction( action, updContext );
	},
	
	/**
	 * @param {object} action //Runner.Dashboard.Action 
	 * @param {object} updContext
	 * @return {?}
	 */
	doAction: function( action, updContext ) {
		var pageObj = this;
		
		// prepare context
		updContext = updContext || new Runner.Dashboard.Context();
		updContext.actions.push( action );
		
		setTimeout( function() {
			// apply action to all elements
			for ( var i in pageObj.elements ) {
				action.apply( pageObj.elements[i], updContext );
			}
		}, 0);
		
		return updContext;
	},
	
	/**
	 * @param {?} initiatorName
	 * @param {?} data
	 * @param {?} updContext
	 * @return {?}
	 */
	doUpdateRecordAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.UpdateRecordAction( this.getActionBaseConfig( initiatorName, data ) ),
			t, masterData, masterAction;

		//	prepare MoveMasterActions
		for ( t in data.masterKeys ) {
			masterData = {
				detailsTable: t,
				masterKeys: data.masterKeys[t]
			};
			
			masterAction = new Runner.Dashboard.MoveMasterAction( this.getActionBaseConfig( initiatorName, masterData ) );
			this.doAction( masterAction, updContext );
		}

		return this.doAction( action, updContext );
	}, 
	
	/**
	 * @param {string} initiatorName
	 * @param {object} data
	 * @param {object} updContext
	 * @return {?}
	 */
	doAddRecordAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.AddRecordAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action, updContext );
	},
	
	/**
	 * @param {string} initiatorName
	 * @param {object} data
	 * @param {object} updContext
	 * @return {?}	
	 */
	doMapViewPortChangedAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.MapViewPortChangedAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action, updContext );	
	},
	
	/**
	 * @param {string} initiatorName
	 * @param {object} data
	 * @param {object} updContext
	 * @return {?}	
	 */	
	doDeleteRecordsAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.DeleteRecordsAction( this.getActionBaseConfig( initiatorName, data ) );
		return this.doAction( action, updContext );			
	},
	
	doNoDataAction: function( initiatorName, data, updContext ) {
		var action = new Runner.Dashboard.NoDataAction( this.getActionBaseConfig( initiatorName, data ) ),
			t, masterData;
		
		if (  data && data.detailTables ) {
			for ( t in data.detailTables ) {
				masterData = {
					detailsTable: t,
				};
				
				masterAction = new Runner.Dashboard.NoDataInMasterAction( this.getActionBaseConfig( initiatorName, masterData ) );
				this.doAction( masterAction, updContext );
			}		
		}
		return this.doAction( action, updContext );	
	},
	
	/**
	 *
	 */
	getActionBaseConfig: function( initiatorName, data ) {
		var initiator = this.getElementByName( initiatorName );
		
		return {
			initiator: initiator,
			dashboard: this,
			table: initiator ? initiator.table : null,
			data: data
		}; 
	},
	
	/**
	 * @param string
	 * @return string
	 */
	getElemenentName: function( itemId ) {
		var $dashItem = this.findItem( itemId );
		var matched = ( $dashItem.attr("id") || "" )
				.match( new RegExp("^dashelement_(.+)" + this.id + "$") );
		
		return matched && matched[1] || "";		
	},		
	
	/**
	 * API
	 * @param string	 
	 * @return {array}: RunnerPage[]
	 */
	getElementPages: function( itemId ) {
		var elementName = this.getElemenentName( itemId ),
			dbelem = this.getElementByName( elementName ),
			pages = [];
			
		if ( !dbelem ) {
			return [];
		}
		
		if ( dbelem.type == Runner.pages.constants.DASHBOARD_RECORD ) {
			pages = [ Runner.pages.constants.PAGE_EDIT, Runner.pages.constants.PAGE_VIEW, Runner.pages.constants.PAGE_ADD ]
				.map( function( type ) {
					return this.getDynamicPageObject( elementName, type );
				}, this );
			
		} else if ( dbelem.type == Runner.pages.constants.DASHBOARD_DETAILS ) {
			pages = dbelem.details.map( function( d ) {
				return this.getDynamicPageObject( elementName, d.dDataSourceTable );
			}, this );		
		} else {
			pages = [ this.getDynamicPageObject( elementName ) ];
		}
		
		return pages.filter( function( page ) { 
				return !!page;
			});
	},
	
	/**
	 * API
	 * @param string
	 * @param string 	pageType | tableName
	 * @return {array}: RunnerPage[]
	 */	
	getElementPage: function( itemId, sub ) {
		var elementName = this.getElemenentName( itemId );
		return this.getDynamicPageObject( elementName, sub ) || null;
	},
	
	loadCodeSnippet: function( dbelem, updContext ) {
		this.dynamicSnippetsData[ dbelem.elementName ] = new Runner.DashboardSnippet( dbelem, this );
	},
	
	destructor: function() {
		Runner.pages.DashboardPage.superclass.destructor.call( this );
		for( const snippet in this.dynamicSnippetsData ) {
			this.dynamicSnippetsData[ snippet ].destructor();
		}
	}
});
	
Runner.namespace('Runner.Dashboard');	

Runner.Dashboard.Context = Runner.extend( Object, {
	/**
	 *
	 */
	actions: null,
	
	constructor: function( ) {
		this.actions = [];
	},
	
	/**
	 * @param {object} dbElem
	 * @param {?} action
	 */	
	continueActions: function( dbElem, action ) {
		// mark action as done
		var i = 0, l = this.actions.length, done;
		
		for ( ; i < l; ++i ) {
			if ( this.actions[i] == action ) {
				done = action.finishPendingTask( dbElem );
				break;
			}
		}
		
		if ( !done ) {
			return;
		}
		// continue with the rest of actions 
		for ( ++i; i < l; ++i ) {
			if ( false === this.actions[i].apply( dbElem, this ) ) {
				break;
			}
		}
	},
	
	/**
	 * @param {object} dbElem
	 * @return {boolean}
	 */
	elementActionPending: function( dbElem ) {
		for ( var i = this.actions.length; i--; ) {
			if ( this.actions[i].pending[ dbElem.elementName ] ) {
				return true;
			}
		}
		return false;
	},
	
	/**
	 * @return {boolean}
	 */	
	hasUpdateRecordAction: function() {
		for ( var i = this.actions.length; i--; ) {
			if ( this.actions[i] instanceof Runner.Dashboard.UpdateRecordAction ) {
				return true;
			}
		}
		return false;
	}
});

Runner.Dashboard.Action = Runner.extend( Object, {
	/**
	 * @type {string}
	 */
	dashboard: null,
	initiator: null,
	appliedTo: null,
	/**
	 *	Element names pending executions
	 */
	pending: null,
	/**
	 *	@type {object} action-specific data
	 */
	data: {},

	handlerMap: null,
	
	constructor: function( cfg ) {
		Runner.apply( this, cfg );
		
		// each object needs its own map and applied list
		this.handlerMap = {};
		this.appliedTo = {};
		this.pending = {};
	},
	
	checkTable: function( dbElem ) {
		return this.table === dbElem.table;
	},
	
	applyToInitiator: function( dbElem ) {
		return false;
	},
	
	applicable: function( dbElem ) {
		return (
			( this.initiator != dbElem || this.applyToInitiator( dbElem ) ) 
			&&  this.handlerMap[ dbElem.type ] 
			&& this.checkTable( dbElem ) ) ;
	},
	/**
	 *
	 */
	apply: function( dbElem, updContext ) {
		var result;
		
		if ( this.appliedTo[ dbElem.elementName ] ) {
			return;
		}
		
		if ( updContext.elementActionPending( dbElem ) ) {
			return;
		}
		
		if ( this.applicable( dbElem ) ) {
			result = this.handlerMap[ dbElem.type ].call( this, dbElem, updContext );
		}
		
		if ( result === false ) {
			this.pending[ dbElem.elementName ] = ( this.pending[ dbElem.elementName ] || 0 ) + 1;
		} else if ( typeof result == "number" && result > 0 ) {
			this.pending[ dbElem.elementName ] = ( this.pending[ dbElem.elementName ] || 0 ) + result;
			result = false;
		} else {
			this.appliedTo[ dbElem.elementName ] = true;
		}
		
		return result;
	},
	
	finishPendingTask: function( dbElem ) {
		if ( !this.pending[ dbElem.elementName ] ) {
			Runner.debugThrow();
			return true;
		}
		
		--this.pending[ dbElem.elementName ];
		
		if ( !this.pending[ dbElem.elementName ] ) {
			this.appliedTo[ dbElem.elementName ] = true;
		}
		
		return !!this.appliedTo[ dbElem.elementName ];
	}
	
});

/**
 *	Reload list of records
 */
Runner.Dashboard.ReloadListAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
			page: <N>
			tab: String, tab id
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.ReloadListAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_REPORT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},
	
	/**
	 *
	 */
	applyList: function( dbElem, updContext ) {
		var baseParams = {};
		
		if ( this.data.page ) {
			baseParams.goto = this.data.page;
		}
		baseParams.tab = this.data.tabId;
		
		this.dashboard.loadPage( dbElem, { 
			updContext: updContext, 
			baseParams: baseParams 
		}, this );
		
		return 1;
	},
	
	/**
	 * @param {object} mapElem
	 * @param {object} updContext
	 * @return {?}
	 */
	applyMap: function( mapElem, updContext ) {
		if ( mapElem.major ) {
			return;
		}		
		
		var pageObj = this.dashboard.getDynamicPageObject( mapElem.elementName );
		
		if ( !pageObj ) {
			Runner.debugThrow();
			return;
		}

		pageObj.setMapMarkers( this.data.markersData[ pageObj.mapId ] );
		if ( this.data.tabId != "" && pageObj.$gridTabControl ) {
			pageObj.$gridTabControl.children().removeClass('active');
			pageObj.$gridTabControl.find('[data-tabid="' + this.data.tabId + '"]').parent().addClass('active');
		}
	}
});


/**
 *	Load 
 */
Runner.Dashboard.LoadAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.LoadAction.superclass.constructor.call(this, cfg);
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applySearch;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_REPORT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_SEARCH ] = this.applySearch;
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_SNIPPET ] = this.applySnippet;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_WIDGET ] = this.applySnippet;
	},

	checkTable: function( dbElem ) {
		return !this.table || this.table === dbElem.table;
	},

	/**
	 * @return {?}
	 */
	applyList: function( dbElem, updContext ) {		
		if ( !dbElem.major || dbElem.masterTable ) {
			return;
		}

		this.dashboard.loadPage( dbElem, { updContext: updContext, baseParams: this.data || {} }, this );
		return 1;
	},

	applyChart: function( dbElem, updContext ) {
		//	load only major elements here
		if ( dbElem.masterTable ) {
			return;
		}
		this.dashboard.loadPage( dbElem, { updContext: updContext }, this );
		return 1;
	},

	applySearch: function( dbElem, updContext ) {
		this.dashboard.loadPage( dbElem, { updContext: updContext, baseParams: this.data }, this );
		return 1;
	},
	
	/**
	 *
	 */
	applyRecord: function( dbElem, updContext ) {		
		var pageTypes = [ Runner.pages.constants.PAGE_ADD ];

		if ( dbElem.masterTable ) {
			return;
		}

		if ( dbElem.major ) {
			pageTypes.push( Runner.pages.constants.PAGE_VIEW );
			pageTypes.push( Runner.pages.constants.PAGE_EDIT );
		}
		
		return this.dashboard.loadSingleRecord( dbElem, pageTypes, { updContext: updContext }, this );
	},
	
	applySnippet: function( dbElem, updContext ) {
		this.dashboard.loadPage( dbElem, { updContext: updContext, baseParams: this.data }, this );
		return 1;
	}
});


Runner.Dashboard.MoveAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
	 *		keys: <array> - array of key field values
	 *		masterKeys: <array> - array of key field values
	 * 		vapCoordinates: "{n:..,w:..,e:..,s:..}" (optional)
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MoveAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyChart;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_DETAILS ] = this.applyDetails;
	},
	
	applyList: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj ) {
			//	???
			return;
		}
		
		return pageObj.moveToKeys( this.data.keys, updContext );	
	},
	
	/**
	 * @return {?}
	 */
	applyMap: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ); 
		
		if ( pageObj ) {
			pageObj.setMarkerActive( null, this.data.keys );
		}
		
		return; //?
	},
	
	/**
	 *
	 */
	applyRecord: function( dbElem, updContext ) {
		var pageParams = {
				updContext: updContext,
				baseParams: {}
			}, i;
		
		for ( i = 0; i < this.data.keys.length; ++i ) {
			pageParams.baseParams[ 'editid' + (i + 1) ] = this.data.keys[ i ];
		}
		
		if ( this.data.vpCoordinates ) {
			pageParams.baseParams.vpCoordinates = this.data.vpCoordinates;
			pageParams.baseParams.mapRefresh = true;
		}
		
		return this.dashboard.loadSingleRecord( dbElem, [ Runner.pages.constants.PAGE_VIEW, Runner.pages.constants.PAGE_EDIT ], pageParams, this );
	},

	/**
	 *
	 */
	applyDetails: function( dbElem, updContext ) {
		var pageParams = {
				updContext: updContext
			};
		
		// unregister ?
		return this.dashboard.loadDetails( dbElem, pageParams, this.data.masterKeys, this );
	},

	applyChart: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		if ( !pageObj ) {
			Runner.debugThrow();
		}
		pageObj.setActiveBar( this.data );
		return;
		// TODO: get Uniq chart object by dbElem after uncoment chart.id set in xtempl.php
		var anyChartId = $("#chart_block" + pageObj.id + " object").attr("id"),
			chart = AnyChart.getChartById(anyChartId); 
		
		chart.selectPoint(this.data.seriesId, this.data.pointId, true);
		return;
	},	
});


/**
 *	Report tab change on a minor element.
 *	This action is sent by minor elements. Major elements are reloaded on this action
 */
Runner.Dashboard.ChangeTabAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
	 *		tabId: id of the tab
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.ChangeTabAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},
	
	applyToInitiator: function( dbElem ) {
		return true;
	},

	applyList: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj || !pageObj.$gridTabControl ) {
			return;
		}
		pageObj.$gridTabControl.find('[data-tabid="' + this.data.tabId + '"]').trigger("click");
		return;
	},
	
	applyMap: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj || !pageObj.$gridTabControl ) {
			return;
		}
		pageObj.processTabChange( updContext, this );
		return 1;
	}
	

});




Runner.Dashboard.MoveAdjacentAction = Runner.extend( Runner.Dashboard.Action, {
	
	/**
	 *	this.data structure : {
	 *		goForward: <bool> - go forward or back
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MoveAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},
	
	/**
	 * @return {?}
	 */
	applyList: function( dbElem, updContext ) {
		//	only the major element handles this action
		if ( !dbElem.major ) {
			return;
		}
		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj ) {
			//	???
			return;
		}
		
		return pageObj.setAdjacentSelection( this.data.goForward, updContext, this );
	},
	
	/**
	 * @return {?}
	 */
	applyRecord: function( dbElem, updContext ) {
		//	only the major element handles this action
		if ( !dbElem.major ) {
			return;
		}
		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName, Runner.pages.constants.PAGE_EDIT ) 
			|| this.dashboard.getDynamicPageObject( dbElem.elementName, Runner.pages.constants.PAGE_VIEW );
		
		if ( !pageObj ) {
			return;
		}
		
		return pageObj.setAdjacentSelection( this.data.goForward, updContext, this );
	}
});


/**
 * The leading map view port was changed.
 * Action reloads dependent grids.
 * @constructor
 */
Runner.Dashboard.MapViewPortChangedAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
			vpCoordinates: "{n:..,e:..,w:..;s:..}",
			markersData: {}
			tabId
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MapViewPortChangedAction.superclass.constructor.call( this, cfg );	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},
	
	/**
	 * @param {object} dbElem
	 * @param {object} updContext
	 * @return {?}
	 */
	applyList: function( dbElem, updContext ) {
		this.dashboard.loadPage( dbElem, { 
			updContext: updContext, 
			baseParams: {
				vpCoordinates: this.data.vpCoordinates,
				mapRefresh: true,
				tab: this.data.tabId
			}
		}, this );
		
		return 1;
	},
	
	/**
	 * @param {object} dbElem
	 * @param {object} updContext
	 * @return {?}
	 */	
	applyMap: function( mapElem, updContext ) {
		if ( mapElem.major ) {
			return;
		}		
		
		var pageObj = this.dashboard.getDynamicPageObject( mapElem.elementName );
		
		if ( pageObj ) {
			pageObj.setMapMarkers( this.data.markersData[ pageObj.mapId ] );
		} else {
			Runner.debugThrow();
		}	
	}
});

/**
 *
 */
Runner.Dashboard.UpdateRecordAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
	 *		keys: <array> - array of new key field values
	 *		detKeys: <array> - array of master key field values
	 *		oldKeys: <array> - array of old key field values
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.UpdateRecordAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_DETAILS ] = this.applyDetails;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},

	applyToInitiator: function( dbElem ) {
		return dbElem.type === Runner.pages.constants.DASHBOARD_RECORD;
	},

	applyList: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj ) {
			return;
		}
		
		pageObj.updateRecord( this.data, updContext, this );	
	},
	
	applyMap: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ),
			recordVals, iconData;
		
		if ( pageObj ) {
			recordVals = Runner.apply( { 
				keys: this.data.keys 
			}, this.data.rawVals );			
			
			iconData = this.data.mapIconsData ? this.data.mapIconsData[ dbElem.elementName ] : "";
			
			pageObj.updateMapAfterGridLineEdited( recordVals, this.data.oldKeys, iconData );
		}		
	},
	
	applyRecord: function( dbElem, updContext ) {
		var pageParams = {
				updContext: updContext,
				baseParams: {}
			} /*jQuery.extend( true, {}, this.data )*/,
			pageTypes = [ Runner.pages.constants.PAGE_VIEW ],
			i;

		//	only reload the View tab in the initiator element
		if ( this.initiator != dbElem ) {
			pageTypes.push( Runner.pages.constants.PAGE_EDIT );
		}
		
		for ( i = 0; i < this.data.keys.length; ++i ) {
			pageParams.baseParams[ 'editid' + (i + 1) ] = this.data.keys[ i ];
		}
		return this.dashboard.loadSingleRecord( dbElem, pageTypes, pageParams, this );
	},
	
	applyDetails: function( dbElem, updContext ) {
		var pageParams = {
				updContext: updContext
			};

		return this.dashboard.loadDetails( dbElem, pageParams, this.data.detKeys, this );
	}
});


Runner.Dashboard.ReportFirstLastAction = Runner.extend( Runner.Dashboard.Action, {
	
	/**
	 *	this.data structure : {
	 *		lastRecord: <bool> - optional flag indicating that 'move next' action is not available
	 *		firstRecord: <bool> - optional flag indicating that 'move prev' action is not available
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MoveAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
	},

	applyRecord: function( dbElem, updContext ) {
		var pageObj, i,
			pageTypes = [ Runner.pages.constants.PAGE_VIEW, Runner.pages.constants.PAGE_EDIT ];
		
		for ( i = 0; i < pageTypes.length; ++i ) {
			pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName, pageTypes[i] );
			if ( pageObj ) {
				pageObj.showNextPrevButtons( !this.data.lastRecord, !this.data.firstRecord );
			}
		}
	}
});

Runner.Dashboard.SearchAction = Runner.extend( Runner.Dashboard.Action, {
	
	/**
	 *	this.data structure : {
	 *		searchParams: <object>
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MoveAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_SEARCH ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_REPORT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},
	
	applyList: function( dbElem, updContext ) {
		//	load only major and search elements
		if ( !dbElem.major && dbElem.type != Runner.pages.constants.DASHBOARD_SEARCH ) {
			return;
		}
		
		this.dashboard.loadPage( dbElem, { baseParams: this.data.searchParams, updContext: updContext }, this );
		return 1;
	},
	
	applyRecord: function( dbElem, updContext ) {
		var pageTypes = [ Runner.pages.constants.PAGE_VIEW, Runner.pages.constants.PAGE_EDIT ];
		
		if (!dbElem.major) {
			return;
		}

		return this.dashboard.loadSingleRecord( dbElem, pageTypes, { baseParams: this.data.searchParams, updContext: updContext }, this );
	}
	
});

Runner.Dashboard.AddRecordAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
		 response object
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.UpdateRecordAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},

	applyToInitiator: function( dbElem ) {
		return dbElem.type === Runner.pages.constants.DASHBOARD_RECORD;
	},

	/** 
	 * @return {?}
	 */
	applyList: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		
		if ( !pageObj ) {
			return;
		}
		
		pageObj.addRecord( this.data, updContext, this, this.data.newRowId );
	},
	
	/**
	 * @return {?}
	 */
	applyMap: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ),
			recordVals, iconData;
		
		if ( pageObj ) {			
			recordVals = Runner.apply( { 
				keys: this.data.keys 
			}, this.data.rawVals );

			iconData = this.data.mapIconsData ? this.data.mapIconsData[ dbElem.elementName ] : "";			
			pageObj.updateMapAfterNewGridLineAdded( updContext, recordVals, iconData );
		}			
	},
	
	/**
	 * @return {number}
	 */
	applyRecord: function( dbElem, updContext ) {
		//	reload only major element
		if ( !dbElem.major ) {
			return;
		}
		
		var pageParams = {
				updContext: updContext,
				baseParams: {}
			},
			pageTypes = [ Runner.pages.constants.PAGE_VIEW, Runner.pages.constants.PAGE_EDIT ], 
			i;
		
		for ( i = 0; i < this.data.keys.length; ++i ) {
			pageParams.baseParams[ 'editid' + (i + 1) ] = this.data.keys[ i ];
		}
		
		return this.dashboard.loadSingleRecord( dbElem, pageTypes, pageParams, this );
	}
});

Runner.Dashboard.DeleteRecordsAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
			keyStrings: [.., .., ..]
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.DeleteRecordsAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
	},
	
	/**
	 * @return {?}
	 */
	applyMap: function( dbElem, updContext ) {
		var mapPageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ),
			recordVals;
		
		if ( mapPageObj ) {			
			mapPageObj.deleteSelectedMarkers( this.data.keyStrings );		
		}			
	}
});

Runner.Dashboard.MoveMasterAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
	 *		masterKeys: <array> - array of key field values
	 *		masterTable: <string> - master table - action initiator
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.MoveMasterAction.superclass.constructor.call(this, cfg);	
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyChart;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_REPORT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;
	},

	checkTable: function( dbElem ) {
		return this.data.detailsTable === dbElem.table && this.table == dbElem.masterTable;
	},
	
	applyList: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}

		var pageParams = {
				updContext: updContext,
				baseParams: {
					mastertable: this.table
				}
			}, i;
			
		for ( i in this.data.masterKeys ) {
			pageParams.baseParams[i] = this.data.masterKeys[i];
		}
		
		this.dashboard.loadPage( dbElem, pageParams, this );
		
		return 1;
	},

	applyChart: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}

		var pageParams = {
				updContext: updContext,
				baseParams: {
					mastertable: this.table,
					mode: "dashdetails",
				}
			}, i;
			
		for ( i in this.data.masterKeys ) {
			pageParams.baseParams[i] = this.data.masterKeys[i];
		}
		this.dashboard.loadPage( dbElem, pageParams, this );
		
		return 1;
	},
	
	/**
	 * @param {object} dbElem
	 * @param {object} updContext
	 * @return {number}
	 */
	applyMap: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}
		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ); 
		
		if ( pageObj ) {
			pageObj.filterMarkersByMaster( this.table, this.data.masterKeys, updContext, this );
			return 1;
		}
		
		return 0; 
	},

	applyRecord: function( dbElem, updContext ) {
		var asyncTasksCount, 
			pageTypes = [], 
			pageParams = {
				updContext: updContext,
				baseParams: {
					mastertable: this.table
				}
			}, i;

		pageTypes.push( Runner.pages.constants.PAGE_ADD );
		
		if ( dbElem.major ) {
			pageTypes.push( Runner.pages.constants.PAGE_EDIT );
			pageTypes.push( Runner.pages.constants.PAGE_VIEW );
		}

		for ( i in this.data.masterKeys ) {
			pageParams.baseParams[i] = this.data.masterKeys[i];
		}

		asyncTasksCount = this.dashboard.loadSingleRecord( dbElem, pageTypes, pageParams, this );
		return asyncTasksCount;
	}
});

	
/**
 *
 */
Runner.Dashboard.NoDataAction = Runner.extend( Runner.Dashboard.Action, {
	constructor: function( cfg ) {
		Runner.Dashboard.NoDataAction.superclass.constructor.call(this, cfg);
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_DETAILS ] = this.applyDetails;
	},
	
	applyRecord: function( dbElem, updContext ) {
		this.dashboard.toggleViewEditSingleRecordTabs( dbElem, false );
		
		this.dashboard.unregisterDashPage( dbElem, dbElem.table, Runner.pages.constants.PAGE_VIEW );
		this.dashboard.unregisterDashPage( dbElem, dbElem.table, Runner.pages.constants.PAGE_EDIT );
	},
	
	applyDetails: function( dbElem, updContext ) {
		var action = this;
		
		this.dashboard.markElementEmpty( dbElem.elementName, true );
		
		$.each( dbElem.details, function( ind, details ) {
			action.dashboard.unregisterDashPage( dbElem, details.dDataSourceTable, details.dType );	
		});
	}
});

/**
 *
 */
Runner.Dashboard.NoDataInMasterAction = Runner.extend( Runner.Dashboard.Action, {
	constructor: function( cfg ) {
		Runner.Dashboard.NoDataInMasterAction.superclass.constructor.call(this, cfg);
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
	},
	
	checkTable: function( dbElem ) {
		return this.data.detailsTable === dbElem.table && this.table == dbElem.masterTable;
	},
	
	applyList: function( dbElem, updContext ) {
		if ( !dbElem.major ) {
			return;
		}

		var pageParams = {
				updContext: updContext,
				baseParams: {
					mastertable: this.table,
					nodata: true
				}
			};
		
		this.dashboard.loadPage( dbElem, pageParams, this );
		
		return 1;
	},
	
	applyRecord: function( dbElem, updContext ) {
		this.dashboard.markElementEmpty( dbElem.elementName, true);
		
		this.dashboard.unregisterDashPage( dbElem, dbElem.table, Runner.pages.constants.PAGE_VIEW );
		this.dashboard.unregisterDashPage( dbElem, dbElem.table, Runner.pages.constants.PAGE_EDIT );
	},
});


Runner.Dashboard.TimerReloadAction = Runner.extend( Runner.Dashboard.Action, {
	/**
	 *	this.data structure : {
			keys: a record to select on the List page
	 *	}
	 */
	constructor: function( cfg ) {
		Runner.Dashboard.TimerReloadAction.superclass.constructor.call(this, cfg);
		this.data = this.data || {};
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_MAP ] = this.applyMap;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_RECORD ] = this.applyRecord;
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_LIST ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CHART ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_REPORT ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_CALENDAR ] = this.applyList;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_GANTT ] = this.applyList;		
		
		this.handlerMap[ Runner.pages.constants.DASHBOARD_SNIPPET ] = this.applySnippet;
		this.handlerMap[ Runner.pages.constants.DASHBOARD_WIDGET ] = this.applySnippet;
	},

	applicable: function( dbElem ) {
		//	apply to initiator only
		return ( this.initiator == dbElem );
	},

	/**
	 * @return {?}
	 */
	applyList: function( dbElem, updContext ) {		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName ),
			reloadData = {
				autoReload: true
			};
		
		if ( !pageObj ) {
			Runner.debugThrow();
		}
		
		reloadData.reloadKeys = this.data.keys;
		
		if ( this.data.resizableColumnsData ) {
			reloadData.resizableColumnsData = this.data.resizableColumnsData;
		}
		
		pageObj.reload( {}, reloadData );
		return 1;
	},

	applyChart: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		if ( !pageObj ) {
			Runner.debugThrow();
		}
		pageObj.reloadChart();
	},
	
	applyMap: function( dbElem, updContext ) {		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		if ( !pageObj ) {
			Runner.debugThrow();
		}
		pageObj.reloadViewportMarkers( updContext, this, { autoReload: true } );
		return 1;
	},
	
	/**
	 *
	 */
	applyRecord: function( dbElem, updContext ) {		
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName, Runner.pages.constants.PAGE_VIEW );
		if ( !pageObj ) {
			Runner.debugThrow();
		}
		
		pageObj.reload( {}, { autoReload: true } );
	},
	
	applySnippet: function( dbElem, updContext ) {
		var pageObj = this.dashboard.getDynamicPageObject( dbElem.elementName );
		pageObj?.reload();

	}
});


Runner.DashboardSnippet = Runner.extend( Runner.emptyFn, {
	constructor: function( dbElem, dashboard ) {
		this.dbElem = dbElem;
		this.dashboard = dashboard;
		
		if ( dbElem.reload ) {
			this.setReloader();
		}
	},
	
	setReloader: function() {
		if ( this.reloadTimerId ) {
			window.clearTimeout( this.reloadTimerId );
		}
		
		this.reloadTimerId = window.setTimeout( () => {
			this.dashboard.doTimerReloadAction( this.dbElem.elementName );
		}, 1000 *  this.dbElem.reload );
	},
	
	reload: function() {
		Runner.runnerAJAX( Runner.pages.getUrl( this.dashboard.tName, this.dashboard.pageType ), {
				id: this.dashboard.pageId,
				page: this.dashboard.pageName,
				tName: this.dashboard.tName,
				pageType: this.dashboard.pageType,
				a: "reloadElement",
				elementName: this.dbElem.elementName,
			}, ( resp ) => {
				$("#dashelement_" + this.dbElem.elementName + this.dashboard.pageId)
					.html( resp.contentHtml );
				this.setReloader();
			});
	},
	
	destructor: function() {
		if ( this.reloadTimerId ) {
			window.clearTimeout( this.reloadTimerId );
		}
	},
});/**
 * The basic class for dashboard list or detail list page
 */
Runner.pages.ListPageDash = Runner.extend( Runner.pages.ListPage, {
	/**
	 * @type {object}
	 */
	baseParams: null,	
	
	/**
	 * The corresponding dashboard element's name
	 * @type {string}
	 */
	dashElement: null,
	
	/**
	 *
	 */
	selectedId: -1,
	
	/**
	 * Flags indicating if there are more pages before or after the current List page
	 */
	firstPage: false,
	lastPage: false,

	dashElementData: null,

	$headerContainer: null,
	$footerContainer: null,
	
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.ListPageDash.superclass.constructor.call(this, cfg);
		
		this.pageCont = $(this.openContainer);
		this.addEvents( "afterPageReady" );
		this.baseParams = this.baseParams || { id: this.pageId };
		this.dashElementData = this.dashboard.getElementByName( this.dashElement );
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		var pageObj = this, 
			$gridElem, reloadActionData;
		
		if( this.dashElementData.tabLocation == 'body' && this.dashElementData.type != 5 /* DETAILS */ ) {
			this.$gridTabControl = $( ".bsgrid_tabs", this.pageCont );
		}
	
		if ( this.pageMode === Runner.pages.constants.LIST_DASHDETAILS ) {
			this.initGridTabs = Runner.pages.ChartPageDPDash.prototype.initGridTabs;
		}
		Runner.pages.ListPageDash.superclass.init.call(this);

		this.moveElements();
		
		$gridElem = $( this.gridElem )
			.removeClass("hoverable");
		
		if ( this.pageMode === Runner.pages.constants.LIST_DASHDETAILS ) {
			return;
		}


		
		this.firstPage = this.controlsMap.pageNumber === 1;
		this.lastPage = this.controlsMap.pageNumber === this.controlsMap.numberOfPages;
		
		reloadActionData = {
			page: this.controlsMap.pageNumber,
			tabId: this.pageData.tabId,
			markersData: this.getMarkersForDependentMaps()
		};
		
		this.selectedId = null;
		if ( this.reloadKeys ) {
			this.moveToKeys( this.reloadKeys, this.updContext );
			this.reloadKeys = null;
		}
		
		if ( this.dashboard.getElementByName( this.dashElement ).major && !this.controlsMap.gridRows.length ) {
			this.dashboard.doNoDataAction( 
				this.dashElement, 
				{
					detailTables : this.pageData.detailTables
				}, 
				this.updContext 
			);
		} else if ( !this.dashAction ) {
			// sorting, pagination, delete etc
			if ( !this.selectedId && this.controlsMap.gridRows.length ) {
				this.setSelection( this.controlsMap.gridRows[0].id );
				this.sendMoveAction();
			}
			this.updContext = this.dashboard.doReloadListAction( this.dashElement, reloadActionData, this.updContext );
			this.fireEvent('afterPageReady', this, this.proxy, this.pageId);
		} else if ( !this.selectedId && this.dashboard.getElementByName( this.dashElement ).major && this.controlsMap.gridRows.length ) {
		//	select proper record in a major element
			if ( !this.moveToLastRecord ) {
				this.setSelection( this.controlsMap.gridRows[0].id );
			} else {
				this.setSelection( this.controlsMap.gridRows[ this.controlsMap.gridRows.length - 1 ].id );
			}
			
			this.moveToLastRecord = false;
			this.updContext = this.dashboard.doReloadListAction( this.dashElement, reloadActionData, this.updContext );
			this.sendMoveAction( this.updContext );
			//	tell reload to other lists/maps
		} 
		
		this.finishDashElementActions();

		this.updContext	= null;

		if ( this.dashboard.getElementByName( this.dashElement ).reload ) {
			//this.silentReloading = true; #12557 2.
			
			if ( this.reloadTimerId ) {
				window.clearTimeout( this.reloadTimerId );
			}
			
			this.reloadTimerId = window.setTimeout( function() {
				var row = pageObj.getRowByRowId( pageObj.selectedId ),
					data = { keys: row && row.keys };
				
				if ( pageObj.bsResizeInitialized ) {
					data.resizableColumnsData = pageObj.getResizableColumnsData();
				}
				
				pageObj.dashboard.doTimerReloadAction( pageObj.dashElement, data );
			}, 1000 * this.dashboard.getElementByName( this.dashElement ).reload );
		}

		// select rows related to this page ( skip details rows )
		var rowSelector = '[data-record-id][data-pageid=' + this.pageId + ']';
		$gridElem.on( 'click', rowSelector, function() {
			var activeId = $(this).data( 'record-id' );		
			if ( pageObj.setSelection( activeId ) ) {
				pageObj.sendMoveAction();
			}
		});
	},
	
	/**
	 * Do a move action. Keep the action's context updating
	 * @param {object} updContext
	 * @return {object}
	 */
	sendMoveAction: function( updContext ) {
		var row = this.getRowByRowId( this.selectedId );
		
		updContext = this.dashboard.doMoveAction( this.dashElement, {
			keys: row.keys, 
			masterKeys: row.masterKeys
		}, updContext );
		
		return this.sendReportNextPrev( updContext );
	},
	
	/**
	 * Do a 'ReportFirstLast' action. Keep the action's context updating
	 * @param {object} updContext
	 * @return {object}
	 */
	sendReportNextPrev: function( updContext ) {
		var firstRecord = false, lastRecord = false, 
			visibleIndex = this.inlineAdd.getRowOrderIndex( this.selectedId );

		if ( this.firstPage ) {
			firstRecord = this.inlineAdd.getRowIdByOrderIndex( visibleIndex - 1 ) <= 0;
		}
		
		if ( this.lastPage ) {
			lastRecord = this.inlineAdd.getRowIdByOrderIndex( visibleIndex + 1 ) <= 0;
		}
		
		updContext = this.dashboard.doReportFirstLastAction( this.dashElement, firstRecord, lastRecord, updContext );
		return updContext;
	},
	
	/**
	 * Inint Inline Add/Edit basing on page's settings
	 */
	initInline: function() {		
		this.inlineButtonsIntialized = false;

		this.initInlineEdit();
		this.initInlineAdd();

		if ( this.inlineAdd || this.inlineEdit ) {
			( this.inlineAdd || this.inlineEdit ).toggleMassRecButt();
		}
	},

	/**
	 * Set scroll position into view record
	 * @param {number} recordId
	 */
	scrollIntoViewRecord: function( recordId ) {
		var $record = $( "[data-record-id=" + recordId + "]", this.gridElem ),
			$scrollDiv = $( ".rnr-dbelembody", this.pageCont );
		
		if ( !$record.length || !$scrollDiv.length ) {
			return;
		}
		
		// if record in viewport
		setTimeout( function() {
			if ( $scrollDiv.offset().top > $record.offset().top || $scrollDiv.offset().top + $scrollDiv.height() < $record.offset().top + $record.height() ) {
				$record.get(0).scrollIntoView();
			}
		}, 500 );
	},
	
	/**
	 * @param {number} recordId
	 * @return {boolean}
	 */
	isRowInGrid: function( recordId ) {
		return !!$( "[data-record-id=" + recordId + "]", this.gridElem ).length;
	},

	/**
	 * Return row id by key values
	 * @param {array} keys
	 * @return {number}
	 */
	getRowIdByKeys: function( keys ) {
		var rows = this.controlsMap.gridRows,
			i;
		
		for (i = 0; i < rows.length; i++) {
			if ( Runner.arraysEqual( keys, rows[i].keys, true ) ) {
				return rows[i].id;
			}
		}
		
		return -1;
	},

	
	/**
	 * Return row id by key value
	 * @param keyField
	 * @param keyValue
	 * @return {number}
	 */
	getRowIdByKey: function( keyField, keyValue ) {
		var rows = this.controlsMap.gridRows,
			i;
		
		for (i = 0; i < rows.length; i++) {
			if ( rows[i].keyFields[0] == keyField && rows[i].keys[0] == keyValue ) {
				return rows[i].id;
			}
		}
		return -2; //?
	},
	
	
	/**
	 * @return {object}
	 */
	getInlineAddParams: function() {
		var params = Runner.pages.ListPageDash.superclass.getInlineAddParams.call( this );
		
		params.dashGridBased = true;
		return params;
	},	
	
	/**
	 * Initialize the inline add functionality
	 */
	initInlineAdd: function() {
		Runner.pages.ListPageDash.superclass.initInlineAdd.call( this );
		
		if ( this.pageMode == Runner.pages.constants.LIST_DASHDETAILS ) {
			return;
		}
		
		this.inlineAdd.on( "afterSubmit", function( vals, fields, keys, rowId, resp ) {
			var updContext = this.dashboard.doAddRecordAction( this.dashElement, resp );
			
			if ( this.mapManager ) {
				this.mapManager.updateMapsOnAfterAddSubmit( vals, fields, keys, rowId, resp );
			}
			
			this.setSelection( rowId );
			
			if ( this.dashboard.getElementByName( this.dashElement ).major ) {			
				this.sendMoveAction( updContext );			
			}
		}, this );
	},	

	/**
	 * @return {object}
	 */
	getInlineEditParams: function() {
		var params = Runner.pages.ListPageDash.superclass.getInlineAddParams.call( this );
		
		params.dashGridBased = true;
		return params;
	},
	
	/**
	 * Initialize the inline edit functionality
	 */
	initInlineEdit: function() {
		if ( this.inlineEdit ) {
			this.inlineEdit.reInit( this.controlsMap.gridRows );
			this.spreadsheet &&	this.inlineEdit.rows
				.forEach( this.initSpreadsheetEidtRow, this );

			return;
		}
	
		Runner.pages.ListPageDash.superclass.initInlineEdit.call( this );
		
		if ( this.pageMode == Runner.pages.constants.LIST_DASHDETAILS || !this.inlineEdit ) {
			return;
		}
		
		this.inlineEdit.on( "afterSubmit", function( vals, fields, keys, rowId, newData, extEditPageObj ) {
			var updContext;
			this.setSelection( rowId );
			updContext = this.dashboard.doUpdateRecordAction( this.dashElement, newData );
			if ( this.dashboard.getElementByName( this.dashElement ).major ) {			
				this.sendReportNextPrev( updContext );			
			}		
		}, this);
	},
	
	deleteClickHandler: function( e ) {
		this.selectedId = -1;
		Runner.pages.ListPageDash.superclass.deleteClickHandler.call( this, e );
	},
	
	ajaxReloadOnDelete: function() {
		return true;
	},

	onSuccessDeleteSelected: function( respObj, formObj, fieldControls ) {
		this.pageReloadHn( respObj );
		this.doDeleteRecordsAction( this.onSuccessDeleteSelected.$selBoxes );

		if ( this.dashboard.getElementByName( this.dashElement ).major && !this.controlsMap.gridRows.length ) {
			this.dashboard.doNoDataAction( 
				this.dashElement, 
				{
					detailTables : this.pageData.detailTables
				}, 
				this.updContext 
			);
		}
	},	
	
	/**
	 * @param {jQuery object} $selBoxes
	 */
	doDeleteRecordsAction: function( $selBoxes ) {
		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			return;
		}
		
		var keyStrings = [];

		$selBoxes.each( function( i, selBox ) {
			keyStrings.push( decodeURIComponent( $(selBox).val() ) );
		});
		
		this.dashboard.doDeleteRecordsAction( this.dashElement, {
			keyStrings: keyStrings
		}, this.updContext );			
	},
	
	
	/**
	 * @deprecated	
	 * @param {string}
	 * @return {array}
	 */
	getMarkersForDependentMap: function( dashMapId ) {
		if ( !this.controlsMap || !this.controlsMap.gMaps || !this.controlsMap.gMaps.mapsData || !this.controlsMap.gMaps.mapsData[ dashMapId ] ) {
			return [];
		}
		
		return this.controlsMap.gMaps.mapsData[ dashMapId ].markers || [];
	},
	
	/**
	 * @return {object}
	 */
	getMarkersForDependentMaps: function() {
		var markersData = {};
		
		if ( !this.controlsMap || !this.controlsMap.gMaps || !this.controlsMap.gMaps.mapsData ) {
			return markersData;
		}
		
		$.each( this.controlsMap.gMaps.mapsData, function( mapId, mapData ) {
			if ( mapData.dashMap ) {
				markersData[ mapId ] = mapData.markers || [];
			}
		});
		
		return markersData;
	},
	
	/**
	 * Get the row order index by its id
	 * @param {number} rowId
	 * @return {number}
	 */
	getRowOrderIndex: function( rowId ) {
		if ( this.inlineAdd ) {
			return this.inlineAdd.getRowOrderIndex( rowId );
		}

		for ( var i = 0; i < this.controlsMap.gridRows.length; ++i ) {
			if ( this.controlsMap.gridRows[ i ].id == rowId ) {
				return i;
			}
		}

		return -1;	
	},
	
	/**
	 * @param {number} rowId
	 * @return {object}
	 */
	getRowByRowId: function( rowId ) {
		if ( this.inlineAdd || this.inlineEdit ) {
			return (this.inlineAdd || this.inlineEdit).getRowById( rowId );
		}
		
		for ( var i = 0; i < this.controlsMap.gridRows.length; ++i ) {
			if ( this.controlsMap.gridRows[ i ].id == rowId ) {
				return this.controlsMap.gridRows[ i ];
			}
		}

		return null;			
	},
	
	/**
	 * Get the row's id by its order index	
	 * @param {number} rowIdx
	 * @return {number}
	 */
	getRowIdByOrderIndex: function( rowIdx ) {
		if ( this.inlineAdd ) {
			return this.inlineAdd.getRowIdByOrderIndex( rowIdx );
		}
		
		return this.controlsMap.gridRows[ rowIdx ].id;
	},
	
	/**
	 *
	 */
	moveToKeys: function( keys, updContext ) {
		var recordId = this.getRowIdByKeys( keys );
		return this.setSelection( recordId, updContext );
	},

	/**
	 *
	 */
	setSelection: function( recordId ) {
		var $row = $("tr.r-gridrow#gridRow" + recordId );
		if ( this.pageMode == Runner.pages.constants.LIST_DASHDETAILS ) {
			return false;
		}
		
		if ( recordId <= 0 || this.selectedId == recordId ) {
			return false;
		}
		
		this.scrollIntoViewRecord( recordId );
		
		this.selectedId = recordId;
		$( ".rnr-dbselectedrow", this.gridElem ).removeClass( "rnr-dbselectedrow" );
		
		if ( $row.length ) {
			$row.addClass( "rnr-dbselectedrow" );
		} else {
			$( "[data-record-id=" + recordId + "]", this.gridElem ).addClass( "rnr-dbselectedrow" );
		}
		
		return true;
	},

	/**
	 *
	 */
	setAdjacentSelection: function( goForward, updContext, action ) {
		var moveToLastRecord, newPage,	
			reloadParams = {},
			selectedIdx = this.getRowOrderIndex( this.selectedId );
		
		if ( selectedIdx === -1 ) {
			return;	
		}
	
		selectedIdx = selectedIdx + ( goForward ? 1 : -1 );
	
		if ( selectedIdx >= 0 && selectedIdx < this.controlsMap.gridRows.length ) {
			// there is a record on the current page
			this.setSelection( this.getRowIdByOrderIndex( selectedIdx ) );
			this.sendMoveAction();
			return;
		}
			
		moveToLastRecord = !goForward && this.controlsMap.pageNumber > 0; 
		newPage = moveToLastRecord ? this.controlsMap.pageNumber - 1 : this.controlsMap.pageNumber + 1;
		
		reloadParams.moveToLastRecord = moveToLastRecord;
		reloadParams.updContext = updContext;
		reloadParams.dashAction = action;
					
		// load new page
		this.reload( { goto: newPage }, reloadParams );
		return false;	
	},
	
	/**
	 *
	 */
	updateRecord: function( data, updContext, action ) {
		var rowId = this.getRowIdByKeys( data.oldKeys ),
			row = this.inlineEdit.getRowById( rowId );

		if ( row ) {
			this.setSelection( row.id );
			this.inlineEdit.updateTableRow( row, data );
			if ( this.spreadsheet ) {
				this.inlineEdit.afterSubmit( row, data );
			}
			
			if ( this.mapManager ) {
				this.mapManager.updateMapsOnAfterEditSubmit( data.vals, data.fields, data.keys, row.id, data );
			}
		}									
		
		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			this.sendReportNextPrev( updContext );
		}
	},
	
	/**
	 *
	 */
	addRecord: function( data, updContext, action, newRowId ) {
		this.hideElement('message');
		
		// fire events to hide save/save all buttons
		var row = this.inlineAdd.addRowToGrid( data, /*this.spreadsheet*/true, newRowId );
		
		this.initNewRowButtons( row );
		this.setSelection( row.id );
		
		//	update other views if major
		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			this.sendMoveAction( updContext );
		}
	},

	/**
	 * POST params for Resize columns Logger
	 * @return {array}
	 */
	getResizeColumnsLoggerParams: function() {
		return {
			saveParam: Runner.pages.constants.CRESIZE_PARAMS_TYPE, 
			data: JSON.stringify( this.getResizableColumnsData() ),
			onDashboard: true,
			dashElementId: this.dashboard.tName + "_" + this.dashElement
		};
	},

	
	/**
	 * @return {boolean}
	 */
	hasInlineEdit: function() {
		return this.inlineEdit && this.dashElementSettings.inlineEdit && Runner.pages.PageSettings.getTableData( this.tName, "inlineEditAvailable" );
	},
	
	/**
	 * @return {object}
	 */
	getSortingReloadParams: function() {
		var reloadParams = Runner.pages.ListPageDash.superclass.getSortingReloadParams.call( this ),
			row = this.getRowByRowId( this.selectedId );
			
		if ( row ) { 
			reloadParams = reloadParams || {};
			reloadParams.reloadKeys = row && row.keys;
		}
		
		return reloadParams;
	},
	
	initSearch: Runner.emptyFn,

	initScrollGridBody: Runner.emptyFn,

	afterPageReady: Runner.emptyFn,

	/**
	 *
	 */
	initGridTabs: function() {
		var pageObj = this, eventName = 'click.' + this.dashElement;
		
		if ( !this.$gridTabControl ) {
			return;
		}
			
		this.$gridTabControl
			.off( eventName )
			.on( eventName, '[data-pageid=' + this.id + ']', function( e ) {
				if ( !pageObj.dashboard.getElementByName( pageObj.dashElement ).major ) {
					pageObj.dashboard.doChangeTabAction( pageObj.dashElement, $(this).data("tabid") );
					return false;
				}

				var baseParams = { 
						tab: $(this).data("tabid")
					},
					reloadParams = pageObj.getExtraPageReloadParams();
					
				pageObj.reload( baseParams, reloadParams );
				e.preventDefault();
			});
	},

	/**
	 * Move above-grid elements to header
	 * Transfer:
	 * - details_found item
	 * - all items from the first ( left-top-most ) cell
	 * - all buttons
	 */
	moveElements: function() {
		if( !this.dashElementData.headerControls ) {
			return;
		}
		var $aboveGridForm = this.layoutHelper.getFormElement("above-grid");
		var $firstCell = $aboveGridForm.find('[data-cellid]:first');
		var $buttonItems = $aboveGridForm.find('[data-button-item]');

		var $displayingItem = this.layoutHelper.findItemType('details_found');
		var $headerCont = $('<div></div>')
			.attr('class', 'rnr-dbebrick rnr-dberight')
			.appendTo( this.$headerContainer );

		$headerCont
			.append( $displayingItem )
			.append( $firstCell.children().not('[data-button-item]') );
		
		if ( !this.dashboard.getElementByName( this.dashElement ).masterTable || !this.baseParams.nodata ) {
			// elem is filtered by master table & master has no records
			$headerCont
				.append( $buttonItems );
		}

		//	update button's style
		var $buttons = $headerCont.find('.btn');
		$buttons
			.removeClass('btn-lg')
			.removeClass('btn-sm')
			.addClass('btn-xs');
			
		$buttons.filter('.btn-primary')
			.removeClass('btn-primary')
			.addClass('btn-default');
	},

	showAddInPopup: function( $link ) {
		return true;
	},
	showEditInPopup: function( $link ) {
		return true;
	},
	showViewInPopup: function( $link ) {
		return true;
	},
});
/**
 * The chart dash page class
 */
Runner.pages.ChartPageDash = Runner.extend( Runner.pages.ChartPage, {
	/**
	 * @type {object}
	 */
	baseParams: null,
	
	/**
	 * @type {boolean}
	 */
	dependentElementsVisible: false,
	
	/**
	 * AnyChart object
	 * @type {object}
	 */
	chartObj: null,

	/**
	 * lastSelectedData {object}
	 */
	lastSelectedData: null,

	dashElementData: null,
	
	/**
	 * @constructor
	 * @param {object} cfg 
	 */
	constructor: function(cfg) {
		Runner.pages.ChartPageDash.superclass.constructor.call(this, cfg);
		
		this.addEvents( "afterPageReady" );
		this.dashElementData = this.dashboard.getElementByName( this.dashElement );
	},
	
	init: function() {
		var detailsMasterKeys;

		if ( this.dashElementData.tabLocation == 'body' && this.pageMode == Runner.pages.constants.CHART_DASHBOARD ) {
			this.$gridTabControl = $( ".bsgrid_tabs", this.pageCont );
		}
		
		Runner.pages.ChartPageDash.superclass.init.call(this);	
		
		this.updatePageWrapper();

		if ( this.pageMode != Runner.pages.constants.CHART_DASHDETAILS ) {
			detailsMasterKeys = this.pageData["detailsMasterKeys"];
			
			this.lastSelectedData = { masterKeys: detailsMasterKeys };
			
			if ( this.dashboard.getElementByName( this.dashElement ).major ) {
				this.updContext = this.dashboard.doReloadListAction( this.dashElement, { masterKeys: detailsMasterKeys }, this.updContext ); 
				this.sendMoveAction( { masterKeys: detailsMasterKeys }, this.updContext );
			}
		}

		this.finishDashElementActions();
	},

	updatePageWrapper: function() {
		if( this.pageData.singleChartPage ) {
			$(this.pageCont).find('.rnr-pagewrapper').attr( 'data-single-chart', '' );
		}
	},
	
	sendMoveAction: function( data, updContext ) {
		this.dashboard.doMoveAction( this.dashElement, {
			pointId: data.pointId, 
			seriesId: data.seriesId,
			masterKeys: data.masterKeys
		}, updContext );
		
	},
	
	handleChartClick: function( data ) {
		this.lastSelectedData = data;
		this.sendMoveAction( data );
	},
	

	setActiveBar: function( data ) {
		this.lastSelectedData = data;
		if ( this.chartObj ) {
			this.chartObj.selectPoint( data.seriesId, data.pointId, true );
		}
	},
	
	/**
	 * @param {object} masterData
	 * @return {boolean}
	 */	
	checkIfMasterKeysUpdated: function( masterData ) {
		return masterData && masterData.seriesId == this.lastSelectedData.seriesId && masterData.pointId == this.lastSelectedData.pointId
	},
	
	/**
	 * @param {object} masterDataD
	 * @param {boolean} masterKeysUpdated
	 */
	refreshDChartData: function(  firstMasterData, masterKeysUpdated ) {
		if ( !firstMasterData ) {
			return;
		}
			
		if ( masterKeysUpdated ) {
			this.lastSelectedData = firstMasterData;
		} 

		// if first point selected
		if ( firstMasterData.pointId != this.lastSelectedData.pointId )
			this.setActiveBar( this.lastSelectedData );

		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			this.handleChartClick( this.lastSelectedData );
		}
	},
	
	initGridTabs: function() {
		this.initGridTabs = Runner.pages.ListPageDash.prototype.initGridTabs;
		this.initGridTabs();
	},
	
	initSearch: Runner.emptyFn
});/**
 * Chart in Dashboard -> Details element
 */
Runner.pages.ChartPageDPDash = Runner.extend( Runner.pages.ChartPageDash, {

	/**
	 * @constructor
	 * @param {object} cfg 
	 */
	constructor: function(cfg) {
		Runner.pages.ChartPageDPDash.superclass.constructor.call(this, cfg);
	},
	
	init: function() {
		Runner.pages.ChartPageDPDash.superclass.init.call(this);
		this.finishDashElementActions();
	},
	/**
	 * Initialize Grid Tabs Where functionality
	 */
	initGridTabs: function() {
		var pageObj = this;
		
		$('[data-pageid=' + this.id + '][data-tabid]').on("click", function(e) {
			
			var $link = $(this),
				$tab = $link.parent(),
				$pane;
			
			if ( $tab.hasClass('active' ) ) {
				return false;
			}
				
			$link.tab( 'show' );

/*			$tab.siblings().removeClass('active');
			$tab.addClass('active');
*/			
			
			//	little trick to control tabs selection after reloading
			pageObj.$originalTab.addClass('active');
			
/*
			if ( $link.data('target') ) {
				$pane = $( $link.data('target') );
				$pane.addClass('active')
					.siblings().removeClass('active');
			}
*/			
			
			var baseParams = { 
					tab: $(this).data("tabid")
				},
				reloadParams = pageObj.getExtraPageReloadParams();
				
			pageObj.reload( baseParams, reloadParams );
			e.preventDefault();
		});
	}	
});Runner.pages.EditPageDash = Runner.extend( Runner.pages.EditPage, {
	/**
	 * The dashboard page object
	 */
	dashboard: null,
	
	/**
	 * The name of the dash element 
	 * where the page is open
	 */
	dashElement: "",
		
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.EditPageDash.superclass.constructor.call( this, cfg );
	},
	
	/**
	 * Initialize the page
	 */	
	init: function() {
		Runner.pages.EditPageDash.superclass.init.call(this);
		
		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			//	don't move record if the page reloaded as a part of update record action
			if ( !this.updContext || !this.updContext.hasUpdateRecordAction() ) {
				this.dashboard.doMoveAction( this.dashElement, {
					keys: this.keys, 
					masterKeys: this.detailsMasterKeys
				}, this.updContext );
			}
			
			this.dashboard.doReportFirstLastAction( this.dashElement, !this.prevKeys, !this.nextKeys, this.updContext );
		}
		
		this.finishDashElementActions();
	},

	/**
	 *
	 */
	showNextPrevButtons: function( next, prev ) {
		this.prevButton.toggleClass("rnr-invisible-button", !prev);
		this.nextButton.toggleClass("rnr-invisible-button", !next);
	},
	/**
	 * Update the navigation buttons visibility
	 * for a page that has a related dash-grid element
	 */
	updateNavigationVisibility: function() {		
		if ( this.dashboard.getElementByName( this.dashElement ).major ) {
			return;
		}

		if ( !this.dashAction || !this.dashAction.data.firstRecord ) {
			this.prevButton.removeClass("rnr-invisible-button");
		}
		
		if ( !this.dashAction || !this.dashAction.data.lastRecord ) {
			this.nextButton.removeClass("rnr-invisible-button");
		}	
	},
	
	/**
	 * Open the page corresponding to the next/previous record
	 * @param {Array} keys
	 * @param {Number} step
	 */
	openPrevNextRecordPage: function( keys, step, updContext ) {
		//	minor element asks grid to move
		if ( !this.dashboard.getElementByName( this.dashElement ).major ) {
			this.dashboard.doMoveAdjacentAction( this.dashElement, step > 0 );
			return;
		}	

		//	major element moves itself
		var pageParams = { 
				baseParams:{} 
			};
		
		keys.forEach( function( key, i ) {
			pageParams.baseParams[ 'editid' + (i + 1) ] = key;
		});
		
		if ( this.pageName ) {
			pageParams.baseParams.page = this.pageName;
		}
		
		pageParams.baseParams = Runner.apply( pageParams.baseParams, this.getStateParams() );
		
		this.dashboard.loadSingleRecord( this.dashboard.getElementByName( this.dashElement ), 
			[ Runner.pages.constants.PAGE_EDIT, Runner.pages.constants.PAGE_VIEW ], pageParams );
	},
	
	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object 
	 * reference. Add the form's 'successSubmit' event handler
	 */
	getForm: function() {
		if ( this.form ) {
			return;
		}
		
		Runner.pages.EditorPage.prototype.getForm.call( this );
		
		this.form.on('successSubmit', function( respObj, basicForm, fieldControls ) {
			if ( respObj.success ) {
				this.dashboard.doUpdateRecordAction( this.dashElement, respObj );
				
				this.detailsMasterKeys = respObj.detKeys;
				this.pageReloadHn( respObj );
				this.fireEvent('afterPageReady', this, this.proxy, this.id);
				this.enablePageButtonsAfterSaving();	
				return true;	
			} 
											
			if ( respObj.lockMessage ) {
				$('.rnr-locking').html( respObj.lockMessage ).show();
				return false; // ?
			}
			
			if ( this.hideCaptcha ) {
				$('.captcha_block').remove();
			}

			this.displayHalfPreparedMessage( respObj.message || "", true );
			this.showElement('message');
			
			this.initLoginLink();
			this.enablePageButtonsAfterSaving();			
			return false;			
		}, this);
	},
	
	/**
	 *
	 */
	setAdjacentSelection: function( goForward, updContext, action ) {
		var pageObj = this;
		this.disablePageButtons();			
		this.unlockPage( function() {
			pageObj.openPrevNextRecordPage( goForward ? pageObj.nextKeys: pageObj.prevKeys, goForward ? 1: -1, updContext );
		});
		
	},
	
	setFirstFocus: Runner.emptyFn,
	setRecountFlagForPopup: Runner.emptyFn	
});
Runner.pages.AddPageDash = Runner.extend( Runner.pages.AddPage, {
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.AddPageDash.superclass.constructor.call(this, cfg);
	},
	
	init: function() {
		Runner.pages.AddPageDash.superclass.init.call( this );	
		this.finishDashElementActions();
	},

	/**
	 * Set 'baseParams' property
	 */
	setBaseParams: function() {
		Runner.pages.AddPageDash.superclass.setBaseParams.call( this );	
		if ( this.isShowDetails ) {
			this.baseParams["editType"] = Runner.pages.constants.ADD_MASTER_DASH;
		}
	},
	
	/**
	 * Assign the 'form' property with the new Runner.form.BasicForm object 
	 * reference. Add the form's 'beforeSubmit', 'successSubmit', 'submitFailed'
	 * events handlers basing on the page's type and settings
	 */	
	getForm: function() {
		if ( this.form ) {
			return;
		}

		Runner.pages.EditorPage.prototype.getForm.call( this );	
		this.addFormEventsForDetails();
	
		var newRowId;

		this.form.on('successSubmit', function( respObj, basicForm, fieldControls ) {
			if ( respObj.success ) {
				respObj.newRowId = newRowId;
				this.dashboard.doAddRecordAction( this.dashElement, respObj );
				
				this.pageReloadHn( respObj );
				this.fireEvent('afterPageReady', this, this.proxy, this.id);
				return true;
			} 
			
			if ( respObj.hideCaptcha ) {
				$('.captcha_block').remove();
			}
			
			if ( respObj.message ) {
				this.displayHalfPreparedMessage( respObj.message, true );
				this.showElement('message');
				
				this.initLoginLink();
			} 	
			
			this.enablePageButtonsAfterSaving();
			return false;
		}, this);

		this.form.on( 'beforeSubmit', function( formObj ) {
			newRowId = Runner.genId();
			formObj.baseParams[ "newRowId" ] = newRowId;
		});
	},
	
	/**
	 * Check if the page's form must use the standard submit process
	 * @return Boolean
	 */
	isSubmitStandard: function() {
		return false;
	},
	
	setFirstFocus: Runner.emptyFn,
	onDetailsSaved: Runner.emptyFn,
	
	/*initDetails: Runner.emptyFn,
	onDetailsSavingFailed: Runner.emptyFn,
	isInlineAddContentChanged: Runner.emptyFn*/
});
/**
 * @class Runner.pages.DashboardMap
 */
Runner.pages.DashboardMap = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * @type {string}
	 */
	pageType: Runner.pages.constants.PAGE_DASHMAP,
	
	/**
	 * @type {string}
	 */
	mapId: "",
	
	/**
	 * An instance of Runner.controls.MapManager 
	 * @type {object}
	 */
	mapManager: null,

	/**
	 * A dashbord page object
	 */
	dashboard: null,
	
	
	/**
	 * @param {object} cfg
	 * @constructor
	 */
	constructor: function( cfg ) {
		Runner.pages.DashboardMap.superclass.constructor.call( this, cfg );

		this.mapId = Runner.goodFieldName(this.dashboard.tName) + '_' + this.dashElement + '_dashMap';
	},
	
	/**
	 * Initialize the page
	 */
	init: function() {
		Runner.pages.DashboardMap.superclass.init.call( this );

		this.initMap();
	},
	
	/**
	 * Initialize the map
	 */	
	initMap: function() {
		var mapCfg = this.getMapCfg(),
			mapPage = this;
		
		if ( !Runner.globalGmapLoader || !mapCfg ) {
			return;
		}
		
		Runner.globalGmapLoader.onLoad( function() {			
			this.mapManager = new Runner.controls.MapManager( mapCfg ); 
			this.mapManager.initMap( mapPage.mapId, mapPage.onMapLoad.bind( mapPage ) );
			
			if ( mapPage.dashboard.getElementByName( mapPage.dashElement ).heatMap ) {
				this.mapManager.initHeatMap( mapPage.mapId );
			}
		}, this );
	},
	
	/**
	 * @return {object}
	 */
	getMapCfg: function() {
		var	mapCfg = this.controlsMap ? this.controlsMap.gMaps : null,
			mapObject = this;
		
		// replace the original Runner.controls.MapManager addMarkerClickEvent method
		mapCfg.addMarkerClickEvent = function( marker, markerData ) {
			this.addOnMarkerClickHandler( marker, mapObject.onMarkerClickHandler.bind( mapObject, marker, markerData ) );	
		};
		
		return mapCfg;
	},
	
	/**
	 * @param {object} marker
	 * @param {object} markerData
	 * @param {object} e ??
	 * @param {object} updContext
	 */
	onMarkerClickHandler: function( marker, markerData, e, updContext ) {
		var moveData = this.getMoveActionData( markerData.keys, markerData.masterKeys );
		
		this.setMarkerActive( markerData );
		return this.dashboard.doMoveAction( this.dashElement, moveData, updContext );	
	},
	
	/**
	 * @param {array} keys
	 * @param {object} masterKeys
	 * @return {object}
	 */
	getMoveActionData: function( keys, masterKeys ) {
		return {
			keys: keys,
			masterKeys: masterKeys
		};
	},
	
	/**
	 * A wrapper
	 * @param {object} markerData
	 * @param {array} keys
	 */
	setMarkerActive: function( markerData, keys ) {
		if ( this.dashboard.getElementByName( this.dashElement ).heatMap ) {
			return;
		}
		
		if ( this.mapManager ) {
			this.mapManager.setMarkerActive( markerData, this.mapId, keys );
		}
	},
	
	/**
	 * @param {object} updContext	 
	 * @param {object} recordVals	 
	 * @param {string} iconData	 
	 */
	updateMapAfterNewGridLineAdded: function( updContext, recordVals, iconData ) {
		if ( this.mapManager ) {
			this.mapManager.updateAfterAdd( -1, recordVals, iconData );
			this.dashboard.doMoveAction( this.dashElement, this.getMoveActionData( recordVals.keys, recordVals.detKeys ), updContext );				
		}	
	},

	/**
	 * @param {object} recordVals
	 * @param {string} iconData	 	 
	 */
	updateMapAfterGridLineEdited: function( recordVals, keys, iconData ) {
		if ( this.mapManager ) {
			this.mapManager.updateAfterEdit( -1, recordVals, keys, iconData );
		}
	},
	
	/**
	 * @param {array} keyStrings
	 */
	deleteSelectedMarkers: function( keyStrings ) {
		if ( this.mapManager ) {
			this.mapManager.deleteSelectedMarkers( this.mapId, keyStrings );
		}	
	},
	
	/**
	 * A stub
	 */
	onMapLoad: function() {
		this.finishDashElementActions();
	},
	
	/**
	 *
	 */
	startTimerReload: function() {
		var pageObj = this;
		
		if ( this.reloadTimerId ) {
			window.clearTimeout( this.reloadTimerId );
		}
		
		if ( this.dashboard.getElementByName( this.dashElement ).reload ) {
			this.reloadTimerId = window.setTimeout( function() {
				var activeMarkerData = pageObj.mapManager.getActiveMarkerData( pageObj.mapId );
				
				if ( activeMarkerData ) {
					pageObj.dashboard.doTimerReloadAction( pageObj.dashElement, { keys: activeMarkerData && activeMarkerData.keys} );
				}
			}, 1000 * this.dashboard.getElementByName( this.dashElement ).reload );
		}
	},
	
	/**
	 * A stub
	 * @param {array} markers	 
	 */
	setMapMarkers: function( markers ) {
	},

	/**
	 *
	 */
	initGridTabs: function() {
		var pageObj = this;
		
		if ( !this.$gridTabControl ) {
			return;
		}
			
		this.$gridTabControl.on('click.' + this.dashElement, '[data-pageid=' + this.id + ']', function( e ) {
			pageObj.$gridTabControl.children().removeClass('active');
			$(this).parent().addClass( 'active' );
			pageObj.dashboard.doChangeTabAction( pageObj.dashElement, $(this).data("tabid") );
			return false;
		});
	},

	onShowTab: function() {
		this.mapObject
	}
	
	
});/**
/**
 * @class Runner.pages.DashboardLeadingMap
 */
Runner.pages.DashboardLeadingMap = Runner.extend( Runner.pages.DashboardMap, {
	/**
	 * @type {boolean}
	 */
	skipMapEvent: false,
	
	/**
	 * @param {object} cfg
	 * @constructor
	 */
	constructor: function( cfg ) {
		Runner.pages.DashboardLeadingMap.superclass.constructor.call( this, cfg );	
	},
	
	/**
	 * @return {object}
	 */
	getMapCfg: function() {
		var mapCfg = Runner.pages.DashboardLeadingMap.superclass.getMapCfg.call( this );

		if ( mapCfg ) {
			mapCfg.mapsData[ this.mapId ].centerLat = 40.71; 
			mapCfg.mapsData[ this.mapId ].centerLng = -74.01;
		}
		
		return mapCfg;
	},
	
	/**
	 *
	 */
	onMapLoad: function() {		
		var pageObj = this;
		this.reloadViewportMarkers( this.updContext );
		
		this.mapManager.onMapViewPortChanged( this.mapId, function() {
			if ( !pageObj.skipMapEvent ) {
				pageObj.reloadViewportMarkers();
			}
		});
	},

	/**
	 * @param {object} updContext (optional)
	 * @param {object} action (optional)
	 */ 
	processTabChange: function( updContext, action ) {		
		var tabId, 
			pageObj = this,
			dashElement = this.dashboard.getElementByName( this.dashElement );
		
		if ( !this.$gridTabControl ) {
			return;
		}
		
		baseParams = Runner.apply( {
			mapRefresh: false,
			tab: action.data.tabId,
			id: this.id
		}, this.baseParams );

		updContext = updContext instanceof Runner.Dashboard.Context ? updContext : null;
		action = action instanceof Runner.Dashboard.Action ? action : null;
		
		Runner.runnerAJAX( this.requestUrl, baseParams, function( respObj ) {
			pageObj.pageReloadHn( respObj, updContext, action );
		});		
	},
	
	
	/**
	 * Update markers in a given viewport
	 * Induce dependent elements to update
	 * 
	 * @param {object} updContext (optional)
	 * @param {object} action (optional)
	 * @param {object} reloadParams (reloadParams)
	 */ 
	reloadViewportMarkers: function( updContext, action, reloadParams ) {		
		var tabId;
		if ( this.mapManager.newMarkerWasAdded( this.mapId ) ) {
			return;
		}
		var dashElement = this.dashboard.getElementByName( this.dashElement );
		
		var vpCoordinates = this.getMapViewPortCoordinates(),
			pageObj = this,
			reloadData, baseParams;
		
		if ( !vpCoordinates ) {
			return;
		}	
		
		baseParams = Runner.apply( {
			vpCoordinates: JSON.stringify( vpCoordinates ),
			mapRefresh: true,
			id: this.id
		}, this.baseParams );

		if ( this.$gridTabControl ) {
			baseParams.tab = this.$gridTabControl.children('.active').find('[data-tabid]').data('tabid');
		}
		
		updContext = updContext instanceof Runner.Dashboard.Context ? updContext : null;
		action = action instanceof Runner.Dashboard.Action ? action : null;
		
		if ( dashElement.clustering || dashElement.heatMap ) {
			reloadData = {
				vpCoordinates: JSON.stringify( vpCoordinates )
			};
			updContext = this.dashboard.doMapViewPortChangedAction( this.dashElement, reloadData , updContext );
			activeMarkerData = this.mapManager.getActiveMarkerData( this.mapId );
			if ( activeMarkerData ) {
				moveData = this.getMoveActionData( activeMarkerData.keys, activeMarkerData.masterKeys );
				this.dashboard.doMoveAction( this.dashElement, moveData, updContext );
			}
			return;
		}

		Runner.runnerAJAX( this.requestUrl, baseParams, function( respObj ) {
			pageObj.pageReloadHn( respObj, updContext, action );
		}, reloadParams && reloadParams.autoReload );		
	},
	
	/**
	 * @return {object}
	 */
	getMapViewPortCoordinates: function() {
		return this.mapManager ? this.mapManager.getMapViewPortCoordinates( this.mapId ) : null;
	},

	/**
	 * @param {array} keys
	 * @param {object} masterKeys
	 * @return {object}
	 */
	getMoveActionData: function( keys, masterKeys ) {
		var data = Runner.pages.DashboardLeadingMap.superclass.getMoveActionData.call( this, keys, masterKeys ),
			vpCoordinates = this.getMapViewPortCoordinates();
		
		if ( vpCoordinates ) {
			data.vpCoordinates = JSON.stringify( vpCoordinates );
		}
		
		return data;
	},
	
	/**
	 * @param {object} marker
	 * @param {object} markerData
	 */
	onMarkerClickHandler: function( marker, markerData, e, updContext ) {
		updContext = Runner.pages.DashboardLeadingMap.superclass.onMarkerClickHandler.call( this, marker, markerData, e, updContext );	
		this.sendReportNextPrev( updContext ); 	
	},
	
	/**
	 * @param {boolean} goForward
	 * @param {object} updContext
	 * @param {object} action
	 */
	setAdjacentSelection: function( goForward, updContext, action ) {
		if ( !this.mapManager ) {
			return;
		}
		
		var markerData = this.mapManager.getPrevNextActiveMarkerData( goForward, this.mapId );
			
		if ( markerData ) {
			this.onMarkerClickHandler( markerData.marker, markerData );
		} 
	},

	/**
	 * @param {boolean} firstRecord (optional)
	 * @param {boolean} lastRecord (optional)
	 */
	sendReportNextPrev: function( updContext, firstRecord, lastRecord  ) {
		if ( firstRecord === undefined ) {
			firstRecord = !this.mapManager.getPrevNextActiveMarkerData( false, this.mapId );
		}
		
		if ( lastRecord === undefined ) {
			lastRecord = !this.mapManager.getPrevNextActiveMarkerData( true, this.mapId );
		}		
			
		this.dashboard.doReportFirstLastAction( this.dashElement, firstRecord, lastRecord, updContext );	
	},
	
	/**
	 * @param {object} updContext	 
	 * @param {object} recordVals	 
	 * @param {string} iconData	 
	 */
	updateMapAfterNewGridLineAdded: function( updContext,  recordVals, iconData ) {
		if ( !this.mapManager ) {
			return;
		}
		
		Runner.pages.DashboardLeadingMap.superclass.updateMapAfterNewGridLineAdded.call( this, updContext, recordVals, iconData );	
		this.sendReportNextPrev( updContext, true );
	},
	
	/**
	 * @param {array} keyStrings
	 */
	deleteSelectedMarkers: function( keyStrings ) {
		var updContext;
		Runner.pages.DashboardLeadingMap.superclass.deleteSelectedMarkers.call( this, keyStrings );
		
		if ( !this.mapManager || this.mapManager.getActiveMarkerData( this.mapId ) ) {
			return;
		}

		var firstMarkerData = this.mapManager.getFirtMarkerData( this.mapId ),
			moveData;
		
		if ( !firstMarkerData ) {
			updContext = this.dashboard.doNoDataAction( this.dashElement );
			return;
		}
		
		moveData = this.getMoveActionData( firstMarkerData.keys, firstMarkerData.masterKeys );
				
		this.mapManager.setMarkerActive( firstMarkerData, this.mapId );
		updContext = this.dashboard.doMoveAction( this.dashElement, moveData, updContext );
		
		this.sendReportNextPrev( updContext, true, false );
	},
	
	/**
	 * @param {object} respObj
	 * @param {object} reloadData
	 * @param {object} updContext (optional)
	 * @param {object} action (optional)
	 */
	pageReloadHn: function( respObj, updContext, action ) {			
		var reloadData = {};
		this.controlsMap = respObj["controlsMap"][ this.tName ][ this.pageType ][ this.id ];
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		
		if ( this.$gridTabControl ) {
			this.$gridTabControl.html( this.pageData.gridTabs );
		}
			
		if ( !this.mapManager ) {
			return;
		}
		
		var mapCfg = this.getMapCfg(),
			activeMarkerData, moveData,
			dontDeleteMarkers = false,
			updateViewport = false;
		
		//	reload markers because viewport changed
		if ( action == null ) {
			dontDeleteMarkers = true;
		}
		//	update viewport if searching or changing tabs. Leave as is if the user has changed viewport or timer-based reloading
		if ( action 
			&& mapCfg.mapsData[ this.mapId ]["markers"]
			&& mapCfg.mapsData[ this.mapId ]["markers"].length
			&& !( action instanceof Runner.Dashboard.TimerReloadAction ) ) {
			
			updateViewport = true;
		}
		
			
		this.skipMapEvent = true;
		this.mapManager.reinitMapMarkers( this.mapId, mapCfg.mapsData[ this.mapId ]["markers"], updateViewport, dontDeleteMarkers );
		this.skipMapEvent = false;

		reloadData.vpCoordinates = JSON.stringify( this.getMapViewPortCoordinates() );
		reloadData.markersData = Runner.pages.ListPageDash.prototype.getMarkersForDependentMaps.call( this );	
		reloadData.tabId = respObj.tabId;
		updContext = this.dashboard.doMapViewPortChangedAction( this.dashElement, reloadData, updContext );
		
		if ( !this.mapManager.hasMarkers( this.mapId ) ) { 
			this.dashboard.doNoDataAction( this.dashElement, null, updContext );
			return;
		}
		
		activeMarkerData = this.mapManager.getActiveMarkerData( this.mapId );
		if ( activeMarkerData ) {
			moveData = this.getMoveActionData( activeMarkerData.keys, activeMarkerData.masterKeys );
			updContext = this.dashboard.doMoveAction( this.dashElement, moveData, updContext );
		}
		
		this.startTimerReload();
		this.sendReportNextPrev( updContext );
		this.finishDashElementActions( updContext, action );
	},

	
	/**
	 * @param {string} masterTable
	 * @param {object} masterKeys
	 * @param {object} updContext
	 * @param {object} action
	 */
	filterMarkersByMaster: function( masterTable, masterKeys, updContext, action ) {
		var idx, 
			mapObj = this,
			baseParams = Runner.apply( {
				mastertable: masterTable
			}, this.baseParams );
		
		for ( idx in masterKeys ) {
			baseParams[ idx ] = masterKeys[ idx ];
		}
		
		baseParams.id = this.id;
		
		Runner.runnerAJAX( this.requestUrl, baseParams, function( respObj ) {
			var markers = respObj.controlsMap[ mapObj.tName ][ mapObj.pageType ][ mapObj.id ].gMaps.mapsData[ mapObj.mapId ].markers || [];
			
			mapObj.skipMapEvent = true;
			mapObj.mapManager.reinitMapMarkers( mapObj.mapId, markers, true );
			mapObj.skipMapEvent = false;
			
			mapObj.reloadViewportMarkers( updContext, action );
		});			
	}

	
});/**
 * @class Runner.pages.DashboardGridBasedMap
 */
Runner.pages.DashboardGridBasedMap = Runner.extend( Runner.pages.DashboardMap, {
	/**
	 *
	 */
	markersData: null, 
	
	/**
	 * @param {object} cfg
	 * @constructor
	 */
	constructor: function( cfg ) {
		Runner.pages.DashboardGridBasedMap.superclass.constructor.call( this, cfg );	
	},
	
	/**
	 * @return {object}
	 */
	getMapCfg: function() {
		var mapCfg = Runner.pages.DashboardGridBasedMap.superclass.getMapCfg.call( this );	
		//add grid markers to the map
		mapCfg.mapsData[ this.mapId ].markers = this.markersData || [];
		
		return mapCfg;
	},

	/**
	 * @param {array} markers
	 */
	setMapMarkers: function( markers ) {
		var mapObj = this;
		
		this.markersData = markers;
		
		if ( mapObj.mapManager ) {
			mapObj.mapManager.reinitMapMarkers( mapObj.mapId, markers, markers.length > 0 );
		} else {
			Runner.debugThrow();
		}
	}		
});
Runner.pages.MembersPage = Runner.extend( Runner.pages.ListPage, {
	
	pageType: Runner.pages.constants.PAGE_ADMIN_MEMBERS,
	
	users: {},
	
	groups: {},
	
	fields: {},
	
	modifiedUsers: {},
	
	searchList: [],
	
	usersFilterState: "all",
	
	isSortTypeAscending: true,
	
	providerLabels: null,
	
	$sourceFilter: null,
	
	
	constructor: function( cfg ) {
		Runner.pages.MembersPage.superclass.constructor.call( this, cfg );
		
		this.users = Runner.pages.PageSettings.getTableData( this.tName, "usersList" );
		this.groups = Runner.pages.PageSettings.getTableData( this.tName, "groupsList" );
		this.fields = Runner.pages.PageSettings.getTableData( this.tName, "fieldsList" );
		
		this.providerLabels = Runner.pages.PageSettings.getTableData( this.tName, "providerLabels" );
	},
	
	init: function() {
		Runner.pages.MembersPage.superclass.init.call(this);
	
		this.initCheckboxes();
		this.initColumnHeaders();
		this.initRowHeaders();
		this.initSearchBox();
		this.initModifiedFilter();
		this.initSort();
		
		this.initSave();
		this.initReset();
		this.initChooseColumns();
		
		this.updateRowHeaderStates();
		this.updateColHeaderStates();
		
		this.initSourceFilter();
	},
	
	/**
	 * Handle grid checkboxes clicks
	 */ 
	initCheckboxes: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$( ".adminMembersTable" ).on( "click", "td:has(.r-admin-checkbox:not(.columnheader):not(.rowheader))", function( e ) {
			// checkbox end cell
			var $checkbox = $( e.target ).add( $( e.target ).find( ".r-admin-checkbox" ) ).filter( ".r-admin-checkbox" ), //?
				userid = $checkbox.attr( "data-userid" ),
				group = $checkbox.attr( "data-group" ),
				newchecked = $checkbox.attr( "data-checked" ) == "1" ? 0 : 1,
				newUserState = {};
			
			// if checkbox disabled and checked (state 3)
			if ( pageObj.users[ userid ][ "groups" ][ group ] == 3 ) {
				return;
			}
			
			newUserState[ group ] = newchecked;
			pageObj.updateModifiedState( newUserState, userid );
			pageObj.updateRowHeaderState( userid );
			pageObj.updateColHeaderStates();
			pageObj.updateRowModifiedState( userid );
			pageObj.updateControlsState();
			
			$checkbox.attr( "data-checked", newchecked );
			return false;
		});
	},
	
	/**
	 * Handle clicks on the checkboxes in column headers
	 */
	initColumnHeaders: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$( ".adminMembersTable" ).on( "click", ".columnheader", function(e) {
			var $checkbox = $( e.target ),
				group = $checkbox.data( "group" ),
				newchecked = $checkbox.attr( "data-checked" ) == "1" ? 0 : 1;
			
			$( ".adminMembersTable" ).find( "[id^=box" + group + "]:visible" ).each( function() {
				var $checkbox = $( this );
					userid = $checkbox.data( "userid" );
					newUserState = {};
				
				// if checkbox disabled and checked (state 3)
				if ( pageObj.users[ userid ][ "groups" ][ group ] == 3 ) {
					return;
				}
				
				newUserState[ group ] = newchecked;
				$checkbox.attr( "data-checked", newchecked );
				pageObj.updateModifiedState( newUserState, userid );
				pageObj.updateRowHeaderState( userid );
				pageObj.updateRowModifiedState( userid );
			});
			
			pageObj.updateColHeaderStates();
			pageObj.updateControlsState();
			return false;
		});
	},
	
	/**
	 *	Handle clicks on the checkboxes in row headers - near user names
	 */
	initRowHeaders: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$( ".adminMembersTable" ).on( "click", ".rowheader", function( e ) {
			var $checkbox = $( e.target ),
				newchecked = $checkbox.attr( "data-checked" ) == "1" ? 0 : 1,
				userid = $checkbox.data( "userid" ),
				newUserState = {};
			
			$.each( pageObj.groups, function( groupId, groupName ) {
				// if checkbox disabled and checked (state 3)
				if ( pageObj.users[ userid ][ "groups" ][ groupId ] == 3 || !pageObj.isVisibleField( groupId ) ) {
					return;
				}
				$( "#box" + groupId + userid ).attr( "data-checked", newchecked );
				newUserState[ groupId ] = newchecked;
			});
			
			pageObj.updateModifiedState( newUserState, userid );
			pageObj.updateColHeaderStates();
			pageObj.updateRowHeaderState( userid );
			pageObj.updateRowModifiedState( userid );
			pageObj.updateControlsState();
			return false;
		});
	},
	
	/**
	 * Updates the modifiedUsers object
	 */
	updateModifiedState: function( newGroupsStates, userId ) {
		var pageObj = this,
			savedUserGroupsStates = this.users[ userId ].groups,
			modifiedUserGroupsStates = this.modifiedUsers[ userId ] || {};
		
		$.each( newGroupsStates, function( group, newGroupState ) {
			if ( typeof savedUserGroupsStates[ group ] !== 'undefined' && savedUserGroupsStates[ group ] != newGroupState ) {
				modifiedUserGroupsStates[ group ] = newGroupState;
			} else {
				delete modifiedUserGroupsStates[ group ];
			}
		});
		
		if ( !$.isEmptyObject( modifiedUserGroupsStates ) ) {
			this.modifiedUsers[ userId ] = modifiedUserGroupsStates;
		} else {
			delete this.modifiedUsers[ userId ];
		}
	},
	
	/**
	 * Sets proper state to the tri-state checkbox in the row header. 
	 */	
	updateRowHeaderState: function( userId ) {
		var	pageObj = this,
			$checkbox = $( '#rowbox' + userId ),
			savedStates = this.users[ userId ][ "groups" ],
			modifiedStates = this.modifiedUsers[ userId ] || {},
			unsavedState = $.extend( {}, savedStates, modifiedStates ),
			summState = 0,
			count = 0;
		
		// get state row box
		$.each( unsavedState, function( group, state ) {
			if ( !pageObj.isVisibleField( group ) ) {
				return;
			}
			
			// if checkbox disabled and checked (state 3) convert to checked (state 1)
			state = state == 3 ? 1 : state;
			summState += state;
			count++;
		});
		
		if ( summState == 0 ) { // none checked
			$checkbox.attr("data-checked", 0);
		} else if ( summState == count ) { // all checked
			$checkbox.attr("data-checked", 1);
		} else { // partially checked
			$checkbox.attr("data-checked", 2);
		}
	},
	
	/**
	 * Runs updateRowHeaderState for all rows
	 */
	updateRowHeaderStates: function() {
		var pageObj = this;
		
		$.each( this.users, function( userId, userInfo ) {
			pageObj.updateRowHeaderState( userId );
		});
	},
	
	/**
	 * Sets proper state to the tri-state checkbox in the column header. 
	 */	
	updateColHeaderStates: function() {
		var pageObj = this,
			colBoxStates = {};
		
		// init colBoxStates
		$.each( pageObj.groups, function( groupId, groupName ) {
			colBoxStates[ groupId ] = { "summState": 0, "count": 0 };
		});
		
		// get states column boxs
		$.each( this.users, function( userId, userInfo ) {
			if ( userInfo[ "visible" ] ) {
				var	unsavedGroups = $.extend({}, userInfo[ "groups" ], pageObj.modifiedUsers[ userId ]); // unsaved state
				
				$.each( unsavedGroups, function( group, state ) {
					// if checkbox disabled and checked (state 3) convert to checked (state 1)
					state = state == 3 ? 1 : state;
					colBoxStates[ group ][ "summState" ] += state;
					colBoxStates[ group ][ "count" ]++;
				});
			}
		});
		
		// set states column boxs
		$.each( colBoxStates, function( group, state ) {
			var $colBox = $( "#colbox" + group );
			
			if ( state[ "summState" ] == 0 ) { // none checked
				$colBox.attr( "data-checked", 0 );
			} else if ( state[ "summState" ] == state[ "count" ] ) { // all checked
				$colBox.attr( "data-checked", 1 );
			} else { // partially checked
				$colBox.attr( "data-checked", 2 );
			}
		});
	},
	
	/**
	 * Yellow colors the row with the given user.
	 */
	updateRowModifiedState: function( userId ) {
		var pageObj = this,
			modifiedGroups = this.modifiedUsers[ userId ] || {},
			savedGroups = this.users[ userId ][ "groups" ],
			userModified = false;
		
		$.each( savedGroups, function( group, state ) {
			var groupModified = group in modifiedGroups;
			
			// update cells color
			$( "#cell" + group + userId ).toggleClass( "r-rights-modified", groupModified );
			userModified = userModified || groupModified;
		});
		
		// update username cell color
		$( "#cellusername" + userId ).toggleClass( "r-rights-modified", userModified );
		$( "#cellDisplayName" + userId ).toggleClass( "r-rights-modified", userModified );
		$( "#cellEmail" + userId ).toggleClass( "r-rights-modified", userModified );
	},
	
	/**
	 * Handle search box input
	 */
	initSearchBox: function() {
		var pageObj = this;
		
		$("#adminMembersSearch").on("input", function() {
			pageObj.searchList = $( this ).val().trim().toUpperCase().match(/[^\s]+/g) || [];
			pageObj.showHideRows();
		});
	},
	
	/**
	 *	Refreshes the users list - displays and hides every single row there
	 */
	showHideRows: function() {
		var pageObj = this;	
		
		$.each( this.users, function( userId, userInfo ) {
			pageObj.toggleJQElement( $("#usernamerow" + userId ), pageObj.isUserDisplayed( userId ) );
		});
		
		this.updateColHeaderStates();
	},
	
	/**
	 * Checks wheter the user shoud be displayed according to the current filtering criteria
	 */
	isUserDisplayed: function( userId ) {
		var pageObj = this,
			show = true, i,
			userName = this.users[ userId ][ "userName" ] || "",
			upperCaseUserName = userName.toUpperCase() ,
			upperCaseDisplayUserName = (this.users[ userId ][ "displayUserName" ] || "").toUpperCase(),
			upperCaseEmailUser = (this.users[ userId ][ "emailUser" ] || "").toUpperCase(),
			allModifiedGroupsVisible = false;
		
		if ( this.searchList.length ) {
			show = false;
			for (i = this.searchList.length - 1; !show && i >= 0 ; --i) {
				show = this.searchList[i] 
					&& ( upperCaseUserName.indexOf( this.searchList[i] ) >= 0 
						|| upperCaseDisplayUserName.indexOf( this.searchList[i] ) >= 0 
							|| upperCaseEmailUser.indexOf( this.searchList[i] ) >= 0 );
			}
		}
		
		var mUserId = userId; //?	
		if ( mUserId in this.modifiedUsers ) {
			$.each( this.modifiedUsers[ mUserId ], function( groupId, groupState ) {
				allModifiedGroupsVisible = allModifiedGroupsVisible || pageObj.isVisibleField( groupId );
			});
		}
		
		if ( this.$sourceFilter && this.$sourceFilter.length ) {
			// filter users by provider
			var provider = this.$sourceFilter.val();
			show = show && ( provider == "" || provider == this.users[ userId ]["provider"] );
		}
		
		//	check modified filter state
		show = show && ( this.usersFilterState == 'all' 
			|| ( this.usersFilterState == 'modified' && userId in this.modifiedUsers && allModifiedGroupsVisible ) );
		
		this.users[ userId ][ "visible" ] = show;
		return show;
	},
	
	/** 
	 * Hide element with inline display style 
	 * Delete inline display style to show
	 */
	toggleJQElement: function( $elem, show ) {
		if ( !show ) {
			$elem.hide();
		} else {
			$elem.css( 'display', '' );
		}
	},
	
	/**
	 * Handle all/modified rows dropdown. 
	 * Filter users by their modified state.
	 */
	initModifiedFilter: function() {
		var pageObj = this,
			$control = $("#linkselectFilter");
		
		$control.on( "click", "a", function( e ) {
			pageObj.usersFilterState = $(this).parent().data('value');
			pageObj.showHideRows();
			
			$control.find( '.btn' ).text( $(this).text() );
			$control.find('.dropdown-toggle').trigger("click");
			return false;
		});		
	},
	
	/**
	 *	set sort icon
	 */
	setSortIcon: function( $link ) {
		// delete all sort icons
		$( "#userNameSort" ).removeAttr( "data-icon" );
		$( "#displayNameSort" ).removeAttr( "data-icon" );
		$( "#EmailSort" ).removeAttr( "data-icon" );
		$( "#SourceSort" ).removeAttr( "data-icon" );
		
		
		$.each( this.groups, function( groupId, groupName ) {
			$( "#colsort" + groupId ).removeAttr( "data-icon" );
		});
		
		// set icon for current link
		$( '.rnr-orderlink > .glyphicon' ).remove();
		$( '<span></span>')
			.insertAfter( $link )
			.addClass( 'glyphicon' )
			.addClass( this.isSortTypeAscending
				? "glyphicon-arrow-up" 
				: "glyphicon-arrow-down"
			);
		//$( $link ).attr( "data-icon", this.isSortTypeAscending ? "sortasc" : "sortdesc" );
	},
	
	/**
	 * sort
	 *
	 */
	sort: function( $link, field, otherSortFieldsData ) {
		var $firstRow = $( ".adminMembersTable" ).find( "tr.rnr-row:first" ),
			$insertRow, group,
			orderedUsers = [],
			pageObj = this;
		
		// init sort array and variables
		$.each( this.users, function( userId, userInfo ) {
			// concat orderedUsers and modifiedUsers
			userInfo[ "groups" ] = $.extend( {}, userInfo[ "groups" ], pageObj.modifiedUsers[ userId ] );
			userInfo = $.extend( {}, {"userId": userId}, userInfo );
			
			orderedUsers.push( userInfo );
		});
		
		// toggle sort type ascending/descending
		this.isSortTypeAscending = !this.isSortTypeAscending;
		this.setSortIcon( $link );
		
		group = $( $link ).data( "group" );
		
		// sorting
		orderedUsers.sort( function( a, b ) {
			var result = 0,
				_a = a, _b = b;
			
			if ( group ) {	// sort by group
				a = a[ "groups" ][ group ] || 0;
				b = b[ "groups" ][ group ] || 0;
			} else {	// sort by field (uername, email or display username)
				a = a[ field ];
				b = b[ field ];
				if ( field == "provider" ) {
					a = pageObj.providerLabels[ a ] || "";
					b = pageObj.providerLabels[ b ] || "";
				}
			}
			
			if ( a > b ) {
				result = pageObj.isSortTypeAscending ? 1 : -1;
			} else if ( b > a ) {
				result = pageObj.isSortTypeAscending ? -1 : 1;
			}
			
			if ( result == 0 && otherSortFieldsData ) {
				// compare basing on other fields
				result = pageObj.subSortCallback( otherSortFieldsData, 0, _a, _b );
			}
			
			return result;
		});
		
		// print result
		orderedUsers.forEach( function( userInfo, idx ) {
			var rowid = "usernamerow" + userInfo[ "userId" ];
			if ( $firstRow.attr( "id" ) != rowid && userInfo[ "visible" ] ) {
				$insertRow = $( "#" + rowid );
				$firstRow.after( $insertRow );
				$firstRow = $insertRow;
			}
		});
	},
	
	/**
	 * Compare a and b elements basing on ordering fields data recursively
	 * @param {array} data  Ordering fields data [{ field: ..., dir: ... }, ...]
	 * @param {number} idx
	 * @param {array} a 
	 * @param {array} b
	 * @return {number}
	 */	
	subSortCallback: function( data, idx, a, b ) {
		if ( data[ idx ] === undefined ) {
			return 0;
		}
		
		var field = data[ idx ]["field"],
			dir = data[ idx ]["dir"];
		
		if ( a[ field ] > b[ field ] ) {
			return dir == "asc" ? 1 : -1;
		}

		if ( a[ field ] < b[ field ] ) {
			return dir == "asc" ? -1 : 1;
		}

		this.subSortCallback( data, idx + 1, a, b );
	},
	
	/**
	 *	init sort link
	 */
	initSort: function() {
		var pageObj = this;
		
		// click on link sort by user name
		$( "#userNameSort" ).on( "click", function( e ) {
			pageObj.sort( this, 'userName' );
			return false;
		});
		
		// click on link sort by display name
		$( "#displayNameSort" ).on( "click", function() {
			pageObj.sort( this, 'displayUserName' );
			return false;
		});
		
		// click on link sort by email
		$( "#EmailSort" ).on( "click", function() {
			pageObj.sort( this, 'emailUser' );
			return false;
		});
		
		// click on link sort by source
		$( "#SourceSort" ).on( "click", function() {
			pageObj.sort( this, "provider", [{ field: "userName", dir: "asc" }] );
			return false;
		});		
		
		
		// click on link sort by group
		$.each( this.groups, function( groupId, groupName ) {
			$( "#colsort" + groupId ).on( "click", function() {
				pageObj.sort( this );
				return false;
			});
		});
	},
	
	/**
	 * Save everything
	 */
	initSave: function() {
		var pageObj = this;
		
		$( "#saveBtn" ).on( "click", function() {
			if ( Runner.isDisabledButton( this ) ) {
				return false;
			}
			
			pageObj.disableControls();
			
			var submitData = Object.keys( pageObj.modifiedUsers ).map( function( userId ) {
				var userInfo = pageObj.users[ userId ];
				return {
					username: userInfo.userName,
					provider: userInfo.provider,
					groups: pageObj.modifiedUsers[ userId ]
				};
			});

			$.ajax({
				type: "POST", 
				data: {
					values: JSON.stringify( submitData ),
					a: 'saveMembers'
				},
				dataType: "json",
				error: function( jqXHR, textStatus, errorThrown ) {
					Runner.displayGenericAjaxError(jqXHR.responseText);
					pageObj.updateControlsState();
				},
				success: function( _respObj ) {
					var respObj = _respObj || { error:"Error. Null response" };
					
					if ( !respObj.success ) {
						pageObj.displayMessage( respObj.error, true, true );
						pageObj.updateControlsState();
						return false;
					}
					
					// update data
					$.each( pageObj.modifiedUsers, function( userId, groups ) {
					
						pageObj.users[ userId ][ "groups" ] = $.extend( {}, pageObj.users[ userId ][ "groups" ], groups );
						delete pageObj.modifiedUsers[ userId ];
						pageObj.updateRowModifiedState( userId );
					});
					pageObj.updateControlsState();
				}
			});
			
			return false;
		});
	},
	
	disableControls: function() {
		Runner.addDisabledClass( $('#resetBtn') );
		Runner.addDisabledClass( $('#saveBtn') );
		this.setPageModified( false );
	},

	enableControls: function() {
		Runner.delDisabledClass( $('#resetBtn') );
		Runner.delDisabledClass( $('#saveBtn') );
		this.setPageModified( true );
	},
	
	updateControlsState: function() {
		if ( $.isEmptyObject( this.modifiedUsers ) ) {
			this.disableControls();
		} else {
			this.enableControls();
		}
	},
	
	/**
	 * Reset everything to saved values
	 */
	initReset: function() {
		var pageObj = this;
		
		$( "#resetBtn" ).on( "click", function() {
			if ( Runner.isDisabledButton( this ) ) {
				return false;
			}
			
			$.each( pageObj.modifiedUsers, function( userId, groups ) {
			
				$.each( groups, function( groupId, state ) {
					state = state ? 0 : 1;
					$( "#box" + groupId + userId ).attr( "data-checked", state );
				});
				delete pageObj.modifiedUsers[ userId ];
				pageObj.updateRowModifiedState( userId );
				pageObj.updateRowHeaderState( userId );
			});
			pageObj.updateColHeaderStates();
			pageObj.updateControlsState();
			return false;
		});
		
		this.updateControlsState();
	},
	
	/**
	 * @param {string} userName
	 * @return {number} user ID
	 */
	getUserIdByUserName: function( userName ) {
		var userId = 0;
		
		$.each( this.users, function( idx, userInfo ) {
			if ( userInfo[ "userName" ] == userName ) {
				userId = idx;
				return false;
			}
		});
		return userId;
	},
	
	/**
	 * Open fly window "choose columns"
	 */
	initChooseColumns: function() {
		var pageObj = this,
			args = {
				modal: true,
				centered: true,
				bodyContent: '',
				headerContent: Runner.lang.constants.CHOOSE_COLUMNS_TO_DIPLAY,
				footerContent: this.getChooseColumnsFooter()
			};
			
		$( '#chooseColumnsButton' ).on( 'click', function( e ) {
			Runner.pages.PageManager.createBSWin( args, false, pageObj.flyWinHandler.bind( pageObj ) );
			return false;
		});
	},
	
	/**
	 * @return string
	 */
	getChooseColumnsFooter: function() {
		return '<a href="#" id="mpSelectAll" class="btn btn-primary">' + Runner.lang.constants.SELECT_ALL + '</a>'
			+ '<a href="#" id="mpAASelectNone" class="btn btn-default">' + Runner.lang.constants.SELECT_NONE + '</a>'
			+ '<a href="#" id="mpAAOk" class="btn btn-default">' + Runner.lang.constants.OK + '</a>'
			+ '<a href="#" id="mpAACancel" class="btn btn-default">' + Runner.lang.constants.CANCEL + '</a>';
	},
	
	/**
	 * Select all/none checkbox to fly win
	 * @param {number} state
	 * @param {array} modifiedFields
	 */
	selectAllNoneToFlyWin: function( state, modifiedFields ) {
		$.each( this.fields, function( idx, fieldInfo ) {
			var $cb = $( "#" + idx + "fieldCheckbox" ),
				currentState = parseInt( $cb.attr( "data-checked"), 10 );
				
			if ( currentState != state ) {
				$cb.attr( "data-checked", state );
				modifiedFields[ idx ] = { "visible": state };
			}
		});
	},
	
	/**
	 * After create popup handler
	 * @param {object} modalWrapper A wrapper for BootStrap Modal
	 */
	flyWinHandler: function( modalWrapper ) {
		if ( !modalWrapper && !modalWrapper._obj ) {
			return false;
		}

		var pageObj = this,
			modifiedFields = {},
			$rows = $( "<div></div>" ),
			$modal = modalWrapper._obj;
		
		// init rows checkboxes
		$.each( this.fields, function( idx, fieldInfo ) {
			var $row = $( '<a class="chooseColumnsLink"></a>' ).text( " " + fieldInfo[ "caption" ] ), 
				$checkBox = $( '<span class="r-admin-checkbox rowheader" id="' + idx + 'fieldCheckbox"></span>' )
					.attr( "data-checked", fieldInfo[ "visible" ] )
					.prependTo( $row );
			
			// on row or checkbox click
			$row.on( "click", function() {
					var newChecked = $checkBox.attr( "data-checked" ) == "1" ? 0 : 1;
				
					$checkBox.attr( "data-checked", newChecked );
					modifiedFields[ idx ] = { "visible": newChecked };
				})
				.appendTo( $rows );
		});
		
		$rows.appendTo( $(".modal-body", $modal) );

		$("#mpSelectAll", $modal).on("click", function(e) {
			pageObj.selectAllNoneToFlyWin( 1, modifiedFields );
			return false;
		});
		
		$("#mpAASelectNone", $modal).on("click", function(e) {
			pageObj.selectAllNoneToFlyWin( 0, modifiedFields );
			return false;
		});	
		
		$("#mpAAOk", $modal).on("click", function(e) {
			$.each( modifiedFields, function( idx, fieldInfo ) {
				var fieldName = pageObj.fields[ idx ][ "name" ],
					sortLink = idx === "0" ? $( "#displayNameSort" ) : $( "#colsort" + fieldName );
				
				// update fields array
				pageObj.fields[ idx ] = $.extend( {}, pageObj.fields[ idx ], fieldInfo );
				
				// hide or show fields
				pageObj.toggleJQElement( $( "#tdsort" + fieldName ), fieldInfo[ "visible" ] );
				pageObj.toggleJQElement( $( "#tdbox" + fieldName ), fieldInfo[ "visible" ] );
				pageObj.toggleJQElement( $( "[id^=cell" + fieldName + "]" ), fieldInfo[ "visible" ] );
				
				// if sort set on hidden field
				if ( sortLink.attr( "data-icon" ) ) {
					// toggle sort type ascending/descending
					pageObj.isSortTypeAscending = !pageObj.isSortTypeAscending;
					pageObj.sort( $( "#userNameSort" ), 'userName' );
				}
				 
				// update members search width 
				if ( fieldName == "DisplayName" || fieldName == "Email" ) {
					var $membersSearchTh = pageObj.findItemType( "members_search" ).closest('[data-cellid]');
					if ( $membersSearchTh.attr("colspan") === undefined ) {
						// no span attr
						return;
					}
					var colspan = parseInt( $membersSearchTh.attr("colspan") || 0, 10 );
					$membersSearchTh.attr( "colspan", fieldInfo[ "visible" ] ? colspan + 1 : colspan - 1 );
				}
			});
			
			pageObj.updateRowHeaderStates();
			pageObj.showHideRows();
			
			$modal.modal("hide");
			return false;
		});
		
		$("#mpAACancel", $modal).on("click", function(e) {
			$modal.modal("hide");
			return false;
		});
	},
	
	/**
	 * check field visible
	 * @param {string} fieldName
	 * @return {boolean}
	 */
	isVisibleField: function( fieldName ) {
		var visible = false;
		
		$.each( this.fields, function( idx, fieldInfo ) {
			if ( fieldInfo[ "name" ] == fieldName && fieldInfo[ "visible" ] == 1 ) {
				visible = true;
				return false;
			}
		});
		
		return visible;
	},
	
	/**
	 * @param {boolean} expired
	 * @return {string}
	 */ 
	getLogoutUrl: function( expired ) {
		return Runner.getPageUrl( this.pageType ) 
			+ "?a=logout" + ( expired ? "&reason=expired" : "" );
	},
	
	/**
	 *
	 */
	initSourceFilter: function() {
		var pageObj = this;	
		this.$sourceFilter = this.findItemType( "members_source_filter" ).find("select")
			.on( "change", function(e) {
				pageObj.showHideRows();
			});
	},
});/**
 * The base class for a rights page
 */
Runner.pages.RightsPage = Runner.extend( Runner.pages.ListPage, {
	/**
	 * Type of the page 
	 */	
	pageType: Runner.pages.constants.PAGE_ADMIN_RIGHTS,
	
	/**
	 * Text message for adding new group alert
	 * {string}
	 */
	TEXT_AA_ADD_NEW_GROUP: Runner.lang.constants.AA_ADD_NEW_GROUP,
	
	/**
	 * Text message for renaming new group alert
	 * {string}
	 */
	TEXT_AA_RENAMEGROUP: Runner.lang.constants.AA_RENAMEGROUP,
	
	/**
	 * List of existing groups
	 * {array}
	 */
	groups: null,

	/**
	 * List of existing tables
	 * {array}
	 */
	tables: null,

	/**
	 * List of existing pages in format
	 * { table: { pageId: permissionType} }
	 */
	pages: null,
	
	/**
	 * Set of mask chars
	 * {object}
	 */
	masks: {},
	
	modifiedRights: {},
	modifiedRestrictedPages: {},
	
	shortTables: {},
	
	tablesFilterState: "all",
	
	SEARCHLIST: [],
	
	menuOrderedTables: null,

	alphaOrderedTables: null,
	
	disabledMode: false,
	
	allExpanded: false, 
	
	orderMethod: 'menu',
	
	$groupControl: null,

	/**
	 * Cached tables rows
	 * tableName => array of main row element and pages rows ("show pages")
	 */
	tableRowsMap: {},	
	
	/**
	 *
	 */
	constructor: function(cfg) {
		var pageObj = this;
		Runner.pages.RightsPage.superclass.constructor.call( this, cfg );
		
		// transfer keys from prototype to own properties
		this.groups = Runner.apply( {}, Runner.pages.PageSettings.getTableData( this.tName, "groups" ) );
		
		this.savedRights = Runner.pages.PageSettings.getTableData( this.tName, "rights" );
		this.savedPageRestrictions = Runner.pages.PageSettings.getTableData( this.tName, "pageRestrictions" );
		this.tables = Runner.pages.PageSettings.getTableData( this.tName, "tables" );
		this.pages = Runner.pages.PageSettings.getTableData( this.tName, "allPages" ) || {};
		this.pageMasks = Runner.pages.PageSettings.getTableData( this.tName, "pageMasks" );
		this.menuOrderedTables = Runner.pages.PageSettings.getTableData( this.tName, "menuOrderedTables" );
		this.alphaOrderedTables = Runner.pages.PageSettings.getTableData( this.tName, "alphaOrderedTables" );
		
		//	fill table short to full table name lookup object
		jQuery.each( this.tables, function(table) {
			pageObj.shortTables[ this[0] ] = table;
		});
		
		this.$groupControl = $( '[data-groups]' );
	},
	
	init: function() {
		Runner.pages.RightsPage.superclass.init.call(this);

		Runner.addDisabledClass( $('#resetBtn') );
		Runner.addDisabledClass( $('#saveBtn') );
		
		this.initRowsCache();
		this.initMasks();
		this.initSearchBox();
		this.initGroupsList();

		this.initColumnHeaders();
		this.initRowHeaders();
		this.initCheckboxes();
		this.initPageCheckboxes();
		this.initShowHidePages();
		
		this.initGroupsButtons();
		this.initModifiedFilter();
		this.initOrderControl();
		this.initCopyPermissions();
		
		this.initExpandAll();
		this.initExpandCollapseControls();
		
		this.initSave();
		this.initReset();
		
		this.showHideRows();
	},
	
	/**
	 * Fill the masks object
	 */
	initMasks: function() {
		this.masks = {
			add: 'A',
			edt: 'E',
			del: 'D',
			lst: 'S',
			exp: 'P',
			imp: 'I',
			adm: 'M'
		}
	},
	
	/**
	 * form displayPopup afterCreate param for buttons dialogs
	 */
	getPopupAfterCreateHn: function( onButtonSaveClick ) {
		var onButtonSaveClickHn = function( win, groupName ) {
			var success = onButtonSaveClick( groupName );
			if ( success ) {
				win.close();
				return;
			} 
			
			$('.alert-danger', win.getContentNode() ).show();
			Runner.pages.PageManager.correctBSWindowSize( win._obj );				
		};
		
		return function( win ) {
			var context = win.getContentNode();
			
			var $grName = $('.groupname', context )
				.css('width', '300px')
				.trigger("focus")
				.on("keydown", function(e) {
					if ( e.keyCode == 13 ) { 
						onButtonSaveClickHn( win, $(this).val() );
					} 
				});
			
			$('.alert-danger', context ).hide();
			
			$('#saveGroupBtn', context ).on("click", function() {
				onButtonSaveClickHn( win, $grName.val() );
				return false;
			});
		};
	},
	
	/**
	 * Init buttons working with groups 
	 */
	initGroupsButtons: function() {
		var pageObj = this, 
			afterClosePopup = this.disableGroupButtons.bind( this, false ),
			afterCreatePopupAdd = this.getPopupAfterCreateHn( this.addNewGroup.bind( this ) ),
			afterCreatePopupRename = this.getPopupAfterCreateHn( this.renameGroup.bind( this ) );

		$('[data-add-group][data-type="%db"]').on("click", function(e) {
				if ( Runner.isDisabledButton( this ) ) {	
				return false;
			}
			
			var params = {
				header: pageObj.TEXT_AA_ADD_NEW_GROUP,
				width: 340,
				height: 200,
				afterClose: afterClosePopup,
				afterCreate: afterCreatePopupAdd,
				html: "<div class='alert alert-danger'></div>"
					+ "<div>"
						+ "<input class='groupname form-control' type='text' value='" 
							+ pageObj.makename( Runner.lang.constants.AA_GROUP_NEW ) + "'>"
					+ "</div>",
				footer: "<a id='saveGroupBtn' class='btn btn-primary' href='#'>Save</a>",	
			};
			
			Runner.displayPopup( params );
			pageObj.disableGroupButtons( true );	
			return false;
		});

		$('[data-add-group][data-type="%ad"]').on("click", function(e) {
			var $button = $( this );
			if ( Runner.isDisabledButton( this ) ) {	
				return false;
			}
			
			var pageParams = {
				tName: "<global>",
				pageType: Runner.pages.constants.PAGE_ADMIN_ADMEMBERS, 
				pageMode: Runner.pages.constants.MEMBERS_PAGE,
				parObj: pageObj,
				modal: true,
				baseParams: {
					table: "<global>",
					provider: $button.data("provider")
				},
				requestUrl: Runner.getPageUrl( "admin_admembers_list" )
			};
			
			Runner.pages.PageManager.openPage( pageParams );
			pageObj.disableGroupButtons( true );
			return false;
		});
		
		$('#delGroupBtn').on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {	
				pageObj.deletegroup(); 
			}
			return false; 
		});
		
		$('#renGroupBtn').on("click", function(e) {
			var selectedIdVal = pageObj.currentGroup 
				|| pageObj.$groupControl.find("li.active").find("a").attr("value");
			
			if ( Runner.isDisabledButton(this) || selectedIdVal < 0 ) {	
				return false;
			}
			
			var params = {
				header: pageObj.TEXT_AA_RENAMEGROUP,
				width: 340,
				height: 200,
				afterClose: afterClosePopup,
				afterCreate: afterCreatePopupRename,
				html: "<div class='alert alert-danger'></div>"
					+ "<div><input class='groupname form-control' type='text' value='" 
						+ pageObj.groups[ selectedIdVal ].label + "'></div>",
				footer: "<a id='saveGroupBtn' class='btn btn-primary' href='#'>Save</a>",		
			};

			Runner.displayPopup( params );
			pageObj.disableGroupButtons( true );
			return false;
		});
	},
	
	/**
	 * Disable 'Add', 'Del', 'Rename' buttons or enable it according to param
	 * @param {boolean} isDisable
	 */	
	disableGroupButtons: function( isDisable ) {
		if ( isDisable ) {
			Runner.addDisabledClass( $('[data-add-group]') );
			Runner.addDisabledClass( $('#delGroupBtn') );
			Runner.addDisabledClass( $('#renGroupBtn') );
			Runner.addDisabledClass( $('#copyPermissions') );
			
			$("li").addClass("disabled");
		} else {
			Runner.delDisabledClass( $('[data-add-group]') );
			Runner.delDisabledClass( $('#copyPermissions') );
			
			$("li").removeClass("disabled");
			
			$('[data-add-group]').prop('disabled', isDisable);
			this.setButtonsActiveState();
		}
	},
	
	/**
	 * Set state for Delete and Rename buttons according to selected group
	 */
	setButtonsActiveState: function() {
		var $activeLink = $("ul.nav-tabs").find("li.active").find("a"),
			$renButton = $('#renGroupBtn'),
			$delButton = $('#delGroupBtn');
		
		var updateButton = function( $button, show ) {
			if( show ) {
				Runner.delDisabledClass( $button );
			} else {
				Runner.addDisabledClass( $button );
			}
			$button.css( "opacity", show ? 100 : 0 );
		}

		var hideDelButton = $('.group :selected').val() < 0 || $activeLink.attr("value") < 0,
			hideRenButton = hideDelButton || !$activeLink.is('[data-renameable]');

		updateButton( $delButton, !hideDelButton );
		updateButton( $renButton, !hideRenButton );

		if( !hideDelButton ) {
			// ??? class hover not init. reinit buttons
			this.initRunnerButtons();
			$('.groupname').val( (this.groups[ $('.group :selected').val() ] || {}).label );
		}
	},
	
	/**
	 * Delete selected group
	 */
	deletegroup: function() {
		var pageObj = this,
			id = $("ul.nav-tabs").find("li.active").find("a").attr("value");
		
		if ( id < 0 ) {
			return;
		}
		
		if ( !confirm( Runner.lang.constants.AA_DELETEGROUP + " " + this.groups[ id ].label + '?' ) ) { 
			return;
		}
		
		Runner.runnerAJAX( Runner.getPageUrl('ug_group'), {
				id: id,
				a: 'del'				
			}, function( respObj ) {
				var groupCount = 0,
					newId;
					
				if ( !respObj.success ) {
					$('.alert-danger').html('Error deleting record!');
					return false;
				}
				
				delete pageObj.groups[ id ];
				
				if ( !pageObj.modifiedRights.hasOwnProperty( id ) ) {
					pageObj.modifiedRights = Runner.apply( {}, pageObj.modifiedRights );
				} 			
				
				delete pageObj.modifiedRights[ id ];

				newId = $("ul.nav-tabs").find("li.active").prev().find("a").attr("value");
				
				$("ul.nav-tabs").find("li.active").remove();
				
				if (newId) {
					$("a[value=" + newId + "]").trigger("click");
				} else {
					$("[role=presentation] :first").find("a").trigger("click");
				}
				
				pageObj.updateGroupsModifiedStates();
		});
	},
	
	/**
	 * request to add new group
	 * @param string name
	 * @return boolean	 
	 */
	addNewGroup: function( name ) {
		// Array.prototype.find isn't supported by IE9
		var existGroupName = !!Object.keys( this.groups ).filter( function( ind ) {
				return this.groups[ ind ].label == name; 
			}, this).length;			
				
		if ( existGroupName ) {
			$('.alert-danger').html('Group with such name is already exist!');
			return false;
		}		
		
		var pageObj = this;
		Runner.runnerAJAX( Runner.getPageUrl('ug_group'), {
				name: name,
				a: 'add'
			}, function( respObj ) {
				if ( !respObj.success ) {
					// popup is closed, message is shown on the main page
					pageObj.displayMessage( 'Error adding group!', true );
					return false;
				}
				
				var groupIdx = respObj.id; 
				var $groupTab = $("<a data-renameable value='" + groupIdx + "' href='#'></a>")
					.text( name );
		
				pageObj.hideElement('message'); //?
				pageObj.groups[ groupIdx ] = { label: name };
		
				pageObj.$groupControl
					.append( $("<li role='presentation'></li>").append( $groupTab ) );		
				
				$groupTab.trigger("click");
			});
		
		return true;	
	},
	
	/**
	 * request to rename a group
	 * @param string name
	 * @return boolean
	 */	
	renameGroup: function( name ) {
		var	groupIdx = this.currentGroup 
			|| this.$groupControl.find("li.active").find("a").attr("value");			
		
		// Array.prototype.find isn't supported by IE9		
		var existGroupName = !!Object.keys( this.groups ).filter( function( ind ) {
				return this.groups[ ind ].label == name && groupIdx != ind
			}, this).length;			
						
		if ( existGroupName ) {
			$('.alert-danger').html('Group with such name is already exist!');
			return false;
		}	
		
		var pageObj = this;	
		Runner.runnerAJAX( Runner.getPageUrl('ug_group'), {	
				id: groupIdx,
				name: name,
				a: 'rename'
			}, function( respObj ) {
				if ( !respObj.success ) {
					// popup is closed, message is shown on the main page
					pageObj.displayMessage( 'Error renaming group!', true );
					return;
				}
				
				pageObj.groups[ groupIdx ].label = name;
				// change $groupTab name
				pageObj.$groupControl
					.find("a[value='" + groupIdx + "']")
						.text( name );
			});
		
		return true;
	},	
	
	/**
	 * Reset everything to saved values
	 */
	initReset: function() {
		var pageObj = this;
		
		$('#resetBtn').on("click", function() {
			if ( !Runner.isDisabledButton( this ) ) {
				pageObj.modifiedRights = {};
				pageObj.modifiedRestrictedPages = {};
				
				pageObj.groupListChange();
				pageObj.updateGroupsModifiedStates();
				pageObj.showHideRows();
			}
			return false;
		});
	},

	disableControls: function() {
		this.disabledMode = true;
		this.setPageModified( false );
		Runner.addDisabledClass( $('#resetBtn') );
		Runner.addDisabledClass( $('#saveBtn') );
	},

	enableControls: function() {
		this.disabledMode = false;
		this.setPageModified( true );
		Runner.delDisabledClass( $('#resetBtn') );
		Runner.delDisabledClass( $('#saveBtn') );
	},
	

	/**
	 * Returns all modified permissions data in the proper format to submit to server
	 * { group: {
	 * 		table: {
	 * 			permissions: <string>,
	 * 			pages: <array>
	 * 		}
	 * }}
	 */
	getModifications: function() {
		var ret = {},
			pageObj = this;
		//	fill in groups
		Object.keys( this.modifiedRights ).forEach( function( g ) { 
			ret[ g ] = {}; 
		});
		Object.keys( this.modifiedRestrictedPages ).forEach( function( g ) { 
			ret[ g ] = {}; 
		});

		Object.keys( ret ).forEach( function( g ) {
			var tableRights = pageObj.modifiedRights[g] || {},
				tablePages = pageObj.modifiedRestrictedPages[g] || {};
			
			Object.keys( tableRights ).forEach( function( t ) {
				ret[g][t] = {
					permissions: tableRights[t],
					pages: tablePages[t] || pageObj.getSavedRestrictedPages( t, g )
				};
			});
			Object.keys( tablePages ).forEach( function( t ) {
				ret[g][t] = ret[g][t] || {
					permissions: pageObj.getTableSavedPermissions( t, g ),
					pages: tablePages[t] 
				};
			});
		});
		return ret;
	},

	/**
	 * Save everything
	 */
	initSave: function() {
		var pageObj = this;
		$('#saveBtn').on("click", function(e) {
			if ( Runner.isDisabledButton( this ) ) {
				return false;
			}
			pageObj.disableControls();
			pageObj.hideElement("message");
			
			$.ajax({
				type: "POST", 
				data: {
					data: JSON.stringify( pageObj.getModifications() ),
					a: 'saveRights'
				},
				dataType: "json",
				error: function( jqXHR, textStatus, errorThrown ) {
					Runner.displayGenericAjaxError(jqXHR.responseText);
					pageObj.enableControls();
				},
				success: function( _respObj ) {
					var respObj = _respObj || { error:"Error. Null response"};
					if ( !respObj.success ) {
						pageObj.displayMessage( respObj.error, true, true );
						pageObj.enableControls();
						return;
					}
					//	merge modifiedRights into savedRights
					jQuery.each( pageObj.modifiedRights, function( group, permissionsObj ) {
						jQuery.each( permissionsObj, function( table, perm ) {
							if ( typeof pageObj.savedRights[ table ] == 'undefined' ) {
								pageObj.savedRights[ table ] = {};
							}
							pageObj.savedRights[ table ][ group ] = perm;
						});
					});

					//	merge modifiedPages into savedPages
					jQuery.each( pageObj.modifiedRestrictedPages, function( group, permissionsObj ) {
						jQuery.each( permissionsObj, function( table, perm ) {
							if ( typeof pageObj.savedPageRestrictions[ table ] == 'undefined' ) {
								pageObj.savedPageRestrictions[ table ] = {};
							}
							pageObj.savedPageRestrictions[ table ][ group ] = perm;
						});
					});
					
					pageObj.modifiedRights = {};
					pageObj.modifiedRestrictedPages = {};
					pageObj.groupListChange();
					pageObj.updateGroupsModifiedStates();
					pageObj.showHideRows();
					pageObj.disabledMode = false;
				}
			});
			return false;
		});
	},
	
	/**
	 * Make a name for a new group
	 * @param {string} groupName	The name of the previous group 
	 */
	makename: function( groupName ) {
		var n = 1,
			tgroupName = groupName,
			found;
			
		while (1) {
			found = !!Object.keys( this.groups ).filter( function( groupId ) { 
				return this.groups[ groupId ].label == tgroupName;
			}, this ).length;

			if ( !found ) {
				return tgroupName;
			}
			tgroupName = groupName + n;
			n++;
		}
	},

	/**
	 * Handle show/hide pages links
	 */ 
	initShowHidePages: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$(".rightsTable").on("click.pages", "[data-show-pages]", function( e ) {
			var $link = $(this);
			var shortTable = $link.data('table');
			$("tr[data-table=" + shortTable + "]").attr('data-hidden', null );
			$( "[data-table=" + shortTable + "][data-hide-pages]" ).attr('data-hidden', null );
			$link.attr('data-hidden', '' );
			return false;
		});

		$(".rightsTable").on("click.pages", "[data-hide-pages]", function( e ) {
			var $link = $(this);
			var shortTable = $link.data('table');
			$("tr[data-table=" + shortTable + "]").attr('data-hidden', '' );
			$( "[data-table=" + shortTable + "][data-show-pages]" ).attr('data-hidden', null );
			$link.attr('data-hidden', '' );
			return false;
		});
	},
	

	/**
	 * Handle grid checkboxes clicks
	 */ 
	initCheckboxes: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$(".rightsTable").on("click.checkboxes", "td:has(.r-admin-checkbox:not(.columnheader):not(.rowheader):not([data-page]))", function(e) {
			if ( pageObj.disabledMode ) {
				return false;
			}
			
			var $checkbox = $(this).find(".r-admin-checkbox"),
				checked = $checkbox.attr("data-checked") == "1",
				newchecked = checked ? "0" : "1",
				shortTable = pageObj.getTableNameFromBox( $checkbox.attr("id") ),
				table = pageObj.shortTables[ shortTable ],
				permission = $checkbox.data('permission'),
				modifiedMask;
			
			$checkbox.attr("data-checked", newchecked);
			
			//	update modified state
			modifiedMask = pageObj.updateModifiedState( pageObj.readTablePermissionsFromGrid( shortTable ), shortTable );

			pageObj.updatePagePermissions( table, permission, newchecked == "1", true );
			pageObj.updatePagesInGrid( shortTable );

			pageObj.updateRowModifiedState( shortTable, modifiedMask );
			pageObj.updateRowHeaderState( table );
			pageObj.updateColHeaderStates();
			pageObj.updateGroupsModifiedStates();
			return false;
		});
	},

	initPageCheckboxes: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$(".rightsTable").on("click.checkboxes", "td:has(.r-admin-checkbox:not(.columnheader):not(.rowheader)[data-page])", function(e) {				
			if ( pageObj.disabledMode ) {
				return false;
			}
			
			var $checkbox = $( this ).find(".r-admin-checkbox"),
				checked = $checkbox.attr("data-checked") == "1",
				newchecked = checked ? "0" : "1",
				shortTable = $checkbox.data("table"),
				table = pageObj.shortTables[ shortTable ],
				page = $checkbox.data("page");
		
			pageObj.updateRestrictedPage( table, page, checked );
			
			var permission = pageObj.pages[ table ][ page ];
			
			if ( pageObj.updateTablePermissions( shortTable, page, newchecked === "1" ) ) {
				pageObj.getBoxObj( permission, shortTable ).attr("data-checked", newchecked);
				pageObj.updateRowModifiedState( shortTable, permission );
				pageObj.updateRowHeaderState( table );
				pageObj.updateColHeaderStates();
			}
			
			pageObj.updatePagesInGrid( shortTable );
			$checkbox.attr("data-checked", newchecked);
			pageObj.updateGroupsModifiedStates()		
			return false;
		});
	},

	/**
	 * Update table permissions when user modifies page permissions
	 * @param {*} table 
	 * @param {*} permission 
	 * @param {*} enable 
	 * @returns {Boolean} true if table permissions were modified
	 */
	updateTablePermissions: function( shortTable, page, enable ) {
		var table = this.shortTables[ shortTable ];
		var permission = this.pages[ table ][ page ];
		var currentPermissions = this.getTableActivePermissions( table );
		var tablePages = this.pages[ table ];
		
		if ( enable && currentPermissions.indexOf( permission ) === -1 ) {
			//	enable permission
			this.updateModifiedState( currentPermissions + permission, shortTable );
			// make all other pages restricted
			Object.keys( tablePages ).forEach( function( p ) {
				if ( p != page && tablePages[p] === permission ) {
					this.updateRestrictedPage( table, p, true );
				}
			}, this );
			
			return true;
		} else if ( !enable ) {
			// check if there ane page in this permission group that is enabled
			var activePages = this.activeRestrictedPages( table );
			
			 // Array.prototype.find isn't supported by IE
			var enabledPage = !Object.keys( tablePages ).filter( function( page ) {
					return tablePages[ page ] === permission && activePages[ page ] === undefined;
				}).length;			
			
			if ( enabledPage ) {
				// remove page permissions
				var idx = currentPermissions.indexOf( permission );
				this.updateModifiedState( currentPermissions.substring( 0, idx ) + currentPermissions.substring( idx + 1 ), shortTable );
				return true;
			}
		}
		return false;
	},

	/**
	 * Update page permissions after table permissions were modified
	 * @param {string} table
	 * @param {string} permission 
	 * @param {boolean} enable 
	 * @param {boolean} updateAllPages - true when user clicks on specific permission/table checkbox. Must enable all pages in this case
	 * 					false when clicks on column header. 
	 */
	updatePagePermissions: function( table, permission, enable, updateAllPages ) {
		var pageObj = this;
		var group = this.currentGroup;
		var groupObj = this.modifiedRestrictedPages[ group ];
		if ( !groupObj ) {
			groupObj = {};
			this.modifiedRestrictedPages[ group ] = groupObj;
		}
		var pages = groupObj[ table ];
		if ( !pages ) {
			pages = JSON.parse( JSON.stringify( 
				this.savedPageRestrictions[ table ] 
				? this.savedPageRestrictions[ table ][ group ] || {} 
				: {} ) );
			groupObj[ table ] = pages;
		}
		var pagePermTypes = this.pages[ table ];

		if ( enable ) {
			if ( updateAllPages ) {
				//	remove all pages that use this permission from the list of modified
				Object.keys( pages ).forEach( function( p ) {
					if ( pagePermTypes[ p ] === permission ) {
						delete pages[ p ];
					}
				} );
			} else {
				//	if all pages are restricted, enable them all
				// do nothing if there is at least one unrestricted page
				var activePages = this.activeRestrictedPages( table );
				var tablePages = this.pages[ table ]
				/*var enabledPage = Object.keys( tablePages ).find( function( page ) {
					return tablePages[ page ] === permission && activePages[page] === undefined;
				}) === undefined ;*/
				
				var enabledPage = !Object.keys( tablePages ).filter( function( page ) {
						return tablePages[ page ] === permission && activePages[ page ] === undefined;
					}).length;
				
				if ( enabledPage ) {
					Object.keys( pages ).forEach( function( p ) {
						if ( pagePermTypes[ p ] === permission ) {
							delete pages[ p ];
						}
					});
				}
			}
		}
		if ( this.pageListsEqual( groupObj[table], this.getSavedRestrictedPages( table, group ) ) ) {
			delete groupObj[table];
		}
	},

	updatePagesInGrid: function( shortTable ) {
		var group = this.currentGroup,
			pageObj = this,
			table = this.shortTables[ shortTable ],
			tableRights = this.getTableActivePermissions( table, group ),
			tableSavedRights = this.getTableSavedPermissions( table, group ),
			activePages = this.activeRestrictedPages( table ),
			savedPages = this.getSavedRestrictedPages( table );
			
			// update page checkboxes
		Object.keys( pageObj.pages[table] || {} ).forEach( function( page ) {
			var enabled = tableRights.indexOf( pageObj.pages[table][page] ) !== -1 && !activePages[ page ];
			var wasEnabled = tableSavedRights.indexOf( pageObj.pages[table][page] ) !== -1 && !savedPages[ page ];
			
			pageObj.getPageBoxObj( pageObj.pages[ table ][ page ], shortTable, page )
				.add( pageObj.getWholePageBoxObj( shortTable, page ) )
				.attr("data-checked", enabled ? "1" : "0" );
				
			var $elems = pageObj.getTablePagesElements( shortTable );
			$elems.find( '#pagecell' + pageObj.pages[ table ][ page ] + shortTable + '_' + page )
				.closest( 'tr' )
				.toggleClass('r-rights-modified', wasEnabled != enabled );
		} );
	},

	/**
	 * Handle clicks on the checkboxes in column headers
	 */
	initColumnHeaders: function() {
		var pageObj = this;
			
		//	click on a checkbox's parent td
		$(".rightsTable").on("click", "td:has(.r-admin-checkbox.columnheader[data-perm][data-checked!='-1'])", function(e) {				
			if ( pageObj.disabledMode ) {
				return;
			}			
			
			var $checkbox = $( this ).find(".r-admin-checkbox.columnheader"),
				perm = $checkbox.data("perm"),
				checked = $checkbox.attr("data-checked") == "1",
				newchecked = checked ? "0" : "1";
			
			$(".rightsTable").find("[id^=box" + perm + "]:visible").each( function() {
				var shortTable = pageObj.getTableNameFromBox( this.id ),
					table = pageObj.shortTables[ shortTable ];
				
				//	no checkbox allowed ?
				if ( pageObj.pageMasks[ table ].indexOf( perm ) == -1 ) {
					return;
				}

				$(this).attr("data-checked", newchecked);
				
				var modifiedMask = pageObj.updateModifiedState( pageObj.readTablePermissionsFromGrid( shortTable ), shortTable );
				
				pageObj.updateRowModifiedState( shortTable, modifiedMask );
				pageObj.updateRowHeaderState( table );
				pageObj.updatePagePermissions( table, perm, newchecked == "1", false );
				pageObj.updatePagesInGrid( shortTable );
			});
			
			pageObj.updateGroupsModifiedStates();
			$checkbox.attr("data-checked", newchecked);
		});
	},
	
	/**
	 *	Handle clicks on the checkboxes in row headers - near table names
	 */
	initRowHeaders: function() {
		var pageObj = this;
		
		//	click on a checkbox or its parent
		$(".rightsTable").on("click", ".rowheader", function(e) {
			var $checkbox = $(e.target),
				checked = $checkbox.attr("data-checked") == "1",
				newchecked = checked ? "0" : "1",
				shortTable = $checkbox.data("table"),
				table = pageObj.shortTables[ shortTable ];
				
			if ( pageObj.disabledMode ) {
				return;
			}
				
			jQuery.each( pageObj.pageMasks[ table ].split(""), function() {
				pageObj.getBoxObj(this, shortTable)
					.attr("data-checked", newchecked);
			});
			
			$checkbox.attr("data-checked", newchecked);

			modifiedMask = pageObj.updateModifiedState( pageObj.readTablePermissionsFromGrid( shortTable ), shortTable );
			pageObj.updateRowModifiedState( shortTable, modifiedMask );
			pageObj.updateRowHeaderState( table );
			pageObj.updateColHeaderStates();
			pageObj.updateGroupsModifiedStates();
			['A', 'E', 'S', 'P', 'I'].forEach( function( perm ) {
				pageObj.updatePagePermissions( table, perm, newchecked == "1", false );
			});
			pageObj.updatePagesInGrid( shortTable );
			
			return false;
		});
	},

	groupListChange: function() {
		var pageObj = this;
		
		this.currentGroup = this.$groupControl.find('li.active > a').attr('value');
		
		//	update all checkboxes
		jQuery.each( this.tables, function( table ) {
			var tableRights = pageObj.getTableActivePermissions( table, pageObj.currentGroup ),
				shortName = this[0];
				
			jQuery.each( pageObj.masks, function( right, permission ) {
				var checked = tableRights.indexOf( permission ) > -1 ? "1" : "0";
				
				pageObj.getBoxObj(permission, shortName)
					.attr("data-checked", checked);
			});
			
			pageObj.updateRowModifiedState( shortName, pageObj.getTableModifiedMask( table ) );

			pageObj.updatePagesInGrid( shortName );
		});

		
		// apply modified filters
		this.updateRowHeaderStates();
	},
	
	/**
	 * Handle group list selection changes 
	 */
	initGroupsList: function() {
		var pageObj = this;
		
		this.$groupControl.on( 'click', 'a[value]', function() {
			pageObj.$groupControl.children('.active').removeClass('active');
			
			$(this).parent().addClass('active');
			pageObj.groupListChange();
			pageObj.setButtonsActiveState();
			pageObj.showHideRows();
			return false;
		}); 

		pageObj.groupListChange();
		pageObj.setButtonsActiveState();
	},
	
	updateGroupHasVisibleItemsFlag: function( menuItem ) {
		var pageObj = this;
		
		menuItem.hasVisibleItems = false;
		if ( !menuItem.items ) {
			return;
		}
		
		jQuery.each( menuItem.items, function() {
			var subitem = pageObj.menuOrderedTables[ this ];
			
			pageObj.updateGroupHasVisibleItemsFlag( subitem );
			if ( subitem.table && pageObj.tables[ subitem.table ].visible || subitem.hasVisibleItems ) {
				menuItem.hasVisibleItems = true;
			}
		});
	},
	
	updateAllHasVisibleItemsFlags: function() {
		var pageObj = this;
		
		jQuery.each( this.menuOrderedTables, function() {
			if ( typeof this.parent === 'undefined' ) {
				pageObj.updateGroupHasVisibleItemsFlag( this );
			}
		});
	},
	
	/**
	 * Checks whether the item should be displayed according to groups expanded/collapsed state
	 */
	isItemInExpandedSet: function( menuItem ) {
		if ( typeof menuItem.parent === 'undefined' ) {
			return true;
		}
		
		if ( this.menuOrderedTables[ menuItem.parent ].collapsed ) {
			return false;
		}
		
		return this.isItemInExpandedSet( this.menuOrderedTables[ menuItem.parent ] );
	},

	/**
	 * Inits cache, that is used to fastify DOM elements search
	 */
	 initRowsCache: function() {
		var pageObj = this;
		// full tables names
		var tables = Object.values( pageObj.shortTables );
		var shortTables = pageObj.shortTables;

		tables.forEach( function( tableName ) {
			pageObj.tableRowsMap[ tableName ] = {
				rowDOMElement: null,
				pagesDOMElements: [],
				rowJQObj: null,
				pagesJQObj: null,
			};
		});

		// collect elements
		var $rows = $(".rightsTable > tbody").find("tr[id^='row_'], tr[data-table], tr[data-groupid]");
		$rows.each( function( idx ) {
			var el = $( this );
			// group
			if ( el.data('groupid') != null ) {
				var groupId = "grouprow_" + el.data('groupid');
				pageObj.tableRowsMap[ groupId ] = {
					rowDOMElement: this,
					rowJQObj: $(this)
				}
			// table row
			} else if ( el.attr('id') ) {
				// cut "row_" prefix
				var shortTName = el.attr('id').slice( 4 );
				pageObj.tableRowsMap[ shortTables[shortTName] ].rowDOMElement = this;
			}
			// pages rows
			if ( el.data('table') ) {
				var shortTName = el.data('table');
				pageObj.tableRowsMap[ shortTables[shortTName] ].pagesDOMElements.push( this );
			}
		});

		tables.forEach( function( tableName  ) {
			var entry = pageObj.tableRowsMap[ tableName ];
			entry.rowJQObj = $( entry.rowDOMElement );
			entry.pagesJQObj = $( entry.pagesDOMElements );
		});

	},

	/**
	 * Returns jquery object of main table row
	 */
	 getTableRowElement: function( shortTName ) {
		var pageObj = this;
		var shortTables = pageObj.shortTables;
		var entry = pageObj.tableRowsMap[ shortTables[shortTName] ];
		return entry.rowJQObj;
	},

	/**
	 * Returns jquery object for array of pages rows
	 */
	getTablePagesElements: function( shortTName ) {
		var pageObj = this;
		var shortTables = pageObj.shortTables;
		var entry = pageObj.tableRowsMap[ shortTables[shortTName] ];
		return entry.pagesJQObj;
	},
	
	/**
	 *	Refreshes the table list - displays and hides every single row there
	 */
	showHideRows: function() {
		var pageObj = this,
			displayGroupControls = false,
			indentTables = false;
		
		// apply search and modified filter, fill visible flag in tables
		jQuery.each( pageObj.tables, function( tableName, table ) {
			table.showOnModifiedMode = false;
			table.visible = pageObj.isTableDisplayed( tableName );
		});
		
		if ( this.orderMethod != 'menu' || this.tablesFilterState != 'all' ) {
			//	just show all tables with visible flag and hide those without it
			jQuery.each( pageObj.tables, function( tableName, table ) {
				var shortTable = table[ 0 ];
				pageObj.toggleJQElement( pageObj.getTableRowElement(shortTable), table.visible );
				table.showOnModifiedMode = table.visible;		
				
				var $pagesRows = pageObj.getTablePagesElements(shortTable);
				pageObj.toggleJQElement( $pagesRows, table.visible );					
			});
			
			//	hide non-table groups
			jQuery.each( pageObj.menuOrderedTables, function( idx ) {
				if ( this.table ) {
					return;
				}
				pageObj.toggleJQElement( pageObj.getGroupRow(idx), false );
			});	
		} else if ( !this.SEARCHLIST.length ) {
			//	show or hide according to group expanded state
			indentTables = true;
			jQuery.each( pageObj.menuOrderedTables, function( idx ) {
				if ( this["items"] ) {
					displayGroupControls = true;
				}
				
				var display = pageObj.isItemInExpandedSet( this );
				pageObj.toggleJQElement( pageObj.getGroupRow( idx ), display );
				
				if ( pageObj.menuOrderedTables[ idx ].table ) {		
					var tName = pageObj.tables[ pageObj.menuOrderedTables[ idx ].table ][ 0 ];	
					var $pagesRows = pageObj.getTablePagesElements(tName);
					pageObj.toggleJQElement( $pagesRows, display );
				}				
			});
		} else {
			//	Search + menu ordered. 
			//	Display found elements with their parents
			this.updateAllHasVisibleItemsFlags();
			indentTables = true;
			
			jQuery.each( pageObj.menuOrderedTables, function( idx ) {
				var display = this.hasVisibleItems || this.table && pageObj.tables[ this.table ].visible;
				
				pageObj.toggleJQElement( pageObj.getGroupRow( idx ), display );
				
				if ( pageObj.menuOrderedTables[ idx ].table ) {
					var tName = pageObj.tables[ pageObj.menuOrderedTables[ idx ].table ][ 0 ];		
					var $pagesRows = pageObj.getTablePagesElements(tName);
					pageObj.toggleJQElement( $pagesRows, display );
				}
			});
		}
		
		pageObj.updateColHeaderStates();
		
		$("#expandAll").toggle( displayGroupControls );		
		
		$(".rightsTable")
			.toggleClass( "groupcontrols", displayGroupControls )
			.toggleClass( "indenttables", indentTables );
	},
	
	/**
	 * Handle search box input
	 */
	initSearchBox: function() {
		var pageObj = this;
		
		$("#tableRightsSearch").on("input", function() {
			pageObj.SEARCHLIST = $(this).val().trim().toUpperCase().match(/[^\s]+/g) || [];
			pageObj.showHideRows();
		});
	},
	
	/**
	 * Returns grid checkbox jQuery object by permission and table name
	 * @param {string} perm
	 * @param {string} shortTable
	 * @retrun {jQuery object}
	 */
	getBoxObj: function( perm, shortTable ) {
		return $("#box" + perm + shortTable);
	},

	/**
	 * Returns grid checkbox jQuery object by permission and table name
	 * @param {string} perm
	 * @param {string} shortTable
	 * @param {string} page
	 * @retrun {jQuery object}
	 */
	getPageBoxObj: function( perm, shortTable, page ) {
		return $("#pagebox" + perm + shortTable + '_' + page);
	},

	getWholePageBoxObj: function( shortTable, page ) {
		return $("#wholepagebox_" + shortTable + '_' + page);
	},

	/**
	 * 
	 * @param {*} table 
	 * @param {*} page 
	 * @param {Boolean} restrict 
	 * @param {*} _group 
	 */
	updateRestrictedPage: function( table, page, restrict, _group )	{
		var group = _group || this.currentGroup;
		var groupObj = this.modifiedRestrictedPages[ group ];
		if ( !groupObj ) {
			groupObj = {};
			this.modifiedRestrictedPages[ group ] = groupObj;
		}
		groupObj[table] = groupObj[table] || JSON.parse( JSON.stringify( this.getSavedRestrictedPages( table, group ) ) );
		if ( restrict ) {
			groupObj[table][ page ] = true;
		} else {
			delete groupObj[table][ page ];
		}
		if ( this.pageListsEqual( groupObj[table], this.getSavedRestrictedPages( table, group ) ) ) {
			delete groupObj[table];
		}
	},
	

	/**
	 * Receives permission state for the table read from checkboxes, say "AESP"
	 * Updates the modifiedRights object
	 * Compares it to the saved table permissions, say "DES"
	 * Returns the string with the list of modified permissions - "AP"
	 * @param {string} perm
	 * @param {string} shortTable
	 * @param {string} _group (optional)
	 * @return {string}
	 */
	updateModifiedState: function( perm, shortTable, _group ) {
		var group = _group || this.currentGroup,
			table = this.shortTables[ shortTable ],
			savedPerm = this.getTableSavedPermissions( table, group ),
			modifiedMask = this.comparePermissions( perm, savedPerm );

		//	update modifiedRights
		this.modifiedRights[ group ] = this.modifiedRights[ group ] || {};
	
		if ( modifiedMask ) {
			this.modifiedRights[ group ][ table ] = perm;
		} else {
			delete this.modifiedRights[ group ][ table ];
		}
		
		return modifiedMask;
	},
	
	/**
	 * Reads table permissions from the checkboxes in the grid and returns the permission mask: "ADES"
	 * @param {string} shortTable
	 * @return {string}
	 */
	readTablePermissionsFromGrid: function( shortTable ) {
		var pageObj = this,
			mask = "";
			
		jQuery.each( this.masks, function( right, identifier ) {
			if ( pageObj.getBoxObj(identifier, shortTable).attr("data-checked") == 1 ) {
				mask = mask + identifier;
			}
		});
		
		return mask;
	},
	
	/**
	 * Returns the set of saved permissions for the given table
	 * @param {string} table
	 * @param {string} _group (optional)
	 * @return {string}
	 */
	getTableSavedPermissions: function( table, _group ) {
		var group = _group || this.currentGroup;
		
		return ( this.savedRights[ table ] ? this.savedRights[ table ][ group ] : "" ) || "";
	},

	/**
	 * Returns the set of saved restricted pages for the given table
	 * @param {string} table
	 * @param {string} _group (optional)
	 * @return {string}
	 */
	getSavedRestrictedPages: function( table, _group ) {
		var group = _group || this.currentGroup;
		
		return ( this.savedPageRestrictions[ table ] ? this.savedPageRestrictions[ table ][ group ] : {} ) || {};
	},
	

	/**
	 * Returns the current (unsaved) set of permissions for the given table
	 * @param {string} table
	 * @param {string} _group (optional)
	 * @return {string}	 
	 */
	getTableActivePermissions: function( table, _group ) {
		var group = _group || this.currentGroup,
			modRights = this.modifiedRights[group],
			modifiedMask;
	
		if ( typeof modRights != "undefined" ) {
			modifiedMask = modRights[table];
			
			//	attention!! modifiedMask == "" is a valid rights mask
			if ( typeof modifiedMask != "undefined") {
				return modifiedMask;
			}
		}
		
		return this.getTableSavedPermissions(table, group);
	},

	/**
	 * Returns the current (unsaved) set of restricted pages for the given table
	 * @param {string} table
	 * @param {string} _group (optional)
	 * @return {Object} { [page]: boolean }	 
	 */
	activeRestrictedPages: function( table, _group ) {
		var group = _group || this.currentGroup,
			modTables = this.modifiedRestrictedPages[group],
			modifiedPages;
	
		modifiedPages = modTables ? modTables[table] : undefined;
			
		if ( modifiedPages ) {
			return modifiedPages;
		}
		return this.getSavedRestrictedPages(table, group);
	},

	/**
	 * Colors the row with the given table.
	 * Cells appearing in modifiedMask become yellow.
	 * @param {string} shortTable
	 * @param {string} modifiedMask
	 */
	updateRowModifiedState: function( shortTable, modifiedMask ) {
		var pageObj = this;
		var $tableCell = $("#tblcell" + shortTable),
			tableModified = false;
		
		var cells = pageObj.getTablePagesElements( shortTable );
		jQuery.each( this.masks , function( right, identifier ) {
			var $cell = cells.find( "#cell" + identifier + shortTable );
			
			$cell.toggleClass( "r-rights-modified", modifiedMask.indexOf( identifier ) > -1 );
			tableModified = tableModified || modifiedMask.indexOf( identifier ) > -1;
		});

		$tableCell.toggleClass("r-rights-modified", tableModified);
	},
	
	/**
	 * Read short table name from grid checkbox ID
	 * @param {string} _boxId
	 * @return {string}
	 */
	getTableNameFromBox: function( _boxId ) {
		var boxId = _boxId || "";
		
		return boxId.substring( 4 );
	},

	/**
	 * Handle all/modified rows dropdown. Filter tables by their modified state.
	 */
	initModifiedFilter: function() {
		var pageObj = this,
			$control = $("#linkselectFilter");
		
		$control.on( "click", "a", function( e ) {
			pageObj.tablesFilterState = $(this).parent().data('value');
			pageObj.showHideRows();
			
			$control.find( '.btn-link' ).text( $(this).text() );
			$control.find('.dropdown-toggle').trigger("click");
			return false;
		});
	},


	/**
	 * Find which table permissions are modified (unsaved). Returns mask with modified permissions
	 * @param {string} table
	 * @param {string} _group (optional)
	 * @return {string}		 
	 */
	getTableModifiedMask: function( table, _group ) {
		return this.comparePermissions( 
			this.getTableSavedPermissions( table, _group ), 
			this.getTableActivePermissions( table, _group ) 
		);
	},
	
	/**
	 * Compares two permission masks. Returns the difference.
	 */
	comparePermissions: function( perm, savedPerm ) {
		var modifiedMask = "";
		
		jQuery.each(this.masks, function( right, identifier ) {
			if ( ( savedPerm.indexOf( identifier ) > -1 ) != ( perm.indexOf( identifier ) > -1 ) ) {
				modifiedMask = modifiedMask + identifier;
			}
		});
		return modifiedMask;
	},
	
	/**
	 * Checks whether there are any unsaved permissions for the given group
	 * @param {string} _group (optional)
	 * @return {boolean}	 
	 */
	areGroupPermissionsModified: function( _group ) {
		var group = _group || this.currentGroup;
		
		return Object.keys( this.modifiedRights[ group ] || {} ).length 
			+ Object.keys( this.modifiedRestrictedPages[ group ] || {} ).length > 0;
	},
	
	/**
	 * Update page's state
	 */
	updateGroupsModifiedStates: function() {		
		var isAnyModified = !!Object.keys( this.groups ).filter( function( groupIdx ) {
				return this.areGroupPermissionsModified( groupIdx );
			}, this ).length;
		
		this.setPageModified( isAnyModified );
		Runner.toggleDisabledClass( $('#resetBtn'), !isAnyModified );
		Runner.toggleDisabledClass( $('#saveBtn'), !isAnyModified );
	},

	/**
	 * Sets proper state to the tri-state checkbox in the row header. 
	 * checked when all boxes in a row a checked, unchecked when unchecked, mixed when mixed
	 * @param {string} table
	 */	
	updateRowHeaderState: function( table ) {
		var $checkbox = $('#rowbox' + this.tables[ table ][0] ),
			tableRights = this.getTableActivePermissions( table ),
			pageMask = this.pageMasks[ table ], 
			diff = this.comparePermissions(pageMask, tableRights);
		
		if ( tableRights && !diff ) { // all checked
			$checkbox.attr("data-checked", "1");
		} else if ( !tableRights && pageMask ) { // none checked
			$checkbox.attr("data-checked", "0");
		} else if ( tableRights && diff ) { // partially checked
			$checkbox.attr("data-checked", "2");
		} else { // no permission possible
			$checkbox.attr("data-checked", "-1");
		}
	},
	
	/**
	 * Runs updateRowHeaderState for all rows
	 */
	updateRowHeaderStates: function() {
		var pageObj = this;
		
		jQuery.each( this.tables, function( table ) {
			pageObj.updateRowHeaderState( table );
		});
	},
	
	/**
	 * Checks wheter the table shoud be displayed according to the current filtering criteria.
	 * Modified/unmodified, searching by names etc
	 * @param {string} table
	 * @return {boolean}
	 */
	isTableDisplayed: function( table ) {
		var CAPTION, TABLE, i,
			show = true;
		
		if ( this.SEARCHLIST.length != 0 ) {
			show = false;
			CAPTION = this.tables[ table ][1].toUpperCase();
			TABLE = table.toUpperCase();
			for (i = this.SEARCHLIST.length - 1; !show && i >= 0 ; --i) {
				show = this.SEARCHLIST[i] && ( TABLE.indexOf( this.SEARCHLIST[i] ) >= 0 || CAPTION.indexOf( this.SEARCHLIST[i] ) >= 0 );
			}
		}
		
		//	check modified filter state
		return show && ( this.tablesFilterState == 'all' || this.getTableModifiedMask( table ) != "" || ( this.tablesFilterState == 'modified' && this.tables[ table ].showOnModifiedMode ) );
	},

	/**
	 * Sets proper state to the tri-state checkbox in the column header. 
	 * Counts displayed rows only.
	 * Sets it checked when all boxes in a column a checked, unchecked when unchecked, mixed when mixed
	 */	
	updateColHeaderStates: function() {
		var pageObj = this,
			colBoxStates = {A:-1, D:-1, E:-1, S:-1, P:-1, I:-1, M:-1};
					
		jQuery.each( this.tables, function( table ) {
			var tableRights, pageMask, finish = true;
			if ( !pageObj.isTableDisplayed( table ) ) {
				return;
			}
			
			tableRights = pageObj.getTableActivePermissions( table ),
			pageMask = pageObj.pageMasks[ table ];
			jQuery.each( colBoxStates, function( perm, state ) {
				/*
				 *	Some magic follows. 
				 *	2 - mixed state of a checkbox - when both checked and unchecked values present
				 *	0 - unchecked
				 *	1 - checked
				 * 	-1 - initial and empty state - when there are no checkboxes to represent. 
				 */
				if ( state == 2 ) {
					return;
				}
				
				finish = false; //	see the comment below
				if ( pageMask.indexOf(perm) == -1 ) {
					return;
				}
				
				if ( tableRights.indexOf(perm) == -1 ) {
					if ( state == -1 ) {
						colBoxStates[ perm ] = 0;
					} else if ( state == 1 ) {
						colBoxStates[ perm ] = 2;
					}
				} else {
					if ( state == -1 ) {
						colBoxStates[perm] = 1;
					} else if ( state == 0 ) {
						colBoxStates[perm] = 2;
					}
				}
			});
			
			if ( finish ) { // all permissions are in mixed state (2), exit
				return false;
			}
		});
		
		jQuery.each( colBoxStates, function( perm, state ) {
			$("#colbox" + perm).attr("data-checked", state);
		});
	},

	orderUpdateHandler: function( orderAlphabetically ) {
		var $table = $(".rightsTable");
		var $expandAll = $("#expandAll"); 


		// expand groups & disable collapse button
		if ( orderAlphabetically ) {
			this.expandAllHandler( true );

			$expandAll.off( 'click' );
			$expandAll.on( "click", function( e ) {
				return false;
			});

			$table.addClass( 'orderAlphabetically' );
			$table.removeClass( 'menuordered' );
		} else {
			$table.removeClass( 'orderAlphabetically' );
			$table.addClass( 'menuordered' );
			this.initExpandAll();
		}

	},
	
	orderTablesAlphabetically: function() {
		var pageObj = this;
		var $table = $(".rightsTable");
		var $tbody = $table.find("tbody");

		this.orderUpdateHandler( true );

		this.alphaOrderedTables.forEach( function( tableName ) {
			var tableRow = pageObj.tableRowsMap[ tableName ].rowDOMElement;
			var pagesRowsList = pageObj.tableRowsMap[ tableName ].pagesDOMElements;
			var $rows = $( [ tableRow ].concat( pagesRowsList ) );
			$tbody.append( $rows );
		});
			
		
		this.showHideRows();
	},
	
	orderTablesAsMenu: function() {
		var pageObj = this;
		var $table = $(".rightsTable");
		var $tbody = $table.find("tbody");

		this.orderUpdateHandler();
		
		var rows = [];
		this.menuOrderedTables.forEach( function( entry ) {
			if ( entry.groupId != null && entry.items.length != 0) {
				var groupId = "grouprow_" + entry.groupId;
				var $row = pageObj.tableRowsMap[ groupId ].rowDOMElement;
				rows.push( $row );
			} 
			if ( entry.table ) {
				var tableRow = pageObj.tableRowsMap[ entry.table ].rowDOMElement;
				var pagesRowsList = pageObj.tableRowsMap[ entry.table ].pagesDOMElements;
				rows.push( tableRow );
				pagesRowsList.forEach( function( pageDOMElement ) {
					rows.push( pageDOMElement );
				});
			}
		});
		$tbody.prepend( $(rows) );
		this.showHideRows();
	},
	
	initOrderControl: function() {
		var pageObj = this,
			$control = $("#linkselectOrder");
		
		$control.on( "click", "a", function( e ) {
			if ( $(this).parent().data("value") == 'menu' ) {
				pageObj.orderTablesAsMenu();
			} else {
				pageObj.orderTablesAlphabetically();
			}
			
			$control.find( '.inline-button' ).text( $(this).text() );
			$control.find('.dropdown-toggle').trigger("click");
			return false;
		});
	},

	expandAllHandler: function( expand ) {
		var pageObj = this;
		jQuery.each( pageObj.menuOrderedTables, function( groupId ) {
			if ( !this.items ) {
				return;
			}
			pageObj.expandGroup( groupId, expand);
		});
		pageObj.showHideRows();
		
	},
	
	initExpandAll: function() {
		var pageObj = this;
		var $expandAll = $("#expandAll"); 
		$expandAll.on('click', function(e) {
			pageObj.allExpanded = !pageObj.allExpanded;
			pageObj.expandAllHandler( pageObj.allExpanded );
			$("#expandAll").text( pageObj.allExpanded ? Runner.lang.constants.COLLAPSE_ALL : Runner.lang.constants.EXPAND_ALL );
			return false;
		});
	},

	getGroupRow: function( groupId ) {
		var table = this.menuOrderedTables[ groupId ].table;
		
		return table ? $("#row_" + this.tables[ table ][ 0 ]) : $("#grouprow_" + groupId);
	},
	
	expandGroup: function( groupId, toExpand ) {
		var $tr = this.getGroupRow( groupId ),
			menuItem = this.menuOrderedTables[ groupId ];
			
		menuItem.collapsed = !toExpand;
		
		$tr.find("[data-group-expander]")
			.toggleClass( "glyphicon-triangle-right", !toExpand)
			.toggleClass( "glyphicon-triangle-bottom", toExpand);
		
	},

	initExpandCollapseControls: function() {
		var pageObj = this;
			
		$(".rightsTable").on("click", "tr[data-groupid]:has([data-group-expander])" , function(e) {
			var groupId = $(this).data("groupid") || 0;		
			pageObj.expandGroup( groupId, pageObj.menuOrderedTables[ groupId ].collapsed/* == true*/ );
			pageObj.showHideRows();
		});
	},
	
	copyGroupPermissions: function( from ) {
		var pageObj = this;
		
		jQuery.each( this.tables, function( table ) {
			var shortTable = this[0],
				rights = pageObj.getTableActivePermissions( table, from ),
				modifiedMask = pageObj.updateModifiedState( rights, shortTable );
		});
		
		this.updateGroupsModifiedStates();
		$('.group').trigger("change");
	},
	
	initCopyPermissions: function() {
		var pageObj = this,
			args = {
				modal: true,
				centered: true,
				width: 400,
				headerContent: Runner.lang.constants.AA_COPY_PERMISS_FROM,
				bodyContent: '<div class="groupCopySelector" id=groupCopySelector></div>'
			},
			afterCreateDialog = function( win ) {
				var $list = $("#groupCopySelector", win.getContextNode()),
					activeGroupId = $("ul.nav-tabs").find("li.active").find("a").attr("value");

				//	fill in groups list
				$.each( pageObj.groups, function( groupId, gr ) {
					groupName = gr.label;
					if ( activeGroupId == groupId ) {
						return;
					} 

					var groupLink = $('<a class="groupCopyLink" href=#></a>')
						.text( groupName )
						.data('groupid', groupId);
						
					if ( groupId < 0) {
						groupLink.prependTo($list);
					} else {
						groupLink.appendTo($list);
					}
				});

				$list.on('click', 'a', function( e ) {
					pageObj.copyGroupPermissions( $(e.target).data('groupid') );
					
					win.destroy();
					
					$("ul.nav-tabs").find("li.active").find("a").trigger("click");
					return false;
				});
			};
			
		$('#copyPermissions').on('click', function(e) {
			if ( !Runner.isDisabledButton( this ) ) {	
				Runner.pages.PageManager.createBSWin( args, false, afterCreateDialog );
			}
			return false;
		});
	},
	
	/** 
	 * Hide element with inline display style 
	 * Delete inline display style to show
	 * @param {jQuery elem} $elem
	 * @param {boolean} show
	 */
	toggleJQElement: function( $elem, show ) {
		if ( !show ) {
			$elem.hide();
		} else {
			$elem.css( 'display', '' );
		}
	},

	pageListsEqual: function (pages1, pages2) {
		var kPages1 = Object.keys( pages1 );
		var kPages2 = Object.keys( pages2 );
		if ( kPages2.length !== kPages1.length ) {
			return false;
		}
		/* //Array.proptotype.find isn't supported in IE
		return kPages1.find( function( p ) {
			return !pages2[p];
		}) === undefined;*/
		
		//	find page in pages1 that is not present in pages2
		return kPages1.filter( function( p ) {
				return !pages2[p];
			}).length === 0;
		// the reverse check is not needed since kPages2.length === kPages1.length
	},
	
	/**
	 * @param {boolean} expired
	 * @return {string}
	 */ 
	getLogoutUrl: function( expired ) {
		return Runner.getPageUrl( this.pageType ) 
			+ "?a=logout" + ( expired ? "&reason=expired" : "" );
	},		
	
	addGroupToTable: function( groupName, id ) {		
		this.groups[ id ] = { label: groupName };
			
		$("li.active", '[data-groups]').removeClass();
		
		$('<li class="active" role="presentation"><a href="#" value="' + id + '">' + groupName + '</a></li>')
			.insertAfter( $('.nav-tabs[data-groups]').find('li:last') );
		
		$("li.active", '[data-groups]').find('a').trigger("click");

		$('.group')
			.attr('size', Object.keys( this.groups ).length )
			.trigger('change');
		
		$('.groupname').val('');
	}
});
Runner.pages.MembersPageAD = Runner.extend( Runner.pages.ListPageCommon, {
		
	pageType: Runner.pages.constants.PAGE_ADMIN_ADMEMBERS,
	
	rows: null,

	provider: '',
	
	constructor: function(cfg) {
		Runner.pages.MembersPageAD.superclass.constructor.call(this, cfg);
		
		this.baseParams.id = this.pageId;
		this.rows = this.controlsMap.gridRows;
		this.provider = this.pageData.provider;
		this.baseParams.provider = this.pageData.provider;
	},

	destructor: function() {
		Runner.pages.MembersPageAD.superclass.destructor.call(this);
		
		this.pageCont = null;
		$('.group').prop('disabled', false);
		this.parObj.disableGroupButtons( false );
	},	
	
	init: function() {
		this.initGridElem();
		Runner.pages.ListPageCommon.superclass.init.call(this);		
	
		this.initButtons();
		this.initRows();
	},
	
	initButtons: function() {
		var pageObj = this,
			$addSelBtn = $('#addSelBtn');
			
		$addSelBtn.off('click').on("click", function(e) {
			if ( !Runner.isDisabledButton(this) ) {	
				pageObj.addSelectedToGroup();	
			}
			return false;
		});
		
		Runner.addDisabledClass( $addSelBtn );
	},
	
	initRows: function() {
		this.rows.forEach( function( row ) {
			this.initAddGrLink( row );
			this.initCheckBoxGr( row );			
		}, this );
	},	
	
	/**
	 * @param {object} row
	 */
	initAddGrLink: function( row ) {
		var pageObj = this;
		
		row.addGrLink = $('#iAddLink' + row.id).on("click", function(e) {
			pageObj.addToGroup( row );
			return false;
		});
	},
	
	/**
	 * @param {object} row
	 */
	initCheckBoxGr: function( row ) {
		var pageObj = this;
		
		row.checkBoxGr = $('#check' + this.id + '_' + row.id).on("click", function(e) {
			var checked = $(this).attr("data-checked") || "0";
			var newcheked = checked === "0" ? "1" : "0";
			
			$(this).attr( "data-checked", newcheked );
			
			var selBoxesArr = pageObj.getSelections(),
				$addSelBtn = $('#addSelBtn');
			
			if ( selBoxesArr.length && Runner.isDisabledButton( $addSelBtn ) ) {
				Runner.delDisabledClass( $addSelBtn );
			} else if ( !selBoxesArr.length && !Runner.isDisabledButton( $addSelBtn ) ) {
				Runner.addDisabledClass( $addSelBtn );
			}
				
			$('.r-admin-checkbox[id^=chooseAll_' + pageObj.id + ']').attr('data-checked', 
				selBoxesArr.length === $('.r-admin-checkbox[id^=check' + pageObj.id + '_]').length ? "1" : "0" );		
		});
	},
	
	/**
	 * @return {array}
	 */
	getSelections: function() {
		var selBoxesArr = [];
		$('.r-admin-checkbox[id^=check][name^=selection][data-checked="1"]').each( function() {
			selBoxesArr.push( $(this) );
		});
		return selBoxesArr;
	},	
	
	initHeaderCheckBox: function() {
		var pageObj = this;
		
		$('#chooseAll_' + this.id).off("click").on("click", function(e) {
			var checked = $(this).attr("data-checked") || "0";
			var newchecked = checked === "0" ? 1 : 0;
			
			//set checked/unchecked for all checkbox in grid rows
			$('.r-admin-checkbox[id^=check' + pageObj.id + '_]').attr('data-checked', newchecked);

			//set checked/unchecked for all chooseAll checkboxes
			$('.r-admin-checkbox[id^=chooseAll_' + pageObj.id + ']').attr('data-checked', newchecked);

			var selBoxesArr = pageObj.getSelections(),
				$addSelBtn = $('#addSelBtn');
			
			if ( selBoxesArr.length && Runner.isDisabledButton( $addSelBtn ) ) {
				Runner.delDisabledClass( $addSelBtn );
			} else if ( !selBoxesArr.length && !Runner.isDisabledButton( $addSelBtn ) ) {
				Runner.addDisabledClass( $addSelBtn );
			}		
		});
	},

	initSearch: function() {
		this.searchController = new Runner.search.SearchController({
			id: this.pageId,
			tName: this.tName,
			fNamesArr: this.controlsMap.search.allSearchFields,
			shortTName: this.shortTName,
			usedSrch: this.controlsMap.search.usedSrch,
			panelSearchFields: this.controlsMap.search.panelSearchFields,
			ajaxSubmit: true,
			useSuggest: false,
			pageType: this.pageType
		});
		
		this.searchController.init( this.controlsMap.search.searchBlocks );
		this.searchController.srchForm.baseParams = this.baseParams;
		
		this.searchController.srchForm.on("beforeSubmit", function(form) {
			this.preparePageForReloading();
		}, this);
		
		this.searchController.on('afterSearch', function(respObj, srchController, srchForm) {
			Runner.stopLoading();
			this.pageReloadHn( respObj );
			this.searchController.toggleShowAll( this.searchController.usedSrch )
		}, this);
		
		this.searchController.srchForm.on('submitFailed', function() {
			Runner.stopLoading();
		}, this);
		
	},
	
	initInline:  Runner.emptyFn,

	
	/**
	 * The page's after ajax reloading handler
	 * @param {Object} respObj
	 */	
	pageReloadHn: function( respObj ) {
		Runner.stopLoading();

		var $content = this.winObject.getContentNode(),
			$header = this.winObject.header(),
			$footer = this.winObject.footer(),
			$body = this.winObject.body();
		
		if ( !respObj.success ) {
			$body.html("REQUEST FAILED");
			return;
		}
		
		Runner.setIdCounter( respObj.idStartFrom );
		
		// fix body height before
		$body.height( $body.height() );		
		$body.html( respObj['html'] );
		$footer.html( respObj['footerCont'] );
	
		$body.height( $content.innerHeight() - $header.outerHeight() - $footer.outerHeight() - $body.outerHeight() + $body.height());				
		// set controlsMap
		this.controlsMap = respObj.controlsMap[ this.tName ][ this.pageType ][ this.pageId ];
		
		Runner.applyPagesData( respObj.pagesData );
		this.pageData = Runner.getPageData( this.id );
		this.proxy = this.pageData.proxy;
		
		this.rows = this.controlsMap.gridRows;
		
		this.initButtons();
		this.initRows();
		this.initHeaderCheckBox();
		
		this.searchController.usedSrch = this.controlsMap.search.usedSrch;			
		this.searchController.toggleShowAll( this.searchController.usedSrch );
		
		this.initSearch();
		this.initPagination();
	},
	
	/**
	 *
	 */
	addSelectedToGroup: function() {		
		this.getSelections().forEach( function( $box ) {
			var row = this.getRowById( parseInt( $box.attr("value"), 10 ) );
			this.addToGroup( row );			
		}, this );
		
		this.destructor();
	},
	
	getRowById: function( rowId ) {
		for ( var i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[i].id === rowId ) {
				return this.rows[i]; 
			}
		}
		return false;
	},	
	
	/**
	 *
	 */
	addToGroup: function( row ) {
		if ( !row || row.keys.name == '' ) {
			return;
		}
		
		var groupName = row.keys.name,
			pageObj = this;
		
		for ( var groupId in this.parObj.groups ) {
			if ( this.parObj.groups[ groupId ] == groupName ) {
				this.displayMessage('Group with such name is allready exist!');
				return;
			}
		}
		
		Runner.runnerAJAX( this.requestUrl, {
				name: groupName,
				a: 'addgr',
				provider: pageObj.provider
			},
			function( ret ) {				
				if ( !ret.success ) {
					pageObj.displayMessage( 'Error adding group!', true, true );
					return;
				}
	
				pageObj.hideElement('message');
				pageObj.parObj.addGroupToTable( ret.label, ret.id );
								
				$('.r-admin-checkbox[id^=check][name^=selection][value=' + row.id + ']').hide();
				row.addGrLink.hide();	
			}
		);
	}
});Runner.pages.RightsPageAD = Runner.extend( Runner.pages.RightsPage, {
	/**
	 * Init buttons working with groups
	 */
	initGroupsButtons: function() {
		var pageObj = this;
		
		$('#addGroupBtn').on("click", function(e) {
			if ( Runner.isDisabledButton( this ) ) {	
				return false;
			}
			
			var pageParams = {
				tName: "<global>",
				pageType: Runner.pages.constants.PAGE_ADMIN_ADMEMBERS, 
				pageMode: Runner.pages.constants.MEMBERS_PAGE,
				parObj: pageObj,
				modal: true,
				baseParams: {
					table: "<global>"
				},
				requestUrl: Runner.getPageUrl( "admin_admembers_list" )
			};
			
			Runner.pages.PageManager.openPage( pageParams );
			pageObj.disableGroupButtons( true );
			return false;
		});
		
		$('#delGroupBtn').on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {	
				pageObj.deletegroup(); 
			}
			return false; 
		});
	},
	
	addGroupToTable: function( groupName, id ) {		
		this.groups[ id ] = groupName;
			
		$("li.active", '[data-groups]').removeClass();
		
		$('<li class="active" role="presentation"><a href="#" value="' + id + '">' + groupName + '</a></li>')
			.insertAfter( $('.nav-tabs[data-groups]').find('li:last') );
		
		$("li.active", '[data-groups]').find('a').trigger("click");

		$('.group')
			.attr('size', Object.keys( this.groups ).length )
			.trigger('change');
		
		$('.groupname').val('');
	},
});Runner.pages.ExportPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * @type {array}
	 */
	selection: null,
	
	/**
	 * @constructor 
	 */
	constructor: function( cfg ) {
		Runner.pages.ExportPage.superclass.constructor.call( this, cfg );
		
		this.submitUrl = Runner.pages.getUrl( this.tName, this.pageType );
		this.selection = this.baseParams ? this.baseParams[ "selection[]" ] : Runner.pages.PageSettings.getTableData( this.tName, "selection" );
	},
	
	/**
	 *
	 */
	init: function() {
		Runner.pages.ExportPage.superclass.init.call( this );	
		
		this.initButtons();
		this.initExportControls();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	/**
	 * TODO Uselocal storage
	 */
	initExportControls: function() {
		var	settings = JSON.parse( localStorage.getItem( this.getSettingsKey() ) ),
			$csvDelimiter = $("#csv_delimiter" + this.id),
			$exportFieldsCtrl = $("#exportFields" + this.id),
			$buttonSave = $("[id=saveButton" + this.id + "]"),
			pageObj = this,
			chosenCtrl, saveExportFieldsStateOnCtrlChange, delimiter;
		
		$('input[type=radio][name=type]', this.pageCont).on("change", function( e ) {
			var disabled = $(this).val() !== "csv";
			
			$csvDelimiter
				.prop( "disabled", disabled )
				.toggleClass( "greyed-input", disabled );
		});
		
		if ( settings ) {
			delimiter = settings.delimiter;
			
			$('input[type=radio][name=type][value="' + settings.exportType + '"]', this.pageCont)
				.prop( "checked", true )
				.trigger("change");
			
			$('input[type=radio][name=records][value="' + settings.records + '"]', this.pageCont).prop( "checked", true );
			$('input[type=radio][name="txtformatting"][value="' + settings.txtFormatting + '"]', this.pageCont).prop( "checked", true );
		} else {
			delimiter = Runner.pages.PageSettings.getTableData( this.tName, "csvDelimiter" );
		}
		
		$csvDelimiter.val( delimiter );
		
		if ( $exportFieldsCtrl.length ) {			
			$exportFieldsCtrl
				.chosen({rtl: Runner.isDirRTL()});
			
			chosenCtrl = $exportFieldsCtrl.data("chosen");
			
			saveExportFieldsStateOnCtrlChange = function( settings ) {
				settings = settings || {};
				settings.exportFields = chosenCtrl ? chosenCtrl.getOrderedMultipleValue() : $exportFieldsCtrl.val();
				localStorage.setItem( pageObj.getSettingsKey(), JSON.stringify( settings ) );			
			};	
				
			$exportFieldsCtrl.on("change", function( e ) {
					var fields = $(this).val() || [];
					Runner.toggleDisabledClass( $buttonSave, !fields.length );
					
					if ( fields.length ) {
						saveExportFieldsStateOnCtrlChange( settings );
					}
				});

			
			chosenCtrl && $( chosenCtrl.search_choices ).sortable({
				item: ".search-choice", 
				stop: function( event, ui ) {
					$(".search-field", this )
						.appendTo( this );
						
					saveExportFieldsStateOnCtrlChange( settings );						
				}
			});
			
			if ( chosenCtrl && settings && settings.exportFields ) {					
				chosenCtrl.setOrderedMultipleValue( settings.exportFields );
			}
			
			Runner.toggleDisabledClass( $buttonSave, !( $exportFieldsCtrl.val() || [] ).length );
		}
	},
	
	/**
	 *
	 */
	initButtons: function() {
		var pageObj = this,
			$exportFieldsCtrl = $("#exportFields" + this.id);
		
		$("[id=saveButton" + this.id + "]").on("click", function(e) {			
			var settings = {
				exportType: $('input[type=radio][name=type]:checked', pageObj.pageCont).val(),
				records: $('input[type=radio][name=records]:checked', pageObj.pageCont).val(), 
				txtFormatting: $("[name='txtformatting']:checked",  pageObj.pageCont ).val(),
				delimiter: $("#csv_delimiter" + pageObj.id).val(),
				exportFields: $exportFieldsCtrl.data("chosen") ? 
					$exportFieldsCtrl.data("chosen").getOrderedMultipleValue() : 
					$exportFieldsCtrl.val(),
			}

			pageObj.form = new Runner.form.BasicForm({
				submitUrl: pageObj.submitUrl,
				standardSubmit: true,
				method: 'POST',
				id: pageObj.pageId,
				baseParams: { 
					type: settings.exportType,
					records: settings.records, 
					txtformatting: settings.txtFormatting,
					delimiter: settings.delimiter,
					"exportFields[]": settings.exportFields,
					"selection[]": pageObj.selection,
					page: pageObj.pageName
				}
			});
			
			pageObj.form.submit();
			pageObj.form.destructor();
			pageObj.form = null;
			
			localStorage.setItem( pageObj.getSettingsKey(), JSON.stringify( settings ) );
			pageObj.close();			
			return false;
		});

		$("[id=cancelButton" + this.id + "]").on("click", function( e ) {
			pageObj.close();
			return false;
		});		
	},
	
	/**
	 * @param {array} selection
	 * @return {object}
	 */
	getOpenPageParams: function( selection ) {
		var params = {
				tName: this.tName, 
				pageType: Runner.pages.constants.PAGE_EXPORT, 
				modal: true,
				requestUrl: Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EXPORT ),	
				baseParams: {
					a: "export",
					"selection[]": selection
				}
			};
		
		params.baseParams = Runner.apply( params.baseParams, this.getStateParams() );
		return params;
	},
	
	getSettingsKey: function() {
		return "export" + this.tName + "-" + this.pageName;
	}
});/**
 * The import page class
 */
Runner.pages.ImportPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * The current page type
	 * @type {string}
	 */
	pageType: Runner.pages.constants.PAGE_IMPORT,

	/**
	 * URL where to submit import forms
	 * @type {string}
	 */
	submitUrl: "",
		
	/**
	 * A flag indicating if a file or raw text is being imported
	 * @type {boolean}
	 */
	fileImport: true,

	/**
	 * A flag indicating if CSV type preview is applied 
	 * for importing data
	 * @type {boolean}
	 */
	CSVPreview: false,
	
	/**
	 * An object containing the import fields labels data
	 * @type {object}
	 */
	importFieldsLabels: {},
	
	/**
	 * An object containing data on correspondence
	 * between the import fields and table preview columns
	 * @type {object}
	 */
	columnsListData: null,	
	
	/**
	 * An object containing the jQuery objects 
	 * representing preview controls
	 * @type {object}
	 */	
	previewControls: null,
	
	/**
	 * An object representing the 'Upload file' button	
	 * @type {jQuery object} 
	 */
	$fileUploadButton: null,
	
	/**
	 * An object representing the 'Copy and paste Text' button	
	 * @type {jQuery object} 
	 */
	$textInsertButton: null,
	
	/**
	 * An object representing the 'Back' button
	 * @type {jQuery object} 
	 */	
	$backButton: null,
	
	/**	
	 * An object representing the 'Import data' button
	 * @type {jQuery object} 
	 */	
	$importButton: null,
	
	/**
	 * An object representing the 'Continue' button
	 * @type {jQuery object} 
	 */
	$continueTextImport: null,
	
	/**
	 * The flag indicating if the html5 File API is using 
	 * to upload a user's import file
	 * @type {boolean}
	 */
	fileUploaderSupported: false,
	
	/**
	 * The hidden file input element that is used
	 * to upload import files via File API
	 * @type {jQuery object}
	 */
	$hiddenFileInput: null,
	
	/**
	 * An object representing a drag-n-drop area 
	 * using to upload import files via File API
	 * @type {jQuery object}
	 */
	$dropZone: null,
	
	/**
	 * The import page's modes
	 * @type {object}
	 */
	modeConstants: {
		DATA_CHOOSING: 0,
		PREVIEW: 1,
		RESULTS: 2
	},
	
	/**
	 * The current import page's mode
	 * @type {number}
	 */
	importPageMode: 0,
	
	/**
	 * The hash to store import settings 
	 * in the localStorage
	 * @type {string}
	 */
	settingsHash: "",

	/**
	 * The list of fields identified as Dates in source excel file
	 * in a form of an array: [col number]
	 * @type Array
	 */
	excelDateFields: [],
	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		this.previewControls = {};
		
		Runner.pages.ImportPage.superclass.constructor.call( this, cfg );
		
		this.importPageMode = this.modeConstants.DATA_CHOOSING;
		
		this.importFieldsLabels = Runner.pages.PageSettings.getTableData(this.tName, "importFieldsLabels");
		this.submitUrl = Runner.pages.getUrl( this.tName, this.pageType );
		this.fileUploaderSupported = this.isAutoFileUploadSupported();
	},
	
	/**
	 * Initialize the page's functionality
	 */
	init: function() {
		Runner.pages.ImportPage.superclass.init.call( this ) ;	
		
		this.initFileUploader();
		this.initButtons();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},

	/**
	 * Check if the html5 File API and drag-n-drop functionality are supported by the browser
	 * return {boolean}
	 */
	isAutoFileUploadSupported: function() {
		var div = document.createElement('div'),
			dndSupport = 'draggable' in div || ('ondragstart' in div && 'ondrop' in div),
			xhr2Support = 'XMLHttpRequest' in window && 'withCredentials' in new XMLHttpRequest();
			
		return !!window.File && !!window.FileList && !!window.FormData && xhr2Support && dndSupport;
	},
	
	/**
	 * Initialize Import page buttons
	 */
	initButtons: function() {
		var pageObj = this;

		this.$fileUploadButton = $("[id='uploadImportFile" + this.id + "']").on("click", function(e) {
			if ( pageObj.fileUploaderSupported ) {
				pageObj.$hiddenFileInput.trigger('click');
			} else {
				pageObj.showImportPreview();
			}
			
			return false;
		});	
			
		this.$textInsertButton = $("[id='switchToTextInsertion" + this.id + "']").on("click", function(e) {
			pageObj.switchToTextInput();
			return false;
		});
		
		this.$backButton = $( "[id='backTo" + this.id + "']").on("click", function(e) {
			pageObj.backToDataChoosing();
			return false;
		});
		
		this.$continueTextImport = $( "[id='continueTextImport" + this.id + "']").on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				$(this).hide();
				pageObj.showImportPreview();
			}
			return false;
		});	
			
		this.$importButton = $("[id='importButton" + this.id + "']").on("click", function(e) {
			if ( !Runner.isDisabledButton( this ) ) {
				$(this).hide();
				pageObj.$backButton.hide();
			
				pageObj.importData();
			}
			return false;
		});		
		
		$("[id='backToList" + this.id + "']").on("click", function(e) {
			window.location.href = pageObj.backToListUrl( $(this).data("page") );
			return false;
		});
	},
	
	bricksToBlocks: {
		import_preview: "importPreview",
		import_rawtext_control: "copyAndPasteText",
		importfields: "importFields",
		importheader_text: "importheaderText",
		import_process: "importProgress",
		import_results: "reportText"
	},
	
	toggleBlock: function( name, toggler ) {
		$( "#" + this.bricksToBlocks[ name ] + this.id ).toggle( toggler );
	},
	
	showBlock: function( name ) {
		this.toggleBlock( name, true );
	},
	
	hideBlock: function( name ) {
		this.toggleBlock( name, false );
	},
	
	/**
	 * Switch the page's appearance 
	 * to allow user to choose an importing data
	 */
	backToDataChoosing: function() {
		if ( this.importPageMode === this.modeConstants.DATA_CHOOSING && !this.fileImport ) {
			this.switchToFileChoosing();
		} else if ( this.importPageMode === this.modeConstants.PREVIEW ) {
			this.hideBlock("import_preview");
			
			this.$importButton.hide();
			
			if ( !this.fileImport ) {
				this.switchToTextInput();
			} else {
				this.switchToFileChoosing();
			}
		}
		
		this.importPageMode = this.modeConstants.DATA_CHOOSING;
	},

	/**
	 * Change the page's appearance 
	 * to allow user to choose a file to import
	 */
	switchToFileChoosing: function() {
		this.fileImport = true;
		
		this.showBlock("importheader_text");
		this.showBlock("importfields");
		this.hideBlock("import_rawtext_control");
		
		this.toggleDropZone( false );
		
		this.$continueTextImport.hide();
		this.$backButton.hide();
	},
	
	/**
	 * Change the page's appearance 
	 * to allow user to insert a csv content
	 */
	switchToTextInput: function() {
		this.fileImport = false;
		
		this.hideBlock("importheader_text");
		this.hideBlock("importfields");	
		this.showBlock("import_rawtext_control");

		this.$continueTextImport.show();
		this.$backButton.show();

		this.initTextArea();
	},
	
	/**
	 * Initialize a text area control
	 */
	initTextArea: function() {
		if ( this.fileImport ) {
			return;
		}
		
		var $continueButton = this.$continueTextImport,
			$textArea;
		
		$textArea = $("#importText" + this.id)
			.on("keyup", function(e) {
				if ( Runner.isAcceptableKeyCode(e) ) {
					Runner.toggleDisabledClass( $continueButton, $textArea.val().trim() === "" );
				}
			})
			.on("change", function(e) {
				Runner.toggleDisabledClass( $continueButton, $textArea.val().trim() === "" );
			})
			.on('paste', function(e) { 
				Runner.toggleDisabledClass( $continueButton, false ); 
			})
			.val("");
		
		this.initTextArea = function() {
			if ( !this.fileImport ) {
				Runner.toggleDisabledClass( $continueButton, $textArea.val().trim() === "");
				$textArea.trigger("focus");
			}
		};
		
		this.initTextArea();
	},
	
	/**
	 * Initialize the file uploader controls
	 */
	initFileUploader: function() {
		if ( !this.fileUploaderSupported ) {
			// show the standard file input control to upload a user's import file
			$("#importUploadFallback" + this.id).show();
			return;
		}
		
		this.initDropZone();
		this.initHiddenFileInput();
	},
	
	/**
	 * Initialize the drag-n-drop zone area 
	 */
	initDropZone: function() {
		var pageObj = this;
		
		this.$dropZone = $("#importDropzone" + this.id, this.pageCont)
			.on('drop', function(e) {
				if ( !pageObj.fileImport || pageObj.importPageMode !== pageObj.modeConstants.DATA_CHOOSING ) {
					return false;
				}
				
				var dataTransfer = e.originalEvent.dataTransfer,
					files = dataTransfer.files;
				
				pageObj.showImportPreview( files );
				pageObj.$dropZone.hide();
				
				return false;
			})
			.on('click', function(e) {
				if ( pageObj.fileImport && pageObj.importPageMode === pageObj.modeConstants.DATA_CHOOSING ) {
					pageObj.$hiddenFileInput.trigger('click');
				}
				return false;
			});
			
		$( this.pageCont )
			.on('dragenter', function(e) {
				if ( pageObj.fileImport && pageObj.importPageMode === pageObj.modeConstants.DATA_CHOOSING ) {
					pageObj.toggleDropZone( true );
				}
				return false;
			})		
			.on('dragover', function(e) {
				return false;
			})
			.on('drop', function(e) {
				setTimeout( function() {
					if ( pageObj.fileImport && pageObj.importPageMode === pageObj.modeConstants.DATA_CHOOSING ) {
						pageObj.toggleDropZone( false );
					}
				}, 400 );
				
				return false;
			});		
	},

	/**
	 * Toggle the dropzone interface
	 * @param {boolean} toggler
	 */ 
	toggleDropZone: function( toggler ) {
		if ( !this.fileUploaderSupported ) {
			return;
		}		
		
		this.$dropZone.toggle( toggler );
		
		this.$fileUploadButton.toggle( !toggler );
		this.$textInsertButton.toggle( !toggler );
		this.toggleBlock("importheader_text", !toggler);
	},
	
	/**
	 * Set up the fidden file imput element 
	 * that is used to upload import files via File API
	 */
	initHiddenFileInput: function() {
		var pageObj = this;
			
		if ( this.$hiddenFileInput && this.$hiddenFileInput.length ) {
			this.$hiddenFileInput.remove();
		}
		
		this.$hiddenFileInput = $('<input type="file" id="hiddenImpotFile' + this.id + '"></input>')
			.css('visibility', 'hidden')
			.appendTo( this.pageCont )
			.on('change', function(e) {
				pageObj.showImportPreview( this.files );
				pageObj.initHiddenFileInput();
			});
	},	
	
	/**
	 * A wrapper for methods that show an import file/text preview
	 * using different types of data transport
	 * @param {FileList} files
	 */
	showImportPreview: function( files ) {
		if ( this.fileImport && this.fileUploaderSupported ) {
			this.showImportPreviewUsingXHR( files );
			return;
		}
		
		this.showImportPreviewUsingIframe();
	},
	
	/**
	 * Show the import preview table and preview settings 
	 * for a file being uploaded via File API 
	 * @param {FileList} files
	 */
	showImportPreviewUsingXHR: function( files ) {
		if ( !this.fileUploaderSupported || !files || !files.length ) {
			return;
		}
		
		var formData = new FormData(), 
			pageObj = this;
		
		formData.append("useXHR", true);
		formData.append("id", this.pageId);
		formData.append("a", "importPreview");
		formData.append("importType", "file");
		formData.append( "importFile" + this.id, files[0] );
		
		$.ajax( {
			url: this.submitUrl + "?" + "rndVal=" + Math.random(),
			data: formData,
			type: "POST", 
			dataType: "json",
			processData: false,
			contentType: false,
			success: function( respObj ) {
				pageObj.processPreviewData( respObj );
			},
			error: function( respObj ) {
				pageObj.switchToFileChoosing();
				Runner.displayGenericAjaxError( respObj.responseText );
			}
		} );
	},
	
	/**
	 * Show the import preview table and preview settings
	 */
	showImportPreviewUsingIframe: function() {
		var pageObj = this,
			importFormParams = {
				baseParams: {
					id: this.pageId,
					a: "importPreview", 
					importType: this.fileImport ? "file" : "text"
				}
			};
		
		if ( this.fileImport ) {
			// add the file input element to the form
			importFormParams.addElems = [ $("#importFile" + this.id) ];
		} else {
			importFormParams.baseParams["importText"] = $("#importText" + this.id).val();
		}
		
		Runner.submitRunnerFormInIframe( this.submitUrl, importFormParams, 
			function( respObj, formObj, fieldControls ) {
				pageObj.processPreviewData( respObj );
			},
			function( rawText ) {
				pageObj.switchToFileChoosing();
				Runner.displayGenericAjaxError( rawText );
			}
		);		
	},
	
	/**
	 * Fill in this.excelDateFields
	 * @param {object} previewData
	 */
	readExcelDateFieldsFromPreviewData: function( previewData ) {
		var i, fData,
			fKeys = Object.keys( previewData.fieldsData );
		this.excelDateFields = [];
		for (i = 0; i < fKeys.length; ++i) {
			fData = previewData.fieldsData[ fKeys[i] ];
			if ( fData.dateTimeType && fData.requireFormatting ) {
				this.excelDateFields.push( Number.parseInt( fKeys[i], 10 ) );
			}
		}
	},

	getLinesData: function( previewData ) {
		if ( previewData.CSVlinesData ) {
			return this.getCSVTableData( previewData.CSVlinesData, previewData.delimiter );
		}

		return previewData.tableData;
	},

	/**
	 * Process the preview data obtained
	 * @param {object} respObj
	 */
	processPreviewData: function( respObj ) {
		var previewData = respObj.previewData;
		
		if ( !previewData || Runner.isArray( previewData ) && !previewData.length ) { 
			// show the message
			return;
		}
		
		this.$importButton.show();
		this.$backButton.show();
		this.importPageMode = this.modeConstants.PREVIEW;

		this.showBlock("import_preview");
		
		this.hideBlock("import_rawtext_control");
		this.hideBlock("importheader_text"); 
		this.hideBlock("importfields");
	
		this.generateSettingsHash( previewData );
		this.readExcelDateFieldsFromPreviewData( previewData );
		this.adjustPreviewSettings( previewData );
		this.initPreviewSettingsControls( previewData );
		this.showPreviewData( previewData );
	},
	
	/**
	 * Generate the hash to store or 
	 * to retrieve import settings in the localStorage
	 * @param {object} previewData
	 * @return {string}
	 */
	generateSettingsHash: function( previewData ) {
		var firstLineData = previewData.CSVPreview ? this.getCSVTableData( [ previewData.CSVlinesData[0] ], previewData.delimiter ) : previewData.tableData[0],
			fieldsData = previewData.fieldsData || [],
			hashObj = { 
				cols: firstLineData.length 
			};		
			
		if ( !fieldsData.length ) { // if some columns names corresponds to the import fields
			hashObj["names"] = firstLineData;
		}
		hashObj["table"] = this.tName;
		
		this.settingsHash = Runner.md5( JSON.stringify( hashObj ) );
	},
	
	/**
	 * Adjust the preview settings data basing on
	 * the previously stored in the localStorage 
	 */
	adjustPreviewSettings: function( previewData ) {	
		var settingsString = localStorage.getItem( "import_" + this.settingsHash ),
			storedSettings;
			
		if ( settingsString ) {
			storedSettings = JSON.parse( settingsString );
			
			previewData.fieldsData = storedSettings.importFieldsData;
			previewData.useHeadersLineOption = storedSettings.useHeadersLineOption;
			previewData.headersLineNumber = storedSettings.headersLineNumber;
			previewData.useSkipLinesOption = storedSettings.useSkipLinesOption;
			previewData.skipLinesAmount = storedSettings.skipLinesAmount;

			if ( storedSettings.dateFormat !== "" ) {
				previewData.dateFormat = storedSettings.dateFormat;
			}
			if ( previewData.CSVPreview ) {
				previewData.delimiter = storedSettings.delimiter;
			} 		
		}
	},
	
	/**
	 * Initialize preview settings controls	
	 * @param {object} previewData
	 */
	initPreviewSettingsControls: function( previewData ) {
		$("#dateFormatSettings" + this.id).toggle( !!previewData.dateFormat );
	
		this.previewControls["dateFormat"] = !previewData.dateFormat ? null : $("#importDateFormat" + this.id)
			.val( previewData.dateFormat );

		this.initHeadersLineControl(previewData.useHeadersLineOption, previewData.headersLineNumber);
		this.initSkipLinesControl(previewData.useSkipLinesOption, previewData.skipLinesAmount);
		
		this.CSVPreview = previewData.CSVPreview;
		this.initCSVPreviewSettingsControls( previewData );
	},

	getSkipLinesAmount: function() {
		var value = this.previewControls["skipLinesAmount"].val();
		return Number(value);
	},

	getHeadersLineNumber: function() {
		var value = this.previewControls["headersLineNumber"].val();
		return Number(value);
	},

	refreshLines: function() {
		var headersLineOption = this.isHeadersLineUsed();
		var headersLineNumber = this.getHeadersLineNumber();
		var skipLinesOption = this.isSkipLinesOption();
		var skipLinesAmount = this.getSkipLinesAmount();

		var $rows = $("tr", "#importPreviewBodyTable" + this.id);
		$rows.removeClass("rnr-import-disabled");

		var headersIdx = headersLineNumber - 1;

		if (headersLineOption) {
			$rows.eq(headersIdx).addClass("rnr-import-disabled");
		}

		this.previewControls["headersLineNumber"].prop('disabled', !headersLineOption);

		if (skipLinesOption) {
			$rows.slice(0, skipLinesAmount).addClass("rnr-import-disabled");
		}
		this.previewControls["skipLinesAmount"].prop('disabled', !skipLinesOption);
	},

	initHeadersLineControl: function (useHeadersLineOption, headersLineNumber) {
		var pageObj = this;
		var enabled = !!useHeadersLineOption;
		this.previewControls["headersLineOption"] = $("#headersLineOption" + this.id )
			.prop("checked", enabled)
			.on("change", function (e) {
				pageObj.refreshLines();
			});

		this.previewControls["headersLineNumber"] = $("#headersLineNumber" + this.id)
			.prop('min', 1)
			.on("change", function(e) {
				pageObj.refreshLines();
			});

		if ( enabled ) {
			this.previewControls["headersLineNumber"].val(Number(headersLineNumber));
		}

		this.initHeadersLineControl = Runner.emptyFn;
	},

	initSkipLinesControl: function (useSkipLinesOption, skipLinesAmount) {
		var pageObj = this;
		var enabled = !!useSkipLinesOption;
		this.previewControls["skipLinesOption"] = $("#skipLinesOption" + this.id )
			.prop("checked", enabled)
			.on("change", function (e) {
				pageObj.refreshLines();
			});
	
		this.previewControls["skipLinesAmount"] = $("#skipLinesAmount" + this.id )
			.prop('min', 1)
			.on("change", function(e) {
				pageObj.refreshLines();
			});

		if ( enabled ) {
			this.previewControls["skipLinesAmount"].val(Number(skipLinesAmount));
		}

		this.initSkipLinesControl = Runner.emptyFn;	
	},
	
	/**
	 * @param {DOMElement} radioEl
	 * @param {object} previewData
	 */
	refreshPreviewData: function( radioEl, previewData ) {
		var updatedPreviewData = $.extend( {}, previewData ),
			radioValue = $(radioEl).val();
		
		updatedPreviewData.delimiter = radioValue !== "other" ? radioValue : this.previewControls["customDelimiter"].val();
		updatedPreviewData.fieldsData = this.columnsListData;
		
		if ( updatedPreviewData.delimiter ) {	
			this.showPreviewData( updatedPreviewData );
		}
	},
	
	/**
	 * Initialize preview settings controls
	 * @param {object} previewData	 
	 */
	initCSVPreviewSettingsControls: function( previewData ) {
		$("#delimiterSettings" + this.id).toggle( !!this.CSVPreview );
		
		if ( !this.CSVPreview ) {
			return;
		}
		
		var pageObj = this,
			delimiter = previewData.delimiter || ",",
			$matchedRadio = $(".importDelimiters", this.pageCont).filter( function() {
					return this.value === delimiter;
				}).prop("checked", true);
		
		this.previewControls["customDelimiter"] = $("#customDelimiter" + this.id);
		
		this.previewControls["delimiters"] = $(".importDelimiters", this.pageCont)
			.off("change")
			.on("change", function(e) {
				pageObj.refreshPreviewData( this, previewData );
			});
			
		$("#customDelimiter" + this.id, this.pageCont)
			.off("input")
			.on("input", function(e) {
				if ( $(".importDelimiters[value='other']", this.pageCont).prop("checked") ) {
					pageObj.refreshPreviewData( this, previewData );
				}
			});
		
		if ( !$matchedRadio.length ) {
			$(".importDelimiters[value='other']", this.pageCont).prop("checked", true);
			this.previewControls["customDelimiter"].val( delimiter );
		}
	},
	
	/**
	 * Show the preview table data and initialize its controls
	 * @param {object} previewData	
	 */
	showPreviewData: function( previewData ) {
		if ( !previewData.tableData && !previewData.CSVlinesData ) {
			return;
		}
				
		var tableData = this.getLinesData( previewData ),
			fieldsData = previewData.fieldsData || [],
			dateFormat = previewData.dateFormat;
		
		this.setPreviewTableMarkup( tableData, fieldsData, dateFormat );
		this.refreshLines();
		this.initPreviewTableControls();

		this.adjustImportPreviewButtons( tableData.length );
		
		$("#importErrorMessage" + this.id)
			.html("")
			.hide();
	},
	
	/**
	 * @param {number} numberOfRows
	 */
	adjustImportPreviewButtons: function( numberOfRows ) {
		if ( numberOfRows === undefined ) {
			numberOfRows = $("tr", "#importPreviewBodyTable" + this.id).length;
		}
		
		Runner.toggleDisabledClass( this.$importButton, 
			!numberOfRows || numberOfRows === 1 && this.previewControls["headersLineOption"].is(":checked") || $.isEmptyObject( this.columnsListData ) );
	},

	
	/**
	 * Set the import preview markup to the DOM
	 * @param {array} tableData
	 * @param {object} fieldsData
	 * @param {string} dateFormat
	 */	
	setPreviewTableMarkup: function( tableData, fieldsData, dateFormat ) {
		var pageObj = this, styleString = '',
			$table = $( '#importPreviewBodyTable' + this.id ),
			$importGridHead = $( "#importGridHead" + this.id ),
			$tableHead = $( '<thead></thead>' ),
			$tableBody = $( '<tbody></tbody>' ),
			headersLineIsNotUsed = this.previewControls["headersLineOption"].is(":checked"),
			dataLength = headersLineIsNotUsed && tableData[1] ? tableData[1].length : tableData[0].length,
			i, j, $tr, rowData, cellValue, headerName, headersColumn, $headTable, $style;

		for (i = 0; i < dataLength; i++) {
			$('<th class="prvcell import-column-' + i + '"></th>')
				.html( this.getImportFieldsSelectMarkup( i, fieldsData[i] ? fieldsData[i].fName : '' ) )
				.appendTo( $tableHead );
		}
		
		for (i = 0; i < tableData.length; i++) {
			$tr = $('<tr></tr>');
			rowData = tableData[i];
			headersColumn = i === 0 && headersLineIsNotUsed
			
			if ( headersColumn ) {
				$tr.addClass("rnr-import-disabled");
			}
			
			for (j = 0; j < dataLength; j++) {
				cellValue = rowData[j];
				if ( !headersColumn && cellValue && this.excelDateFields.indexOf( j ) >=0 ) {
					cellValue = this.getDateFormattedValue( cellValue, dateFormat );
				}
				
				$('<td class="prvcell import-column-' + j + '"></td>').text( cellValue ).appendTo( $tr );
			}
			
			$tr.appendTo( $tableBody );
		}

		$table.children().remove();
		
		$table
			.append( $tableHead )
			.append( $tableBody );
		
		$tableHead.children().each( function( i, th ) {
			var $th = $( th ), 
				width = $th.width(),
				thWidth = width + 'px', 
				tdWidth = (width) + 'px';
				
			$th.css({
				'width': thWidth,
				'min-width': thWidth,
				'max-width': thWidth 
			});
			
			styleString += '#importPreviewBodyTable' + pageObj.id + ' > * > * > td:nth-child(' + ( i + 1 ) + ') '
				+ '{ width:' + tdWidth + '; min-width:' + tdWidth + '; max-width:' + tdWidth + '; }';
		});
		
		$style = $('#previewTableStyle' + pageObj.id);
		if ( !$style.length ) {
			$style = $("<style id=previewTableStyle" + pageObj.id + "></style>").appendTo( document.head );
		}
		$style.html( styleString );

		$( ".importColumnSelector" ).addClass( "form-control" );
		$table.addClass( "table table-condensed table-bordered" );
		
		$headTable = $( Runner.domShallowCopy( $table.get( 0 ) ) );
		$importGridHead.empty().height( $tableHead.height() ).append( $headTable );
		$headTable.attr( "id", "importPreviewHeadTable" + this.id ).css( "position", "absolute" ).append( $tableHead );
		
		$headTable.offset( { left: $table.offset().left } );
		$table.parent().on("scroll", function( e ) {
			$headTable.offset( { left: $table.offset().left } );
		});	
	},
	
	/**
	 * Initalize the preview table controls
	 */
	initPreviewTableControls: function() {
		var pageObj = this,
			$previewTable = $('#importPreviewBodyTable' + this.id),
			// the import fields selectors
			$importcolumnsSelectors = $(".importColumnSelector", this.pageCont);
		
		this.columnsListData = {};
		
		$importcolumnsSelectors.on("change", function(e) {
			var	$select = $(this),
				currValue = $select.val(),
				idx = $select.data("idx");

			$(".import-column-" + idx, $previewTable)
				.toggleClass("rnr-import-disabledColumn", currValue === "" );
				
			if ( currValue === "" ) {
				delete pageObj.columnsListData[ idx ];
				pageObj.adjustImportPreviewButtons();
				return false;
			}
			
			pageObj.columnsListData[ idx ] = { fName: currValue };
			pageObj.adjustImportPreviewButtons();

			$importcolumnsSelectors.not( this )
				.filter( function() {
					return $(this).val() === currValue;
				})
				.val( "" )
				.trigger("change");
		});
		
		$importcolumnsSelectors.each( function(idx, select) {
			var currValue = $(this).val();
			
			$(".import-column-" + idx, $previewTable)
				.toggleClass("rnr-import-disabledColumn", currValue === "" );
			
			if ( currValue !== "" ) {
				pageObj.columnsListData[ idx ] = { fName: $(this).val() };
			}
		});
	},
	
	/**
	 * Get a table-like data array basing on CSV lines data
	 * @param {array} lines
	 * @param {string} delimiter
	 * @return {array}
	 */
	getCSVTableData: function( lines, delimiter ) {
		var tableData = [], i;
		
		for (i = 0; i < lines.length; i++) {
			tableData.push( this.parceCSVLine( lines[i], delimiter ) );
		}
		
		return tableData;
	},
	
	/**
	 * Parse a CSV-like string with a prticular delimiter into an array
	 * @param {string} line
	 * @param {string} delimiter
	 * @return {array} 
	 */
	parceCSVLine: function( line, delimiter ) {
		if ( typeof line !== "string" ) {
			return [];
		}
		
		if ( line.charAt( line.length - 1 ) === '\n' ) {
			// remove \n at the end of the line
			line = line.slice(0, -1);
		}
		
		if ( !line.length ) {
			return [];
		}
		
		var currentChar, i, 
			cellValue = '', 
			quoted = false,
			cells = [];
			
		for (i = 0; i < line.length; i++) {
			currentChar = line.charAt( i );
			
			if ( currentChar === '"' ) {
				if ( !quoted ) {
					quoted = true;
				} else {
					if ( line.charAt( i + 1 ) === '"' ) {
						i = i + 1;
					} else {
						quoted = false;
					}
				}
				//continue;
			}
			
			if ( currentChar === delimiter && !quoted ) {
				cells.push( cellValue );
				cellValue = '';
			} else {
				cellValue += currentChar;
			}
		}
		cells.push( cellValue );
		return cells;
	},
	
	/**
	 * @param {number} dateValue	A time stamp value in seconds
	 * @param {string} dateFormat
	 * @return {string}
	 */
	getDateFormattedValue: function( dateValue, dateFormat ) {
		if ( isNaN( parseInt(dateValue, 10) ) ) {
			return dateValue;
		}
		
		var date = new Date( dateValue * 1000 ),
			day = date.getDate(),
			month = date.getMonth() + 1,
			year = date.getFullYear(),
			formattedValue = dateFormat;
		
		formattedValue = formattedValue.replace(/yyyy/i, year);
		formattedValue = formattedValue.replace(/yy/i, year);
		formattedValue = formattedValue.replace(/MM/i, month < 10 ? '0' + month : month);
		formattedValue = formattedValue.replace(/M/i, month);
		formattedValue = formattedValue.replace(/dd/i, day < 10 ? '0' + day : day);
		formattedValue = formattedValue.replace(/d/i, day);
		
		return formattedValue;
	},
	
	/**
	 * Get markup for the preview dropdowns
	 * @param {number} idx
	 * @param {string} headerName
	 * @return {string}
	 */
	getImportFieldsSelectMarkup: function( idx, headerName ) {
		var optionsList = this.getImportFieldsOptions( headerName );
		
		return '<select class="importColumnSelector" data-idx="' + idx + '" id="importColumn' + idx + '">' + optionsList + '</select>';
	},
	
	/**
	 * Get a murkup for the preview dropdowns' options
	 * @param {string} headerName
	 * @return {string}
	 */	
	getImportFieldsOptions: function( headerName ) {
		var options = [ '<option value=""></option>' ],
			selectedAttr;	
		
		$.each( this.importFieldsLabels, function( fieldName, fieldLabel ) {
			selectedAttr = fieldName === headerName ? 'selected="selected"' : '';
			options.push( '<option value="' + fieldName.replace(/"/g, '&quot;') + '" ' + selectedAttr + '>' + fieldLabel + '</option>' );
		});
		
		return options.join("");
	},
	
	/**
	 * Get the user dateTime format
	 * @return {string}
	 */
	getImportDateFormat: function() {
		var $dateFomat = this.previewControls["dateFormat"];
		return $dateFomat && $dateFomat.length ? $dateFomat.val() : "";
	},
	
	/**
	 * Check if to use the headers' line data
	 * @return {boolean}
	 */
	isHeadersLineUsed: function() {
		return this.previewControls["headersLineOption"].prop("checked");
	},

	/**
	 * Get the delimiter for CSV-like import data
	 * @return {string}
	 */
	getImportDelimiter: function() {
		if ( !this.CSVPreview ) {
			return;
		} 	
		
		var	delimiter = this.previewControls["delimiters"]
				.filter(":checked").val();
		
		if ( delimiter === "other" ) {
			delimiter = this.previewControls["customDelimiter"].val();
		}
		
		return delimiter || ","; 
	},
	
	isSkipLinesOption: function() {
		return this.previewControls["skipLinesOption"].prop("checked");
	},
	
	/**
	 * @return {object}
	 */
	getImportFieldsData: function() {
		return this.columnsListData;
	},
	
	/**
	 * Import data basing on user import settings
	 */
	importData: function() {
		var pageObj = this,
			importData = {
				CSV: this.CSVPreview,
				delimiter: this.getImportDelimiter(),
				useHeadersLineOption: this.isHeadersLineUsed(),
				headersLineNumber: this.getHeadersLineNumber(),
				useSkipLinesOption: this.isSkipLinesOption(),
				skipLinesAmount: this.getSkipLinesAmount(),
				dateFormat: this.getImportDateFormat(),
				importFieldsData: this.getImportFieldsData()
			}, 
			ajaxParams = {
				a: "importData",
				importData: JSON.stringify( importData )
			};
			
		// show the fake progress
		this.showBlock("import_process");

		$("#importProgress" + this.id)
			.empty()
			.append( Runner.getLoadingBlock( Runner.lang.constants.IMPORT_PROCESSING_RECORDS ) );
		
		this.saveImportSettings( importData );

		Runner.submitRunnerFormInIframe( this.submitUrl, { baseParams: ajaxParams },
			function( respObj, formObj, fieldControls ) {	
				if ( respObj.failed ) {
					$("#importErrorMessage" + pageObj.id)
						.html( respObj.message || Runner.lang.constants.IMPORT_FAILED )	
						.show();
						
					// process respObj.message
					pageObj.backToImportPreview();
					return;
				}
				pageObj.importPageMode = pageObj.modeConstants.RESULTS;
				
				$("#reportText" + pageObj.id).html( respObj.reportText );
				
				pageObj.showBlock("import_results");
				
				pageObj.hideBlock("import_preview");
				pageObj.hideBlock("import_process");

				pageObj.initalizeReportResultButtons( respObj.unprocessedRecordsNumber > 0 );
			},
			function( rawText ) {
				pageObj.backToImportPreview();
				Runner.displayGenericAjaxError( rawText );
			}
		);
	},
	
	/**
	 * Go back to the import preview step
	 * when the import proecces is failed
	 */
	backToImportPreview: function() {
		if ( this.importPageMode !== this.modeConstants.PREVIEW ) {
			return;
		}
	
		this.hideBlock("import_process");
		
		this.$backButton.show();
		this.$importButton.show();
	},
	
	/**
	 * Store the current import settings in the localStorage
	 * @param {object} importData
	 */
	saveImportSettings: function( importData ) {
		localStorage.setItem( "import_" + this.settingsHash, JSON.stringify( importData ) );
	},
	
	/**
	 * Initialize the report buttons
	 * @param {boolean} hasNotImportedRecords
	 */
	initalizeReportResultButtons: function( hasNotImportedRecords ) {
		var pageObj = this,
			saveReportSelectorBase = hasNotImportedRecords ? 'saveErrorReport' : 'saveReport';
		
		$("[id='newImport" + this.id + "']").on("click", function(e) {
			window.location.href = Runner.pages.getUrl( pageObj.tName, Runner.pages.constants.PAGE_IMPORT, "", "", pageObj.getStateParams() );
			return false;	
		}).show();
		
		$("[id='" + saveReportSelectorBase + this.id + "']").on("click", function(e) {
			Runner.submitRunnerFormInIframe( pageObj.submitUrl, { baseParams: { a: "downloadReport" } }, 
				function( respObj, formObj, fieldControls ) {
					//process errors
				},
				function( rawResponce ) {
					Runner.displayGenericAjaxError( rawResponce );
				}
			);
			return false;
		}).show();
	
		if ( hasNotImportedRecords ) {
			$(".errorReportButtons" + pageObj.id).show();
			
			$("[id='saveUnprocessedData" + this.id + "']").on("click", function(e) {
				Runner.submitRunnerFormInIframe( pageObj.submitUrl, { baseParams: { a: "downloadUnprocessed" } }, 
					function( respObj, formObj, fieldControls ) {
						//process errors
					},
					function( rawResponce ) {
						Runner.displayGenericAjaxError( rawResponce );
					}
				);
				return false;
			});
		}
	}
});Runner.pages.RegisterPage = Runner.extend( Runner.pages.RunnerPage, {
	
	submitUrl: "",
	
	registred: false,
	
	/**
	 * The name of the Password field
	 * @type {String}
	 */
	passFieldName: "",
	
	/**
	 * The name of the Username field (login)
	 * @type {String}
	 */
	userFieldName: "",
	
	/**
	 * The name of the User email field
	 * @type {String}
	 */
	emailFieldName: "",
	
	fileFieldsCount: 0,
	
	form: null,
	
	upploadErrorHappened: false,
	
	/**
	 * The object containig pairs a field name, a field control for
	 * teh passFieldName, userFieldName, emailFieldName fields if they are set fot the page
	 * @type {object}
	 */
	controls: {},
	
	/**
	 * The object cashing the Submit button's jQuery object 
	 * @type {object}
	 */
	saveButton: {},
	
	/**
	 * @constructor 
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.RegisterPage.superclass.constructor.call(this, cfg);
		
		this.submitUrl = Runner.getPageUrl( Runner.pages.constants.PAGE_REGISTER );
		this.addEvents("beforeSave");
	},
	
	/**
	 *
	 */
	init: function() {
		Runner.pages.RegisterPage.superclass.init.call( this );
		
		if ( this.beforeSave ) {
			this.on( {'beforeSave': this.beforeSave} );
		}

		this.initFields();		
		this.initButtons();
		this.initControlEvents();
		this.addValidation();		
		
		if ( this.openMode === Runner.pages.constants.OPENMODE_POPUP ) {
			this.on('windowSizeCorrected', this.setFirstFocus);
		} else {
			this.setFirstFocus();
		}
		
		this.initPreValidation();
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	/**
	 * Assign the passFieldName, userFieldName, emailFieldName properties 
	 * with actual fields names and get their controls
	 */
	initFields: function() {
		var i, fName, realFieldName,
			fields = ["passFieldName", "userFieldName", "emailFieldName"];
		
		for (i = 0; i < fields.length; i++) {
			fName = fields[i];
			
			//get the actual database field's name
			realFieldName = Runner.pages.PageSettings.getTableData( this.tName, fName );	
					
			this[ fName ] = realFieldName;
			if ( realFieldName ) {
				this.controls[ realFieldName ] = Runner.getControl( this.pageId, realFieldName );	
			}
		}	
	},
	
	/**
	 * Add the 'IsEmail' validation to the emailFieldName field's control.
	 * Add the 'DenyDuplicated' validation and the custom validation failed message to the userFieldName field's control.
	 * Add the 'CheckStrongPassword' validation to the passFieldName field's control.
	 * Add the 'IsRequired' validation to the passFieldName, userFieldName, emailFieldName fields's control.
	 */
	addValidation: function() {
		var ctrlName, ctrl,
			ctrlEmail = this.controls[ this.emailFieldName ], 
			ctrlLogin = this.controls[ this.userFieldName ],
			ctrlPass = this.controls[ this.passFieldName ];
		
		if (ctrlEmail) {
			ctrlEmail.valueElem.attr('autocomplete', 'off');
			ctrlEmail.addValidation('IsEmail');
			ctrlEmail.addValidation('DenyDuplicated');
			ctrlEmail.customValidationFailedMessages['DenyDuplicated'] = {
				text: Runner.lang.constants.EMAIL_ALREADY1
					+ "&nbsp;<i>%value%</i>&nbsp;" 
					+ Runner.lang.constants.EMAIL_ALREADY2,
				type: 0
			}
		}
			
		if (ctrlLogin) {
			//ctrlLogin.valueElem.attr('autocomplete', 'off');
			ctrlLogin.addValidation('DenyDuplicated');
			ctrlLogin.customValidationFailedMessages['DenyDuplicated'] = {
				text: Runner.lang.constants.USERNAME_EXISTS1
					+ "&nbsp;<i>%value%</i>&nbsp;" 
					+ Runner.lang.constants.USERNAME_EXISTS2,
				type: 0
			}
		}
		
		if ( ctrlPass && Runner.pages.PageSettings.getGlobalData("pwdStrong") ) {
			ctrlPass.addValidation('CheckStrongPassword');
		}
		
		for (ctrlName in this.controls) {
			ctrl = this.controls[ ctrlName ];	
			if (!ctrl) {
				continue;
			}
			
			if ( !ctrl.isSetValidation('IsRequired') ) {
				ctrl.addValidation('IsRequired');
			}
		}
	},
	
	/**
	 * Initialize the page's buttons
	 */
	initButtons: function() {
		var pageObj = this;
		
		this.saveButton = $("a[id=saveButton" + this.id + "]").on("click", function(e) {
			var ctrls = Runner.controls.ControlManager.getAt(pageObj.tName),
				$button = $(this),
				ctrl, index;
			
			$button.addClass('disabled');
			pageObj.upploadErrorHappened = false;
			pageObj.fileFieldsCount = 0;
			
			for (index = 0; index < ctrls.length; index++) {
				ctrl = ctrls[ index ];
				
				if (ctrl.editFormat == Runner.controls.constants.EDIT_FORMAT_FILE && ctrl.filesToUploadCount > 0) {				
					pageObj.fileFieldsCount++;
					ctrl.errorHappened = false;
					
					ctrl.uploadForm.on('fileuploadstopped', { ctrl: ctrl }, function(e, data) {
						pageObj.fileFieldsCount--;
						$button.off('fileuploadstopped');
						
						if (e.data.ctrl.errorHappened) {
							pageObj.upploadErrorHappened = true;
							pageObj.callErrorHn();
						} else {
							pageObj.callSaveHn();
						}	
					});
					$(".btn-primary.start", ctrl.uploadForm).trigger("click");
				} 
			}
			
			if ( pageObj.fileFieldsCount < 1 ) {
				pageObj.callSaveHn();
			}
			
			return false;
		});


		this.findItemType("register_reset").on("click", function(e) {
			pageObj.resetControls();
		});
	},
	
	/**
	 * Error handler for fileuploadstopped event
	 */
	callErrorHn: function() {
		if (this.fileFieldsCount < 1) {
			this.saveButton.removeClass('disabled');
		}
	},
	
	/**
	 * Save handler for save button click event
	 */
	callSaveHn: function() {	
		if ( this.upploadErrorHappened || this.fileFieldsCount > 0 ) {
			return;
		}
				
		this.getForm();
		this.setPageModified(false);
		this.form.submit();
	},
		
	/**
	 * Add the "password" and "confirm" field's "on blur" handlers, checking if their values are the same,
	 */
	initControlEvents: function() {
		var confirmCtrl = Runner.controls.ControlManager.getAt(this.tName, this.id, "confirm"),
			passCtrl = this.controls[ this.passFieldName ];
		
		if ( !confirmCtrl ) {
			return;
		}


		var $confirmItem = $( "[data-itemtype$=confirm_password][data-pageid=" + this.id + "]" );
		if ( !$confirmItem.length ) {
			return;
		}

		passCtrl.on("blur", function(e) {
			if ( this.getValue() != confirmCtrl.getValue() && confirmCtrl.getValue() != "" ) {	
				confirmCtrl.markInvalidWidthCusttomMessages( "notMatchPasswords", [ Runner.lang.constants.PASSWORDS_DONT_MATCH ] );
				confirmCtrl.setFocus();
				return;
			}
			
			confirmCtrl.clearInvalid();
		});
		
		confirmCtrl.on("blur", function(e) {
			if ( this.getValue() != passCtrl.getValue() ) {
				this.markInvalidWidthCusttomMessages( "notMatchPasswords", [ Runner.lang.constants.PASSWORDS_DONT_MATCH ] );
				return;
			}
			
			this.clearInvalid();			
		});
	},
	
	/**
	 * Marks the "username", "password" and/or "confirm" fields as invalid basing on fields error messages
	 */
	initPreValidation: function() {
		var invalidControls = [],
			userErrorMessage = Runner.pages.PageSettings.getTableData(this.tName, "msg_userError"),
			emailErrorMessage = Runner.pages.PageSettings.getTableData(this.tName, "msg_emailError"),
			passwordErrorMessage = Runner.pages.PageSettings.getTableData(this.tName, "msg_passwordError");
			
		if ( userErrorMessage !== "" ) {
			this.controls[ this.userFieldName ].markInvalidWidthCusttomMessages( "userErrorMessage", [ userErrorMessage ] );
			invalidControls.push( this.controls[ this.userFieldName ] );
		}
		
		if ( this.controls[ this.emailFieldName ] && emailErrorMessage !== "" ) {
			this.controls[ this.emailFieldName ].markInvalidWidthCusttomMessages( "emailErrorMessage", [ emailErrorMessage ] );
			invalidControls.push( this.controls[ this.emailFieldName ] );
		}
		
		if ( passwordErrorMessage !== "" ) {
			this.controls[ this.passFieldName ].markInvalidWidthCusttomMessages( "passwordErrorMessage", [ passwordErrorMessage ] );
			invalidControls.push( this.controls[ this.passFieldName ] );
		}
		
		this.setFirstFocus( this.id, invalidControls );
	},

	/**
	 * @param {string} restore ( to restore session on add/edit page )
	 * @param {object} hostPage
	 */
	openLoginPageOnPopUpClose: function( restore, hostPage ) {
		var user = this.registred ? this.controls[ this.userFieldName ].getValue() : '',
			pass = this.registred ? this.controls[ this.passFieldName ].getValue() : '';

		this.submitAndOpenLoginPopup( user, pass, restore, hostPage );
	},

	/**
	 *
	 */
	getForm: function() {
		var pageObj = this;
		
		this.form = this.form || new Runner.form.BasicForm({
			id: this.pageId,
			method: 'POST',
			submitUrl: this.submitUrl,
			isFileUpload: true,
			standardSubmit: this.openMode !== Runner.pages.constants.OPENMODE_POPUP, 
			baseParams: {
				btnSubmit: "Register", 
				id: this.pageId, 
				onFly: this.openMode === Runner.pages.constants.OPENMODE_POPUP ? 1 : 0,
				page: this.pageName
			},
			fieldControls: Runner.controls.ControlManager.getAt( this.tName ),
			beforeSubmit: {
				fn: function( formObj ) {
					var confirmCtrl = Runner.controls.ControlManager.getAt( this.tName, this.id, "confirm" ),
						passCtrl = this.controls[ this.passFieldName ],
						pdConfirmCheck = true;
					

					var $confirmItem = $( "[data-itemtype$=confirm_password][data-pageid=" + this.id + "]" );
					pdConfirmCheck = $confirmItem.length;

					if ( confirmCtrl && pdConfirmCheck && passCtrl && passCtrl.getValue() !== confirmCtrl.getValue() ) {
						confirmCtrl.markInvalidWidthCusttomMessages( "notMatchPasswords", [ Runner.lang.constants.PASSWORDS_DONT_MATCH ] );
						Runner.delDisabledClass( this.saveButton );
						return false;
					}
					
					return this.fireEvent("beforeSave", formObj, formObj.fieldControls, this);
				},
				scope: this
			},
			successSubmit: {
				fn: function(respObj, basicForm, fieldControls) {
					var $procToLogin, $header;
					
					if ( respObj.html ) {
						this.registred = true;
						if ( this.bsWin ) {
							$(".modal-body", this.bsWin ).html( respObj.html );							
						}

						$("a[id=closeWindowRegister]")
							.on("click", function(e) {
								pageObj.openLoginPageOnPopUpClose();
								pageObj.close();
								return false;
							})

						$header = $(".modal-header", this.bsWin );
						$header.children("*:not(.close)").remove();
						$header.prepend(respObj.headerCont);
						this.toggleItemType("register_reset", false);
							
						$procToLogin = $("a[id=ProceedToLogin]").hide();
						if ( $procToLogin.length ) {
							this.saveButton.hide();
						}
						
						if ( respObj.additionalJS ) { // for Custom button support
							$.each(respObj.additionalJS, function( jsFile, jsFileReq ) {
								Runner.util.ScriptLoader.addJS( [jsFile], jsFileReq );
							});
		
							Runner.util.ScriptLoader.onFilesLoaded( function() {						
								pageObj.pageData.buttons = respObj.pagesData[pageObj.id]["buttons"];						
								pageObj.initCustomButtons();							
							}, pageObj );	
							Runner.util.ScriptLoader.load();
						}
					} else if ( !respObj.success && respObj.message ) {				
						this.displayHalfPreparedMessage( respObj.message, true );

						// for tab/section/steps
						if ( respObj.wrongCaptchaFieldName !== undefined ) {
							this.openFieldTabsSections( [ respObj.wrongCaptchaFieldName ] );
							Runner.getControl( this.pageId, respObj.wrongCaptchaFieldName ).setFocus();
							this.adjustHeightAndWidth();
						}
					}
					
					// temp update for popup register with steps/tabs/sections before refrakt
					else if ( !respObj.success && respObj.withoutMessage ) {						
						this.setFirstFocus();
					}
					
					Runner.delDisabledClass( this.saveButton );
				},
				scope: this
			},
			validationFailed: {
				fn: function(formObj, fieldControls, failedControlsArr) {
					var fNamesArr = [], i;
					
					for (i = 0; i < failedControlsArr.length; i++) {
						fNamesArr.push( failedControlsArr[i].fieldName );
					}
					
					this.openFieldTabsSections( fNamesArr );
					
					// scroll to the firt field, failed its validation, to make it visible once tab/sec is open
					setTimeout(function() { failedControlsArr[0].setFocus(); }, 4);

					Runner.delDisabledClass( this.saveButton );
				},
				scope: this
			},
			submitFailed: {
				fn: function( response, formObj, fieldControls ) {
					Runner.delDisabledClass( this.saveButton );
				},
				scope: this
			},
		});
	},

	resetControls: function() {
		var cntrls = Runner.controls.ControlStorage.byId( this.id ),
			updContext, i;

		if ( !cntrls || !cntrls.length ) {
			return;
		}
			
		updContext = {			
			resetHappend: true,
			values: {}
		};
			
		for (i = 0; i < cntrls.length; i++) {	
			if ( cntrls[i].editFormat != "Readonly" ) {					
				updContext.values[ cntrls[i].fieldName ] = cntrls[i].defaultValue;			
			}
		}
		
		for (i = 0; i < cntrls.length; i++) {
			if ( cntrls[i].editFormat != "Readonly" ) {
				cntrls[i].reset( updContext );
			}
		}
	}
});Runner.pages.ChangePwdPage = Runner.extend( Runner.pages.RunnerPage, {
	/**
	 * Form object
	 * @type {Object}
	 */
	form: null,
	/**
	 * Fields list
	 * @type {Object}
	 */
	fields: null,
	/**
	 * Submit URL string for form action
	 * @type {String}
	 */
	submitUrl: "",
	/**
	 * The object contain pairs a field name, a field control 
	 * for the oldPass, newPass and confirm fields 
	 * @type {object}
	 */
	controls: null,
	/**
	 * The object cashing the Submit button's jQuery object 
	 * @type {object}
	 */
	saveButton: {},
	/**
	 * The name of the Old Password field
	 * @type {String}
	 */
	oldPassFieldName: "oldpass", 
	/**
	 * The name of the New Password field
	 * @type {String}
	 */
	newPassFieldName: "newpass",
	/**
	 * The name of the Confirm Password field
	 * @type {String}
	 */
	confirmFieldName: "confirm",
	
	hostPage: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */	
	constructor: function( cfg ) {
		Runner.pages.ChangePwdPage.superclass.constructor.call( this, cfg );
		
		this.fields = [ this.oldPassFieldName, this.newPassFieldName, this.confirmFieldName ];
		this.submitUrl = Runner.getPageUrl( Runner.pages.constants.PAGE_CHANGEPASS );

		this.addEvents("beforeSave");
	},
	
	init: function() {
		Runner.pages.ChangePwdPage.superclass.init.call( this );
		
		if ( this.beforeSave ) {
			this.on( {'beforeSave': this.beforeSave} );
		}

		this.initFields();
		this.initButtons();
		this.initControlEvents();
		this.addValidation();
		this.initPreValidation();
		
		this.setFirstFocus();
		this.on('windowSizeCorrected', this.setUserFocus);
		
		this.fireEvent('afterInit', this, this.proxy, this.id);
	},
	
	/**
	 * Assign the passFieldName, userFieldName properties 
	 * with actual fields names and get their controls
	 */
	initFields: function() {
		this.controls = {};
		
		for (var i = 0; i < this.fields.length; i++) {
			this.controls[ this.fields[i] ] = Runner.getControl( this.pageId, this.fields[i] );	
		}	
	},
	
	initButtons: function() {
		var pageObj = this;
		
		this.saveButton = $("[id=saveButton" + this.id + "]").on("click", function(e) {
			$(this).addClass('disabled');
			pageObj.callSaveHn();
			
			return false;
		}); 
	},
	
	callSaveHn: function() {
		this.setPageModified( false );
		
		this.initForm();
		this.form.submit();
	},
	
	initForm: function() {
		if ( this.form ) {
			return;
		}
		
		this.form = new Runner.form.BasicForm({
			id: this.pageId,
			method: 'POST',
			submitUrl: this.submitUrl,
			standardSubmit: this.openMode !== Runner.pages.constants.OPENMODE_POPUP, 
			isFileUpload: true,
			fieldControls: Runner.controls.ControlManager.getAt( this.tName ),
			baseParams: {
				btnSubmit: "Change", 
				id: this.pageId,
				token: this.proxy["token"],
				mode: this.openMode === Runner.pages.constants.OPENMODE_POPUP ? "popup" : "",
			},
			beforeSubmit: {
				fn: function( formObj ) {
					return this.fireEvent( "beforeSave", formObj, formObj.fieldControls, this );
				},
				scope: this
			},
			validationFailed: {
				fn: function( formObj, fieldControls ) {
					Runner.delDisabledClass( this.saveButton );
				},
				scope: this
			},
			submitFailed: {
				fn: function( response, formObj, fieldControls ) {
					Runner.delDisabledClass( this.saveButton );
				},
				scope: this
			},
		});
		
		this.form.on( 'successSubmit', function( respObj, basicForm, fieldControls ) {
			if ( respObj.success  ) {
				$(".modal-body", this.bsWin ).html( respObj.html );
				$(".modal-footer", this.bsWin ).html( respObj.footerCont );

				this.initSuccessBackButton();
			} else {
				if ( respObj.message ) {
					this.displayHalfPreparedMessage( respObj.message, true );
					this.showElement( 'message' );
				}
				
				if ( respObj.wrongCaptchaFieldName !== undefined ) {
					Runner.getControl( this.pageId, respObj.wrongCaptchaFieldName ).setFocus();
				}
				
				this.adjustHeightAndWidth();
				Runner.delDisabledClass( this.saveButton );
			}
			
		}, this );
	},
	
	initSuccessBackButton: function() {
		var pageObj = this;
		$("[id=backButton" + this.id + "]").on("click", function(e) {
			pageObj.close();
			return false;
		});
	},
	
	/**
	 * Add the "new password" and "confirm password" field's "on blur" handlers, checking if their values are the same,
	 */
	initControlEvents: function() {		
		var confirmCtrl = this.controls[ this.confirmFieldName ],
			newPassCtrl = this.controls[ this.newPassFieldName ];
			
		newPassCtrl && newPassCtrl.on('edited', function(e) {
			if ( this.getValue() != confirmCtrl.getValue() && confirmCtrl.getValue() != "" ) {	
				confirmCtrl.markInvalidWidthCusttomMessages( "notMatchPasswords", [ Runner.lang.constants.PASSWORDS_DONT_MATCH ] );
				
				confirmCtrl.setValue('');
				confirmCtrl.setFocus();
				return;
			}
			
			confirmCtrl.clearInvalid();
		});
		
		confirmCtrl && confirmCtrl.on('edited', function(e) {
			if ( this.getValue() != newPassCtrl.getValue() ) {
				this.markInvalidWidthCusttomMessages( "notMatchPasswords", [ Runner.lang.constants.PASSWORDS_DONT_MATCH ] );
				
				this.setValue('');
				newPassCtrl.setValue('');
				newPassCtrl.setFocus();
				return;
			}
			
			this.clearInvalid();
		});		
	},
	
	/**
	 * Add the 'CheckStrongPassword' validation to the passFieldName field's control.
	 * Add the 'IsRequired' validation to the fields's control.
	 */
	addValidation: function() {
		var ctrlName, ctrl,
			newPassCtrl = this.controls[ this.newPassFieldName ];
		
		if ( newPassCtrl && Runner.pages.PageSettings.getGlobalData("pwdStrong") ) {
			newPassCtrl.addValidation('CheckStrongPassword');
		}
		
		for ( ctrlName in this.controls ) {
			ctrl = this.controls[ ctrlName ];
			if ( !ctrl ) {
				continue;
			}
			
			if ( !ctrl.isSetValidation('IsRequired') ) {
				ctrl.addValidation('IsRequired');
			}
		}
	},
	
	/**
	 * Marks the fields as invalid basing on server error messages
	 */
	initPreValidation: function() {
		var passwordErrorMessage = Runner.pages.PageSettings.getTableData( this.tName, "msg_passwordError" );
			
		if ( passwordErrorMessage !== "" && this.controls[ this.newPassFieldName ] ) {
			this.controls[ this.newPassFieldName ].markInvalidWidthCusttomMessages( "passwordErrorMessage", [ passwordErrorMessage ] );
		}
	},
	
	messageItemType: function() {
		return 'changepwd_message';
	},
	
	/**
	 * openPage params
	 */
	getOpenPageParams: function() {
		return {
			pageType: Runner.pages.constants.PAGE_CHANGEPASS,
			tName: Runner.pages.PageSettings.getGlobalData( "loginTName" ),
			modal: true,
			hostPage: this,
			baseParams: {
				mode: "popup",
			},
			requestUrl: Runner.getPageUrl( "changepwd" )
		};
		
	},
});/**
 * Runner.pages.SessionExpiredPage class
 */
Runner.pages.SessionExpiredPage = Runner.extend( Runner.pages.RunnerPage, {	
	/**
	 * popup host page link to control session.
	 */
	hostPage: null,
	
	/**
	 * updating seconds timer
	 */
	timerId: -1,

	/**
	 * a number of milliseconds showing time untill logout 
	 * at that moment popup is opened
	 */
	logoutOn: null,
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.pages.SessionExpiredPage.superclass.constructor.call( this, cfg );
	},

	/**
	 * Initialize the page
	 */
	init: function() {
		Runner.pages.SessionExpiredPage.superclass.init.call( this );
		
		this.initButtons();
		this.initTimer();
		this.fireEvent( 'afterInit', this, this.proxy, this.id );
	},

	getCommentElement: function() {
		return $( "[data-itemtype=session_expired_comment]" );
	},
	
	initTimer: function() {
		var pageObj = this;

		var commentItem = this.getCommentElement();
		if ( !commentItem ) {
			return;
		}

		var template = commentItem.text();
		if(template == '') {
			template = Runner.lang.constants.SESSION_EXPIRED_COMMENT;
		}

		var timerFn = function() {
			var seconds = Math.floor( ( pageObj.logoutOn - Date.now() ) / 1000 );		
			if ( seconds < 0 ) {
				pageObj.hostPageObj.sessionControlLogout( true );
				return;
			}
			commentItem.text( template.replace( "%seconds%", seconds ) );
		};

		timerFn();
		this.timerId = setInterval( function() {
			timerFn();
		}, 1000 );
	},

	destroyTimer: function() {
		clearInterval( this.timerId );
	},
	
	/**
	 * Initialize the login page's buttons
	 */
	initButtons: function() {
		var pageObj = this;

		$( "#stayLogged" + this.id, this.pageCont ).on("click", function(e) {
			Runner.lastUserActivity = Date.now();
			// send request from the host page
			pageObj.hostPageObj.sendForceExpiredRequest();
			
			pageObj.destroyTimer();
			pageObj.close();
			return false;
		});
	
		$( "#logout" + this.id, this.pageCont ).on( "click", function(e) {
			pageObj.hostPageObj.sessionControlLogout();
			return false;
		});
	},
});
Runner.pages.AddPageCalendar = Runner.extend( Runner.pages.AddPage, {
	init: function() {
		Runner.pages.AddPageCalendar.superclass.init.call( this );
		this.initCalendarPage();
	},

	initCalendarPage: function() {
		//	close window on escape
		$( this.pageCont ).on( 'keyup', ( e ) => {
			if( e.keyCode == 27 ) {
				this.close();
			}
		} );

		//	show/hide time on fullday event
		var calendarSettings = Runner.pages.PageSettings.getTableData(this.tName, 'calendarSettings' );
		var fullDayField = Runner.getControl( this.pageId, calendarSettings.fullDayField );
		if( fullDayField ) {
			fullDayField.on( 'change', () => {
				if( fullDayField.getValue() ) {
					this.hideField( calendarSettings.timeField );
					this.hideField( calendarSettings.endTimeField );
				} else {
					this.showField( calendarSettings.timeField );
					this.showField( calendarSettings.endTimeField );
				}
			});
		}

	}
	
});
Runner.pages.EditPageCalendar = Runner.extend( Runner.pages.EditPage, {
	hostPage: null,

	init: function() {
		Runner.pages.EditPageCalendar.superclass.init.call( this );
		Runner.pages.AddPageCalendar.prototype.initCalendarPage.call( this );
	},

	
	initButtons: function () {
		Runner.pages.EditPageCalendar.superclass.initButtons.call( this );

		// init delete button
		$( "[id=deleteButton" + this.id + "]" ).on( "click", () => {
			var baseParams = {
				a: 'deleteEvent',
			}
			for ( var i = 0; i < this.keys.length; ++i ) {
				baseParams["editid" + ( i + 1 )] = this.keys[i];
			}
			baseParams.page = this.pageName;
			var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );

			Runner.runnerAJAX( url, baseParams, ( respObj ) => {
				if ( respObj.success ) {
					this.close();
					this.hostPage.updateEditedEvent( this.keys, null );
				} else {
					Runner.displayGenericAjaxError( respObj.error );
				}
			});
		} );
	}

} );Runner.pages.ViewPageCalendar = Runner.extend( Runner.pages.ViewPage, {
	hostPage: null,

	init: function() {
		Runner.pages.ViewPageCalendar.superclass.init.call( this );
		Runner.pages.AddPageCalendar.prototype.initCalendarPage.call( this );
		this.pageCont.focus();
	},

	
	initButtons: function () {
		Runner.pages.ViewPageCalendar.superclass.initButtons.call( this );

		// init delete button
		$( "[id=deleteButton" + this.id + "]" ).on( "click", () => {
			var baseParams = {
				a: 'deleteEvent',
			}
			for ( var i = 0; i < this.keys.length; ++i ) {
				baseParams["editid" + ( i + 1 )] = this.keys[i];
			}
			baseParams.page = this.pageName;
			var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_VIEW );

			Runner.runnerAJAX( url, baseParams, ( respObj ) => {
				if ( respObj.success ) {
					this.close();
					this.hostPage.updateEditedEvent( this.keys, null );
				} else {
					Runner.displayGenericAjaxError( respObj.error );
				}
			});
		} );

		// init edit button
		$( "[id=editPageButton" + this.id + "]" ).off('click').on( "click", () => {
			this.close();
			this.hostPage.editEvent( this.keys );
		} );

	}

} );Runner.pages.EditPageGantt = Runner.extend( Runner.pages.EditPage, {
	hostPage: null,

	init: function() {
		Runner.pages.EditPageGantt.superclass.init.call( this );
		Runner.pages.EditPageGantt.prototype.initGanttPage.call( this );
	},

	initGanttPage: function() {
		//	close window on escape
		$( this.pageCont ).on( 'keyup', ( e ) => {
			if( e.keyCode == 27 ) {
				this.close();
			}
		} );
	},
	
	initGanttButtons: function() {
		// init delete button
		$( "[id=deleteButton" + this.id + "]" ).on( "click", () => {
			var baseParams = {
				a: 'deleteTask',
				gantt: 1,
			}
			for ( var i = 0; i < this.keys.length; ++i ) {
				baseParams["editid" + ( i + 1 )] = this.keys[i];
			}
			baseParams.page = this.pageName;
			var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );

			Runner.runnerAJAX( url, baseParams, ( respObj ) => {
				if ( respObj.success ) {
					this.close();
					this.hostPage.updateEditedTask( this.keys, null );
				} else {
					Runner.displayGenericAjaxError( respObj.error );
				}
			});
		} );

		// add child button
		$( "[id=addChildButton" + this.id + "]" ).on( "click", () => {
			this.hostPage.addChildTask( this.keys[0] );
		} );

	},
	
	initButtons: function () {
		Runner.pages.EditPageCalendar.superclass.initButtons.call( this );
		this.initGanttButtons();
	}

} );Runner.pages.ViewPageGantt = Runner.extend( Runner.pages.ViewPage, {
	hostPage: null,

	init: function() {
		Runner.pages.ViewPageGantt.superclass.init.call( this );
		Runner.pages.EditPageGantt.prototype.initGanttPage.call( this );
		this.pageCont.focus();
	},

	
	initButtons: function () {
		Runner.pages.ViewPageGantt.superclass.initButtons.call( this );

		// init delete button
		$( "[id=deleteButton" + this.id + "]" ).on( "click", () => {
			var baseParams = {
				a: 'deleteEvent',
			}
			for ( var i = 0; i < this.keys.length; ++i ) {
				baseParams["editid" + ( i + 1 )] = this.keys[i];
			}
			baseParams.page = this.pageName;
			var url = Runner.getPageUrl( this.shortTName, Runner.pages.constants.PAGE_EDIT );

			Runner.runnerAJAX( url, baseParams, ( respObj ) => {
				if ( respObj.success ) {
					this.close();
					this.hostPage.updateEditedEvent( this.keys, null );
				} else {
					Runner.displayGenericAjaxError( respObj.error );
				}
			});
		} );
	}

} );
/**
 * Search form controller. Need for submit form in advanced and panel mode
 */
Runner.search.SearchForm = Runner.extend( Runner.util.Observable, {	
	/**
	 * Id of page, used when page loades dynamicly
	 * @type {number}
	 */
	id: -1,
	
	/**
	 * Name of table for which instance of class was created
	 * @type {string}
	 */
	tName: "",

	/**
	 * The host page object reference
	 * @type {object} 
	 */
	pageObj: null,

	/**
	 * Short table name, used for create urls
	 */
	shortTName: "",
	
	/**
	 * @type {string}
	 */
	pageType: "",

	/**
	 * Indicator. 
	 * True when simple search edit box get focus 
	 * @type {Boolean}
	 */
	usedSrch: false,
	
	/**
	 * @type {boolean}
	 */
	useSuggest: false,
	
	/**
	 * Type of search: panel on list, or advanced search page
	 * @type {string}
	 */
	searchType: "panel",

	/**
	 * Array of the search fields
	 */
	fNamesArr: [],
	
	/**
	 * @type {object}
	 */
	baseParams: null,

	/**
	 * The search form base params
	 * @type {object}
	 */
	formBaseParams: null,
	
	/**
	 * Indicator showing if the search is required to show data
	 */
	isSearchRequired: false,
	
	/**
	 * Array containing the names of the required Search panel fields
	 * @param {Array}
	 */
	requiredSearchFields: null,
	
	/**
	 * jQuery obj
	 * @type {object} 
	 */
	srchForm: null,
	
	/**
	 * ctrls map. Used for indicate which index conected with which search ctrl
	 * @type {object}
	 */
	ctrlsShowMap: {},

	ajaxSubmit: false,
		
	optCombosArr: [],
	
	/**
	 * Array of user fields ojects
	 */
	searchFields: null,

	/**
	 * Search button object
	 * @type {jQuery object}
	 */
	searchButton: null,
	
 	/**
	 * jQuery obj of top radio with conditions
	 * @type {object}
	 */
	conditionRadioTop: null,
 
	/**
	 * The simple search edit box
	 * @type {jQuery object}
	 */
	smplSrchBox: null,
	
	/**
	 * jQuery object representing the page's DOM element allowing 
	 * to chose the search option ('Contains', 'Equals', etc.)
	 * @type {jQuery object} 
	 */
	simpleSrchTypeCombo: null,
	
	/**
	 * jQuery object representing the page's DOM element allowing 
	 * to chose a particular search field or the 'Any field' option 
	 * @type {jQuery object} 
	 */	
	simpleSrchFieldsCombo: null,
 
	/**
	 * if case we need envoke special submit procedure from form's page
	 */
	delegatedSubmit: null,
 
	delegatedExtraSuggestParams: null,
 
 
	/**
	 * Override parent contructor
	 * Add interaction with server
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		this.fNamesArr = [];
		this.ctrlsShowMap = {};
		this.baseParams = {};
		this.searchFields = {};
		this.optCombosArr = [];
		this.requiredSearchFields = [];
		// copy properties from cfg to controller obj
		Runner.apply( this, cfg );

		this.pageObj = Runner.pages.PageManager.getAt( this.tName, this.id );

		Runner.search.SearchForm.superclass.constructor.call(this, cfg);

		// radio with contion choose or|and
		this.conditionRadioTop = $('input:radio[name=srchType' + this.id + ']');	
		 // Set the Simple search panel's search input elem
		this.smplSrchBox = $('#ctlSearchFor' + this.id);
		
		this.simpleSrchTypeCombo = $('#simpleSrchTypeCombo' + this.id);
		this.simpleSrchFieldsCombo = $('#simpleSrchFieldsCombo' + this.id);
		
		this.addEvents('beforeSearch', 'afterSearch');
	},

	init: function( ctrlsBlocks ) {
		this.initControlBlocks( ctrlsBlocks ); 
		this.initSearchFields();
		this.initSearchFieldsHandlers();
		this.initForm();
		this.initButtons();
	},

	/**
	 * Initialize the page's search form
	 */
	initForm: function() { 	
		this.srchForm = new Runner.form.BasicForm({
			id: this.id,
			addRndVal: false,
			isSearchForm: true,
			initImmediately: true,
			submitUrl: this.getSubmitUrl(),
			standardSubmit: !this.ajaxSubmit,
			baseParams: this.formBaseParams || this.baseParams || {},
			method: this.ajaxSubmit ? "POST" : "GET"
		});
		
		this.srchForm.on('successSubmit', function( respObj ) {
			this.fireEvent('afterSearch', respObj, this, this.srchForm);
		}, this);
		
	},
	
	/**
	 * Returns the submit url for the form
	 * @description For the webreports' reports and charts it returns the URL in form of "dreport.ext"/"dcahrt.ext".
	 * For the report, chart and print pages It returns the URL in form of "<tableName>_repor.ext", "<tableName>_chart.ext", "<tableName>_print.ext".
	 * For all other pages it returns "<tableName>_list.ext".
	 * @return {String}
	 */
	getSubmitUrl: function() {
		//this.searchTableName is set for the search panel activated by adding the "searchpanel" brick to the page's layout 
		//It sets the real search table's name for the non table page's search panels 
		var tName = this.searchTableName || this.tName,
			pageType = this.pageType;
		
		// prepare tName, pageType params for the BasicForm's submitUrl generation		
		if ( this.pageType === Runner.pages.constants.PAGE_DREPORT || this.pageType === Runner.pages.constants.PAGE_DCHART ) {
			tName = "";
		} else if ( ![
			Runner.pages.constants.PAGE_GANTT,
			Runner.pages.constants.PAGE_CALENDAR,
			Runner.pages.constants.PAGE_REPORT,
			Runner.pages.constants.PAGE_CHART,
			Runner.pages.constants.PAGE_PRINT,
			Runner.pages.constants.PAGE_DASHBOARD,
			Runner.pages.constants.PAGE_SEARCH,
			Runner.pages.constants.PAGE_ADMIN_MEMBERS,
			Runner.pages.constants.PAGE_ADMIN_ADMEMBERS,
			Runner.pages.constants.PAGE_ADMIN_RIGHTS
		].includes( this.pageType ) ) {
			pageType = Runner.pages.constants.PAGE_LIST;
		}
		
		return Runner.pages.getUrl( tName, pageType );	
	},

	/**
	 * Add on "change" handler to search fields search 
	 * options dropdowns. Add the corresponding dorpdown's 
	 * jQuery object to optCompoArr array.
	 * @param {number} recId
	 * @param {string} fName
	 * @param {Array} map
	 */
	initCombo: function( recId, fName, map ) {
		var $combo = $( "#" + this.getComboId( fName, recId ) ),
			tName = this.tName,
			controller = this;
			
		var ctrl1 = Runner.controls.ControlManager.getAt( tName, recId, fName, 0 ),
			ctrl2;
			
		if ( !ctrl1 ) {
			return;
		}
		
		if ( map.length === 2 ) {
			ctrl2 = Runner.controls.ControlManager.getAt( tName, recId, fName, 1 );
		}			
		
		var onSearchOptionChange = function() {
			var value = $combo.val();
			
			ctrl1.updateAppearance( value );		
			ctrl1.toggle( value !== 'Empty' && value !== 'NOT Empty' );	
			
			if ( map.length === 2 ) {
				ctrl2 = Runner.controls.ControlManager.getAt( tName, recId, fName, 1 );
			}		
			
			ctrl2 && ctrl2.toggle( value === 'Between' || value === 'NOT Between' );				
		}
		
		onSearchOptionChange();
		
		$combo.on( "change", function(e) {
			onSearchOptionChange();
			controller.toggleSearhButtonStatus();
		});
		
		this.optCombosArr.push( $combo );
	},
	
	/**
	 * Init common search page's buttongs
	 */
	initButtons: function() {				
		this.searchButton = $("#searchButton" + this.id);
		this.initSearchButtonStatus();		
	},
		
	/**
	 * @param {Array} ctrlsBlocks
	 */
	initControlBlocks: function( ctrlsBlocks ) {
		for (var i = 0; i < ctrlsBlocks.length; i++) {
			this.addRegCtrlsBlock( ctrlsBlocks[i].fName, ctrlsBlocks[i].recId, ctrlsBlocks[i].ctrlsMap );
				
			if ( this.isSearchPanelInflexible ) { //is invoked for the Search Panel only
				//add the field's search option for the inflexible Search panel's field
				this.inflexSearchOptions[ ctrlsBlocks[i].fName ] = ctrlsBlocks[i].inflexSearchOption;
			}
		}
	},
	
	/**
	 * Look over all search field controls and 
	 * set search fields handlers using these controls
	 */
	initSearchFieldsHandlers: function() {
		var fName, recId, ctrlIndices, i, ctrl;
		
		for (fName in this.ctrlsShowMap) {
			for (recId in this.ctrlsShowMap[ fName ]) {
				ctrlIndices = this.ctrlsShowMap[ fName ][ recId ];
				
				for (i = 0; i < ctrlIndices.length; i++) {
					ctrl = Runner.controls.ControlManager.getAt( this.tName, recId, fName, ctrlIndices[i] );
					this.initSearchField( ctrl );
				}	
			}
		}
	},
	
	/**
	 * Init Search Suggest, add search field's object to the SearchFields, 
	 * set disablings/enabling Search button handlers for a required field
	 * @param {Object} ctrl
	 */	
	initSearchField: function( ctrl ) {
		if ( !ctrl ) {
			return;
		}

		var controller = this;
		
		if ( ctrl.ctrlInd === 0 && !ctrl.cached ) {
			//invoke "addSearchField" for the first control only
			//as far as It creates a new one Search Field instance
			//that then represents both the first and seconds controls
			this.addSearchField( ctrl.fieldName, ctrl.id );
		}
		
		this.initSuggest( ctrl );
		ctrl.on('keydown', function(e) {
			controller.listenEvent( e, this.valueElem.get(0) );
		});
		
		this.setHandlerForRequiredField( ctrl );
	},
	
	/**
	 * Init Seatch Suggest 
	 * @param {Object} ctrl
	 */
	initSuggest: function( ctrl ) {		
		if ( !this.useSuggest || !ctrl.useSearchSuggests() ) {
			return;
		}

		var controller = this,
			$searchSuggestDiv = $('#search_suggest');
			
		//adjust the markup
		if ( !$searchSuggestDiv.length ) {
			$searchSuggestDiv = $('<div id="search_suggest"></div>').appendTo('body');
		}
		
		$searchSuggestDiv.addClass('search_suggest');
		
		ctrl.on('keyup', function(e) {	
			if ( !Runner.isAcceptableKeyCode(e) || !ctrl.implySuggests() ) {
				return;
			}

			if ( controller.submitTimeout ) {
				clearTimeout( controller.submitTimeout );
			}			

			controller.submitTimeout = setTimeout( function() {	
				controller.searchSuggestForCtrl( ctrl );			
			}, 700);
		});
	},	
	
	/**
	 * Listen keyboard events
	 * @param {obj} oEvent
	 * @param {obj} oElement
	 */
	listenEvent: function( oEvent, oElement ) {				
		switch ( ( window.event || oEvent ).keyCode ) {
			case 38: //up arrow
				this.useSuggest && Runner.suggests.moveUp( oElement );
			break;
			case 40: //down arrow
				this.useSuggest && Runner.suggests.moveDown( oElement );
			break;
			case 9: //tab
				this.useSuggest && Runner.suggests.DestroySuggestDiv();
			break;			
			case 13: //enter
				this.useSuggest && Runner.suggests.DestroySuggestDiv();
				
				if ( !Runner.isDisabledButton( this.searchButton ) ) {
					this.delegatedSubmit ? this.delegatedSubmit() : this.submitSearch(); 			
				}
			break;
		}
	},
	
	/**
	 * Fill the search form with the saved search's params 
	 * and submit the form. The standart submit is used even
	 * for the pages with ajax search functionality
	 * @param {Object} searchParams
	 * It may contains: 
	 *	 'qs' The simple search params
	 *	 'q' The search panel's params	
	 *	 'f' The filters' params	
	 *	 'criteria' The search panel's criterion
	 */	
	submitSavedSearch: function( searchParams ) {
		this.ajaxSubmit = false;
		this.srchForm.baseParams = {};
		this.srchForm.standardSubmit = true; 
	
		var q = searchParams['q'], 
			qs = searchParams['qs'], 
			f = searchParams['f'], 
			criterion = searchParams['criteria'];
			
		this.fireEvent('beforeSearch', this, this.srchForm);
		this.srchForm.clearForm();
		this.srchForm.searchSubmit = true;	
		
		//add the Simple Search params to the form
		if ( qs !== undefined && qs !== '' ) {
			this.srchForm.addToForm( 'qs', qs );
			this.srchForm.addToSearchForm( 'qs', encodeURIComponent( qs ) );
		}	
		
		//add the Search panel params to the form
		if ( q !== undefined && q !== '' ) {
			this.srchForm.addToForm( 'q', q );
			this.srchForm.addToSearchForm( 'q', encodeURIComponent( q ) );
		}		
		
		//add the filters' params to the form
		if ( f !== undefined && f !== '' ) {
			this.srchForm.addToForm( 'f', f );
			this.srchForm.addToSearchForm( 'f', encodeURIComponent( f ) );
		}
		
		//add the search panel's criterion to the form
		if ( criterion && criterion !== 'and' ) {
			this.srchForm.addToForm( 'criteria', criterion );
			this.srchForm.addToSearchForm( 'criteria', encodeURIComponent( criterion ) );
		}

		if ( qs === '' && this.ajaxSubmit ) {
			this.srchForm.addToForm( 'q', '' );
		}

		if ( !this.pageObj.isDefaultPage ) {
			this.srchForm.addToForm( 'page', this.pageObj.pageName );
			this.srchForm.addToSearchForm( 'page', this.pageObj.pageName );
		}
		
		this.srchForm.addToForm( 'savedSearch', true );
		this.srchForm.addToSearchForm( 'savedSearch', true );
		
		this.usedSrch = true;
		this.srchForm.submit();
	},

	/**
	 * Fill the search form params and submit it 
	 */
	submitSearch: function() {
		this.prepareSubmitSearch();
		this.srchForm.submit();
	},

	/**
	 * Prepare search controller props for submit
	 */
	prepareForSearchSubmit: function() {
		this.usedSrch = true;
	},
	
	/**
	 * Add simple search params to the form
	 * @return {string}
	 */
	addSimpleQueryParams: function() {
		var i, valSeparator = '~', 
			simpleQuery = '', simpleQueryEncoded = '',
			simpleSrchTypeComboVal = this.simpleSrchTypeCombo.val(),
			simpleQueryArr = [ this.searchEscape( ( this.smplSrchBox.val() || "" ).toString().trim() , true ), '', '' ];
		
		// for simple search with combos
		simpleQueryArr[1] = this.searchEscape( this.simpleSrchFieldsCombo.val() || "", true );
			
		//default combo value
		if ( simpleSrchTypeComboVal == 'Contains' ) {
			simpleSrchTypeComboVal = "";
		} else {
			simpleSrchTypeComboVal = Runner.pages.constants.SEARCH_OPTIONS[ simpleSrchTypeComboVal ];
		}
		simpleQueryArr[2] += simpleSrchTypeComboVal || "";
		
		for (i = simpleQueryArr.length - 1; i >= 0; i--) {
			simpleQuery = simpleQueryArr[i] + (simpleQuery == '' ? '' : valSeparator) + simpleQuery;
			simpleQueryEncoded = encodeURIComponent( simpleQueryArr[i] )
				+ (simpleQueryEncoded == '' ? '' : valSeparator) + simpleQueryEncoded;
		}
		
		if ( simpleQuery != '' ) {
			this.srchForm.addToForm('qs', simpleQuery);
			this.srchForm.addToSearchForm('qs', simpleQueryEncoded);
		}
	
		return simpleQuery;
	}, 
	
	/**
	 * Add a criteria param to the form
	 */
	addCriteriaParam: function() {
		var criterion = this.getSearchCriterion();
		
		if ( criterion !== 'and' ) {
			this.srchForm.addToForm( 'criteria', criterion );	
			this.srchForm.addToSearchForm( 'criteria', encodeURIComponent( criterion ) );		
		}
	},
	
	/**
	 * Get the search criterion
	 * @return {string}
	 */	 
	getSearchCriterion: function() {
		return this.conditionRadioTop.length ? this.conditionRadioTop.filter(':checked').val() : 'and';
	},
	
	/**
	 * Add search panel's and search page's params to the form
	 * @param {string} simpleQuery
	 */
	addAdvansedQueryParams: function( simpleQuery ) {
		var queryParams = this.getSearchControlsQueryParams(),
			query = queryParams['query'],
			queryEncoded = queryParams['queryEncoded'];
		
		if ( query != '' ) {
			this.srchForm.addToForm( 'q', query );
			this.srchForm.addToSearchForm( 'q', queryEncoded );	
			return;
		} 
		
		if ( simpleQuery == '' && this.ajaxSubmit ) {
			this.srchForm.addToForm( 'q', '' );
		}	
	},
	
	/**
	 * @return {object}
	 */
	getSearchControlsQueryParams: function() {
		var i, fName, ind,
			valSeparator = '~', 
			fieldSeparator = ')(',
			query = '', queryEncoded = '';			
			
		// add search params for each field
		for (fName in this.ctrlsShowMap) {
			// loop through all ctrls, except cached and deleted
			for (ind in this.ctrlsShowMap[ fName ]) {	
				var fMap = this.ctrlsShowMap[ fName ][ ind ], // get ctrls map for field name
					ctrl1 = Runner.controls.ControlManager.getAt( this.tName, ind, fName, fMap[0] ),
					srchCombo = $('#' + this.getComboId( fName, ind )),
					cachedRow = $("#" + this.getFilterRowId( fName, ind )),
					srchCheckBox = $('#' + this.getCheckBoxId( fName, ind )),
					fieldQuery = '', fieldQueryEncoded = '',
					comboVal, fieldArray, 
					ctrl2 = null;
					
				if ( !ctrl1.appearOnPage() ) { 
					continue;
				}
				
				//get the control's searh option 
				comboVal = srchCombo.length ? srchCombo.val() : this.getInflexSearchOption( fName );

				if ( !comboVal ) {
					continue;
				}
				
				// if search type contains between and second ctrl exists
				if ( comboVal.indexOf('Between') !== -1 && fMap[1] ) {
					ctrl2 = Runner.controls.ControlManager.getAt( this.tName, ind, fName, fMap[1] );
				}
				
				// add only non empty and not cashed vals
				if ( ctrl1.isEmptyForSearch() && comboVal.indexOf('Empty') == -1 
					&& ( comboVal.indexOf('Between') == -1 || ctrl2 && ctrl2.isEmptyForSearch() ) 
						|| cachedRow.css('display') == 'none' ) {
					continue;
				}
				
				// define first value and type
				fieldArray = ['', this.searchEscape( ctrl1.getUserValue() ), ctrl1.ctrlType, ''];
				
				// define option
				if (comboVal != '') {
					if ( srchCheckBox.length && srchCheckBox.prop("checked") ) {
						comboVal = "NOT " + comboVal;
					}
					fieldArray[0] = Runner.pages.constants.SEARCH_OPTIONS[ comboVal ];
				}
				
				// if search type contains between and second ctrl exists
				if ( ctrl2 ) {	
					fieldArray[3] = this.searchEscape( ctrl2.getUserValue() ); 
				}
				
				for (i = fieldArray.length; i--; ) {
					fieldQuery = fieldArray[i] + (fieldQuery == '' ? '' : valSeparator) + fieldQuery;
					fieldQueryEncoded = encodeURIComponent( fieldArray[i] ) + (fieldQueryEncoded == '' ? '' : valSeparator) + fieldQueryEncoded;
				}
				// add fName to query
				query += (query != '' ? fieldSeparator : '(') + this.searchEscape( fName ) + valSeparator + fieldQuery;
				queryEncoded += (queryEncoded != '' ? fieldSeparator : '(') + encodeURIComponent( this.searchEscape( fName ) ) + 
					valSeparator + fieldQueryEncoded;
			}
		}
		
		return {
			query: query === '' ? query : query + ')',
			queryEncoded: queryEncoded === '' ? queryEncoded : queryEncoded + ')'
		};
	},
	
	/**
	 * Add filters' params to the form
	 */
	addFilterParams: function() { 
		var i, ctrl, filterValue, filterValueEncoded, 
			query = '',	queryEncoded = '';
		
		if ( !this.filterControls || !this.filterControls.length ) {
			return;
		}
		
		for (i = 0; i < this.filterControls.length; i++) {
			ctrl = this.filterControls[i];

			filterValue = ctrl.getFilterValue();
			if ( filterValue ) {	
				query += filterValue;
				
				filterValueEncoded = ctrl.getFilterValueEncoded();
				queryEncoded += filterValueEncoded;
			}
		}
		
		if ( query === '' ) {
			queryEncoded = query ='all';
		}
		this.srchForm.addToForm( 'f', query );
		this.srchForm.addToSearchForm( 'f', /*encodeURIComponent( decodeURIComponent( */ queryEncoded /*) )*/ );
	},
	
	/**
	 * @param {Mixed} value
	 * @param {Boolean} searchSimple
	 */
	searchEscape: function( value, searchSimple ) {
		if ( typeof value == "object" ) {
			var formObj = this;
			
			$.each(value, function( index, element ) {
				value[ index ] = formObj.searchEscape( value[ index ], searchSimple );
			});
			
			return value;
		}
		
		if ( !value ) {
			return "";
		}

		value = value
			.replace( /\\/g, "\\\\" )
			.replace( /~/g, "\\~" );
		
		if ( searchSimple ) {
			return value;
		}
		
		return value.replace(")(","\\)(");
	},
	
	/**
	 * @deprecated
	 * Add params for crosstable report
	 */
	addExtraPageParams: function() {
		return;
		
		/*if ( !this.pageObj ) {
			return;
		}
		
		var croossParams = this.pageObj.getExtraParamsFroSearch(), p;
		
		if ( !croossParams ) {
			return;
		}
		
		for ( p in croossParams ) {
			this.srchForm.addToForm( p, croossParams[ p ] );
		}*/
	},
		
	/**
	 * Register ctrl in show map
	 * @param {string} fName
	 * @param {number} ind
	 * @param {array} ctrlMap	A search control's indices array
	 */
	addToShowMap: function( fName, ind, ctrlMap ) {
		this.ctrlsShowMap[ fName ] = this.ctrlsShowMap[ fName ] || {};
		this.ctrlsShowMap[ fName ][ ind ] = ctrlMap;
	},
	
	/**
	 * Adds block to map, regs its components and ands HTML
	 * @param {string} fName
	 * @param {number} ind
	 * @param {array} ctrlMap
	 */
	addRegCtrlsBlock: function( fName, ind, ctrlMap ) {
		if ( ctrlMap ) {
			this.addToShowMap( fName, ind, ctrlMap );
		}
		this.initCombo( ind, fName, ctrlMap );
	},
	
	/**
	 * Return search type combo id
	 * @param {string} fName
	 * @param {int} ind
	 * @return {string}
	 */
	getComboId: function( fName, ind ) {
		return "srchOpt_" + ind + "_" + Runner.goodFieldName( fName );
	},
	
	/**
	 * Return filter div id
	 * @param {string} fName
	 * @param {int} ind
	 * @return {string}
	 */
	getFilterRowId: function( fName, ind ) {
		return "filter_" + ind + "_" + Runner.goodFieldName( fName );
	},
	
	/**
	 * Return search checkbox id
	 * @param {string} fName
	 * @param {int} ind
	 * @return {string}
	 */
	getCheckBoxId: function( fName, ind ) {
		return "not_" + ind + "_" + Runner.goodFieldName( fName );
	},
	
	/**
	 * Submit the 'Show all' search
	 */
	showAllSubmit: function() {
		this.srchForm.clearForm();
		this.srchForm.addToForm('a', 'showall');

		this.prepareForShowAllSubmit();
		this.srchForm.submit();
	},
	
	/**
	 * Prepare search controller props for 'search all' submit
	 */
	prepareForShowAllSubmit: function() {
		this.usedSrch = false;
		
		this.simpleSearchActive = false;
		this.clearSimpleSearch();
		
		this.clearCtrls();
	},
	
	/**
	 * Submit the search form to return to the list page
	 */
	returnSubmit: function( page ) {
		this.srchForm.clearForm();
		this.srchForm.searchSubmit = false;
		
		this.srchForm.addToForm('a', 'return');
		if ( page ) {
			this.srchForm.addToForm('page', page);
		}
		
		this.srchForm.submit();
	},
	
	/**
	 * Resets the form controls
	 */
	clearCtrls: function() {
		var $defaultOption, $select, i;
		jQuery.each( Runner.controls.ControlStorage.byTable( this.tName ), function() {
			this.clear();
		});
		
		for (i = 0; i < this.optCombosArr.length; i++) {
			$select = this.optCombosArr[i];
			if ( !$select.length ) {
				continue;
			}
			
			$defaultOption = $select.find('option[data-default-option]');
			$defaultOption = $defaultOption.length ? $defaultOption : $select.find('option').eq(0);
			
			$defaultOption.prop('selected', true);
			$select.trigger("change");
		}
		
		$("input[id^=not_]").prop('checked', false);
		this.conditionRadioTop.first().prop('checked', true);
	}, 
	
	/**
	 * Check there are a not empty search control on the page
	 * @return {Boolean}
	 */
	hasNotEmptyControl: function() {
		var fName, searchFields, i;
		
		for (fName in this.searchFields) {
			searchFields = this.searchFields[ fName ];
			
			for (i = 0; i < searchFields.length; i++) {
				if ( searchFields[i].checkIfCtrlsValuesSet() ) {
					return true;
				}
			}
		}
		
		return false;
	},
	
	/**
	 * Reset the Simple search controls' elements values
	 */
	clearSimpleSearch: function() {
		var defaultOption;
		
		this.smplSrchBox.val('');

		if ( this.simpleSrchFieldsCombo.length ) {
			defaultOption = $("option:visible:eq(0)", this.simpleSrchFieldsCombo).val(); 
			this.simpleSrchFieldsCombo.val( defaultOption );
		}
		if ( this.simpleSrchTypeCombo.length ) {
			defaultOption = $("option:visible:eq(0)", this.simpleSrchTypeCombo).val(); 
			this.simpleSrchTypeCombo.val( defaultOption );
		}
	},
	
	/**
	 * Init SearchFields object for all search fields
	 */
	initSearchFields: function() {
		for (var i = 0; i < this.fNamesArr.length; i++) {
			this.searchFields[ this.fNamesArr[i] ] = [];
		}
	},
	
	/**
	 * Creates a new SearchField object and adds it to 
	 * the SearchController's searchFields object
	 * @param {string} fName
	 * @param {integer} id
	 */
	addSearchField: function( fName, id ) {
		if ( !this.searchFields[ fName ] ) {
			return;
		}
				
		this.searchFields[fName].push( new Runner.search.SearchField({
				id: id,
				fName: fName,
				searchObj: this
			}) );
	},
	
	/**
	 * Set "change" event's handler for visible Search panel's ctrls
	 * @param {Object} ctrl
	 */
	setHandlerForRequiredField: function( ctrl ) {
		var searchController = this,
			$ctrlElem;
		
		if ( !this.isFieldRequired( ctrl.fieldName ) ) {
			return false;
		}		
		ctrl.on("editing", function() {
			searchController.toggleSearhButtonStatus();
		});
		// since the chosen search suggest fires the 'change' event
		ctrl.on("edited", function() {
			searchController.toggleSearhButtonStatus();
		});		
		
		$ctrlElem = ctrl.getDispElem(); // #8064
		if ( $ctrlElem instanceof jQuery ) {
			// #8064 it allows to handle the IE10 clear-button click
			$ctrlElem.on("input", function() {
				searchController.toggleSearhButtonStatus();
			});	
		}
	},
	
	/**
	 * Check if the field is search required or not 
	 * @param {String} fName
	 * @return {Boolean}
	 */ 
	isFieldRequired: function( fName ) {
		if ( $.inArray(fName, this.requiredSearchFields) === -1 ) {
			return false;
		}
		return true;
	},

	/**
	 * Set Search Button enabled/disabled basing on 
	 * the required search fields' ctrls values
	 */
	toggleSearhButtonStatus: function() {
		var i, j, rfName, rfSearchFields, 
			disable;
		
		if ( !this.requiredSearchFields.length ) {
			return;
		}
		
		for (i = 0; i < this.requiredSearchFields.length; i++) {	
			rfName = this.requiredSearchFields[i];
			rfSearchFields = this.searchFields[rfName];
			disable = true;
			
			for (j = 0; j < rfSearchFields.length; j++) {
				if ( rfSearchFields[j].checkIfCtrlsValuesSet() ) {
					disable = false;
					break;
				}
			}		
			
			if ( disable && ( this.simpleSrchFieldsCombo.val() !== rfName || this.smplSrchBox.val() === '' ) ) { 
				break;
			}
		}
		
		this.searchButton.closest('.rnr-button, .btn').toggleClass('disabled', disable);
	},

	/**
	 * If "Required Search to show data" is turned on 
	 * add "change" handlers to the simple search controls and
	 * and set the Search button enabled/disabled
	 */
	initSearchButtonStatus: function() {
		var searchController = this;
		
		if ( !this.requiredSearchFields.length ) {
			return false;
		}
	
		this.toggleSearhButtonStatus();
		
		this.simpleSrchFieldsCombo.on("change", function(e) {
			searchController.toggleSearhButtonStatus();
		});
		
		this.smplSrchBox.on("change", function(e) {
			searchController.toggleSearhButtonStatus();
		});	
	},
	
	/**
	 * Get the search params for a dashboard search page
	 * @param {boolean} showAllMode
	 * @return {object}
	 */
	getDashSearchParams: function( showAllMode ) {
		var searchParams = {},
			searchQueryParams;
		
		if ( showAllMode ) {
			searchParams['a'] = 'showall';
		} else {
			searchQueryParams = this.getSearchControlsQueryParams();
							
			if ( searchQueryParams['query'] ) {
				searchParams['q'] = searchQueryParams['query'];
				searchParams['criteria'] = this.getSearchCriterion();
			}
		}
		
		return searchParams;
	},
	
	/**
	 * get and show search suggests for simple search
	 */
	searchSuggestForSimple: function( $searchBox ) {
		var fieldName = $("#simpleSrchFieldsCombo1").val() || '',
			startWith = $("#simpleSrchTypeCombo1").val() == "Starts with",
			setValueFn = function( value ) {
				$searchBox.val( value );
			};

		this.searchSuggest( $searchBox, fieldName, $searchBox.val(), startWith, setValueFn );	
	},
	
	/**
	 * get and show search suggests for search ctrl
	 */
	searchSuggestForCtrl: function( ctrl ) {
		var srchTypeComboId = this.getComboId( ctrl.fieldName, this.id ),
			startWith = $('#' + srchTypeComboId).val() == 'Starts with',
			setValueFn = function( value ) {
				ctrl.setValue( value, true );
			};
			
		this.searchSuggest( ctrl.getDispElem(), ctrl.fieldName, ctrl.getValue(), startWith, setValueFn );
	},
	
	getSuggestUrl: function() {
		return Runner.getPageUrl( 'searchsuggest', '', 'table=' + this.shortTName );
	},
	
	searchSuggest: function( $el, fName, currValue, startWith, setValueFn ) {
		Runner.suggests.searchSuggest( $el );
		
		var reqParams = {
				searchFor: currValue, 
				searchField: fName || '',
				rndVal: new Date().getTime(),
				start: startWith ? 1 : 0,
				pageType: this.pageType,
				page: this.pageObj.pageName
			};
		
		reqParams = Runner.apply( reqParams, this.pageObj.getStateParams() );
		
		if ( typeof this.delegatedExtraSuggestParams === 'function' ) {
			reqParams = Runner.apply( reqParams, this.delegatedExtraSuggestParams() );	
		}
		
		Runner.runnerAJAX( this.getSuggestUrl(), reqParams, function ( respObj ) {
			Runner.suggests.onSearchSuggestResponse( respObj, setValueFn );
		});
	},

	hideSearchFieldsSuggests: function() {		
		for (var fName in this.searchFields) {
			var searchFields = this.searchFields[ fName ];
			for (var i = 0; i < searchFields.length; i++) {
				var ctrl = searchFields[i].getControl();
				if ( ctrl instanceof Runner.controls.EditBoxLookup ) {
					ctrl.destroySuggest();
				}
			}
		}
	},

	addParamsToForm: function( params = {} ) {
		var keys = Object.keys( params );
		for ( var i = 0; i < keys.length; i++ ) {
			var key = keys[i];
			this.srchForm.addToSearchForm( key, params[key] );
		}
	},

	submitSearchWithParams: function( params = {} ) {
		this.prepareSubmitSearch();
		
		this.addParamsToForm( params );

		this.srchForm.submit();
	},

	prepareSubmitSearch: function() {
		var simpleQuery;
	
		this.fireEvent('beforeSearch', this, this.srchForm);
		this.srchForm.clearForm();
		this.srchForm.searchSubmit = true;	

		//add the Simple Search params to the form
		simpleQuery = this.addSimpleQueryParams();
		//add the Search panel params to the form
		this.addAdvansedQueryParams( simpleQuery );
		//add the filters' params to the form
		this.addFilterParams();

		if ( !Object.keys( this.srchForm.searchForm ).length ) {
			this.srchForm.addToSearchForm( 'a', 'return' );	
		}
		
		//add the search panel's criterion to the form
		this.addCriteriaParam();

		this.prepareForSearchSubmit();
	}

});
Runner.search.options = {
	CONTAINS: "Contains",
	EQUALS: "Equals",
	STARTS_WITH: "Starts with",
	MORE_THAN: "More than",
	LESS_THAN: "Less than",
	BETWEEN: "Between",
	EMPTY: "Empty",
	
	NOT_CONTAINS: "NOT Contains",
	NOT_EQUALS: "NOT Equals",
	NOT_STARTS_WITH: "NOT Starts with",
	NOT_MORE_THAN: "NOT More than",
	NOT_LESS_THAN: "NOT Less than",
	NOT_BETWEEN: "NOT Between",
	NOT_EMPTY: "NOT Empty"
};

Runner.search.optionsText = {

	"Contains": Runner.lang.constants.CONTAINS,
	"Equals": Runner.lang.constants.EQUALS,
	"Starts with": Runner.lang.constants.STARTS_WITH,
	"More than": Runner.lang.constants.MORE_THAN,
	"Less than": Runner.lang.constants.LESS_THAN,
	"Between": Runner.lang.constants.BETWEEN,
	"Empty": Runner.lang.constants.EMPTY,
	
	"NOT Contains": Runner.lang.constants.NOT_CONTAINS,
	"NOT Equals": Runner.lang.constants.NOT_EQUALS,
	"NOT Starts with": Runner.lang.constants.NOT_STARTS_WITH,
	"NOT More than": Runner.lang.constants.NOT_MORE_THAN,
	"NOT Less than": Runner.lang.constants.NOT_LESS_THAN,
	"NOT Between": Runner.lang.constants.NOT_BETWEEN,
	"NOT Empty": Runner.lang.constants.NOT_EMPTY
};

/**
 * Global object for 
 * @object
 */
Runner.search.SearchField = Runner.extend(Runner.emptyFn, {
	/**
	 * Field name
	 * @type {string}
	 */
	fName: "",
	/**
	 * Field id
	 * @type {integer}
	 */
	id: 0,
	/**
	 * Select element with search options
	 * @type {object}
	 */
	opt: null,
	/**
	 * Search controller object
	 * @type {object}
	 */
	searchObj: null,
	
	constructor: function(cfg) {
		Runner.apply(this, cfg);
		this.init();
	},
	
	init: function() {
		this.opt = $('#' + this.searchObj.getComboId(this.fName, this.id)).get(0);
	},
	
	reInit: function() {
		if (!this.id) {
			this.getId();
		}
		this.init();
	},
	/**
	 * Is reinit was implemented successful
	 */
	isReInitSuccess: function() {
		if (typeof this.opt == 'undefined') {
			this.reInit();
			if (!this.id) {
				return false;
			}
		}
		return true;
	},
	/**
	 * Get field name
	 * @return {string}
	 */
	getName: function() {
		return this.fName;
	},
	/**
	 * Get field id
	 * @return {integer}
	 */
	getId: function() {
		if (!this.id) {
			var id = this.searchObj.getLastAddedInd(this.fName);
			if (!this.searchObj.isFieldShownById(this.fName, id)) {
				this.id = id;
			}
		}
		return this.id;
	},
	/**
	 * Get field control object
	 * @return {object}
	 */
	getControl: function() {
		return Runner.controls.ControlManager.getAt(this.searchObj.tName, this.id, this.fName);
	},
	/**
	 * Get second control object
	 * @return {object}
	 */
	getSecondControl: function() {
		return Runner.controls.ControlManager.getAt(this.searchObj.tName, this.id, this.fName, 1);
	},
	/**
	 * Get selected option value
	 * @param {string}
	 * @return {string}
	 */
	getOption: function() {
		if ( !this.isReInitSuccess() || !this.opt ) {
			return false;
		}
		for (var j = 0; j < this.opt.options.length; j++) {
			if (this.opt.options[j].selected) {
				return this.opt.options[j].value;
			}
		}
	},
	/**
	 * Set option value
	 * @param {string}
	 */
	setOption: function(value) {
		if (typeof value == 'undefined' || !value) {
			return;
		}
		if (!this.isReInitSuccess()) {
			return;
		}
		for (var j = 0; j < this.opt.options.length; j++) {
			if (this.opt.options[j].value == value) {
				this.opt.selectedIndex = j;
				$(this.opt).trigger("change");
				break;
			}
		}
	},
	/**
	 * Add new option 
	 * @param {string}
	 */
	addOption: function(value) {
		if (typeof value == 'undefined' || !value) {
			return;
		}
		if (!this.isReInitSuccess()) {
			return;
		}
		var text = Runner.search.optionsText[value];
		var opt = new Option(text, value);
		$(opt).html(text);
		$(this.opt).append(opt);
	},
	/**
	 * Get all options
	 * @param {string}
	 * @return {array}
	 */
	getOptions: function() {
		if (!this.isReInitSuccess()) {
			return false;
		}
		return this.opt.options;
	},
	/**
	 * Remove option
	 * @param {string}
	 */	
	removeOption: function(value) {
		if (!this.isReInitSuccess()) {
			return;
		}
		if (this.opt.options.length == 1) {
			this.searchObj.hideCtrlTypeCombo();
			return;
		}
		for (var j = 0; j < this.opt.options.length; j++) {
			if (this.opt.options[j].value == value) {
				if (this.opt.options[j].selected) {
					if (j == 0) {
						this.opt.selectedIndex = j + 1;
					} else {
						this.opt.selectedIndex = j - 1;
					}
				}
				this.opt.remove(j);
				break;
			}
		}
	},
	/**
	 * Remove field from serach panel
	 * @param {string}
	 */
	remove: function() {
		this.searchObj.deleteField(this.fName, this.id);
	},
	
	/**
	* Clear first and second search field's controls
	*/
	clearFieldCtrls: function() {
		var ctrl;
		
		ctrl = this.getControl();
		if (ctrl) {
			ctrl.clear();
		}
		
		ctrl = this.getSecondControl();
		if ( ctrl && !ctrl.spanContElem.is(":visible") ) {
			ctrl.clear();
		}
	},
	
	/**
	* Cheks if the Search field's controls are not empty 
	* @return {Boolean} 
	*/
	checkIfCtrlsValuesSet: function() {
		var ctrl, ctrl1,
			option = this.getOption();
		
		if ( option === "Empty" || option === "NOT Empty" ) {
			return true;
		}
		
		ctrl = this.getControl();
		ctrl1 = this.getSecondControl();

		if ( ( option === "Between" || option === "NOT Between" ) && ( !ctrl.isEmptyForSearch() || !ctrl1.isEmptyForSearch() ) ) {
			return true;
		}

		if ( ctrl && ctrl.isEmptyForSearch() ) {
			return false;
		}
		
		if ( ctrl1 && ctrl1.spanContElem.is(":visible") && ctrl1.isEmptyForSearch() ) {
			return false;
		}
		
		return ctrl || ctrl1;
	}
});/**
 * The method creates a Filter-control basing on its format
 * @param {object} baseCfg
 * @param {object} searchController		The searchController object reference
 */
Runner.controls.FilterControlFactory = function( baseCfg, searchController ) {
	//add to the filter's cfg a reference to the Search Controller object
	var cfg = {
		searchController: searchController
	};
	
	cfg = Runner.apply( cfg, baseCfg );
	
	switch ( cfg.filterFormat ) {
		case Runner.controls.constants.FILTER_FORMAT_VALUES_LIST:
		case Runner.controls.constants.FILTER_FORMAT_INTERVAL_LIST:
			switch ( cfg.multiSelect ) {
				case Runner.controls.constants.FILTER_MULTISELECT_NONE:
				return new Runner.controls.SimpleFilterControl( cfg ); 	

				case Runner.controls.constants.FILTER_MULTISELECT_ON_DEMAND:
				return new Runner.controls.OnDemandFilterControl( cfg ); 			

				case Runner.controls.constants.FILTER_MULTISELECT_ALWAYS:
				return new Runner.controls.MultiSelectFilterControl( cfg );
				
				default:
					throw( "Invalid multiselect param(" + cfg.multiSelect + ") for the " + cfg.filterFormat + " format. Cannot create control!" );
				return;	
			}
		
		case Runner.controls.constants.FILTER_FORMAT_BOOLEAN:
		return new Runner.controls.SimpleFilterControl( cfg ); 
		
		case Runner.controls.constants.FILTER_FORMAT_INTERVAL_SLIDER:
			if ( cfg.isFieldDateType ) {
				return new Runner.controls.DateSliderFilterControl( cfg );
			}
			if ( cfg.isFieldTimeType ) {
				return new Runner.controls.TimeSliderFilterControl( cfg );
			}			
		return new Runner.controls.SliderFilterControl( cfg ); 
		
		default:
			throw( "Invalid filter control's format = " + cfg.filterFormat + ". Cannot create control!" );
	}
}


/**
 * The base abstract class for filter controls
 */
Runner.controls.FilterControl = Runner.extend( Runner.emptyFn, {
	/**
	 * It Stores the contol's value for non-multiselect filter contorls 
	 * @type {String | Number}
	 */
	ctrlValue: '',
	
	/**
	 * Filter control field's name	 
	 * @type {String}
 	 */
	fieldName: '',
	
	/**
	 * Filter contorl "good" field's name
	 * @type {String}
	 */
	gfieldName: '',
	
	/**
	 * Filter contorl's format
	 * @type {String}
	 */	
	filterFormat: '',
	
	/**
	 * The number indicating if a contol allows
	 * multiselection always, "on demand" or never
	 * @type {number}
	 */
	multiSelect: 0,
	
	/**
	 * The indicator if the control's field is filtered
	 * @type {Boolean}
	 */
	filtered: false,
	
	/** 
	 * It stores filtration values 
	 * for a filtered control
	 * @type {Array}
	 */
	defaultValuesArray: [],
	
	/**
	 * Values to show corresp. to defaultValuesArray prop
	 * @type {Array}
	 */
	defaultShowValues: [],
	
	/**
	 * The name of the Filter panel container's class
	 * @type {String}
	 */
	filterPanelContainerClass: "filterPanelContainer",
	
	/**
	 * The jQuery object representing the Filter panel's div container
	 * @type {object}
	 */
	filterPanelContainer: {},

	/**
	 * The jQuery object representing the filter control's container
	 * @type {object}
	 */
	filterContainer: {},
	
	/**
	 * The the link to the search Controller intance 
	 * (that is the same for all filter controls)
	 * @type {Object} 
	 */
	searchController: {},

	/**
	 * The name of the Filter "Apply" button's class
	 * @type {String}
	 */
	btnApplyClass: "filter-btn-apply",

	/**
	 * The class of the element toggling control visibility
	 * @type {String}
	 */
	btnExpanderClass: "filter-expander",

	/**
	 * The class of the element showing whether filter is hidden or displayed
	 * @type {String}
	 */
	btnExpanderIndicatorClass: "filter-indicator",

	/**
	 * The jQuery object representing the filter control's expander element
	 * @type {object}
	 */
	expanderElem: {},
	
	/**
	 * The name of the filter's hidden values class
	 * @type {String}
	 */	
	hiddenValuesClass: "filter-hidden",

	/**
	 * The name of the filter's collapsed values class
	 * @type {String}
	 */	
	collapsedValuesClass: "filter-collapsed",

	/**
	 * The name of the filter value block container's class
	 * @type {String}
	 */
	filterValueBlockContainerClass: "filter-values",
	
	/**
	 * The name of the filter link-value container's class
	 * @type {String}
	 */
	filterLinkClass: "filter-link",
	
	/**
	 * The filter's URL params separator
	 * @type {String}
	 */
	separator: "",
	
	/**
	 * The flag indicating if the filter control is dependent
	 * @type {Boolean}
	 */
	dependent: false,
	
	/**
	 * The flag indicating if the filter control has any dependent filters	
	 * @type {Boolean}
	 */	
	hasDependent: false,
	
	/**
	 * The name of the outermost parent filter
	 * It's not equal to an empty string for dependent filters only
	 * @type {String}
	 */
	goodOutermostParentName: "",

	/**
	 * The name of the immediate parent filter
	 * It's not equal to an empty string for dependent filters only
	 * @type {String}
	 */	
	goodParentName: "",
	
	/**
	 * An array storing the parent filters names 
	 * for a dependent filter control
	 * @param {Array}
	 */
	parentFilterNames: null,	
	
	/**
	 * @type {string}
	 */
	hiddenFilterItemClass: "filter-item-hidden",
	
	/**
	 * @type {boolean}
	 */
	baseCtrlButtonsInitialized: false,

	horizontal: false,
	
	
	constructor: function( cfg ) {
		this.defaultValuesArray = [];
		
		Runner.apply( this, cfg );
		
		this.filterPanelContainer = $("." + this.filterPanelContainerClass);
		this.horizontal = this.filterPanelContainer.is( '[data-horizontal]' );
		this.filterContainer = $(".filter-" + ( this.dependent ? this.goodOutermostParentName : this.gfieldName ) + "-container");
		this.expanderElem = $("." + this.btnExpanderClass, this.filterContainer);
		
		this.init();
	},

	/**
	 *
	 */
	destructor: function() {
	},
	
	/**
	 * The stub preventing children objects that don't have 
	 * any 'init' method from undefined method invocations
	 */
	init: Runner.emptyFn,
	
	/**
	 * Add the Apply, 'Clear' and Expander buttons' click event handlers
	 * to the Filter panel's container.
	 */
	initButtons: function() {
		if ( this.baseCtrlButtonsInitialized ) {
			return;
		}
		
		this.initBasicButtons();
		this.initClearButtons();
		this.initShowMoreButton();
		
		this.baseCtrlButtonsInitialized = true;
	},

	/**
	 * Initialize the basic filter buttons
	 * A dependatn control doesn't have its own expander and apply button
	 */
	initBasicButtons: function() {	
		if ( this.dependent ) {
			return;
		}
		
		var ctrl = this,
			hiddenValuesClass = this.hiddenValuesClass,
			hiddenFilterItemClass = this.hiddenFilterItemClass,
			collapsedValuesClass = this.collapsedValuesClass,
			btnExpanderIndicatorClass = this.btnExpanderIndicatorClass;	

		// delegate clicks on a particular filter container	
		this.filterContainer
			.on('click', '.' + this.btnApplyClass, function(e) {
				$( this ).data( 'activated', true ); 
				ctrl.searchController.submitSearch();
				
				return false;
			})
			.on('click', '.' + this.btnExpanderClass, function(e) {
				var expander = $( this ),
					siblings = expander.parent().find('.' + ctrl.filterValueBlockContainerClass + ',.filter-show-more,.filter-field-btn').not('.' + hiddenValuesClass + ',.' + hiddenFilterItemClass),
					goingToHide = siblings.not('.' + collapsedValuesClass).length !== 0,
					expanderIndicator = expander.children('.' + btnExpanderIndicatorClass);
				
				//set the right 'expand/collapse' sign for the filer
				ctrl.adjustExpander( goingToHide );
				//expanderIndicator.html( goingToHide ? '&#x25BC;' : '&#x25B2;' );	
				
				//show/hide the filter's content. The last collapsed sibling is actually visible 1px height element due to collapsedValuesClasss styles	
				siblings.slideToggle( 100 ).promise().done( function() {
					siblings
						.css('display', '')
						.toggleClass( collapsedValuesClass, goingToHide );
				});
				
				return false;
			});	
	},
	
	/**
	 * Filters are hidden by the server part
	 * even not initially collapsed
	 */
	updateCollapsedState: function() {
		var inCollapsedState = this.isCollapsed();

		if ( !this.collapsed && inCollapsedState ) {
			this.toggleCollapsed( false );
		} else if ( this.collapsed && !inCollapsedState ) {
			this.toggleCollapsed( true );
		}
	},
	
	/**
	 * Toggle the collapsedValuesClass class
	 * for a filter's items
	 * @param {boolean} collapsed
	 */
	toggleCollapsed: function( collapsed ) {
		var $filterItems = this.filterContainer.find('.' + this.filterValueBlockContainerClass + ',.filter-show-more,.filter-field-btn');
		
		$filterItems.toggleClass( this.collapsedValuesClass, collapsed );
	},
	
	/**
	 * Init the 'Clear' buttons
	 */
	initClearButtons: function() {
		if ( !this.filtered ) {
			return;
		}
			
		var ctrl = this,
			clearBtnClass = 'filter-clear-' + ( this.dependent ? this.goodOutermostParentName : this.gfieldName );			
			
		$('.' + clearBtnClass).on('click', function(e) {
			ctrl.clearControl();
			ctrl.clearDependentFilters();		
			
			ctrl.searchController.submitSearch();
			return false;
		});
	},
	
	/**
	 * Initialize the 'Show N more' button's functionality
	 */
	initShowMoreButton: function() {
		var ctrl = this;
		
		$(".filterShowMore_" + this.gfieldName).on("click", function(e) {
			var $showMoreElement = $(this).parent();
	
			$showMoreElement.siblings("." + ctrl.hiddenFilterItemClass)
				.removeClass( ctrl.collapsedValuesClass )
				.removeClass( ctrl.hiddenFilterItemClass );
				
			$showMoreElement
				.removeClass("filter-show-more")
				.hide();
			return false;
		});		
	},
	
	/**
	 * Clear dependent filters
	 * @param {Boolean} notMultiSelected
	 */
	clearDependentFilters: function( notMultiSelected ) {
		if ( !this.hasDependent ) {
			return;
		}
		this.searchController.clearDependentFilters( this.dependentFilterNames, notMultiSelected );
	},
	
	/**
	 * Set the control's value
	 * @param {String | Number} value
	 */
	setValue: function( value ) {
		this.ctrlValue = value;
	},
	
	/**
	 * Get the control's value
	 * @return {String}
	 */	
	getValue: function() {
		return this.ctrlValue;
	},
	
	/**
	 * Get displayed values string
	 * @return {String}
	 */
	getDisplayedValue: function() {
		if ( this.defaultShowValues && this.defaultShowValues.length ) {
			return this.defaultShowValues.join(", ");
		}
		
		return this.getValue();
	},
	
	/**
	 * Put backslashes in front of every search-syntax character (~,(,))
	 * @param {String | Number} value
	 * @param {Boolean} encoded
	 */ 
	getPreparedValue: function( value, encoded ) {
		var prepValue = value; 
		
		if ( typeof value === "string" ) {
			prepValue = this.searchController.searchEscape( value );
		}
		if ( encoded && typeof value === "string" ) {
			return encodeURIComponent( prepValue );
		}
		
		return prepValue;
	},
	
	/**
	 * Get filter's value ready to use in the field's filtration
	 * If the encoded param is defined and equal to true 
	 * It returns the encoded value
	 * @param {Boolean} encoded
	 * @return {String}
	 */
	getFilterValue: function( encoded ) {
		var value, prepFName;
		
		if ( this.ctrlValue === '' ) {
			return '';
		}
		
		value = this.getPreparedValue( this.ctrlValue, encoded );
		
		prepFName = this.searchController.searchEscape( this.fieldName );
		prepFName = encoded ? encodeURIComponent( prepFName ) : prepFName;
				
		return '(' + prepFName + this.separator + value + ')';
	},

	/**
	 * Get filter's encoded value ready to use in the field's filtration
	 * @return {String}
	 */
	getFilterValueEncoded: function() {
		return this.getFilterValue( true ); 	
	},
	
	/**
	 * Check if the filter control is visible	
	 * @return {Boolean}
	 */	
	isVisible: function() {
		return this.filterContainer.is(":visible");
	},
	
	/**
	 * Check if the filter control is collapsed or not	
	 * @return {Boolean}
	 */
	isCollapsed: function() {
		if( this.horizontal ) {
			return false;
		}
		var $filterItems = this.expanderElem.siblings().children("li");
		
		return !$filterItems.not( "." + this.collapsedValuesClass + ",." + this.hiddenValuesClass + ",." + this.hiddenFilterItemClass ).length;
	},
	
	/**
	 * Adjust the flter's expander sign basing on the its state (expanded/collapsed).
	 * @pamam {Boolean} collapsed	
	 * @return {Boolean}
	 */
	adjustExpander: function( collapsed ) {
		var $expanderIcon = $("." + this.btnExpanderIndicatorClass, this.expanderElem);
		
		$expanderIcon.toggleClass( "glyphicon-triangle-bottom", collapsed );
		$expanderIcon.toggleClass( "glyphicon-triangle-top", !collapsed );
	},
	
	/**
	 * Clear the filter control's value
	 * the 'Clear' buttons 'click' handler
	 */
	clearControl: function() {
		this.setValue('');
	},

	/**
	 * Set a single value to the filter control
	 * @param {Mixed}
	 */
	updateValue: function( value ) {
		this.setValue( value );	
	},
	
	/**
	 * Check if the filter control is dependent
	 * @return {Boolean}
	 */
	isDependent: function() {
		return this.dependent;
	},
	
	/**
	 * Check if the filter control is multiselected
	 * @return {Boolean}
	 */
	isMultiSelected: function() {
		return false;
	},

	/**
	 * Prevent dropdown close on click inside 
	 */
	initDropdown: function() {
		var selector = '.' + this.filterPanelContainerClass + ' .dropdown-menu';
		$( document ).on( 'click', selector, function (e) {
			e.stopPropagation();
		});
	}
});


Runner.controls.SimpleFilterControl = Runner.extend( Runner.controls.FilterControl, {
	/**
	 * The link to the Runner.controls.OnDemandFilterControl's base class
	 * It makes easier to borrow its methods
	 * @type {Object}
	 */
	superclass: {},
		
	/**
	 * The filter values links class name
	 * @type {String}
	 */		
	valueLinkClass: "",
	

	constructor: function( cfg ) {
		Runner.controls.SimpleFilterControl.superclass.constructor.call( this, cfg );	
	},
	
	/**
	 * Set the control's default value and init contol's buttons and links
	 */
	init: function() {	
		this.superclass = Runner.controls.SimpleFilterControl.superclass ;
		this.valueLinkClass = this.gfieldName + "-filter-value";
		
		this.setValue( this.defaultValuesArray.join(', ') );
		this.initButtons();
		this.initDropdown();
	},
	
	/**
	 * Init the control's buttons and links
	 */
	initButtons: function() {
		var ctrl = this,
			delBtnClass = "delFilterCtrlButt_" + ( this.dependent ? this.goodOutermostParentName : this.gfieldName ) + "_" + this.searchController.id;
		
		this.filterPanelContainer.off("click.filters" + this.gfieldName);
		
		this.filterPanelContainer
			.on("click.filters" + this.gfieldName, "." + delBtnClass, function(e) {
				var $delButton = $(this);
					valueToDelete = $(this).data('delete').toString(); 
				
				ctrl.clearFilterValue( valueToDelete, $delButton.parent(".filter-values") );
				ctrl.searchController.rememberFilterPanelState();
				ctrl.searchController.submitSearch();
				return false;	
			});

		if ( !this.hasDependent ) {
			this.filterPanelContainer.on("click.filters" + this.gfieldName, "." + this.filterValueBlockContainerClass + "-" + this.gfieldName, function(e) {
				var $valueLink = $("." + ctrl.valueLinkClass , this);
				if ( !$valueLink.length ) {
					return;
				}
				
				ctrl.ctrlValue = $valueLink.attr("data-filtervalue");
				
				ctrl.updateParentFilters( $valueLink.data("parentFiltersValues") );
				ctrl.clearDependentFilters();	
				ctrl.searchController.rememberFilterPanelState();						
				ctrl.searchController.submitSearch();

				return false;
			})
			.on("click", "." + this.valueLinkClass, function(e) {		
				e.preventDefault();
			});
		} else {
			$("." + ctrl.filterValueBlockContainerClass + "-" + ctrl.gfieldName, ctrl.filterContainer).removeClass( ctrl.filterLinkClass );
			this.filterPanelContainer.on("click", "." + this.valueLinkClass, function(e) {		
				var $valueLink = $(this);
				
				ctrl.ctrlValue = $valueLink.data("filtervalue");
				ctrl.updateParentFilters( $valueLink.data("parentFiltersValues") );
				ctrl.clearDependentFilters();	
				ctrl.searchController.rememberFilterPanelState();						
				ctrl.searchController.submitSearch();
				
				return false;
			});
		}
				
		//call the base class method
		this.superclass.initButtons.call( this );	
	},
	
	/**
	 * Update parent filters' values
	 * @param {Object} parentFiltersValuesData
	 */
	updateParentFilters: function( parentFiltersValuesData ) {
		if ( !this.dependent ) {
			return;
		}
		this.searchController.updateParentFilters( parentFiltersValuesData );
	},

	/**
	 * Delete a particular value from the filter's submit-values
	 * @param {String} valueToDelete
	 * @param {jQuery object} $container
	 */
	clearFilterValue: function( valueToDelete, $container ) {
		this.setValue('');
	},
}); 


Runner.controls.MultiSelectFilterControl = Runner.extend( Runner.controls.FilterControl, {
	/**
	 * It stores the array of the filter checkboxes' jQuery objects
	 * @type {Array}
	 */
	checkboxes: [],
	
	/**
	 * The name of the filter checkboxes' class
	 * @type {String}
	 */
	checkboxClass: '',
	
	/**
	 * The link to the Runner.controls.OnDemandFilterControl's base class
	 * It makes easier to borrow its methods
	 * @type {Object}
	 */
	superclass: {},
	
	/**
	 * The common multiselect checkbox class name
	 * @type {String}	
	 */
	commonCheckboxClass: "multifilter-checkbox",
	
	/**
	 * The parent filter checkbox class name
	 * @param {String}
	 */
	parentCheckboxClass: "",
	
	
	constructor: function( cfg ) {
		this.checkboxes = [];
		
		Runner.controls.MultiSelectFilterControl.superclass.constructor.call( this, cfg );
	},
	
	/**
	 * Init the checkboxes array with the array of the filter checkboxes' jQuery objects.
	 * Set the control's default value and init contol's buttons and links. 
	 */
	init: function() {
		this.superclass = Runner.controls.MultiSelectFilterControl.superclass;

		this.applyButton = $( ".filter-apply-" + ( this.dependent ? this.goodOutermostParentName : this.gfieldName) ); 
		
		this.checkboxClass = "filter_" + this.gfieldName + "_" + this.searchController.id;
		this.checkboxes = $("." + this.checkboxClass);	
		this.selectAllCheckbox = $( ".filter-selectAll-" + ( this.dependent ? this.goodOutermostParentName : this.gfieldName ) );
		this.parentCheckboxClass = this.dependent ? "filter_" + this.goodParentName + "_" + this.searchController.id : "";
				
		this.adjustSelectAllCheckbox();
		
		if ( this.filtered && !this.dependent ) {
			this.setValue( this.defaultValuesArray );
		}
		
		this.initButtons();
		this.initDropdown();
	},
		
	/**
	 * Adjust the flter's 'Select/Deselect All' checkbox basing on the filter's checkboxes state.
	 */	
	adjustSelectAllCheckbox: function() {
		var currentState = this.selectAllCheckbox.is(":checked"),
			allChecked = this.checkboxes.length === this.checkboxes.filter(":checked").length
		
		this.selectAllCheckbox.prop("checked", allChecked);
	},	
		
	/**
	 * Init the control's buttons
	 */	
	initButtons: function() {
		var ctrl = this;
				
		this.selectAllCheckbox
			.on("click", function(e) {
				e.stopPropagation();
			})
			.on("change", function(e) {
				ctrl.checkboxes.prop("checked", this.checked);
				ctrl.clearDependentFilters( true );
				return false;
			});
		
		this.checkboxes
			.on("click", function(e) {
				ctrl.adjustSelectAllCheckbox();
				ctrl.adjustDependentFilters( this );
				ctrl.adjustInvisibleCheckboxes( this );	
				e.stopPropagation();
			})
			.on("change", function(e) {
				ctrl.adjustParentFilterCheckbox( this );
			});

		if ( !this.hasDependent ) {
			$("." + this.filterValueBlockContainerClass + "-" + this.gfieldName ).on("click", function(e) {
				var $checkbox = $("." + ctrl.checkboxClass, this),
					checked = $checkbox.prop("checked"),
					checkbox;
				
				if ( !$checkbox.length || $checkbox.is( e.target ) ) {
					return;
				}
				
				$checkbox.prop("checked", !checked);
				
				checkbox = $checkbox.get(0);
				ctrl.adjustSelectAllCheckbox();
				ctrl.adjustDependentFilters( checkbox );
				ctrl.adjustInvisibleCheckboxes( checkbox );
				
				$checkbox.trigger("change");
			});
		}
		
		//call the base class method
		this.superclass.initButtons.call( this );	
	},
	
	/**
	 * Clear check boxes for multiselect dependent controls
	 * and clear values for not multiselected ones
	 * @param {DOM element} checkbox
	 */	
	adjustDependentFilters: function( checkbox ) {
		this.clearDependentFilters( true );
		this.adjustDependentFilterCheckboxes( checkbox );		
	},
	
	/**
	 * Update dependent filters checkboxes of the corresponding value blocks
	 * basing on the main filter's current checkbox state
	 * The method depends on the Filter panel value blocks markup
	 * @param {DOM element} checkbox
	 */
	adjustDependentFilterCheckboxes: function( checkbox ) {
		if ( !this.hasDependent ) {
			return;
		}
		
		var $checkbox = $( checkbox ),
			$filterValueBlock = $checkbox.parents( '.' + this.filterValueBlockContainerClass );
		
		$('.' + this.commonCheckboxClass, $filterValueBlock).prop('checked', $checkbox.is(':checked'));
	},
	
	/**
	 * Update the corresoinding parent filter's checkbox state
	 * basing on the value blocks's checkboxes state	
	 * The method depends on the Filter panel value blocks markup	 
	 * @param {DOM element} checkbox
	 */
	adjustParentFilterCheckbox: function( checkbox ) {
		if ( !this.dependent ) {
			return;
		}
		
		var $checkbox = $( checkbox ),
			$parentFilterValueBlock = $checkbox.parents( '.' + this.filterValueBlockContainerClass ).eq(1),
			checked = $checkbox.is(':checked') || $('.' + this.checkboxClass + ':checked', $parentFilterValueBlock).length > 0;

		
		$parentFilterValueBlock.find( '.' + this.parentCheckboxClass ).prop('checked', checked)
			.trigger('change');
	},	
	
	/**
	 * Update the corresponding checkboxes from invisible parent filters value blocks
	 * @param {DOM element} checkbox
	 */
	adjustInvisibleCheckboxes: function( checkbox ) {
		if ( !this.dependent ) {
			return
		}
		
		var $checkbox = $( checkbox ),
			checkboxValue = $checkbox.val(),
			parentValues = $checkbox.data('parentFiltersValues'),
			$similarHiddenBoxes = this.checkboxes.not(':visible').filter( function() {
				var $box = $(this),
					hiddenParentValues, pName;
					
				if ( $box.val() !== checkboxValue ) {
					return false;
				}				
				
				hiddenParentValues = $box.data('parentFiltersValues');
					
				for ( pName in hiddenParentValues ) {
					if ( hiddenParentValues[ pName ] !== parentValues[ pName ] ) {
						return false;
					}
				}
				
				return true; 
			});
			
		$similarHiddenBoxes.prop('checked', $checkbox.is(':checked'))
			.trigger('change');	
	},
	
	/**
	 * Checked the filter's checkboxes having the values contained in the array param
	 * @param {Array} arr
	 */
	setValue: function( arr ) {
		this.checkboxes.each( function(index, domCheckbox) {
			var checkbox = $( domCheckbox ),
				pos = $.inArray( checkbox.val(), arr );
			
			if ( pos !== -1 ) {
				checkbox.prop('checked', true);
				arr.splice(pos, 1); 
			} 
		});
		
	},
	
	/**
	 * Get the contorl's value as an array.
	 * The returned array contains all the cehcked control's cehckboxes' values 
	 * and some default values that are not represented among the contol's checkboxes
	 * (to the "filter" operation reversible)
	 *
	 * @return {Array} 
	 */
	getValue: function() {
		var arr = [], i,
			ctrl = this,
			$checked = $('.' + this.checkboxClass + ':checked'),
			onDemand = this instanceof Runner.controls.OnDemandFilterControl;
		
		if ( this.dependent ) {
			$checked = $checked.filter(':visible');
		}
		
		if ( this.filtered || this.applyButton.data('activated') || $checked.length !== this.checkboxes.length ) {	
			$checked.each( function( index, domCheckbox ) {						
				var $box = $( domCheckbox ),
					checkData = { 
						value: $box.val(), 
					}; 
				
				if ( ctrl.dependent ) {
					var parentFiltersValueData = $box.data('parentFiltersValues'),	
						parentValues = [];
					
					ctrl.parentFilterNames.forEach( function( pfName ) {
						parentValues.push( parentFiltersValueData[ pfName ].toString().replace('|', '\\|') );
					});
					
					checkData.parentValues = parentValues.join('|');
				}
				
				arr.push( checkData );				
			});			
		}
		
		return arr;
	},
	
	/**
	 * Get filter's value ready to use in the field's filtration
	 * If the encoded param is defined and equal to true 
	 * It returns the encoded value
	 * @param {Boolean} encoded
	 *
	 * @return {String}
	 */
	getFilterValue: function( encoded ) {
		var arr = this.getValue(),
			result = ''; 
		
		for (i = 0; i < arr.length; i++) {			
			var value = arr[i].value,
				parentValues = arr[i].parentValues,
				prepFName;
			
			if ( value === '' ) {
				continue;
			}
			
			value = this.getPreparedValue( value, encoded );		
			prepFName = this.searchController.searchEscape( this.fieldName );
			prepFName = encoded ? encodeURIComponent( prepFName ) : prepFName;
						
			result += '(' + prepFName + this.separator + value 
				+ ( parentValues ? '~~' + parentValues : '' ) + ')';
		}
		
		return result;
	},	
	
	/**
	 * Clear the filter control's value
	 */
	clearControl: function() {			
		this.checkboxes.prop('checked', false);
		this.selectAllCheckbox.prop('checked', false);		
	},
	
	/**
	 * Set a single value to the filter control
	 * @param {Mixed}
	 */
	updateValue: function( value ) {
		this.checkboxes.prop('checked', false);
		this.setValue( [value] );	
	},

	/**
	 * Check if the filter control is multiselected
	 * @return {Boolean}
	 */
	isMultiSelected: function() {
		return true;
	}	
});

/**
 * The class combaining the SimpleFilterControl and MultiSelectFilterControl functionalities
 */
Runner.controls.OnDemandFilterControl = Runner.extend( Runner.controls.FilterControl, {
	/**
	 * The indicator showing if control in multiselection mode ot not
	 * (this mode is turning on by clicking on the "Multiselect" button)
	 * @type {Boolean}
	 */
	multiSelection: false,
	
	/**
	 * It stores the array of the filter checkboxes' jQuery objects
	 * @type {Array}
	 */
	checkboxes: [],
	
	/**
	 * The name of the filter checkboxes' class
	 * @type {String}
	 */
	checkboxClass: '',
		
	/**
	 * The name of the Filter "Multiselect" button's class
	 * @type {String}
	 */
	btnMultiselectClass: "",
	
	/**
	 * The caption of the filter's "Apply" button
	 * @type {String}
	 */
	btnApplyCaption: Runner.lang.constants.FILTER_APPLY,
		
	/**
	 * The link to the Runner.controls.OnDemandFilterControl's base class
	 * It makes easier to borrow its methods
	 * @type {Object}
	 */
	superclass: {},

	/**
	 * The link to the Runner.controls.MultiSelectFilterControl
	 * It makes easier to borrow its methods
	 * @type {Object}
	 */
	multiSelectFCtrl: {},
	
	/**
	 * The link to the Runner.controls.SimpleFilterControl
	 * It makes easier to borrow its methods
	 * @type {Object}
	 */
	simpleFCtrl: {},

	/**
	 * The class name of the filtered not multiselected filter's values
	 * @type {String}
	 */
	filteredValuesClass: "filter-ready-value",	
	
	/**
	 * The filter values links class name
	 * @type {String}
	 */	
	valueLinkClass: "",

	/**
	 * The common multiselect checkbox class name
	 * @type {String}	
	 */
	commonCheckboxClass: "multifilter-checkbox",
	
	/**
	 * The parent filter checkbox class name
	 * @param {String}
	 */
	parentCheckboxClass: "",	
	
	
	constructor: function( cfg ) {
		this.checkboxes = [];
		
		Runner.controls.OnDemandFilterControl.superclass.constructor.call( this, cfg );
	},
	
	/**
	 * Init the checkboxes array with the array of the filter checkboxes' jQuery objects.
	 * Set the control's default value and init contol's buttons and links. 
	 */
	init: function() {	
		this.superclass = Runner.controls.OnDemandFilterControl.superclass;
		this.multiSelectFCtrl = Runner.controls.MultiSelectFilterControl.prototype;
		this.simpleFCtrl = Runner.controls.SimpleFilterControl.prototype; 

		this.btnMultiselectClass = 'filter-' + ( this.dependent ? this.goodOutermostParentName : this.gfieldName ) + '-btn-multiselect';		
		this.applyButton = $("." + this.btnMultiselectClass);
		
		this.checkboxClass = 'filter_' + this.gfieldName + "_" + this.searchController.id;
		this.checkboxes = $('.' + this.checkboxClass);		
		this.parentCheckboxClass = this.dependent ? 'filter_' + this.goodParentName + "_" + this.searchController.id : '';
		this.selectAllCheckbox = $('.filter-selectAll-' + ( this.dependent ? this.goodOutermostParentName : this.gfieldName ));	
		
		this.valueLinkClass = this.gfieldName + '-filter-value';
		
		this.setValue( this.defaultValuesArray.join(', ') );
		this.initButtons();
		this.initDropdown();		
	},
	
	/**
	 * Adjust the flter's 'Select/Deselect All' checkbox basing on the filter's checkboxes state.
	 */	
	adjustSelectAllCheckbox: Runner.controls.MultiSelectFilterControl.prototype.adjustSelectAllCheckbox,
	
	/**
	 * Clear check boxes for multiselect dependent controls
	 * and clear values for not multiselected ones
	 * @param {DOM element} checkbox
	 */	
	adjustDependentFilters: Runner.controls.MultiSelectFilterControl.prototype.adjustDependentFilters,
	
	/**
	 * Update dependent filters checkboxes of the corresponding value blocks
	 * basing on the main filter's current checkbox state
	 * The method depends on the Filter panel blocks markup
	 * @param {DOM element} checkbox
	 */
	adjustDependentFilterCheckboxes: Runner.controls.MultiSelectFilterControl.prototype.adjustDependentFilterCheckboxes,

	/**
	 * Update the corresoinding parent filter's checkbox state
	 * basing on the value blocks's checkboxes state	
	 * The method depends on the Filter panel value blocks markup	 
	 * @param {DOM element} checkbox
	 */
	adjustParentFilterCheckbox: Runner.controls.MultiSelectFilterControl.prototype.adjustParentFilterCheckbox,
	
	/**
	 * A stub for the Runner.controls.OnDemandFilterControl class
	 */
	adjustInvisibleCheckboxes: Runner.emptyFn,
	
	/**
	 * Init control's buttons and links
	 */
	initButtons: function() {
		var ctrl = this,
			anotherApplyButtonExists = !!$(".filter-apply-" + this.gfieldName).not( this.applyButton ).length;
		
		if ( this.dependent ) {
			anotherApplyButtonExists = !!$(".filter-apply-" + this.goodOutermostParentName).not( this.applyButton ).length;
		}
		
		this.applyButton.one("click", function(e) {
			var $button = $(this);

			if ( anotherApplyButtonExists ) { 
				$button.hide();
			}		
			
			$button
				.text( ctrl.btnApplyCaption )
				.addClass( ctrl.btnApplyClass )
				.removeClass( ctrl.btnMultiselectClass );			
						
			$("." + ctrl.valueLinkClass)
				.removeClass( ctrl.valueLinkClass )
				.addClass( "multiSelect-" + ctrl.valueLinkClass )
				// remove link hrefs
				.removeAttr('href');
			
			$("." + ctrl.filterValueBlockContainerClass + "-" + ctrl.gfieldName, ctrl.filterContainer)
				.removeClass( ctrl.filterLinkClass );
			
			ctrl.checkboxes.show();
			ctrl.selectAllCheckbox.show();
			
			if ( !ctrl.dependent || ctrl.filtered ) {				
				$("." + ctrl.hiddenValuesClass, ctrl.filterContainer)
					/*.css("display", "block") // bootstrap filter fix
					.show()*/
					.removeClass( ctrl.hiddenValuesClass );
				
				$("." + ctrl.filteredValuesClass + ".ondemand", ctrl.filterContainer)
					.hide()
					.addClass( ctrl.hiddenValuesClass );
			}
			
			$(".show-more-hidden", ctrl.filterContainer) //temp fix
				.removeClass("show-more-hidden");
			
			if ( ctrl.dependent && !ctrl.searchController.hasFilteredParentFilter( ctrl.parentFilterNames) ) {
				// hide the 'delete filter' button after switching ti the multiselect mode
				$(".delFilterCtrlButt_" + ( ctrl.dependent ? ctrl.goodOutermostParentName : ctrl.gfieldName ) + "_" + ctrl.searchController.id).hide();
			}
			
			ctrl.multiSelection = true;
			return false;
		});

		//call the SimpleFilterControl's method
		this.simpleFCtrl.initButtons.call( this );
		//call the MultiSelectFilterControl's method
		this.multiSelectFCtrl.initButtons.call( this );		
	},

	/**
	 * Update parent filters' values
	 * @param {Object} parentFiltersValuesData
	 */
	updateParentFilters: Runner.controls.SimpleFilterControl.prototype.updateParentFilters,
	
	/**
	 * Get filter's value. 
	 * If the multiselection is turned on for the "on Demand" control or its field is filtered
	 * It uses the borrowed method "getFilterValue" from the MultiSelectFilterControl.
	 * Otherwise it borrows the superclass "getFilterValue" method
	 *
	 * @return {String}
	 */
	getValue: function() { 
		if ( this.multiSelection || this.filtered ) {
					//call the MultiSelectFilterControl's method
			return this.multiSelectFCtrl.getValue.call( this );
		}
				//call the base class method
		return this.superclass.getValue.call( this );
	},
	
	/**
	 * Get filter's value ready to use in the field's filtration
	 * If the multiselection is turned on for the "on Demand" control or its field is filtered
	 * It uses the borrowed method "getFilterValue" from the MultiSelectFilterControl.
	 * Otherwise it borrows the superclass "getFilterValue" method
	 *
	 * @return {String}
	 */
	getFilterValue: function( encoded ) {
		if ( this.multiSelection || this.filtered ) {
					//call the MultiSelectFilterControl's method
			return this.multiSelectFCtrl.getFilterValue.call( this, encoded );
		}
				//call the base class method
		return this.superclass.getFilterValue.call( this, encoded );
	},

	/**
	 * Clear the filter control
	 */
	clearControl: function() {
		if ( this.multiSelection || this.filtered ) {
			this.checkboxes.prop('checked', false);
			this.selectAllCheckbox.prop('checked', false);
		} 
		
		this.setValue('');
	},
	
	/**
	 * Delete a particular value from the filter's submit-values
	 * by unchecking the corresponding checkbox
	 * @param {Strging} valueToDelete
	 * @param {jQuery object} $container	 
	 */
	clearFilterValue: function( valueToDelete, $container ) {
		if ( !this.dependent ) {
			this.checkboxes
				.filter( function() {	
					return this.value === valueToDelete;
				})
				.prop('checked', false);
			
			return;
		}
		
		this.checkboxes
			.filter( function() {
				return !!$container.find( this ).length;
			})
			.prop('checked', false);
		
		if ( this.hasDependent ) {
			$('.' + this.commonCheckboxClass, $container).prop('checked', false); 			
		}			
	},
	
	/**
	 * Set a single value to the filter control
	 * @param {Mixed}
	 */
	updateValue: function( value ) {
		if ( this.multiSelection ) {
			this.checkboxes.prop('checked', false);
			this.setValue( [value] );
		} 
		
		this.setValue( value );
	},
	
	/**
	 * Check if the filter control is multiselected	
	 * @return {Boolean}
	 */
	isMultiSelected: function() {
		return this.multiSelection;
	}
});


/**
* The Slider Filter control class
*/
Runner.controls.SliderFilterControl = Runner.extend( Runner.controls.FilterControl, {
	/**
	 * The jQueryUI slider object 
	 * @type {Object}
	 */
	sliderElem: {},
	
	/**
	 * The jQueryUI slider's range param
	 * It could be assigned with a corresponding key from the config
	 * @type {Number}
	 */
	range: true,

	/**
	 * The jQueryUI slider's min value
	 * It could be assigned with a corresponding key from the config
	 * @type {String|Number}
	 */	
	minValue: 0,

	/**
	 * The jQueryUI slider's max value
	 * It could be assigned with a corresponding key from the config
	 * @type {String|Number}
	 */	
	maxValue: 0, 

	/**
	 * The jQueryUI slider's step value
	 * It could be assigned with a corresponding key from the config
	 * @type {Number}
	 */	
	step: 1,

	/**
	 * A param setting th slider type 
	 * ("min", "max", "both")
	 * @type {Number}
	 */
	knobsType: 0,
	
	/**
	 * The rounded max slider's value
	 * @type {Number}
	 */
	roundedMax: 0,
	
	/**
	 * The rounded min slider's value	
	 * @type {Number}
	 */	
	roundedMin: 0,
	
	/**
	 * The rounded max slider knob's value	
	 * @type {Number}
	 */		
	roundedMaxKnobValue: 0,
	
	/**
	 * The rounded min slider knob's value		
	 * @type {Number}
	 */		
	roundedMinKnobValue: 0,	
	
	/**
	 * An indicator showing if the 'Apply' 
	 * button is added to the filter 
	 * @type {Boolean}
	 */
	useApllyBtn: false,


	formatSettings: null,
	
	
	constructor: function( cfg ) {
		Runner.controls.SliderFilterControl.superclass.constructor.call( this, cfg );
	},

	/**
	 * Set default filter's values
	 */
	setDefaultValues: function() {
		this.ctrlValues = [ this.minKnobValue, this.maxKnobValue ];
	},

	/**
	 * Get the string representation of the control's value
	 * @return {String}
	 */
	getValue: function() {
		return this.getCaptionValue( this.ctrlValues[0] ) + " - " + this.getCaptionValue( this.ctrlValues[1] );
	},
	
	/**
	 * Init the slider control
	 */
	init: function() {
		var jQsliderValues, sliderParams;
		
		jQsliderValues= $("#slider_values_" + this.gfieldName);
		this.jQmax = $(".slider-max", jQsliderValues); 
		this.jQmin = $(".slider-min", jQsliderValues);	
	
		this.setDefaultValues();	
		this.range = this.getRange();
		
		sliderParams = this.getSliderParams();	
		this.sliderElem = $("#slider_" + this.gfieldName)
			.slider( sliderParams );
			
		this.setPageMaxZIndex();	
		
		this.initButtons();			
	},
	
	/**
	 * A stub
	 */
	initShowMoreButton: Runner.emptyFn,
	
	/**
	 * Set the Runner 'z-index' counter not lower that the slider's handle 'z-index' value
	 */ 
	setPageMaxZIndex: function() {
		var handleZIndex = parseInt( $('.ui-slider-handle', this.sliderElem).css('z-index'), 10 );
		Runner.setZindexMax( handleZIndex );	
	},
	
	/**
	 * Get the jQuery slider range param
	 */
	getRange: function() {
		if ( this.knobsType === Runner.controls.constants.FILTER_KNOBS_MIN_ONLY ) {
			return 'max';
		}		
		if ( this.knobsType === Runner.controls.constants.FILTER_KNOBS_MAX_ONLY ) {
			return 'min';
		}	
		return true;
	},
	
	/**
	 * Get jQuery slider params
	 * @return {object}
	 */
	getSliderParams: function() {
		var filter = this,
			sliderParams = {
				range: this.range,
				min: this.roundedMin,
				max: this.roundedMax,
				step: parseFloat( this.step )
			},
			typeBasedParams;
			
		typeBasedParams = this.getSliderParamsByType( this.range );
		Runner.apply( sliderParams, typeBasedParams );
		
		if ( !this.useApllyBtn ) {	
			sliderParams.stop = function() {
				filter.searchController.submitSearch();
			}			
		}	
		return sliderParams;
	},
	
	/**
	 * Get jQuery slider params basing on the slider's range type
	 * @param {string | boolean} range
	 * @return {object}
	 */
	getSliderParamsByType: function( range ) {
		var filter = this;
		
		switch ( this.range ) {
			case 'min':
			return {
				value: parseFloat( this.roundedMaxKnobValue ),
				slide: function( event, ui ) {
					filter.processMaxValue( ui.value, $(this).slider("option", "min") === ui.value );	
				}
			};

			case 'max':
			return {
				value: parseFloat( this.roundedMinKnobValue ),
				slide: function( event, ui ) {
					filter.processMinValue( ui.value, $(this).slider("option", "max") === ui.value );
				}
			};

			default:
			return {
				values: [ parseFloat( this.roundedMinKnobValue ), parseFloat( this.roundedMaxKnobValue ) ],	
				slide: function( event, ui ) {
					var oldValues = $( this ).slider( "values" ),
						min = ui.values[ 0 ],
						max = ui.values[ 1 ],
						oldMin = oldValues[ 0 ],
						oldMax = oldValues[ 1 ];
								
					if ( min !== oldMin ) {
						filter.processMinValue( min, min === max );
						return;			
					}
					if ( max !== oldMax ) {			
						filter.processMaxValue( max, min === max );
						return;							
					}
				}
			};
		}		
	},

	/**
	 * Set the conttol's min value and caption
	 * @param {number} min
	 * @param {boolean} minIsEqualToMax	 
	 */	
	processMinValue: function( min, minIsEqualToMax ) {
		var value = this.getSliderValue( min, true, minIsEqualToMax );
		
		this.setMinValue( value );
		this.setCaptions( value, true );
	},

	/**
	 * Set the conttol's max value and caption
	 * @param {number} max
	 * @param {boolean} minIsEqualToMax
	 */	
	processMaxValue: function( max, minIsEqualToMax ) {
		var value = this.getSliderValue( max, false, minIsEqualToMax );
					
		this.setMaxValue( value );
		this.setCaptions( value, false );	
	},
	
	/**
	 * Set the control's captions	
	 * @param {number} value	
	 * @param {boolean} isLower
	 */		
	setCaptions: function( value, isLower ) {
		var jQmainCaption = isLower ? this.jQmin : this.jQmax,
			caption = this.getCaptionValue( value );	
		
		jQmainCaption.html( caption );	
	},
	
	/**
	 * Replace the obtained slider's extreme value with
	 * the real corresponding database value		
	 * @param {number} value
	 * @param {boolean} isLower
	 * @param {boolean} minIsEqualToMax
	 * @return {number}
	 */
	getSliderValue: function( value, isLower, minIsEqualToMax ) {
		if ( value === this.roundedMin ) {
			return parseFloat( this.minValue );
		}
		
		if ( value === this.roundedMax ) { 
			return parseFloat( this.maxValue )
		}
		
		return value;
	},

	/**	
	 * Get the correct slider's caption	
	 * @param {number} value
	 * @return {number}
	 */
	getCaptionValue: function( value ) {
		if ( this.viewAsCurrency ) {
			return Runner.util.getCurrencyFormatted( value, this.formatSettings );
		}
		
		if ( this.viewAsNumber ) {
			return Runner.util.getNumberFormatted( value, this.formatSettings );
		}
		
		return value;
	},	
	
	/**
	 * Set the min slider control's value
	 * @param {number} minValue
	 */
	setMinValue: function( minValue ) {
		this.ctrlValues[0] = minValue;
	},
	
	/**
	 * Set the max slider control's value
	 * @param {number} maxValue
	 */
	setMaxValue: function( maxValue ) {
		this.ctrlValues[1] = maxValue;
	},
		
	/**
	 * Get filter's value ready to use in the field's filtration
	 * If the encoded param is defined and equal to true 
	 * It returns the encoded value
	 * @param {boolean} encoded 
	 * @return string
	 */
	getFilterValue: function( encoded ) {
		var value, prepFName;
		
		if ( this.ctrlValues[0] === undefined && this.ctrlValues[1] === undefined ) {
			// the slider isn't activated
			return '';
		}
		
		if ( !this.filtered ) {
			this.prepareControlValues();
		}
		
		if ( this.knobsAreOnTheLimits() ) {
			// the knobs are set on the min and max slider's values
			return '';
		}
		
		value = this.getTypeBasedValue();
		prepFName = this.searchController.searchEscape( this.fieldName );
		prepFName = encoded ? encodeURIComponent( prepFName ) : prepFName;
		
		return "(" + prepFName + this.separator + value + ")";
	},
	
	/**
	 * Get the control's value/values to add them 
	 * to the submit-string 
	 * @param {boolean} encoded
	 * @return {string}
	 */
	getTypeBasedValue: function( encoded ) {			
		switch ( this.range ) {
			case 'min':
			return this.getPreparedMaxValue( encoded );

			case 'max':
			return this.getPreparedMinValue( encoded );
				
			default:				
				var valueMin = this.getPreparedMinValue( encoded ),
					valueMax = this.getPreparedMaxValue( encoded );
					
			return valueMin + "~" + valueMax;
		}
	},

	/**
	 * Put backslashes in front of every search-syntax character (~,(,))
	 * in the slider's min value string
	 * @param {boolean} encoded
	 * @return {string}
	 */		
	getPreparedMinValue: function( encoded ) {
		return this.getPreparedValue( this.ctrlValues[0], encoded );
	},

	/**
	 * Put backslashes in front of every search-syntax character (~,(,))
	 * in the slider's max value string
	 * @param {boolean} encoded
	 * @return {string}
	 */		
	getPreparedMaxValue: function( encoded ) {
		return this.getPreparedValue( this.ctrlValues[1], encoded );
	},	
	
	/**
	 * Replace undefined filter's values with corresponding
	 * slider's min and max values
	 */	
	prepareControlValues: function() {
		if ( this.ctrlValues[0] === undefined ) {
			this.ctrlValues[0] = this.minValue;
		}
		if ( this.ctrlValues[1] === undefined ) {
			this.ctrlValues[1] = this.maxValue;
		}
	},
	
	/**
	 * Check if the slider's min and max knobs are equal to the
	 * the min and max slider's values respectively
	 * @return {boolean}
	 */
	knobsAreOnTheLimits: function() {
		return this.ctrlValues[0] === this.minValue && this.ctrlValues[1] === this.maxValue;
	},
	
	/**
	 * Clear the filter control's values
	 */
	clearControl: function() {
		this.ctrlValues = [];
	}
});

/**
 * The Date Slider Filter control class
 */
Runner.controls.DateSliderFilterControl = Runner.extend( Runner.controls.SliderFilterControl, {
	/**
	 * @type {number}
	 */
	stepType: Runner.controls.constants.FILTER_STEP_DAY,

	/**
	 * @type {boolean}
	 */
	isFieldDateType: true,
	
	/**
	 * @type {boolean}
	 */	
	processWrappers: true,
	
	constructor: function( cfg ) {
		Runner.controls.DateSliderFilterControl.superclass.constructor.call( this, cfg ); 
	},

	/**
	 * Set default filter's values
	 * Elements of the ctrlValues array should have 
	 * the object (Date) or undefined type
	 */
	setDefaultValues: function() {
		var minKnob = this.minKnobValue,
			maxKnob = this.maxKnobValue;

		if ( minKnob ) {
			minKnob = this.parseDateTime( minKnob, /*this.dateFormat*/ 2 );
		}
		if ( maxKnob ) {
			maxKnob = this.parseDateTime( maxKnob, /*this.dateFormat*/ 2 );
		}
		this.ctrlValues = [ minKnob, maxKnob ];
	},
	
	/**
	 * Init the date-slider control
	 */
	init: function() {
		var jQsliderValues= $("#slider_values_" + this.gfieldName);
		this.jQcaptPrefix = $(".slider-caption-prefix", jQsliderValues);
		this.jQcaptPostfix = $(".slider-caption-postfix", jQsliderValues);
	
		this.dateFormat = Runner.pages.PageSettings.getGlobalData("locale").dateFormat;	
		this.dateDelimiter = Runner.pages.PageSettings.getGlobalData("locale").dateDelimiter;

		this.minDate = this.parseDateTime( this.minValue, 2 );
		this.minDateInMs = this.minDate.getTime();
		this.maxDate = this.parseDateTime( this.maxValue, 2 );
		this.maxDateInMs = this.maxDate.getTime();
		
		if ( this.stepType === Runner.controls.constants.FILTER_STEP_SEC 
			|| this.stepType === Runner.controls.constants.FILTER_STEP_MIN ) {
			
			this.realMaxDate = this.parseDateTime( this.realMaxValue, 2 );
			this.realMinDate = this.parseDateTime( this.realMinValue, 2 ); 
		}		
		
		Runner.controls.DateSliderFilterControl.superclass.init.call( this );		
	},
	
	/**
	 * Get date-time formatted string	
	 */
	printDateTime: Runner.controls.DateField.prototype.printDateTime,

	/**
	* Create the Date object from the string representation
	* basing on the date format
	*/
	parseDateTime: Runner.controls.DateField.prototype.parseDateTime,	
			
	/**
	 * Replace the obtained slider's extreme value with
	 * the corresponding corresponding Date object		
	 * @param {number} value
	 * @param {boolean} isLower
	 * @param {boolean} minIsEqualToMax
	 * @return {Date object}
	 */
	getSliderValue: function( value, isLower, minIsEqualToMax ) {
		var prepDate = new Date( this.minDateInMs ),
			year, month, day, unitStep;
		
		switch ( this.stepType ) {
			case Runner.controls.constants.FILTER_STEP_SEC :		
			case Runner.controls.constants.FILTER_STEP_MIN :	
				if ( value === this.roundedMax ) {
					prepDate = this.realMaxDate;
					break;
				}	
				if ( value === this.roundedMin ) {
					prepDate = this.realMinDate;
					break;	
				} 
				prepDate = new Date( this.minDateInMs + value * 1000 );	
			break;	
				
			case Runner.controls.constants.FILTER_STEP_MONTH :				
				month = prepDate.getMonth() + value;			
				prepDate.setMonth( month );
				
				if (!isLower) {
					prepDate.setDate( prepDate.getDate() - 1 );
				}

				if ( minIsEqualToMax ) {
					month = prepDate.getMonth();
					year = prepDate.getFullYear();
					
					month = isLower ? month - 1 : month + 2;					
					day = isLower ? 1 : 0;
					prepDate = new Date(year, month, day);
				}			
			break;			
		
			case Runner.controls.constants.FILTER_STEP_YEAR :				
				month = isLower ? 0 : 11;
				day = isLower ? 1 : 31
				
				if ( value === this.roundedMax ) {
					year = this.maxDate.getFullYear();
				} else {				
					unitStep = parseFloat( this.step ) === 1;
					year = prepDate.getFullYear() + value;
					
					if (!isLower) {
						year = ( minIsEqualToMax || unitStep ) ? year : year - 1;
					} else {
						year = ( minIsEqualToMax && !unitStep ) ? year - 1 : year;
					}
				}
				prepDate = new Date(year, month, day);		
			break;		
		
			default: 
				prepDate = new Date( this.minDateInMs + value * 1000 );	
		}
			
		return prepDate;
	},
	
	/**
	 * Set the control's captions	
	 * @param {Date object} prepDate	
	 * @param {boolean} isLower
	 */		
	setCaptions: function( prepDate, isLower ) {
		Runner.controls.DateSliderFilterControl.superclass.setCaptions.call( this, prepDate, isLower );
		
		if ( this.processWrappers ) {
			this.setWrapperCaption( isLower, prepDate );
		}
	},
	
	/**
	 * Set the captions for the sliders of the
	 * 'month', 'hours', 'minutes' or 'seconds' 
	 * step types basing on which knob was moving 
	 * @param {boolean} isLower
	 * @param {Date object} date
	 */	
	setWrapperCaption: function( isLower, date ) {
		this.setCaptionsVisibility( isLower, date );
		
		if ( !isLower && this.showTime ) {
			this.jQcaptPostfix.html( this.printTime(date) );
			return;
		}
		if ( isLower && this.stepType === Runner.controls.constants.FILTER_STEP_MONTH ) {
			this.jQcaptPrefix.html( Runner.monthNames[ date.getMonth() ] );
		}
	},
	
	/**
	 * Set the captions vsibility for the sliders of the
	 * 'month', 'hours', 'minutes' or 'seconds' step types 
	 * @param {boolean} isLower
	 * @param {Date object} date 
	 */	
	setCaptionsVisibility: function( isLower, date ) {
		var otherKnobDate = isLower ? ( this.ctrlValues[1] || this.maxDate ) : ( this.ctrlValues[0] || this.minDate ), 
			visible;
		
		if ( this.showTime ) {
			visible = this.checkDatesEqual( otherKnobDate, date );	
			this.jQcaptPostfix.toggle( visible );
		
			this.jQmax.toggle( !visible );
			return;	
		} 

		if ( this.stepType === Runner.controls.constants.FILTER_STEP_MONTH ) {
			visible = otherKnobDate.getFullYear() === date.getFullYear();			
			this.jQcaptPrefix.toggle( visible );
			
			this.jQmin.toggle( !visible );
		}
	},	
	
	/**
	 * Check if two dates passed are equal or not
	 * disregarding the time values
	 * @param {Date object} date1 
	 * @param {Date object} date2 
	 * @param {Boolean}
	 */
	checkDatesEqual: function( date1, date2 ) {
		return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
	},
	
	/**	
	 * Get the correct slider's caption
	 * @param {Date object} date
	 * @return {string}
	 */
	getCaptionValue: function( date ) {
		if ( this.stepType === Runner.controls.constants.FILTER_STEP_YEAR ) {
			return date.getFullYear();
		}
		
		if ( this.stepType === Runner.controls.constants.FILTER_STEP_MONTH ) {
			return Runner.monthNames[ date.getMonth() ] + " " + date.getFullYear();
		}
		
		return this.printDateTime( date, this.dateFormat, true );
	},	
	
	/**
	 * Put backslashes in front of every search-syntax character (~,(,))
	 * in the slider's max knob value's string representation
	 * @param {boolean} encoded
	 * @return {string}
	 */
	getPreparedMaxValue: function( encoded ) {
		var date = this.ctrlValues[1], 
			stringValue = this.getStringValueFromDate( date );
			
		return Runner.controls.DateSliderFilterControl.superclass.getPreparedValue.call( this, stringValue, encoded );
		
	},
	
	/**
	 * Put backslashes in front of every search-syntax character (~,(,))
	 * in the slider's min knob value's string representation
	 * @param {boolean} encoded
	 * @return {string}
	 */	
	getPreparedMinValue: function( encoded ) {
		var date = this.ctrlValues[0],
			stringValue = this.getStringValueFromDate( date );
			
		return Runner.controls.DateSliderFilterControl.superclass.getPreparedValue.call( this, stringValue, encoded );
	},
	
	/**
	 * Replace undefined filter's values with corresponding
	 * slider's min and max date values
	 */	
	prepareControlValues: function() {
		var date;
		
		if ( this.ctrlValues[0] === undefined || this.ctrlValues[0] === null ) {
			date = this.realMinDate || this.minDate;
			if ( this.stepType === Runner.controls.constants.FILTER_STEP_YEAR ) {
				date = new Date( date.getFullYear(), 0, 1 );
			}			
			this.ctrlValues[0] = date;
		}
		
		if ( this.ctrlValues[1] === undefined || this.ctrlValues[1] === null ) {
			date = this.realMaxDate || this.maxDate;
			if ( this.stepType === Runner.controls.constants.FILTER_STEP_YEAR ) {
				date = new Date( date.getFullYear(), 11, 31 );
			} 				
			this.ctrlValues[1] = this.maxDate;
		}
	},

	/**
	 * Check if the slider's min and max knobs are equal to the
	 * the min and max slider's values respectively
	 * @return {boolean}
	 */
	knobsAreOnTheLimits: function() {
		if ( this.ctrlValues[0].toString() === ( this.realMinDate || this.minDate ).toString() &&
			 this.ctrlValues[1].toString() === ( this.realMaxDate || this.maxDate ).toString() ) {
			 
			return true;
		}
		return false;
	},
	
	/**
	 * Get the db-formatted date/date-time string
	 * @param {Date object} date
	 * @return {string}
	 */
	getStringValueFromDate: function( date ) {
		var dateString,
			day = date.getDate(),
			month = date.getMonth() + 1,
			year = date.getFullYear(),
			hours, minutes, seconds;
		
		dateString = year + '-' + month + '-' + day;
		
		if ( this.showTime ) {
			hours = date.getHours();
			hours = hours < 10 ? '0' + hours : hours;
			minutes = date.getMinutes();
			minutes = minutes < 10 ? '0' + minutes : minutes;
			seconds = date.getSeconds();
			seconds = seconds < 10 ? '0' + seconds : seconds;
			
			dateString += ' ' + hours + ':' + minutes + ':' + seconds;
		}
		
		return dateString;
	},
	
	/**
	 * Get the formated time string
	 * Here is the possible time formatch list:
	 * ("H:mm:ss","HH.mm.ss","HH:mm:ss","HH:mm:ss 'ch'","h::mm::ss tt","h:mm:ss tt","h:mm:ss.tt","hh:mm:ss tt","tt h:mm:ss","tt hh:mm:ss")	
	 *			 
	 * @param {Date object} date
	 * @return {string}
	 */
	printTime: function( date ) {
		var hours = date.getHours(),
			minutes = date.getMinutes(),
			seconds = date.getSeconds(),
			time = this.timeFormat,
			designator;
		
		if ( !this.is24hoursFormat ) {
			designator = hours >= 12 ? this.designators["pm"] : this.designators["am"];
			//designators are no added to 'hours' values greater than 23 
			designator = hours >= 24 ? "" : designator;
			time = time.replace( "tt", designator );
			
			//a correction is not applied to 'hours' values greater than 24 or lesser than 12
			hours = hours > 12 && hours < 24 ? hours - 12 : hours;
			hours = hours === 0 ? 12 : hours;
			
			hours = this.leadingZero && hours < 10 ? '0' + hours : hours;
			time = time.replace( this.leadingZero ? "hh" : "h", hours );
		} else {
			hours = this.leadingZero && hours < 10 ? '0' + hours : hours;
			time = time.replace( this.leadingZero ? "HH" : "H", hours );
		}
		
		time = time.replace( "mm", minutes < 10 ? '0' + minutes : minutes );
		
		if ( this.showSeconds ) {
			time = time.replace( "ss", seconds < 10 ? '0' + seconds : seconds );
		} else {
			time = time.replace( this.timeDelimiter + "ss", "" );
		}		

		return time;
	}
});

/**
 * The Time Slider Filter control class
 */ 
Runner.controls.TimeSliderFilterControl = Runner.extend( Runner.controls.DateSliderFilterControl, {
	/**
	 * @type {number}
	 */
	TimezoneOffsetInHours: 0,

	/**
	 * @type {boolean}
	 */
	isFieldTimeType: true,
	
	/**
	 * @type {number}
	 */
	offsetDaytimeHours: 24,

	/**
	 * @type {boolean}
	 */	
	processWrappers: false,	
	
	
	constructor: function( cfg ) {
		Runner.controls.TimeSliderFilterControl.superclass.constructor.call(this, cfg); 
	},
	
	/**
	 * Init the time-slider control
	 */	
	init: function() {
		this.TimezoneOffsetInHours = new Date().getTimezoneOffset() / 60;
		this.offsetDaytimeHours = 24 + this.TimezoneOffsetInHours;	
		
		Runner.controls.TimeSliderFilterControl.superclass.init.call( this ); 
	},	
	
	/**
	 * Get the real hours value for the date passed
	 * @param {Date object} date
	 * @return {number}
	 */	
	getHoursValue: function( date ) {
		var dateInHours = date.getTime() / 3600000;
		
		// process the case when the hours value could be greater then 24
		if ( dateInHours < this.offsetDaytimeHours ) {	
			return date.getHours();
		}
		return Math.floor( dateInHours - this.TimezoneOffsetInHours );
	},	
	
	/**
	 * Get the db-formatted time string
	 * @param {Date object} date
	 * @return {string}
	 */
	getStringValueFromDate: function( date ) {
		var hours = this.getHoursValue( date ), 
			minutes = date.getMinutes(), 
			seconds = date.getSeconds();

		hours = hours < 10 ? '0' + hours : hours;
		minutes = minutes < 10 ? '0' + minutes : minutes;
		seconds = seconds < 10 ? '0' + seconds : seconds;
		
		return hours + ':' + minutes + ':' + seconds;
	}, 	
	
	/**	
	 * Get the correct slider's caption
	 * @param {Date object} date
	 * @return {string}
	 */
	getCaptionValue: function( date ) {				
		return this.printTime( date );	
	}
});/**
 * Search form with user interface.
 */
Runner.search.SearchFormWithUI = Runner.extend( Runner.search.SearchForm, {	
	/**
	 * Options panel show status indicator
	 * @type Boolean
	 */
	srchOptShowStatus: false,

	/**
	 * Show status indicator of div, which contains add filter buttons
	 * @type Boolean
	 */
	ctrlChooseMenuStatus: false,
	/**
	 * Show status indicator of search type combos
	 * @type Boolean
	 */
	ctrlTypeComboStatus: false,
	/**
	 * jQuery object of img-button options panel expander
	 * @type {object}
	 */
	srchOptExpander: null,

	/**
	 * jQuery object with div, that contains all search elements
	 * @type {object}
	 */
	srchBlock: null,
	
	/**
	 * The 'Show all' button object
	 * @type {jQuery object} 
	 */
	showAllButton: null,
	
	showAllButtStatus: false,
	
	/**
	 * jQuery object with div, that contains all search controls
	 * @type {object}
	 */
	srchCtrlsBlock: null,
	
	/**
	 * Show status indicator of search block
	 * @type Boolean
	 */
	srchBlockStatus: true,
	
	/**
	 * The list of search fields that can be added to the Search panel
	 * @type {jQuery object}
	 */
	ctrlChooseMenuList: null,
	
	
	/**
	 * Array of search type combos
	 * @type {array}
	 */
	searchTypeCombosArr: null,
	
	/**
	 * Array of divs, that used as containers for one search control with its combos, delete buttons etc.
	 * @type {array}
	 */
	srchFilterRowArr: null,
	
	/**
	 * The search fields labels array
	 * @type {object}
	 */
	searchFieldsLabels: null,
	
	/**
	 * The 'Show\Hide options' button that toggles search type combos
	 * @type {jQuery object}
	 */
	showHideSearchOptionsButton: null,


	/**
	 * Array of bricks for search
	 * @type object 
	 */
	searchBricks: null,
	
	/**
	 * Criteria container
	 * @type object 
	 */
	topCritCont: null,
	
	/**
	 * Temporary array needed for floating/fixed panel reposition
	 * @type array 
	 */
	clonedContainers: [],
	
	/**
	 * Constructor
	 * @param {obj} cfg
	 */
	constructor: function( cfg ) {
		// recreate objects
		this.srchFilterRowArr = [];
		this.searchFieldsLabels = {};
		this.searchTypeCombosArr = [];
		
		// set search bricks array 
		this.searchBricks = ['search', 'vsearch2', 'searchpanel'];
	
		Runner.search.SearchFormWithUI.superclass.constructor.call(this, cfg);
	

		this.srchOptExpander = $("[id=showOptPanel" + this.id + "]");
		this.topCritCont = $('.srchCritTop', this.pageObj.pageCont);	
		this.srchBlock = $("#search_block" + this.id);	
		this.srchCtrlsBlock = $(".controlsBlock", this.pageObj.pageCont);
			
		
		this.showAllButton = $("[id=showAll" + this.id + "]");
		this.showHideSearchOptionsButton = $('#showHideSearchType' + this.id);
		this.addSearchFieldButton = $("#showHideControlChooseMenu" + this.id);
		
		this.showAllButtStatus = this.usedSrch;
		
		this.initCtrlChooseMenu();
		this.initCtrlPanelOptions();
	},
	
	/**
	 *
	 */
	initCtrlPanelOptions: function() {
		var self = this,
			panelOptions = $("#searchPanelOptions" + this.id);

		$(".close-button", panelOptions).on( "click", function(e) {
			self.toggleSearchOptions();
		});

		Runner.util.dropPositionSet(panelOptions.find(".dropdown-toggle"));
	},

	/**
	 * Create a Menu list filling it with the searchable fields' names.
	 * It allows users to add choosen ctrls to the Search Panel
	 */	
	initCtrlChooseMenu: function() {
		if ( this.ctrlChooseMenuList && !this.ctrlChooseMenuList.length ) {
			return;
		}
		
		var list = $('<ul class="list-group rnr-list controlChooseMenu"></ul>'), 
			controller = this,
			i, li, fName;

		list.addClass("dropdown-menu");
		
		for (i = 0; i < this.fNamesArr.length; i++) {	
			li = $('<li></li>').appendTo( list );

			fName = this.fNamesArr[i];
			
			$('<a href="#" class="rnr-button link" >' + this.searchFieldsLabels[ fName ]  + '</a>')
				.attr( "id", "addSearchControl_" + fName )
				.appendTo( li )
				// 'touchend' safari + ios fix
				.on( "click touchend", { fName: fName }, function(e) { 
					controller.addFilter( e.data.fName );
					e.preventDefault();
				});			
		}
		
		this.ctrlChooseMenuList = list.insertAfter( this.addSearchFieldButton );
		Runner.util.dropPositionSet(this.addSearchFieldButton);
	},

	/**
	 * Return search type combo container ids
	 * @param {string} fName
	 * @param {int} ind
	 * @return {string}
	 */
	getComboContId: function( fName, ind ) {
		return "searchType_" + ind + "_" + Runner.goodFieldName( fName );
	},
	
	getComboId: function( fName, id ) {
		return "srchOpt_" + id + "_" + Runner.goodFieldName( fName );
	},
	
	/**
	 * Entry point for Page's hideSearchControllerContainers function
	 */
	hideSearchControllerContainers: function() {
		this.pageObj.hideSearchControllerContainers();
	},
	
	/**
	 * Add search controls HTML block to DOM
	 * @param {string} fName
	 * @param {string} ind
	 * @param {object} blockHTML
	 */
	addCtrlsHtml: function( fName, ind, blockHTML ) {
		this.addPanelHtml( fName, ind, blockHTML );
		// take tr container
		var rowCont = $('#' + this.getFilterRowId( fName, ind )),
			// put into cells block html
			cells = rowCont.children(),
			cellHTML = '<div class="rnr-searchtype">' + blockHTML.comboHtml + '</div>'
					+ '<div class="rnr-searchcontrol">' + blockHTML.control1 + '</div>'
					+ '<div class="rnr-searchcontrol">' + blockHTML.control2 + '</div>';
		
		rowCont.append( cellHTML );
	},

	/**
	 * Toggle the 'Show all' button
	 * @param {Boolean} toggler
	 */
	toggleShowAll: function( toggler ) {
		toggler ? this.showShowAll() : this.hideShowAll() ;
	},
	
	hideShowAll: function() {
		Runner.hideButton( this.showAllButton );
		this.showAllButtStatus = false;
	},
	
	showShowAll: function() {
		Runner.showButton( this.showAllButton );
		this.showAllButton.parent().show();
		this.showAllButtStatus = true;
	},


	/**
	 * Showes search options div and changes image expander 
	 */
	showSearchBlock: function() {
		this.srchBlock.show();
		this.srchBlockStatus = true;
	},

	/**
	 * Closes search options div and changes image expander 
	 */
	hideSearchBlock: function() {
		this.srchBlock.hide();
		this.srchBlockStatus = false;
	},
	
	/**
	 * Search options switcher
	 * opens and closes options in search panel
	 */
	toggleSearchBlock: function() {
		this.srchBlockStatus ? this.hideSearchBlock() : this.showSearchBlock();
	},
	
	/**
	 * Show the search options div 
	 */
	showSearchOptions: function() {
		if ( this.pageObj.leftBarCollapsed() && this.pageObj.leftBarItems && this.pageObj.leftBarItems.indexOf('search_panel') != -1 ) {
			this.pageObj.toggleLeftBar( true );
		}
		this.pageObj.showElement('searchpanel');
		this.pageObj.toggleItemType('hide_search_panel', true );
		this.pageObj.toggleItemType('show_search_panel', false );

		this.srchOptShowStatus = true;
	},
	
	/**
	 * Closes the search options div and change the expander's image 
	 */
	hideSearchOptions: function() {
		this.pageObj.hideElement('searchpanel');

		this.pageObj.toggleItemType('hide_search_panel', false );
		this.pageObj.toggleItemType('show_search_panel', true );

		$('.rnr-datepicker').hide();
		this.srchOptShowStatus = false;
	},
	
	/**
	 * Search options switcher
	 * opens and closes options in search panel
	 */
	toggleSearchOptions: function() {
		this.srchOptShowStatus && !this.isSearchPanelInflexible ? this.hideSearchOptions() : this.showSearchOptions();
		this.hideCtrlChooseMenu();
	},

	/**
	 * Show the search options div and chang the expander's image
	 */
	showCtrlChooseMenu: function() { 
		return;
		if ( !this.ctrlChooseMenuList || !this.ctrlChooseMenuList.length ) {
			this.initCtrlChooseMenu();
		}

		var controller = this,
			hideHandler = function() {
				controller.hideCtrlChooseMenu();
			},
			showHandler = function() {
				controller.showCtrlChooseMenu();
			};
		this.showTask = new Runner.util.DelayedTask( showHandler );
		this.hideTask = new Runner.util.DelayedTask( hideHandler );
		
		this.ctrlChooseMenuList
			.on('mouseover', function(e) {
				controller.hideTask.cancel();
				controller.showTask.delay(50, showHandler, null, [e]);
			})
			.on('mouseout', function(e) {
				controller.showTask.cancel();
				controller.hideTask.delay(50, hideHandler, null, [e]);
			});

		this.showCtrlChooseMenu = function() {
			var buttonPosition = this.addSearchFieldButton.offset();
			this.hideTask.cancel();
			this.ctrlChooseMenuList.css({
				'top': buttonPosition.top + this.addSearchFieldButton.outerHeight() + 'px', 
				'left': buttonPosition.left + ( Runner.isDirRTL() ? this.addSearchFieldButton.outerWidth() - this.ctrlChooseMenuList.outerWidth() : 0 ) + 'px'
			}).show();
			
			if ( this.ctrlChooseMenuList.width() < this.addSearchFieldButton.width() ) {
				this.ctrlChooseMenuList.width( this.addSearchFieldButton.width() );
			}

			// set max z-index
			Runner.setZindexMaxToElem( this.ctrlChooseMenuList );
			this.ctrlChooseMenuStatus = true;
		}
		// call function, after lazy-init
		this.showCtrlChooseMenu();
	},
	
	/**
	 * Closes search options div and changes image expander 
	 */
	hideCtrlChooseMenu: function() {
		var _self = this;
		
		if ( !this.ctrlChooseMenuList || !this.ctrlChooseMenuList.length ) {
			this.initCtrlChooseMenu();
		}
		this.ctrlChooseMenuList.hide();
		
		// reset the inline display style to css ':hover' display rules will be applied 
		this.pageObj.resetButtonGroupDisplayStyle( this.ctrlChooseMenuList );

		
		this.ctrlChooseMenuStatus = false;
	},
	
	/**
	 * Search options switcher
	 * opens and closes options in search panel
	 */
	toggleCtrlChooseMenu: function() {
		this.ctrlChooseMenuStatus ? this.hideCtrlChooseMenu() : this.showCtrlChooseMenu();
	},
	
	/**
	 * Search type combos show handler
	 */
	showCtrlTypeCombo: function() {
		this.toggleSearchControlsOptionsCombo( true );
	},
	
	/**
	 * Search type combos hide handler
	 */
	 hideCtrlTypeCombo: function() {
		this.toggleSearchControlsOptionsCombo( false );
	},
	
	/**
	 * Toggle the seach controls' search options select elements
	 * Update the ctrlTypeComboStatus and changd the combo button's label.
	 * If toggler is falsy It hides the search options otherwise It shows them
	 * @param {boolean} toggler
	 */
	toggleSearchControlsOptionsCombo: function( toggler ) {
		var $select, selectValue;
		
		for (var i = 0; i < this.searchTypeCombosArr.length; i++) {
			$select = this.searchTypeCombosArr[i].find('select');
			selectValue = $select.val();
			
			if ( selectValue === 'Empty' || selectValue === 'NOT Empty' ) {
				this.updateControlSearchOptionsCombo( $select, toggler );
				$select.val( selectValue );
				continue;
			} 
			
			this.searchTypeCombosArr[i].toggle( toggler );
			$select.toggle( toggler );
		}

		var buttonText = toggler 
			? Runner.lang.constants.SEARCH_HIDE_OPTIONS_BUTTON 
			: Runner.lang.constants.SEARCH_SHOW_OPTIONS_BUTTON;	

		this.showHideSearchOptionsButton.find("a")
			.attr('title', buttonText )
			.html( buttonText );

		this.ctrlTypeComboStatus = toggler;	
	},
	
	/**
	 * Update the search options list basing on the toggler value.
	 * If toggler is falsy the combo will show 'empty' and 'Is not empty' options empty
	 * otherwise It'll show all allowed contol's search options
	 * @param {jQuery object} $select	
	 * @param {boolean} toggler	
	 */
	updateControlSearchOptionsCombo: function( $select, toggler ) {
		var $options = $select.find('option'), 
			$emptyOptions;
		
		if ( !toggler && $options.not("[value='Empty'],[value='NOT Empty']").length ) {
			$select.data( "options", $options.clone() );
			$emptyOptions = $select.find("option[value='Empty'], option[value='NOT Empty']").clone();
			
			$options.remove();
			$select.append( $emptyOptions );
			return;
		} 
		
		if ( toggler && $select.data("options") ) {
			$options.remove();
			$select.append( $select.data("options") );		
			$select.data("options", null);
		}
	},
	
	/**
	 * Search type combos show\hide switcher
	 */
	toggleCtrlTypeCombo: function() {
		this.toggleSearchControlsOptionsCombo( !this.ctrlTypeComboStatus );
	},
	
	/**
	 * Show/hide the criteria block, the Search button and 
	 * the 'Show/Hide options' button basing on the number of 
	 * visible search field blocks on the Search panel
	 * @param {number} ctrlsCount
 	 */
	toggleSearchButtonsState: function( ctrlsCount ) {

		var topCriteriaController = this.topCritCont,
			optionsButton = this.showHideSearchOptionsButton,
			bottomSearchButt = $('.bottomSearchButt', this.pageObj.pageCont),
			controlsBlock = $('.controlsBlock', this.pageObj.pageCont);

		this.toggleSearchButtonsState = function( ctrlsCount ) {
			topCriteriaController.toggle( ctrlsCount > 1 );
			bottomSearchButt.toggle( ctrlsCount > 0 );
			optionsButton.toggle( ctrlsCount > 0 );
			
			controlsBlock.toggle( ctrlsCount > 0 );
		}

		this.toggleSearchButtonsState( ctrlsCount );
	}
	
});
/**
 * search panel controller. Used for manage search on the list page
 * for multiple search classes use id param.
 * @class
 * @param {object} cfg
 */
Runner.search.SearchController = Runner.extend( Runner.search.SearchFormWithUI, {
	/**
	 * Ajax add filter cache url
	 * @type String
	 */
	ajaxSearchUrl: "",
	
	/**
	 * Reusable style display none
	 * @type String
	 */
	styleDispNoneText: 'display: none;',
	
	/**
	 * Indicator: true when simple or google-like search is active
	 * @type {Boolean}
	 */
	simpleSearchActive: false,
	
	/**
	 * Indicator, that is true if the Search panel is flexible
	 */
	isSearchPanelInflexible: false,
	
	/**
	 * Array containing the Search panle fields' names
	 */
	panelSearchFields: [],
	
	/**
	 *
	 */
	searchPanelRunByUserApi: false,

	/**
	 * Search options for all inflexible Search panel's fields
	 */
	inflexSearchOptions: {},
	
	/**
	 * The search table's name
	 */
	searchTableName: "",
	
	/**
	 * The array containg filter controls data
	 */
	filterContolsData: [], 
	
	/**
	 * The array of filter controls
	 */
	filterControls: [],
	
	/**
	 * Reference to timeout set up during the last keyup event
	 */
	submitTimeout: null,

	/**
	 * The 'Reset' button object
	 * @type {jQuery object} 
	 */
	resetButton: null,	
	
	
	/**
	 * Override parent contructor
	 * Add interaction with server
	 * @param {obj} cfg
	 */
	constructor: function( cfg ) {
		Runner.search.SearchController.superclass.constructor.call( this, cfg );

		this.ajaxSearchUrl = Runner.getPageUrl(this.shortTName, "search");
		this.simpleSearchActive = Runner.pages.PageSettings.getTableData( cfg.tName, "simpleSearchActive" );
	},
	
	/**
	 * @param {array} ctrlsBlocks
	 */
	init: function( ctrlsBlocks ) {
		Runner.search.SearchController.superclass.init.call( this, ctrlsBlocks );
		
		this.initFastSearch();
		this.initFilters();
		this.remindSearchPanelState();		

		this.savePanelsStateBeforeUnload();		
	},

	/**
	 * Add the rememberSearchPanelState and rememberFilterPanelState
	 * methods invocation to the 'onbeforeUnload' event's handler
	 */
	savePanelsStateBeforeUnload: function() {
		var serachController = this;
		 
		$(window).on('beforeunload', function() {
			serachController.rememberSearchPanelState();
			serachController.rememberFilterPanelState();
		}); 
	},
	
	/**
	 * Add 'keyup'/'keydown' listeners to the Simple search input element
	 */
	initFastSearch: function() {
		var controller = this;
		
		this.smplSrchBox.on('change keyup', function(e) {
			Runner.trackUserActivity();
		});
		
		this.smplSrchBox.on('keydown', function(e) {
			controller.listenEvent(e, this);
		});
		
		if ( this.useSuggest ) {
			//adjust the markup
			if ( !$('#search_suggest').length ) {
				$('<div id="search_suggest"></div>').addClass("search_suggest").appendTo('body');
			}
				
			this.smplSrchBox.on('keyup', function(e) {
				if ( !Runner.isAcceptableKeyCode(e) ) {
					return;
				}
				
				if ( controller.submitTimeout ) {
					clearTimeout( controller.submitTimeout );
				}

				controller.submitTimeout = setTimeout( function() {
					controller.searchSuggestForSimple( controller.smplSrchBox );	
				}, 500);
			})
		}
	},
	
	/**
	 * @return string
	 */
	getSuggestUrl: function() {
		if ( this.pageType === Runner.pages.constants.PAGE_DASHBOARD ) {
			return Runner.getPageUrl( 'searchsuggest', '', 'table=' + Runner.pages.PageSettings.getShortTName( this.tName ) );
		}
		return Runner.getPageUrl( 'searchsuggest', '', 'table=' + this.shortTName );
	},	
	
	/**
	 * Initialize the search buttons
	 */	 
	initButtons: function() {
		var searchController = this;
		Runner.search.SearchController.superclass.initButtons.call(this);
		
		$("#searchButtTop" + this.id).on("click", function(e) {
			searchController.submitSearch();
			return false;	
		});
							
		this.resetButton = $("[id=clearSearch" + this.id + "]").on("click", function(e) {
			searchController.clearSearch();
			return false;	
		}); 
		
		this.initShowHideToggler();

		this.showHideSearchOptionsButton.on("click", function(e) {
			searchController.toggleCtrlTypeCombo();
			$(this).parent().trigger("click");
			return false;
		});
			
		this.showAllButton.on("click", function(e) {
			searchController.showAllSubmit();
			return false;
		}); 			
		
		this.initSearchButton();
		this.initDelButtons();
	},

	initShowHideToggler: function() {
		var searchController = this;
		$("#showOptPanel" + this.id).add("#hideOptPanel" + this.id).on("click", function(e) {
			$(this).parent().trigger("click");
			searchController.toggleSearchOptions();
			if ( searchController.getVisibleBlocksCount() === 0 ) {
				$("#showHideControlChooseMenu" + searchController.id).next().find("a:first").trigger("click");
			}
			
			return false;
		});
	},
	
	/**
	 * Toggle the 'Reset' button
	 * @param {Boolean} toggler
	 */
	toggleResetButton: function( toggler ) {
		toggler ? this.showResetButton() : this.hideResetButton() ;
	},
	
	hideResetButton: function() {
		Runner.hideButton( this.resetButton );
	},
	
	showResetButton: function() {
		Runner.showButton( this.resetButton );
		this.resetButton.parent().show();
	},
	
	/**
	 * Init the Search button functionality
	 */
	initSearchButton: function() {
		var searchController = this;
		
		this.searchButton.on("click", function(e) {
			if ( !Runner.isDisabledButton(this) ) {
				searchController.submitSearch();
			}
			return false;			
		});
	},
	
	/**
	 * Add click event's handler to the Search panel's controls block element
	 */
	initDelButtons: function() {
		// 'touchend' safari + ios fix
		this.srchCtrlsBlock.on('click touchend', {srchController: this}, this.delButtonHandler);
	},

	/**
	 * The click event handler that invokes the delCtrl method
	 * if the Delete Search Control button is clicked
	 * The event object's date property is augmented with 
	 * the srchController property pointing to this
	 * @params {object} e	
	 */
	delButtonHandler: function(e) {
		var srchController = e.data.srchController,
			$target = $( e.target ).closest(".searchPanelButton"),
			fName = $target.attr("fName"), 
			ctrlId, i;
				
		if ( !$target.length || !fName ) {
			return;
		}
		
		ctrlId = parseInt( $target.attr("ctrlId"), 10 );
		for (i = 0; i < srchController.fNamesArr.length; i++) {
			if ( fName == Runner.goodFieldName( srchController.fNamesArr[i] ) ) {
				fName = srchController.fNamesArr[i];
				break;
			}
		}
		
		srchController.delCtrl(fName, ctrlId);
		srchController.pageObj.updateDropdownButtons();
		return false;
	},

	/**
	 * Get the string representation value of the any field engaged in the current search
	 * @return {String}
	 */
	getTheFirstSearchControlValue: function() {
		var i, fName, searchFields;
		
		if ( this.simpleSearchActive ) {
			return this.smplSrchBox.val();
		}
		
		for (i = 0; i < this.filterControls.length; i++) {
			if ( this.filterControls[i].filtered ) {
				return this.filterControls[i].getDisplayedValue();
			}
		}
		
		for (fName in this.searchFields) {
			searchFields = this.searchFields[ fName ];
			for (i = 0; i < searchFields.length; i++) {
				if ( searchFields[i].checkIfCtrlsValuesSet() ) {
					return searchFields[i].getControl().getUserValue();
				}
			}
		}
		
		return "search";
	},
	
	/**
	 * Get an index of the last control added from cache. 
	 * @param {string} filterName
	 * @return {number}
	 */
	getLastAddedInd: function( filterName ) {
		// if no map for this field
		if ( !this.ctrlsShowMap[ filterName ] ) {
			return 0;
		}
		
		// get last added and not cached ctrls block index
		var maxInd = 0, 
			beforeMaxInd = 0, 
			i = 0, ind;
			
		for ( ind in this.ctrlsShowMap[ filterName ] ) {
			// need to convert to int from string. May be because object property name is string, typeof return string
			ind = parseInt( ind, 10 );
			// get max index, it will give last cached
			if ( maxInd < ind ) {
				beforeMaxInd = maxInd;
				maxInd = ind;
			}
			// at first time take maxInd, because 0 may not appear
			if ( i === 0 ) {
				beforeMaxInd = maxInd;
			}
			i++;
		}
		
		return beforeMaxInd;
	},
	
	/**
	 * Get the last added filter object
	 * @param {string} filterName 
	 * @return {object} 
	 */
	getLastAddedRow: function( filterName ) {
		var beforeMaxInd = this.getLastAddedInd( filterName ),
			$filterObj;
			
		if ( !beforeMaxInd ) {
			return null;
		}

		$filterObj = $( '#' + this.getFilterRowId( filterName, beforeMaxInd ) );
		return !!$filterObj.length ? $filterObj : null;
	},
	
	/**
	 *
	 */
	addPanelHtml: function( fName, ind, blockHTML ) {
		// ctrl main container id
		var newSrchCtrlContId = this.getFilterRowId( fName, ind ),
			// add ctrl main container
			filterRowHtml = this.createTableRow( newSrchCtrlContId, 'srchPanelRow', this.styleDispNoneText, '' ),
			newSrchCtrlCont, fNameCellHtml, comboHtml;
		
		this.srchCtrlsBlock.append( filterRowHtml );
		// main container obj
		newSrchCtrlCont = $("#" + newSrchCtrlContId);
		// add del button
		newSrchCtrlCont.append( '<label>' + blockHTML.fLabel + '</label>' + blockHTML.delButt );
		
		return newSrchCtrlCont;
	},

	
	/**
	 * Adds block to map, regs its components and ands HTML
	 * @param {string} fName
	 * @param {number} ind
	 * @param {number} ctrlIndArr
	 * @param {object} blockHTML
	 */
	addRegCtrlsBlock: function( fName, ind, ctrlIndArr, blockHTML ) {
		if ( blockHTML ) {
			//add ctrls block HTML to DOM
			this.addCtrlsHtml( fName, ind, blockHTML );
		}
		
		Runner.search.SearchController.superclass.addRegCtrlsBlock.call( this, fName, ind, ctrlIndArr );
			
		// reg combos
		this.searchTypeCombosArr.push( $("#" + this.getComboContId( fName, ind )) );
		// reg filter div block
		this.srchFilterRowArr.push( $("#" + this.getFilterRowId( fName, ind )) );
		// call crit controller
		this.toggleSearchButtonsState( this.getVisibleBlocksCount() );			
	},
	
	/**
	 * Creates table row and cell containers html
	 * @param {string} id
	 * @param {string} cssClass
	 * @param {string} style
	 * @param {string} innerHtml
	 * @return {string}
	 */
	createTableRow: function(id, cssClass, style, innerHtml) {	
		return '<div class="' + cssClass + ' form-group" id="' + id + '" style="' + style + '">' + innerHtml + '</div>';
	},
	
	createTableCell: function(cssClass, style, innerHtml) {
		return '<td class="' + cssClass + '" style="' + style + '">' + innerHtml + '</td>';
	},
	
	/**
	 * Put block into right place depending on ctrl type. 
	 * If parent field name passed, ctrl will be placed bellow parent
	 * If no parent passed, ctrl will be placed above last added for this field
	 * @param {string} filterName
	 * @param {number} cachedInd
	 * @param {array} parentFieldNames
	 */
	putCachedBlock: function( filterName, cachedInd, parentFieldNames ) {
		var cachedRowId = this.getFilterRowId( filterName, cachedInd ),
			$cachedRow = $( "#" + cachedRowId ),
			$lastAddedRow, $lastAddedParentRow, i;
	
		parentFieldNames = parentFieldNames || [];	
	
		for ( i = 0; i < parentFieldNames.length; i++ ) {
			$lastAddedParentRow = this.getLastAddedRow(  parentFieldNames[ i ] );
			
			if ( $lastAddedParentRow ) {
				$cachedRow.insertAfter( $lastAddedParentRow );
				// show row with controls
				$cachedRow.show();
				return;
			}
		}
		
		$lastAddedRow = this.getLastAddedRow( filterName );
		
		if ( $lastAddedRow && $lastAddedRow.attr("id") !== cachedRowId ) {
			$cachedRow.insertBefore( $lastAddedRow );
		} else {
			this.srchCtrlsBlock.prepend( $cachedRow );
		}
		
		// show row with controls
		$cachedRow.show();
	},
	
	/**
	 * @param {string} filterName
	 * @return {jQuery object}
	 */
	createLoadingBox: function( filterName ) {
			// add tr for panel mode;
		var loadTr = $('<tr>').addClass('srchPanelRow'), 
			loadTd = $('<td>');
			
		loadTd.attr('colspan', '3')
			.addClass('cellBorderRightHovered')
			.addClass('cellBorderLeftHovered')
			.addClass('cellBorderCenterHovered')
			.html( '&nbsp;&nbsp;' + filterName + ':&nbsp;loading&nbsp;...&nbsp;' )		
			.appendTo( loadTr );
		
		return loadTr;
	},
	
	/**
	 * @param {jQuery object} loadBox
	 * @param {string} filterName
	 */
	putLoadingBox: function( loadBox, filterName ) {
		// move cached div to top, insert it after control choose menu
		var $lastAddedRow = this.getLastAddedRow( filterName );
		
		if ( $lastAddedRow ) {
			loadBox.insertBefore( $lastAddedRow );
		} else {
			this.srchCtrlsBlock.append( loadBox );
		}
	},
	
	/**
	 * Set dependent and parent links to ctrls. 
	 * If the 'triggerReload' param is true the corresponding 
	 * event of parent ctrl will be invoked to reload dependent ctrls
	 * 
	 * @param {object} ctrl 			
	 * @param {boolean} triggerReload 
	 */
	setDependencies: function( ctrl, triggerReload ) {  
		var parentFNames, parentFData, i, parentFName, parentInd;
		
		if ( ctrl.cached || !ctrl.hasDependencies() ) { 
			return;
		}
		
		parentFData = {};
		parentFNames = ctrl.getParentLookupFieldNames();
		
		for ( i = 0; i < parentFNames.length; i++ ) {
			parentFName =  parentFNames[ i ];
			if ( !this.ctrlsShowMap[ parentFName ] ) {
				continue;
			}

			parentInd = this.getLastAddedInd( parentFName );
			
			if ( !this.ctrlsShowMap[ parentFName ][ parentInd ] ) {
				continue;
			}
			
			parentFData[ parentFName ] = {
				rowId: parentInd,
				ctrlIndex: this.ctrlsShowMap[ parentFName ][ parentInd ][0]
			}			
		}
		
		ctrl.setDependencies( this.tName, 0, parentFData/*, triggerReload */);
	},
	
	/**
	 * Get the open Search panel seach blocks' names
	 * @return {Array}
	 */
	getShownFilterNames: function() {
		var fNamesArr = [],
			fName, ind;
		
		for ( fName in this.ctrlsShowMap ) {
			for ( ind in this.ctrlsShowMap[ fName ] ) {
				// need to convert to int from string. May be because object property name is string, typeof return string
				ind = parseInt( ind, 10 );
				
				if ( this.isFieldShownById(fName, ind) ) { 
					fNamesArr.push( fName );
				}
			} 
		}
		return fNamesArr;
	},
	
	/**
	 * Show cached search field on the Search panel.
	 * If there is a cached search block for a field
	 * It shows the first control, sets focus on it and
	 * sets dependences for dropdowns
	 *
	 * @param {string} filterName
	 * @return {boolean} 
	 */
	showCached: function( filterName ) {
		if ( !this.ctrlsShowMap[ filterName ] ) { 
			return false;
		}
		
		var cachedInd, ind, cachedCtrlIndArr, 
			firstCtrlFromCache;
			
		for ( ind in this.ctrlsShowMap[ filterName ] ) {
			// Get index of the first cached search panel field's div
			// The object property name is string so It's need converting to int number
			ind = parseInt( ind, 10 );
			if ( !this.isFieldShownById( filterName, ind ) ) {
				// get max not shown index
				cachedInd = ind;
				break;
			}
		}
		// no cached ctrls, only already shown
		if ( cachedInd === undefined ) {
			return false;
		}
		
		// index of last cached ctrl for this field
		cachedCtrlIndArr = this.ctrlsShowMap[ filterName ][ cachedInd ];

		// get ctrl
		firstCtrlFromCache = Runner.controls.ControlManager.getAt( this.tName, cachedInd, filterName, cachedCtrlIndArr[0] );
		firstCtrlFromCache.cached = false;
		
		// show ctrl
		if ( !firstCtrlFromCache.hidden ) {
			firstCtrlFromCache.show();
		}

		// set dependeces between child and parent if these links could be
		this.setDependencies( firstCtrlFromCache, true );
		// place ctrl depend on it's type: lookup or simple
		this.putCachedBlock( filterName, cachedInd, firstCtrlFromCache.parentFieldNames ); 
		
		// show type combo, if it shown in others ctrl
		if ( this.ctrlTypeComboStatus ) {
			$( "#" + this.getComboContId(filterName, cachedInd) ).show();
		}	

		// set focus to added ctrl
		firstCtrlFromCache.setFocus();
		
		return firstCtrlFromCache;
	},
	
	/**
	 * Adds filter to panel or window, and loads another one for cache
	 * @param {string} filterName
	 * @param {function} callback user function
	 */
	addFilter: function( filterName, userCallBack ) {		
		var isShown = !!this.showCached( filterName ),
			controller = this,
			$loadBox, ajaxParams;
			
		if ( !isShown ) { //the contol isn't shown on the Search Panel
			$loadBox = this.createLoadingBox( filterName );
			this.putLoadingBox( $loadBox, filterName );
		} else {
			this.toggleSearchControlsOptionsCombo( this.ctrlTypeComboStatus );
		}
		
		ajaxParams = {
			//isNeedSettings: this.isTableSearchable() ? !Runner.pages.PageSettings.checkSettings( this.tName, filterName, this.pageType ) : true,
			isNeedSettings: true, // custom user controls may add some special settings, e.g. Knob
			searchControllerId: this.id,
			mode: "inlineLoadCtrl",
			ctrlField: filterName,
			id: Runner.genId()
		};
		
		Runner.runnerAJAX( this.ajaxSearchUrl, ajaxParams, function( ctrlJSON, queryStatus ) {
			var i, ctrl, ind, sFInd, firstCtrlFromCache;
			
			// register new ctrl block
			controller.addRegCtrlsBlock( filterName, ctrlJSON.divInd, (ctrlJSON.control2 ? [0, 1] : [0]), ctrlJSON );
			
			if ( ctrlJSON.settings ) {
				// TODO:  addFieldSettings method 
				Runner.pages.PageSettings.addSettings( controller.searchTableName, ctrlJSON.settings );
			}
			
			for (i = 0; i < ctrlJSON.ctrlMap.length; i++) {
				//replacing the config's "table" property with the current table name guarantees the correct control saving for the non table's page Search panel
				ctrlJSON.ctrlMap[i].cached = isShown;
				ctrl = Runner.controls.ControlFactory( ctrlJSON.ctrlMap[i], controller.pageType, false, controller.pageObj );
	
				//init SearchSuggest, set dependencies and add disablings/enabling Search button handlers for a required field
				controller.initSearchField( ctrl );
			}
			
			// init search option when search controls are initialized 
			controller.initCombo( ctrlJSON.divInd, filterName, (ctrlJSON.control2 ? [0, 1] : [0]) );
			
			if ( !isShown ) { 
				//the control wasn't cached before and It isn't shown on the Search Panel
				firstCtrlFromCache = controller.showCached( filterName );
				$loadBox.remove();
				controller.toggleSearchButtonsState( controller.getVisibleBlocksCount() );
				// because ajax ctrl will shown with delay
				controller.toggleSearchControlsOptionsCombo( controller.ctrlTypeComboStatus );
			}
			$(".srchPanelCell2").show();
			
			//if the filterName loaded is shown on the Search panel, it has been alredy added to SearchController's searchFields in initSearchField
			if ( !controller.isFieldShownById( filterName, ctrlJSON.divInd ) ) {
				//the just loaded filterName is cashed and isn't show on the Search panel
				ind = controller.getLastAddedInd( filterName );
				//add the filterName search field's object that have just appeared to SearchController's searchFields 
				controller.addSearchField( filterName, ind );
			}
			
			// set focus to added ctrl
			if ( firstCtrlFromCache ) {
				firstCtrlFromCache.setFocus();
			}				
			
			if ( typeof userCallBack === 'function' ) {
				sFInd = controller.searchFields[ filterName ].length - 1;
				userCallBack( controller.searchFields[ filterName ][ sFInd ] );
			}
			controller.pageObj.updateDropdownButtons();			
		});
	},
	
	addFilterInNumber: function(filterName, number, userCallBack) {
		for (var i = 0; i < number; i++) {
			this.addFilter(filterName, userCallBack);
		}
	},
	
	isFieldShownById: function(fName, id) {
		var fieldId = this.getFilterRowId(fName, id);
		return $("#" + fieldId).is(":visible");
	},
	
	/**
	 * Delete controls objects add the corresponding html-markup from DOM
	 * @param {string} fName
	 * @param {int} ind
	 */
	delCtrl: function( fName, ctrlId ) {
		var ctrlsShowMap = this.ctrlsShowMap[ fName ][ ctrlId ],
			ctrl, i;
		
		// ureg ctrls, loop will delete also second ctrl, if it was created
		for (i = 0; i < ctrlsShowMap.length; i++) {
			ctrl = Runner.controls.ControlManager.getAt( this.tName, ctrlId, fName, ctrlsShowMap[i] );
			// for lookup ctrls, clear links from children and trigger reload them with all values
			if ( ctrl && ctrl.isLookupWizard() ) {
				ctrl.clearChildrenLinks( true );
			}
			Runner.controls.ControlStorage.unregister( ctrlId, fName, ctrlsShowMap[i] );
		}
		
		// remove element from dom
		this.removeComboById( this.getComboContId(fName, ctrlId) );
		this.removeFilterById( this.getFilterRowId(fName, ctrlId) );
		
		// call crit controller
		this.toggleSearchButtonsState( this.getVisibleBlocksCount() );
		// remove from ctrl show map
		delete this.ctrlsShowMap[ fName ][ ctrlId ];
		if ( this.useSuggest ) {
			Runner.suggests.DestroySuggestDiv();
		}
		
		//remove the corresponding search Field
		for (i = 0; i < this.searchFields[ fName ].length; i++) {
			if ( parseInt( this.searchFields[ fName ][i].id, 10 ) === ctrlId ) {
				this.searchFields[ fName ].splice(i, 1);
				break;
			}
		}
		
		this.toggleSearhButtonStatus();
	},
	
	/**
	 * Deletes filter by id, removes from array and DOM element
	 * @param {string} id
	 */
	removeFilterById: function( id ) {
		var controller = this;
		$.each( this.srchFilterRowArr, function( idx, $elem ) {
			if ( $elem.attr('id') === id ) {
				$elem.remove();
				controller.srchFilterRowArr.splice( idx, 1 );
				return false;
			}
		});
	},
	
	/**
	 * Deletes combo cont by id from, removes from array and DOM element
	 * @param {string} id
	 */
	removeComboById: function( id ) {
		var controller = this;
		$.each( this.searchTypeCombosArr, function( idx, $elem ) {
			if ( $elem.attr('id') == id ) {
				controller.searchTypeCombosArr.splice( idx, 1 );
				return false;
			}
		});
	},
	
	/**
	 * Get number of visible ctrls blocks
	 * @return {number}
	 */
	getVisibleBlocksCount: function() {
		var visCount = 0,
			// use tr arr if window mode, or div arr if panel
			rowArr = this.srchFilterRowArr,
			i;
			
		// loop through all filters to get which are visible
		for (i = 0; i < rowArr.length; i++) {
			//check on 'display' stype property, because the element
			//could have the visibility prop set to hidden 
			if ( rowArr[i].css('display') !== 'none' ) {
				visCount++;
			}
		}
		return visCount;
	},
	
	/**
	 * Resets form ctrls, for panel
	 * @return {Boolean}
	 */
	resetCtrls: function() {
		var objIndForCM, fName, ind, i, ctrl;
		
		for (fName in this.ctrlsShowMap) {
			for (ind in this.ctrlsShowMap[fName]) {
				for (i = 0; i < this.ctrlsShowMap[fName][ind].length; i++) {
					// index of object that stored in CM
					objIndForCM = this.ctrlsShowMap[fName][ind][i];
					// delete each object
					ctrl = Runner.controls.ControlManager.getAt(this.tName, this.id, fName, objIndForCM);
					ctrl.reset();
				}
			}
		}
		return false;
	},
	
	/**
	 * Save to localStorage the open Search panel's controls,
	 * the search panel state ('open'/'closed') 
	 */
	rememberSearchPanelState: function() {
		var searchPanelString = localStorage.getItem('searchPanel'),
			panelStateObj = {
				srchPanelOpen: this.srchOptShowStatus, 
				srchCtrlComboOpen: this.ctrlTypeComboStatus,
				openFilters: this.usedSrch ? [] : this.getShownFilterNames(),
			},
			searchPanel = {};
		
		if ( searchPanelString ) {
			searchPanel = JSON.parse( searchPanelString );
		}
		searchPanel['panelState_' + this.shortTName + '_' + this.id] = panelStateObj;
		localStorage.setItem('searchPanel', JSON.stringify( searchPanel ));
	},
	
	/**
	 * Get form localStorage the search panel state ('open'/'closed'/'unpinned')
	 * and the Search panel's popup window params (if the Search panel is unpinned)
	 * Adjust the Search panel in accordance with the obtained data
	 */
	remindSearchPanelState: function() {
		var panelStateString = localStorage.getItem('searchPanel'),
			panelStateObj, searchPanel;
		
		if ( panelStateString ) {
			searchPanel = JSON.parse( panelStateString );
			panelStateObj = searchPanel['panelState_' + this.shortTName + '_' + this.id];
		}
		
		if ( !panelStateObj && !this.searchPanelRunByUserApi ) {
			if ( this.panelSearchFields.length ) {
				this.showSearchOptions();
			}		
			return;
		}
				
		if ( !this.isSearchPanelInflexible ) {
			if ( panelStateObj && panelStateObj.srchPanelOpen || this.searchPanelRunByUserApi ) {
				this.showSearchOptions();
			}
		} else {
			this.showSearchOptions();
		}
		
		
		if ( panelStateObj && panelStateObj.srchCtrlComboOpen ) {
			this.showCtrlTypeCombo();
			return;	
		} 
		
		this.hideCtrlTypeCombo();
	},
	
	getFieldIds: function(fName, disp) {
		var idsArr = [], id;
		
		if (this.ctrlsShowMap[fName]) {
			for ( id in this.ctrlsShowMap[fName] ) {
				if ( disp !== true || this.isFieldShownById(fName, id) ) {
					idsArr.push(id);
				}
			}		
		}
		return idsArr;
	},
	
	/**
	 * Get list of search panel fields
	 * User function
	 * @return {array}
	 */
	getSearchFields: function(fName) {
		var allFields, i, j, fName;
		
		if (typeof fName !== 'undefined') {
			return this.searchFields[fName];
		}
		
		allFields = [];
		//traverse the array, containing searchable fields' names, backwards to get the search fields in the right order 
		for (i = this.fNamesArr.length; i--; ) {
			fName = this.fNamesArr[i];
			
			for (j = 0; j < this.searchFields[fName].length; j++) {
				allFields.push( this.searchFields[fName][j] );
			}
		}
		return allFields;
	},
	
	/**
	 * Add field to search filter
	 * User function
	 * @param {string}
	 * @param {function} user callback
	 */
	addField: function(fName, callback) {
		if ( !fName ) {
			return;
		}
		this.addFilter(fName, callback);
	},
	
	/**
	 * Delete field from search filter
	 * User function
	 * @param {string}
	 * @param {integer}
	 */
	deleteField: function(fName, id) {
		if ( !fName ) {
			return;
		}

		if (typeof id !== 'undefined') {
			//delete only field with current id
			this.delCtrl(fName, id);
			return; 
		}
		
		//delete all fields with name like fName from search panel		
		var ids = this.getFieldIds(fName, true);
		for (var i = 0; i < ids.length; i++) {
			this.delCtrl(fName, ids[i]);
		}
		this.searchFields[fName] = [];
	},
	
	/**
	 * Clear search panel
	 * User function
	 */
	clear: function() {
		var shownFields = this.getShownFilterNames();
		
		for (var i = 0; i < shownFields.length; i++) {
			this.deleteField( shownFields[i] );
		}
	},
	
	/**
	 * Display search panel
	 * User API function
	 * @param {string}
	 */
	display: function( as ) {
		switch( as ) {
			case "show":
				this.showSearchOptions();
				break;
			case "hide":
				this.hideSearchOptions();
				break;
		}
	}, 	
	
	/**
	 * Toggle options
	 * User function
	 */
	toggleOptions: function(as) {
		switch (as) {
			case "show":
				this.showCtrlTypeCombo();
				break;
			case "hide":
				this.hideCtrlTypeCombo();
				break;
		}
	},
	
	/**
	 * Toggle criteria
	 * User function
	 * @param {string}
	 */
	toggleCriteria: function(as) {
		switch (as) {
			case "all":
				$('#all_checkbox', this.pageObj.pageCont).prop('checked', true);
				break;
			case "any":
				$('#any_checkbox', this.pageObj.pageCont).prop('checked', true);
				break;
			case "show":
				this.topCritCont.show();
				break;
			case "hide":
				this.topCritCont.hide();
				break;
		}
	},
	
	/**
	 * Get a search option for a inflexible Search panel's field
	 * @param {String} fName	
	 */
	getInflexSearchOption: function( fName ) {
		return this.inflexSearchOptions[ fName ];
	},

	/**
	 * Reset all the Search panel settings to default
	 */
	clearSearchPanel: function() {
		var fName, i, fSearchFields,
			defaultOption;
	
		for (fName in this.searchFields) {
			fSearchFields = this.searchFields[fName];
			defaultOption = this.isSearchPanelInflexible ? this.getInflexSearchOption(fName) : Runner.lang.constants.CONTAINS;
	 
			for (i = 0; i < fSearchFields.length; i++) {
				fSearchFields[i].clearFieldCtrls();
				fSearchFields[i].setOption(defaultOption);
			}
		}

		this.toggleCriteria("all");	
	},
	
	/**
	 * Clear Simple search and Simple panel without the page reloading
	 */
	clearSearch: function() {
		this.clearSearchPanel();
		this.clearSimpleSearch();	
	},
	
	/**
	 * Init Search Suggest, add search field's object to the SearchFields,
	 * set disablings/enabling Search button handlers for a required field,
	 * set dependencies for a dependent lookup
	 * @param {Object} ctrl
	 */	
	initSearchField: function( ctrl ) {
		if ( !ctrl ) {
			return;
		}
		
		//borrow the Runner.search.SearchForm initSearchField method
		Runner.search.SearchController.superclass.initSearchField.call(this, ctrl);
		this.setDependencies( ctrl );	
	},

	/**
	 * Checks if the search panel is set on the table's page or not
	 * @return {boolean}
	 */	
	isTableSearchable: function() {
		return this.tName === this.searchTableName;
	},

	/**
	 * Init the filter panel
	 */
	initFilters: function() {
		this.initFilterControls();
		this.adjustExpanderSigns();

	},
	
	/**
	 * Create all filter controls and store them in filterControls array
	 */
	initFilterControls: function() {
		var i, filterControl;
	
		for (i = 0; i < this.filterControls.length; i++) {
			this.filterControls[ i ].destructor();
		}
	
		this.filterControls = [];
	
		for (i = 0; i < this.filterContolsData.length; i++) {
			filterControl = Runner.controls.FilterControlFactory( this.filterContolsData[i], this );
			if ( filterControl ) { 
				this.filterControls.push( filterControl );
			}		
		}
	},
	
	/**
	 * Save in localStorage the filters' states (expanded/collapsed)
	 */
	rememberFilterPanelState: function() {
		if ( !this.filterControls.length ) {
			return;
		}
		
		var stateId = 'filtersState_' + this.shortTName + '_' + this.id,
			filterPanelString = localStorage.getItem('filterPanel'),
			oldFiltersState = {},
			filtersState = {},
			filterPanel = {},
			filter, i;			

		if (filterPanelString) {
			filterPanel = JSON.parse( filterPanelString );
			oldFiltersState = filterPanel[ stateId ] || {};
		}
			
		for (i = 0; i < this.filterControls.length; i++) {
			filter = this.filterControls[i];
			filtersState[ filter.fieldName ] = filter.isVisible() ? filter.isCollapsed() : oldFiltersState[ filter.fieldName ];
		}
			
		filterPanel[ stateId ] = filtersState;
		localStorage.setItem('filterPanel', JSON.stringify( filterPanel ));
	},
	
	/**
	 * Adjust the filtes' expander signs basing on the document.coockie data
	 */	
	adjustExpanderSigns: function() {
		if ( !this.filterControls.length ) {
			return;
		}

		var filtersStateString = localStorage.getItem('filterPanel'),
			filterPanel, filtersStateObj, i, isCollapsed;
		
		if ( filtersStateString ) {
			filterPanel = JSON.parse( filtersStateString );
			filtersStateObj = filterPanel['filtersState_' + this.shortTName + '_' + this.id];
		}
		
		if ( !filtersStateString || !filtersStateObj ) {
			// no localStorage data for a page
			for (i = 0; i < this.filterControls.length; i++) {
				this.filterControls[ i ].updateCollapsedState();
			}		
			return;
		}
			
		for (i = 0; i < this.filterControls.length; i++) {
			isCollapsed = filtersStateObj[ this.filterControls[i].fieldName ];
			
			this.filterControls[i].toggleCollapsed( isCollapsed );
			this.filterControls[i].adjustExpander( isCollapsed );
		}
	},
	
	/**
	 * Update some filters controls' values basing on data passed
	 * @param {object} parentFiltersData	The object's keys are parent filters' names,
	 * the object's values are the corresponding filters' values 
	 */
	updateParentFilters: function( parentFiltersData ) {
		var i, filter, filterValue;
		
		for (i = 0; i < this.filterControls.length; i++) {
			filter = this.filterControls[i];
			filterValue = parentFiltersData[ filter.fieldName ];
			
			if ( filterValue !== undefined ) {
				filter.updateValue( filterValue );
			}			
		}
	},	
	
	/**
	 * Clear controls' values for paticular filters
	 * @param {array} dependentFiltersNames		The array of filters controls names
	 * @param {boolean} notMultiSelected
	 */	
	clearDependentFilters: function( dependentFiltersNames, notMultiSelected ) {
		var i, filter;
		
		for (i = 0; i < this.filterControls.length; i++) {
			filter = this.filterControls[i];
			
			if ( filter.isDependent() && $.inArray( filter.fieldName, dependentFiltersNames) !== -1 && ( !notMultiSelected || !filter.isMultiSelected() ) ) {
				filter.clearControl(); 
			}
		}
	},
	
	/**
	 * Check if there is filtered one among the filters with the names passed
	 * @param {array} parentFiltersNames
	 * return boolean
	 */
	hasFilteredParentFilter: function( parentFiltersNames ) {
		var i, filter;
		for (i = 0; i < this.filterControls.length; i++) {
			filter = this.filterControls[i];
			if ( $.inArray( filter.fieldName, parentFiltersNames) !== -1 && filter.filtered ) {
				return true;
			}
		}
		
		return false;	
	}
});
/**
 * The SearchParamsLogger class provides the base search saving functionality.
 * Instances are strongly connected with the page's Search controller and
 * invoke its methods through the proxy searchControllerConnector
 * object without spoiling its state.
 */
Runner.search.SearchParamsLogger = Runner.extend( Runner.emptyFn, {
	/**
	 * The object that stores the saved searches' names
	 * and the corresponding search params data
	 */
	savedSearches: {},
	
	/**
	 * The jQuery element representing
	 * the 'Save search' button	
	 * @type {jQeury object}
	 */
	saveSearchButton: null,
	
	/**
	 * The jQuery element representing
	 * the 'Saved searches' button	
	 * @type {jQeury object}
	 */	
	savedSearchesButton: null,
	
	/**
	 * The proxy instance to provide a particular Search 
	 * controller object functionality without spoiling
	 * the page's search controller state
	 * @type {Object}
	 */
	searchControllerConnector: null,
	
	/**
	 * The page's id value
	 */
	id: -1,	
	
	/**
	 * The page's table name
	 */
	tName: "",
	
	/**	
	 * The page's type
	 */
	pageType: "",

	/**
	 * The name of the  Saved search link class
	 */
	savedSeachLinkClass: "savedSearch",
	
	/**
	 * The name of Delete saved search button 	
	 */
	deleteSearchBtnClass: "deleteSearch",
	
	
	constructor: function( cfg ) {
		Runner.apply( this, cfg );
	},
	
	/**
	 * The method returns the reference to itself
	 * to provide an instance with lazy init syntax	 
	 * @return {Object}
	 */
	init: function( searchController ) {
		this.initSearchControllerConntector( searchController );
		this.initSearchSavingButtons();
		this.initSavedSearchesList();
		
		return this;
	},

	/**
	 * Create the searchControllerConnector instance to provide a particular Search 
	 * controller object functionality without spoiling the page's search controller state.
	 * The Lazy Function Definition is used to prevent the connector creating code from
	 * being invoked more than once.
	 *
	 * @param {Object} searchController		The page's Search controller obejct reference
	 */
	initSearchControllerConntector: function( searchController ) {
		// define a search controller proxy object constructor
		var Connector = function() {
			/** 
			 * Get the search controller property value
			 * @param {String} propertyName
			 * @return {Mixed}
			 */
			this.getProperty = function( propertyName ) {
				return searchController[ propertyName ];
			}
			/** 
			 * Submit the saved search through the search controller 
			 * @param {Object}
			 */
			this.submitSavedSearch = function( searchParams ) {
				searchController.submitSavedSearch( searchParams );
			}
			/**
			 * Get a sting value of the first controller involved in the current search
			 * @return {String}
			 */
			this.getTheFirstSearchControlValue = function() {
				return searchController.getTheFirstSearchControlValue();
			}
			
			this.pageObj = searchController.pageObj;
		}
		
		this.searchControllerConnector = new Connector();
		this.initSearchControllerConntector = Runner.emptyFn;
	},	
	
	/**
	 * Init the 'Search saving' buttons
	 */
	initSearchSavingButtons: function() {
		var logger = this;
		
		// add click handler for the 'Search save' button
		this.saveSearchButton = $( "#saveSearch" + this.id )
			.off("click.slogger")
			.on("click.slogger", function(e) {
				if ( !Runner.isDisabledButton( this ) ) {
					Runner.addDisabledClass( this );
					logger.saveSearchClickHandler(e);	
				}			
				return false;
			});
		
		this.savedSearchesButton = $( "#savedSearches" + this.id )
			.on("click", function(e) {
				return false;
			});
	},	

	/**
	 * The 'Save search' button's jQuery click handler
	 * @param {jQeury Event object}
	 */	 
	saveSearchClickHandler: function(e) {
		if ( !e ) { 
			return;
		}
		
		var logger = this,
			searchNameId = "searchName" + this.id,
			searchName = this.getDefaultSearchName(), 
			args = {
				modal: true,
				centered: true,
				headerContent: Runner.lang.constants.SEARCH_SAVING,
				bodyContent: this.getSaveSearchDialogContent( searchName, searchNameId )
			};
		
		args.footerContent = '<a href="#" id="spButtonSave' + this.id + '" class="btn btn-primary">' + Runner.lang.constants.SAVE + '</a>'
			+ '<a href="#" id="spButtonCancel' + this.id + '" class="btn btn-default">' + Runner.lang.constants.CANCEL + '</a>';
		
		Runner.pages.PageManager.createBSWin( args, false, 
			function( win ) {
				var $modal = win._obj,
					$searchName = $("#" + searchNameId).trigger("focus");
				
				$("#spButtonSave" + logger.id).on("click", function(ev) {
					var searchName = $searchName.val();
		
					logger.saveSearch( searchName );
					$modal.modal("hide");
					return false;
				});
				
				$("#spButtonCancel" + logger.id).on("click", function(ev) {
					$modal.modal("hide");
					return false;						
				});				
			},
			function( win ) {
				var $modal = win._obj;
				Runner.delDisabledClass( logger.saveSearchButton );
			});
		
		return;
	},	

	/**
	 * Save the current search under a particular name
	 * @param {String} searchName
	 */
	saveSearch: function( searchName ) {		
		var	logger = this,
			ajaxURL = Runner.pages.getUrl( this.tName, this.pageType ),
			ajaxParams = {
				saveSearch: true,
				searchName:	searchName
			};
			
		Runner.runnerAJAX( ajaxURL, ajaxParams, function( params ) {
			if ( !logger.savedSearches[ searchName ] ) {
				logger.addSearchToSavedSearchesList( searchName );
			}
			logger.savedSearches[ searchName ] = params
			logger.searchControllerConnector.pageObj.toggleItemType( "saved_searches", true );
			
			Runner.pages.PageManager.getById( logger.id ).updateDropdownButtons();
		});
		
		logger.searchControllerConnector.pageObj.toggleItemType( "save_search", false );
	},
	
	/**
	 * Get the unique name for the saving search
	 * @return {String}
	 */
	getDefaultSearchName: function() {
		var searchName = this.searchControllerConnector.getTheFirstSearchControlValue(),
			re = /(.+_)(\d+)$/,
			matches; 
		
		while ( this.savedSearches[ searchName ] ) {
			matches = re.exec( searchName );
			
			if ( matches ) {
				searchName = matches[1] + ( parseInt( matches[2], 10 ) + 1 );
			} else {
				searchName += '_1'; 
			}
		}
		
		return searchName;
	},

	/**
	 * Get the Search save dialog html markup
	 * @param {String} searchName
	 * @param {String} searchNameId
	 * @return {String} markup
	 */
	getSaveSearchDialogContent: function( searchName, searchNameId ) {
		return '<div class="rnr-searchname">'
				+ '<label for="searchName">' + Runner.lang.constants.SEARCH_NAME + '&nbsp</label>'
				+ '<input id="' + searchNameId + '" type="text" value="' + searchName + '" name="searchName">'
			+ '</div>';
	},
	
	/** 
	 * Create the saved searches list's markup
	 */	 
	initSavedSearchesList: function() {
		var searchName,
			logger = this;
			
		this.savedSearchesList = $("#savedSearchedList" + this.id );	
		
		for ( searchName in this.savedSearches ) {
			if ( this.savedSearches.hasOwnProperty( searchName ) ) {
				this.addSearchToSavedSearchesList( searchName );
			}
		}
		
		this.savedSearchesList
				.on('click', '.' + this.savedSeachLinkClass, function(e) {
				var searchName = $(this).parent('li').data('search'),
					searchParams = logger.savedSearches[ searchName ];
				
				logger.searchControllerConnector.submitSavedSearch( searchParams ); 
				return false;
			})
			.on('click', '.' + this.deleteSearchBtnClass, function() {
				var deleteSearch = $(this),
					searchName;
					
				if ( deleteSearch.hasClass('disabled') ) {
					return;
				}				
				
				searchName = deleteSearch.parent('li').data('search');
				logger.deleteSavedSearch( searchName );
				deleteSearch.addClass('disabled');
				return false;
			});
			
		//Runner.setZindexMaxToElem( this.savedSearchesList );	
	},
	
	/** 
	 * Get the saves searches list item's markup
	 * @param {String} searchName
	 */ 
	addSearchToSavedSearchesList: function( searchName ) {
		var item = $('<li></li>')
				.data('search', searchName)
				.appendTo( this.savedSearchesList );
			
		$('<a href="#"></a>')
			// safari + ios fix
			.on("touchend", function() { 
				$(this).trigger("click")
			})
			.addClass( this.deleteSearchBtnClass )
			.append( '<span class="glyphicon glyphicon-remove"></span>')
			.appendTo( item )
			.attr( 'title', Runner.lang.constants.DELETE_SEARCH_CAPTION );
		
		$('<a href="#"></a>')
			// safari + ios fix
			.on("touchend", function() { 
				$(this).trigger("click")
			})		
			.addClass( this.savedSeachLinkClass )
			.html( searchName )
			.appendTo( item );	
	},
	
	/**
	 * Delete a saved search with a particular name
	 * @param {String} searchName
	 */
	deleteSavedSearch: function( searchName ) {	
		if ( !this.savedSearches[ searchName ] ) {
			return;
		}
		
		var	logger = this,
			args = {
				modal: true,
				centered: true,
				headerContent: Runner.lang.constants.DELETE_SEARCH_CAPTION,
				bodyContent: this.getDeleteSearchDialogContent()
			};

		args.footerContent = '<a href="#" id="spButtonYes' + this.id + '" class="btn btn-primary">' + Runner.lang.constants.YES + '</a>'
			+ '<a href="#" id="spButtonNo' + this.id + '" class="btn btn-default">' + Runner.lang.constants.NO + '</a>';
		
		Runner.pages.PageManager.createBSWin( args, false, 
			function( win ) {
				var $modal = win._obj;
				
				$("#spButtonYes" + logger.id).on("click", function(ev) {	
					var ajaxURL = Runner.pages.getUrl( logger.tName, logger.pageType ),
						ajaxParams = {
							deleteSearch: true,
							searchName:	searchName
						};
						
					Runner.runnerAJAX( ajaxURL, ajaxParams, Runner.emptyFn );	
					
					logger.removeSavedSearchesListItem( searchName );
					delete logger.savedSearches[ searchName ];
					
					if ( $.isEmptyObject( logger.savedSearches ) ) {
						Runner.pages.PageManager.getById( logger.id ).toggleItemType( 'saved_searches', false );
						Runner.pages.PageManager.getById( logger.id ).toggleItemType( 'save_search', true );
						Runner.pages.PageManager.getById( logger.id ).updateDropdownButtons();
					}
					
					$modal.modal("hide");
					return false;	
				});
				
				$("#spButtonNo" + logger.id).on("click", function(ev) {
					$modal.modal("hide");
					return false;
				});				
			},
			function( win ) {
				var $modal = win._obj;
				
				logger.getSavedSearchesListItem( searchName )
					.children('.' + logger.deleteSearchBtnClass)
				.removeClass('disabled');
			}
		);
	},
	
	/**
	 * Get the Delete saved search dialog html markup
	 * @return {String} markup
	 */	
	getDeleteSearchDialogContent: function() {
		return '<div class="rnr-delete-search">'
					+ Runner.lang.constants.DELETE_SEARCH
			+ '</div>';
	},
	
	/**
	 * Remove the saved searches list's item with a particular search name
	 * @param {String} searchName
	 */
	removeSavedSearchesListItem: function( searchName ) {
		 this.getSavedSearchesListItem( searchName ).remove();
	},
	
	/**
	 * Get the saved searches list's item by its search name
	 * @param {String} searchName
	 * @return {jQuery object}
	 */
	getSavedSearchesListItem: function( searchName ) {
		return $('li', this.savedSearchesList).filter( function() {
					return $(this).data('search') === searchName;
				});
	}
});Runner.namespace('Runner.form');

Runner.form.BasicForm = Runner.extend( Runner.util.Observable, {
	
	id: -1,

	/**
	 * @type {object}
	 */
	baseParams: null,	
	
	isFileUpload: false,
	
	/**
	 * A flag indicating if to submit a form element
	 * without using iframe
	 * @type {boolean}
	 */
	standardSubmit: false,
	
	/**
	 * @type {object}
	 */
	ajaxForm: null,
	
	/**
	 * @type {object}
	 */	
	searchForm: null,
	
	isSearchForm: false,

	searchSubmit: false,
	
	/**
	 * An array objects created with Runner.controls.ControlFactory method
	 * @type {array}
	 */
	fieldControls: null,
	
	/**
	 * An array of DOM element (or jQuery object)
	 * that will be added to the form element
	 * @ytpe {array}
	 */
	addElems: null,
	
	/**
	 * The form element	
	 * @type {jQuery object}
	 */
	formEl: null,

	/**
	 * The form's iframe io element	
	 * @type {jQuery object}
	 */
	ioEl: null,
	
	/**
	 * The form's iframe id
	 * @type {string}
	 */
	ioElId: '',
	
	/**
	 * The request URL
	 * @type {string}
	 */
	submitUrl: '',
	
	/**
	 * The form method attribute value
	 * @type {string}
	 */
	method: 'POST',
	
	/**
	 * The form element's target attribute value
	 * @type {string}
	 */
	target: '',	
	
	/**
	 * A flag indicating if to add to request url
	 * a random value param to prevent caching
	 * @type {boolean}
	 */
	addRndVal: true,
	
	/**
	 * A flag indicating if to add 'enctype' attribute 
	 * with 'multipart/form-data' value to the form
	 * @type {boolean}
	 */
	useMultipart: false,
	
	/**
	 * A flag indicating if to destroy the form and iframe elements
	 * and unregister fields controls after submit
	 * @type {boolean}
	 */
	deleteAfterSubmit: false,
	
	/**
	 * @type {boolean}
	 */
	initImmediately: false,
	
	/**
	 * @type {boolean}
	 */
	isFormReady: false,	
	
	/**
	 * Set focus on the first invalid control or not
	 * Need for Save All inline Add/Edit grid's records
	 * @type {boolean}
	 */
	setFocus: true,
	
	/**
	 * track user activity before submit
	 */
	trackingUserActivity: true,
	
	
	/**
	 * @constructor
	 */
	constructor: function( cfg ) {
		this.addElems = [];
		this.ajaxForm = {};
		this.baseParams = {};
		this.fieldControls = [];

		Runner.apply(this, cfg);
		
		this.addEvents('beforeSubmit', 'successSubmit', 'submitFailed', 'validationFailed', 'formSent');	
		
		if ( this.formSent ) {
			this.on( {'formSent': this.formSent} );
		}
		
		if ( this.beforeSubmit ) {
			this.on( {'beforeSubmit': this.beforeSubmit} );
		}
		
		if ( this.successSubmit ) {
			this.on( {'successSubmit': this.successSubmit} );
		}
		
		if ( this.submitFailed ) {
			this.on( {'submitFailed': this.submitFailed} );
		}
		
		if ( this.validationFailed ) {
			this.on( {'validationFailed': this.validationFailed} );
		}
		
		Runner.form.BasicForm.superclass.constructor.call( this, cfg );
		
		if ( this.initImmediately ) {
			this.initForm();
		}
	},
	
	/**
	 * Destroy the form element
	 * @param {boolean} destroyControls
	 */
	destructor: function( destroyControls ) {
		if ( destroyControls === true ) {
			for (var i = 0; i < this.fieldControls.length; i++) {
				this.fieldControls[i].unregister();
			}
		}
		
		if ( this.ioEl && this.ioEl.length ) {
			var $iframe = this.ioEl;
			setTimeout( function() {
				$iframe.remove();
			}, 0);
		}
		
		if ( this.formEl && this.formEl.length ) {
			this.formEl.remove();
		}
	},
	
	/**
	 * Submit the form
	 * @return {boolean}
	 */
	submit: function() {
		this.triedToSubmit = true;
		
		// to control user activity
		this.trackingUserActivity 
			&& Runner.trackUserActivity();
		
		var beforeSubmitRes = this.fireEvent('beforeSubmit', this),
			formObj = this;
		
		if ( beforeSubmitRes === false || !this.validate() ) {
			return false;
		}
		
		if ( this.isFileUpload || this.standardSubmit ) {
			if ( this.searchSubmit && Runner.charSet == "utf-8" ) {
				var queryStr, i,
					queryParts = [],
					vars = {}, 
					paramsStr = window.location.href,
					paramNames = ['rname', 'cname'],
					param;
				
				$.each( this.searchForm, function( formParameter, parameterValue ) {
					queryParts.push( formParameter + '=' + parameterValue );
				});
				
				if ( paramsStr.substr(paramsStr.length - 1) == '#' ) {
					paramsStr = paramsStr.substr(0, paramsStr.length - 1);
				}	
				paramsStr.replace(/[?&]+([^=&]+)=([^&]*)/gi, function( m, key, value ) {
					vars[ key ] = value;
				});
				
				for (i = 0; i < paramNames.length; i++) {
					if ( vars[ paramNames[i] ] ) {
						queryParts.push( paramNames[i] + '=' + encodeURIComponent( vars[ paramNames[i] ] ) );
					}	
				}

				for ( param in this.baseParams ) {
					queryParts.push( param + '=' + encodeURIComponent( this.baseParams[ param ] ) );
				}
				
				queryStr = queryParts.join('&');
				queryStr = this.submitUrl + ( queryStr ? '?' + queryStr : queryStr );
				
				if ( queryStr.length < 2040 ) {
					location.href = queryStr;
					return true;
				} 
				
				this.formEl.attr('method', 'POST');
			}
			
			this.initForm();
			this.addFormSubmit();
			this.formEl.trigger("submit");
		} else {
			this.addFormSubmit();

			Runner.runnerAJAX(this.submitUrl, this.ajaxForm, function( respObj ) {
				formObj.fireEvent("successSubmit", respObj, formObj, formObj.fieldControls);
			});
		}
		
		formObj.fireEvent("formSent", formObj, formObj.fieldControls);
		return true;
	},
	
	/**
	 * Initialize the form element
	 */
	initForm: function() {
		if ( this.isFormReady ) {
			return;
		}
		
		if ( this.isFileUpload && !this.standardSubmit ) {
			this.createIFrame();
			this.createForm();
		} else if ( this.standardSubmit ) {
			this.createForm();
		}
		
		this.isFormReady = true;
	},
	
	/**
	 * Clear the form element
	 */
	clearForm: function() {
		if ( this.formEl && this.formEl.length ) {
			this.formEl.children().remove();
		}
		this.ajaxForm = {};
		this.searchForm = {};
	},

	/**
	 * Add submited elements to the form
	 */	 
	addFormSubmit: function() {
		var arrClns, i, j, param;
		
		if ( this.isFormReady && !this.isSearchForm ) {
			this.clearForm();
		}
		
		if ( this.addRndVal ) {
			this.baseParams[ "rndVal" ] = Math.random();
		}
		
		if ( this.formEl && this.formEl.length ) {
			for (i = 0; i < this.fieldControls.length; i++) {
				arrClns = this.fieldControls[i].getForSubmit();
				
				for (j = 0; j < arrClns.length; j++) { 
					$( arrClns[j] ).appendTo( this.formEl );
				}
			}
			
			for (param in this.baseParams) {
				this.addToForm( param, this.baseParams[ param ] );
			}
			
			for (i = 0; i < this.addElems.length; i++) {
				$( this.addElems[i] ).appendTo( this.formEl );
			}
			return;
		} 
		
		this.ajaxForm = Runner.apply( this.ajaxForm, this.baseParams );
		
		for (i = 0; i < this.fieldControls.length; i++) {
			this.ajaxForm[ this.fieldControls[i].fieldName ] = this.fieldControls[i].getUserValue(); 
		}
		
		for (i = 0; i < this.addElems.length; i++) {
			$( this.addElems[i] ).appendTo( this.formEl );			
			this.addToForm( this.addElems[i].attr("id") || this.addElems[i].attr("name"), this.addElems[i].val() );
		}
	},
	
	/**
	 * Add param to form elements
	 * @param {string} id
	 * @param {string} val	 
	 */
	addToForm: function( id, val ) {
		var i;
		
		if ( val === undefined || id === undefined || val === null || id === null ) {
			return;
		}
		
		if ( Runner.isArray( val ) ) {
			for ( i = 0; i < val.length; ++i ) {
				this.addToForm( id, val[i] );
			}
			return;
		}
		
		if ( this.isFileUpload || this.standardSubmit ) {
			if ( !this.formEl ) { 
				this.initForm();
			}
			$('<input id="' + id + '" name="' + id + '" type="hidden"></input>')
				.val( val.toString() )
				.appendTo( this.formEl );

			if ( this.searchSubmit ) {
				this.ajaxForm[ id ] = val;
			}
		} else {
			this.ajaxForm[ id ] = val;
		}
	},
	
	/**
	 * Add param to the search form
	 * @param {string} id
	 * @param {string} val
	 */
	addToSearchForm: function( id, val ) {
		if ( val !== undefined && id !== undefined && val !== null & id !== null ) {
			this.searchForm[ id ] = val;
		}
	},
	
	/**
	 * Validate form controls
	 * @param {array} controlsNames (optional)
	 * @return {boolean}
	 */
	validate: function( controlsNames ) {
		var invalidControls, controlsToValidate;
			
		if ( controlsNames ) {
			// validate only some controls
			controlsToValidate = [];
			
			$.each( this.fieldControls, function( idx, control ) {
				if ( $.inArray( control.fieldName, controlsNames ) !== -1 ) {
					controlsToValidate.push( control );
				}
			});
		} else {
			controlsToValidate = this.fieldControls;
		}

		invalidControls = Runner.pages.RunnerPage.prototype.getInvalidControls.call( this, controlsToValidate );
		
		
		if ( invalidControls.length ) {
			if ( invalidControls[0].pageObj.pageType == Runner.pages.constants.PAGE_LIST ) {
				$.each( invalidControls, function( i, ctrl ) {
					// show columns if hidden by 'hide column control'
					var hiddenColumnName = ctrl.spanContElem.closest('[data-field]').data('field');
					if ( hiddenColumnName ) {
						ctrl.pageObj.showHiddenColumn( hiddenColumnName );
					}
				});
			}

			if ( this.setFocus ) {
				Runner.pages.RunnerPage.prototype.setFirstFocus.call( this, this.id, invalidControls );
			}
			
			this.fireEvent("validationFailed", this, controlsToValidate, invalidControls);
			return false;
		}
		
		return true;
	},
	
	/**
	 * Create the form's iframe io element
	 */
	createIFrame: function() {
		if ( this.ioEl ) {
			return false;
		}
		var frameId = 'uploadFrame_' + this.id,
			basicForm = this,
			$iframe = $('<iframe id="' + frameId + '" name="' + frameId + '"/>')
				.css({
					border: '0', 
					height: '1px', 
					width: '1px',
					display: 'none'
				})
				.appendTo('body');
		
		$iframe.on('load', function(e) {
			var bodyElem = $(this).contents().find('body').get(0),
				response, responseObj,
				rawResponse = bodyElem.innerHTML;
				
			if ( rawResponse !== '' ) {
				try {
					response = $('<div>' + rawResponse + '</div>').text();
					responseObj = JSON.parse(response);
				} catch(e) {
					// if response contain PHP error
					if ( !basicForm.isEventExist( 'submitFailed' ) ) {
						//	display generic error message
						rawResponse = $(this).contents().find('head').html() + rawResponse;
						Runner.displayGenericAjaxError( rawResponse );
					}
					
					basicForm.fireEvent('submitFailed', rawResponse, basicForm, basicForm.fieldControls);
					
					if ( basicForm.deleteAfterSubmit ) {
						basicForm.destructor(true);
					}
					return;
				}
				
				basicForm.fireEvent('successSubmit', responseObj, basicForm, basicForm.fieldControls);
			} else {
				basicForm.fireEvent('submitFailed', rawResponse, basicForm, basicForm.fieldControls);
			}
			
			if ( basicForm.deleteAfterSubmit ) {
				basicForm.destructor(true);
			}
		});
		
		this.ioEl = $iframe;
		this.ioElId = frameId;	
	},
	
	/**
	 * Create the form element
	 */
	createForm: function() {
		if ( this.formEl ) {
			return false;
		}
		
		var $form =  $('<form/>')
			.attr({
				action: this.submitUrl,
				method: this.method
			})
			.css('display', 'none')
			.appendTo('body');
				
		if ( this.target ) {
			$form.attr('target', this.target);
		}
		if ( this.ioEl ) {
			$form.attr('target', this.ioElId);
		}
		
		if ( this.isFileUpload || this.useMultipart ) {
			$form.attr('enctype', 'multipart/form-data');
		}
		
		this.formEl = $form;			
	},
	
	/**
	 * Set all the Runner controls added to the form to the read-only state
	 */
	makeReadonly: function() {
		for (var i = 0; i < this.fieldControls.length; i++) {
			this.fieldControls[ i ].makeReadonly();
		}
	},
	
	/**
	 * Set all the Runner controls added to the form to the normal (no read-only) state
	 */
	makeReadWrite: function() {
		for (var i = 0; i < this.fieldControls.length; i++) {
			this.fieldControls[ i ].makeReadWrite();
		}
	}
});

/**
 * Runner.submitRunnerFormInIframe( submitUrl, { baseParams: baseParams }, succesHandler ) ;
 *
 * @param {string} submitUrl
 * @param {object} baseParams
 * @param {function} succesHandler
 * @param {boolean} skipTrackingActivity
 */
Runner.runnerAJAX = function( submitUrl, baseParams, succesHandler, skipTrackingActivity ) {
	if ( baseParams && baseParams.__method__ === 'GET' ) {	
		var urlParams = Object.keys( baseParams )	
			.filter( function( k, idx ) {
				return typeof baseParams[ k ] !== 'object' && k !== 'method';
			})
			.map( function( k, idx ) {
				return k + "=" + encodeURIComponent( baseParams[ k ] );
			})
			.join( "&" );		
		
		submitUrl += ( submitUrl.split('?')[1] ? '&' : '?' ) + urlParams;	
	}
	
	var basicForm = new Runner.form.BasicForm({
		id: Runner.genId(),	
		standardSubmit: false,
		isFileUpload: true,
		submitUrl: submitUrl,
		method: baseParams.__method__ || 'POST',
		baseParams: baseParams || {},
		deleteAfterSubmit: true,
		successSubmit: {
			fn: succesHandler,
			scope: window
		},
		trackingUserActivity: !skipTrackingActivity,
	});
	
	delete baseParams.__method__
	basicForm.submit();
};

/**
 * @param {string} submitUrl
 * @param {object} cfgParams
 * @param {function} succesSubmitHandler
 * @param {function} submitFailedHandler
 * @return {Runner.form.BasicForm object}
 */
Runner.submitRunnerFormInIframe = function( submitUrl, cfgParams, succesSubmitHandler, submitFailedHandler ) {
	var basicForm,
		formParams = {
			method: 'POST',
			id: Runner.genId(),	
			standardSubmit: false,
			isFileUpload: true,
			submitUrl: submitUrl,
			deleteAfterSubmit: true
		};
	
	if ( cfgParams.baseParams && typeof cfgParams.baseParams === "object" ) {
		formParams.baseParams = cfgParams.baseParams
	}
	
	if ( Runner.isArray( cfgParams.addElems ) ) {
		formParams.addElems = cfgParams.addElems;
	}

	if ( typeof succesSubmitHandler === "function" ) {
		formParams.successSubmit = {
			fn: succesSubmitHandler,
			scope: window
		}
	}	
	
	if ( typeof submitFailedHandler === "function" ) {
		formParams.submitFailed = {
			fn: submitFailedHandler,
			scope: window			
		}
	}
	
	basicForm = new Runner.form.BasicForm( formParams );
	basicForm.submit();
}// create namespace
Runner.namespace('Runner.util.details');

/**
 * Base abstract class for details preview
 * provides base functionality and event handling
 * @class Runner.util.details.DP
 */
Runner.util.details.DP = Runner.extend( Runner.util.Observable, {
	/**
	 * Detail data source table name
	 * @type {string}
	 */
	tName: "",
	/**
	 * Detail short table name
	 * @type {string}
	 */
	shortTName: "",
	/**
	 * Master data source table name
	 * @type {string}
	 */
	masterTName: "",
	/**
	 * Master short table name
	 * @type {string}
	 */
	masterShortTName: "",

	masterPageType: "",


	/**
	 * Ajax request url
	 * @type {string}
	 */
	ajaxRequestUrl: "",

	/**
	 * Detail page type
	 * @type {string}
	 */
	pageType: "",

	/**
	 * A property indicating how to display a detail page
	 * ('in popup', 'inline')
	 */
	dpShowType: Runner.pages.constants.DP_NONE,

	/**
	 * Detail page id
	 * @type {string}
	 */
	id: 0,

	/**
	 * Parent page id
	 * @type {string}
	 */
	parId: 1,

	/**
	 * The indicator showing if the layout
	 * of a new or old version is used
	 * @type {boolean}
	 */
	oldLayout: false,

	/**
	 * The layout's main class prefix
	 * @type {string}
	 */
	classBasePrefix: "",


	constructor: function( cfg ) {
		Runner.apply( this, cfg );
		Runner.util.details.DP.superclass.constructor.call( this, cfg );

		this.shortTName = Runner.pages.PageSettings.getShortTName( this.tName );
		this.masterShortTName = Runner.pages.PageSettings.getShortTName( this.masterTName );

		this.ajaxRequestUrl = Runner.getPageUrl( this.shortTName, this.pageType );
		this.classBasePrefix = this.oldLayout ? "runner-" : "rnr-";

		this.parPageObj = Runner.pages.PageManager.getById( this.parId );
		if ( this.parPageObj ) {
			this.masterPageType = this.parPageObj.pageType;
		}
	},

	init: Runner.emptyFn
});

/**
 * @class Runner.util.details.InlineDP
 * Provides base functionality for inline details
 */
Runner.util.details.InlineDP = Runner.extend( Runner.util.details.DP, {
	/**
	 * Current row object
	 * @type {array}
	 */
	rows: null,


	/**
	 * Hide child link or not
	 * @type {boolean}
	 */
	hideChild: false,

	/**
	 * A reference to the parent page's object
	 * @type {object}
	 */
	parPageObj: null,

	/**
	 * The host page's context
	 * @type {Mixed}
	 */
	pageContext: null,

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		this.rows = [];

		Runner.util.details.InlineDP.superclass.constructor.call( this, cfg );

		this.addEvents("beforeShow", "afterShow");

	},

	/**
	 *
	 */
	init: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.initRow( this.rows[i] );
		}
	},


	/**
	 * @param {object} row
	 * @param {boolean} setHref
	 */
	initRow: function( row, setHref ) {
		if ( !row.row || !row.row.length ) {
			row.row = $("[id=gridRow" + row.id + "]", this.pageContext);

			if ( !row.row.length ) {
				row.row = $("#master_" + this.shortTName + row.id, this.pageContext);
			}
			if ( !row.row.length ) {
				row.row = $("#" + this.shortTName + "_preview" + row.id, this.pageContext).closest('tr[id^=gridRow]');
			}
			if ( !row.row.length ) {
				row.row = $("#details_" + row.id, this.pageContext).closest('tr[id^=gridRow]');
			}

			if ( !row.row.length ) {
				row.row = $(".bs-grid-vertical-cell[data-record-id='" + row.id + "']", this.pageContext).closest('div[id^=gridRow]');
			}
		}

		if ( this.linksType == Runner.pages.constants.DL_INDIVIDUAL || this.linksType == Runner.pages.constants.DL_SINGLE
			|| this.tabSettings.tabIndex == 0 ) {
			this.initLink( row, setHref );
		}
	},


	/**
	 * A stub
	 * @param {object} row
	 * @param {boolean} setHref set attr href or not
	 */
	initLink: function( row, setHref ) {
	},

	/**
	 * Add new master row, added with inlineAdd
	 * @param {object} new added row
	 */
	addRow: function( row ) {
		row.rowInd = this.rows.length;
		this.rows.push( row );

		// init new added row
		this.initRow( row, this.detailsHrefAvailable );
		// set correct det num
		this.setChildRecNum( row, this.getChildCountBySameKeys( row ) );
	},

	/**
	 * @param {object} row
	 * @return {string}
	 */
	getChildCountBySameKeys: function( row ) {
		var mKeysAsString = JSON.stringify( row.masterKeys[ this.tName ] ),
			i;

		for ( i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[ i ].id != row.id && mKeysAsString == JSON.stringify( this.rows[ i ].masterKeys ) ) {
				return this.rows[ i ].childNum;
			}
		}

		return "0";
	},

	/**
	 * Update the number of details displayed in the main table's 'details' link
	 * @param {object} masterKeys
	 * @param {number} rowId
	 */
	updateChildRecNum: function( masterKeys, rowId ) {
		var dp = this;

		if( !this.hideChild && !this.dispChildCount ) {
			if( dp.linksType == Runner.pages.constants.DL_SINGLE ) {
				//	show 'all details' link
				dp.updateAllLinkVisibility( rowId, 1 );
			}
			return;
		}

		var mKeys = Object.keys( masterKeys ).sort()
			.map( function( key ) {
				return masterKeys[ key ] } );

		var reqParams = {
			mKeys: JSON.stringify( mKeys ),
			dTable: this.tName,
			dSTable: this.shortTName,
			pageType: this.parPageObj.pageType,
			mTable: this.masterTName,
			mSTable: this.masterShortTName
		}

		Runner.runnerAJAX( Runner.getPageUrl("detreccount"), reqParams, function( respObj ) {
			if ( !respObj.success ) {
				return;
			}

			if ( dp.hideChild && dp.linksType == Runner.pages.constants.DL_SINGLE ) {
				dp.updateAllLinkVisibility( rowId, parseInt( respObj.recsCount, 10 ) );
			}

			if ( rowId != undefined ) {
				dp.updateChildRecNumById( masterKeys, respObj.recsCount, rowId );
			} else {
				dp.updateChildRecNumByKeys( masterKeys, respObj.recsCount );
			}

			dp.updatePreviewVisibility( rowId, respObj.recsCount, masterKeys );
		});
	},

	updateAllLinkVisibility: function( rowId, count ) {
		$("#details_" + rowId).toggle( !!count );
	},

	updatePreviewVisibility: function( rowId, detNum, masterKeys ) {
	},

	/**
	 * @param {object} masterKeys
	 * @param {string} detNum
	 * @param {number} rowId
	 */
	updateChildRecNumById: function( masterKeys, detNum, rowId ) {
		for ( var i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[ i ].id == rowId ) {
				this.setChildRecNum( this.rows[i], detNum );
			}
		}
	},

	/**
	 * @param {object} masterKeys
	 * @param {string} detNum
	 */
	updateChildRecNumByKeys: function( masterKeys, detNum ) {
		var mKeysAsString = JSON.stringify( masterKeys ),
			i;

		for ( i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[ i ].masterKeys && mKeysAsString == JSON.stringify( this.rows[ i ].masterKeys[ this.tName ] ) ) {
				this.setChildRecNum( this.rows[ i ], detNum );
			}
		}
	},

	setPDChildRecNum: function( row, detNum ) {
		var $detNumberSpan = $("#cntDet_" + this.shortTName + "_" + row.id),
			numberOfDetails = parseInt( detNum, 10 ),
			countText = "";

		if ( $detNumberSpan.length ) {
			if ( numberOfDetails > 0 ) {
				$detNumberSpan.show();
			}
			if ( !this.hideChild || numberOfDetails > 0 ) {
				$detNumberSpan.parent().show();
			}

			if ( $detNumberSpan.closest(".details-badge").length ) {
				$detNumberSpan.html( numberOfDetails ).parent().toggleClass( "hidden-badge", this.hideChild && !numberOfDetails );
			} else {
				$detNumberSpan.html( "(" + numberOfDetails + ")"  );
			}

			return;
		}

		if( row.row ) {
			row.row.find('#details_' + row.id + '_' + this.shortTName).toggle( !this.hideChild || numberOfDetails > 0 );
		}
	},

	/**
	 * Update the 'details' link appearance and the
	 * number of details displayed within this link
	 * @param {object} row
	 * @param {string} detNum
	 */
	setChildRecNum: function( row, detNum ) {
		this.setPDChildRecNum( row, detNum );
	},

	/**
	 * @param {number} ind
	 * @return {object}
	 */
	getRowByInd: function( ind ) {
		for ( var i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[ i ].rowInd == ind ) {
				return this.rows[ i ];
			}
		}

		return null;
	},

	/**
	 * @param {number} id
	 * @return {object}
	 */
	getRowById: function( id ) {
		for ( var i = 0; i < this.rows.length; i++ ) {
			if ( this.rows[ i ].id == id ) {
				return this.rows[ i ];
			}
		}

		return null;
	},

	/**
	 * A stub
	 */
	closeDetailsById: function( rowId ) {
	}
})

/**
 * @class Runner.util.details.DPPopUp
 * provides base functionality for popup details on list page
 */
Runner.util.details.DPPopUp = Runner.extend( Runner.util.details.InlineDP, {

	timeout: null,

	masterDetails: null,

	/**
	 * Previously handled link
	 */
	prevLink: null,

	/**
	 *
	 */
	constructor: function( cfg ) {
		Runner.util.details.DPPopUp.superclass.constructor.call( this, cfg );

		this.dpShowType = Runner.pages.constants.DP_POPUP;
		this.masterDetails = {
			show: false,
			flag: "",
			counter: 0
		};
	},

	getUrl: function( page ) {
		return Runner.getPageUrl( this.shortTName,
			page === Runner.pages.constants.PAGE_LIST
				? 'detailspreview'
				: page
		);
	},


	/**
	 * Set event handler for detail preview links
	 * @param {object} row
	 * @param {boolean} setHref set attr href or not
	 */
	initLink: function( row, setHref ) {
		var href, mKeys, key;

		if ( setHref === true ) {
			mKeys = [];
			for (key in row.masterKeys[ this.tName ]) {
				mKeys.push( row.masterKeys[ this.tName ][ key ] );
			}

			href = Runner.pages.getUrl( this.tName, this.pageType, mKeys, "masterkey" ) + "&mastertable=" + encodeURIComponent(this.masterTName);
			$( "[id=details_"  + row.id + "_" + this.shortTName + "]" ).attr('href', href);
		}
	},

	/**
	 * showPopup
	 * Show detail preview in popup
	 * @param {object} link 	details link
	 * @param {string} url		request url
	 * @param {string} mastertable
	 * @param {object} masterKeys
	 */
	showPopup: function( link, url, mastertable, masterKeys ) {
		var urlsDiff = false,
			preview = this,
			//cash the jQuery object
			$detailsPopupEl = $("." + this.classBasePrefix + "details-popup");

		if ( url && url !== $detailsPopupEl.data("url") ) {
			urlsDiff = true;
			$detailsPopupEl.data("url", url);
		}

		clearTimeout( $detailsPopupEl.data("timeout") );

		if ( $detailsPopupEl.css("display") == 'none' || (url != undefined && this.prevLink != link) || urlsDiff ) {
			this.timeout = setTimeout( function() {
				preview.prevLink = link;
				preview.masterDetails.flag = url;
				preview.masterDetails.show = true;
				preview.masterDetails.counter++;

				var params = Runner.apply({
					counter: preview.masterDetails.counter,
					mastertable: mastertable,
					mode: 'listdetailspopup',
					page: preview.previewPageId,
					id: Runner.genId()
				}, masterKeys);

				params.__method__ = 'GET';
				Runner.runnerAJAX( url, params,
					function( respObj ) {
						var detailsPopup, linkPosition;

						if (!respObj.success || !preview.masterDetails.show) {
							return;
						}

						//cash the jQuery object
						detailsPopup = $("." + preview.classBasePrefix + "details-popup");
						if ( !detailsPopup.length ) {
							detailsPopup = $('<div class="' + preview.classBasePrefix
								+ 'details-popup" style="display:none; position: absolute; left: 0px; top: 0px;"> </div>')
									.appendTo('body');
						}

						detailsPopup
							.on( 'mouseenter', function() {
								preview.showPopup();
							})
							.on( 'mouseleave', function(e) {
								// check target is not more text link
								if ( !$( e.target ).is("a[data-query]") ) {
									preview.hidePopup();
								}
							});

						if ( preview.masterDetails.counter == respObj.counter ) {
							detailsPopup.html( respObj.body );
							if ( respObj.CSSFiles ) {
								detailsPopup.addClass("page-detailspreview " + preview.classBasePrefix + "pagewrapper ");
								Runner.util.ScriptLoader.loadCSS( respObj.CSSFiles );
							}

							// delegate more click to parent page object
							preview.parPageObj.initMoreText( detailsPopup );

							if ( preview.pageType === Runner.pages.constants.PAGE_CHART ) {
								Runner.util.ScriptLoader.addJS( [ window.settings.global["webRootPath"] + 'libs/js/anychart-base.min.js' ] );
								
								Runner.util.ScriptLoader.loadCSS( [ window.settings.global["webRootPath"] + 'libs/js/anychart-ui.min.css' ] );
								Runner.util.ScriptLoader.loadCSS( [ window.settings.global["webRootPath"] + 'libs/js/anychart-font.min.css' ] );
//								Runner.util.ScriptLoader.addJS( [ window.settings.global["webRootPath"] + 'libs/js/migrationTool.js' ], window.settings.global["webRootPath"] + 'libs/js/anychart.min.js' );

								Runner.util.ScriptLoader.onFilesLoaded( function() {
									Runner.Charts.createChart( $('[data-runner-chart-params]', detailsPopup).data('runner-chart-params') );
								}, preview );

								Runner.util.ScriptLoader.load();
							}
						}

						linkPosition = $(link).offset();

						detailsPopup.css({
							top: linkPosition.top + "px",
							left: linkPosition.left + ( Runner.isDirRTL() ? -detailsPopup.outerWidth() : $(link).outerWidth() ) + "px",
							display: ''
						});
					}
				);
			}, 100);
			$("." + this.classBasePrefix + "details-popup").data("timeout", this.timeout);
		}
	},

	/**
	 * hidePopup
	 * Hide popup with details preview
	 */
	hidePopup: function() {
		var detailPopup = $('.' + this.classBasePrefix + 'details-popup');
		this.masterDetails.show = false;

		if ( detailPopup.css("display") == 'none' || !detailPopup.length ) {
			clearTimeout( detailPopup.data("timeout") );
			return;
		}

		this.timeout = setTimeout( function() {
			detailPopup.hide();
			detailPopup.html("");
		}, 200);
		detailPopup.data("timeout", this.timeout);
		$(".runner-details-popup").data("timeout", this.timeout);
	}
});

/**
 * @class Runner.util.details.recDP
 * The base class for the details preview list pages
 * hosted on the non-list pages
 */
Runner.util.details.recDP = Runner.extend( Runner.util.details.DP, {
	/**
	 * A page object representing the details preview page
	 * @type {object}
	 */
	detailsPageObj: null,

	id: -1,

	submitMade: false,

	submitSucceded: true,

	useChildCount: false,


	init: function() {
		Runner.util.details.recDP.superclass.init.call( this );
		this.initButton();
		this.initDpPage();

		this.addEvents('detailsSaved');

		if ( this.detailsSaved ) {
			this.on( {'detailsSaved': this.detailsSaved} );
		}

		if ( this.saveFailed ) {
			this.on( {'saveFailed': this.saveFailed} );
		}

		if ( this.afterDeleteDetails ) {
			this.on( {'afterDeleteDetails': this.afterDeleteDetails} );
		}
	},

	/**
	 * Init the Detail preview list page object
	 * @param {object} cfg
	 */
	initDpPage: function() {
		var cfg = this.getPageConfig();
		this.detailsPageObj = Runner.pages.PageManager.initPage( cfg );
	},


	/**
	 * Initialize Grid Tabs Where functionality
	 *	This function is called in the context of the details page ( this == details page object )
	 */
	initGridTabs: function() {
		var pageObj = this;

		$('[data-pageid=' + this.id + '][data-tabid]').on("click", function(e) {
			if ( !$(this).parent().hasClass('active') ) {
				pageObj.reload( { tab: $(this).data("tabid") } );
			}

			e.preventDefault();
		});
	},


	/**
	 * Get the detail preview page's config
	 * @paran {object}
	 * @return {object}
	 */
	getPageConfig: function( cfg ) {
		return Runner.apply(cfg || {}, {
			tName: this.tName,
			pageType: this.pType,
			openMode: Runner.pages.constants.OPENMODE_PRELOADED_CONTAINER,
			openContainer: $("#detailPreview" + this.id),
			pageId: this.id,
			controlsMap: this.controlsMap,
			viewControlsMap: this.viewControlsMap,
			masterTName: this.masterTName,
			detCont: $("#detailPreview" + this.id),
			useChildCount: this.useChildCount,
			childRecNum: 0,
			parId: this.parId,
			masterPageObj: this.masterPageObj,
			pageMode: Runner.pages.constants.MODE_LIST_DETAILS,
			baseParams: {
				id: this.id,
				mode: Runner.pages.constants.MODE_LIST_DETAILS,
				rndVal: Math.random(),
				masterpagetype: this.masterPageType
			},
			afterSaveDetails: {
				fn: function(allVals, fields, allKeys, allRowIds, isEdit) {
					this.submitMade = true;
					this.submitSucceded = true
					this.fireEvent("detailsSaved", this, allVals, fields, allKeys, allRowIds);
				},
				scope: this
			},
			saveFailed: {
				fn: function(respObj, formObj, fieldControls) {
					this.submitMade = true;
					this.submitSucceded = false;
					this.fireEvent("saveFailed", this, respObj, formObj);
				},
				scope: this
			},
			afterDeleteDetails: {
				fn: function() {
					this.fireEvent("afterDeleteDetails");
				},
				scope: this
			},
			validationFailed: this.validationFailed,
			initGridTabs: this.initGridTabs
		});
	},

	/**
	 * A stub
	 * Details inlineAdd/InlineEdit rows' validationFailed event handler
	 */
	validationFailed: Runner.emptyFn,

	/**
	 * Initialize the show/hide details preview button
	 */
	initButton: function() {
		var id = this.id,
			$button = $('#dpMinus' + id);


		$button.on('click', function(e) {
			if ( $button.is(':visible') ) {
				$('.video-js', '#detailPreview' + id).each(function() {
					videojs( this.id ).pause();
				});
			}
			setTimeout( function() {
				var src = $button.attr('src'),
					newSrc = src == Runner.pages.constants.MINUS_GIF ? Runner.pages.constants.PLUS_GIF : Runner.pages.constants.MINUS_GIF;

				$('#detailPreview' + id).toggle();
				$button.attr('src', newSrc);
			}, 100);
		});
	},

	/**
	 * Save list details
	 * @param {array} mKeys (optional)
	 */
	saveDetails: function( mKeys ) {
		if ( !this.detailsPageObj || this.detailsPageObj.pageType !== Runner.pages.constants.PAGE_LIST ) {
			return;
		}

		this.submitSucceded = false;
		this.submitMade = false;
		return this.detailsPageObj.saveAll( mKeys );
	},

	/**
	 * Validate added/edited list details
	 */
	validate: function() {
		if ( this.detailsPageObj && this.detailsPageObj.pageType === Runner.pages.constants.PAGE_LIST
			&& typeof this.detailsPageObj.validateRecords === "function" ) {
			return this.detailsPageObj.validateRecords();
		}
	},

	destructor: function() {
		if ( this.detailsPageObj ) {
			this.detailsPageObj.destructor();
		}
	}
});


/**
 * @class Runner.util.details.AddDP
 * provides base functionality for details on add page
 */
Runner.util.details.AddDP = Runner.extend( Runner.util.details.recDP, {
	constructor: function( cfg ) {
		this.pageType = Runner.pages.constants.PAGE_ADD;
		Runner.util.details.AddDP.superclass.constructor.call( this, cfg );
	},

	/**
	 * Init the Detail preview page object
	 */
	initDpPage: function() {
		Runner.util.details.AddDP.superclass.initDpPage.call( this );

		this.detailsPageObj.on('beforeSave', function( row, inlineObj, formObj ) {
			if ( inlineObj.inlineAddChangeContent !== true ) {
				inlineObj.cancelAll();

				return false;
			}
		}, this);
	},

	/**
	 * Get the Add page's detail preview page's config
	 * @return {object}
	 */
	getPageConfig: function() {
		var cfg = {
			hideSaveButton: true,
			afterInit: {
				fn: function( pageObj, proxy, pageId ) {
					if ( !pageObj.inlineAdd ) {
						return;
					}

					// to control auto-added record behavior
					pageObj.autoAddRecordId = pageObj.inlineAdd.inlineAdd( true, true );

					pageObj.inlineAdd.on("createControls", function( row, ctrls ) {
						var inlineAddContentChangeHandler, i;

						if ( this.inlineAddChangeContent ) {
							return true;
						}

						inlineAddContentChangeHandler = function() {
							this.fireEvent('inlineAddContentChange', row);
						}

						var prevAddRow = this.rows.find( function( _row ) {
								return _row !== row && !$( _row.cancelLink ).is(":visible") && _row.isAdd;
							});

						prevAddRow && this.showCancelButton( prevAddRow );

						this.inlineAddChangeContent = false;
						this.on('inlineAddContentChange', this.showCancelButton, this, { single: true });

						for (i = 0; i < ctrls.length; i++) {
							ctrls[i].on('change', inlineAddContentChangeHandler, { single: true }, this);
						}
					}, pageObj.inlineAdd );
				},
				scope: this
			}
		};

		return Runner.util.details.AddDP.superclass.getPageConfig.call( this, cfg );
	}
});

/**
 * @class Runner.util.details.EditDP
 * provides base functionality for details on edit page
 */
Runner.util.details.EditDP = Runner.extend( Runner.util.details.recDP, {
	constructor: function( cfg ) {
		this.pageType = Runner.pages.constants.PAGE_EDIT;
		Runner.util.details.EditDP.superclass.constructor.call( this, cfg );
	}
});

/**
 * @class Runner.util.details.ViewDP
 * provides base functionality for details on view page
 */
Runner.util.details.ViewDP = Runner.extend( Runner.util.details.recDP, {
	constructor: function( cfg ) {
		this.pageType = Runner.pages.constants.PAGE_VIEW;
		Runner.util.details.ViewDP.superclass.constructor.call( this, cfg );
	}
});

/**
 * @class Runner.util.details.ViewEditDashDP
 * It shows details preview on view/edit dash sr elements
 */
Runner.util.details.ViewEditDashDP = Runner.extend( Runner.util.details.recDP, {
	/**
	 * view/edit
	 */

	dashTName: "",

	dashElement: "",

	dashPage: "",

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.util.details.ViewEditDashDP.superclass.constructor.call( this, cfg );

		this.openContainer = $("#dp_" + Runner.goodFieldName( this.masterTName ) + "_" + this.masterPageType + "_" + this.id);
	},

	/**
	 * Initialize details
	 */
	init: function() {
		this.addEvents('detailsSaved', 'saveFailed');

		if ( this.detailsSaved ) {
			this.on( {'detailsSaved': this.detailsSaved} );
		}

		if ( this.saveFailed ) {
			this.on( {'saveFailed': this.saveFailed} );
		}

		this.getDetails();
	},

	/**
	 *
	 */
	getDetails: function() {
		var dpObj = this,
			params = Runner.apply( this.getDetailsRequestParams(), this.masterKeys );

		//	allow id space for 1000 records
		Runner.setIdCounter( params.id + 1000 );

		params.__method__ = 'GET';
		Runner.runnerAJAX( this.ajaxRequestUrl, params, function( respObj ) {
			var cfg, jsFile;

			if ( respObj.noData || !respObj.success ) {
				dpObj.openContainer.parents(".panel").eq(0).hide();
				//hide container
				return;
			}

			Runner.setIdCounter( respObj.idStartFrom );
			Runner.pages.PageSettings.addSettings( dpObj.tName, respObj.settings, false );
			Runner.applyPagesData( respObj.pagesData );

			if ( respObj.additionalCSS ) {
				Runner.util.ScriptLoader.loadCSS( respObj.additionalCSS );
			}

			if ( respObj.CSSFiles ) { // 8.1 version fix #9925
				Runner.util.ScriptLoader.loadCSS( respObj.CSSFiles );
			}

			cfg = dpObj.getNewDetailPageConfig( respObj, params.id );

			if ( dpObj.masterPageType == Runner.pages.constants.PAGE_EDIT && respObj.headerButtonsBlock ) {
				cfg.afterCreateHn = function() {
					this.openContainer.parents(".panel").eq(0).find(".panel-heading .panel-title span").html( respObj.headerButtonsBlock )
				}
			}


			if ( respObj.additionalJS ) {
				for ( jsFile in respObj.additionalJS ) {
					Runner.util.ScriptLoader.addJS( [ jsFile ], respObj.additionalJS[ jsFile ] );
				}

				Runner.util.ScriptLoader.onFilesLoaded( function() {
					dpObj.detailsPageObj = Runner.pages.PageManager.initPage( cfg );
				}, this );

				Runner.util.ScriptLoader.load();
			} else {
				dpObj.detailsPageObj = Runner.pages.PageManager.initPage( cfg );
			}
		});
	},

	/**
	 * @return {object}
	 */
	 getDetailsRequestParams: function() {
		return {
			id: Runner.genId(),
			mode: Runner.pages.constants.MODE_LIST_DETAILS,
			rndVal: Math.random(),
			mastertable: this.masterTName,
			masterid: this.id,
			masterpagetype: this.masterPageType,
			table: this.dashTName,
			dashPage: this.dashPage,
			dashelement: this.dashElement
		};
	 },

	/**
	 * @param {object} respObj
	 * @param {object} row
	 * @param {string} newId
	 */
	getNewDetailPageConfig: function( respObj, newId ) {
		return {
			bodyCont: respObj.html,
			headerCont: respObj.headerCont,
			footerCont: respObj.footerCont,
			openMode: Runner.pages.constants.OPENMODE_CONTAINER,
			openContainer: this.openContainer,
			tName: this.tName,
			pageId: newId,
			pageType: this.pageType,
			controlsMap: respObj.controlsMap[ this.tName ][ this.pageType ][ newId ] || {},
			viewControlsMap: respObj.viewControlsMap[ this.tName ][ this.pageType ][ newId ] || {},
			pageMode: Runner.pages.constants.MODE_LIST_DETAILS,
			masterKeys: this.masterKeys,
			parId: this.parId,
			masterTName: this.masterTName,
			pageCont: this.openContainer,
			detCont: this.openContainer,
			afterSaveDetails: {
				fn: function(allVals, fields, allKeys, allRowIds, isEdit) {
					this.submitMade = true;
					this.submitSucceded = true
					this.fireEvent("detailsSaved", this, allVals, fields, allKeys, allRowIds);
				},
				scope: this
			},
			saveFailed: {
				fn: function(respObj, formObj, fieldControls) {
					this.submitMade = true;
					this.submitSucceded = false;
					this.fireEvent("saveFailed", this, respObj, formObj);
				},
				scope: this
			},
			afterDeleteDetails: this.afterDeleteDetails,
			validationFailed: this.validationFailed,
			baseParams: {
				id: newId,
				mode: Runner.pages.constants.MODE_LIST_DETAILS,
				rndVal: Math.random(),
				masterpagetype: this.masterPageType,
				mastertable: this.masterTName,
				table: this.dashTName,
				dashPage: this.dashPage,
				dashelement: this.dashElement
			},
			initGridTabs: this.initGridTabs

		};
	}
});


/**
 * @class Runner.util.details.ListTabbedPreview
 * Provides base functionality for inline details
 */
Runner.util.details.ListTabbedPreview = Runner.extend( Runner.util.details.InlineDP, {
	/**
	 *
	 */
	tabSettings: null,

	/**
	 *
	 */
	proceedLink: false,

	/**
	 *
	 */
	dispChildCount: false,

	/**
	 * Records per row on list
	 * @type {integer}
	 */
	recsPerRowList: 1,


	/**
	 *
	 */
	detailsHrefAvailable: false,

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.util.details.ListTabbedPreview.superclass.constructor.call( this, cfg );

		this.dpShowType = Runner.pages.constants.DP_INLINE;
		this.recsPerRowList = Runner.pages.PageSettings.getTableData( this.masterTName, "recsPerRowList" );
		this.listGridLayout = Runner.pages.PageSettings.getTableData( this.masterTName, "listGridLayout" );

		this.tabSettings = this.tabSettings || {};
		this.tabSettings.tabIndex = this.tabSettings.tabIndex || 0;
		this.tabSettings.tabId = this.tabSettings.tabId || this.shortTName;
		this.tabSettings.tabLabel = this.tabSettings.tabLabel/* || this.shortTName*/;
		this.tabSettings.hideTabsContent = this.tabSettings.hideTabsContent || false;
	},

	/**
	 * @param {object} row
	 */
	getTabTableLink: function( row ) {
		var mKeys = [],
			key,
			params = { page: this.previewPageId };

		for ( key in row.masterKeys[ this.tName ] ) {
			mKeys.push( row.masterKeys[ this.tName ][ key ] );
		}
		
		return Runner.pages.getUrl( this.tName, this.pageType, mKeys, "masterkey", params ) 
			+ "&mastertable=" + encodeURIComponent( this.masterTName );
	},

	/**
	 * @param {object} row
	 * @return {string}
	 */
	getTabLabel: function( row ) {
		if ( !this.proceedLink ) {
			return '<span id="bsproceed_' + this.shortTName + row.id + '" >'
					+ this.tabSettings.tabLabel
				+ '</span>';
		}

		var href = this.getTabTableLink( row );

		return '<span id="bsproceed_' + this.shortTName + row.id + '" class="bs-proceed-link" data-plink="' + href + '">'
				+ this.tabSettings.tabLabel
				+ '<button class="btn btn-xs btn-link bs-proceed-icon">'
					+ ' <span class="glyphicon glyphicon-new-window "></span>'
				+ '</button>'
			+ '</span>';
	},



	/**
	 * Set event handler for detail preview links
	 * @param {object} row
	 * @param {boolean} setHref set attr href or not
	 */
	initLink: function( row, setHref ) {
		var dpObj = this,
			href, mKeys, key,
			$allDetailsLink = $("[id=details_" + row.id + "]"),
			$singleDetailsLink = $("[id=details_" + row.id + "_" + this.shortTName + "]"),
			$oldStyleDetailsLink = $("#" + this.shortTName + "_preview" + row.id),
			$links = $($allDetailsLink).add($singleDetailsLink).add($oldStyleDetailsLink);

		$links.each( function() {
			var $link = $(this);
			if ( $link.data("initialized") ) {
				return;
			}
			$link.on("click", function(e) {
				return dpObj.linkClickHandler( row, e );
			})
			.data("initialized", true);
		});

		if ( setHref === true ) {
			mKeys = [];
			for (key in row.masterKeys[ this.tName ]) {
				mKeys.push( row.masterKeys[ this.tName ][ key ] );
			}
			href = Runner.pages.getUrl( this.tName, this.pageType, mKeys, "masterkey" ) + "&mastertable=" + encodeURIComponent(this.masterTName);
			$links.attr('href', href);
		}
	},

	/**
	 * @param {object} row
	 * @param {boolean} setHref
	 */
	initRow: function( row, setHref ) {
		Runner.util.details.ListTabbedPreview.superclass.initRow.call( this, row, setHref );
		this.setDetailLinksIds( row );
	},

	/**
	 * This function is used
	 *  Assign the 'row' object's 'detLinksId' property
	 *  It is used only in multiple records per row mode to create containers
	 *  for two or more details previews in a single TR
	 * @param {object} row
	 */
	setDetailLinksIds: function( row ) {
		if ( this.recsPerRowList <= 1 ) {
			return;
		}

		var $detLinks, pattern;

		row.detLinksId = [];

		if ( this.linksType == Runner.pages.constants.DL_SINGLE ) {
			$detLinks = $("[id^='details_'].glyphicon", row.row);
			$detLinks = $detLinks.length ? $detLinks : $("[id^='details_']", row.row);

			$detLinks.each( function( idx, el ) {
				row.detLinksId.push( $(el).attr("id").substr(8) );
			});

			return;
		}

		//	individual preview links
		pattern = this.shortTName + "_preview";
		$("[id^=" + pattern + "]", row.row).each( function() {
			row.detLinksId.push( this.id.substr( pattern.length ) );
		});

		if ( row.detLinksId.length ) {
			return;
		}

		//	single preview link
		pattern = "details_";
		$("[id^=" + pattern + "]:not(.glyphicon)", row.row).each( function() {
			row.detLinksId.push( this.id.substr( pattern.length ) );
		});
	},

	/**
	 * show all details
	 * @param {object} row
	 * @param {jQuery event object} e
	 */
	linkClickHandler: function( row, e ) {
		var switchCondition = false,
			closeCondition = row.isShown,
			$link = $( e.target ),
			corTabIdx;

		if ( row.isShown && ( this.linksType == Runner.pages.constants.DL_INDIVIDUAL
			|| this.linksType == Runner.pages.constants.DL_SINGLE && $link.attr("id") !== "details_" + row.id ) ) {
			
			corTabIdx = this.parPageObj.dpObjs[ this.tName ].tabSettings.tabIndex;
			switchCondition = !$("li[data-tabidx='" + corTabIdx + "']", row.detTd).hasClass("active"); //temp fix it!!
		}

		if ( switchCondition ) {
			$("[data-toggle='tab'][href='" + "#tabdetails" + row.id + '_' + this.shortTName + "']").trigger("click"); 
			return false;
		}

		if ( closeCondition ) {
			this.closeDetails( row, $link );
			return false;
		}

		this.openDetailsTabs( row, $link );
		return false;
	},

	/**
	 * @param {string} tName
	 * @param {object} row
	 * @return {boolean}
	 */
	isTabToShow: function( tName, row ) {
		if ( this.parPageObj.dpObjs[ tName ].dpShowType != Runner.pages.constants.DP_INLINE ) {
			return false;
		}

		// check if the tab is not hidden. hiddentabs used in not bs layouts
		return !row.hiddentabs || jQuery.inArray( tName, row.hiddentabs ) === -1;
	},

	/**
	 * @param {object} row
	 * @param {jQuery object} $link
	 */
	openDetailsTabs: function( row, $link ) {
		var tabTNames = [],
			$tabControl, tabSettings,
			dpObjs, tabElementId, tName, i, href;

		for ( tName in this.parPageObj.dpObjs ) {
			if ( this.isTabToShow( tName, row ) ) {
				if ( tName == this.tName ) {
					tabTNames.unshift( tName );
				} else {
					tabTNames.push( tName );
				}
			}
		}

		if ( !tabTNames.length || this.recsPerRowList == 1 && row.detTd ) {
			return;
		}

		// create row.detTr & row.detTd
		this.createPreviewRow( row );
		row.isShown = true;

		// highlight a master row	
		if ( this.recsPerRowList > 1 ) {
			var $linkTd = $link.closest('td');

			$linkTd.addClass("bs-details-opened");
			$linkTd.nextUntil("[colid='endrecord']").addClass("bs-details-opened");
			$linkTd.prevUntil("[colid='endrecord']").addClass("bs-details-opened");
		} else {
			row.row.addClass( "bs-details-opened" );
		}

		// create a new row's tab-control
		$tabControl = Runner.pages.PageManager.findOrCreateTabControl( row.detTd, "details" + row.id );

		for ( i = 0; i < tabTNames.length; i++ ) {
			tName = tabTNames[ i ],
			dpObjs = this.parPageObj.dpObjs[ tName ],
			tabElementId = "tabdetails" + row.id + "_" + dpObjs.tabSettings.tabId;

			href = dpObjs.proceedLink ? dpObjs.getTabTableLink( row ) : "";

			Runner.pages.PageManager.addTabToTabsControl( $tabControl, "", tabElementId, dpObjs.getTabLabel( row ),
				tName == this.tName, dpObjs.tabSettings.tabIndex, false, href );

			this.parPageObj.dpObjs[ tName ].getDetails( row );
		}

		$tabControl.find( '.bs-proceed-link' ).closest('[data-toggle="tab"]').addClass( "bs-proceed-link" );
		$tabControl.on('click.detailsTabControl', '.bs-proceed-link', function() {
			var $pLink = $(this);
			if ( $pLink.closest("li").hasClass("active") && $pLink.data("plink") ) {
				window.location.href = $pLink.data("plink");
				return false;
			}
		});

		this.parPageObj.hideElement("message");
	},

	/**
	 * @param {object} row
	 */
	getParams: function( row ) {
		return Runner.apply({
			id: Runner.genId(),
			mode: Runner.pages.constants.MODE_LIST_DETAILS,
			rndVal: Math.random(),
			mastertable: this.masterTName,
			masterid: this.id,
			masterpagetype: this.masterPageType,
			goto: 1
		}, row.masterKeys[ this.tName ]);
	},

	/**
	 * @param {object} row
	 */
	getDetails: function( row ) {
		var params = this.getParams( row ),
			dt = this;
		//	allow id space for 1000 records
		Runner.setIdCounter( params.id + 1000 );
		this.fireEvent('beforeShowDetails', this, row);

		params.__method__ = 'GET';
		Runner.runnerAJAX( this.ajaxRequestUrl, params, function( respObject ) {
			dt.showDetails( respObject, row, params );
		});
	},

	/**
	 * Show tabbed inline details preview
	 * @param {object} respObj
	 * @param {object} row
	 * @param {object} params
	 */
	showDetails: function( respObj, row, params ) {
		var errContent, $tabsControl, cfg, jsFile;

		$tabsControl = Runner.pages.PageManager.getTabControl( row.detTd, "details" + row.id );
		if ( respObj.success === false ) {
			// set the correspondent tab's content with TEXT_NO_RECORDS
			errContent = Runner.pages.RunnerPage.prototype.getMessageText( Runner.lang.constants.NO_RECORDS, true );
			Runner.pages.PageManager.setTabWithinTabsControl( $tabsControl, errContent, "tabdetails" + row.id + "_" + this.tabSettings.tabId,
				this.getTabLabel( row ), false, this.tabSettings.tabIndex );
			return;
		}

		Runner.setIdCounter( respObj.idStartFrom );
		Runner.pages.PageSettings.addSettings( this.tName, respObj.settings, false );
		Runner.applyPagesData( respObj.pagesData );

		//	multiple records per row case. The container might be deleted from the document
		if ( this.recsPerRowList > 1 && row.detTd && row.detTd.length && !$.contains(row.detTd[0], document.body) ) {
			delete row.detTd;
		}

		if ( !row.detTd ) {
			this.createPreviewRow( row );
		}

		row.isShown = true;

		if ( respObj.additionalCSS ) {
			Runner.util.ScriptLoader.loadCSS( respObj.additionalCSS );
		}

		if ( respObj.CSSFiles ) { // 8.1 version fix #9925
			Runner.util.ScriptLoader.loadCSS( respObj.CSSFiles );
		}

		row.detPage = row.detPage || {};
		cfg = this.getNewDetailPageConfig( respObj, row, params.id );


		if ( Runner.pagesData[ params.id ].gridTabs ) {
			cfg.$gridTabControl = $tabsControl.children('ul');
			//	add attributes to the tab to get it replaced by the page class

		}

		if ( respObj.additionalJS ) {
			for ( jsFile in respObj.additionalJS ) {
				Runner.util.ScriptLoader.addJS( [ jsFile ], respObj.additionalJS[ jsFile ] );
			}

			Runner.util.ScriptLoader.onFilesLoaded( function() {
				row.detPage[ this.tName ] = Runner.pages.PageManager.initPage( cfg );
			}, this );

			Runner.util.ScriptLoader.load();
		} else {
			row.detPage[ this.tName ] = Runner.pages.PageManager.initPage( cfg );
		}
	},


	/**
	 * Initialize Grid Tabs Where functionality
	 *	This function is called in the context of the details page ( this == details page object )
	 */
	initGridTabs: function() {
		var pageObj = this;

		$('[data-pageid=' + this.id + '][data-tabid]').on("click", function(e) {
			var $link = $(this),
				url = $(this).attr('href'),
				$tab = $link.parent(),
				$pane;

			if ( $tab.hasClass('active' ) ) {
				return;
			}

			$tab.siblings().removeClass('active');
			$tab.addClass('active');


			if ( $link.data('target') ) {
				$pane = $( $link.data('target') );
				$pane.addClass('active')
					.siblings().removeClass('active');
			}

			//	little trick to control tabs selection after reloading
			pageObj.$originalTab.addClass('active');


			pageObj.openContainer = pageObj.detCont[0];
			pageObj.silentReloading = true;

			pageObj.reload( { tab: $(this).data("tabid") } );
			return false;
		});
	},


	/**
	 *	This function is called in the context of the details page ( this == details page object )
	 */
	replaceGridTabs: function() {
		if ( !this.$gridTabControl ) {
			return;
		}

		var $replacementTabs = $( this.pageData.gridTabs ),
			$originalTab = $('#bsproceed_' + this.shortTName + this.rowId ).closest('li'),
			$originalLink = $originalTab.children('a');

		$originalTab.hide();

		this.$gridTabControl.find('[data-tabid][data-pageid=' + this.id + ']').parent().remove();

		//	take care of the active tab
		if ( !$originalTab.hasClass('active') ) {
			$replacementTabs.removeClass('active');
		}

		this.$originalTab = $originalTab;

		$replacementTabs.each( function( i ) {
			var $tab = $(this),
				$link = $tab.children('a'),
				$span = $originalLink.children().clone(),
				attrMap = {};

			//	copy all attributes
			if( $originalLink.length ) {
				jQuery.each( $originalLink.get(0).attributes, function() {
					attrMap[ this.name ] = this.value;
				});
			}

			//	update URL
			attrMap.href = attrMap.href + '&tab=' + $link.data('tabid');
			$span.attr('data-plink', attrMap.href );



			$link.attr( attrMap );
			if( $span.length ) {
				$span.contents()[0].data = $link.text();
			}
			$link.html('').append( $span );
		});

		$originalTab.after( $replacementTabs );
	},


	/**
	 * @param {object} respObj
	 * @param {object} row
	 * @param {string} newId
	 */
	getNewDetailPageConfig: function( respObj, row, newId ) {
		var newTabSettings = jQuery.extend( true, {}, this.tabSettings, {
					tabControlName: "details" + row.id,
					label: this.getTabLabel( row )
				});
		return {
			tabControl: Runner.pages.PageManager.getTabControl( row.detTd, "details" + row.id ),
			bodyCont: respObj.html,
			headerCont: respObj.headerCont,
			footerCont: respObj.footerCont,
			openMode: Runner.pages.constants.OPENMODE_TABVIEW,
			tabSettings: newTabSettings,
			openContainer: row.detTd,
			tName: this.tName,
			pageId: newId,
			rowId: row.id,
			pageType: this.pageType,
			controlsMap: respObj.controlsMap[ this.tName ][ this.pageType ][ newId ] || {},
			viewControlsMap: respObj.viewControlsMap[ this.tName ][ this.pageType ][ newId ] || {},
			pageMode: Runner.pages.constants.MODE_LIST_DETAILS,
			detCont: row.detCont,
			masterKeys: row.masterKeys[ this.tName ],
			parId: this.parId,
			baseParams: this.getDetailPageBaseParams( newId, row.masterKeys[ this.tName ] ),
			afterSaveDetails: {
				fn: function( allVals, fields, allKeys, allRowIds, isEdited ) {
					this.submitMade = true;
					this.submitSucceded = true;
					if ( !isEdited ) {
						this.updateChildRecNum( row.masterKeys[ this.tName ] );
					}
					this.fireEvent("detailsSaved", this, allVals, fields, allKeys, allRowIds);
				},
				scope: this
			},
			afterDeleteDetails: {
				fn: function() {
					this.updateChildRecNum( row.masterKeys[ this.tName ] );
				},
				scope: this
			},
			saveFailed: {
				fn: function( respObj, formObj, fieldControls ) {
					this.submitMade = true;
					this.submitSucceded = false;
					this.fireEvent("saveFailed", this, respObj, formObj);
				},
				scope: this
			},
			afterInit: {
				fn: function() {
					this.fireEvent('afterInit', this, this.proxy, this.id);
					this.fireEvent('afterShowDetails');
				},
				scope: this
			},
			replaceGridTabs: this.replaceGridTabs,
			initGridTabs: this.initGridTabs
		};
	},

	/**
	 * Create the html markup to show the details preview
	 * @param {object} row
	 */
	createPreviewRow: function( row ) {
		var $lastRecordRow = row.row.filter(":last"),
			$nextGridRow = $lastRecordRow.next(),
			$childrenTd = $lastRecordRow.children('td'),
			previewCellName = this.masterShortTName + "_previewcell",
			previewRowClass = this.classBasePrefix + "bs-dpreviewrow",
			detTdselector, colspan, colSpanArr, start, i;

		if ( $nextGridRow.hasClass( previewRowClass ) ) {
			row.detTr = $nextGridRow;
			detTdselector = this.recsPerRowList > 1 ? "#" + previewCellName + row.id : '.dpinline';
			row.detTd = $( detTdselector, $nextGridRow );
			if ( this.recsPerRowList > 1 ) {
				row.detTd.addClass('dpinline');
			}
			return;
		}

		//create new row
		row.detTr = $("<tr></tr>").addClass( previewRowClass )
			.insertAfter( $lastRecordRow );

		if ( this.recsPerRowList > 1 ) {
			if ( this.listGridLayout == Runner.pages.constants.VERTICAL_LAYOUT  ) {
				for (i = 0; i < this.recsPerRowList; i++) {
					$("<td></td>")
						.attr('id', previewCellName + row.detLinksId[ i ])
						.addClass('dpframe-cl dpframe-cc dpframe-cr')
						.appendTo( row.detTr );
				}
			} else {
				colSpanArr = [];
				start = 0;

				$childrenTd.each( function( i, td ) {
					if ( $(td).attr("colid") == "endrecord" ) {
						colSpanArr.push( i - start );
						start = i + 1;
					}
				});
				colSpanArr.push( $childrenTd.length - start );

				for (i = 0; i < colSpanArr.length; i++) {
					$('<td colspan="' + colSpanArr[ i ] + '"></td>')
						.attr('id', previewCellName + row.detLinksId[ i ])
						.addClass('dpframe-cl dpframe-cc dpframe-cr')
						.appendTo( row.detTr );

					$("<td></td>").addClass( this.classBasePrefix + 'cs' ).attr('colid', "endrecord")
						.appendTo( row.detTr );
				}
			}

			row.detTd = $("#" + previewCellName + row.id, row.detTr).addClass('dpinline');
		} else {
			colspan = 0;
			$childrenTd.each( function( i, td ) {
				colspan += $childrenTd[i].colSpan;
			});

			colspan = this.oldLayout ? $childrenTd.length - 2 : colspan;
			row.detTd = $("<td></td>").addClass('dpinline dpframe-cc').appendTo( row.detTr )
				.attr('colspan', colspan );

			if ( this.listGridLayout == Runner.pages.constants.VERTICAL_LAYOUT ) {
				row.detTd.addClass('dpframe-cl dpframe-cr');
				return;
			}
			if ( this.oldLayout ) {
				$("<td></td>").addClass( this.classBasePrefix + 'cl dpframe-cl' ).prependTo( row.detTr );
				$("<td></td>").addClass( this.classBasePrefix + 'cr dpframe-cr' ).appendTo( row.detTr );
			}
		}
	},


	/**
	 * @param {string} newId
	 * @param {object} masterKeys
	 */
	getDetailPageBaseParams: function( newId, masterKeys ) {
		return Runner.apply( {
				id: newId,
				mode: Runner.pages.constants.MODE_LIST_DETAILS,
				rndVal: Math.random(),
				masterpagetype: this.masterPageType,
				mastertable: this.masterTName
			}, masterKeys );

	},

	/**
	 * Close the inline details, destroy detail's page
	 * @param {object} row
	 * @param {jQuery object} $link
	 */
	closeDetails: function( row, $link ) {
		if ( !row.isShown ) {
			return;
		}

		for ( var tName in row.detPage ) {
			Runner.pages.PageManager.unregister( row.detPage[ tName ].tName, row.detPage[ tName ].pageId );
		}
	
		if ( this.recsPerRowList > 1 && $link ) {
			var $linkTd = $link.closest('td');

			$linkTd.removeClass("bs-details-opened");
			$linkTd.nextUntil("[colid='endrecord']").removeClass("bs-details-opened");
			$linkTd.prevUntil("[colid='endrecord']").removeClass("bs-details-opened");
		} else {
			row.row.removeClass( "bs-details-opened" );
		}

		if ( this.recsPerRowList > 1 && !this.isTdReset( row ) ) {
			this.resetCell( row );
		} else {
			row.detTr.remove();
			delete row.detTr;

			row.detTd.remove();
			delete row.detTd;
		}

		if ( row.detCont ) {
			row.detCont.remove();
			delete row.detCont;
		}

		row.isShown = false;
	},

	/**
	 * @param {number} ind
	 */
	closeDetailsByInd: function( ind ) {
		var row = this.getRowByInd( ind );

		if ( row ) {
			this.closeDetails( row );
		}
	},

	/**
	 * @param {number} rowId
	 */
	closeDetailsById: function( rowId ) {
		var row = this.getRowById( rowId );

		if ( row ) {
			this.closeDetails( row );
		}
	},

	/**
	 *  //? work logic ?
	 * @param {object} row
	 * @return {boolean}
	 */
	isTdReset: function( row ) {
		var removeTd = false,
			i, dpTd;

		for ( i = 0; i < row.detLinksId.length; i++ ) {
			dpTd = $( "#" + this.masterShortTName + "_previewcell" + row.detLinksId[ i ] );

			if ( row.detLinksId[i] != row.id ) {
				if ( dpTd.html() != '' ) {
					return false;
				}

				removeTd = true;
			}
		}

		return removeTd;
	},

	/**
	 * Set the details preview container empty
	 * @param {object} row
	 */
	resetCell: function( row ) {
		row.detTd.empty();
		if ( this.recsPerRowList > 1 ) {
			row.detTd.removeClass('dpinline');
		}
	},

	/**
	 *
	 */
	destructor: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.closeDetails( this.rows[i] );
		}
	}
});

/**
 * @class Runner.util.details.ListWithoutPreview
 * Provides base functionality for list without details
 */
Runner.util.details.ListWithoutPreview = Runner.extend( Runner.util.details.InlineDP, {

	constructor: function( cfg ) {
		Runner.util.details.ListWithoutPreview.superclass.constructor.call( this, cfg );
		this.dpShowType = Runner.pages.constants.DP_NONE;
	},

	init: Runner.emptyFn,

	/**
	 * Set event handler for detail preview links
	 * @param {object} row
	 * @param {boolean} setHref set attr href or not
	 */
	initLink: function( row, setHref ) {
		var key,
			mKeys = [];

		for ( key in row.masterKeys[ this.tName ] ) {
			mKeys.push( row.masterKeys[ this.tName ][ key ] );
		}

		$("#details_" + row.id + "_" + this.shortTName)
			.attr( "href", Runner.pages.getUrl( this.tName, this.pageType, mKeys, "masterkey" ) + "&mastertable=" + encodeURIComponent(this.masterTName) );
	}
});	
// create namespace
Runner.namespace('Runner.util.details');

/**
 * @class Runner.util.details.PDPopupDetails
 * Provides base functionality for popup details on Page Designer-created pages
 */
Runner.util.details.PDPopupDetails = Runner.extend( Runner.util.details.DPPopUp, {

	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.util.details.PDPopupDetails.superclass.constructor.call( this, cfg );				
	},

	getUrl: function( page ) {		
		return Runner.getPageUrl( this.shortTName, page );
	}
	
});

/**
 * @class Runner.util.details.NewListPreview
 * Provides base functionality for inline details on Page Designer-created pages
 */
Runner.util.details.NewListPreview = Runner.extend( Runner.util.details.ListTabbedPreview, {
	
	previewItemId: '',	
	
	previewContainers: null,
	previewPageId: '',	
	
	/**
	 * @constructor
	 * @param {object} cfg
	 */
	constructor: function( cfg ) {
		Runner.util.details.NewListPreview.superclass.constructor.call( this, cfg );
		this.previewContainers = {};		
	},
	
	/**
	 * @param {string} tName
	 * @param {object} row
	 * @return {boolean}
	 */
	isTabToShow: function( tName, row ) {
		if ( this.hideEmptyPreview && row.hiddenPreviewTabs && row.hiddenPreviewTabs.indexOf( tName ) !== -1 ) {
			return false;
		}
		
		return Runner.util.details.NewListPreview.superclass.isTabToShow.call( this, tName, row );
	},
	
	/**
	 * @param {object} row
	 * @param {jQuery object} $link
	 */
	openDetailsTabs: function( row, $link ) {
		var tabTNames = [],
			tName, i;
		
		for ( tName in this.parPageObj.dpObjs ) {
			if ( this.isTabToShow( tName, row ) ) {
				if ( tName == this.tName ) {
					tabTNames.unshift( tName );
				} else {
					tabTNames.push( tName );
				}
			}
		}

		if ( !tabTNames.length ) {
			return;
		}

		// highlight a master row
		if ( this.recsPerRowList > 1 ) {
			var $linkTd = $link.closest('td');

			$linkTd.addClass("bs-details-opened");
			$linkTd.nextUntil("[colid='endrecord']").addClass("bs-details-opened");
			$linkTd.prevUntil("[colid='endrecord']").addClass("bs-details-opened");
		} else {
			row.row.addClass( "bs-details-opened" );
		}
		

		row.isShown = true;
		
		for ( i = 0; i < tabTNames.length; i++ ) {
			tName = tabTNames[ i ];
			var dpObj = this.parPageObj.dpObjs[ tName ];
			dpObj.prepareTabControl( row, tName === this.tName );
		}
		
		this.parPageObj.hideElement( "message" );
	},

	prepareTabControl: function( row, setActive ) {			
		this.createPreviewRow( row );
		
		var $tabControl = Runner.pages.PageManager.findOrCreateTabControl( 
				this.previewContainers[ row.id ], 
				"details_" + this.previewItemId + row.id 
			);
		
		var tabElementId = "tab" + this.tabControlName(row) + "_" + this.tabSettings.tabId;

		var href = this.proceedLink && this.getTabTableLink( row );	

		Runner.pages.PageManager.addTabToTabsControl( 
			$tabControl, 
			"", 
			tabElementId, 
			this.getTabLabel( row ), 
			setActive, 
			this.tabSettings.tabIndex, 
			false, 
			href );
		
		$tabControl.find( '.bs-proceed-link' ).closest('[data-toggle="tab"]').addClass( "bs-proceed-link" );
		$tabControl.on('click.detailsTabControl', '.bs-proceed-link', function() {
			var $pLink = $(this);
			if ( $pLink.closest("li").hasClass("active") && $pLink.data("plink") ) {
				window.location.href = $pLink.data("plink");
				return false;
			}
		});
		this.getDetails( row );	
	},

	/**
	 * @param {object} row
	 */
	getParams: function( row ) {
		return Runner.apply({
			id: Runner.genId(),
			page: this.previewPageId || '',
			mode: Runner.pages.constants.MODE_LIST_DETAILS,
			rndVal: Math.random(),
			mastertable: this.masterTName,
			masterid: this.id,
			masterpagetype: this.masterPageType,
			masterpage: this.parPageObj.pageName,
			masterPageObj: this.masterPageObj,
			goto: 1,
			firstTime: 1,
		}, row.masterKeys[ this.tName ]);
	},
	
	/**
	 * @param {object} row
	 */
	getDetails: function( row ) {
		var params = this.getParams( row ),
			dt = this;
		//	allow id space for 1000 records
		Runner.setIdCounter( params.id + 1000 );
		this.fireEvent('beforeShowDetails', this, row);
		
		params.__method__ = 'GET';

		Runner.runnerAJAX( this.ajaxRequestUrl, params, function( respObject ) {
			dt.showDetails( respObject, row, params );
		});
	},
	
	/**
	 * Show tabbed inline details preview
	 * @param {object} respObj
	 * @param {object} row
	 * @param {object} params
	 */	
	showDetails: function( respObj, row, params ) {
		var errContent, $tabsControl, cfg, jsFile;

		if ( !row.isShown || !this.previewContainers[row.id] ) {
			return;
		}

		$tabsControl = Runner.pages.PageManager.getTabControl( this.previewContainers[row.id], "details_" + this.previewItemId + row.id );
		if ( respObj.success === false ) {
			// set the correspondent tab's content with TEXT_NO_RECORDS
			errContent = Runner.pages.RunnerPage.prototype.getMessageText( Runner.lang.constants.NO_RECORDS, true );
			Runner.pages.PageManager.setTabWithinTabsControl( $tabsControl, errContent, "tab" + this.tabControlName(row) + "_" + this.tabSettings.tabId,
				this.getTabLabel( row ), false, this.tabSettings.tabIndex );
			return;
		}
		
		Runner.setIdCounter( respObj.idStartFrom );
		Runner.pages.PageSettings.addSettings( this.tName, respObj.settings, false );
		Runner.applyPagesData( respObj.pagesData );


//		row.isShown = true;
		
		if ( respObj.additionalCSS ) {
			Runner.util.ScriptLoader.loadCSS( respObj.additionalCSS );
		}
		
		if ( respObj.CSSFiles ) { // 8.1 version fix #9925
			Runner.util.ScriptLoader.loadCSS( respObj.CSSFiles );
		}		
		
		row.detPage = row.detPage || {};
		cfg = this.getNewDetailPageConfig( respObj, row, params.id );
		
		
		if ( Runner.pagesData[ params.id ].gridTabs ) {
			cfg.$gridTabControl = $tabsControl.children('ul');
			//	add attributes to the tab to get it replaced by the page class

		}
		
		if ( respObj.additionalJS ) {
			for ( jsFile in respObj.additionalJS ) {
				Runner.util.ScriptLoader.addJS( [ jsFile ], respObj.additionalJS[ jsFile ] );
			}
			
			Runner.util.ScriptLoader.onFilesLoaded( function() { 
				row.detPage[ this.tName ] = Runner.pages.PageManager.initPage( cfg ); 
				this.makeTmpAboveGrid(row.detPage[ this.tName ]);
			}, this );
				
			Runner.util.ScriptLoader.load();
		} else {
			row.detPage[ this.tName ] = Runner.pages.PageManager.initPage( cfg );
			this.makeTmpAboveGrid(row.detPage[ this.tName ]);
		}	
		
	},
	
	makeTmpAboveGrid: function(detPage) {
		var $aboveGridForm = detPage.layoutHelper.getFormElement("above-grid"),
			$previewItem = $('[data-details-'+ this.shortTName + '][data-record-id="' + detPage.rowId + '"]'),
			$newForm, $newCol, $newRow, $items;
			
		$newCol = $("<div/>")
			.addClass("r-align-left col-md-12")
			.attr("data-cellid", 'tmp-above-grid_c1')
			.attr("data-pageid", $aboveGridForm.attr("data-pageid"));

		$("[data-cellid]:first-child > [data-itemid]", $aboveGridForm).appendTo($newCol);

		$items = $("[data-details-inside][data-short-table=" + this.shortTName + "]", $previewItem).children();
		
		$items.appendTo($newCol);
		$items.attr("from-data-details-inside", '');

		$newRow = $("<div/>").addClass("row").append($newCol);
		$newForm = $("<div/>")
			.addClass("r-form")
			.attr("data-location", "tmp-above-grid")
			.attr("data-pageid", $aboveGridForm.attr("data-pageid"))
			.append($newRow);

		$newForm.insertAfter($aboveGridForm);
		$("[data-itemid]", $newForm).after(" ");				
		$aboveGridForm.remove();
	},
	
	revertTmpAboveGrid: function(detPage) {
		var $previewItem = $('[data-details-'+ detPage.shortTName + '][data-record-id="' + detPage.rowId + '"]'),
			$dataDetailsInside = $("[data-details-inside][data-short-table=" + detPage.shortTName + "]", $previewItem);

		detPage.layoutHelper.getFormElement("tmp-above-grid").find("[from-data-details-inside]").appendTo( $dataDetailsInside );
	},
	
	/**
	 * Initialize Grid Tabs Where functionality
	 *	This function is called in the context of the details page ( this == details page object )
	 */
	initGridTabs: function() {
		var pageObj = this;
		
		$('[data-pageid=' + this.id + '][data-tabid]').on("click", function(e) {			
			var $link = $(this),
				url = $(this).attr('href'),
				$tab = $link.parent(),
				$pane;
			
			if ( $tab.hasClass('active' ) ) {
				return;
			}
				
			$tab.siblings().removeClass('active');
			$tab.addClass('active');
			
			
			if ( $link.data('target') ) {
				$pane = $( $link.data('target') );
				$pane.addClass('active')
					.siblings().removeClass('active');
			}

			//	little trick to control tabs selection after reloading
			pageObj.$originalTab.addClass('active');
			
			pageObj.openContainer = pageObj.detCont[0];
			pageObj.silentReloading = true;
			
			pageObj.reload( { tab: $(this).data("tabid") } );			
			return false;
		});
	},
	
	
	/**
	 *	This function is called in the context of the details page ( this == details page object )
	 */
	replaceGridTabs: function() {
		if ( !this.$gridTabControl ) {
			return;
		}
		
		var $replacementTabs = $( this.pageData.gridTabs ),
			$originalTab = $('#bsproceed_' + this.shortTName + this.rowId ).closest('li'),
			$originalLink = $originalTab.children('a');
			
		$originalTab.hide();

		this.$gridTabControl.find('[data-tabid][data-pageid=' + this.id + ']').parent().remove();

		//	take care of the active tab
		if ( !$originalTab.hasClass('active') ) {
			$replacementTabs.removeClass('active');
		}
		
		this.$originalTab = $originalTab;
		
		$replacementTabs.each( function( i ) {
			var $tab = $(this),
				$link = $tab.children('a'),
				$span = $originalLink.children().clone(),
				attrMap = {};
				
			//	copy all attributes
			jQuery.each( $originalLink.get(0).attributes, function() {
				attrMap[ this.name ] = this.value;
			});
			
			//	update URL
			attrMap.href = attrMap.href + '&tab=' + $link.data('tabid');
			$span.attr('data-plink', attrMap.href );
			
				
			
			$link.attr( attrMap );
			$span.contents()[0].data = $link.text();
			$link.html('').append( $span );
		});
		
		$originalTab.after( $replacementTabs );
	},

	
	/**
	 * Create the html markup to show the details preview
	 * @param {object} row
	 */
	createPreviewRow: function( row ) {
		
		//row.detTd = $('[data-details-'+ this.shortTName + '][data-record-id="' + row.id + '"]');
		var $previewItem = $('[data-details-'+ this.shortTName + '][data-record-id="' + row.id + '"]');
		this.previewContainers[row.id] = $previewItem.children('[data-details-preview]');
		this.previewItemId = this.previewItemId || $previewItem.attr('data-itemid');
		this.parPageObj.toggleItem( this.previewItemId, true, row.id );
	},
	
	
	/**
	 * @param {string} newId
	 * @param {object} masterKeys
	 */
	getDetailPageBaseParams: function( newId, masterKeys ) {
		return Runner.apply( {
				id: newId,
				mode: Runner.pages.constants.MODE_LIST_DETAILS,
				rndVal: Math.random(),
				masterpagetype: this.masterPageType,
				mastertable: this.masterTName
			}, masterKeys );

	},
	
	/**
	 * Close the inline details, destroy detail's page
	 * @param {object} row
	 * @param {jQuery object} $link
	 */
	closeDetails: function( row, $link ) {
		if ( !row.isShown ) {
			return;
		}
		
		for ( var tName in row.detPage ) {
			Runner.pages.PageManager.unregister( row.detPage[ tName ].tName, row.detPage[ tName ].pageId );
			this.revertTmpAboveGrid(row.detPage[ tName ]);
		}
		
		var previewContainer = this.previewContainers[ row.id ] || 
			$('[data-details-' + this.shortTName + '][data-record-id="' + row.id + '"]')
				.children('[data-details-preview]');
		
		previewContainer.empty();
		delete previewContainer;
		
		row.row.removeClass( "bs-details-opened" );
		this.parPageObj.toggleItem( this.previewItemId, false, row.id );
		row.isShown = false;
	},
	
	/**
	 * @param {number} ind
	 */
	closeDetailsByInd: function( ind ) {
		var row = this.getRowByInd( ind );
		
		if ( row ) {
			this.closeDetails( row );
		}
	},
	
	/**
	 * @param {number} rowId
	 */
	closeDetailsById: function( rowId ) {
		var row = this.getRowById( rowId );
		
		if ( row ) {
			this.closeDetails( row );
		}
	},
	
	/**
	 * //? work logic ?
	 * @param {object} row
	 * @return {boolean}
	 */
	isTdReset: function( row ) {
		var removeTd = false,
			i, dpTd;
		
		for ( i = 0; i < row.detLinksId.length; i++ ) {
			dpTd = $( "#" + this.masterShortTName + "_previewcell" + row.detLinksId[ i ] );
			
			if ( row.detLinksId[i] != row.id ) {
				if ( dpTd.html() != '' ) {
					return false;
				} 
				
				removeTd = true;
			}
		}
		
		return removeTd;
	},
	
	/**
	 * Set the details preview container empty
	 * @param {object} row
	 */
	resetCell: function( row ) {
		this.previewContainers[row.id].empty();
	},
	
	/**
	 *
	 */
	destructor: function() {
		for (var i = 0; i < this.rows.length; i++) {
			this.closeDetails( this.rows[i] );
		}
	},

	tabControlName: function( row ) {
		return "details_" + this.previewItemId + row.id;
	},
	
	getNewDetailPageConfig: function( respObj, row, newId ) {	
		var cfg = Runner.util.details.NewListPreview.superclass.getNewDetailPageConfig.call( this, respObj, row, newId );
		
		//add link to master
		cfg.masterPageObj = this.masterPageObj;
		
		cfg.tabSettings.tabControlName = this.tabControlName( row );
		cfg.tabControl = Runner.pages.PageManager.getTabControl( this.previewContainers[ row.id ], "details_" 
			+ this.previewItemId + row.id );
			
		cfg.openContainer = this.previewContainers[ row.id ];

		return cfg;
	},

	initLink: function( row, setHref ) {
		var dpObj = this,
			href, mKeys, key,
			$allDetailsLink = $("[id=details_" + row.id + "]"),
			$singleDetailsLink = $("[id=details_" + row.id + "_" + this.shortTName + "]"),
			$links = $singleDetailsLink;
		
		$links.each( function() {
			var $link = $(this);
			if ( $link.data("initialized") ) {
				return;
			}
			$link.on("click", function(e) {
				return dpObj.linkClickHandler( row, e );
			})
			.data("initialized", true);
		});

		//	initialize all details link
		//	must open tab with the smallest index
		var prevTabIndex = $allDetailsLink.data('openTabIndex');
		if ( prevTabIndex === undefined || prevTabIndex > this.tabSettings.tabIndex ) {
			$allDetailsLink.off('click.details').on( 'click.details', function( e ) {
				return dpObj.linkClickHandler( row, e );
			} )
			.data('openTabIndex', this.tabSettings.tabIndex );
		}
		
		if ( setHref === true ) {
			mKeys = [];
			for (key in row.masterKeys[ this.tName ]) {
				mKeys.push( row.masterKeys[ this.tName ][ key ] );
			}
			href = Runner.pages.getUrl( this.tName, this.pageType, mKeys, "masterkey" ) + "&mastertable=" + encodeURIComponent(this.masterTName);
			$links.attr('href', href);
		}
	},
	
	/**
	 * show all details
	 * @param {object} row
	 * @param {jQuery event object} e
	 */
	linkClickHandler: function( row, e ) {
		var switchCondition = false,
			closeCondition = row.isShown,
			$link = $( e.target ),
			corTabIdx;
		
		var $clickedItemElem = $(e.target).closest('[data-itemid]');
		if ( row.isShown && $clickedItemElem.attr('data-itemtype') == 'grid_details_link') {
			switchCondition = $(".active > a[data-target=\"#tabdetails_" + this.previewItemId + row.id +"_" + this.shortTName + "\"]" ).length === 0;
		} 
		
		if ( switchCondition ) {
			$("a[data-target=\"#tabdetails_" + this.previewItemId + row.id +"_" + this.shortTName + "\"]").eq(0).trigger("click");
			return false;	
		}
		
		if ( closeCondition ) {
			this.closeDetails( row, $link );
			return false;
		} 
		
		this.openDetailsTabs( row, $link );		
		return false;
	},
	
	/**
	 * update grid_alldetails_link visibility
	 * basing on detail tables records recount
	 * @param {number} rowId
	 * @param {number} detNum
	 */	 
	updateAllLinkVisibility: function( rowId, count ) {
		var $allLink = $("#details_" + rowId);
		
		var countData = $allLink.data('countdata') || {};
		countData[ this.shortTName ] = count;
		
		var toggler = !!Object.keys( countData ).filter( function( name ) {
				return countData[ name ] !== 0;
			}).length;

		$allLink.toggle( toggler );
		$allLink.data('countdata', countData);
	},
	
	/**
	 * @param {number} rowId
	 * @param {string} detNum
	 * @param {array} masterKeys
	 */
	updatePreviewVisibility: function( rowId, detNum, masterKeys ) {
		if ( !this.hideEmptyPreview ) {
			return;
		}

		if ( rowId === undefined && masterKeys ) {
			this.rows.forEach( function( _row, idx ) {
				if ( Runner.arraysEqual( masterKeys, _row.masterKeys[ this.tName ] ) ) {
					this.updateRowPreviewVisibility( _row, detNum );
				}
			}, this);
		
			return;
		}

		this.updateRowPreviewVisibility( this.getRowById( rowId ), detNum );
	},
	
	/**
	 * @param {row} object | null
	 * @param {string} detNum
	 */
	updateRowPreviewVisibility: function( row, detNum )	{
		if ( !row ) {
			return;
		}

		row.hiddenPreviewTabs = row.hiddenPreviewTabs || [];
		var idx = row.hiddenPreviewTabs.indexOf( this.tName );

		if ( !parseInt( detNum, 10 ) ) {
			if ( idx === -1 ) {
				row.hiddenPreviewTabs.push( this.tName );
			}
		} else {
			if ( idx !== -1 ) {
				row.hiddenPreviewTabs.splice( idx, 1 );
			}
		}
	},
});
	
/**
export class CellMapElement {
	cols: number[] = [];
	rows: number[] = [];
	visibleCols: number[] = [];
	// list of items that can be hidden at the server
	tags: string[] = [];
	// list of items that can be hidden at the client
	items: string[] = [];
	fixedAtServer: boolean;
	fixedAtClient: boolean;
}
 * 
 * 
 * @param {*} page 
 * @param {*} layoutHelper 
 */


Runner.ItemDisplayCondition = {
	Always: 0,
	MobileOpened: 1,
	MobileAlways: 2,
	DesktopMobileOpened: 3,
	DesktopMobileAlways: 4,
	Desktop: 5,
}


Runner.PDLayout = function ( page, layoutHelper ) {
	this.page = page;
	this.formMap = layoutHelper.formMap;
	this.itemsByType = layoutHelper.itemsByType;
	this.gridCellMap = layoutHelper.gridCellMap;
	this.itemVisiblity = layoutHelper.itemVisiblity || {};
	this.fieldItems = layoutHelper.fieldItems;
}

Runner.PDLayout.prototype = {

	itemQuery: function ( id, recordId ) {
		if ( recordId === undefined ) {
			return '[data-itemid=' + id + '][data-pageid=' + this.page.id + ']';
		}
		return '[data-itemid=' + id + '][data-pageid=' + this.page.id + '][data-record-id=' + recordId + ']';
	},

	findForm: function( location, context ) {
		return this.getFormElement( location, context );
	},

	getFormElement: function( formLocation, context ) {
		return $( '#form_' + formLocation + '_' + this.page.id, context );
	},


	findItem: function( id, recordId, context ) {
		if( !id ) {
			return $();
		}
		return $( this.itemQuery( id, recordId ), context );
	},

	/**
	 * 
	 * @param {string} type 
	 * @param {number} optional recordId 
	 * @param {*} context 
	 * @returns array
	 */
	findItemType: function( type, recordId, context ) {
		if( !this.itemsByType ) {
			return $([]);
		}
		var self = this;
		return (this.itemsByType[ type ] || []).reduce( function( obj, itemId ) {
			return obj.add( self.itemQuery( itemId, recordId ), context );
		}, $() );
	},

	/**
	 * Return item array of ids of items of certain type
	 * @param {string} type 
	 * @returns { array }
	 */
	getItemIds: function( type ) {
		return this.itemsByType[ type ] || [];
	},


	hasVisibleItem: function ( id, recordId, $formElement ) {
		return $( this.itemQuery( id, recordId ) + ':not([data-hidden]):not([data-media-hidden]):first', $formElement ).length != 0;
	},

	findCell: function ( formLocation, cellId, recordId ) {
		var searchString = '[data-cellid=' + formLocation + '_' + cellId + '][data-pageid=' + this.page.id + ']';
		if ( recordId !== undefined ) {
			searchString += '[data-record-id=' + recordId + ']';
		}
		
		return $( searchString );
	},

	findCellRow: function ( formLocation, rowIdx, recordId ) {
		return this.findCell( formLocation, rowIdx, recordId ).parent();
	},


	toggleItem: function ( itemId, show, recordId ) {
		this.toggleItems([ {
			itemId: itemId,
			show: show,
			recordId: recordId,
		}] );
	},

	elementVisible: function( $elem ) {
		return ($elem.attr('data-hidden') === undefined 
			&& $elem.attr('data-media-hidden') === undefined);
	},

	/**
	 * Returns array, subset of itemList with visible only items
	 * @param {array} itemList 
	 */
	getVisibleItems: function( itemList ) {
		var pd = this;
		return itemList.filter( function(id) {
			return pd.elementVisible( pd.findItem( id ) );
		});
	},

	/**
	 * @param items Array of { itemId, show, recordId }
	 * @param mediaToggle boolean. True when toggle is related to screen media only. False when toggling due to application logic.
	 * Examples:
	 * When screen changes from mobile to desktop, toggleItems is called with mediaToggle == true
	 * When user clicks mobile [-] button, mediaToggle == true
	 * When user displays details preview or does show/hide search panel, mediaToggle == false( undefined )
	 */
	toggleItems: function ( items, mediaToggle ) {
		/* update all item elements first */
		var attribute = mediaToggle ? 'data-media-hidden' : 'data-hidden';
		var _this = this;
		var modifiedItems = [];
		
		items.forEach( function( item ) {
			var $itemElem = _this.findItem( item.itemId, item.recordId );
			
			//	topbar items must be controlled through their containers
			if ( $itemElem.length == 1 && $itemElem.parent().is('[data-itemid="' + item.itemId + '"]') ) {
				$itemElem = $itemElem.add( $itemElem.parent() );
			}
			
			var wasVisible = _this.elementVisible( $itemElem );
		
			if ( $itemElem.length == 1 && item.show === ( $itemElem.attr( attribute ) === undefined ) ) {
				//	nothing to change
				return;
			}
			$itemElem.attr( attribute, ( !item.show ) || null );

			//	since there are two attributes controlling item visibility, changing 
			//	one of them may not lead to item being hidden or shown. We need to find its state out.
			var nowVisible = _this.elementVisible( $itemElem );
			
			// update the form only if actual visibility of the item has changed
			if ( wasVisible !== nowVisible ) {
				modifiedItems.push( item );
			}
		});


		/* update cells visibility */

		/**
		 * For performance reasons, ensure that each row and column is processed only once
		 * processedRowsCols = {
		 * 		<location>: {
		 * 			rows: { <row_recordId>: true },
		 * 			cols: { <col>: true }
		 * 		}
		 * }
		 */
		var processedRowsCols = {};

		var formsToShow = {};
		var formsToHide = {};
		
		//	show cells that need to be shown first
		modifiedItems
		.filter(function( item ) { return item.show; } )
		.forEach( function( item ) {
			var formLocation = _this.formMap.itemForms[ item.itemId ];
			formsToShow[ formLocation ] = true;
			_this.updateFormCells( formLocation, item.itemId, item.recordId, item.show, processedRowsCols );
		});

		//	hide cells that need to be hidden
		modifiedItems
		.filter(function( item ) { return !item.show; } )
		.forEach( function( item ) {
			var formLocation = _this.formMap.itemForms[ item.itemId ];
			formsToHide[ formLocation ] = true;
			_this.updateFormCells( formLocation, item.itemId, item.recordId, item.show, processedRowsCols );
		});

		// show whole forms
		Object.keys( formsToShow ).forEach( function( location ) {
			_this.updateFormVisiblity( location, true );
		});
		
		// hide whole forms
		Object.keys( formsToHide ).forEach( function( location ) {
			if ( !formsToShow[ location ] ) {
				_this.updateFormVisiblity( location, false );
			}
		});

	},


	/**
	 * 
	 */
	updateFormCells: function ( formLocation, itemId, recordId, show, processedRowsCols ) {
		var self = this;
		if ( this.gridCellMap === undefined || !this.gridCellMap[formLocation] ) {
			return;
		}

		var itemLoc = this.formMap.itemLocations[ itemId ];
		if ( itemLoc === undefined || itemLoc.location !== formLocation ) {
			return;
		}

		var map = this.gridCellMap[formLocation];
		var cell = map.cells[itemLoc.cellId];

		// prepare data for future updateFormCells calls
		cell = this.skipAndUpdateProcessedRowsCols( processedRowsCols, formLocation, cell, recordId, show );
		if ( !cell.rows.length && 
			( 
				show && !cell.cols.length || 
				!show && !cell.visibleCols.length 
			)
		) {
			return;
		}

		if ( show ) {
			//	make sure the row is displayed the row first 
			this.showRowOnly( formLocation, itemLoc.cellId, recordId );
			//	show the row and column the cell belongs to
			if ( !this.cellColVisible( formLocation, itemLoc.cellId ) ) {
				this.toggleCol( formLocation, cell.cols[0], true );
			}
			if ( !this.cellVisible( formLocation, itemLoc.cellId, recordId ) ) {
				//	show the first row
				this.toggleRow( formLocation, cell.rows[0], recordId, true );
			}
		} else {
			//	check if we need to hide the whole column or row the cell belongs to
			cell.rows.forEach( function ( rowIdx ) {
				if ( self.canHideRow( formLocation, rowIdx, recordId ) ) {
					self.toggleRow( formLocation, rowIdx, recordId, false );
				}
			});
			cell.visibleCols.forEach( function ( colIdx ) {
				if ( self.canHideCol( formLocation, colIdx ) ) {
					self.toggleCol( formLocation, colIdx, false );
				}
			});
		}
	},

	/**
	 * Adds mapCell data to processedRowsCols
	 * Returns a copy of mapCell with already processed rows and cols removed
	 */
	skipAndUpdateProcessedRowsCols: function( processedRowsCols, formLocation, mapCell, recordId, show ) {
		var _this = this;
		var formRowsCols = processedRowsCols[ formLocation ];
		
		if ( !formRowsCols ) {
			formRowsCols = {
				rows: {},
				cols: {}
			};
			processedRowsCols[ formLocation ] = formRowsCols;	
		}
		
		var ret = {
			rows:[],
			cols:[],
			visibleCols:[]
		};

		// process rows
		mapCell.rows.forEach( function( row, idx ) {
			if ( show && idx > 0 ) {
				// only first row matters when showing
				return;
			}
			
			var rowKey = row + '_' + recordId;
			if ( formRowsCols.rows[ rowKey ] ) {
				//	row already processed
				return;
			}
			
			ret.rows.push( row );
			formRowsCols.rows[ rowKey ] = true;
		});

		// process cols
		srcCols = show ? mapCell.cols : mapCell.visibleCols;
		dstCols = show ? ret.cols : ret.visibleCols;

		if( show && mapCell.visibleCols.length !== 0 ) {
			// one of the cell columns is already visible. No need to do anything else
			return ret;
		}

		srcCols.forEach( function( col ) {
			if ( show && dstCols.length ) {
				// only first col matters when showing
				return;
			}
			
			if ( formRowsCols.cols[ col ] ) {
				//	col already processed
				return;
			}
			dstCols.push( col );
			formRowsCols.cols[ col ] = true;
		});
		return ret;
	},

	/**
	 * Show form or hide if all of its items are hidden
	 * @param {string} formLocation 
	 * @param {boolean} show 
	 */
	updateFormVisiblity: function( formLocation, show ) {
		var $formElement = this.getFormElement(formLocation);
		
		if ( $formElement.siblings( '[data-location]' ).length == 0 ) {
			$formElement = $formElement.add( $formElement.parent() );
		}		

		var formHidden = $formElement.is( '[data-hidden]' );
		if ( formHidden == !show ) {
			return;
		}
		
		if ( show ) {
			$formElement.attr( 'data-hidden', null );
			return;
		}

		// check if the form should be hidden
		
		for ( var i in this.formMap.formItems[formLocation] ) {
			if ( this.hasVisibleItem( this.formMap.formItems[formLocation][i], undefined, $formElement ) ) {
				return;
			}
		}
		$formElement.attr( 'data-hidden', '' );
	},

	toggleForm: function( formLocation, show ) {
		var $formElement = this.getFormElement(formLocation);
		
		//	left bar in 'topbar' layout only. Show/hide 'parent' element instead
		if ( $formElement.parent().is( '[data-formblock]' ) ) {
			$formElement = $formElement.parent();
		}

		$formElement.attr( 'data-hidden', show ? null : '' );
	},

	/**
	 * 
	 * @param {string} itemType
	 * @param {boolean} show 
	 */
	toggleItemType: function ( itemType, show, recordId ) {
		var itemIds = this.itemsByType[itemType] || [],
			self = this;
		
		this.toggleItems( itemIds.map( function( itemId ) {
			return {
				itemId: itemId,
				show: show,
				recordId: recordId
			};
		}));
	},

	/**
	 * 
	 * @param {string} itemType
	 */
	hasItemType: function ( itemType ) {
		var itemIds = this.itemsByType[itemType] || [];		
		return itemIds.length > 0;
	},


	/**
	 * Checks if any of the columns the cell belongs to are
	 *  visible
	 * @param {string} cellId
	 * @return {boolean}
	 */
	cellColVisible: function ( formLocation, cellId ) {
		var map = this.gridCellMap[formLocation];
		var cellCols = map.cells[cellId].cols;

		return Object.keys( map.cells ).filter( function ( cell ) {
			return map.cells[cell].visibleCols.filter( function ( colIdx ) {
				return cellCols.indexOf( colIdx  ) !== -1;
			} ).length !== 0;
		} ).length !== 0;

	},
	/**
	 * 
	 * @param {string} formLocation 
	 * @param {integer} colIdx 
	 * @param {boolean} show 
	 */
	toggleCol: function ( formLocation, colIdx, show ) {
		var self = this;
		var map = this.gridCellMap[formLocation];
		var colCells = Object.keys( map.cells ).filter( function ( cellId ) {
			return map.cells[cellId].cols.indexOf( colIdx ) !== -1;
		} );
		colCells.forEach( function ( cellId ) {
			var cell = map.cells[cellId];
			if ( show ) {
				cell.visibleCols.push( colIdx );
				var $cells = self.findCell( formLocation, cellId );
				//	update colspans
				if ( cell.visibleCols.length > 1 ) {
					$cells.attr( 'colspan', cell.visibleCols.length );
				}
				//	only show cells in visible rows
				$cells.each( function() { 
					$c = $(this);
					if( $c.parent().attr('data-hidden') === undefined ) {
						$c.attr( 'data-hidden', null );
					}
				} );
			} else {
				cell.visibleCols = cell.visibleCols.filter( function ( idx ) {
					return idx !== colIdx;
				} );
				
				var cells = self.findCell( formLocation, cellId );
				cells.attr( {
					'colspan': cell.visibleCols.length > 1 ? cell.visibleCols.length : null,
				} );
				
				if ( cell.visibleCols.length === 0 ) {
					cells.attr( 'data-hidden', '');
				}
			}
		} );
	},

	showRowOnly: function ( formLocation, cellId, recordId ) {
		var $row = this.findCellRow( formLocation, cellId, recordId );
		if ( $row.attr('data-hidden') !== undefined ) {
			$row.attr( 'data-hidden', null );
		}
	},

	cellRowVisible: function ( formLocation, cellId, recordId ) {
		var $row = this.findCellRow( formLocation, cellId, recordId );
		return $row.attr('data-hidden') === undefined;
	},

	toggleRow: function ( formLocation, rowIdx, recordId, show ) {
		var self = this;
		var map = this.gridCellMap[formLocation];
		var rowCells = Object.keys( map.cells ).filter( function ( cellId ) {
			return map.cells[cellId].rows.indexOf( rowIdx ) !== -1;
		} );
		var cellsArr = [];
		rowCells.forEach( function ( cellId ) {
			var cell = map.cells[cellId];
			//	show only cells in visible columns
			if( show && cell.visibleCols.length == 0 ) {
				return;
			}
			var $cell = self.findCell( formLocation, cellId, recordId );
			cellsArr = cellsArr.concat( $cell.toArray() );
			if ( show ) {
				if ( $cell.attr( 'data-hidden' ) !== undefined ) {
					$cell.attr( 'data-hidden', null );
					$cell.attr( 'rowspan', null );
				} else {
					var rowspan = $cell.attr( 'rowspan' ) || 1;
					$cell.attr( 'rowspan', rowspan + 1 );
				}
			} else {
				var rowspan = $cell.attr( 'rowspan' ) || 1;
				if ( rowspan > 1 ) {
					$cell.attr( 'rowspan', rowspan - 1 );
				} else {
					$cell.attr( 'rowspan', null );
					$cell.attr( 'data-hidden', '' );
				}
			}
		} );
		$( cellsArr ).closest('tr').attr('data-hidden', show ? null : '' );

	},

	cellVisible: function ( formLocation, cellId, recordId ) {
		var $cell = this.findCell( formLocation, cellId, recordId );
		return !$cell.is( '[data-hidden]' ) && !$cell.closest('tr').is( '[data-hidden]' );
	},

	canHideRow: function ( formLocation, rowIdx, recordId ) {
		var self = this;
		var map = this.gridCellMap[ formLocation ];
		var rowCells = Object.keys( map.cells ).filter( function ( cellId ) {
			return map.cells[ cellId ].rows.indexOf( rowIdx ) !== -1;
		});

		if ( rowCells.filter( function( cellId ) {
				return !!map.cells[ cellId ].fixedAtClient;
			}).length !== 0 ) {
			return false;
		}

		var rowItems = rowCells.reduce( function ( items, cellId ) {
			return items.concat( map.cells[ cellId ].items );
		}, [] );

		return !rowItems.filter( function ( itemId ) {
				return self.hasVisibleItem( itemId, recordId );
			}).length;

		/*return rowItems.find( function ( itemId ) {
			return self.hasVisibleItem( itemId, recordId );
		}) === undefined;*/
	},
	
	canHideCol: function ( formLocation, colIdx ) {
		var self = this;
		var map = this.gridCellMap[ formLocation ];
		var colCells = Object.keys( map.cells ).filter( function ( cellId ) {
			return map.cells[ cellId ].cols.indexOf( colIdx ) !== -1;
		});

		if ( colCells.filter( function( cellId ) {
				return !!map.cells[ cellId ].fixedAtClient;
			}).length !== 0 ) {
			return false;
		}

		var colItems = colCells.reduce( function ( items, cellId ) {
			return items.concat( map.cells[ cellId ].items );
		}, [] );

		return !colItems.filter( function ( itemId ) {
				return self.hasVisibleItem( itemId );
			}).length;
		
		/*return colItems.find( function ( itemId ) {
			return self.hasVisibleItem( itemId );
		}) === undefined;*/
	},

	/**
	 * update visibility of each media-dependent item
	 * @param {number} media 
	 * 0 - desktop
	 * 1 - mobile, 'more' closed
	 * 2 - mobile, 'more' opened
	 */
	updateMedia: function( media ) {
		var _this = this;
		
		var items = Object.keys( this.itemVisiblity ).map( function( itemId ) {
			return {
				itemId: itemId,
				show: !!_this.mediaMap[media][ _this.itemVisiblity[ itemId ] ],
			};
		});
		this.toggleItems( items, true );
	},

	/**
	 * IE doesn't support this syntax:
	 * 
	 * 
	mediaMap: {
		0: { 
			[Runner.ItemDisplayCondition.Always]: true,
			[Runner.ItemDisplayCondition.DesktopMobileOpened]: true,
			[Runner.ItemDisplayCondition.DesktopMobileAlways]: true,
			[Runner.ItemDisplayCondition.Desktop]: true,
		}, 
		1: {
			[Runner.ItemDisplayCondition.Always]: true,
			[Runner.ItemDisplayCondition.MobileAlways]: true,
			[Runner.ItemDisplayCondition.DesktopMobileAlways]: true,
		},
		2: {
			[Runner.ItemDisplayCondition.Always]: true,
			[Runner.ItemDisplayCondition.MobileAlways]: true,
			[Runner.ItemDisplayCondition.DesktopMobileOpened]: true,
			[Runner.ItemDisplayCondition.MobileOpened]: true,
			[Runner.ItemDisplayCondition.DesktopMobileAlways]: true,
		}
	}
	*/

	mediaMap: {
		0: { 
			0: true,
			3: true,
			4: true,
			5: true,
		}, 
		1: {
			0: true,
			2: true,
			4: true,
		},
		2: {
			0: true,
			2: true,
			3: true,
			1: true,
			4: true,
		}
	},

	getFieldItems: function( field ) {
		return this.fieldItems[ field ] || [];
	},

	toggleField: function ( field, show ) {
		this.toggleItems( this.getFieldItems( field ).map( function( id ) {
			return { itemId: id, show: show };
		}) );
	}


}

	
/**
 * @class Runner.util.details.PDPopupDetails
 * provides base functionality for popup details on list page
 */
Runner.util.details.PDPopupDetails = Runner.extend( Runner.util.details.DPPopUp, {
	
	/**
	 *
	 */
	constructor: function( cfg ) {
		Runner.util.details.PDPopupDetails.superclass.constructor.call( this, cfg );
	},	
	
	getUrl: function( page ) {
		return Runner.getPageUrl( this.shortTName, page );
	}
	
});
	


/**
 * InlineEditor row object wrapper
 * @constructor
 * @param {object} inlineRow
 */
Runner.AjaxRow = function( pageObj, inlineRow ) {
	this.row = inlineRow;
	this.pageObj = pageObj;
	
	if ( !inlineRow ) {
		// A stub
		this.row = {
			id: 1,
			gridLayout: ""
		};
	}
}

Runner.AjaxRow.prototype = {
	/**
	 * @param {string} fName
	 * @return {jQuery object}
	 */
	fieldCell: function( fName ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );

		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT ) {
			return $span.closest("div");
		}
		
		return $span.closest("[data-field]");
	},
	
	/**
	 * @param {string} fName
	 * @return {string}
	 */
	getFieldText: function( fName ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		return $span.html();
	},
	
	/**
	 * @param {string} fName
	 * @param {string} text
	 */	
	setFieldText: function( fName, text ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		$span.html( text );		
	},
	
	/**
	 * @param {string} fName
	 * @return {mixed}
	 */
	getFieldValue: function( fName ) {
		var ctrl = Runner.getControl( this.row.id, fName );
		if ( ctrl ) {
			return ctrl.getValue();
		}
		if ( this.row.rowVals && typeof this.row.rowVals[ fName ] != 'undefined' ) {
			return this.row.rowVals[ fName ];
		}
		
		var $span = Runner.getFieldSpan( fName, this.row.id );
		return ($span.attr("val") || "").trim();		
	},
	
	/**
	 * @param {string} fName
	 * @param {mixed} value
	 */	
	setFieldValue: function( fName, value ) {
		var $span = Runner.getFieldSpan( fName, this.row.id );
		$span.attr("val", value);		
	},
	
	/**
	 * @return {jQuery object}
	 */
	record: function() {
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			return $(".panel-body", "[data-record-id='" + this.row.id + "']");
		}	
		return $("[data-record-id='" + this.row.id + "'][data-field]"); //layout dep ?
	},
	
	/**
	 * @return {jQuery object}
	 */
	getDefaultMessageCell: function() {
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			return this.record();
		}
		
		return this.record().eq(0);
	},
	
	/**
	 * @param {string} message
	 * @param {mixed} container
	 */
	setMessage: function( message, container ) {
		var $messageWrap;
		
		if ( !container && this.row.messageContainer ) {
			this.row.messageContainer.html( message );
			return;	
		}

		container = container || this.getDefaultMessageCell();
		$messageWrap = $(".row-message", container);
			
		if ( $messageWrap.length ) {
			this.row.messageContainer = $messageWrap.html( message );
			return;
		}
		
		if ( this.row.gridLayout === Runner.pages.constants.VERTICAL_LAYOUT || this.row.gridLayout === Runner.pages.constants.COLUMNS_LAYOUT ) {
			this.row.messageContainer = $("<div class='row-message'></div>").html( message ).prependTo( container );
		} else {
			this.row.messageContainer = $("<span class='row-message'></span>").html( message ).appendTo( container );
		}		
	},
	
	/**
	 *
	 */
	getMessage: function() {
		if ( this.row.messageContainer ) {
			return this.row.messageContainer.html();
		}
		
		return "";
	},
	
	/**
	 * @return {string}
	 */
	id: function() {
		return this.row.id;
	},
	
	recordId: function() {
		return this.row.id;
	},

	/**
	 * @return {object}
	 */
	getRowObject: function() {
		return this.row;
	},

	/**
	 * @return {Array} - array of key field values
	 */
	getKeys: function() {
		return this.row.keys;
	},

	getDetails: function() {
		return this.row.detPage;
	}

};
Runner.PDF = {
	customTableLayouts: {
		vrecord: {
			hLineWidth: function( i, node ) {
				return ( i === 0 || i === node.table.body.length ) ? 0.1 : 0;
			},
			vLineWidth: function( i, node ) {
				return ( i === 0 || i === node.table.widths.length ) ? 0.1 : 0;
			},
			hLineColor: function( i, node ) { return 'gray'; },
			vLineColor: function( i, node ) { return 'gray'; },
		},
		panel: {
			hLineWidth: function( i, node ) {
				return ( i < 2 || i === node.table.body.length ) ? 0.1 : 0;
			},
			vLineWidth: function( i, node ) {
				return ( i === 0 || i === node.table.widths.length ) ? 0.1 : 0;
			},
			hLineColor: function( i, node ) { return 'gray'; },
			vLineColor: function( i, node ) { return 'gray'; },
		}
	},

	styleProps: [
		"font", 
		"fontSize", 
		"fontFeatures", 
		"lineHeight", 
		"bold", 
		"italics", 
		"alignment", 
		"color", 
		"background", 
		"markerColor", 
		"decoration", 
		"decorationStyle", 
		"decorationColor"
	],
	
	/**
	 * @boolean
	 */
	applyBidi: true,

	/**
	 * "AL", "L", "R" bidi classes regexps
	 */
	alRegexp: (/[\u0608\u060B\u060D\u061B\u061C\u061E-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u08A0-\u08B4\u08B6-\u08BD\uFB50-\uFBC1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]/),
	lRegexp: (/[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02B8\u02BB-\u02C1\u02D0\u02D1\u02E0-\u02E4\u02EE\u0370-\u0373\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0482\u048A-\u052F\u0531-\u0556\u0559-\u055F\u0561-\u0587\u0589\u0903-\u0939\u093B\u093D-\u0940\u0949-\u094C\u094E-\u0950\u0958-\u0961\u0964-\u0980\u0982\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD-\u09C0\u09C7\u09C8\u09CB\u09CC\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09FA\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3E-\u0A40\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD-\u0AC0\u0AC9\u0ACB\u0ACC\u0AD0\u0AE0\u0AE1\u0AE6-\u0AF0\u0AF9\u0B02\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B3E\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0BD0\u0BD7\u0BE6-\u0BF2\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C41-\u0C44\u0C58-\u0C5A\u0C60\u0C61\u0C66-\u0C6F\u0C7F\u0C80\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD-\u0CC4\u0CC6-\u0CC8\u0CCA\u0CCB\u0CD5\u0CD6\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D4E\u0D4F\u0D54-\u0D61\u0D66-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCF-\u0DD1\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2-\u0DF4\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E4F-\u0E5B\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00-\u0F17\u0F1A-\u0F34\u0F36\u0F38\u0F3E-\u0F47\u0F49-\u0F6C\u0F7F\u0F85\u0F88-\u0F8C\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE-\u0FDA\u1000-\u102C\u1031\u1038\u103B\u103C\u103F-\u1057\u105A-\u105D\u1061-\u1070\u1075-\u1081\u1083\u1084\u1087-\u108C\u108E-\u109C\u109E-\u10C5\u10C7\u10CD\u10D0-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1360-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u167F\u1681-\u169A\u16A0-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1735\u1736\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17B6\u17BE-\u17C5\u17C7\u17C8\u17D4-\u17DA\u17DC\u17E0-\u17E9\u1810-\u1819\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A19\u1A1A\u1A1E-\u1A55\u1A57\u1A61\u1A63\u1A64\u1A6D-\u1A72\u1A80-\u1A89\u1A90-\u1A99\u1AA0-\u1AAD\u1B04-\u1B33\u1B35\u1B3B\u1B3D-\u1B41\u1B43-\u1B4B\u1B50-\u1B6A\u1B74-\u1B7C\u1B82-\u1BA1\u1BA6\u1BA7\u1BAA\u1BAE-\u1BE5\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1BFC-\u1C2B\u1C34\u1C35\u1C3B-\u1C49\u1C4D-\u1C88\u1CC0-\u1CC7\u1CD3\u1CE1\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200E\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u214F\u2160-\u2188\u2336-\u237A\u2395\u249C-\u24E9\u26AC\u2800-\u28FF\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D70\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u302E\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31F0-\u321C\u3220-\u324F\u3260-\u327B\u327F-\u32B0\u32C0-\u32CB\u32D0-\u32FE\u3300-\u3376\u337B-\u33DD\u33E0-\u33FE\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA60C\uA610-\uA62B\uA640-\uA66E\uA680-\uA69D\uA6A0-\uA6EF\uA6F2-\uA6F7\uA722-\uA787\uA789-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA824\uA827\uA830-\uA837\uA840-\uA873\uA880-\uA8C3\uA8CE-\uA8D9\uA8F2-\uA8FD\uA900-\uA925\uA92E-\uA946\uA952\uA953\uA95F-\uA97C\uA983-\uA9B2\uA9B4\uA9B5\uA9BA\uA9BB\uA9BD-\uA9CD\uA9CF-\uA9D9\uA9DE-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA2F\uAA30\uAA33\uAA34\uAA40-\uAA42\uAA44-\uAA4B\uAA4D\uAA50-\uAA59\uAA5C-\uAA7B\uAA7D-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAAEB\uAAEE-\uAAF5\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB65\uAB70-\uABE4\uABE6\uABE7\uABE9-\uABEC\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uE000-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD00\uDD02\uDD07-\uDD33\uDD37-\uDD3F\uDD8D\uDD8E\uDDD0-\uDDFC\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF23\uDF30-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDF9F-\uDFC3\uDFC8-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD6F\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD804[\uDC00\uDC02-\uDC37\uDC47-\uDC4D\uDC66-\uDC6F\uDC82-\uDCB2\uDCB7\uDCB8\uDCBB-\uDCC1\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD2C\uDD36-\uDD43\uDD50-\uDD72\uDD74-\uDD76\uDD82-\uDDB5\uDDBF-\uDDC9\uDDCD\uDDD0-\uDDDF\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2E\uDE32\uDE33\uDE35\uDE38-\uDE3D\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA9\uDEB0-\uDEDE\uDEE0-\uDEE2\uDEF0-\uDEF9\uDF02\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D-\uDF3F\uDF41-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63]|\uD805[\uDC00-\uDC37\uDC40\uDC41\uDC45\uDC47-\uDC59\uDC5B\uDC5D\uDC80-\uDCB2\uDCB9\uDCBB-\uDCBE\uDCC1\uDCC4-\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB1\uDDB8-\uDDBB\uDDBE\uDDC1-\uDDDB\uDE00-\uDE32\uDE3B\uDE3C\uDE3E\uDE41-\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEAC\uDEAE\uDEAF\uDEB6\uDEC0-\uDEC9\uDF00-\uDF19\uDF20\uDF21\uDF26\uDF30-\uDF3F]|\uD806[\uDCA0-\uDCF2\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2F\uDC3E-\uDC45\uDC50-\uDC6C\uDC70-\uDC8F\uDCA9\uDCB1\uDCB4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC70-\uDC74\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uDB80-\uDBBE\uDBC0-\uDBFE][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE6E\uDE6F\uDED0-\uDEED\uDEF5\uDF00-\uDF2F\uDF37-\uDF45\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9C\uDC9F]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD66\uDD6A-\uDD72\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDE8\uDF60-\uDF71]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEDA\uDEDC-\uDF14\uDF16-\uDF4E\uDF50-\uDF88\uDF8A-\uDFC2\uDFC4-\uDFCB]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE8B]|\uD83C[\uDD10-\uDD2E\uDD30-\uDD69\uDD70-\uDDAC\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D]|[\uDBBF\uDBFF][\uDC00-\uDFFD]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/),
	rRegexp: (/[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05F0-\u05F4\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFB4F]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC57-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD3F\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE40-\uDE47\uDE50-\uDE58\uDE60-\uDE9F\uDEC0-\uDEE4\uDEEB-\uDEF6\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDF99-\uDF9C\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDCFF]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD50-\uDD59\uDD5E\uDD5F]/),


	/**
	 *
	 * @param {*} params
	 * @param {*} pageObj
	 * @param {*} callback
	 */
	create: function( params, pageObj, callback ) {
		params.orientation = params.orientation || 'portrait';
		params.scale = params.scale || 100;
		params.table = params.table || pageObj.tName;

		if ( !params.pageType ) {
			if ( params.page ) {
				var pages = Runner.pages.PageSettings.getTableData( params.table, 'pages' ) || {};
				params.pageType = pages[ page ];
			}
			if ( !params.pageType ) {
				//	choose default page type
				if ( params.table == pageObj.tName && pageObj.pageType == 'view' ) {
					params.pageType = 'view';
				} else {
					params.pageType = 'print';
				}
			}
		}
		
		params.page = params.page || pageObj.pageName;
		params.scope = params.scope || 0;
		params.split = params.split || 0;
		params.records = params.records || 20;
		params.filename = params.filename || 'file.pdf';
		params.pageSize = params.pageSize || 'LETTER';

		return Runner.PDF.loadLibrary( function() {
			Runner.PDF.loadJSON( params, pageObj, function( docDefinition, userFonts ) {
				var fontsToLoad = [];
				
				var pageIndicatorInFooter = false;
				if ( docDefinition.footer ) {
					pageIndicatorInFooter = Runner.PDF.hasPageIndicator( docDefinition.footer );
					// add a link to process footer tables
					docDefinition.content.footer = docDefinition.footer;
				}
				
			
				var pageIndicatorNode = Runner.PDF.extractPageIndicatorNode( docDefinition, pageIndicatorInFooter );
				if ( pageIndicatorNode ) {
					// add a link to docDefinition to load rtl fonts
					docDefinition.content.pageIndicatorNode = pageIndicatorNode;
				}
				
				var callEventAndCreatePDF = function() {
					var ret = true;
					
					if ( typeof params.beforeCreate === "function" ) {
						ret = params.beforeCreate.call( pageObj, docDefinition, create );
					}
					
					if ( ret !== false ) {
						create();
					}
				}
				
				var create = function() {
					callback( pdfMake.createPdf( docDefinition ) );
				};


				Runner.PDF.getFontLoaded( 
						Runner.PDF.loadUserFonts.bind( Runner.PDF, userFonts || [], docDefinition ), 
						fontsToLoad 
					);
				
				var loadingCJK = Runner.PDF.processCJK( docDefinition, params );
				
				Runner.PDF.postProcessJson( docDefinition, params, function() {
					Runner.PDF.prepareBackground( docDefinition, params )
					pdfMake.tableLayouts = Runner.PDF.customTableLayouts;
					
					loadingCJK && 
						Runner.PDF.getFontLoaded( Runner.PDF.loadCJKFont, fontsToLoad );
						
					Runner.PDF.processHebrew( docDefinition, params ) &&
						Runner.PDF.getFontLoaded( Runner.PDF.loadHebrewFont, fontsToLoad );
					
					Runner.PDF.processArabic( docDefinition, params ) && 
						Runner.PDF.getFontLoaded( Runner.PDF.loadArabicFont, fontsToLoad );

					if ( pageIndicatorNode ) {
						// remove pageIndicatorNode link before rtl processing
						delete docDefinition.content.pageIndicatorNode;
					}
						
					Runner.PDF.reverseDirection( docDefinition );
					Runner.PDF.processBidi( docDefinition.content );

					Runner.PDF.reorderColumns( docDefinition, params );
					
					if ( !pageIndicatorInFooter ) {
						Runner.PDF.addHeader( docDefinition, pageIndicatorNode );
					}

					if ( docDefinition.footer ) {
						// remove the link from content
						delete docDefinition.content.footer;
						
						if ( pageIndicatorInFooter ) {
							Runner.PDF.addFooter( docDefinition, pageIndicatorNode );
						}
					}
					
					$.when.apply( $, fontsToLoad ).then( function() {
						callEventAndCreatePDF();
					});
				});
			});
		});

	},
	
	/**
	 * @param {function} fontLoadingFunc
	 * @param {array} fontsToLoad
	 */
	getFontLoaded: function( fontLoadingFunc, fontsToLoad ) {
		var deff = new $.Deferred();
		fontsToLoad.push( deff );
		fontLoadingFunc.call( this, function() {
			deff.resolve();
		});		
	},

	prepareBackground: function( docDefinition, params ) {
		if ( !docDefinition.background || !docDefinition.background.image ) {
			return;
		}
		//	calculate size and position of the image
		var height = 792,
			width = 612;

		if ( params.orientation === 'landscape' ) {
			height = 612;
			width = 792;
		}
		//	don't knwo why, but otherwise there are blank paddings
		var multiplier = 1.08;
		height *= multiplier;
		width *= multiplier;

		var imgWidth = docDefinition.background.imgWidth;
		var imgHeight = docDefinition.background.imgHeight;
		if ( !imgHeight || !imgWidth ) {
			imgWidth = width;
			imgHeight = height;
		}

		var xoffset = 0, yoffset = 0;
		if ( imgWidth / imgHeight > width / height ) {
			//	image is wider than the page
			docDefinition.background.height = height;
			docDefinition.background.width = imgWidth * height / imgHeight;
			xoffset = (width - docDefinition.background.width) / 2;
		} else {
			//	image is taller than the page
			docDefinition.background.width = width;
			docDefinition.background.height = imgHeight * width / imgWidth;
			yoffset = (height - docDefinition.background.height) / 2;
		}
		docDefinition.background.absolutePosition = {
			x: xoffset,
			y: yoffset
		}

		var opacity = parseInt( params.backgroundOpacity );
		if ( !isNaN( opacity ) ) {
			docDefinition.background.opacity = opacity/100;
		}
	},

	loadLibrary: function( callback ) {
		var path = settings.global["webRootPath"] + 'include/pdfmake/';
		var mainFile = 'pdfmake.min.js';
		var fontFiles = [ 'roboto.js' ];

		Runner.util.ScriptLoader.addJS( [path + mainFile] );
		Runner.util.ScriptLoader.addJS( fontFiles.map( function( f ) { return path + f; } ), path + mainFile );
		Runner.util.ScriptLoader.addJS( [path + 'html2make.js'] );
		
		if ( this.applyBidi ) {
			Runner.util.ScriptLoader.addJS( [path + 'textBreaker.js'] );
			Runner.util.ScriptLoader.addJS( [path + 'unicode.bidirectional.js'] );
			Runner.util.ScriptLoader.addJS( [path + 'core.js'] );
			if ( !String.prototype.normalize ) {
				// add String.prototype.normalize polyfill
				Runner.util.ScriptLoader.addJS( [path + 'normalizePolyfill.js'] );
			}
		}
	
		Runner.util.ScriptLoader.onFilesLoaded( function() {
			if ( pdfMake && pdfMake.addVirtualFileSystem ) {
				pdfMake.addVirtualFileSystem( Runner.PDF.vfs );
			}

			pdfMake.fonts = {
				Roboto: {
						normal: 'Roboto-Regular.ttf',
						bold: 'Roboto-Medium.ttf',
						italics: 'Roboto-Italic.ttf',
						bolditalics: 'Roboto-MediumItalic.ttf'
				},
			};
			
			
			callback();
		});
		Runner.util.ScriptLoader.load();
	},

	/**
	 * Load and unzip the Noto CJK font
	 * @param {*} callback
	 */
	loadCJKFont: function( callback ) {

		var assignFonts = function() {
			var filename = 'NotoSansCJKtc-Regular.ttf';
			pdfMake.fonts['Noto Sans CJK'] = {
				normal: filename,
				bold: filename,
				italics: filename,
				bolditalics: filename
			};
		}

		// don't load the file for the second time
		if ( Runner.PDF.vfs['NotoSansCJKtc-Regular.ttf'] ) {
			assignFonts();
			window.setTimeout( callback, 0 );
			return;
		}
		Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/pdfmake/cjk.js'] );
		Runner.util.ScriptLoader.addJS( [settings.global["webRootPath"] + 'include/pdfmake/zip.js'] );
		Runner.runLoading( Runner.lang.constants.LOADING_FONTS );
		Runner.util.ScriptLoader.onFilesLoaded( function() {
			zip.workerScriptsPath = "include/pdfmake/";
			zip.createReader(new zip.Data64URIReader( Runner.PDF.vfs['NotoSansCJKtc-Regular.zip'] ), function(zipReader) {
				zipReader.getEntries(function(entries) {
					entries[0].getData(new zip.Data64URIWriter("text/plain"), function(data) {
						zipReader.close();
						Runner.PDF.vfs['NotoSansCJKtc-Regular.ttf'] = data;
						delete Runner.PDF.vfs['NotoSansCJKtc-Regular.zip'];
						assignFonts();
						Runner.stopLoading();
						callback();
					});
				});
			});
		});
		Runner.util.ScriptLoader.load();

	},
	
	loadUserFonts: function( userFotns, docDefinition, callback ) {	
		var fontNames = !!userFotns.length ? Runner.PDF.detectUserFornts( docDefinition ) : [];
		var fonts = userFotns.filter( function( f ) {
			return fontNames.indexOf( f.name ) !== -1 && !pdfMake.fonts[ f.name ];
		});

		if ( !fonts.length ) {
			window.setTimeout( callback, 0 );
			return;			
		}
		
		Runner.util.ScriptLoader.addJS( fonts.map( function(f) { 
				return settings.global['webRootPath'] + 'include/pdfmake/fonts/' + f.name + '.js'; 
			}));
			
		Runner.util.ScriptLoader.onFilesLoaded( function() {			
			fonts.forEach( function(f) {
				var ufont = new Runner.userFont(f);
				pdfMake.fonts[ f.name ] = ufont.getFontDescription();
			}, this );
			
			callback();
		}, this );		

		Runner.util.ScriptLoader.load();	
	},
	
	/**
	 * Load Hebrew font
	 * @param {function} callback
	 */
	loadHebrewFont: function( callback ) {
		if ( pdfMake.fonts["Rubik"] ) {
			window.setTimeout( callback, 0 );
			return;
		}
		
		Runner.util.ScriptLoader.addJS( [ settings.global["webRootPath"] + 'include/pdfmake/hebrew.js' ] );		
		Runner.util.ScriptLoader.onFilesLoaded( function() {			
			pdfMake.fonts["Rubik"] = {
				normal: 'Rubik-Regular.ttf',
				bold: 'Rubik-Medium.ttf',
				italics: 'Rubik-Italic.ttf',
				bolditalics: 'Rubik-MediumItalic.ttf',
			};
			callback();
		});
		
		Runner.util.ScriptLoader.load();
	},
	
	/**
	 * Load Hebrew font
	 * @param {function} callback
	 */
	loadArabicFont: function( callback ) {
		if ( pdfMake.fonts["Amiri"] ) {
			window.setTimeout( callback, 0 );
			return;
		}
		
		Runner.util.ScriptLoader.addJS( [ settings.global["webRootPath"] + 'include/pdfmake/arabic.js' ] );		
		Runner.util.ScriptLoader.onFilesLoaded( function() {
			pdfMake.fonts["Amiri"] = {
				normal: 'Amiri-Regular.ttf',
				bold: 'Amiri-Bold.ttf',
				italics: 'Amiri-Italic.ttf',
				bolditalics: 'Amiri-BoldItalic.ttf',
			};
			callback();
		});
		
		Runner.util.ScriptLoader.load();		
	},
	
	
	loadJSON: function( params, pageObj, callback ) {
		var baseParams = pageObj
			? Runner.apply( {}, pageObj.getBaseParams() )
			: {};

		baseParams = Runner.apply( baseParams, {
			pdfjson: true,
			id: Runner.genId(),
			page: params.page
		});
		
		if ( params.backgroundImage ) {
			baseParams['pdfBackgroundImage'] = params.backgroundImage;
		}

		var printPanelParams = pageObj
			? pageObj.printPanelParams()
			: {};
		
		if ( Object.keys( printPanelParams ).length !== 0 ) {
			baseParams = Runner.apply( baseParams, printPanelParams );
		}

		var stateParams = pageObj
			? pageObj.getStateParams()
			: {};		
		
		baseParams = Runner.apply( baseParams, stateParams );
		
		if ( params.scope == 0 ) {
			/* default or print panel */
		}
		else if ( params.scope == 1 ) {
			baseParams.all = 1;
			delete baseParams.selection;
		}
		else if ( params.scope === 2 ) {
			
			baseParams.selection = [];
			if ( pageObj ) {
				baseParams.selection = pageObj.getSelectedRecordKeys().map( function( keys ) {
					if ( keys instanceof Array) {
						return keys.map( function(k) {
							return encodeURIComponent( k );
						})
						.join( '&' );
					} else {
						return keys;
					}
				} );
			}
			if ( !baseParams.selection || !baseParams.selection.length ) {
				return;
			}
		}

		if ( params.split == 0 ) {
			delete baseParams.records;
		} else if ( params.split == 1 ) {
			baseParams.records = params.records;
		}

		params.table = params.table || pageObj.tName;

		//	add key field values from params if any
		if ( params.pageType === 'view' ) {
			( params.keys || [] ).forEach( function( value, idx ) {
				baseParams['editid' + ( idx + 1 )] = value;
			});
		}

		// override selection[] with user-supplied selection
		if ( params.selection instanceof Array ) {
			baseParams[ 'selection[]'] = params.selection.map( function( keys ) {
				return keys.map( function( k ) {
					return Runner.escapeHtml( k );
				}).join('&');
			});
		}
		
		jQuery.post( Runner.pages.getUrl( params.table, params.pageType ), baseParams,
			function( respData ) {
				var pdfDocument, message, pdfFonts;
				try {
					// sorry. We have to use POST request, because 'baseparams' can be of substantional size.
					// And I see no other way to run code returned by POST
					eval( respData );
				} catch ( e ) {
					message = ( typeof e == 'string' ? e : e.message ) + "<br>" || '';
				}
				if ( pdfDocument === undefined ) {
					Runner.displayGenericAjaxError( message + '<br>' + respData );
					return;
				}
				callback.call( pageObj, pdfDocument, pdfFonts );
			});	
	},

	/**
	 *
	 * @param {*} obj
	 * @param {*} predicate( obj, key, parent )
	 */
	traversePdfObject: function( obj, predicate, exitPredicate ) {
		Object.keys( obj ).forEach( function( k ) {
			if ( obj[k] instanceof Object ) {
				if ( predicate( obj[k], k, obj ) !== false ) {
					if ( obj[k] instanceof Object ) {
						Runner.PDF.traversePdfObject( obj[k], predicate, exitPredicate );
						if ( exitPredicate ) {
							exitPredicate( obj[k], k, obj );
						}
					}
				}
			}
		});
	},
	
	/**
	 *
	 */
	detectUserFornts: function( docDefinition ) {
		var fontNames = [];		
		Runner.PDF.traversePdfObject( docDefinition.content,
			function( obj, key, parent ) {
				if ( obj && obj.font ) {
					if ( fontNames.indexOf( obj.font ) === -1 ) {
						fontNames.push( obj.font );
					}
				}
			});
			
		return fontNames;
	},

	processCJK: function( docDefinition, params ) {
		if ( !Runner.PDF.detectChinese ) {
			return false;
		}
		Runner.PDF.needCJK = false;
		Runner.PDF.createCJKRegex();
		Runner.PDF.traversePdfObject( docDefinition.content,
			function( obj, key, parent ) {

			},
			function( obj, key, parent ) {
				if ( typeof obj.text == 'string' && !obj.font ) {
					obj.text = Runner.PDF.processCJKString( obj.text );
				} else {
					if ( obj instanceof Array && key !== 'widths' ) {
						parent[ key ] = obj.map( function( str ) {
							if ( typeof str !== 'string' ) {
								return str;
							}
							var updated = Runner.PDF.processCJKString( str );
							if ( typeof updated === 'string' ) {
								return updated;
							}
							return {
								text: updated
							};
						});
					}
				}
			}
		);
		return Runner.PDF.needCJK;
	},


	/**
	 * @param {object} node
	 * @param {function} fontCallback
	 */
	processRtlFontForNode: function( node, fontCallback ) {
		Runner.PDF.traversePdfObject( node,
			function( obj, key, parent ) {
			},
			function( obj, key, parent ) {
				if ( typeof obj.text == 'string' && !obj.font ) {
					obj.text = fontCallback.call( Runner.PDF, obj.text );
				} else {
					if ( Array.isArray( obj ) && key !== 'widths' ) {
						parent[ key ] = obj.map( function( str ) {
							if ( typeof str !== 'string' ) {
								return str;
							}
							
							var updated = fontCallback.call( Runner.PDF, str );
							if ( typeof updated === 'string' ) {
								return updated;
							}
							return {
								text: updated
							};
						});
					}
				}
			});		
	},
	
	/**
	 *
	 */
	processHebrew: function( docDefinition, params ) {
		Runner.PDF.createHebrewRegex();
		Runner.PDF.needHebrew = false;
		
		Runner.PDF.processRtlFontForNode( docDefinition.content, Runner.PDF.processHebrewString );
	
		return Runner.PDF.needHebrew;
	},
	
	/**
	 *
	 */
	processArabic: function( docDefinition, params ) {
		Runner.PDF.createArabicRegex();
		Runner.PDF.needArabic = false;
		
		Runner.PDF.processRtlFontForNode( docDefinition.content, Runner.PDF.processArabicString );
		
		return Runner.PDF.needArabic;
	},
	
	processImages: function( pdfDocument, callback, multiplier ) {
		var imgUrlsObj = {};
		// collect all image urls
		Runner.PDF.traversePdfObject( pdfDocument.content,
			function( obj, key, parent ) {
				if (obj && (typeof obj.image == 'string')) {
					imgUrlsObj[obj.image] = obj.image;
				}
			});
			
		var imgUrlsArr = Object.keys(imgUrlsObj);
		var imgSizes = {};
		var corruptedImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACLSURBVDhP7ZBBDsAgCAT7/9/0Ub7DboQQtItK06NzICuBCfGqP3FEa4jojtEJBhdpcqBZSpm4EiLUiSsnApErIfJo17ErwiFGQgSwgCxr8rQmQhvsICLBlu35bnpCEaALtAm4yE9HmwP8j+z+IUuVMMBFqLKP6rPVNtgRiiYkREt01ME/+wNHtKLWB6NMdkWl1LLKAAAAAElFTkSuQmCC';

		var processImageUrl = function( url ) {
			// XMLHttpRequest used to avoid disabling canvas importing functionality for crossdomain images
			var xhr = new XMLHttpRequest();
			xhr.open('GET', url, true);
			xhr.responseType = 'arraybuffer';
			xhr.onload = function(e) {
				var contentType = xhr.getResponseHeader('Content-Type');
				var arr = new Uint8Array(this.response);
				var strArr = '';
				for (var i = 0; i < arr.byteLength; i++) {
					strArr += String.fromCharCode(arr[i]);
				}
				var b64 = btoa(strArr);
				var dataURL = "data:" + contentType + ";base64," + b64;

				// pdf make support only png and jpeg images
				var canvas = document.createElement("canvas");
				var ctx = canvas.getContext("2d");
				var img = new Image();
				img.onload = function() {
					try {
						canvas.width = img.width;
						canvas.height = img.height;
						imgSizes[url] = [ img.width, img.height ];
						ctx.drawImage(img, 0, 0);
						var dataUrlPng = canvas.toDataURL("image/png");
						$(canvas).remove();
						imgUrlsObj[url] = dataUrlPng;
						processImgUrlsList();
					} catch (e) {
						$(canvas).remove();
						imgUrlsObj[url] = corruptedImg;
						processImgUrlsList();
					}
				};
				img.onerror = function() {
					$(canvas).remove();
					imgUrlsObj[url] = corruptedImg;
					processImgUrlsList();
				}
				img.src = dataURL;
			};
			xhr.onerror = function(e) {
				imgUrlsObj[url] = corruptedImg;
				processImgUrlsList();
			};
			xhr.send();
		}

		var processImgUrlsList = function() {
			if (imgUrlsArr.length == 0) {
				Runner.PDF.traversePdfObject(pdfDocument.content, function(obj, key, parent) {
					if (obj && (typeof obj.image == 'string')) {
						var url = obj.image;
						obj.image = imgUrlsObj[ obj.image ] || corruptedImg;
						if ( imgSizes[url] ) {
							if ( !obj.width && !obj.height ) {
								obj.width = imgSizes[url][0] * multiplier;
								obj.height = imgSizes[url][1] * multiplier;
							} else if ( !obj.width && obj.height ) {
								obj.width = ( obj.height / imgSizes[url][1] ) * imgSizes[url][0];
							} else if ( obj.width && !obj.height ) {
								if( (/%$/).test( obj.width ) && !isNaN( parseFloat( obj.width ) ) ) {
									obj.width = ( parseFloat( obj.width ) / 100 ) * imgSizes[url][0];
								}							
								obj.height = ( obj.width / imgSizes[url][0] ) * imgSizes[url][1];
							}
						}
						 
					}
				}, function() { });
				callback();
			} else {
				processImageUrl(imgUrlsArr.pop());
			}
		}

		processImgUrlsList();

	},
	
	processHtml: function( pdfDocument, multiplier ) {
		var testHtml = function( str ) {
			return str.indexOf("<") !== -1;
		}

		var processHtmlString = function( str ) {
			// returns an array!
			return htmlToPdfMake( str );
/*			
			if ( !testHtml( str ) ) {
				return str;
			}

			var $tmpPDFHTML = $( '<iframe id="tmpPDFHTML"/>' ).hide().appendTo( 'body' )
				.contents().find( 'body' ).append( str );

			var ret = $tmpPDFHTML.text();
			$( 'iframe#tmpPDFHTML' ).remove();
			return ret;
*/			
		}

		var htmlStack = [];
		Runner.PDF.traversePdfObject( pdfDocument.content,
			function( obj, key, parent ) {
				if ( obj.isHtml ) {
					htmlStack.push( true );
				}
			},
			function( obj, key, parent ) {
				if ( htmlStack.length && obj && ( typeof obj.text == 'string' || obj instanceof Array ) ) {
					if ( typeof obj.text == 'string' ) {
						obj.text = processHtmlString( obj.text );
					} else {
						parent[key] = obj.map( function( o ) {
							if ( typeof o == 'string' )	{
								return processHtmlString( o );
							}
							return o;
						});
					}
				}
				if ( obj.isHtml ) {
					htmlStack.pop();
				}
			}
		);
	},
	
	/**
	 * Copy style props to text array child nodes
	 * #15240
	 */
	processStyleAttrs: function( pdfDocument ) {
		var styleAttrs = this.styleProps;
		
		// copy styles to text array nodes
		Runner.PDF.traversePdfObject( pdfDocument.content, 
			function( obj, key, parent ) {
				if ( obj.text && Array.isArray( obj.text ) ) {
					var _styleAttrs = styleAttrs.filter( function( prop ) {
							return obj[ prop ] !== undefined;
						});
					
					if ( !_styleAttrs.length ) {
						return;
					}
					
					obj.text.forEach( function( textNode, idx ) {
						var _textNode = textNode;
						if ( typeof textNode === "string" ) {
							_textNode = { text: textNode };
							obj.text[ idx ] = _textNode;
						}
						if ( typeof _textNode === "object" && _textNode.text ) {
							_styleAttrs.forEach( function( prop ) {
								_textNode[ prop ] = _textNode[ prop ] || obj[ prop ];
							});
						}
					});
				}
			});	
	},

	hasPageIndicator: function( node ) {
		var found = false;
		Runner.PDF.traversePdfObject( node, function( obj, key, parent ) {
			if ( obj.page_indicator ) {
				found = true;
				return;
			}
		});
		return found;
	},

	
	/**
	 *
	 */
	extractPageIndicatorNode: function( pdfDocument, placeHolder ) {
		var pageIndicatorNode; 
		
		Runner.PDF.traversePdfObject( pdfDocument.content, function( obj, key, parent ) {
			if ( obj.page_indicator ) {
				if ( !placeHolder ) {
					parent[ key ] = '';
				} else {
					parent[ key ] = { page_indicator: true, text: '' };
				}
				pageIndicatorNode = obj;
				return;
			}
		});
		
		return pageIndicatorNode;
	},
	
	
	postProcessJson: function( pdfDocument, params, callback ) {
		var textStack = [];
		var textNode = function( obj ) {
/*			if ( obj && obj.isHtml ) {
				return false;
			}
*/			
			if ( !obj || obj.text !== undefined || typeof obj == 'string' ) {
				return true;
			}
			return false;
		}
		var stackNode = function( obj ) {
			return obj && typeof obj === 'object' && !( obj instanceof Array ) && ( obj.text === undefined /* || obj.isHtml */ );
		}
		var canConvertToText = function( obj ) {
			if ( obj.stack ) {
				if ( !obj.stack.length ) {
					return true;
				}

				if ( obj.stack.length === 1 ) {
					return textNode( obj.stack[0] );
				}
			}
			return false;
		}

		var multiplier = 0.5;	// magic number
		var qrMultiplier = 1.5;	// another magic number.

		if ( params.pageType === 'view' ) {
			multiplier *= 1.2;
		}

		if ( params.orientation === 'landscape' ) {
			multiplier = multiplier * 11 / 8.5;
		}
		multiplier = multiplier * params.scale / 100;

		var firstPageBreak = true;
		var inColor = [];
		var vPadding = 4;
		var hPadding = 8;

		var maxImgWidthPx = params.orientation === 'landscape' ? 761 : 510;

		Runner.PDF.processHtml( pdfDocument, multiplier );
		Runner.PDF.processStyleAttrs( pdfDocument );
		
		/** process chinese etc languages in all text nodes */

		Runner.PDF.traversePdfObject( pdfDocument.content, function( obj, key, parent ) {
			/* delete first page break */
			if ( obj.standAlonePage && firstPageBreak && obj.pageBreak !== undefined ) {
				delete obj.pageBreak;
				firstPageBreak = false;
			}

			if ( obj.page_indicator ) {
				Runner.PDF.pageIndicatorText = JSON.parse( JSON.stringify( obj.text ) );
				return;
			}

			if ( obj.table && obj.layout ) {
				obj.layout.paddingLeft = function() { return hPadding * multiplier; }
				obj.layout.paddingRight = function() { return hPadding * multiplier; }
				obj.layout.paddingTop = function() { return vPadding * multiplier; }
				obj.layout.paddingBottom = function() { return vPadding * multiplier; }
			}

			if ( obj.heights ) {
				obj.heights.forEach( function( height, _idx ) {
					obj.heights[_idx] *= multiplier;
				});
			}

			if ( obj.hasOwnProperty( 'checkbox' ) ) {
				obj.canvas = Runner.PDF.pdfCheckbox(obj.checkbox, multiplier );
				delete obj.checkbox;
				delete obj.text;
			}

			if ( obj.text && typeof obj.text === 'object' && !(obj.text instanceof Array) ) {
				obj.text = [obj.text];
			}

			if ( obj.stack && typeof obj.stack === 'object' && !(obj.stack instanceof Array) ) {
				obj.stack = [obj.stack];
			}

			if ( !!obj.hidden ) {
				parent[ key ] = '';
				return;
			}

			if ( obj.fontSize ) {
				obj.fontSize *= multiplier;
			}

			if ( obj.margin ) {
				if ( Array.isArray( obj.margin ) ) {
					obj.margin.forEach( function( m, index, array ) {
						array[index] = m * multiplier;
					} );
				} else {
					obj.margin *= multiplier;
				}
			}

			//	QR code
			if ( obj.fit ) {
				obj.fit *= multiplier * qrMultiplier;
				if ( obj.fit < 40 ) {
					obj.fit = 40;
				}
			}

			if ( canConvertToText( obj ) ) {
				parent.text = parent.stack;
				delete parent.stack;
			}

			if ( textNode( obj ) ) {
				textStack.push( obj );
			}

			if ( obj.color ) {
				inColor.push( true );
			}
			//	make hyperlinks blue by default
			if ( obj.link && !inColor.length ) {
				obj.color = '#337ab7';
				inColor.push( true );
			}

			if ( obj.image ) {
				if( (/%$/).test( obj.width ) && !isNaN( parseFloat( obj.width ) ) ) {
					obj.width = parseFloat( obj.width ) * multiplier + '%';
				} else {
					obj.width *= multiplier;
				}
				obj.height *= multiplier;

				if ( !isNaN( obj.width ) && obj.width > maxImgWidthPx ) {
					// to fit a page
					obj.width = maxImgWidthPx;
				}
			}
			//	replace empty tables with ''
			if ( obj.table ) {
				if ( !obj.table.body || !obj.table.body.length ) {
					obj.text = '';
					delete obj.table;
					return;
				}
			}

			// handle colspan/rowspan; process table row at a time
			if ( obj.tableForm ) {

				/**  spannedCells[cellId] = [rowspan, colspan]
				 */
				var spannedCells = {};
				obj.body = obj.body.map( function( row ) {
					return row.filter( function( cell ) {
						if ( cell.cellId ) {
							if ( !cell.row && !cell.col ) {
								spannedCells[ cell.cellId ] = [ cell.rowSpan || 1, cell.colSpan || 1 ];
								return true;
							} else {
								var spannedCell = spannedCells[ cell.cellId ];
								if ( !spannedCell || spannedCell[0] <= cell.row || spannedCell[1] <= cell.col ) {
									// extra compensation cell, delete it
									return false;
								}
							}
						}
						return true;
					});
				});
			}

			// create cells to compensate for colspans

			if ( obj.tableForm ) {
				obj.body.forEach( function( row ) {
					var addCells = 0;
					while ( row.length < obj.widths.length ) {
						row.push( { text: '', compensateColspan: true } );
					}
				} );
			}
		}, function( obj, key, parent ) {
			if ( textNode( obj ) ) {
				textStack.pop();
				if ( obj.hasStack ) {
					/**
					 *  transform object to stack
					 *  original object: { text: [<text1>,<text2>,<stack1>,<text3>,<text4>] }
					 *  new object: 	{ stack:[
					 * 						text: [<text1>,<text2>],
					 * 						<stack1>,
					 * 						text: [<text3>,<text4>],
					 * 					]}
					 */

					var lastTextElem = null;
					obj.stack = ( obj.text instanceof Array ? obj.text : [obj.text] ).reduce( function( stack, e ) {
						if ( typeof e === 'string' || textNode( e ) ) {
							if ( lastTextElem ) {
								lastTextElem.text.push( e );
							} else {
								lastTextElem = {
									text: [e]
								}
								stack.push( lastTextElem );
							}
						} else {
							stack.push( e );
							lastTextElem = null;
						}
						return stack;
					}, [] );
					delete obj.text;
				} else if ( obj.text instanceof Array ) {
					//	nested array may have come from html processing
					obj.text = obj.text.map( function( t) {
						return t instanceof Array
							? { text: t }
							: t;
					});
							
				}
			}

			if ( stackNode( obj ) ) {
				if ( textStack.length ) {
					textStack[ textStack.length - 1 ].hasStack = true;
				}
			}

			if ( obj.color ) {
				inColor.pop();
			}

		});

		//	insert spaces between text elements
		Runner.PDF.traversePdfObject( pdfDocument, function( obj, key, parent ) {
			if ( obj.text && obj.text instanceof Array && obj.text.length > 1 ) {
				obj.text = obj.text.reduce( function( newText, txt, idx ) {
					if ( idx > 0 ) {
						newText.push( ' ' );
					}
					newText.push( txt );
					return newText;
				}, [] );
			}
		});

		pdfDocument.pageOrientation = params.orientation;
		pdfDocument.pageSize = params.pageSize;
		if( params.pageMargins )
			pdfDocument.pageMargins = params.pageMargins;

		// update styles
		if ( pdfDocument.defaultStyle && pdfDocument.defaultStyle.fontSize ) {
			pdfDocument.defaultStyle.fontSize *= multiplier;
		}
		
		Runner.PDF.traversePdfObject( pdfDocument.styles, function( obj, key, parent ) {
			if ( obj.fontSize ) {
				obj.fontSize *= multiplier;
			}
		});

		

		Runner.PDF.processCharts( pdfDocument, function() {
			Runner.PDF.processImages( pdfDocument, callback, multiplier );
		});
	},
	
	/**
	 * Add dynamic header
	 */
	addHeader: function( pdfDocument, pageIndicatorNode ) {
		if ( !pageIndicatorNode || !pageIndicatorNode.text ) {
			return;
		}
		
		var applyRtlProcessing = Runner.PDF.checkRTLInPageIndicator( pageIndicatorNode );
		
		pdfDocument.header = function( page, pagecount ) {
			var headerNode = {
				text: Runner.PDF.processPageIndicatorText( page, pagecount ),
				fontSize: 7,
				alignment: Runner.isDirRTL() ? 'left' : 'right',
				margin:[ 20, 3, 20, 3 ]
			}	
		

			if ( applyRtlProcessing ) {
				// add rtl fonts and bidi processing
				Runner.PDF.processRtlFontForNode( headerNode, Runner.PDF.processHebrewString );
				Runner.PDF.processRtlFontForNode( headerNode, Runner.PDF.processArabicString );
				
				Runner.PDF.processBidi( headerNode );
			}
			
			return headerNode;
		};
	},
	
	
	/**
	 * Add dynamic footer, for pageIndicator item only
	 */
	addFooter: function( pdfDocument, pageIndicatorNode ) {
		if ( !pageIndicatorNode || !pageIndicatorNode.text ) {
			return;
		}

		var applyRtlProcessing = Runner.PDF.checkRTLInPageIndicator( pageIndicatorNode );
		var footerNode = pdfDocument.footer;
		
		pdfDocument.footer = function( page, pagecount ) {
			Runner.PDF.traversePdfObject( footerNode, function( obj, key, parent ) {
				if ( obj.page_indicator ) {
					obj.text = Runner.PDF.processPageIndicatorText( page, pagecount );

					if ( applyRtlProcessing ) {
						// add rtl fonts and bidi processing
						Runner.PDF.processRtlFontForNode( obj, Runner.PDF.processHebrewString );
						Runner.PDF.processRtlFontForNode( obj, Runner.PDF.processArabicString );
						
						Runner.PDF.processBidi( obj );
					}				
				}
			});			
			return footerNode;
		};
	},		
	
	checkRTLInPageIndicator: function( pageIndicatorNode ) {
		var hasRTLText = false;
		if ( Runner.isDirRTL() && ( Runner.PDF.needArabic || Runner.PDF.needHebrew ) ) {
			Runner.PDF.traversePdfObject( pageIndicatorNode, function( obj, key, parent ) {
				if ( Runner.PDF.isTextNodeRtl( obj ) ) {
					hasRTLText = true;
				}					
			});
		}
		return hasRTLText;
	},
	
	processPageIndicatorText: function( page, pagecount ) {
		var pageIndicatorText = Runner.PDF.pageIndicatorText;
		var multiplier = 0.5;
		
		if ( typeof pageIndicatorText == 'string' ) {
			return pageIndicatorText.replace('%current%', page).replace('%total%', pagecount);
		} 
		if ( pageIndicatorText instanceof Array ) {
			return pageIndicatorText.map( function( str ) {
				if ( str.text && typeof str.text == 'string' ) {
					var clone = JSON.parse( JSON.stringify( str ) );

					clone.text = clone.text.replace('%current%', page).replace('%total%', pagecount);
					delete clone.alignment;
					if ( clone.fontSize ) {
						// update font size set by user
						// it has priority over parent node's fontSize
						clone.fontSize *= multiplier;
					}
					
					return clone;
				} else if ( typeof str == 'string' ) {
					return str.replace('%current%', page ).replace('%total%', pagecount);
				}
			});
		}
		return '';
	},
	
	
	pdfCheckbox: function( checked, zoom ) {
		zoom *= 0.6;
		var color = checked ? '#000000' : '#a0a0a0';
		var checkbox = [{
			lineColor: color,
			type: 'rect',
			x: 0, y: 0, w: 20 * zoom, h: 20 * zoom,
			lineWidth: 1.5 * zoom,
		}]

		if ( checked ) {
			checkbox.push({
				lineColor: color,
				type: 'polyline',
				lineWidth: 2.5 * zoom,
				points: [
					{ x: 4 * zoom, y: 10 * zoom },
					{ x: 8 * zoom, y: 15 * zoom },
					{ x: 16 * zoom, y: 5 * zoom }]
			});
		}
		return checkbox;
	},

	open: function( params, pageObj, win ) {
		if ( window.navigator && window.navigator.msSaveOrOpenBlob ) {
			win = null;
		} else {
			win = win || window.open('', '_blank');
		}
		Runner.PDF.create( params, pageObj, function(pdf) {
			if ( !win ) {
				pdf.download( params.filename );
			} else {
				pdf.open( win );
			}
		});
	},

	download: function( params, filename, pageObj ) {
		if ( filename ) {
			params.filename = filename;
		}
		Runner.PDF.create( params, pageObj, function( pdf ) {
			pdf.download( params.filename );
		});
	},
	
	/**
	 * 
	 */
	reverseDirection: function( pdfDocument ) {
		if ( !Runner.isDirRTL() ) {
			return;
		}
		
		Runner.PDF.traversePdfObject( pdfDocument.content, function( obj, key, parent ) {
			if ( obj.table ) {
				if ( !!obj.table.body.length && obj.table.body[0].length > 1 ) {
					obj.table.body.forEach( function( row, rowIdx ) {
						obj.table.body[ rowIdx ] = Runner.PDF.reverseTableRow( row );
					});
				}
				
				if ( obj.table.widths && obj.table.widths.length > 1 ) {
					obj.table.widths = obj.table.widths.reverse();
				}
			}
			
			if ( obj.alignment ) {
				if ( obj.alignment == "left" ) {
					obj.alignment = "right";
				} else if ( obj.alignment == "right" ) {
					obj.alignment = "left";
				}
			}
		});
	},
	
	/* 
	 * cell with colspan is followed by its dummies
		{ text: "text" }{ colSpan: 3, ... } {} {} =>
		{ colSpan: 3, ... } {} {} { text: "text" }
	 * @param {array} dummy
	 * @return {array}
	 */
	reverseTableRow: function( row ) {
		var reversed = [];

		for ( var i = 0; i < row.length; i++ ) {
			var cell = row[i];
			if ( cell.colSpan ) {
				reversed = row.slice( i, i + cell.colSpan ).concat( reversed );
				i = i + cell.colSpan - 1;
			} else {
				reversed.unshift( cell );
			}
		}		
		
		return reversed;
	},
	
	
	JAPANESE_RANGE: '[\\u4e00-\\u9fff]|[\\u3400-\\u4dbf]|[\\u{20000}-\\u{2a6df}]|[\\u{2a700}-\\u{2b73f}]|[\\u{2b740}-\\u{2b81f}]|[\\u{2b820}-\\u{2ceaf}]|[\\uf900-\\ufaff]|[\\u3300-\\u33ff]|[\\ufe30-\\ufe4f]|[\\uf900-\\ufaff]|[\\u{2f800}-\\u{2fa1f}]',
	CHINESE_RANGE: '[\\u3000-\\u303f]|[\\u3040-\\u309f]|[\\u30a0-\\u30ff]|[\\uff00-\\uff9f]|[\\u4e00-\\u9faf]|[\\u3400-\\u4dbf]',
	KOREAN_RANGE: '[\\uac00-\\ud7af]|[\\u1100-\\u11ff]|[\\u3130-\\u318f]|[\\ua960-\\ua97f]|[\\ud7b0-\\ud7ff]',

	CJK_REGEX: null,
	CJK_FONT: 'Noto Sans CJK',
	
	//unicode-range: U+0590-05FF, U+20AA, U+25CC, U+FB1D-FB4F;
	HEBREW_RANGE: '[\\u0590-\\u05ff]|[\\ufb1d-\\ufb4f]|\\u20aa|\\u25cc',
	HEBREW_EXTRA_RANGE: '[\\u200c-\\u200f]',
	HEBREW_REGEX: null,
	HEBREW_FONT: 'Rubik',
		
	//unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC
	//ARABIC_RANGE: '[\\u0600-\\u06ff]|[\\u200c-\\u200e]|[\\u2010-\\u2011]|\\u204f|\\u2e41|[\\ufb50-\\ufdff]|[\\ufe80-\\ufefc]',
	ARABIC_MAIN_RANGE: '[\\u0600-\\u06ff]|[\\ufb50-\\ufdff]|[\\ufe80-\\ufefc]',
	ARABIC_FONT_EXTRA: '[\\u200c-\\u200f]|[\\u2010-\\u2011]|\\u204f|\\u2e41',
	ARABIC_REGEX: null,
	ARABIC_FONT: 'Amiri',


	createCJKRegex: function() {
		this.CJK_REGEX = new RegExp( '\\s*(('
			+ [this.JAPANESE_RANGE, this.CHINESE_RANGE, this.KOREAN_RANGE].join( '|' )
			+ ')+\\s*)+'
			, 'ug');
		this.createCJKRegex = function() {};
	},
	
	/**
	 * @return {RegExp}
	 */
	createHebrewRegex: function() {
		try {
			this.HEBREW_REGEX = this.HEBREW_REGEX 
				|| new RegExp( '\\s*((' + this.HEBREW_EXTRA_RANGE + ')*(' + this.HEBREW_RANGE + ')+(' + this.HEBREW_EXTRA_RANGE + ')*\\s*)+', 'g' );
		} catch( e ) {
			// 'u' flag is not supported, use regexpu-compiled one 
			this.ARABIC_REGEX = this.ARABIC_REGEX 
				|| new RegExp( '[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*((' + this.HEBREW_EXTRA_RANGE + ')*(' 
					+ this.HEBREW_RANGE
						+ ')+(' + this.HEBREW_EXTRA_RANGE + ')*[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*)+', 'g' );
		}	
	},
	
	/**
	 * @return {RegExp}
	 */
	createArabicRegex: function() {
		try {
			this.ARABIC_REGEX = this.ARABIC_REGEX 
				|| new RegExp( '\\s*((' + this.ARABIC_FONT_EXTRA + ')*(' + this.ARABIC_MAIN_RANGE + ')+(' + this.ARABIC_FONT_EXTRA + ')*\\s*)+', 'ug');
		} catch( e ) {
			// 'u' flag is not supported, use regexpu-compiled one 
			this.ARABIC_REGEX = this.ARABIC_REGEX 
				|| new RegExp( '[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*((' + this.ARABIC_FONT_EXTRA + ')*(' 
					+ this.ARABIC_RANGE
						+ ')+(' + this.ARABIC_FONT_EXTRA + ')*[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*)+', 'g' );
		}			
	},
	
	/**
	 *
	 * @param {string} str
	 * @return {string or Array}
	 */
	processCJKString: function( str ) {
		return this.updateStringFont( str, this.CJK_REGEX, this.CJK_FONT, 'needCJK' );
	},
	
	processHebrewString: function( str ) {
		return this.updateStringFont( str, this.HEBREW_REGEX, this.HEBREW_FONT, 'needHebrew' );
	},
	
	processArabicString: function( str ) {
		return this.updateStringFont( str, this.ARABIC_REGEX, this.ARABIC_FONT, 'needArabic' );
	},
	
	/**
	 * Create a new node with style props from a node given
	 * Redo ?
	 */
	copyNodeProps: function( _node ) {
		return this.setNodeProps( {}, _node );
		/*var node = {};
		for ( var key in _node ) {
			if ( _node.hasOwnProperty( key ) ) {
				node[ key ] = _node[ key ];
			}
		}
		return node;*/
	},
	
	setNodeProps: function( nodeDest, nodeSource, skipProps ) {
		skipProps = skipProps || [];
		for ( var key in nodeSource ) {
			if ( nodeSource.hasOwnProperty( key ) && skipProps.indexOf( key ) === -1 ) {
				nodeDest[ key ] = nodeSource[ key ];
			}
		}
		return nodeDest;		
	},

	/**
	 * Break a line at line break  marks to make an array.
	 * "Lorem ipsum \n\r dolor sit amet,\n\r consectetur adipiscing elit" =>
		[ "Lorem ipsum ", "\n\r", " dolor sit amet,", "\n\r", consectetur adipiscing elit" ]
	 * @param string line
	 * @return array
	 */ 
	getLineAndBreaksArray: function( line ) {
		var data = [];
		
		var fromIndex = 0;
		( line.match( /[\r\n]+/g ) || [] ).forEach( function( lineBreak ) {
			var toIndex = line.indexOf( lineBreak, fromIndex );
			var lineBefore = line.substring( fromIndex, toIndex );
			lineBefore && data.push( lineBefore );
			data.push( lineBreak );
			fromIndex = toIndex + lineBreak.length;
		});
		
		var tail = line.substring( fromIndex );
		tail && data.push( tail );
		return data;
	},	
	
	/**
	 * It breaks nodes at linebreaks marks. linebreaks nodes are included in the result set
	 *  [ { text: "word\r\n word1" }, { text: "\nword3", font: "myfont" } ] =>
			[ { text: "word" }, { text: "\r\n" }, { text: " word1" }, 
				{ text: "\n", font: "myfont" }, {text: "word3", font: "myfont" } ];
		 
	 * @param {array} flattenTextArray
	 * @return {array}
	 */
	getLineAndBreaksTextArray: function( flattenTextArray ) {
		var textArray = [];
		flattenTextArray.forEach( function( tNode ) {
			var data = this.getLineAndBreaksArray( tNode.text );
			if ( data.length > 1 ) {
				var linesBreaks = data.map( function( _text ) {
					var newNode = this.copyNodeProps( tNode );
					newNode.text = _text;
					return newNode; 
				}, this );
				textArray = textArray.concat( linesBreaks );
			} else {
				textArray.push( tNode );
			}
		}, this );
		return textArray;
	},
	
	
	/**
	 * @param {object} docNode ( eg pdfDocument.content )
	 */
	processBidi: function( docNode ) {
		if ( !this.applyBidi ) {
			return;
		}
	
		Runner.PDF.traversePdfObject( docNode, function( obj, key, parent ) {
			if ( obj.text && Array.isArray( obj.text ) && !obj.bidiProcessed ) {
				var _flattenText = Runner.PDF.flattenNodeTextArray( obj );
				
				if ( _flattenText.find( Runner.PDF.isTextNodeRtl ) === undefined ) {
					obj.bidiProcessed = true;
					return;
				}
				
				obj.text =_flattenText;
				
				obj.text.forEach( function( tNode ) {
					// ignore format characters, embeddings, overrides, pops
					//'RLE', 'LRE', 'RLO', 'LRO', 'PDF', 'ZWJ'
					tNode.text = tNode.text.toString().replace( /[\u202A\u202B\u202C\u202D\u202E\u200D]/g, "" );
				});

				
				// node text content lines
				var contentLine = obj.text.reduce( function( _text, tNode ) {
						return _text + tNode.text;
					}, "" ),
					lines = [ contentLine ];
					
				if ( !contentLine ) {
					return;
				}					

				var isMultiline = ( /[\r\n]+/g ).test( contentLine );
				if ( isMultiline ) {
					// UnicodeBidirectional works with codepoints 
					// that are assumed to be all in one paragraph
					// so we need to extract all paragraphs from a content line
					// and prepare text array of line-breaked nodes for easy matching
					
					lines = Runner.PDF.getLineAndBreaksArray( contentLine );
					obj.text = Runner.PDF.getLineAndBreaksTextArray( obj.text );
				}

				var textArray = [],
					paragraphShift = 0,
					stack = [],
					baseLevel;
					
				lines.forEach( function( line ) {		
					var lineCodepoints = Runner.PDF.stringToCodepoints( line.normalize('NFC') );
					var stackTextArray = [];
					
					if ( ( /^[\r\n]+$/g ).test( line ) ) {
						// this line is a linebreak
						paragraphShift += lineCodepoints.length;
						textArray.push( { text: line } );

						isMultiline && stack.push( { text: line, bidiProcessed: true } );
						return;
					}
					
					var levels = Runner.PDF.getLevels( lineCodepoints );
					// lineCodepoints[i] should be positioned at index flippedRp[i] in a bidi-reordered line
					var flippedRp = Runner.PDF.getFlippedReorderPermutation( lineCodepoints, levels, line );
						
					var lineNodesData = [],
						currNodeContentLength = 0,
						flippedRpShift = 0;

					obj.text.forEach( function( tNode, idx ) {
						// node text codepoints
						var textCodepoints = Runner.PDF.stringToCodepoints( tNode.text.normalize('NFC') );
						currNodeContentLength += textCodepoints.length;
						
						if ( paragraphShift > currNodeContentLength
							|| currNodeContentLength > paragraphShift + lineCodepoints.length ) {
							 //skip nodes before and after
							return;
						}	
						
						if ( ( /^[\r\n]+$/g ).test( tNode.text ) || !tNode.text ) {
							// skip line break or empty node
							return;
						}	
						
						var tempSubNodes = [{
							nodeIdx: idx,
							// word position in a permutation array
							wordPos: flippedRp[ flippedRpShift ],
							// word position in tNode.text codepoints array
							textPos: 0,
							dir: levels[ flippedRpShift ] % 2 === 1 ? "rtl" : "ltr",
							frpPos: flippedRpShift,
 						}];
						
						
						for ( var i = 1; i < textCodepoints.length; i++ ) {
							var frpPos = flippedRpShift + i;							
							if ( Math.abs( flippedRp[ frpPos ] - flippedRp[ frpPos - 1 ] ) != 1 ) {
								// next word 
								tempSubNodes.push({
									nodeIdx: idx,
									// word position in a line permutation array
									wordPos: flippedRp[ frpPos ],
									// word position in tNode.text codepoints array
									textPos: i,
									dir: levels[ frpPos ] % 2 === 1 ? "rtl" : "ltr",
									frpPos: frpPos,
								})
							}
						}
						
						tempSubNodes.forEach( function( _sn,_idx ) {
							// extract text value from the intial text node
 							var _nextSn = tempSubNodes[ _idx + 1 ];
							if ( _nextSn ) {
								_sn.word = textCodepoints.slice( _sn.textPos, _nextSn.textPos );
							} else {
								_sn.word = textCodepoints.slice( _sn.textPos/*, paragraphShift + lineCodepoints.length*/ );
							}
							// to test
							_sn.levels = levels.slice( _sn.frpPos, _sn.frpPos + _sn.word.length );
							
							_sn.text = Runner.PDF.codepointsToString( _sn.word );
						});
						
						lineNodesData = lineNodesData.concat( tempSubNodes );
						flippedRpShift += textCodepoints.length;
					}, this );


					lineNodesData.sort( function( sn1, sn2 ) {
							if ( sn1.wordPos < sn2.wordPos ) {
								return -1;
							}
							if ( sn1.wordPos > sn2.wordPos ) {
								return 1;
							}
							return 0;
						});
						

					lineNodesData.forEach( function( sp ) {
						var node = Runner.PDF.copyNodeProps( obj.text[ sp.nodeIdx ] );
						node.text = sp.text;
						
						if ( Runner.PDF.isTextNodeRtl( node ) ) {
							// break node.text to words array as pdfmake does
							var textBreaker = new TextBreaker();
							var breakedText = textBreaker.getBreaks( node, {} );
							// refine breakedText ?
							
							// reverse words' order
							var breakedReverse = breakedText.reverse();
							textArray = textArray.concat( breakedReverse );
							textArray.push( {text: " "} );
							
							stackTextArray = stackTextArray.concat( breakedReverse );
							stackTextArray.push( {text: " "} );
						} else {
							if ( sp.dir == "rtl" ) {
								// it's a node with weak characters eg punctuation, braces, space
								var textCodepoints = Runner.PDF.stringToCodepoints( node.text );
								var textLevels = UnicodeBidirectional.resolve( textCodepoints, Runner.isDirRTL() ? 1 : 0 );
								// use sp.levels instead?
							
								node.text = Runner.PDF.codepointsToString(
									// reorder ? levels.slice ? 
									UnicodeBidirectional.mirror( textCodepoints, textLevels ).reverse()
								);
							}
							stackTextArray.push( node );
							textArray.push( node );
						}	
					});
					
					// base embedding paragraph level
					baseLevel = Runner.PDF.getBaseEmbeddingLevel( lineCodepoints, levels );
					
					isMultiline && stack.push( { text: stackTextArray, rtl: baseLevel % 2 === 1, bidiProcessed: true } );
					paragraphShift += lineCodepoints.length;
				}, this );

				if ( isMultiline ) {
					obj.stack = stack;
					delete obj.text;
				} else {
					obj.text = textArray;
					obj.rtl = baseLevel % 2 === 1;
				}
			}
		});
	},
	
	/**
	 * @param array lineCodepoints
	 * @param boolean auto ?
	 * @return array
	 */
	getLevels: function( lineCodepoints, auto ) {
		var levels = UnicodeBidirectional.resolve( lineCodepoints, Runner.isDirRTL() ? 1 : 0, auto );
		
		// fix some invisible charachters 'x' level
		var xstart = -1, 
			prevStrongLevel = -1;
			
		for( var i = 0; i < levels.length; i++ ) {
			if ( levels[ i ] === 'x' ) {
				if ( xstart === -1 ) {
					xstart = i;
				}
			} else {
				if ( prevStrongLevel !== -1 && xstart !== -1 ) {
					for ( var j = xstart; j < i; j++ ) {
						levels[ j ] = prevStrongLevel;
					}
				}
				prevStrongLevel = levels[ i ];
			}
		}
		
		return levels;
	},
	
	/**
	 * Get flipped reorder permutation array
	 * Codepoints bidi reordering. 
	 * An array index is an initial position, an array value at this index is 
	 * a codepoint position after bidi reodering
	 * @param {array} codepoints
	 * @param {array} levels
	 * @return array
	 */
	getFlippedReorderPermutation: function( codepoints, levels, line ) {
		var planB = false;
		var rp;
		
		if ( planB ) {
			// augment TwitterCldr.Bidi
			this.augmentBidiCore();
			rp = TwitterCldr.Bidi.from_string( line ).reorderPermutation();
		} else {
			// reorder permutation array
			rp = UnicodeBidirectional.reorderPermutation( levels );
		}
		
		return Object.keys( rp ).reduce( function( ret, key ) {
				ret[ parseInt( rp[ key ], 10 ) ] = parseInt( key, 10 );
				return ret;
			}, [] );
	},
	
	/**
	 * @deprecated
	 * to test
	 */
	augmentBidiCore: function() {
		if ( !TwitterCldr || !TwitterCldr.Bidi || TwitterCldr.Bidi.prototype.reorderPermutation ) {
			return;
		}
		
		TwitterCldr.Bidi.prototype.reorderPermutation = function() {
			var arr = [];
			var depth, finish, i, level, lowest_odd, max, start, tmpb, tmpo, _i, _j, _k, _len, _ref, _ref1;
			if ( !this.string_arr ) {
				throw "No string given!";
			}
			max = 0;
			lowest_odd = 62 + 1;
			_ref = this.levels;
			for (_i = 0, _len = _ref.length; _i < _len; _i++) {
				level = _ref[_i];
				max = TwitterCldr.Utilities.max( [level, max] );
				if ( !TwitterCldr.Utilities.is_even( level ) ) {
					lowest_odd = TwitterCldr.Utilities.min( [lowest_odd, level] );
				}
			}
			for (depth = _j = max; max <= 0 ? _j < 0 : _j > 0; depth = max <= 0 ? ++_j : --_j) {
				start = 0;
				while ( start < this.levels.length ) {
					while ( start < this.levels.length && this.levels[ start ] < depth ) {
						start += 1;
					}
					if ( start === this.levels.length ) {
						break;
					}
					finish = start + 1;
					while ( finish < this.levels.length && this.levels[finish] >= depth ) {
						finish += 1;
					}
					for (i = _k = 0, _ref1 = (finish - start) / 2; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
						arr[ start + i ] = finish - i - 1;
						arr[ finish - i - 1 ] = start + i;
					}
					start = finish + 1;
				}
			}
			return arr;
		}
	},
	
	
	/**
	 * @param {string} str
	 * @return {array}
	 */
	stringToCodepoints: function( str ) {
		var codepoints = [];
		// broken pair case?
		
		for ( var idx = 0; idx < str.length; idx++ ) {
			var code = str.charCodeAt( idx );
			if ( 0xD800 <= code && code <= 0xDBFF && str.length > idx + 1 ) {
				var high = code;
				var low = str.charCodeAt( idx + 1 );
				code = ( high - 0xD800 ) * 0x400 + low - 0xDC00 + 0x10000;
				idx++;
			}

			codepoints.push( code );
		}
		
		return codepoints;
	},

	/**
	 * @param {array} codepoints
	 * @return {string}
	 */
	codepointsToString: function( codepoints ) {
		var parts = [];	
		for ( var i = 0; i < codepoints.length; i++ ) {
			var code = codepoints[ i ];
			
			if ( code > 0xFFFF ) {
				code -= 0x10000;
				var high = ( code >> 10 ) + 0xD800;
				var low = ( code & 0x3FF ) + 0xDC00;
			
				parts.push( String.fromCharCode( high, low ) );
			} else {
				parts.push( String.fromCharCode( code ) );
			}
		}	

		return parts.join("");
	},

	/**
	 * @param {array} paragraphCodepoints
	 * @param {array} levels
	 * @return {number}
	 */
	getBaseEmbeddingLevel: function( paragraphCodepoints, levels ) {

		
		var firstStrongIdx = paragraphCodepoints.findIndex( function( cp ) {
			var encoded = Runner.PDF.codepointsToString( [ cp ] );
			// test bidi strong classes
			return this.alRegexp.test( encoded ) 
				|| this.lRegexp.test( encoded ) 
					|| this.rRegexp.test( encoded );
		}, this );
	
		return levels[ firstStrongIdx === -1 ? 0 : firstStrongIdx ];
	},
	
	/**
	 * @return boolean
	 */
	isTextNodeRtl: function( textNode ) {
		return typeof textNode.text == "string" 
			&& ( Runner.PDF.needArabic && textNode.font == Runner.PDF.ARABIC_FONT 
				|| Runner.PDF.needHebrew && textNode.font == Runner.PDF.HEBREW_FONT ) ;
	},
	
	
	/**
	 * @param object node
	 * @return array
	 */
	flattenNodeTextArray: function( node ) {
		if ( !node.text || !Array.isArray( node.text ) ) {
			return;
		}
	
		var self = this;
		return node.text.reduce( function( prev, next ) {
			var nodes = [ next ];
			if ( typeof next === "string" || typeof next === "number" ) {
				nodes = [{ text: next }];
			} else if ( next.text && Array.isArray( next.text ) ) {
				nodes = self.flattenNodeTextArray( next );
			}
			
			nodes.forEach( function( _node ) {
				self.styleProps.forEach( function( prop ) {
					if ( node[ prop ] !== undefined && _node[ prop ] === undefined ) {
						_node[ prop ] = node[ prop ];
					}
				});
			});
			
			return prev.concat( nodes );
		}, [] );
	},

	/**
	 * Check if a string must be rendered with alternative font. Split into standard and alternative font sections. Add fons specificaion.
	 * 'My name is 笔记本电脑' => [
	 * 	 'My name is',
	 *   {
	 * 		text: ' 笔记本电脑',
	 * 		font: 'CJK'
	 *   }
	 * ]
	 *
	 * @param {*} str
	 * @param {*} re
	 * @param {*} font
	 */
	updateStringFont: function( str, re, font, flag ) {
		var ret = str,
			lastIndex = 0,
			match;
		while( ( match = re.exec( str ) ) !== null ) {
			if ( !( ret instanceof Array ) ) {
				ret = [];
			}
			if ( match.index > lastIndex ) {
				ret.push( str.substr( lastIndex, match.index - lastIndex ) );
			}
			ret.push({
				text: match[0],
				font: font
			});
			lastIndex = match.index + match[0].length;
		}
		if ( lastIndex < str.length && ( ret instanceof Array ) ) {
			ret.push( str.substr( lastIndex ) );
		}

		// replacement was made
		if ( ret instanceof Array ) {
			this[ flag ] = true;
		}

		return ret;
	},

	/**
	 * @param {object} docDefinition pdfDocument object 
	 * @param {object} params
	 */
	reorderColumns: function( docDefinition, params ) {
		if ( params.pageType != 'print' || !Runner.pages.PageSettings.getTableData( params.table, "isAllowFieldsReordering" ) ) {
			return;
		}

		var columnOrder = Runner.pages.PageSettings.getTableData( params.table, "columnOrder" );
		if ( !columnOrder || !columnOrder.length ) {
			return;
		}

		// index => fName
		var columnOrderMap = columnOrder.reduce( function( acc, cur, idx ) {
			acc[cur] = idx;
			return acc;
		}, {});

		docDefinition.content.forEach( function( contentElement ) {
			var table = contentElement.table;
			var rows = table.body;
			var headStartIdx = table.embeddedPage ? 0 : 1;
			var startFieldsOrder = rows[ headStartIdx ].map( function( cell ) {
					return cell.fieldName;
				});
			
			// row object contains fieldName" key, 
			// so we can reorder each row using columnOrderMap using sort by new index
			for( var i = headStartIdx ; i < rows.length; i++ ) {
				var rowLength = rows[i].length;
				// save original index to sort non-field items
				for( var j = 0; j < rowLength; j++ ) {
					rows[i][j].originalIndex = j;
					rows[i][j].fieldName = rows[i][j].fieldName || startFieldsOrder[j];
				}
				// non-field items will be at the start of the row in original order (originalIndex)
				rows[i].sort( function( left, right ) {
					var lftOrder = columnOrderMap[left.fieldName] !== undefined ? columnOrderMap[left.fieldName] + rowLength : left.originalIndex;
					var rgtOrder = columnOrderMap[right.fieldName] !== undefined ? columnOrderMap[right.fieldName] + rowLength : right.originalIndex;
					if( left.compensateColspan ) {
						lftOrder = 10000;
					}
					if( right.compensateColspan ) {
						rgtOrder = 10000;
					}

					return (lftOrder - rgtOrder);
				});
			}
		});
	},

	_chartRescaleText: function( chart, chartParams ) {
		var title = chart.title();
		
		var magicNumberScale = 3/4;
		var rescale = magicNumberScale/(chartParams.width/chartParams.height);
		title.fontSize( title.fontSize() * rescale );

		if ( chart.legend ) {
			var legend = chart.legend();
			var paginator = legend.paginator();
			legend.fontSize( legend.fontSize() * rescale );
			paginator.fontSize( paginator.fontSize() * rescale );
		}

		if ( chart.xAxis && chart.yAxis ) {
			var x = chart.xAxis();
			var y = chart.yAxis();
			var xLabels = x.labels();
			var yLabels = y.labels();
			var xTitle = x.title();
			var yTitle = y.title();
			xLabels.fontSize( xLabels.fontSize() * rescale );
			yLabels.fontSize( yLabels.fontSize() * rescale );
			xTitle.fontSize( xTitle.fontSize() * rescale );
			yTitle.fontSize( yTitle.fontSize() * rescale );
		}
	},

	_chartGetSvg: function( data, chartParams ) {
		try {
			var chart = anychart.fromJson( data );

			if ( chartParams.settings ) {
				if ( typeof chart.getSeriesAt == 'function' ) {
					for( var i = 0; i < chartParams.settings.seriesColor.length; ++i ) {
						chart.getSeriesAt(i).color('#' + chartParams.settings.seriesColor[i], 1 );
					}
				}
			}

			if ( !data.gauge ) {
				credits = chart.credits();
				credits.enabled( false );
			}

			var el = document.createElement( "div" );
			chart.container(el);
			chart.animation(false, 0);

			this._chartRescaleText( chart, chartParams );

			chart.draw();

			var svg = chart.toSvg( chartParams.width, chartParams.height );
			return svg;
		} catch(e) {
			console.error(e);
			return null;
		}
	},
	_chartFetchProcess: function( chartParams, svgCallback ) {
		$.get( chartParams.xmlFile, function( data ) {
			// wiil be used to calculate each svg width
			// at the final traverse
			chartParams.splitCount = data.gauge ? data.gauge.length : 1;
			if ( data.gauge ) {
				chartParams.title = data.header;
				chartParams.legend = data.footer;
				chartParams.splitCount = data.gauge.length;
				var gwidth = Math.round( chartParams.width / data.gauge.length );
				var svgList = [];
				for( let i = 0; i < data.gauge.length; i++ ) {
					var svg = Runner.PDF._chartGetSvg(data.gauge[i], {
						height: chartParams.height,
						width: gwidth,
						seriaNum: i,
					});
					svgList.push(svg);
				}

				svgCallback(svgList);
				return;
			}
		
			var svg = Runner.PDF._chartGetSvg(data, chartParams);
			svgCallback([svg]);

		}, 'json' )
		.fail(function() {
			svgCallback([]);
		});
	},

	processCharts: function( pdfDocument, callback ) {
		// chartName => chartParams
		var chartParamsMap = {};
		// chartName => svg[] (gauge ~ svg[]);
		var chartSvgMap = {};
		Runner.PDF.traversePdfObject( pdfDocument.content,
			function( obj, key, parent ) {
				if ( !(obj && obj.chartParams) ) {
					return;
				}

				obj.chartParams.width = obj.width;
				obj.chartParams.height = obj.height;

				var chartName = obj.chartParams.chartName;

				chartParamsMap[chartName] = obj.chartParams;
				// svg to display if something went wrong
				var stubSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="300" height="150" viewBox="0 0 300 150"><rect fill="#ddd" width="300" height="150"/><text fill="rgba(0,0,0,0.5)" font-family="sans-serif" font-size="30" dy="10.5" font-weight="bold" x="50%" y="50%" text-anchor="middle">Something went wrong while preparing chart</text></svg>';
				chartSvgMap[chartName] = [ stubSvg ];
			});
	
		var chartsNamesList = Object.keys(chartParamsMap);

		var processChartsList = function() {
			if ( chartsNamesList.length == 0) {
				Runner.PDF.traversePdfObject( pdfDocument.content,
					function( obj, key, parent ) {
						if ( !(obj && obj.chartParams) ) {
							return;
						}

						var chartName = obj.chartParams.chartName;
						var chartParams = obj.chartParams;
						var svgList = chartSvgMap[chartName];

						var gwidth = Math.round( chartParams.width / chartParams.splitCount );
						var height = chartParams.height;

						var fixedSvgList = svgList.map( function(svg) { return svg.replace('<svg', '<svg viewBox="0 0 ' + gwidth + ' ' + height + '"') } );

						var table = {
							layout: 'noBorders',
							alignment: 'center',
							widths: fixedSvgList.map( function(_) { return 'auto' } ),
							table: {
								body: [
									fixedSvgList.map( function(svg) { return { svg: svg, width: gwidth }; } ),
								]
						}};

						obj.stack = [ 
							{text: chartParams.title || "", alignment: 'center'},
							table,
							{text: chartParams.legend || "", alignment: 'center'} 
						];
						delete obj.chartParams;
				}, function() {} );
				callback();
			} else {
				var chartName = chartsNamesList.pop();
				var chartParams = chartParamsMap[chartName];
				Runner.PDF._chartFetchProcess( chartParams, function( svgList ) {
					if ( svgList.length > 0 ) {
						chartSvgMap[chartName] = svgList.slice();
					}
					processChartsList();
				});
			}
		}

		processChartsList();
	},

	detectChinese: Runner.settings.project.pdfCJK,
}

/**
 *
 */
Runner.userFont = function( cfg ) {
	this.files = cfg.files;
	
	this.desc =	{
		normal: { bold: false, italic: false },
		bold: { bold: true, italic: false },
		italics: { bold: false, italic: true },
		bolditalics: { bold: true, italic: true },
	};	
	
	return this;
};

Runner.userFont.prototype = {
	/**
	 * @return {object}
	 */
	getFontDescription: function() {	
		return {
			normal: this.getFontFileByType( 'normal' ),
			bold: this.getFontFileByType( 'bold' ),
			italics: this.getFontFileByType( 'italics' ),
			bolditalics: this.getFontFileByType( 'bolditalics' ),
		};
	},
	
	/**
	 * @param {string} type
	 * @return {string} filename ( eg Koulen-Regular.ttf )
	 */
	getFontFileByType: function( type ) {
		var bold = this.desc[ type ].bold,
			italic = this.desc[ type ].italic;

		/*
			choose one for any type using a rule
			normal: normal || bold || italic || bolditalic
			bold: bold ||  normal || bolditalic || italic
			italic: italic || bolditalic || normal || bold
			bolditalic: bolditalic || italic || bold || normal		
		*/
		return this.findFontFile( bold, italic ) || this.findFontFile( !bold, italic ) 
			|| this.findFontFile( bold, !italic ) || this.findFontFile( !bold, !italic ) || '';
	},
	
	/**
	 * @param {boolean} bold
	 * @param {boolean} italic
	 * @return {string}
	 */
	findFontFile: function( bold, italic ) {
		var fontFileData = this.files.find( function( f ) {
				return f.italic === !!italic && ( bold && f.weight === 700 || !bold && f.weight === 400 );
			});
			
		return fontFileData && fontFileData.filename || '';
	},	
};	

setTimeout( function() {
	$("body, a").on("click", Runner.suggests.DestroySuggestDiv);
	$("body").on("click", "input", Runner.suggests.DestroySuggestDiv);
}, 2000);


 
/**
 * search suggests block
 */ 
Runner.suggests = {
	cur: -1,
	suggestValues: [],
	
	DestroySuggestDiv: function() {
		Runner.suggests.cur = -1;
		$("#search_suggest").html("").css("visibility", "hidden");
		$("#search_suggest_iframe").remove();
	},	
	
	moveUp: function( oElement ) {
		var $suggests = $("#search_suggest").children();
		
		if ( !$suggests.length || Runner.suggests.cur < -1 ) {
			return;
		}
		
		Runner.suggests.cur--;
		if ( Runner.suggests.cur == -2 ) {
			Runner.suggests.cur = $suggests.length - 1; 
			oElement.focus(); 
		}
		$suggests.each( function( i, suggest ) {
			suggest.className = i == Runner.suggests.cur ? "suggest_link_over" : "suggest_link";
			if ( i == Runner.suggests.cur ) {
				oElement.value = Runner.suggests.suggestValues[ Runner.suggests.cur ];
			}
		});
	},
		
	moveDown: function( oElement ) {
		var $suggests = $("#search_suggest").children();
		
		if ( !$suggests.length || Runner.suggests.cur >= $suggests.length ) {
			return;
		}
		
		Runner.suggests.cur++;
		$suggests.each( function( i, suggest ) {
			suggest.className = i == Runner.suggests.cur ? "suggest_link_over" : "suggest_link";
			if ( i == Runner.suggests.cur ) {
				oElement.value = Runner.suggests.suggestValues[ Runner.suggests.cur ];
			}
		});

		if ( Runner.suggests.cur == $suggests.length ) {
			Runner.suggests.cur = -1; 
			oElement.focus(); 
		}
	},

	searchSuggest: function( $el ) {
		Runner.suggests.cur = -1;
	
		//Set position for the search suggest
		var offset = $el.offset(),		
			$suggest = $("#search_suggest");	

		const style = {
			top: offset.top + $el.outerHeight() +  "px",
			
		};
		if( Runner.isDirRTL() ) { 
			const bounds = $el[0]?.getBoundingClientRect();
			style.right = ( document.documentElement.clientWidth - bounds?.right ) + "px";
			style.position = 'fixed';
		} else {
			style.left = offset.left + "px"
		}
		$suggest.css( style );
	
	},

	onSearchSuggestResponse: function( respObj, setValueFn ) {
		if ( !respObj.success || !respObj.result.length ) {
			Runner.suggests.DestroySuggestDiv();
			return;	
		}
		
		var $suggests = $("#search_suggest")
			.css({ visibility: "visible", 'z-index': 2000 })
			.html("");

		var $suggest, i, j;
		
		for (i = 0, j = 0; i < respObj.result.length; i++, j++) {
			Runner.suggests.suggestValues[j] = respObj.result[i].realValue;
			
			$suggest = $('<div id="suggestDiv' + i + '"></div>')
				.addClass('suggest_link')
				.html( respObj.result[i].value )
				.appendTo( $suggests )
				.on( 'mouseover', function(e) {
					$("div.suggest_link_over").each(function(){
						this.className = 'suggest_link';
					});
					this.className = 'suggest_link_over';
					Runner.suggests.cur = this.id.substring(10);
				})
				.on( 'mouseout', function(e) {
					this.className = 'suggest_link';
				})
				.on( 'click', { suggest: Runner.suggests.suggestValues[j] },  function(e) {
					if ( typeof setValueFn === 'function' ) {
						setValueFn( e.data.suggest );
					}
					Runner.suggests.DestroySuggestDiv();
				});
		}
	},	
};
 Runner.Notifications = function( pageObj, initData ) {

	/**
	 * in seconds
	 */
	var refreshInterval = 30;

	Runner.setDatePickerLocale();
	var toDate = function( str ) {
		if( !str ) {
			return new Date( 0 );
		}
		return moment( str, "YYYY-MM-DD hh:mm:ss").toDate();
	}

	var dateToString = function( dt ) {
		return moment( dt ).format('YYYY-MM-DD HH:mm:ss');
	}

	var self = {
		/**
		 * loaded message IDs stored to prevent duplicates
		 */
		messageIds: {},

		messages: initData.messages,
		
		lastUpdate: new Date(0),
		lastId: 0,

		/**
		 * Current time from server
		 */
		currentTime: toDate( initData.currentTime ),

		/**
		 * time difference between server and client in seconds
		 */
		timeDifference: Date.now() - toDate( initData.currentTime ).getTime(),

		/**
		 * Last read id for to send to the server.
		 * Updated when user opens the popup
		 */
		lastRead: parseInt( initData.lastRead, 10 ),

		/**
		 * Last read id for display purpose.
		 * Updated when user closes the popup
		 */
		lastReadDisplay: parseInt( initData.lastRead, 10 ),

		popupOpen: false,
	


		display: function() {
			var messageHtml = self.messages.map( function( m ) {
				m.id = parseInt( m.id, 10 );
				self.messageIds[ m.id ] = true;
				var o = {
					title:  m.title,
					message:  m.message,
					url: m.url,
					newWindow: m.newWindow
				};
				if( m.icon ) {
					o.icon = m.icon;
					o.iconType = 'file';
					if( m.icon.indexOf('/') == -1 && m.icon.indexOf('.') == -1 ) {
						if( m.icon.substr( 0, 3 ) == 'fa-') {
							o.iconType = 'fa';
						} else if( m.icon.substr( 0, 10 ) == 'glyphicon-' ) {
							o.iconType = 'bs';
						}
					}
				}
				var created = toDate( m.created );
				o.new = m.id > self.lastReadDisplay;
				o.time = self.formatTime( created );
				
				self.lastUpdate = self.lastUpdate > created ? self.lastUpdate : created;
				self.lastId = self.lastId > m.id ? self.lastId : m.id;
				return tmpl( 'noti-message', o );
			} );
			var $notifications = $('#notifications' + pageObj.id);
			$notifications.find( '.dropdown-menu > .r-noti-messages' ).html( messageHtml.join( "" ) );
			var newNotifications = self.messages.filter( function( m ) {
				return m.id  > self.lastReadDisplay;
			}).length;
			$notifications.children('button').children('.badge').html( newNotifications ? newNotifications : '' );
	
		},

		/**
		 * 
		 * @param {DateTime} created
		 */
		formatTime: function( created ) {
			var localTime = new Date( created.getTime() + this.timeDifference );
			var sInterval = ( Date.now() - localTime.getTime() ) / 1000;
			if( Runner.lang.constants.current_language == 'English' ) {
				//	use 'N<units> ago' for less than few days notifications, then date.
				if( sInterval < 2 * 24 * 60 * 60 ) {
					return this.formatInterval( sInterval );
				} else {
					return this.formatDate( localTime )
				} 
			} else {
				//	use 'now' for 2 min notifications, time for today's and date for all the rest
				if( sInterval < 2 * 60 ) {
					return Runner.lang.constants.NOW;
				} else if( moment(localTime).format('YYYYMMDD') == moment( new Date() ).format('YYYYMMDD') ) {
					return moment( localTime ).format( Runner.getTimeFormatString( false ) );
				} else {
					return self.formatDate( localTime );
				}
			}
		},

		formatDate: function( created ) {
			return moment( created ).format( Runner.getDateFormatString() );
		},

		formatInterval: function( seconds ) {
			if( seconds < 60 ) {
				return Runner.lang.constants.NOW;
			}
			if( seconds < 3600 ) {
				return Math.floor( seconds / 60 ) + Runner.lang.constants.NOTI_MINUTE;
			}
			if( seconds < 24 * 3600 ) {
				return Math.floor( seconds / ( 3600 ) ) + Runner.lang.constants.NOTI_HOUR;
			}
			if( seconds < 30 * 24 * 3600 ) {
				return Math.floor( seconds / ( 24 * 3600 ) ) + Runner.lang.constants.NOTI_DAY;
			}
			if( seconds < 365 * 30 * 24 * 3600 ) {
				return Math.floor( seconds / ( 30 * 24 * 3600 ) ) + 'mo';
			}
			return Math.floor( seconds / ( 365 * 30 * 24 * 3600 ) ) + 'y';
		},
		toDate: toDate,
		dateToString: dateToString,
		update: function( sendLastRead ) {
			var params = {
				lastId: self.lastId,
			};
			if( sendLastRead ) {
				params.lastRead = self.lastRead;
			}
			Runner.runnerAJAX( Runner.getPageUrl( "", "notifications" ), 
				params,
				function( resp ) {
					self.currentTime = toDate( resp.time );
					
					var renderNeeded = false;
					var newMessages = resp.messages.filter( function(m) {
						return !self.messageIds[ m.id ];
					});
					if( newMessages.length ) {
						self.messages = newMessages.concat( self.messages );
						renderNeeded = true;
					}
					//	remove expired
					self.messages = self.messages.filter( function( m ) {
						var expired = m.expire && self.currentTime >= toDate( m.expire ); 
						if( expired ) {
							renderNeeded = true;
						}
						return !expired;
					});
					
					if( renderNeeded ) {
						self.display();
					}
					if( self.popupOpen && newMessages.length ) {
						self.lastRead = self.lastId;
						self.update( true );
					}
			}, true );	
		},
		initModalClose: function() {
			var $notifications = $('#notifications' + pageObj.id);
			var $toggle = $notifications.find( '.dropdown-toggle' );
			var $close = $notifications.find( '.r-noti-close' );

			$notifications.find( '.dropdown-menu' ).on( 'click', function(e) {
				e.stopPropagation();
			});

			$close.on( 'click', function(e) {
				$toggle.trigger( "click" );
				return false;
			});
		},
		initToggle: function() {
			var $notifications = $('#notifications' + pageObj.id);
			//	set dropdown handler
			$notifications.on( 'shown.bs.dropdown', function() {
				self.popupOpen = true;
				self.lastRead = self.lastId;
				self.update( true );
				
				if( Runner.smallScreen ) {
					$('body').addClass( 'modal-open' );
				}
			} )
			.on( 'hidden.bs.dropdown', function() {
				self.lastReadDisplay = self.lastId;
				self.popupOpen = false;
				self.display();
				$('body').removeClass( 'modal-open' );
			} );

			//	start listening
			setInterval( function() {
				self.update();
			}, refreshInterval * 1000 );
		}
	};

	self.initToggle();
	self.initModalClose();
	self.display();
	
	return self;
}
