mirror of
https://github.com/postgres/pgweb.git
synced 2025-08-06 09:57:57 +00:00
Import django-selectable
This will also be used to do /admin/ autocompletes.
This commit is contained in:
390
media/selectable/js/jquery.dj.selectable.js
Normal file
390
media/selectable/js/jquery.dj.selectable.js
Normal file
@ -0,0 +1,390 @@
|
||||
/*jshint trailing:true, indent:4*/
|
||||
/*
|
||||
* django-selectable UI widget
|
||||
* Source: https://github.com/mlavin/django-selectable
|
||||
* Docs: http://django-selectable.readthedocs.org/
|
||||
*
|
||||
* Depends:
|
||||
* - jQuery 1.7+
|
||||
* - jQuery UI 1.8 widget factory
|
||||
*
|
||||
* Copyright 2010-2014, Mark Lavin
|
||||
* BSD License
|
||||
*
|
||||
*/
|
||||
(function ($) {
|
||||
|
||||
$.widget("ui.djselectable", $.ui.autocomplete, {
|
||||
|
||||
options: {
|
||||
removeIcon: "ui-icon-close",
|
||||
comboboxIcon: "ui-icon-triangle-1-s",
|
||||
defaultClasses: {
|
||||
"text": "ui-widget ui-widget-content ui-corner-all",
|
||||
"combobox": "ui-widget ui-widget-content ui-corner-left ui-combo-input"
|
||||
},
|
||||
prepareQuery: null,
|
||||
highlightMatch: true,
|
||||
formatLabel: null
|
||||
},
|
||||
|
||||
_initDeck: function () {
|
||||
/* Create list display for currently selected items for multi-select */
|
||||
var self = this;
|
||||
var data = $(this.element).data();
|
||||
var style = data.selectablePosition || data['selectable-position'] || 'bottom';
|
||||
this.deck = $('<ul>').addClass('ui-widget selectable-deck selectable-deck-' + style);
|
||||
if (style === 'bottom' || style === 'bottom-inline') {
|
||||
$(this.element).after(this.deck);
|
||||
} else {
|
||||
$(this.element).before(this.deck);
|
||||
}
|
||||
$(self.hiddenMultipleSelector).each(function (i, input) {
|
||||
self._addDeckItem(input);
|
||||
});
|
||||
},
|
||||
|
||||
_addDeckItem: function (input) {
|
||||
/* Add new deck list item from a given hidden input */
|
||||
var self = this,
|
||||
li = $('<li>').addClass('selectable-deck-item'),
|
||||
item = {element: self.element, input: input, wrapper: li, deck: self.deck},
|
||||
button;
|
||||
li.html($(input).attr('title'));
|
||||
if (self._trigger("add", null, item) === false) {
|
||||
input.remove();
|
||||
} else {
|
||||
button = this._removeButtonTemplate(item);
|
||||
button.click(function (e) {
|
||||
e.preventDefault();
|
||||
if (self._trigger("remove", e, item) !== false) {
|
||||
$(input).remove();
|
||||
li.remove();
|
||||
}
|
||||
});
|
||||
li.append(button).appendTo(this.deck);
|
||||
}
|
||||
},
|
||||
|
||||
_removeButtonTemplate: function (item) {
|
||||
var options = {
|
||||
icons: {
|
||||
primary: this.options.removeIcon
|
||||
},
|
||||
text: false
|
||||
},
|
||||
button = $('<a>')
|
||||
.attr('href', '#')
|
||||
.addClass('selectable-deck-remove')
|
||||
.button(options);
|
||||
return button;
|
||||
},
|
||||
|
||||
select: function (item, event) {
|
||||
/* Trigger selection of a given item.
|
||||
Item should contain two properties: id and value
|
||||
Event is the original select event if there is one.
|
||||
Event should not be passed if triggered manually.
|
||||
*/
|
||||
var $input = $(this.element);
|
||||
$input.removeClass('ui-state-error');
|
||||
this._setHidden(item);
|
||||
if (item) {
|
||||
if (this.allowMultiple) {
|
||||
$input.val("");
|
||||
this.term = "";
|
||||
if ($(this.hiddenMultipleSelector + '[value="' + item.id + '"]').length === 0) {
|
||||
var newInput = $('<input />', {
|
||||
'type': 'hidden',
|
||||
'name': this.hiddenName,
|
||||
'value': item.id,
|
||||
'title': item.value,
|
||||
'data-selectable-type': 'hidden-multiple'
|
||||
});
|
||||
$input.after(newInput);
|
||||
this._addDeckItem(newInput);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
$input.val(item.value);
|
||||
var ui = {item: item};
|
||||
if (typeof(event) === 'undefined' || event.type !== "djselectableselect") {
|
||||
this.element.trigger("djselectableselect", [ui ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_setHidden: function (item) {
|
||||
/* Set or clear single hidden input */
|
||||
var $elem = $(this.hiddenSelector);
|
||||
if (item && item.id) {
|
||||
$elem.val(item.id);
|
||||
} else {
|
||||
$elem.val("");
|
||||
}
|
||||
},
|
||||
|
||||
_comboButtonTemplate: function (input) {
|
||||
// Add show all items button
|
||||
var options = {
|
||||
icons: {
|
||||
primary: this.options.comboboxIcon
|
||||
},
|
||||
text: false
|
||||
},
|
||||
button = $("<a>")
|
||||
.html(" ")
|
||||
.attr("tabIndex", -1)
|
||||
.attr("title", "Show All Items")
|
||||
.button(options)
|
||||
.removeClass("ui-corner-all")
|
||||
.addClass("ui-corner-right ui-button-icon ui-combo-button");
|
||||
return button;
|
||||
},
|
||||
|
||||
_create: function () {
|
||||
/* Initialize a new selectable widget */
|
||||
var self = this,
|
||||
$input = $(this.element),
|
||||
data = $input.data(),
|
||||
options, button;
|
||||
this.url = data.selectableUrl || data['selectable-url'];
|
||||
this.allowNew = data.selectableAllowNew || data['selectable-allow-new'];
|
||||
this.allowMultiple = data.selectableMultiple || data['selectable-multiple'];
|
||||
this.textName = $input.attr('name');
|
||||
this.hiddenName = this.textName.replace(new RegExp('_0$'), '_1');
|
||||
this.hiddenSelector = ':input[data-selectable-type=hidden][name=' + this.hiddenName + ']';
|
||||
this.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + this.hiddenName + ']';
|
||||
this.selectableType = data.selectableType || data['selectable-type'];
|
||||
if (this.allowMultiple) {
|
||||
this.allowNew = false;
|
||||
$input.val("");
|
||||
this._initDeck();
|
||||
}
|
||||
options = data.selectableOptions || data['selectable-options'];
|
||||
if (options) {
|
||||
this._setOptions(options);
|
||||
}
|
||||
// Call super-create
|
||||
// This could be replaced by this._super() with jQuery UI 1.9
|
||||
$.ui.autocomplete.prototype._create.call(this);
|
||||
$input.addClass(this.options.defaultClasses[this.selectableType]);
|
||||
// Additional work for combobox widgets
|
||||
if (this.selectableType === 'combobox') {
|
||||
// Add show all items button
|
||||
button = this._comboButtonTemplate($input);
|
||||
button.insertAfter($input).click(function (e) {
|
||||
e.preventDefault();
|
||||
// close if already visible
|
||||
if (self.widget().is(":visible")) {
|
||||
self.close();
|
||||
}
|
||||
// pass empty string as value to search for, displaying all results
|
||||
self._search("");
|
||||
$input.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Override the default source creation
|
||||
_initSource: function () {
|
||||
var self = this,
|
||||
$input = $(this.element);
|
||||
this.source = function dataSource(request, response) {
|
||||
/* Custom data source to uses the lookup url with pagination
|
||||
Adds hook for adjusting query parameters.
|
||||
Includes timestamp to prevent browser caching the lookup. */
|
||||
var now = new Date().getTime(),
|
||||
query = {term: request.term, timestamp: now},
|
||||
page = $input.data("page");
|
||||
if (self.options.prepareQuery) {
|
||||
self.options.prepareQuery.apply(self, [query]);
|
||||
}
|
||||
if (page) {
|
||||
query.page = page;
|
||||
}
|
||||
function unwrapResponse(data) {
|
||||
var results = data.data,
|
||||
meta = data.meta;
|
||||
if (meta.next_page && meta.more) {
|
||||
results.push({
|
||||
id: '',
|
||||
value: '',
|
||||
label: meta.more,
|
||||
page: meta.next_page,
|
||||
term: request.term
|
||||
});
|
||||
}
|
||||
return response(results);
|
||||
}
|
||||
$.getJSON(self.url, query, unwrapResponse);
|
||||
};
|
||||
},
|
||||
// Override the default auto-complete render.
|
||||
_renderItem: function (ul, item) {
|
||||
/* Adds hook for additional formatting, allows HTML in the label,
|
||||
highlights term matches and handles pagination. */
|
||||
var label = item.label,
|
||||
self = this,
|
||||
$input = $(this.element),
|
||||
re, html, li;
|
||||
if (this.options.formatLabel && !item.page) {
|
||||
label = this.options.formatLabel.apply(this, [label, item]);
|
||||
}
|
||||
if (this.options.highlightMatch && this.term && !item.page) {
|
||||
re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
|
||||
$.ui.autocomplete.escapeRegex(this.term) +
|
||||
")(?![^<>]*>)(?![^&;]+;)", "gi");
|
||||
if (label.html) {
|
||||
html = label.html();
|
||||
html = html.replace(re, "<span class='highlight'>$1</span>");
|
||||
label.html(html);
|
||||
} else {
|
||||
label = label.replace(re, "<span class='highlight'>$1</span>");
|
||||
}
|
||||
}
|
||||
li = $("<li></li>")
|
||||
.data("item.autocomplete", item)
|
||||
.append($("<a></a>").append(label))
|
||||
.appendTo(ul);
|
||||
if (item.page) {
|
||||
li.addClass('selectable-paginator');
|
||||
}
|
||||
return li;
|
||||
},
|
||||
// Override the default auto-complete suggest.
|
||||
_suggest: function (items) {
|
||||
/* Needed for handling pagination links */
|
||||
var $input = $(this.element),
|
||||
page = $input.data('page'),
|
||||
ul = this.menu.element;
|
||||
if (page) {
|
||||
$('.selectable-paginator', ul).remove();
|
||||
} else {
|
||||
ul.empty();
|
||||
}
|
||||
$input.data('page', null);
|
||||
ul.zIndex($input.zIndex() + 1);
|
||||
this._renderMenu(ul, items);
|
||||
// jQuery UI menu does not define deactivate
|
||||
if (this.menu.deactivate) {
|
||||
this.menu.deactivate();
|
||||
}
|
||||
this.menu.refresh();
|
||||
// size and position menu
|
||||
ul.show();
|
||||
this._resizeMenu();
|
||||
ul.position($.extend({of: this.element}, this.options.position));
|
||||
if (this.options.autoFocus) {
|
||||
this.menu.next(new $.Event("mouseover"));
|
||||
} else if (page) {
|
||||
$input.focus();
|
||||
}
|
||||
},
|
||||
// Override default trigger for additional change/select logic
|
||||
_trigger: function (type, event, data) {
|
||||
var $input = $(this.element),
|
||||
self = this;
|
||||
if (type === "select") {
|
||||
$input.removeClass('ui-state-error');
|
||||
if (data.item.page) {
|
||||
$input.data("page", data.item.page);
|
||||
this._search(data.item.term);
|
||||
return false;
|
||||
}
|
||||
return this.select(data.item, event);
|
||||
} else if (type === "change") {
|
||||
$input.removeClass('ui-state-error');
|
||||
this._setHidden(data.item);
|
||||
if ($input.val() && !data.item) {
|
||||
if (!this.allowNew) {
|
||||
$input.addClass('ui-state-error');
|
||||
}
|
||||
}
|
||||
if (this.allowMultiple && !$input.hasClass('ui-state-error')) {
|
||||
$input.val("");
|
||||
this.term = "";
|
||||
}
|
||||
}
|
||||
// Call super trigger
|
||||
// This could be replaced by this._super() with jQuery UI 1.9
|
||||
return $.ui.autocomplete.prototype._trigger.apply(this, arguments);
|
||||
},
|
||||
close: function (event) {
|
||||
var page = $(this.element).data('page');
|
||||
if (page !== null) {
|
||||
return;
|
||||
}
|
||||
// Call super trigger
|
||||
// This could be replaced by this._super() with jQuery UI 1.9
|
||||
return $.ui.autocomplete.prototype.close.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
window.bindSelectables = function (context) {
|
||||
/* Bind all selectable widgets in a given context.
|
||||
Automatically called on document.ready.
|
||||
Additional calls can be made for dynamically added widgets.
|
||||
*/
|
||||
$(":input[data-selectable-type=text]", context).djselectable();
|
||||
$(":input[data-selectable-type=combobox]", context).djselectable();
|
||||
};
|
||||
|
||||
function djangoAdminPatches() {
|
||||
/* Listen for new rows being added to the dynamic inlines.
|
||||
Requires Django 1.5+ */
|
||||
$('body').on('click', '.add-row', function (e) {
|
||||
var wrapper = $(this).parents('.inline-related'),
|
||||
newRow = $('.form-row:not(.empty-form)', wrapper).last();
|
||||
window.bindSelectables(newRow);
|
||||
});
|
||||
|
||||
/* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
|
||||
if (typeof(dismissAddAnotherPopup) !== "undefined" &&
|
||||
typeof(windowname_to_id) !== "undefined" &&
|
||||
typeof(html_unescape) !== "undefined") {
|
||||
var django_dismissAddAnotherPopup = dismissAddAnotherPopup;
|
||||
dismissAddAnotherPopup = function (win, newId, newRepr) {
|
||||
/* See if the popup came from a selectable field.
|
||||
If not, pass control to Django's code.
|
||||
If so, handle it. */
|
||||
var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */
|
||||
var field = $('#' + fieldName);
|
||||
var multiField = $('#' + fieldName + '_0');
|
||||
/* Check for bound selectable */
|
||||
var singleWidget = field.data('djselectable');
|
||||
var multiWidget = multiField.data('djselectable');
|
||||
if (singleWidget || multiWidget) {
|
||||
// newId and newRepr are expected to have previously been escaped by
|
||||
// django.utils.html.escape.
|
||||
var item = {
|
||||
id: html_unescape(newId),
|
||||
value: html_unescape(newRepr)
|
||||
};
|
||||
if (singleWidget) {
|
||||
field.djselectable('select', item);
|
||||
}
|
||||
if (multiWidget) {
|
||||
multiField.djselectable('select', item);
|
||||
}
|
||||
win.close();
|
||||
} else {
|
||||
/* Not ours, pass on to original function. */
|
||||
return django_dismissAddAnotherPopup(win, newId, newRepr);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Patch the django admin JS
|
||||
if (typeof(djselectableAdminPatch) === "undefined" || djselectableAdminPatch) {
|
||||
djangoAdminPatches();
|
||||
}
|
||||
// Bind existing widgets on document ready
|
||||
if (typeof(djselectableAutoLoad) === "undefined" || djselectableAutoLoad) {
|
||||
window.bindSelectables('body');
|
||||
}
|
||||
});
|
||||
})(jQuery || grp.jQuery);
|
Reference in New Issue
Block a user