Basic styling, lots of feature work

This commit is contained in:
2013-05-12 17:12:48 +01:00
parent 33cf34ba92
commit a644b280eb
24 changed files with 10681 additions and 24 deletions

View File

@@ -0,0 +1,346 @@
(function ($) {
/* KEY constant copied from jquery autocomplete:
* http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/
*/
var KEY = {
UP: 38,
DOWN: 40,
DEL: 46,
TAB: 9,
ENTER: 13,
ESC: 27,
COMMA: 188,
PAGEUP: 33,
PAGEDOWN: 34,
BACKSPACE: 8
};
function InputDropdown($elem, options) {
this.$elem = $elem;
this.options = $.extend({
update_input: true,
no_results_html: 'Sorry, we couldn\'t find anything.',
ignore_history: true,
process_results: function (r) { return r; }
}, options);
var $field_with_icon = this.$elem.closest('.field_with_icon'),
$existing_results = $([]);
if ($field_with_icon.length) {
$existing_results = $field_with_icon.siblings('.results');
} else {
$existing_results = this.$elem.siblings('.results');
}
if ($existing_results.length) {
this.$results = $existing_results;
this.$results.find('div.no_results').remove();
} else {
this.$results = $('<div class="results"></div>');
if ($field_with_icon.length) {
$field_with_icon.after(this.$results);
} else {
this.$elem.after(this.$results);
}
}
if (!this.$results.find('ul.result_list').length) {
this.$results.append('<ul class="result_list"></ul>');
}
this.$no_results = $('<div class="no_results">' + this.options.no_results_html + '</div>');
this.$no_results.hide();
this.$results.append(this.$no_results);
this.handle_optional_content();
// css should do this
//this.$results.hide();
//this.$results.width(this.$elem.outerWidth());
this.livesearch = $elem.livesearch(this.options).data('livesearch');
this._attach();
}
$.fn.livesearch_input_dropdown = function (options) {
options = options || {};
return $(this).each(function () {
var input_dropdown = $(this).data('livesearch.input_dropdown');
if (!input_dropdown) {
input_dropdown = new InputDropdown($(this), options);
$(this).data('livesearch.input_dropdown', input_dropdown);
}
});
};
$.extend(InputDropdown.prototype, {
_attach: function () {
var _this = this;
this.$elem.bind('livesearch:results', function (e, results) {
if (_this.options.process_results) {
results = _this.options.process_results(results);
}
_this.show_results(results);
_this.push_history(results);
});
this.bind_results();
this.bind_popstate();
this.$elem.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function (e) {
var something_selected = !!_this.$results.find('.selected').length,
$prev,
$next,
$selected;
switch (e.keyCode) {
case KEY.UP:
if (something_selected) {
$prev = _this.$results.find('.selected').prev(':not(.not_result)');
if (!$prev.length) {
$prev = _this.$results.find('li:not(.not_result)').last();
}
_this.select($prev, false);
} else {
_this.select(_this.$results.find('li:not(.not_result)').last(), false);
}
e.preventDefault();
break;
case KEY.DOWN:
if (something_selected) {
$next = _this.$results.find('.selected').next(':not(.not_result)');
if (!$next.length) {
$next = _this.$results.find('li:not(.not_result)').first();
}
_this.select($next, false);
} else {
_this.select(_this.$results.find('li:not(.not_result)').first(), false);
}
e.preventDefault();
break;
case KEY.ENTER:
// we want to trigger the selected event
$selected = _this.$results.find('.selected');
if (!$selected.length && _this.input_can_submit()) {
return;
}
_this.select($selected, true);
e.preventDefault();
break;
default:
break;
}
});
},
history_api_supported: function () {
return window.history && window.history.pushState;
},
bind_popstate: function () {
if (!this.history_api_supported() || this.options.ignore_history) { return; }
var _this = this;
this.replacing_history_state = false;
window.onpopstate = function (e) {
if (e.state && e.state.livesearch) {
// We've got a search object in history, so let's restore the input to that state.
_this.show_results(e.state.livesearch.results);
_this.livesearch.suspend_while(function () {
_this.$elem.val(e.state.livesearch.input_value).trigger('input');
});
} else {
// We're at a new or null history state. We may have got
// here via the back button from an executed search, or have landed here via http search.
// Let's ensure a blank input.
_this.reset(true);
}
};
},
input_can_submit: function () {
var
readyState = this.livesearch.search_xhr.readyState,
search_in_progress = (readyState > 0 && readyState < 4);
return this.options.input_can_submit_on_enter && !search_in_progress && this.$elem.is(":focus");
},
bind_results: function () {
var _this = this;
this.$results.find('li').bind('click', function () {
_this.select($(this), true);
});
this.$results.find('li').bind('mouseover', function () {
_this.select($(this));
_this.unselect_on_mouseout = true;
});
this.$results.bind('mouseout', function () {
if (_this.unselect_on_mouseout) {
_this.unselect_currently_selected();
}
});
this.$elem.bind('livesearch:reveal_results', function () {
_this.reveal_results();
});
},
/* Accepts an array of results, and returns an array of result
* names (strings)
*/
build_result_names_list: function (results) {
var result_names = [],
_this = this;
$.each(results, function () {
var name = this;
if (_this.options.return_name_from_result) {
name = _this.options.return_name_from_result(this);
} else if (this !== 'string') {
name = this[0];
}
result_names.push(name);
});
return result_names;
},
show_results: function (results) {
var $results_ul = this.$results.children('ul'),
$full_search_link = $([]),
result_names,
text,
arrow_icon,
meta;
if (results.results) {
meta = results;
results = results.results;
}
this.unselect_currently_selected();
$results_ul.empty();
if (!results.length) {
this.$no_results.show();
$results_ul.hide();
} else {
this.$no_results.hide();
$results_ul.show();
}
result_names = this.build_result_names_list(results);
$.each(result_names, function (index) {
var name = this,
$li = $('<li>' + name + '</li>');
$li.data('livesearch_result', results[index]);
$results_ul.append($li);
});
if (meta && meta.count > results.length) {
text = 'see all ' + meta.count + ' results';
arrow_icon = '<span class="icon-arrow"></span>&nbsp;';
$full_search_link = $('<a>').attr({
'href': this.livesearch.url + '?' + this.livesearch.last_search,
'title': text,
'class': 'see_all not_result'
}).html(arrow_icon + text).wrap('<li>');
$results_ul.append($full_search_link);
}
this.bind_results();
this.$elem.trigger('livesearch:reveal_results');
},
push_history: function (results) {
if (!this.history_api_supported() || this.options.ignore_history) { return; }
if (typeof results === 'string') {
results = JSON.parse(results);
}
var state = {
livesearch : {
input_value : this.$elem.val(),
results : results
}
};
if (this.replacing_history_state) {
window.history.replaceState(state);
this.replacing_history_state = false;
} else {
window.history.pushState(state, "", '?' + this.livesearch.last_search);
}
},
reveal_results: function () {
var _this = this;
this.$results.slideDown(function () {
$(window).resize();
_this.$results.trigger('sticky_bar:fix_to_bottom');
_this.$results.trigger('shifted');
});
},
reset: function (clear) {
this.$results.hide();
this.$elem.val('');
if (clear) {
this.$results.children('ul.result_list').html('');
}
},
unselect_currently_selected: function () {
var $results_ul = this.$results.children('ul'),
$last_selected = $results_ul.children('li.selected');
$last_selected.trigger('livesearch:unselect');
$last_selected.removeClass('selected');
// We're here because of a mouseout, or because the user selected something with
// the keyboard, or clicking. In either case, we don't want to unselect on the
// next mouseout.
this.unselect_on_mouseout = false;
},
// There are two kinds of selects:
// - Hard selects, these are triggered by clicks or the enter key. They
// trigger the select event.
// - Soft selects, these are triggered by arrowing up or down. They do not
// trigger the select event.
select: function ($li, trigger) {
var _this = this;
if ($li.is('.not_result')) { return; }
this.unselect_currently_selected();
$li.addClass('selected');
if (this.options.update_input) {
this.livesearch.suspend_while(function () {
_this.$elem.val($li.text());
_this.$elem.focus();
});
}
$li.trigger('livesearch:soft_select');
if (trigger) {
$li.trigger('livesearch:selected', [$li.data('livesearch_result')]);
}
},
handle_optional_content: function () {
if (this.options.prepend) {
this.$results.prepend(this.options.prepend);
}
}
});
}(jQuery));

View File

@@ -0,0 +1,148 @@
(function ($) {
//The timeout has to live in this scope
var timeout;
function LiveSearch($elem, options) {
this.$elem = $elem;
this.$form = $elem.closest('form');
this.options = $.extend({
delay: 400,
minimum_characters: 3,
serialize: this.$form,
client_side_cache: true,
process_data: false
}, options);
if (this.options.file_extension) {
this.ajax_url = this.ensure_file_extension(this.options.file_extension);
} else {
this.ajax_url = this.url;
}
this.last_search = false;
this.search_xhr = false;
if (this.options.client_side_cache) {
this.cache = {};
} else {
this.cache = false;
}
this.active = true;
this._attach();
}
$.fn.livesearch = function (options) {
options = options || {};
return $(this).each(function () {
var livesearch = $(this).data('livesearch');
if (!livesearch) {
livesearch = new LiveSearch($(this), options);
$(this).data('livesearch', livesearch);
}
});
};
$.extend(LiveSearch.prototype, {
_attach: function () {
var _this = this;
this.$elem.attr('autocomplete', 'off'); //we got this, yall
this.$elem.bind("keypress cut paste input", function () {
if (!_this.active) { return; }
clearTimeout(timeout);
timeout = setTimeout(function () {
_this.search();
}, _this.options.delay);
});
this.options.serialize.bind('change', function () {
_this.search();
});
this.$elem.bind('livesearch:suspend', function () {
_this.active = false;
});
this.$elem.bind('livesearch:activate', function () {
_this.active = true;
});
this.$elem.bind('livesearch:cancel', function () {
if (_this.search_xhr) {
_this.search_xhr.abort();
}
_this.last_search = false;
});
},
ensure_file_extension: function (extension) {
var
host_regexp_string = window.location.host.replace(/[^\w\d]/g, function (m) { return '\\' + m; }),
file_extension_regexp = new RegExp("((?:" + host_regexp_string + ")?[^\\.$#\\?]+)(\\.\\w*|)($|#|\\?)");
return this.url.replace(file_extension_regexp, function (m, _1, _2, _3) { return _1 + '.' + extension + _3; });
},
suspend_while: function (func) {
this.active = false;
func();
// TODO: this timeout is to to allow events to bubble before re-enabling,
// but I'm not sure why bubbling doesn't occur synchronously.
var _this = this;
setTimeout(function () {
_this.active = true;
}, 100);
},
search: function () {
var _this = this,
form_data = this.options.serialize.serialize();
if (this.options.process_data) {
form_data = this.options.process_data.apply(this, [form_data]);
if (typeof form_data === 'object') {
form_data = $.param(form_data);
}
}
if (form_data === this.last_search) { return; }
if (this.$elem.val().length < this.options.minimum_characters) { return; }
if (this.search_xhr) {
this.search_xhr.abort();
}
if (this.cache && this.cache[form_data] && typeof (this.cache[form_data]) !== 'function') {
this.$elem.trigger('livesearch:results', [this.cache[form_data]]);
} else {
this.$elem.trigger('livesearch:searching');
this.$elem.addClass('searching');
this.search_xhr = $.ajax({
type: 'get',
url: this.options.url || this.$form.attr('action'),
dataType: 'json',
data: form_data,
global: false,
success: function (data, textStatus, xhr) {
// this is the best workaround I can think of for
// http://dev.jquery.com/ticket/6173
if (data === null) { return; }
_this.$elem.trigger('livesearch:results', [data]);
_this.$elem.removeClass('searching');
if (_this.cache) {
_this.cache[form_data] = data;
}
},
error: function () {
_this.$elem.trigger('livesearch:ajax_error');
_this.$elem.removeClass('searching');
}
});
}
this.last_search = form_data;
}
});
}(jQuery));

View File

@@ -0,0 +1,100 @@
(function ($) {
$.fn.livesearch_multi_selector = function (options) {
options = $.extend({
url: false,
cancel_copy: 'Remove',
sortable: false,
update_input: false
}, options);
return $(this).each(function () {
var $div = $(this),
$input = $div.find('input[type="text"]'),
$field_with_icon = $input.closest('.field_with_icon'),
$list = $div.find('ol.search-selected,ul.search-selected'),
$search_loading_icon = $input.siblings('.icon-search'),
name = $input.attr('name'),
position_name = $input.siblings('.position').attr('name'),
this_options = options,
input_dropdown;
this_options.url = options.url || $div.closest('form').attr('action');
input_dropdown = $input.livesearch_input_dropdown(this_options);
if (options.sortable) {
$list.sortable({
containment: 'document',
axis: 'y',
update: function () {
$list.find('input.position').each(function (i) {
$(this).val(i);
});
}
});
}
function unselectable($li) {
var $cancel = $('<a class="cancel-link" href="#">' + options.cancel_copy + '</a>'),
$destroy = $li.find('.destroy');
$li.append($cancel);
if ($destroy.val() && $destroy.val() !== 'false') {
$li.hide();
}
$cancel.click(function (e) {
e.preventDefault();
$li.fadeOut(function () {
$destroy.val('1');
if (!$destroy.length) { //new record
$li.remove();
}
});
});
}
function reset() {
$input.val('');
$field_with_icon.siblings('.results').slideUp();
$input.trigger('livesearch:cancel');
}
$div.addClass('search');
$input.attr('name', '');
$list.find('li').each(function () {
unselectable($(this));
});
$input.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$input.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$div.bind('livesearch:selected', function (e, data) {
if (!data) { return; }
var this_name = name.replace('[template]', '[' + $list.children('li').length + ']'),
this_position_name = false,
$li;
if (position_name) {
this_position_name = position_name.replace('[template]', '[' + $list.children('li').length + ']');
}
$li = $('<li>' + data[0] + '<input type="hidden" name="' + this_name + '" value="' + data[1] + '"/></li>');
if (options.sortable) {
if (this_position_name) {
$li.append('<input type="hidden" class="position" name="' + this_position_name + '" />');
}
$list.find('input.position').each(function (i) {
$(this).val(i);
});
}
$list.append($li);
unselectable($li);
reset();
});
});
};
}(jQuery));

View File

@@ -0,0 +1,52 @@
(function ($) {
/*
* Behavior:
*
* A user starts typing into an input, when they stop typing, a list of matching pages are
* displayed. The user can select an entry with the up and down arrow key and hit enter,
* or they can click a result. When a result is selected, the corresponding url is navigated
* to.
*
* Requirements:
*
* jquery.livesearch
* jquery.livesearch.input_dropdown
* jquery.livesearch.pretty_input_dropdown
*
* Expected markup:
*
* <form>
* <div>
* <span class='icon-search'></span>
* <span class='icon-search-clear'></span>
* <input></input>
* <div class='results'></div>
* </div>
* </form>
*
* Expected JSON response from the XHR:
*
* [
* ['Title of page', 'http://url/'],
* ['Title of another page', 'http://url2/']
* ]
*
*/
$.fn.livesearch_navigator = function (options) {
options = options || {};
return $(this).each(function () {
var $form = $(this);
$form.livesearch_pretty_input_dropdown();
$form.bind('livesearch:selected', function (e, data) {
if (data) {
window.location = data[1];
}
});
});
};
}(jQuery));

View File

@@ -0,0 +1,103 @@
(function ($) {
/*
* Behavior:
*
* A user starts typing into an input, when they stop typing, a list of matching pages are
* displayed. The user can select an entry with the up and down arrow key and hit enter,
* or they can click a result. When a result is selected, the corresponding url is navigated
* to.
*
* Requirements:
*
* jquery.livesearch
* jquery.livesearch.input_dropdown
*
* Expected markup:
*
* <form>
* <div>
* <span class='icon-search'></span>
* <span class='icon-search-clear'></span>
* <input></input>
* <div class='results'></div>
* </div>
* </form>
*
* Expected JSON response from the XHR:
*
* [
* ['Title of page', 'http://url/'],
* ['Title of another page', 'http://url2/']
* ]
*
*/
$.fn.livesearch_pretty_input_dropdown = function (options) {
options = options || {};
return $(this).each(function () {
var $form = $(this),
$text = $form.find('input[type=text]'),
$icon_search_clear = $form.find('.icon-search-clear'),
$search_loading_icon = $text.siblings('.icon-search'),
input_dropdown;
$text.livesearch_input_dropdown($.extend(options, {update_input : false}));
input_dropdown = $text.data('livesearch.input_dropdown');
function handle_close_button() {
if (this.value) {
$icon_search_clear.show();
} else {
$icon_search_clear.hide();
}
}
handle_close_button.call($text[0]);
$text.bind('keypress cut paste input livesearch:close_results blur', handle_close_button);
$text.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$text.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$text.bind('livesearch:results', function () {
// if this option is set, assume we want the cursor to stay in the input after search is done
if (options.input_can_submit_on_enter) {
return;
}
var $results = $text.siblings('.results');
input_dropdown.select($results.find('li:first'));
});
function clear_results() {
$text.val('');
$text.trigger('livesearch:cancel').trigger('livesearch:close_results');
}
$icon_search_clear.bind('click', function () {
$text.focus();
clear_results();
});
$text.bind('livesearch:close_results', function () {
$text.siblings('.results').slideUp(function () {
$(window).resize();
$(this).trigger('sticky_bar:fix_to_bottom');
});
});
$text.bind('blur', function () {
if ($text.val().length < 3) {
clear_results();
}
});
});
};
}(jQuery));

View File

@@ -0,0 +1,69 @@
(function ($) {
$.fn.livesearch_selector = function (options) {
options = $.extend({url: false, cancel_copy: 'Cancel', target_input: false}, options);
return $(this).each(function () {
var $div = $(this),
$input = $div.find('input[type="text"]'),
$hidden_input = options.target_input || $div.find('input[type="hidden"]'),
$search_loading_icon = $input.siblings('.icon-search'),
input_dropdown;
$div.addClass('search');
function select() {
$input.hide();
$input.attr('disabled', 'disabled');
$input.siblings('.icon-search-clear, .icon-search').hide();
var $value_div = $('<div class="field-selected"><span class="value">' + $input.val() + '</span><a class="cancel-link" href="#">' + options.cancel_copy + '</a></div>');
$input.siblings('div.field-selected').remove();
$input.after($value_div);
$input.siblings('.results').slideUp();
$input.trigger('livesearch_selector:select', [ $input.val() ]);
$value_div.find('a.cancel-link').click(function (e) {
e.preventDefault();
$input.val('');
$value_div.remove();
$input.removeAttr('disabled');
$input.siblings('.icon-search-clear, .icon-search').show();
$input.show();
$input.focus();
$hidden_input.val('');
$input.trigger('dirty');
$input.trigger('livesearch_selector:unselect');
});
}
//if the page loads with values in the inputs, switch to selected state
if ($hidden_input.val() && $input.val()) {
select();
}
options.url = options.url || $div.closest('form').attr('action');
input_dropdown = $input.livesearch_input_dropdown(options);
$input.bind('livesearch:searching', function () {
$search_loading_icon.removeClass('icon-search').addClass('icon-loading-small');
});
$input.bind('livesearch:results livesearch:ajax_error', function () {
$search_loading_icon.removeClass('icon-loading-small').addClass('icon-search');
});
$input.bind('livesearch:results', function () {
var $results = $input.siblings('.results');
input_dropdown.select($results.find('li:first'));
});
$div.bind('livesearch:selected', function (e, data) {
if (!data) {
return;
}
$hidden_input.val(data[1]);
select();
});
});
};
}(jQuery));