/*! HTML5 Finder - v1.0.2 - 2014-05-05 * https://github.com/jgerigmeyer/jquery-html5finder * Copyright (c) 2014 Jonny Gerig Meyer; Licensed MIT */ (function ($) { 'use strict'; var cache = {}; var methods = { init: function (opts) { var options = $.extend({}, $.fn.html5finder.defaults, opts); var context = $(this); var finder = context.find(options.finderSelector); var numberCols = finder.find(options.sectionSelector).length || 1; methods.updateNumberCols(finder, numberCols); methods.markSelected(finder, opts); methods.attachHandler(context, finder, opts); return context; }, // We want to be able to treat already-selected items differently markSelected: function (finder, opts) { var options = $.extend({}, $.fn.html5finder.defaults, opts); finder.find(options.itemSelector).filter(options.selected).attr({ 'data-selected': true, 'checked': 'checked' }).data('selected', true); finder.find(options.itemSelector).filter(options.notSelected) .removeAttr('checked data-selected').data('selected', false); }, updateNumberCols: function (finder, numberCols) { finder.data('cols', numberCols).attr('data-cols', numberCols); }, // Define the function for horizontal scrolling: // Scrolls to the previous section (so that the active section is centered) horzScroll: function (finder, scrollCont, opts, last) { var options = $.extend({}, $.fn.html5finder.defaults, opts); var scroll = $.Deferred(); if (options.horizontalScroll) { var scrollTarget = 0; var currentScroll = scrollCont.scrollLeft(); var focusSection = finder.find(options.sectionSelector + '.focus'); var prevSection = focusSection.prev(options.sectionSelector); if (prevSection.length) { scrollTarget = currentScroll + prevSection.position().left; // If the active section is going to be the last section, // ...modify scrollTarget so that nothing will be visible to the right // ...of active section, instead of scrolling directly to prevSection. if (last) { scrollTarget = scrollTarget - ( scrollCont.innerWidth() - focusSection.outerWidth() - prevSection.outerWidth() ); } } if (currentScroll === scrollTarget) { scroll.resolve(); } else { $.when( scrollCont.animate({scrollLeft: scrollTarget}, 'fast') ).done(function () { scroll.resolve(); }); } } else { scroll.resolve(); } return scroll.promise(); }, addItems: function (data, colName, newCol, context, finder, opts) { var options = $.extend({}, $.fn.html5finder.defaults, opts); var scrollCont = context.find(options.scrollContainer); var items; data.colname = colName; items = options.itemTplFn(data); newCol.find(options.sectionContentSelector).html(items); methods.horzScroll(finder, scrollCont, opts); if (options.itemsAddedCallback) { options.itemsAddedCallback(items); } }, attachHandler: function (context, finder, opts) { var options = $.extend({}, $.fn.html5finder.defaults, opts); var scrollCont = context.find(options.scrollContainer); finder.on('click', options.itemSelector, function () { methods.itemClick(context, finder, $(this), opts); }); // Clicking a disabled input adds focus to that section finder.on('click', options.labelSelector, function (e) { var el = $(this); var section = el.closest(options.sectionSelector); if (section.find('#' + el.attr('for')).is(':disabled')) { section.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); methods.horzScroll(finder, scrollCont, opts); } e.stopPropagation(); }); // Clicking empty space (not input or label) adds focus to section finder.on('click', options.sectionSelector, function (e) { var section = $(this); if (!$(e.target).is('input, label')) { section.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); methods.horzScroll(finder, scrollCont, opts); } }); }, itemClick: function (context, finder, thisItem, opts) { var options = $.extend({}, $.fn.html5finder.defaults, opts); var scrollCont = context.find(options.scrollContainer); var container = thisItem.closest(options.sectionSelector); var ajaxUrl = options.getAjaxUrl(thisItem); var target = container.next(options.sectionSelector); var colName, newCol, numberCols; // Clicking an already-selected input only scrolls (if applicable), // ...adds focus, and empties subsequent sections if (thisItem.data('selected') === true) { if (!container.hasClass('focus')) { container.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); } else { target.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); } target.nextAll(options.sectionSelector).empty(); target.find(options.itemSelector).filter(options.selected) .removeAttr('checked data-selected').data('selected', false); $.when(methods.horzScroll(finder, scrollCont, opts)).done(function () { target.nextAll(options.sectionSelector).remove(); numberCols = finder.find(options.sectionSelector).length; methods.updateNumberCols(finder, numberCols); }); if (thisItem.data('children') && options.itemSelectedCallback) { options.itemSelectedCallback(thisItem); } } else { // Last-child section only receives focus on-click by default if (!thisItem.data('children')) { container.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); $.when( methods.horzScroll(finder, scrollCont, opts, true) ).done(function () { container.nextAll(options.sectionSelector).remove(); numberCols = finder.find(options.sectionSelector).length; methods.updateNumberCols(finder, numberCols); }); if (options.lastChildSelectedCallback) { options.lastChildSelectedCallback(thisItem); } } else { var addOrReplaceTargetCol = function () { numberCols = container.prevAll(options.sectionSelector).addBack() .removeClass('focus').length + 1; methods.updateNumberCols(finder, numberCols); colName = 'col' + numberCols.toString(); newCol = options.columnTplFn({colname: colName}); if (target.length) { target.nextAll(options.sectionSelector).remove(); target.replaceWith(newCol); } else { container.after(newCol); } // Use cached data, if exists (and ``option.cache: true``) if (options.cache && cache[ajaxUrl]) { var response = cache[ajaxUrl]; methods.addItems( response, colName, newCol, context, finder, opts ); } else { // Add loading screen while waiting for Ajax call to return data if (options.loading) { newCol.loadingOverlay(); } $.when($.get(ajaxUrl)).done(function (response) { if (options.cache) { cache[ajaxUrl] = response; } // Add returned data to the next section methods.addItems( response, colName, newCol, context, finder, opts ); }).always(function () { if (options.loading) { newCol.loadingOverlay('remove'); } }); } }; // If the target section already exists and doesn't have focus... if (target.length && !target.hasClass('focus')) { // First empty target section & scroll, then add or replace target. target.nextAll(options.sectionSelector).addBack().empty(); target.addClass('focus').siblings(options.sectionSelector) .removeClass('focus'); $.when( methods.horzScroll(finder, scrollCont, opts, true) ).done(function () { addOrReplaceTargetCol(); }); } else { // Otherwise, just add or replace target without scrolling. addOrReplaceTargetCol(); } if (options.itemSelectedCallback) { options.itemSelectedCallback(thisItem); } } methods.markSelected(finder, opts); } }, // Expose internal methods to allow stubbing in tests exposeMethods: function () { return methods; } }; $.fn.html5finder = function (method) { if (methods[method]) { return methods[method].apply( this, Array.prototype.slice.call(arguments, 1) ); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist on jQuery.html5finder'); } }; /* Setup plugin defaults */ /* jshint -W101 */ $.fn.html5finder.defaults = { itemTplFn: null, // Fn accepts data, returns rendered items columnTplFn: null, // Fn accepts data, returns rendered col loading: false, // If true, adds loading a overlay while waiting for Ajax response // ...requires jquery.ajax-loading-overlay // ...https://github.com/jgerigmeyer/django-ajax-loading-overlay horizontalScroll: false, // If true, scrolls to center active section scrollContainer: null, // The container (window) to be scrolled selected: 'input:checked', // A selected element notSelected: 'input:not(:checked)', // An unselected element finderSelector: '.finder-body', // Finder container sectionSelector: null, // Sections sectionContentSelector: null, // Content to be replaced by Ajax function itemSelector: '.finderinput', // Selector for items in each section labelSelector: '.finderselect', // Selector for item labels in each section itemSelectedCallback: null, // Callback function, runs after input in any section (except lastChild) is selected lastChildSelectedCallback: null, // Callback function, runs after input in last section is selected itemsAddedCallback: null, // Callback function, runs after new items are added cache: true, // If true, ajax response data will be cached getAjaxUrl: function (item) { // Function that returns the ajax-url to get an item's children return item.data('url'); } }; /* jshint +W101 */ }(jQuery));