/**
 * NS Interface elements
 * Contains specific classes that interact with the interface.
 *
 * @version   1.00.090813
 * @author    LBI Lost Boys
 */
NS(function($){

	var CLASS_ACTIVE = 'active';
	var CLASS_ERROR  = 'error';

	var PRIORITY_LOW    = 1;
	var PRIORITY_MEDIUM = 2;
	var PRIORITY_HIGH   = 3;

	var ATTR_REQUIRED = 'ns:required';
	var ATTR_COMPLETE = 'ns:complete';
	var ATTR_PATTERN  = 'ns:pattern';

	var TYPE_TEXT   = 'text';
	var TYPE_PASS	= 'password';
	var TYPE_AREA   = 'textarea';
	var TYPE_SELECT = 'select-one';
	var TYPE_MULTI  = 'select-multiple';
	var TYPE_CHECK  = 'checkbox';
	var TYPE_RADIO  = 'radio';

	var FOCUS_NEXT  = 'next';
	var FOCUS_PREV  = 'previous';

	var PREF_VALIDATE_REQUIRED = 'validateRequired';
	var PREF_VALIDATE_SERVER = 'validateServer';

	/**
	 * Interface namespace
	 */
	NS.Interface = {
		initialize: function() {

			// create custom click events on links with an existing hash
			NS.Dispatcher.createEvent('click:link:hash', function(e){
				var link = e.target;
				var hash = link.hash;
				var target = hash && $(hash)[0];
				if(target) {
					// store the target on the event
					e.setProperty('hashTarget', target);
					return link;
				}
				return null;
			});

			// static interface elements
			this.briefings		 = new Briefings();
			this.clickAndShow	 = new ClickAndShow();
			this.smoothScrolling = new SmoothScrolling();
			this.highlighter	 = new Highlighter();
			this.validator		 = new Validator();
			this.readspeaker	 = new Readspeaker();
			this.compatibility	 = new BrowserCompatibility();
			this.layoutMonitor   = new LayoutMonitor();
		//	this.focusManager    = new FocusManager();
			
			// ajax enabled interface elements
			this.remembered		= NS.AjaxWrapper(new RememberedInputs());
			this.external		= NS.AjaxWrapper(new ExternalLinks());
			this.modifications	= NS.AjaxWrapper(new DOMModifications());
			this.drawers		= NS.AjaxWrapper(new Drawers());
			this.hints			= NS.AjaxWrapper(new InputHints());
		},

		displayError:function(input, toggle){
			var form = input.form;
			var error = this.getErrorElement(form);
			var origin = input.parentNode;
			var reg = /div|li/i;
			
			// use the closest div or li to set an error class on
			while(origin && !reg.test(origin.nodeName)) {
				origin = origin.parentNode;
			}
			
			var switchClass = toggle? 'addClass':'removeClass';
			$(origin)[switchClass](CLASS_ERROR);
			$(input)[switchClass](CLASS_ERROR);
			
			if(toggle) {
				error.show();
				error.html(NS.getProperty('MSG_REQUIRED'));
			}
		},
		
		displayErrorMessage:function(messages, form){
			var error = this.getErrorElement(form);
			if(messages) {
				error.html(messages);
				error.show();
			} else {
				error.html('');
			}
		},
		
		getErrorElement:function(form){
			return $('p.error', form).eq(0);
		},

		createIEFrame:function() {
			if(!/msie (5|6)/i.test(navigator.userAgent)) {
				return false;
			}

			var frame = $('iframe.cover-frm');
			if(frame.length == 0) {
				frame = $('<iframe src="/ns2009/static/blank.html" frameborder="0" class="cover-frm"></iframe>');
				$('#canvas').append(frame);
			}

			return frame;
		},

		/**
		 * "facades"
		 */
		scrollTo:function(target) {
			this.smoothScrolling.scrollTo(target);
		},

		highlight: function(target) {
			this.highlighter.highlight(target);
		}
	};

	/**
	 * DOMModifications adds or alters specific markup for styling purposes.
	 */
	function DOMModifications(){
		this.parseNode(document);
	}
	
	DOMModifications.prototype = {
		parseNode:function(root) {
			var jRoot = $(root);
			this.roundCorners(jRoot);
			this.stripeTables(jRoot);
			this.splitLists(jRoot);
		},

		roundCorners: function(root) {
			var elements = root.filter('.rounded').add(root.find('.rounded'));
			elements.each(function(){
				var element = $(this);
				if(element.find('> span.top').length > 0) {
					return;
				}
				element.prepend($('<span class="top"></span>'));
				element.append($('<span class="bottom"></span>'));
			});
		},
		
		stripeTables: function(root) {
			var rows = root.find('tbody tr:odd');
			rows.addClass('odd');
		},
		
		splitLists: function(root) {
			var lists = root.find('ul.splitted, ol.splitted');
			lists.removeClass('splitted');
			lists.each(function(){
				var items = $('li', this);
				var l = items.length -1;
				var pos = Math.ceil(items.length/2);
				var other = $(document.createElement(this.nodeName));
				
				for(var i=l; i>=pos; i--) {
					other.prepend(items[i]);
				}

				other.addClass(this.className);
				$(this).after(other);
			});
		}
	};


	/**
	 * Drawers controls all toggleable (haha) elements. A link is inserted
	 * to the respective headers to switch the drawer's visibility.
	 */
	function Drawers() {
		NS.relateLink(/drawer-/, this.handleClick.bind(this) /*, null */);
		this.parseNode(document);
	}

	Drawers.prototype = {
		parseNode:function(root) {
			
			// caused occasional IE6 warnings about slowness and unresponsiveness
			// var items = $('div.drawer-item .drawer-switch:not(a)', root);
			var items = $('.drawer-item', root).find('h2, strong').filter('.drawer-switch');
			
			items.each(function(){
				var link = $('a', this);
				if(link.length > 0) {
					link[0].rel = 'drawer-toggle';
					return;
				}
				
				var stat = this.getAttribute('ns:sitestat');
				$(this).wrapInner('<a href="#' + this.id + '" rel="drawer-toggle" ' +
					(stat? (' ns:sitestat="'+stat+'"'):'') + '></a>');
			});
		},

		handleClick:function(link, rel) {
			var item = $(link).closest('.drawer-item');
			var list = item.closest('.drawer-collection');
			if(!list.hasClass('multiple')) {
				item.siblings('.drawer-item').removeClass('opened');
			} 
			
			item.toggleClass('opened');
			NS.Dispatcher.fire('layoutchanged', item[0]);
			
			return true;
		}
	};

	/**
	 * InputHints displays the title attribute of text inputs as their initial value
	 */
	function InputHints() {
		this.parseNode(document);
	}

	InputHints.prototype = {
		parseNode:function(root) {
			var inputs = $('input:text[title], textarea[title]', root);
			
			inputs.bind('focus', function(e){ 
				if(this.value === this.title) {
					this.value = ''; 
					this.style.color = '';
				}
			});
			
			inputs.bind('blur',  function(e){ 
				if(this.value === '' || this.value === this.title) {
					this.style.color = '#7F7FB2';
					this.value = this.title; 
				}
			});

			inputs.triggerHandler('blur');
		}
	};

	/**
	 * Briefings toggles the summary on T6 pages.
	 */
	function Briefings() {
		var lang = NS.getLanguage();
		var briefing = $('#main-content > .briefing');
		var text = /nl/i.test(lang)? 'Toon extra tekst' : 'Show additional text';
		
		var labelSwitches = [
			{ reg: /^Toon/, value: 'Verberg' },
			{ reg: /^Verberg/, value: 'Toon' },
			{ reg: /^Show/, value: 'Hide' },
			{ reg: /^Hide/, value: 'Show' }
		];

		function getSwitchedLabel(label) {
			var l = labelSwitches.length;
			for(var i=0; i<l; i++) {
				var item = labelSwitches[i];
				if(item.reg.test(label)) {
					return label.replace(item.reg, item.value);
				}
			}
		}
		
		if (briefing.hasClass('open')) {
			text = getSwitchedLabel(text);
		}
		
		var button = $('<a href="#" class="briefing-trigger">' + text + '</a>');
		
		briefing.before(button);

		button.click(function(){
			var label = getSwitchedLabel(button.text());
			briefing.toggleClass('open');
			button.html(label);

			NS.Dispatcher.fire('layoutchanged', briefing[0]);
		});
	}

	/**
	 * ClickAndShow is used on T5 pages to switch between hidden content sections.
	 * The "clicknshow" list links to the id's of these sections using a hash.
	 * Observes in high priority mode to precede other events that might rely on visibility
	 */
	function ClickAndShow() {
		var lists = $('ul.clicknshow');
		if(lists.length > 0) {
			NS.subscribe('click:link:hash', this.handleClick.bind(this), PRIORITY_HIGH);
			this.checkLocation();
		}
	}

	ClickAndShow.prototype = {
		checkLocation:function(){
			var hash = window.location.hash;
			if(hash) {
				var link = $('a[href$="'+hash+'"]')[0];
				var target = $(hash)[0];
				if(link && target) {
					this.activate(link, target);
				}
			}
		},

		handleClick:function(e) {
			var link = e.target;
			var target = e.hashTarget;
			var list = $(link).closest('.clicknshow')[0];
			if(target && list) {
				this.activate(link, target);
			}
		},

		activate:function(link, target) {
			try {
				var active = $(link).closest('ul').find('li.active');
				var current = $(active.find('a')[0].hash);
				active.removeClass(CLASS_ACTIVE);
				current.removeClass(CLASS_ACTIVE);
			} catch (e) { }
			$(link.parentNode).addClass(CLASS_ACTIVE);
			$(target).addClass(CLASS_ACTIVE);
			$("body").addClass('clickednshown');//hide promos once a briefing is clicked
			
			NS.Dispatcher.fire('clickandshow', target);
			NS.Dispatcher.fire('layoutchanged', document.body);
		}
	};

	/**
	 * External links or links that point to enclosures like pdf files, are provided
	 * with an icon marking them as external. A new window (or tab) is opened for
	 * these links via the link relation object.
	 */
	function ExternalLinks() {
		NS.relateLink(/external/, this.handleClick /*, null */);
		this.extension = /msie 6/i.test(navigator.userAgent)? 'gif' : 'png';
		this.parseNode(document);
	}

	ExternalLinks.prototype = {
		handleClick: function(link, rel) {
			window.open(link.href);
			return true;
		},

		parseNode:function(root) {
			var iconHTML = '<img src="/ns2009/static/images/icons/external.' + 
				this.extension + '" class="icon" width="22" height="15" />';
				
			$('a[rel=external], a[rel=enclosure]', root).each(function(){
				$(this).append(iconHTML);
			});
		}
	};

	/**
	 * Smoothscrolling captures clicks on links that point to a specific hash (id) on
	 * the page, and scrolls to them over a short period. rather than jumping directly.
	 * Observes in low priority, to follow after any events that may change the layout.
	 */
	function SmoothScrolling() {
		NS.subscribe('click:link:hash', this.handleClick.bind(this), PRIORITY_LOW);
	}

	SmoothScrolling.prototype = {
		handleClick: function(e) {
			e.preventDefault(); // don't jump to the hash
			this.hash = e.target.hash;
			this.scrollTo(e.hashTarget);
		},

		scrollTo:function(target) {
			if(!this.window) {
				this.window = $(window);
				this.animator = new NS.Animator(this.scroll, this.highlightTarget.bind(this));
			}
			
			var jTarget = $(target);
			if(jTarget.is(':visible')) {
				this.target = target;
				var currentY = this.window.scrollTop();
				var viewHeight = this.window.height();
				var targetY = $(target).offset().top;
				if(targetY < currentY || targetY > (currentY + viewHeight)) {
					this.animator.run(currentY, targetY, 600);
				} else {
					this.highlightTarget(false);
				}
			}
		},

		scroll:function(value) {
			window.scrollTo(0, value);
		},

		highlightTarget:function(doHash) {
			if(this.target == document.body) { 
				return; 
			}
			
			NS.Interface.highlight(this.target);
			var hash = this.hash || this.target.id;
			if(hash && doHash !== false) {
				window.location.hash = hash;
			}
		}
	};

	/**
	 * The Highlighter highlights elements on the page, for instance when the visibility
	 * is toggled from one element to another, or a smoothscroll animation has ended.
	 */
	function Highlighter() {
		// don't do anything untill needed
	}

	Highlighter.prototype = {
		highlight: function(target){
			if(NS.Dispatcher.fire('highlight', target) === false) {
				return;
			}

			if(!this.pointer) {
				this.pointer = $('<div id="pointer"></div>');
				this.root = $('#canvas');
				this.animator = new NS.Animator(
					this.animate.bind(this),
					this.hide.bind(this)
				);
			}
			
			var jTarget = $(target);
			var offset = jTarget.offset();
			this.pointer.css({
				left: offset.left + 'px',
				top: offset.top + 'px',
				width: jTarget.outerWidth() + 'px',
				height: jTarget.outerHeight() + 'px',
				opacity: 0
			});

			this.root.append(this.pointer);
			this.animator.run(0, Math.PI, 1000);
		},

		animate:function(value) {
			this.pointer.css({ opacity: Math.sin(value) * 0.4 });
		},

		hide:function(e) {
			this.pointer.remove();
		}
	};

	/**
	 * Readspeaker reads either the full page, a selection of text, or the contents of an 
	 * element (id-) targeted by a link's hash. That link must have a rel="readspeaker".
	 */
	function Readspeaker() {
		this.exceptions = /^(tr|option)$/i;
		NS.relateLink(/readspeaker/, this.readElement.bind(this));
	}

	Readspeaker.prototype = {
		read:function(node) {
			var content = this.getSelection();
			
			if(!content) {
				content = this.parse(node).innerHTML;
			}
			
			this.form = document.getElementById('rs_form');
			if(!this.form) {
				this.form = this.createForm();
			}
			
			this.form.target = 'rs';
			this.form.rshtml.value = content;
			var rs = window.open("about:blank", "rs", "width=400, height=220");
			rs.focus();
			this.form.submit();
		},
		
		readElement:function(link, rel) {
			var target = $(link.hash || '#content')[0];
			if(target) {
				this.read(target);
			}
			return true;
		},
		
		createForm:function() {
			var form = $('<form method="post" action="http://asp.readspeaker.net/cgi-bin/nsrsone" target="rs" id="rs_form">' + 
					'<input type="hidden" name="rshtml" value="" />' + 
					'<input type="hidden" name="url" value="' + window.location.href + '" />' + 
					'<input type="hidden" name="customerid" value="1003740" />' + 
					'<input type="hidden" name="lang" value="nl" />' + 
				'</form>');
			
			$('body').append(form);
			return form[0];
		},

		getSelection:function() {
			return document.getSelection? document.getSelection() :
				window.getSelection? window.getSelection() :
				document.all? document.selection.createRange().text : false;
		},
			
		parse:function(node) {
			var buffer = document.createElement("div");
			this.parseNode(node, buffer);
			return buffer;
		},

		parseNode:function(node, buffer) {
			var l = node.childNodes.length, child, clone;
			for(var i=0; i<l; i++) {
				child = node.childNodes.item(i);
				if(child.nodeType === 1 && !child.offsetHeight && !child.offsetWidth && !this.exceptions.test(child.nodeName)) {
					continue;
				}

				clone = buffer.appendChild(child.cloneNode(false));
				this.parseNode(child, clone);
			}
		}
	};

	/**
	 * The validator observes submit and ajax submit events, and validates the form's 
	 * input before allowing it to be posted. Applications are queried for preferences.
	 */
	function Validator() {
		NS.Dispatcher.subscribe('submit', this.onsubmit.bind(this));
		NS.Dispatcher.subscribe('ajaxsubmit', this.onsubmit.bind(this));
	}

	Validator.prototype = {
		/**
		 * main onsubmit handler, queries apps for preferences and validates
		 */
		onsubmit:function(e) {
			var valid = true;
			var form = e.target;
			var app = NS.findApplication(form);

			if(app.prefers(PREF_VALIDATE_REQUIRED)) {
				valid &= this.validateRequired(form);
			}

			if(valid && app.prefers(PREF_VALIDATE_SERVER)) {
				// e.target is the form, the explicitTarget is the clicked submit button
				valid &= this.validateServer(form, e.data.explicitTarget);
			}

			if(!valid) {
				e.preventDefault();
				return false;
			}
		},

		validateRequired:function(form){
			var elements = form.elements || $('input,select,textarea', form), valid = true;
			var l = elements.length;

			for (var j=0; j<l; j++) {
				NS.Interface.displayError(elements[j], false);
			}
			
			for(var i=0; i<l; i++) {
				var input = elements[i];
				var hasValue = this.hasValue(input);
				
				if(this.isRequired(input)) {
					if(!hasValue) {
						NS.Interface.displayError(input, true);
					}
					valid &= hasValue;
				}

				var pattern = input.getAttribute(ATTR_PATTERN);
				if(pattern && hasValue) {
					var match = this.matches(input, pattern);
					NS.Interface.displayError(input, !match);
					valid &= match;
				}
			}
			
			if(valid) {
				NS.Interface.displayErrorMessage(false);
			}
			
			return valid;
		},

		isRequired:function(input) {
			var req = input.getAttribute(ATTR_REQUIRED);
			var required = /true/i.test(req);
			var optional = /false/i.test(req);
			var completed = input.getAttribute(ATTR_COMPLETE);
			var disabled = input.disabled;
			var visible = input.offsetHeight > 0;
			
			if(completed) { required = true; } // autocomplete implies required
			if(optional) { required = false; } // optional overrules implied required

			return (visible && !disabled && required);
		},

		hasValue:function(input) {
			if(!input.type) { return false; } //sometimes fomr.elements returns a fieldset (without a type attribute)) which should be skipped in this check
			
			var type = input.type.toLowerCase();
			switch (type) {
				case TYPE_TEXT: 
				case TYPE_PASS: 
				case TYPE_AREA:
					return (input.value && input.value !== input.title) ? true : false;
				case TYPE_CHECK: 
				case TYPE_RADIO: 
					return this.checkedOne(input);
				case TYPE_SELECT: 
				case TYPE_MULTI: 
					return this.selectedOne(input);
			}
		},

		checkedOne:function(input) {
			var name = input.name, checks = name? input.form.elements[name] : [];
			checks = checks.length? checks : [input];
			for (var i=0; i<checks.length; i++) {
				if(checks[i].checked) { return true; }
			}	return false;
		},

		selectedOne:function(select) {
			return select.selectedIndex > 0; // the first option is considered a label, not a value
		},

		matches:function(input, pattern) {
			var reg = new RegExp("^"+pattern+"$", "i");
			return reg.test(input.value);
		},

		validateServer:function(form, explicitTarget) {
			var post = NS.getFormValues(form);
			var url = NS.getProperty('POST_VALIDATE', form);
			NS.XHR.sendAndLoad(post, url, function(xml){
				this.handleResponse(xml, form, explicitTarget);
			}.bind(this));
			
			// submitting is delayed, so return invalid here
			return false;
		},
		
		handleResponse:function(xml, form, explicitTarget) {
			var messages = [];
			$('error', xml).each(function(){
				var name = this.getAttribute('veld'),
					input = form.elements[name];

				if(input) {
					messages.push('- ' + this.firstChild.nodeValue);
					NS.Interface.displayError(input, true);
				}
			});

			if(messages.length > 0) {
				NS.Interface.displayErrorMessage(messages.join(' <br />'), form);
			} else {
				NS.Interface.displayErrorMessage(false);
				if(explicitTarget) {
					// if the original event had an explicitTarget input, click it
					$(explicitTarget).trigger('click');
				} else {
					// otherwise just submit the form
					form.submit();
				}
			}
		}
	};
	
	/**
	 * RememberedInputs sets a cookie for input fields that have their value stored
	 */
	function RememberedInputs() {
		NS.subscribe('submit', this.handleSubmit.bind(this));
		NS.subscribe('click:input', this.handleClick.bind(this));
		
		this.restoreValues(document);
	}
	
	RememberedInputs.prototype = {
		parseNode:function(node) {
			this.restoreValues(node);
		},

		handleSubmit:function(e) {
			var checks = $(e.target).find('input:checkbox');
			var l = checks.length;
			for(var i=0; i<l; i++) {
				var check = checks[i];
				if(check.getAttribute('ns:remember')) {
					this.storeValues(check);
				}
			}
		},

		handleClick:function(e){
			var input = e.target;
			
			// old way; store input value based on implied id reference
			var id = input.id;
			var reg = /remember-([^ ]+)/i;
			if(id && reg.test(id)) {
				var relatedInput = document.getElementById(reg.exec(id)[1]);
				if(relatedInput) {
					this.storeValue(relatedInput, input.checked);
				}
			}	
			
			// new way; store named inputs via custom attribute reference
			var remember = input.getAttribute('ns:remember');
			if(remember) {
				this.storeValues(input);
			}
		},

		storeValue:function(target, checked) {
			var name = 'remember-' + target.name;
			NS.setCookie(name, checked? target.value : "");
		},

		storeValues:function(input) {
			var form = input.form;
			var names = (input.name + ',' + input.getAttribute('ns:remember')).split(',');
			var l = names.length;
			for (var i=0; i<l; i++) {
				var target = form.elements[names[i]];
				if(target) {
					this.storeValue(target, input.checked);
				}
			}
		},
		
		getRememberedValues:function() {
			if(this.values) {
				return this.values;
			}

			// check the cookie for any remembered-xyz entries
			var value, cookie = document.cookie, remember = /remember-[^=]+=[^;\$]+/mig;
			var values = [];
			while((value = remember.exec(cookie))) {
				values.push(String(value));
			}

			this.values = values;
			return values;
		},

		restoreValues:function(root) {
			var values = this.getRememberedValues();
			var inputs = $(root || document).find('input');
			var l = values.length;
			var reg = /remember-/;

			for (var i=0; i<l; i++) {
				var splitted = values[i].split('=');
				var id = splitted[0];
				var name = id.replace(reg, '');
				var value = splitted[1];

				var input = inputs.filter('[name="'+name+'"]')[0];
				if(input && value) {
					switch (input.type.toLowerCase()) {
						case 'text': 
							if(!input.value) input.value = decodeURIComponent(value); 
						break;
						case 'checkbox':
						case 'radio':
							input.checked = true;
						break;
					}

					// backward compatibility
					var check = document.getElementById(id);
					if(check) {
						check.checked = true;
					}
				}
			}
		}
	};
	
	/**
	 * BrowserCompatibility checks various browser specific quirks
	 */
	function BrowserCompatibility() {
		this.cacheBackgrounds();
		this.checkDPI();
	}
	
	BrowserCompatibility.prototype = {
		cacheBackgrounds:function() {
			// IE Anti-background flicker, conditional comment, runs in IE only
			/*@cc_on
				try { document.execCommand('BackgroundImageCache', false, true); } catch (e) {}
			@*/
		},
		
		checkDPI:function() {
			// MSIE DPI setting, only for IE7 or below, not for IE8 running as IE7,
			// because in IE8 the DPI is interpreted as a full page zoom factor, not as textzoom
			if(!document.documentMode && screen.deviceXDPI && (screen.deviceXDPI != 96)) {
				var ratio = (96/screen.deviceXDPI) * 100;
				document.body.style.fontSize = ratio + '%';
			}
		}
	};

	/**
	 * Monitors the "layoutchanged" event to update various components. This event may be fired by 
	 * objects that (for instance) change classnames in order to toggle element visibility.
	 */
	function LayoutMonitor() {
		NS.subscribe('layoutchanged', this.layoutUpdated.bind(this));
	}

	LayoutMonitor.prototype = {
		layoutUpdated: function(e) {
			var root = $(e.target);

			// if object/embed elements become invisible, they need to stop playing.
			this.updateJWPlayer(root);

			// possible future layout checks
			// ...
		},

		updateJWPlayer:function(root) {
			var players = root.find('object,embed');
			players.each(function(){
				var player = this;
				var h = player.offsetHeight;
				if(!h || h === 0) { // hidden elements have 0 or no offsetheight
					try {
						player.sendEvent("PLAY", false);
					} catch (e) {
						NS.log('SendEvent failed or was not supported', player, e);
					}
				}
			});
		}
	};

	/**
	 * automatically passes the focus from a clicked form element to an indicated other element
	 */
	function FocusManager() {
		NS.subscribe('change', this.delayedClick.bind(this));
	}

	FocusManager.prototype = {
		delayedClick:function(e) {
			var input = e.target;
			var self = this;
			setTimeout(function(){
				self.handleClick(input);
			}, 200);
		},

		handleClick:function(input) {
			var target = $(input).attr('ns:autofocus');
			if(!target) {
				return;
			}

			switch (target) {
				case FOCUS_NEXT:
					this.focusNext(input, 1);
				break;
				case FOCUS_PREV:
					this.focusPrevious(input, -1);
				break;
				default:
					var element = input.form.elements[target];
					this.focus(element);
				break;
			}
		},
		
		focusNext:function(origin, offset) {
			var form = origin.form;
			var elements = $($.makeArray(form.elements)).filter('input:visible,select:visible');
			for(var i=0; i<elements.length; i++) {
				if(elements[i] == origin) {
					var index = Math.max(i + offset, 0);
					this.focus(elements[index]);
					break;
				}
			}
		},

		focus:function(element) {
			try {
				element.focus();
			} catch (e) {
			}
		}
	};

	/**
	 * Bind to NS.initialize
	 */
	NS.subscribe('initialize', function(){
		NS.Interface.initialize();
	});

});
