Skip to content

Instantly share code, notes, and snippets.

@janjongboom
Created October 4, 2011 09:43
Show Gist options
  • Save janjongboom/1261250 to your computer and use it in GitHub Desktop.
Save janjongboom/1261250 to your computer and use it in GitHub Desktop.
Cloud9 test
// hello this is a test
/// <reference path="jquery-1.4.1.js" />
/*
* jQuery plugin: zoekbox - v1.0.0
* (c) 2010-2011 funda real estate <jan@funda.nl> - http://www.funda.nl
*
* This is the core library for EVERYTHING that uses the zoekbox;
* either geo, nieuwbouw-projects, or even the names of all funda employees
* therefore NEVER EVER add anything that is implementation-specific to this code.
*
* BEFORE SUBMITTING:
* Always validate this file against JsLint, only the
* -Missing '{' before 'something'- validation errors are acceptable.
*/
(function ($) {
$.fn.extend({
zoekbox: function (opts) {
if (typeof console === 'undefined') {
console = { log: function () { } };
}
return this.each(function() {
var options = {
endpoints: {
suggest: '',
alternatives: ''
},
templates: {
/* el: the container object (jQuery)
* item: the item received from the service
* query: the executed query
* currentItems: array of previous selected items
*
* return false to cancel */
item: function (el, item, query, currentItems) {
return true;
},
didyoumean: function () {
return $('<h4/>').text('Bedoelde u...');
},
clickitem: function (el) { // el is the selected item
return '';
},
dataparameters: function () { // returns an hashmap with extra params for the ajax service
return {};
},
matchingkey: function (el) {
return '';
}
},
events: {
onListBound: function (el) { // el is the bound list
},
onItemSelected: function (el, boundEle) { // the <a/> that was selected
},
onZoekboxCleared: function (zb) { // when the zoekbox was made empty
},
onKeyUp: function (ev) { // fires after keyup
},
onNiveauBepaald: function (el, level) { // fires after a niveau is determined
}
},
maxresults: 5, // the max number of results that will be added to the list
splittingChars: /[;\+&]/g, // the chars that can be used as an splitting char for multiple values
direction: 'bottom', // bottom of top
enablesubmit: true,
enablemultiple: true,
lastNiveau: null,
boundEle: this,
controls: {
clearInputButton: null // an element to clear the text in the zoekbox (null if not available)
},
classnames: {
container: {
alternatives: 'alternatives',
suggestions: 'suggestions'
},
item: {
hover: 'active'
},
positionContainer: 'auto-suggest'
}
};
$.extend(true, options, opts); // doesn't break intellisense this way
$(this).data('zoekbox', options);
var controls = {
zoekbox: $(this), // the textbox
positionContainer: null, // the object that needs positioning
container: null, // the container around the <ul/>
arrow: null,
suggestlist: null, // the unordered list wherein the items are rendered
clearinput: null,
iframe: null,
form: $(this).closest('form') // form dat we evt. kunnen submitten
};
var state = {
requestCounter: 0, // keeps track of the number of requests sent by the AJAX handler
termBeforeKeyboardNav: '', // gives back the original term when navigating with keyboard
inMouseOver: false, // keeps track whether using mouse-navigation
inFormSubmit: false, // are we processing form submit
previousQuery: { // to prevent executing the same term twice in a row
endpoint: null,
term: null
},
serviceEnabled: false,
itemkeys: [] // array containing the matching keys for all items returned by the service
};
var keycodes = {
enter: 1,
up: 2,
down: 3,
seperator: 4,
escape: 5,
shift: 6
};
var zoekboxHelper = $.zoekboxHelper(controls.zoekbox, options.splittingChars);
/// Initialize controls
var initControls = function ($) {
// controls aanmaken en binden
controls.positionContainer = $('<div/>').addClass(options.classnames.positionContainer).hide();
// voeg aan de container een iframe (IE 6) toe
controls.iframe = $('<iframe/>');
controls.positionContainer.append(controls.iframe);
// en de wrappers om de suggestlist heen
controls.suggestlist = $('<ul/>');
controls.container = $('<div/>').addClass('auto-suggest-container');
controls.container.append($('<div/>').addClass('auto-suggest-arrow')) // voeg de arrow toe
.append(
$('<div/>')
.addClass('auto-suggest-border') // border
.append(options.templates.didyoumean()) // bedoelde u...
.append($('<div/>').addClass('auto-suggest-content').append(controls.suggestlist)) // de <ul/>
);
controls.positionContainer.append(controls.container); // container toevoegen in de main container
controls.arrow = controls.container.find('.auto-suggest-arrow');
$('body').append(controls.positionContainer); // en toevoegen aan de body
//--- clear input button
if (options.controls.clearInputButton) {
controls.clearinput = $(options.controls.clearInputButton).hide();
if (controls.zoekbox.width()) {
// pas tekstbox breedte aan, aan de clearButton; en de margin-right same same
controls.zoekbox.width(controls.zoekbox.width() - (controls.clearinput.width() * 2))
.css({ 'margin-right': (controls.clearinput.width() * 2) + 'px' });
}
}
//--- autocomplete: off zetten als die er nog niet is
controls.zoekbox.attr('autocomplete', 'off');
// Do we need to init search
if (controls.zoekbox.hasClass('auto-suggest-input')
&& controls.zoekbox.val().length > 0) {
// autocomplete waarbij we aangeven dat de list niet populated moet worden
autocomplete(options.endpoints.suggest, null, false, controls.zoekbox[0]);
}
};
/// Initialize events
var initEvents = function ($) {
// bind events aan de aangemaakte controls
if (controls.clearinput) {
controls.clearinput.click(clearInput);
// als de lengte > 0 dan wel clearinput tonen
controls.zoekbox.keyup(function () {
controls.clearinput.toggle(controls.zoekbox.val().length > 0);
}).keyup();
}
$(window).resize(windowResized).resize(); // herpositioneren
// keyboard
controls.zoekbox.keyup(zoekboxKeyup).keydown(zoekboxKeydown);
controls.zoekbox.blur(zoekboxBlur);
// events doorgeven als leeggemaakt
controls.zoekbox.keyup(function () {
if (controls.zoekbox.val().length === 0) {
options.events.onZoekboxCleared(controls.zoekbox);
}
});
// formulier submit overschrijven
// ooit ga ik hier iets netjes voor maken
controls.form.find('input[type=submit],input[type=button],button').click(function(ev){
if(!options.enablesubmit) return;
state.inFormSubmit = true; // we are submitting
controls.zoekbox.focus();
controls.suggestlist.find('li').removeClass(options.classnames.item.hover);
// dit is hacky ja, maar we willen ev uitbreiden met een nieuwe prop
// which: 13 betekent: enter-toets
$.extend(true, ev, {
which: 13
});
// handle keydown
zoekboxKeydown(ev);
});
// mouse
controls.suggestlist.delegate('li', 'hover', itemHover);
controls.suggestlist.delegate('a', 'click', itemClick);
// bijhouden of we op dit moment in mousenav zitten
controls.positionContainer.mouseover(function () { state.inMouseOver = true; }).mouseout(function () { state.inMouseOver = false; });
};
/// Clear zoekbox, en geef focus
var clearInput = function (ev) {
controls.zoekbox.val('').focus().keyup(); // handle keyup voor hiden van image
ev.preventDefault(); // en niet wegnavigeren hea!
};
/// Positioneer de suggestbox
var windowResized = function () {
var hasInputWrapper = true;
var inputWrapper = controls.zoekbox.closest('div.input-wrap');
// geen wrap?
if(inputWrapper.length === 0) {
inputWrapper = controls.zoekbox;
hasInputWrapper = false;
}
// als :hidden, dan crasht .offset() in IE 8 in IE7 Compat mode
if(inputWrapper.is(':hidden')) {
return;
}
var offset = inputWrapper.offset(); // we gaan positioneren op basis van deze wrapper
var top;
switch (options.direction) {
case 'top':
top = offset.top - controls.positionContainer.height() ;
break;
default:
top = offset.top + (hasInputWrapper ? inputWrapper.height() : inputWrapper.outerHeight());
break;
}
controls.positionContainer.css({
position: 'absolute',
top: top,
left: offset.left,
width: hasInputWrapper ? inputWrapper.innerWidth() : inputWrapper.outerWidth()
});
};
/// Keystroke
var zoekboxKeyup = function (ev) {
var keycode = getKeycode(ev);
switch (keycode) {
case keycodes.enter:
case keycodes.up:
case keycodes.down:
case keycodes.seperator:
case keycodes.escape:
case keycodes.shift:
ev.preventDefault();
options.events.onKeyUp(ev);
return; // deze events doen we al in keydown; dus moeten we preventen hier
default:
state.termBeforeKeyboardNav = zoekboxHelper.zbVal(); // we slaan de laatst handmatig ingevoerde term in
autocomplete(options.endpoints.suggest); // en anders doen we gewoon autocomplete
options.events.onKeyUp(ev);
break; // ach, voor de zekerheid nog even een break
}
};
var zoekboxKeydown = function (ev) {
if (!controls.zoekbox.is(':focus')) {
// de focus is verloren; dit kan bv. doordat TAB is ingedrukt
controls.positionContainer.hide(); // hide de container
return; // en stop execution
}
if ($.browser.msie && zoekboxHelper.browserVersion() <= 8) {
zoekboxHelper.updateCaret();
}
if (controls.positionContainer.position().left === 0) {
windowResized();
}
var keycode = getKeycode(ev);
if (keycode === keycodes.shift) {
return; // hier doen we al de return; handiger met debuggen
}
var listLen = null;
switch (keycode) {
case keycodes.enter:
// als we al in alternatives mode zijn, dan gewoon doorgaan
// je kan niet 2 keer achter elkaar alternatives krijgen
if (state.previousQuery.endpoint === options.endpoints.alternatives) {
controls.positionContainer.hide();
return;
}
// als we niets geselecteerd hebben en op enter rammen...
if (getCurrentIndex() === -1 && !handleSeperatorEnterKeyDown(keycode)) {
// niet goed gegaan? prevent submit!
ev.preventDefault();
} else if (getCurrentIndex() > -1) {
// klik op een item!
itemClick.call(controls.suggestlist.find('li:nth(' + getCurrentIndex() + ')').find('a:first'));
ev.preventDefault();
}
if(!options.enablesubmit) {
ev.preventDefault();
}
break;
case keycodes.seperator:
if (handleSeperatorEnterKeyDown(keycode)) {
// goed gegaan? voeg een lege waarde toe aan de tekstbox (dus Rotterdam -> 'Rotterdam + ')
zoekboxHelper.zbVal([zoekboxHelper.zbVal(), '']);
}
ev.preventDefault();
break;
case keycodes.up:
// see if we should wrap to last item
if (getCurrentIndex() === -1) {
listLen = controls.suggestlist.find('li:visible').length;
setKeyboardNavigationIndex(listLen - 1);
} else {
setKeyboardNavigationIndex(getCurrentIndex() - 1);
}
ev.preventDefault();
break;
case keycodes.down:
listLen = controls.suggestlist.find('li:visible').length;
if (getCurrentIndex() === listLen - 1) {
setKeyboardNavigationIndex(-1);
} else {
setKeyboardNavigationIndex(getCurrentIndex() + 1);
}
ev.preventDefault();
break;
case keycodes.escape:
setKeyboardNavigationIndex(-1); // escape gaan we terug naar initiele situatie (niets geselecteerd -> -1)
ev.preventDefault();
break;
}
};
/// Handle seperator or enter char
var handleSeperatorEnterKeyDown = function (keycode) {
// als de zoekbox niet beschikbaar is, altijd true teruggeven
if(!state.serviceEnabled) {
return true;
}
// we hebben een aantal opties
// 1. Item komt overeen met een item in onze lijst. Mooi.
var matchingElement = findMatchingElement();
if (matchingElement) {
itemClick.call(matchingElement); // boots een klik na
return true;
}
// 2. Item komt niet overeen met iets in onze lijst.
// Nu kunnen we gaan kijken of er misschien een alternatief beschikbaar is
autocomplete(options.endpoints.alternatives, function (data) {
if (data.Results.length === 0) { // niets gevonden?
// als keycode enter is? dan submitten
if (keycode === keycodes.enter && options.enablesubmit) {
controls.form.submit();
}
// anders gewoon niets doen
}
});
return false;
};
/// Keyboard navigation
var setKeyboardNavigationIndex = function (ix) {
if (ix >= controls.suggestlist.find('li:visible').length || ix < -1) {
return; // moet wel in de boundaries zijn
}
if (ix === -1) {
zoekboxHelper.zbVal(state.termBeforeKeyboardNav); // herstel originele term
controls.suggestlist.find('li').removeClass(options.classnames.item.hover); // weg met die hover
return;
}
var item = controls.suggestlist.find('li:nth(' + ix + ')');
zoekboxHelper.zbVal(options.templates.clickitem(item.find('a:first'))); // in de tekstbox zetten we dit dus
itemHover.call(item); // roep de hover functie aan met 'item' als callee
};
/// Grab the keycode uit het event
var getKeycode = function (ev) {
if (ev.which === 13) { return keycodes.enter; }
if (ev.which === 40) { return keycodes.down; }
if (ev.which === 38) { return keycodes.up; }
// er zijn verschillen tussen chrome en firefox, da's niet zo mooi; dus hebben we hier wat rare code
if(options.enablemultiple) {
if (ev.which === 59 || ev.which === 186 /* semicolon */) {
return keycodes.seperator;
}
if (ev.which === 107 /* ampersand */) {
return keycodes.seperator;
}
if ((ev.which === 55 || ev.which === 187) && ev.shiftKey /* plus */) {
return keycodes.seperator;
}
}
if (ev.which === 27) { return keycodes.escape; }
if (ev.which === 16) { return keycodes.shift; }
};
/// Current item
var getCurrentIndex = function () {
// ja dit is raar; maar is workaround een bug
return controls.suggestlist.find('li:visible').index(controls.suggestlist.find('li:visible').filter('.' + options.classnames.item.hover));
};
/// Mouse events
var itemHover = function (ev) {
controls.suggestlist.find('li').removeClass(options.classnames.item.hover);
$(this).addClass(options.classnames.item.hover);
};
var itemClick = function (ev) {
var itemtext = options.templates.clickitem($(this)); // pak de tekst
options.events.onItemSelected($(this), options.boundEle);
selectItem(itemtext);
return false;
};
/// Inputdevice onafhankelijke behavior
/// Selecteren van een item bv. uit de suggestlijst
var selectItem = function (text) {
zoekboxHelper.zbVal(text); // zet de tekst
controls.zoekbox.focus(); // geef focus terug aan zoekbox
var hideContainer = function () { controls.positionContainer.hide(); }; // hide de container
// in IE 6 en 7 wordt de focus() async gedaan; en daardoor wordt de container niet gehide. Shitty dus.
if ($.browser.msie && zoekboxHelper.browserVersion() <= 8) {
setTimeout(hideContainer, 1);
} else {
hideContainer();
}
controls.suggestlist.find('li').removeClass(options.classnames.item.hover); // verwijder de hover style
};
var zoekboxBlur = function (ev) {
if (controls.positionContainer.is(':hidden')) {
return;
}
if (state.inMouseOver) {
// dit is een click in de suggestlist... die wordt apart afgehandeld.
} else {
controls.positionContainer.hide();
}
};
/// Bekijkt of de tekst in de textbox op dit moment overeenkomt met een item in de lijst, en retouneert deze
var findMatchingElement = function () {
var val = zoekboxHelper.trim(zoekboxHelper.zbVal()).toLowerCase();
var foundItemIx = -1;
// we kijken eerst of er een match is op basis van de exacte input
controls.suggestlist.find('li').each(function (i, ele) {
if(foundItemIx !== -1) return;
if (options.templates.matchingkey($(ele)).toLowerCase() === val) {
foundItemIx = i;
}
});
// dan kijken we of de service vond dat iets een exacte match was
if (foundItemIx === -1) {
var ix;
for (ix = 0; ix < state.itemkeys.length; ix++) {
if (state.itemkeys[ix] === true) {
foundItemIx = ix;
break;
}
}
}
// iets gevonden? return obj.
if(foundItemIx > -1) {
return controls.suggestlist.find('li:nth(' + foundItemIx + ')').find('a:first');
}
return false;
};
/// Switch style in container object
var switchContainerStyle = function (className) {
var changeStyle = function (oldC, newC) {
if (!controls.container.hasClass(newC) && className === newC) {
controls.container.removeClass(oldC);
}
};
changeStyle(options.classnames.container.alternatives, options.classnames.container.suggestions);
changeStyle(options.classnames.container.suggestions, options.classnames.container.alternatives);
controls.container.addClass(className);
// in IE 6 gaat hier iets niet goed met het weghalen van pijltje; vandaar deze hack
if ($.browser.msie && zoekboxHelper.browserVersion() < 7) {
controls.arrow.toggle(className === options.classnames.container.alternatives);
}
// toon de container
controls.positionContainer.show();
// pas iframe hoogte aan aan container-height
// omdat de positionContainer nu wel zichtbaar is, is height() > 0
controls.iframe.css({
height: (controls.container.outerHeight() + parseInt(controls.container.css("borderBottomWidth"), 10) + 1) + 'px'
});
return controls.positionContainer;
};
/// Doet een request naar de service
var autocomplete = function (endpoint, callback, showResults, elem) {
// fixup optional parameter
if (typeof showResults === 'undefined') {
showResults = true;
}
// huidige zoekterm
var term = zoekboxHelper.zbVal().toLowerCase();
if (!term) { zoekboxHelper.zbVal(term); } // als term leeg is, dan moeten we ff het veld opnieuw renderen
var requestCounter = ++state.requestCounter;
// als er niets veranderd is, sinds laatste query preventen we de execution
if (state.previousQuery.term === term && state.previousQuery.endpoint === endpoint) return;
// do een get naar de service (jsonp)
var postdata = $.extend({}, { query: term, max: options.maxresults }, options.templates.dataparameters());
$.get(endpoint + '?callback=?', postdata, function (data) {
// het kan zijn dat er meerdere requests door elkaar lopen; in dat geval moeten we zeker weten dat dit de actieve is
if (requestCounter !== state.requestCounter) return; // in state.requestCounter zit altijd het aantal
// clear de suggestlist, en verberg als items is 0
controls.suggestlist.empty();
state.itemkeys = [];
// service beschikbaar; dan gaan we dat aangeven
state.serviceEnabled = true;
// in IE 6 wordt de z-index verkracht wanneer we de container niet hiden voor het opnieuw vullen...
if ($.browser.msie && zoekboxHelper.browserVersion() < 7) controls.positionContainer.hide();
// callback functie; mits deze is meegegeven
if (callback && $.isFunction(callback)) { callback(data); }
// pak alle huidige items, behalve degene die nu geselecteerd is
var currentItems = zoekboxHelper.splitZbValue();
currentItems.items.splice(currentItems.ix, 1);
var ix, item;
for (ix = 0, item = data.Results[ix]; ix < data.Results.length; item = data.Results[++ix]) {
// template vullen
var a = $('<a/>').attr({ href: '#', ix: ix });
// de template functie vult de <a/>'
if (options.templates.item(a, item, data.Query, currentItems.items)) {
controls.suggestlist.append($('<li/>').append(a)); // append aan de lijst
state.itemkeys[ix] = item.Exact; // item.Exact should be used to determine whether this is an exact match
}
}
state.previousQuery = { term: term, endpoint: endpoint };
// fire event
if ($.isFunction(options.events.onNiveauBepaald)) {
if (options.lastNiveau != data.Results[0].Niveau) {
options.events.onNiveauBepaald(elem, data.Results[0].Niveau);
lastNiveau = data.Results[0].Niveau;
}
}
// wanneer geen resultaten; dan hiden we de container
if (data.Results.length === 0 || controls.suggestlist.find('li').length === 0 || !showResults) {
controls.positionContainer.hide();
return;
}
// verander de style van de container (met / zonder arrow enzo)
switch (endpoint) {
case options.endpoints.suggest:
switchContainerStyle(options.classnames.container.suggestions); break;
case options.endpoints.alternatives:
switchContainerStyle(options.classnames.container.alternatives); break;
}
// event vuren
if ($.isFunction(options.events.onListBound)) {
options.events.onListBound(controls.suggestlist);
// eventueel herpositioneren
if(options.direction === 'top') {
windowResized();
}
}
}, 'json');
};
/// Initialize behavior
initControls($);
initEvents($);
});
}
});
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment