/* DX namespace */
if (typeof DX === 'undefined') { DX = {}; }

DX.emptyFunction = function() {};

DX.typeOf = function typeOf(value) {
	var type = typeof value;
	if (type === 'object') {
		if (!value) { return 'null'; }
		if (typeof value.length === 'number' && !(value.propertyIsEnumerable('length')) && typeof value.splice === 'function') { return 'array'; }
	}
	return type;
};

DX.supplant = String.prototype.supplant = function (supplantData, removeUnmatched) {
	return this.replace(/#{([^{}]*)}/g, function(fullMatch, subMatch) {
		var replacement = supplantData[subMatch];
		return (typeof replacement === 'string' || typeof replacement === 'number') ? replacement : (removeUnmatched === true ? '' : fullMatch);
	});
};

DX.regEscape = RegExp.escape = function(str) {
	return str.replace(/[.*+?|(){}\[\]\\]/g, '\\$&');
};

DX.log = function(text) {
	if (typeof console === 'undefined') { return false; }
	var date = new Date();
	console.log(date.toLocaleString() + '.' + date.getMilliseconds() + ' ' + text);
	return true;
};

DX.logRaw = function(obj) {
	if (typeof console === 'undefined') { return false; }
	console.log(obj);
	return true;
};


/* DX.Fragment */

DX.Fragment = {
	// Fields
	previousFragments: null,
	pollInterval: 20,
	observers: [],

	// Methods
	hasFragment: function(key) {
		return this.getFragments().hasOwnProperty(key);
	},
	getFragment: function(key) {
		return this.getFragments()[key];
	},
	getFragments: function() {
		if (window.location.hash === '' || window.location.hash === '#') { return {}; }
		var fragments = {}, pairs = window.location.hash.substring(1).split('&');
		for (var i=0; i<pairs.length; i++) {
			var pair  = pairs[i].split('=');
			var key   = decodeURIComponent(pair[0]);
			var value = typeof pair[1] === 'undefined' ? undefined : decodeURIComponent(pair[1]);
			fragments[key] = value;
		}
		return fragments;
	},
	updateFragment: function(key, value) {
		var keyAndValue = {};
		keyAndValue[key] = value;
		this.updateFragments(keyAndValue);
	},
	updateFragments: function(keysAndValues) {
		var fragments = this.getFragments();
		for (var key in keysAndValues) {
			var value = keysAndValues[key];
			if (value === null) { delete fragments[key]; }
			else { fragments[key] = value; }
		}
		this.setFragments(fragments);
	},
	setFragments: function(fragments) {
		var fragmentString = this.encodeFragments(fragments, false);
		if (fragmentString.length === 0 && Object.keys(this.getFragments()).length === 0) { return; } // Don't attempt to modify the hash if it was and is supposed to be empty (this is just a cosmetic fix to avoid having a useless '#' at the end of the url)
		window.location.hash = fragmentString;
	},
	removeFragment: function(key) {
		this.updateFragment(key, null);
	},
	encodeFragments: function(fragments, entityAmps) {
		var parts = [];
		for (var key in fragments) {
			if (!fragments.hasOwnProperty(key)) { continue; }
			parts.push(encodeURIComponent(key) + (typeof fragments[key] === 'undefined' ? '' : '=' + encodeURIComponent(fragments[key])));
		}
		return parts.join(entityAmps ? '&amp;' : '&');
	},
	start: function() {
		setInterval(this.pollChanges.bind(this), this.pollInterval);
	},
	pollChanges: function() {
		if (window.location.hash != '' && this.previousFragments != window.location.hash) {
			this.notifyObservers();
			this.previousFragments = window.location.hash;
		}
	},
	notifyObservers: function() {
		var fragments = this.getFragments();
		for (var i=0; i<this.observers.length; i++) {
			this.observers[i](fragments);
		}
	},
	observe: function(callBackFn) {
		this.observers[this.observers.length] = callBackFn;
	}
};


/* DX.ValueChangeNotifier */

DX.ValueChangeNotifier = function(element, callback, optionsOverrides) {
	this.element   = $(element);
	this.callback  = callback;
	this.lastValue = this.element.value;
	this.timer     = null;

	this.options = {
		delay:     0.3,
		minLength: 2,
		notifyWhenDecreasing: false // Notify when length is decreasing, even if minLength requirement isn't fulfilled (so styles changes done by the callback can be reversed)
	};
	Object.extend(this.options, optionsOverrides || {});

	this.element.observe('keyup', this.elementChanged.bindAsEventListener(this));
};

DX.ValueChangeNotifier.prototype.elementChanged = function(e) {
	if (this.element.value === this.lastValue) { return; } // No actual change.
	if (this.element.value.length >= this.options.minLength || (this.options.notifyWhenDecreasing && this.element.value.length < this.lastValue.length)) {
		var belowMinLength = this.element.value.length < this.options.minLength;
		this.lastValue = this.element.value;
		this.cancelPendingNotication();
		this.timer = this.checkLengthAndNotifyCallback.bind(this, e.keyCode, belowMinLength).delay(this.options.delay);
	}
};

DX.ValueChangeNotifier.prototype.cancelPendingNotication = function() {
	if (this.timer) { window.clearTimeout(this.timer); }
};

DX.ValueChangeNotifier.prototype.checkLengthAndNotifyCallback = function(keyCode, belowMinLength) {
	if (belowMinLength || this.element.value.length >= this.options.minLength) { this.callback(keyCode, this, belowMinLength); }
};


/* DX.Autocomplete */

// Methods related to construction/initialization =>

DX.Autocomplete = function(formFieldElement, suggestionsElement, url, optionsOverrides) {
	this.formFieldElement   = $(formFieldElement);
	this.suggestionsElement = $(suggestionsElement);
	this.url                = url;
	this.ignoreRequests     = true;

	this.options = {
		delay:           0.3,
		minLength:       2,
		parameterName:   'value',
		selectedClass:   'ac_selected',
		oddClass:        'ac_odd',
		evenClass:       'ac_even',
		evalJSON:        true,
		topAdjustment:   0,
		leftAdjustment:  0,
		widthAdjustment: 0,
		debug:           false
	};
	Object.extend(this.options, optionsOverrides || {});

	this.positionRelativeTo = $(this.options.positionRelativeTo)      || this.formFieldElement;
	this.getParameterValue  = this.options.getParameterValueCallback  || this.defaultGetParameterValueCallback;
	this.findSuggestions    = this.options.findSuggestionsCallback    || this.defaultFindSuggestionsCallback;
	this.render             = this.options.renderCallback             || this.defaultRenderCallback;
	this.renderSelection    = this.options.renderSelectionCallback    || this.defaultRenderSelectionCallback;
	this.renderSuggestions  = this.options.renderSuggestionsCallback  || this.defaultRenderSuggestionsCallback;
	this.getSuggestionId    = this.options.getSuggestionIdCallback    || this.defaultGetSuggestionIdCallback;
	this.getSuggestionValue = this.options.getSuggestionValueCallback || this.defaultGetSuggestionValueCallback;
	this.getSuggestionItem  = this.options.getSuggestionItemCallback  || this.defaultGetSuggestionItemCallback;
	this.getSuggestionsHead = this.options.getSuggestionsHeadCallback || this.defaultGetSuggestionsHeadCallback;
	this.getSuggestionsTail = this.options.getSuggestionsTailCallback || this.defaultGetSuggestionsTailCallback;
	this.useSuggestion      = this.options.useSuggestionCallback      || this.defaultUseSuggestionCallback;
	this.unhandledKeyDown   = this.options.unhandledKeyDownCallback   || DX.emptyFunction;

	this.templateItem = this.options.templateItem || "<div id='#{id}' class='dx_autocomplete_item #{oddeven}'>#{content}</div>";
	this.templateHead = this.options.templateHead || "<div class='dx_autocomplete_head'><div class='dx_autocomplete_head_inner'>#{content}</div></div><div class='dx_autocomplete_body'><div class='dx_autocomplete_body_inner'>";
	this.templateTail = this.options.templateTail || "</div></div><div class='dx_autocomplete_tail'><div class='dx_autocomplete_tail_inner'>#{content}</div></div>";

	this.valueChangeNotifier = new DX.ValueChangeNotifier(formFieldElement, this.requestSuggestions.bind(this), { delay: this.options.delay, minLength: this.options.minLength });

	this.reset(); // Initializes/resets suggestions-related data, see below.
	this.formFieldElement.observe('keydown', this.onElementKeyDown.bindAsEventListener(this));
	this.formFieldElement.observe('blur',    this.onElementBlur.bindAsEventListener(this));
};

DX.Autocomplete.prototype.reset = function() {
	this.searchValue   = null;
	this.suggestions   = null;
	this.selectedIndex = -1;
	this.hoveredIndex  = -1;
};

// Methods related to Ajax and rendering the response =>

DX.Autocomplete.prototype.requestSuggestions = function(keyCode) {
	if (keyCode === 13) { return; } // Don't react to selection of suggestions using enter key (see onElementKeyDown function).
	this.hideSuggestions(); // Hide previous suggestions while a new request is in progress.
	var params  = {}; params[this.options.parameterName]  = this.getParameterValue(this);
	var sparams = {}; sparams[this.options.parameterName] = encodeURIComponent(this.getParameterValue(this)); // Have to encode the supplantable parameter to make it safe to insert into a url (i.e. supplantedUrl, below)
	if (this.options.debug) { DX.log('Requesting: ' + this.url + ' with parameter: ' + this.options.parameterName + '=' + sparams[this.options.parameterName]); }
	var supplantedUrl = this.url.supplant(sparams);
	new Ajax.Request(supplantedUrl, {
		method:     'get',
		parameters: this.url === supplantedUrl ? params : {}, // If the value was supplanted into the url, it's not added as a parameter.
		onSuccess:  Function.prototype.defer.bind(this.handleSuccess.bind(this, this.formFieldElement.value)), // Defer to make exceptions happen outside the request thread.
		onFailure:  Function.prototype.defer.bind(this.handleFailure.bind(this, this.formFieldElement.value)), // Defer to make exceptions happen outside the request thread.
		evalJSON:   this.options.evalJSON
	});
	this.ignoreRequests = false;
};

DX.Autocomplete.prototype.ignoreActiveRequests = function() {
	this.valueChangeNotifier.cancelPendingNotication();
	this.ignoreRequests = true;
};

DX.Autocomplete.prototype.defaultGetParameterValueCallback = function(autocomplete) {
	return this.formFieldElement.value;
};

DX.Autocomplete.prototype.handleSuccess = function(searchValue, response) {
	if (this.ignoreRequests) { DX.log('Autocompletion for the term "' + searchValue + '" succeeded and was ignored.'); }
	else {
		if (this.options.debug) { DX.log('Autocompletion for the term "' + searchValue + '" succeeded.'); }
		var t = response.request.transport;
		if (response.responseJSON == null && t.instanceId && t.instanceId.startsWith('flXHR') && response.responseText.isJSON()) { response.responseJSON = response.responseText.evalJSON(); } // Workaround for flash/flXHR requests not preserving the content type and thus not auto-evaluating a JSON response. Cannot use evalJSON: 'force' as that makes flXHR crash the browser if the response isn't valid JSON.
		this.reset();
		this.searchValue = searchValue;
		this.suggestions = this.findSuggestions(response);
		this.render(this, false);
	}
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};

DX.Autocomplete.prototype.handleFailure = function(searchValue, response) {
	if (this.ignoreRequests) { DX.log('Autocompletion for the term "' + searchValue + '" failed and was ignored.'); }
	else {
		if (this.options.debug) { DX.log('Autocompletion for the term "' + searchValue + '" failed.'); }
		this.reset();
		this.searchValue = searchValue;
		this.render(this, false);
	}
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};

DX.Autocomplete.prototype.defaultRenderCallback = function(autocomplete, onlySelectionChange, previouslySelectedIndex) {
	if (onlySelectionChange) { this.renderSelection(this, previouslySelectedIndex); } else { this.renderSuggestions(this); }
};

DX.Autocomplete.prototype.defaultRenderSuggestionsCallback = function(autocomplete) {
	if (this.suggestions == null && this.options.debug) { return DX.log('No suggestions to render.'); }

	var idList = [];
	var markup = this.templateHead.supplant({ content: this.getSuggestionsHead(this) });
	for (var i = 0; i < this.suggestions.length; i++) {
		var suggestion = this.suggestions[i];
		var id = this.getSuggestionId(suggestion, i);
		idList.push(id);
		var oddeven = i % 2 ? this.options.oddClass : this.options.evenClass;
		markup += this.templateItem.supplant({ id: id, content: this.getSuggestionItem(this, suggestion), oddeven: oddeven });
	}
	markup += this.templateTail.supplant({ content: this.getSuggestionsTail(this) });
	this.suggestionsElement.update(markup);

	for (var i = 0; i < idList.length; i++) {
		id = idList[i];
		$(id).observe('click',     this.onSuggestionClick.bindAsEventListener(this, i));
		$(id).observe('mouseover', this.onSuggestionMouseOver.bindAsEventListener(this, i));
		$(id).observe('mouseout',  this.onSuggestionMouseOut.bindAsEventListener(this, i));
	}

	// TODO - Refactor the positioning code below into one or more separate DX functions (positionBelow(), etc.).
	var sLayout  = this.suggestionsElement.getLayout();
	var sPadding = sLayout.get('border-box-width') - sLayout.get('width');
	var rLayout  = this.positionRelativeTo.getLayout();

	this.suggestionsElement.setStyle({
		position: 'absolute',
		top:      (rLayout.get('top') + rLayout.get('border-box-height') + this.options.topAdjustment) + 'px',
		left:  	  (rLayout.get('left') + this.options.leftAdjustment) + 'px',
		width:    (rLayout.get('border-box-width') - sPadding + this.options.widthAdjustment) + 'px'
	});
	this.positionRelativeTo.parentNode.insert(this.suggestionsElement);
	this.suggestionsElement.show();
};

DX.Autocomplete.prototype.defaultRenderSelectionCallback = function(autocomplete, previouslySelectedIndex) {
	var suggestion = this.getSelectedSuggestion();
	if (suggestion != null) {
		var suggestionId = this.getSuggestionId(suggestion, this.selectedIndex);
		$(suggestionId).addClassName(this.options.selectedClass);
	}

	suggestion = this.getSuggestion(previouslySelectedIndex);
	if (suggestion != null) {
		suggestionId = this.getSuggestionId(suggestion, previouslySelectedIndex);
		$(suggestionId).removeClassName(this.options.selectedClass);
	}
};

DX.Autocomplete.prototype.defaultGetSuggestionIdCallback = function(suggestion, index) {
	return this.suggestionsElement.id + '_index_' + index;
};

DX.Autocomplete.prototype.defaultGetSuggestionValueCallback = function(suggestion) {
	if (typeof suggestion === 'string') { return suggestion; }
	if (DX.typeOf(suggestion) === 'object') {
		if (suggestion.hasOwnProperty('value')) { return suggestion.value; }
		if (suggestion.hasOwnProperty('name'))  { return suggestion.name; }
		if (suggestion.hasOwnProperty('id'))    { return suggestion.id; }
	}
	return '';
};

DX.Autocomplete.prototype.defaultGetSuggestionItemCallback = function(autocomplete, suggestion) {
	var exp = new RegExp('(' + RegExp.escape(this.searchValue) + ')', 'i');
	return this.getSuggestionValue(suggestion).replace(exp, "<b>$1</b>");
};

DX.Autocomplete.prototype.defaultGetSuggestionsHeadCallback = function(autocomplete) {
	var suggestions = this.getSuggestionsLength();
	return "<b>" + suggestions + "</b> suggestion" + (suggestions === 1 ? '' : 's');
};

DX.Autocomplete.prototype.defaultGetSuggestionsTailCallback = function(autocomplete) {
	return "&nbsp;";
};

DX.Autocomplete.prototype.hideSuggestions = function() {
	this.selectedIndex = -1;
	this.suggestionsElement.hide();
};

// Methods related to the suggestion properties =>

DX.Autocomplete.prototype.defaultFindSuggestionsCallback = function(response) {
	return DX.typeOf(response.responseJSON) === 'array' ? response.responseJSON : null;
};

DX.Autocomplete.prototype.getSuggestionsLength = function() {
	return (this.suggestions == null) ? 0 : this.suggestions.length;
};

DX.Autocomplete.prototype.selectSuggestion = function(index) {
	if (index == this.selectedIndex) { return; }
	var previousIndex  = this.selectedIndex;
	this.selectedIndex = index;
	this.render(this, true, previousIndex);
};

DX.Autocomplete.prototype.getSuggestion = function(index) {
	if (this.suggestions == null || index <= -1 || index >= this.suggestions.length) { return null; }
	return this.suggestions[index];
};

DX.Autocomplete.prototype.getSelectedSuggestion = function() {
	return this.getSuggestion(this.selectedIndex);
};

DX.Autocomplete.prototype.getSelectedSuggestionValue = function() {
	var selectedSuggestion = this.getSelectedSuggestion();
	return (selectedSuggestion == null) ? null : this.getSuggestionValue(selectedSuggestion);
};

DX.Autocomplete.prototype.useSelectedSuggestion = function() {
	var selectedSuggestion = this.getSelectedSuggestion();
	if (selectedSuggestion != null) { this.useSuggestion(this, selectedSuggestion); }
	this.hideSuggestions();
};

DX.Autocomplete.prototype.defaultUseSuggestionCallback = function(autocomplete, selectedSuggestion) {
	this.formFieldElement.value = this.getSuggestionValue(selectedSuggestion);
	this.formFieldElement.activate();
};

DX.Autocomplete.prototype.selectNextSuggestion = function() {
	var newIndex = this.selectedIndex + 1;
	if (newIndex >= this.getSuggestionsLength()) { newIndex = -1; }
	this.selectSuggestion(newIndex);
};

DX.Autocomplete.prototype.selectPreviousSuggestion = function() {
	var newIndex = this.selectedIndex - 1;
	if (newIndex < -1) { newIndex = this.getSuggestionsLength() - 1; }
	this.selectSuggestion(newIndex);
};

// Methods related to ui event handling =>

DX.Autocomplete.prototype.onElementKeyDown = function(e) {
	switch (e.keyCode) {
		case 13: // Enter
			if (this.selectedIndex !== -1) { e.stop(); return this.useSelectedSuggestion(); }
			break;
		case 27: e.stop(); return this.hideSuggestions();          // Escape
		case 38: e.stop(); return this.selectPreviousSuggestion(); // Up-arrow
		case 40: e.stop(); return this.selectNextSuggestion();     // Down-arrow
	}
	this.unhandledKeyDown(e, this);
};

DX.Autocomplete.prototype.onElementBlur = function(e) {
	if (this.hoveredIndex === -1) { this.hideSuggestions(); }
};

DX.Autocomplete.prototype.onSuggestionMouseOver = function(e, index) {
	if (index === this.hoveredIndex) { return; }
	this.hoveredIndex = index;
	this.selectSuggestion(index);
};

DX.Autocomplete.prototype.onSuggestionMouseOut = function(e, index) {
	this.hoveredIndex = -1;
};

DX.Autocomplete.prototype.onSuggestionClick = function(e, index) {
	this.useSelectedSuggestion();
};


/* Loop/Sortere Namespaces */
if (typeof Loop === 'undefined') { Loop = {}; }
if (typeof Loop.Sortere === 'undefined') { Loop.Sortere = {}; }
if (typeof Loop.Sortere.PluginTemplates === 'undefined') { Loop.Sortere.PluginTemplates = {}; }

Loop.Sortere.Plugin = function(element, optionsOverrides) {
	this.version     = '3.0';
	this.element     = $(element);
	this.kommune     = '';
	this.flashError  = false;
	this.queryError  = false;
	this.queryData   = null;
	this.breadcrumbs = [];
	this.searchFieldAutocompleter  = null;
	this.kommuneFieldAutocompleter = null;
	this.ignoreNextFragmentChange  = false;

	this.options = {
		debug:             DX.Fragment.hasFragment('debug'),
		beskrivelseLengde: 200,
		malform:           null,
		kommuneNr:         '',
		kommuneListe:      [],
		initialQuery:      {},
		showKommune:       true,
		showMinimal:       false,
		queryOnEnter:      true,
		redirectOnQuery:   false,
		startFocus:        false,
		startOpenOverlay:  false,
		showSearchTip:     true,
		useFlash:          true,
		useRelativePath:   true,

		flashPrefix:         'http://admin2.sortere.no/',
		urlPrefix:           'http://admin2.sortere.no/',
		kommuneInitUrl:      'http://admin2.sortere.no/service/kommuner/nummersok/#{nummer}',
		suggesterKommuneUrl: 'http://admin2.sortere.no/service/kart/sted/#{value}',
		suggesterAvfallUrl:  'http://admin2.sortere.no/service/sortere/forslag/#{value}',
		searchUrl:           'http://admin2.sortere.no/service/sortere/sok/fritekst/#{query}?kommune=#{kommune}&client_version=#{version}',
		entityUrl:           'http://admin2.sortere.no/service/sortere/sok/entitet/#{entity}/#{entityId}?kommune=#{kommune}&client_version=#{version}',
		queryUrl:            'http://sortere.no/#query=#{query}&kommune=#{kommune}',
		miljogiftImgUrl:     'http://lt.sortere.no/images/avfallstype_miljogift.png',
		unknownAtImgUrl:     'http://lt.sortere.no/images/avfallstype_ukjent.png',
		mapUrl:              'http://kart.sortere.no/#lat=#{lat}&lng=#{lng}&zoom=#{zoom}&kommune=#{kommune}&avfallstyper=#{atyperEnc}',

		overlayBgId:       'ls_overlay_background',
		searchFieldId:     'ls_search_field',
		kommuneFieldId:    'ls_kommune_field',
		buttonId:          'ls_search_button',
		suggesterId:       'ls_autosuggester',
		oddClass:          'ls_odd',
		evenClass:         'ls_even',
		farligClass:       'ls_farlig_avfall',
		loadingId:         'ls_loading',
		blockedNotifierId: 'ls_blocked_notifier',
		closeResultId:     'ls_close_result_handle',
		closeOverlayId:    'ls_close_overlay_handle',
		openOverlayIdExt:  null,
		tipsIdExt:         null,
		kontaktIdExt:      null,

		fastGjenvinningsstasjonstekst: null
	};
	Object.extend(this.options, optionsOverrides || {});

	this.templates = Loop.Sortere.PluginTemplates;

	// Overstyring ifm. minimal visning (kommunefelt vises ikke, så evt. kommune-data fjernes)
	if (this.options.showMinimal) {
		this.options.showKommune = false;
		this.options.kommuneListe = [];
		this.options.redirectOnQuery = true;
	};

	if (this.options.useFlash) {
		Ajax.flXHRproxy.registerOptions(this.options.flashPrefix, {
			autoUpdatePlayer: true,
			xmlResponseText: false,
			binaryResponseBody: false,
			instancePooling: true
		});
	}

	this.observeOpenOverlayId();
	this.render(this.options.startFocus);
	this.initFragments();
	this.initKommune();

	DX.Fragment.observe(this.fragmentChangeObserver.bind(this));
	DX.Fragment.start();

	Ajax.Responders.register({
		onCreate:    function()    { $(this.options.loadingId).show(); }.bind(this),
		onComplete:  function()    { $(this.options.loadingId).hide(); }.bind(this),
		onException: Function.prototype.defer.bind(this.onAjaxException.bind(this))
	});
};

/* Handles flXHR errors. We assume that flXHR is loaded if this function is called. */
Loop.Sortere.Plugin.prototype.onAjaxException = function(request, exception) {
	var t = request.transport;
	if (t && t.instanceId && t.instanceId.startsWith('flXHR')) {
		DX.log('An error happened in flXHR instance "' + t.instanceId + '": ' + exception.number + '/"' + exception.message + '". Logging to console:');
		if (exception.number === flensed.flXHR.PLAYER_VERSION_ERROR) {
			this.flashError = true;
			this.render(true);
			return;
		}
	}

	(function() { DX.logRaw(exception); throw exception; }).defer(); // Default handling of exceptions is to log and throw them in another thread, so they'll be visible outside Prototype's XHR thread.
};

Loop.Sortere.Plugin.prototype.getTemplate = function(templateName) {
	var postfix = this.options.malform === 'nynorsk' || this.options.malform === 'ny' ? 'Ny' : 'Nb';
	if (this.templates.hasOwnProperty(templateName + postfix)) { return this.templates[templateName + postfix]; }
	return this.templates[templateName];
};

Loop.Sortere.Plugin.prototype.observeOpenOverlayId = function() {
	if (!this.options.openOverlayIdExt || !$(this.options.openOverlayIdExt)) { return; }
	this.element.setStyle({
		display:  this.options.startOpenOverlay ? '' : 'none',
		position: 'absolute',
		top:      '0px',
		left:     '0px',
		width:    '100%',
		height:   '100%'
	});
	$(this.options.openOverlayIdExt).observe('click', Element.show.bind(null, this.element));
};

Loop.Sortere.Plugin.prototype.initFragments = function() {
	var fragments = DX.Fragment.getFragments();
	if (!fragments.hasOwnProperty('produkttype') && !fragments.hasOwnProperty('avfallstype') && !fragments.hasOwnProperty('miljogift') && !fragments.hasOwnProperty('query') && !fragments.hasOwnProperty('kommune')) {
		DX.Fragment.updateFragments(this.options.initialQuery);
	}
};

Loop.Sortere.Plugin.prototype.initKommune = function() {
	if (!this.options.kommuneNr) { return; }

	var url = this.options.kommuneInitUrl.supplant({ nummer: encodeURIComponent(this.options.kommuneNr) });
	if (this.options.debug) { DX.log('Requesting: ' + url); }
	new Ajax.Request(url, {
		method:    'get',
		onSuccess: Function.prototype.defer.bind(this.onInitKommuneSuccess.bind(this)),
		onFailure: Function.prototype.defer.bind(this.onInitKommuneFailure.bind(this))
	});
};

Loop.Sortere.Plugin.prototype.onInitKommuneSuccess = function(response) {
	if (this.options.debug) { DX.log('Kommune-init succeeded.'); }
	var t = response.request.transport;
	if (response.responseJSON == null && t.instanceId && t.instanceId.startsWith('flXHR') && response.responseText.isJSON()) { response.responseJSON = response.responseText.evalJSON(); } // Workaround for flash/flXHR requests not preserving the content type and thus not auto-evaluating a JSON response. Cannot use evalJSON: 'force' as that makes flXHR crash the browser if the response isn't valid JSON.
	if (response.responseJSON == null || response.responseJSON.result == null || response.responseJSON.result.length != 1) { return this.onInitKommuneFailure(response); }
	this.setKommune(response.responseJSON.result[0].navn);
	if (this.options.malform == null) { this.options.malform = response.responseJSON.result[0].malform; }
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};

Loop.Sortere.Plugin.prototype.onInitKommuneFailure = function(response) {
	if (this.options.debug) { DX.log('Kommune-init failed.'); }
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};

Loop.Sortere.Plugin.prototype.getKommune = function() {
	return this.options.showKommune ? this.getKommuneFieldValue() : this.kommune;
};

Loop.Sortere.Plugin.prototype.setKommune = function(kommune) {
	this.kommune = kommune;
	if (this.options.showKommune) { this.setKommuneFieldValue(kommune); }
};

Loop.Sortere.Plugin.prototype.getKommuneFieldValue = function() {
	if (this.options.kommuneListe.length > 0) {
		return $(this.options.kommuneFieldId).selectedIndex > 0 ? $(this.options.kommuneFieldId).options[$(this.options.kommuneFieldId).selectedIndex].text : '';
	}
	if (!this.options.showKommune) { return ''; }

	var value = $(this.options.kommuneFieldId).value;
	if (value === this.getTemplate('kommuneTip')) { return ''; }
	return value;
};

Loop.Sortere.Plugin.prototype.setKommuneFieldValue = function(value) {
	if (this.options.kommuneListe.length > 0) {
		for (var i=0; i<$(this.options.kommuneFieldId).options.length; i++) {
			if ($(this.options.kommuneFieldId).options[i].text == value) {
				$(this.options.kommuneFieldId).selectedIndex = i;
				break;
			}
		}
		return;
	}
	$(this.options.kommuneFieldId).value = value;
};

Loop.Sortere.Plugin.prototype.getSearchFieldValue = function() {
	var value = $(this.options.searchFieldId).value;
	if (value === this.getTemplate('searchTip')) { return ''; }
	return value;
};

Loop.Sortere.Plugin.prototype.setSearchFieldValue = function(value) {
	$(this.options.searchFieldId).value = value;
};

Loop.Sortere.Plugin.prototype.onSearchClick = function(e) {
	this.focusOnSearchField(e.element().id !== this.options.searchFieldId);
};

Loop.Sortere.Plugin.prototype.onKommuneClick = function(e) {
	e.stop(); // Prevents click from bubbling up to the search form, triggering a click event there too (which changed the focus to the search field again).
	this.focusOnKommuneField(e.element().id !== this.options.kommuneFieldId);
};

Loop.Sortere.Plugin.prototype.focusOnSearchField = function(activate) {
	if (!this.element.visible()) { return; }
	if (typeof activate === 'undefined') { activate = true; }
	if (activate) { $(this.options.searchFieldId).activate(); } else { $(this.options.searchFieldId).focus(); } // activate() markerer evt. tekst i tekstfelt
};

Loop.Sortere.Plugin.prototype.focusOnKommuneField = function(activate) {
	if (!this.options.showKommune) { return; }
	if (typeof activate === 'undefined') { activate = true; }
	if (activate) { $(this.options.kommuneFieldId).activate(); } else { $(this.options.kommuneFieldId).focus(); } // activate() markerer evt. tekst i tekstfelt
};

Loop.Sortere.Plugin.prototype.unhandledKeyDown = function(e, autocomplete) {
	if (e.keyCode === 13 && this.options.queryOnEnter) { e.stop(); this.registerQuery(); } // Enter
};

Loop.Sortere.Plugin.prototype.registerQuery = function() {
	if (this.options.redirectOnQuery) { window.top.location.href = this.getLinkHref(); return; }

	var fragments = { query: this.getSearchFieldValue(), kommune: this.getKommune() };
	if (!fragments.query)   { delete fragments.query; }
	if (!fragments.kommune) { delete fragments.kommune; }
	DX.Fragment.setFragments(fragments);
};

/* 2010-04-21: Removed in favour of the simpler redirect behaviour in the registerQuery method, by request from LOOP.
Loop.Sortere.Plugin.prototype.doMinimalQuery = function() {
	var url = this.setLinkHref(); // Useful if the popup window is bloked.
	var sortereWindow = window.open(url, 'sortereWindow');
	if (!sortereWindow || !sortereWindow.open) {
		var el = $(this.options.blockedNotifierId);
		el.show();
		el.clonePosition($(this.options.buttonId), {
			setWidth: false,
			setHeight: false,
			offsetTop: $(this.options.buttonId).getHeight(),
			offsetLeft: -154 + $(this.options.buttonId).getWidth()
		});
	}
};
*/

Loop.Sortere.Plugin.prototype.getLinkHref = function() {
	return this.options.queryUrl.supplant({ query: encodeURIComponent(this.getSearchFieldValue()), kommune: encodeURIComponent(this.getKommune()) });
};

Loop.Sortere.Plugin.prototype.setLinkHref = function() {
	return $(this.options.buttonId).href = this.getLinkHref();
};

Loop.Sortere.Plugin.prototype.setFragmentsIgnoreChange = function(fragments) {
	this.ignoreNextFragmentChange = true;
	DX.Fragment.setFragments(fragments);
	(function() { this.ignoreNextFragmentChange = false; }).bind(this).delay(0.1);
};

Loop.Sortere.Plugin.prototype.fragmentChangeObserver = function(fragments) {
	if (this.ignoreNextFragmentChange) { this.ignoreNextFragmentChange = false; return; }
	if (this.options.showKommune) { this.setKommune(fragments.kommune ? fragments.kommune : ''); } // Pay attention to the kommune fragment only if the user is supposed to be able to change this.
	this.setSearchFieldValue(fragments.query ? fragments.query : (this.options.showSearchTip ? this.getTemplate('searchTip') : ''));

	if (fragments.produkttype) { // We are showing a specific produkttype
		this.requestEntity('produkttype', fragments.produkttype);
	} else if (fragments.avfallstype) { // We are showing a specific avfallstype
		this.requestEntity('avfallstype', fragments.avfallstype);
	} else if (fragments.miljogift) { // We are showing a specific miljogift
		this.requestEntity('miljogift', fragments.miljogift);
	} else if (fragments.query || fragments.kommune) { // We are doing a search
		this.sendQuery();
	}
};

Loop.Sortere.Plugin.prototype.requestEntity = function(entity, id) {
	this.ignoreActiveAutocompletionRequests();
	var url = this.options.entityUrl.supplant({ entity: encodeURIComponent(entity), entityId: encodeURIComponent(id), kommune: encodeURIComponent(this.getKommune()), version: encodeURIComponent(this.version) }) + '&origin=' + encodeURIComponent(window.location.hostname);
	if (this.options.debug) { DX.log('Requesting: ' + url); }
	new Ajax.Request(url, {
		method:    'get',
		onSuccess: Function.prototype.defer.bind(this.onQuerySuccess.bind(this)),
		onFailure: Function.prototype.defer.bind(this.onQueryFailure.bind(this))
	});
};

Loop.Sortere.Plugin.prototype.sendQuery = function() {
	this.ignoreActiveAutocompletionRequests();
	var url = this.options.searchUrl.supplant({ query: encodeURIComponent(this.getSearchFieldValue()), kommune: encodeURIComponent(this.getKommune()), version: encodeURIComponent(this.version) }) + '&origin=' + encodeURIComponent(window.location.hostname);
	if (this.options.debug) { DX.log('Requesting: ' + url); }
	new Ajax.Request(url, {
		method:    'get',
		onSuccess: Function.prototype.defer.bind(this.onQuerySuccess.bind(this)),
		onFailure: Function.prototype.defer.bind(this.onQueryFailure.bind(this))
	});
	this.flashError = false;
	this.queryError = false;
};

Loop.Sortere.Plugin.prototype.ignoreActiveAutocompletionRequests = function() {
	this.searchFieldAutocompleter.ignoreActiveRequests();
	this.kommuneFieldAutocompleter.ignoreActiveRequests();
};

Loop.Sortere.Plugin.prototype.onQuerySuccess = function(response) {
	if (this.options.debug) { DX.log('Query succeeded.'); }
	var t = response.request.transport;
	if (response.responseJSON == null && t.instanceId && t.instanceId.startsWith('flXHR') && response.responseText.isJSON()) { response.responseJSON = response.responseText.evalJSON(); } // Workaround for flash/flXHR requests not preserving the content type and thus not auto-evaluating a JSON response. Cannot use evalJSON: 'force' as that makes flXHR crash the browser if the response isn't valid JSON.
	if (response.responseJSON == null) { return this.onQueryFailure(response); }
	this.queryData = response.responseJSON;
	this.render(true);
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};

Loop.Sortere.Plugin.prototype.onQueryFailure = function(response) {
	if (this.options.debug) { DX.log('Query failed.'); }
	this.queryError = true;
	this.queryData = null;
	this.render(true);
	if (typeof response.transport.Destroy === 'function') { response.transport.Destroy(); } // For when flXHR is used.
};


Loop.Sortere.Plugin.prototype.getLocalizedValue = function(object, property) {
	var valueNb = object[property + 'Nb'];
	var valueNy = object[property + 'Ny'];
	var value = (valueNb == null || valueNb === '' || ((this.options.malform === 'nynorsk' || this.options.malform === 'ny') && valueNy != null && valueNy != '')) ? valueNy : valueNb;
	return value == null ? '' : value;
};

Loop.Sortere.Plugin.prototype.getAvfallstypeHandteringFromTips = function(object) {
	var handtering = '', handteringer = (object.tips || {}).handtering || [];
	for (var i = 0; i < handteringer.length; i++) {
		handtering += '<p>' + handteringer[i].tekst + '</p>';
	}
	return handtering;
};

Loop.Sortere.Plugin.prototype.getKommuneKontaktinfoHtml = function(kommune) {
	kommune.hjemmeside = kommune.hjemmeside || this.getExternalLinkHtml(kommune.hjemmesideUrl);
	var data = {};
	data.overskrift     = kommune.kontaktinfoOverskrift;
	data.kommuneAdresse = this.getTemplate('kommuneAdresse').supplant(kommune, true);
	data.kommuneTelefon = this.getTemplate('kommuneTelefon').supplant(kommune, true);
	data.kommuneEpost   = this.getTemplate('kommuneEpost').supplant(kommune, true);
	data.kommuneWeb     = this.getTemplate('kommuneWeb').supplant(kommune, true);
	return this.getTemplate('kontaktinfo').supplant(data, true);
};

Loop.Sortere.Plugin.prototype.getTipsBoksHtml = function(object, kommune) {
	var t = this.getTips(object, kommune || {});
	if (!t || !t.obs || !t.visste || !t.hva || !t.lenke) { return ''; }
	if (t.obs.length + t.visste.length + t.hva.length + t.lenke.length === 0) { return ''; }

	var tips = {};
	tips.obsTips         = this.getTipsHtml(t.obs,    this.getTemplate('obsTipsHeader'));
	tips.vissteTips      = this.getTipsHtml(t.visste, this.getTemplate('vissteTipsHeader'));
	tips.hvaTips         = this.getTipsHtml(t.hva,    this.getTemplate('hvaTipsHeader'));
	tips.lenkeTips       = this.getTipsHtml(t.lenke,  this.getTemplate('lenkeTipsHeader'));
	return this.getTemplate('tipsBoks').supplant(tips);
};

Loop.Sortere.Plugin.prototype.getTips = function(object, kommune) {
	var tips = { obs: [], visste: [], hva: [], lenke: [] };
	for (type in tips) {
		var t1 = object.tips || {}, t2 = kommune.tips || {};
		tips[type] = tips[type].concat(t1[type] || [], t2[type] || []);
	}
	return tips;
};

Loop.Sortere.Plugin.prototype.getTipsHtml = function(tips, header) {
	return this.getTemplate('tips').supplant({ tips: this.getTipsListHtml(tips, header) });
};

Loop.Sortere.Plugin.prototype.getTipsListHtml = function(tips, header) {
	if (tips.length <= 0) { return ''; }
	var num = tips.length <= 3 ? tips.length : 3;
	var resultHtml = header.supplant({ teller: num < tips.length ? this.getTemplate('tipsTeller').supplant({ num: num, maks: tips.length }) : '' });
	if (tips.length > 3) {
		tips = this.randomizeArray(tips);
	}
	for (var i = 0; i < num; i++) {
		resultHtml += this.getTemplate(tips[i].nasjonal ? 'tipNasjonalt' : 'tipKommunalt').supplant({ tekst: tips[i].tekst });
	}
	return resultHtml;
};

Loop.Sortere.Plugin.prototype.randomizeArray = function(array) {
	for (var i=0; i<array.length; i++) {
		var swapIndex = Math.floor(((array.length - i)* Math.random())) + i;
		var temp = array[i];
		array[i] = array[swapIndex];
		array[swapIndex] = temp;
	}
	return array;
};

Loop.Sortere.Plugin.prototype.getAvfallstyper = function(sorteringer, kommune) {
	if (!sorteringer || !sorteringer.length || sorteringer.length <= 0) { return '???'; }
	var avfallstyper = [];
	for (var i = 0; i < sorteringer.length; i++) {
		var type = sorteringer[i];
		type.farligClass = (type.farlig ? this.options.farligClass : '');
		var html = "<a class='" + type.farligClass + "' href='" + this.createLink('avfallstype', type.id, kommune) + "'>" + type.navn + "</a>";
		avfallstyper.push(html);
	}
	return avfallstyper.join(' eller ');
};

Loop.Sortere.Plugin.prototype.getAvfallstyperHtml = function(avfallstyper, kommune, mapOptions) {
	if (!avfallstyper) { return ''; }
	mapOptions.avfallstyper = avfallstyper.pluck('id').join(',');

	var resultHtml = '';
	for (var i = 0; i < avfallstyper.length; i++) {
		var avfallstype = avfallstyper[i];
		avfallstype.bilde         = this.getImageHtml(avfallstype, 'avfallstype');
		avfallstype.lenke         = this.createLink('avfallstype', avfallstype.id, kommune);
		avfallstype.ordningerHtml = this.getOrdningerHtml(avfallstype.ordninger, mapOptions);
		resultHtml += this.getTemplate('atInProduktype').supplant(avfallstype);
	}
	return resultHtml;
};

Loop.Sortere.Plugin.prototype.getImageHtml = function(object, datatype) {
	if (datatype === 'produkttype') {
		object.farlig = '';
		for (var i = 0; object.sorteringer && i < object.sorteringer.length; i++) {
			if (object.sorteringer[i].farlig === 1) { object.farlig = ' ' + this.options.farligClass; }
		}
		if (object.bildeUrl == null) { object.bildeUrl = ''; }
		else if (!object.bildeUrl.startsWith('http')) { object.bildeUrl = this.options.urlPrefix + (object.bildeUrl.startsWith('/') ? object.bildeUrl.substring(1) : object.bildeUrl); }
		return this.getTemplate('ptImage').supplant(object);
	}
	else if (datatype === 'miljogift') {
		object.bildeUrl = this.options.miljogiftImgUrl;
		return this.getTemplate('mgImage').supplant(object);
	}
	else if (datatype === 'avfallstype') {
		if (object.bildeUrl == null || object.bildeUrl === '') { object.bildeUrl = this.options.unknownAtImgUrl; }
		else if (!object.bildeUrl.startsWith('http')) { object.bildeUrl = this.options.urlPrefix + (object.bildeUrl.startsWith('/') ? object.bildeUrl.substring(1) : object.bildeUrl); }
		return this.getTemplate('atImage').supplant(object);
	}
	return '';
};

Loop.Sortere.Plugin.prototype.getOrdningerHtml = function(ordninger, mapOptions) {
	if (!ordninger || ordninger.henteordninger.length + ordninger.kompostordninger.length + ordninger.punktordninger.length + ordninger.hyttepunktordninger.length + ordninger.batpunktordninger.length + ordninger.gjenvinningsstasjoner.length <= 0) { return ''; }
	ordninger.henteHtml = ordninger.kompostHtml = ordninger.punktHtml = ordninger.gjenvinningHtml = '';
	for (var i=0; i<ordninger.henteordninger.length;        i++) { ordninger.henteHtml       += this.getOrdningHtml(this.getTemplate('henteordning'),        ordninger.henteordninger[i],        mapOptions); }
	for (var j=0; j<ordninger.kompostordninger.length;      j++) { ordninger.kompostHtml     += this.getOrdningHtml(this.getTemplate('kompostordning'),      ordninger.kompostordninger[j],      mapOptions); }
	for (var k=0; k<ordninger.punktordninger.length;        k++) { ordninger.punktHtml       += this.getOrdningHtml(this.getTemplate('punktordning'),        ordninger.punktordninger[k],        mapOptions); }
	for (var l=0; l<ordninger.hyttepunktordninger.length;   l++) { ordninger.punktHtml       += this.getOrdningHtml(this.getTemplate('punktordning'),        ordninger.hyttepunktordninger[l],   mapOptions); }
	for (var m=0; m<ordninger.batpunktordninger.length;     m++) { ordninger.punktHtml       += this.getOrdningHtml(this.getTemplate('punktordning'),        ordninger.batpunktordninger[m],     mapOptions); }
	for (var n=0; n<ordninger.gjenvinningsstasjoner.length; n++) { ordninger.gjenvinningHtml += this.getOrdningHtml(this.getTemplate('gjenvinningsstasjon'), ordninger.gjenvinningsstasjoner[n], mapOptions); }
	if (ordninger.gjenvinningsstasjoner.length > 0 && this.options.fastGjenvinningsstasjonstekst) { ordninger.gjenvinningHtml = this.options.fastGjenvinningsstasjonstekst; }
	return this.getTemplate('ordninger').supplant(ordninger);
};

Loop.Sortere.Plugin.prototype.getOrdningHtml = function(template, ordning, mapOptions) {
	ordning.avfallstyper = (ordning.avfallstyper || []).join(',');
	ordning.hjemmeside   = this.getExternalLinkHtml(ordning.hjemmesideUrl);
	ordning.kartlenke    = this.getSortereKartlenke(ordning, mapOptions);
	return template.supplant(ordning, true);
};

Loop.Sortere.Plugin.prototype.getSortereKartlenke = function(ordning, mapOptions) {
	if (!mapOptions.kommune) { return ''; } // Har ikke valgt sted (kommune).
	mapOptions.lat  = encodeURIComponent(mapOptions.kommuneLat) || 'default';
	mapOptions.lng  = encodeURIComponent(mapOptions.kommuneLng) || 'default';
	mapOptions.zoom = 'default';

	if (ordning.type === 'punktordning' || ordning.type === 'hyttepunktordning' || ordning.type === 'batpunktordning') {
		// Lar områdets/kommunens koordinater stå for punktordninger
		mapOptions.atyperEnc = encodeURIComponent(mapOptions.avfallstyper || ordning.avfallstyper);
		mapOptions.zoom      = 12;
	} else if (ordning.type === 'gjenvinningsstasjon') {
		// Bruker gjenvinningsstasjonens egne koordinater, dersom de er oppgitt
		if (ordning.lat !== null && ordning.lng !== null && ordning.lat != 0.0 && ordning.lng != 0.0) {
			mapOptions.lat  = encodeURIComponent(ordning.lat);
			mapOptions.lng  = encodeURIComponent(ordning.lng);
			mapOptions.zoom = 14;
		}
		mapOptions.atyperEnc = encodeURIComponent(mapOptions.avfallstyper || 'default');
	} else {
		return '';
	}
	return this.getTemplate('kartlenke').supplant({ url: this.options.mapUrl.supplant(mapOptions) });
};

Loop.Sortere.Plugin.prototype.getExternalLinkHtml = function(url, description, target) {
	if (!url) { return description; }
	if (!url.startsWith('http')) { url = 'http://' + url; }
	if (!description) { description = url; }
	if (!target) { target = '_blank'; } // Åpner ny tab som default
	return "<a href='" + url + "' target='" + target + "'>" + description + "</a>";
};

Loop.Sortere.Plugin.prototype.renderMinimal = function(focusOnSearchField) {
	var data = {};
	Object.extend(data, this.options);

	data.form          = this.getTemplate('minimalForm').supplant(data, true);
	data.mainHeadHtml  = this.getTemplate('mainHead').supplant(data);
	data.mainBodyHtml  = this.getTemplate('mainBody').supplant(data, true);
	data.mainTailHtml  = this.getTemplate('mainTail').supplant(data);
	data.suggesterHtml = this.getTemplate('suggester').supplant(data);

	this.element.innerHTML = this.getTemplate('master').supplant(data);
	//$(this.options.buttonId).observe('click', Element.hide.bind(null, $(this.options.blockedNotifierId))); // Commented out due to the changes in doMinimalQuery.

	$(this.options.searchFieldId + '_wrapper').observe('click', this.onSearchClick.bindAsEventListener(this));
	$(this.options.searchFieldId).observe('blur', function (e) { this.setLinkHref(); }.bindAsEventListener(this));
	this.createSearchFieldAutocompleter();

	if (focusOnSearchField) { this.focusOnSearchField.bind(this).delay(0.25); }

	if (this.options.showSearchTip) {
		var searchTip = this.getTemplate('searchTip');
		$(this.options.searchFieldId).value = $(this.options.searchFieldId).value || searchTip;
		$(this.options.searchFieldId).observe('focus', function() { if($(this.options.searchFieldId).value === searchTip) { $(this.options.searchFieldId).value = ''; } }.bind(this));
		$(this.options.searchFieldId).observe('blur',  function() { if($(this.options.searchFieldId).value === '') { $(this.options.searchFieldId).value = searchTip; } }.bind(this));
	}
};

Loop.Sortere.Plugin.prototype.render = function(focusOnSearchField) {
	if (this.options.showMinimal) { return this.renderMinimal(focusOnSearchField); }

	var data = {};
	Object.extend(data, this.options); // TODO - Sjekke hvilke options som faktisk brukes, kanskje skille disse ut (hvis det bare er css-klassenavnene som brukes)?

	if (this.queryData && this.queryData.error) { data.results = this.getTemplate('errorMsg').supplant(this.queryData.error); }
	else if (this.queryData && this.queryData.result) {
		var result = this.queryData.result;
		if (this.options.malform == null) { this.options.malform = result.kommune.malform; }
		data.kommune    = result.kommune.navn;
		var antallTreff = result.resultattype === 'resultatliste' ? result.antallTreff : 1;

		var crumbFragments = {};
		if (result.soketekst)                                { crumbFragments.query   = result.soketekst; }
		if (this.options.showKommune && result.kommune.navn) { crumbFragments.kommune = result.kommune.navn; }

		var mapOptions  = { kommune: encodeURIComponent(result.kommune.navn), kommuneLat: result.kommune.lat, kommuneLng: result.kommune.lng };
		var kontaktHtml = result.kommune.navn ? this.getKommuneKontaktinfoHtml(result.kommune) : '';
		var tipsHtml    = '';

		var objData = null;
		if (result.resultattype === 'resultatliste') {
			var produkttypeHtml = '', miljogiftHtml = '', avfallstypeHtml = '';
			if (result.produkttyper.length > 0) {
				produkttypeHtml = this.getTemplate('preProdukttype').supplant({ treff: result.produkttyper.length, totaltAntall: antallTreff });
				for (var i = 0; i < result.produkttyper.length; i++) {
					objData              = result.produkttyper[i];
					objData.handtering   = (objData.handtering || '').stripTags().truncate(this.options.beskrivelseLengde);
					objData.sokeord      = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
					objData.avfallstyper = this.getAvfallstyper(objData.sorteringer, result.kommune.navn);
					objData.bilde        = this.getImageHtml(objData, 'produkttype');
					objData.lenke        = this.createLink('produkttype', objData.id, result.kommune.navn);
					objData.oddeven      = i % 2 ? this.options.evenClass : this.options.oddClass;
					produkttypeHtml     += this.getTemplate('produkttype').supplant(objData);
				}
				produkttypeHtml += this.getTemplate('postProdukttype').supplant({ treff: result.produkttyper.length, totaltAntall: antallTreff });
			}

			if (result.miljogifter.length > 0) {
				miljogiftHtml = this.getTemplate('preMiljogift').supplant({ treff: result.miljogifter.length, totaltAntall: antallTreff });
				for (var j = 0; j < result.miljogifter.length; j++) {
					objData             = result.miljogifter[j];
					objData.beskrivelse = (objData.beskrivelse || '').stripTags().truncate(this.options.beskrivelseLengde);
					objData.sokeord     = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
					objData.bilde       = this.getImageHtml(objData, 'miljogift');
					objData.lenke       = this.createLink('miljogift', objData.id, result.kommune.navn);
					objData.oddeven     = j % 2 ? this.options.evenClass : this.options.oddClass;
					miljogiftHtml      += this.getTemplate('miljogift').supplant(objData);
				}
				miljogiftHtml += this.getTemplate('postMiljogift').supplant({ treff: result.miljogifter.length, totaltAntall: antallTreff });
			}

			if (result.avfallstyper.length > 0) {
				avfallstypeHtml = this.getTemplate('preAvfallstype').supplant({ treff: result.avfallstyper.length, totaltAntall: antallTreff, atClass: this.options.atClass });
				for (var k = 0; k < result.avfallstyper.length; k++) {
					objData             = result.avfallstyper[k];
					objData.beskrivelse = (objData.beskrivelse || '').stripTags().truncate(this.options.beskrivelseLengde);
					objData.sokeord     = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
					objData.bilde       = this.getImageHtml(objData, 'avfallstype');
					objData.lenke       = this.createLink('avfallstype', objData.id, result.kommune.navn);
					objData.oddeven     = k % 2 ? this.options.evenClass : this.options.oddClass;
					avfallstypeHtml    += this.getTemplate('avfallstype').supplant(objData);
				}
				avfallstypeHtml += this.getTemplate('postAvfallstype').supplant({ treff: result.avfallstyper.length, totaltAntall: antallTreff });
			}

			data.query   = result.soketekst;
			data.results = this.getTemplate('resultList').supplant({ 'produkttypeHtml': produkttypeHtml, 'miljogiftHtml': miljogiftHtml, 'avfallstypeHtml': avfallstypeHtml });
		}

		else if (result.resultattype === 'produkttype') {
			objData                  = result.produkttype;
			objData.sokeord          = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
			objData.avfallstyper     = this.getAvfallstyper(objData.sorteringer, result.kommune.navn);
			objData.avfallstyperHtml = this.getAvfallstyperHtml(objData.sorteringer, result.kommune.navn, mapOptions);
			objData.bilde            = this.getImageHtml(objData, result.resultattype);
			tipsHtml                 = this.getTipsBoksHtml(objData, result.kommune);
			objData.tipsHtml         = this.options.tipsIdExt ? '' : tipsHtml;
			data.results             = this.getTemplate('resultProdukttype').supplant(objData, true);

			data.query = objData.navn;
			crumbFragments.produkttype = objData.id;
		}
		else if (result.resultattype === 'miljogift') {
			objData          = result.miljogift;
			objData.sokeord  = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
			objData.bilde    = this.getImageHtml(objData, result.resultattype);
			tipsHtml         = this.getTipsBoksHtml(objData, result.kommune);
			objData.tipsHtml = this.options.tipsIdExt ? '' : tipsHtml;
			data.results     = this.getTemplate('resultMiljogift').supplant(objData, true);

			data.query = objData.navn;
			crumbFragments.miljogift = objData.id;
		}
		else if (result.resultattype === 'avfallstype') {
			objData                 = result.avfallstype;
			objData.handtering      = this.getAvfallstypeHandteringFromTips(objData);
			objData.sokeord         = objData.sokeord ? this.getTemplate('sokeord').supplant(objData) : '';
			objData.bilde           = this.getImageHtml(objData, result.resultattype);
			tipsHtml                = this.getTipsBoksHtml(objData, result.kommune);
			objData.tipsHtml        = this.options.tipsIdExt ? '' : tipsHtml;
			mapOptions.avfallstyper = objData.id;
			objData.ordningerHtml   = this.getOrdningerHtml(objData.ordninger, mapOptions);
			data.results            = this.getTemplate('resultAvfallstype').supplant(objData, true);

			data.query = objData.navn;
			crumbFragments.avfallstype = objData.id;
		}
		else if (result.resultattype === 'kommune') {
			objData               = result.kommune;
			objData.ordningerHtml = this.getOrdningerHtml(objData.ordninger, mapOptions);
			objData.kontaktHtml   = this.options.kontaktIdExt ? '' : kontaktHtml;
			tipsHtml              = this.getTipsBoksHtml(objData);
			objData.tipsHtml      = this.options.tipsIdExt ? '' : tipsHtml;
			data.results          = this.getTemplate('resultKommune').supplant(objData, true);
		}
		else {
			data.results = this.getTemplate('failure').supplant(data);
		}

		if (Object.keys(crumbFragments).length > 0) {
			this.addBreadcrumb(crumbFragments, data.query || data.kommune, antallTreff);
			this.setFragmentsIgnoreChange(crumbFragments);
		}
		if (this.breadcrumbs.length > 0) {
			var breadcrumbsHtml = '';
			for (var l=0; l<this.breadcrumbs.length; l++) {
				breadcrumbsHtml += (l === 0 ? this.getTemplate('breadcrumbFirst') : this.getTemplate('breadcrumb')).supplant(this.breadcrumbs[l]);
			}
			data.breadcrumbsHtml = this.getTemplate('breadcrumbs').supplant({ breadcrumbs: breadcrumbsHtml });
		}

		var queryUrl   = data.query ? this.options.queryUrl.supplant({ query: encodeURIComponent(data.query), kommune: encodeURIComponent(result.kommune.navn) }) : '';
		data.sokelenke = data.query ? this.getTemplate('sokelenke').supplant({ url: queryUrl }) : '';
	}

	if (this.flashError) { data.results = this.getTemplate('flashError').supplant(data); }
	if (this.queryError) { data.results = this.getTemplate('failure').supplant(data); }
	if (this.queryData || this.flashError || this.queryError) {
		data.result  = this.getTemplate('resultHead').supplant(data, true);
		data.result += this.getTemplate('resultBody').supplant(data);
		data.result += this.getTemplate('resultTail').supplant(data, true);
	}

	data.kontaktHtml    = this.options.kontaktIdExt ? '' : kontaktHtml;
	data.tipsHtml       = this.options.tipsIdExt    ? '' : tipsHtml;
	data.kommuneOptions = this.options.kommuneListe.length > 0 ? this.generateKommuneOptions() : '';
	data.kommuneField   = this.options.showKommune ? (this.getTemplate(this.options.kommuneListe.length > 0 ? 'kommuneDropDown' : 'kommuneTextField').supplant(data, true)) : '';
	data.form           = this.getTemplate('form').supplant(data, true);
	data.mainHeadHtml   = this.getTemplate('mainHead').supplant(data);
	data.mainBodyHtml   = this.getTemplate('mainBody').supplant(data, true);
	data.mainTailHtml   = this.getTemplate('mainTail').supplant(data);
	data.suggesterHtml  = this.getTemplate('suggester').supplant(data);

	this.element.innerHTML = this.getTemplate('master').supplant(data);
	$(this.options.overlayBgId).setOpacity(0.5);
	if ($(this.options.kontaktIdExt)) { $(this.options.kontaktIdExt).update(kontaktHtml); }
	if ($(this.options.tipsIdExt)) { $(this.options.tipsIdExt).update(tipsHtml); }
	$(this.options.buttonId).observe('click', this.registerQuery.bind(this));

	$(this.options.searchFieldId + '_wrapper').observe('click', this.onSearchClick.bindAsEventListener(this));
	this.createSearchFieldAutocompleter();

	if (this.options.showKommune) {
		$(this.options.kommuneFieldId + '_wrapper').observe('click', this.onKommuneClick.bindAsEventListener(this));
		this.createKommuneFieldAutocompleter();
	}

	if (focusOnSearchField) { this.focusOnSearchField.bind(this).delay(0.25); }

	if (this.options.showSearchTip) {
		var searchTip = this.getTemplate('searchTip');
		$(this.options.searchFieldId).value = $(this.options.searchFieldId).value || searchTip;
		$(this.options.searchFieldId).observe('focus', function() { if($(this.options.searchFieldId).value === searchTip) { $(this.options.searchFieldId).value = ''; } }.bind(this));
		$(this.options.searchFieldId).observe('blur',  function() { if($(this.options.searchFieldId).value === '') { $(this.options.searchFieldId).value = searchTip; } }.bind(this));
	}
	if (this.options.showSearchTip && this.options.showKommune && this.options.kommuneListe.length === 0) {
		var kommuneTip = this.getTemplate('kommuneTip');
		$(this.options.kommuneFieldId).value = $(this.options.kommuneFieldId).value || kommuneTip;
		$(this.options.kommuneFieldId).observe('focus', function() { if($(this.options.kommuneFieldId).value === kommuneTip) { $(this.options.kommuneFieldId).value = ''; } }.bind(this));
		$(this.options.kommuneFieldId).observe('blur',  function() { if($(this.options.kommuneFieldId).value === '') { $(this.options.kommuneFieldId).value = kommuneTip; } }.bind(this));
	}

	var closeResultHandle = $(this.options.closeResultId), closeOverlayHandle = $(this.options.closeOverlayId);
	if (closeResultHandle)  { closeResultHandle.observe('click',  this.clearResult.bind(this)); }
	if (closeOverlayHandle) { closeOverlayHandle.observe('click', Element.hide.bind(null, this.element)); }

	// Attempt to fix broken <img> tag 'src' attributes (i.e. those with relative urls intended only to be viewed inside the CMS at sortere.no)
	$$('.ls_plugin img[src]').each(function(img) { if (!img.getAttribute('src').startsWith('http')) { img.src = this.options.urlPrefix + img.getAttribute('src'); } }.bind(this));
};

Loop.Sortere.Plugin.prototype.clearResult = function() {
	this.queryData   = null;
	this.flashError  = false;
	this.queryError  = false;
	this.breadcrumbs = [];
	this.render(true);
};

Loop.Sortere.Plugin.prototype.createLink = function(datatype, id, kommune) {
	var fragments = {};
	if (kommune) { fragments['kommune'] = kommune; }
	fragments[datatype] = id;

	return this.absolutizeLink('#' + DX.Fragment.encodeFragments(fragments, true));
};

Loop.Sortere.Plugin.prototype.absolutizeLink = function(link) {
	if (this.options.useRelativePath) { return link; }
	return window.location.protocol + '//' + window.location.hostname + window.location.pathname + window.location.search + link;
};

Loop.Sortere.Plugin.prototype.generateKommuneOptions = function() {
	var result = '';
	for (var i=0; i<this.options.kommuneListe.length; i++) {
		var kommune = this.options.kommuneListe[i];
		result += this.getTemplate('htmlOption').supplant({ text: kommune, selected: (this.kommune == kommune) ? " selected='selected'" : '' });
	}
	return result;
};

Loop.Sortere.Plugin.prototype.addBreadcrumb = function(fragments, tekst, antallTreff) {
	if (fragments.query) { this.breadcrumbs = []; }

	var breadcrumb = {
		lenke: this.absolutizeLink('#' + DX.Fragment.encodeFragments(fragments, true)),
		tekst: tekst,
		antallTreff: antallTreff
	};

	var newCrumbs = [];
	for (var i=0; i<this.breadcrumbs.length; i++) {
		newCrumbs[i] = this.breadcrumbs[i];
		if (this.breadcrumbs[i].lenke === breadcrumb.lenke) {
			this.breadcrumbs = newCrumbs;
			return;
		}
	}
	this.breadcrumbs[this.breadcrumbs.length] = breadcrumb;
};

Loop.Sortere.Plugin.prototype.createSearchFieldAutocompleter = function() {
	return this.searchFieldAutocompleter = new DX.Autocomplete(this.options.searchFieldId, this.options.suggesterId, this.options.suggesterAvfallUrl, {
		positionRelativeTo: this.options.searchFieldId + '_wrapper',
		findSuggestionsCallback: function(response) { return response.responseJSON.result; },
		getSuggestionValueCallback: function(suggestion) { return this.getLocalizedValue(suggestion, 'text'); }.bind(this),
		getSuggestionsHeadCallback: function(autocomplete) { return this.getTemplate('suggestHead').supplant({ treff: autocomplete.getSuggestionsLength() }); }.bind(this),
		unhandledKeyDownCallback: this.unhandledKeyDown.bind(this),

		templateHead: '#{content}',
		templateTail: this.getTemplate('suggestTail'),
		templateItem: this.getTemplate('suggestion'),
		debug: this.options.debug
	});
};

Loop.Sortere.Plugin.prototype.createKommuneFieldAutocompleter = function() {
	var url = this.options.suggesterKommuneUrl;
	return this.kommuneFieldAutocompleter = new DX.Autocomplete(this.options.kommuneFieldId, this.options.suggesterId, url, {
		positionRelativeTo: this.options.kommuneFieldId + '_wrapper',
		findSuggestionsCallback:   this.findLocationSuggestions.bind(this),
		getSuggestionValueCallback: function(suggestion) { return suggestion.kommune || suggestion.navn; },
		getSuggestionItemCallback:  this.getLocationSuggestionItem.bind(this),
		getSuggestionsHeadCallback: function(autocomplete) { return this.getTemplate('suggestHead').supplant({ treff: autocomplete.getSuggestionsLength() }); }.bind(this),
		unhandledKeyDownCallback: this.unhandledKeyDown.bind(this),

		templateHead: '#{content}',
		templateTail: this.getTemplate('suggestTail'),
		templateItem: this.getTemplate('suggestion'),
		debug: this.options.debug
	});
};


Loop.Sortere.Plugin.prototype.findLocationSuggestions = function(response) {
	var data = response.responseJSON.result;
	(data.kommune || []).each(function (result) { result.type = 'K'; });
	(data.postnr  || []).each(function (result) { result.type = 'P'; });
	(data.adresse || []).each(function (result) { result.type = 'A'; });
	return (data.kommune || []).concat(data.postnr || []).concat(data.adresse || []);
};


Loop.Sortere.Plugin.prototype.getLocationSuggestionItem = function(autocomplete, suggestion) {
	var exp = new RegExp('(' + RegExp.escape(autocomplete.searchValue) + ')', 'i');
	suggestion.boldedName = suggestion.navn.replace(exp, "<b>$1</b>");
	if (suggestion.type === 'P') { suggestion.boldedNumber = suggestion.nummer.replace(exp, "<b>$1</b>"); }
	return this.templates['suggestionLoc' + suggestion.type].supplant(suggestion);
};
