mirror of
https://github.com/postgres/pgweb.git
synced 2025-07-25 16:02:27 +00:00
391 lines
16 KiB
JavaScript
391 lines
16 KiB
JavaScript
/*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);
|