trac/htdocs/js/query.js
// Create and modify custom queries (incl. support for batch modification)
(function($){
// Create a <label>
function createLabel(text, htmlFor) {
var label = $($.htmlFormat("<label>$1</label>", text));
if (htmlFor)
label.attr("for", htmlFor).addClass("control");
return label;
}
function createTextHtml(name, size) {
return $.htmlFormat('<input type="text" name="$1" size="$2" />',
name, size);
}
// Create an <input type="text">
function createText(name, size) {
return $(createTextHtml(name, size));
}
// Create an <input type="checkbox">
function createCheckbox(name, value, id) {
return $($.htmlFormat('<input type="checkbox" id="$1" name="$2"' +
' value="$3">', id, name, value));
}
// Create an <input type="radio">
function createRadio(name, value, id) {
// Workaround for IE, otherwise the radio buttons are not selectable
return $($.htmlFormat('<input type="radio" id="$1" name="$2"' +
' value="$3">', id, name, value));
}
// Append a list of <option> to an element
function appendOptions(e, options) {
for (var i = 0; i < options.length; i++) {
var opt = options[i], v = opt, t = opt;
if (typeof opt == "object")
v = opt.value, t = opt.name;
$($.htmlFormat('<option value="$1">$2</option>', v, t)).appendTo(e);
}
}
// Create a <select>
function createSelect(name, options, optional, optgroups) {
var e = $($.htmlFormat('<select name="$1">', name));
if (optional)
$("<option>").appendTo(e);
appendOptions(e, options);
if (optgroups) {
for (var i = 0; i < optgroups.length; i++) {
var grp = optgroups[i];
var optgrp = $($.htmlFormat('<optgroup label="$1">', grp.label));
appendOptions(optgrp, grp.options);
optgrp.appendTo(e);
}
}
return e;
}
window.initializeFilters = function() {
// Remove an existing row from the filters table
function removeRow(button, propertyName) {
var m = propertyName.match(/^(\d+)_(.*)$/);
var clauseNum = m[1], field = m[2];
var tr = $(button).closest("tr");
// Keep the filter label when removing the first row
var label = $("#label_" + propertyName);
if (label.length && (label.closest("tr")[0] == tr[0])) {
var next = tr.next("." + field);
if (next.length) {
var thisTh = tr.children().eq(1);
var nextTh = next.children().eq(1);
if (nextTh.attr("colSpan") == 1) {
nextTh.replaceWith(thisTh);
} else {
nextTh.attr("colSpan", 1).before(thisTh);
next.children().eq(2).replaceWith(tr.children().eq(1));
}
}
}
// Remove the row, filter tbody or clause tbody
var tbody = tr.closest("tbody");
if (tbody.children("tr").length > 1) {
tr.remove();
} else {
var table = tbody.closest("table.trac-clause");
var ctbody = table.closest("tbody");
if (table.children().length > 2 || !ctbody.siblings().length) {
tbody.remove();
if (!ctbody.siblings().length && table.children().length == 1) {
$("#add_clause").attr("disabled", true);
}
} else {
var add_clause = $("#add_clause", ctbody);
if (add_clause.length)
$("tr.actions td.and", ctbody.prev()).attr("colSpan", 2)
.after(add_clause.closest("td"));
if (ctbody.prev().length == 0)
ctbody.next().children("tr:first").attr("style", "display: none");
ctbody.remove();
return;
}
}
// Re-enable non-multiline filter
$("#add_filter_" + clauseNum + " option[value='" + field + "']")
.enable();
}
var $filters = $("#filters");
// Invert or flip checkboxes in the row
$filters.on("click", "th label", function(event) {
var name = this.id.slice(6);
var $checkboxes = $filters.find("input[name=" + name + "]")
.filter(":checkbox");
if (event.metaKey || event.altKey) {
$checkboxes.each(function() {
$(this).prop("checked", !this.checked)
});
}
else {
var numChecked = $checkboxes.filter(":checked").length;
var allChecked = numChecked == $checkboxes.length;
$checkboxes.each(function() { $(this).prop("checked", !allChecked) });
}
return false;
});
// Select only clicked checkbox and clear others
$filters.exclusiveOnClick("td :checkbox, td :checkbox + label");
$("fieldset#columns").exclusiveOnClick("label");
// Make the submit buttons for removing filters client-side triggers
$filters.find("input[type='submit'][name^='rm_filter_']").each(function() {
var idx = this.name.search(/_\d+$/);
if (idx < 0)
idx = this.name.length;
var propertyName = this.name.substring(10, idx);
$(this).replaceWith(
$($.htmlFormat('<input type="button" value="$1">', this.value))
.click(function() {
removeRow(this, propertyName);
return false;
}));
});
// Make the drop-down menu for adding a filter a client-side trigger
$filters.find("select[name^=add_filter_]").change(function() {
if (this.selectedIndex < 1)
return;
if (this.options[this.selectedIndex].disabled) {
// IE doesn't support disabled options
alert(_("A filter already exists for that property"));
this.selectedIndex = 0;
return;
}
var propertyName = this.options[this.selectedIndex].value;
var property = properties[propertyName];
var table = $(this).closest("table.trac-clause")[0];
var tbody = $("tr." + propertyName, table).closest("tbody").eq(0);
var tr = $("<tr>").addClass(propertyName);
var clauseNum = $(this).attr("name").split("_").pop();
propertyName = clauseNum + "_" + propertyName;
// Add the remove button
tr.append($('<td>')
.append($('<div class="inlinebuttons">')
.append($('<input type="button" value="–">')
.click(function() { removeRow(this, propertyName); }))));
// Add the row header
var th = $('<th scope="row">');
if (!tbody.length) {
th.append(createLabel(property.label)
.attr("id", "label_" + propertyName));
} else {
th.attr("colSpan", property.type == "time"? 1: 2)
.append(createLabel(_("or")))
}
tr.append(th);
var td = $("<td>");
var focusElement = null;
if (property.type == "radio" || property.type == "checkbox"
|| property.type == "time") {
td.addClass("filter").attr("colSpan", 2);
if (property.type == "radio") {
for (var i = 0; i < property.options.length; i++) {
var option = property.options[i];
var control = createCheckbox(propertyName, option,
propertyName + "_" + option);
if (i == 0)
focusElement = control;
td.append(control).append(" ")
.append(createLabel(option ? option : "none",
propertyName + "_" + option)).append(" ");
}
} else if (property.type == "checkbox") {
focusElement = createRadio(propertyName, "1", propertyName + "_on");
td.append(focusElement)
.append(" ").append(createLabel(_("yes"), propertyName + "_on"))
.append(" ")
.append(createRadio(propertyName, "0", propertyName + "_off"))
.append(" ").append(createLabel(_("no"), propertyName + "_off"));
} else if (property.type == "time") {
var startElement = createTextHtml(propertyName, 14);
var endElement = createTextHtml(propertyName + "_end", 14);
td.append($.parseHTML(
_("<label>between %(start)s</label> <label>and %(end)s</label>",
{start: startElement, end: endElement})));
focusElement = td.find('input[name="' + propertyName + '"]');
if (property.format == "datetime") {
td.find('input').datetimepicker();
} else if (property.format == "date" ||
property.format == "relative") {
td.find('input').datepicker();
}
}
tr.append(td);
} else {
if (!tbody.length) {
// Add the mode selector
td.addClass("mode")
.append(createSelect(propertyName + "_mode", modes[property.type]))
.appendTo(tr);
}
// Add the selector or text input for the actual filter value
td = $("<td>").addClass("filter");
if (property.type == "select") {
focusElement = createSelect(propertyName, property.options,
property.optional, property.optgroups);
} else if ((property.type == "text") || (property.type == "id")
|| (property.type == "textarea")) {
focusElement = createText(propertyName, 42);
}
td.append(focusElement).appendTo(tr);
}
if (!tbody.length) {
tbody = $("<tbody>");
// Find the insertion point for the new row. We try to keep the filter
// rows in the same order as the options in the 'Add filter' drop-down,
// because that's the order they'll appear in when submitted
var insertionPoint = $(this).closest("tbody");
outer:
for (var i = this.selectedIndex + 1; i < this.options.length; i++) {
for (var j = 0; j < table.tBodies.length; j++) {
if (table.tBodies[j].rows[0].className == this.options[i].value) {
insertionPoint = $(table.tBodies[j]);
break outer;
}
}
}
insertionPoint.before(tbody);
}
tbody.append(tr);
if(focusElement)
focusElement.focus();
// Disable the add filter in the drop-down list
if (property.type == "radio" || property.type == "checkbox"
|| property.type == "id")
this.options[this.selectedIndex].disabled = true;
this.selectedIndex = 0;
// Enable the Or... button if it's been disabled
$("#add_clause").attr("disabled", false);
}).next("div.inlinebuttons").remove();
// Add a new empty clause at the end by cloning the current last clause
function addClause(select) {
var clauseNum = parseInt($(select).attr("name").split("_").pop());
var tbody = $(select).closest("tbody").parents("tbody").eq(0);
var copy = tbody.clone(true);
$(select).closest("td").next().attr("colSpan", 4).end().remove();
$("tr:first", copy).removeAttr("style");
$("tr tbody:not(:last)", copy).remove();
var newId = "add_filter_" + clauseNum;
$("select[name^=add_filter_]", copy).attr("id", newId)
.attr("name", newId)
.children().enable().end()
.prev().attr("for", newId);
$("select[name^=add_clause_]", copy)
.attr("name", "add_clause_" + (clauseNum + 1));
tbody.after(copy);
}
var add_clause = $("#add_clause");
add_clause.change(function() {
// Add a new clause and fire a change event on the new clause's
// add_filter select.
var field = $(this).val();
addClause(this);
$("#add_clause").closest("tr").find("select[name^=add_filter_]")
.val(field).change();
}).next("div.inlinebuttons").remove();
if (!add_clause.closest("tbody").siblings().length) {
// That is, if there are no filters added to this clause
add_clause.attr("disabled", true);
}
};
window.initializeBatch = function() {
// Create the appropriate input for the property.
function createBatchInput(propertyName, property) {
var td = $('<td class="batchmod_property">');
var inputName = getBatchInputName(propertyName);
var focusElement = null;
switch (property.type) {
case 'select':
focusElement = createSelect(inputName, property.options,
property.optional, property.optgroups);
focusElement.attr("id", inputName);
td.append(focusElement);
break;
case 'radio':
for (var i = 0; i < property.options.length; i++) {
var option = property.options[i];
var control = createRadio(inputName, option,
inputName + "_" + option);
if (i == 0)
focusElement = control;
td.append(control).append(" ")
.append(createLabel(option ? option : "none",
inputName + "_" + option))
.append(" ");
}
break;
case 'checkbox':
focusElement = createRadio(inputName, "1", inputName + "_on");
td.append(focusElement)
.append(" ").append(createLabel(_("yes"), inputName + "_on"))
.append(" ")
.append(createRadio(inputName, "0", inputName + "_off"))
.append(" ").append(createLabel(_("no"), inputName + "_off"));
break;
case 'text':
if ($.inArray(propertyName, batch_list_properties) >= 0) {
focusElement = appendBatchListControls(td, propertyName,
inputName);
} else {
focusElement = createText(inputName, 42).attr("id", inputName);
td.append(focusElement);
}
break;
case 'time':
focusElement = createText(inputName, 42).addClass("time")
.attr("id", inputName);
if (property.format == "datetime") {
focusElement.datetimepicker();
} else if (property.format == "date") {
focusElement.datepicker();
}
td.append(focusElement);
break;
}
return [td, focusElement];
}
function appendBatchListControls(td, propertyName, inputName) {
var modeSelect = createSelect("batchmod_mode_" + propertyName,
batch_list_modes).attr("id", inputName);
var name1 = "batchmod_primary_" + propertyName;
var name2 = "batchmod_secondary_" + propertyName;
var text1 = createText(name1, 42).attr("id", name1);
var text2 = createText(name2, 42).attr("id", name2);
var label1 = createLabel(" " + batch_list_modes[0]['name'] + ":", name1);
var label2 = createLabel(_(" remove:"), name2);
td.append(modeSelect);
td.append(label1);
td.append(text1);
// Only mode 2 ("Add / remove") has a second text field:
$(modeSelect).change(function() {
if (this.selectedIndex == 2) {
td.append(label2);
td.append(text2);
label1.text(_(" add:"));
} else {
if (td.has(text2).length) {
text2.remove();
label2.remove();
}
label1.text(" " + batch_list_modes[this.selectedIndex]['name'] +
":"); // FIXME: french l10n
}
});
return text1;
}
function getBatchInputName(propertyName) {
return 'batchmod_value_' + propertyName;
}
function getDisabledBatchOptions() {
return $("#add_batchmod_field option:disabled");
}
var $table = $("table.listing");
// Add a new column with checkboxes for each ticket.
// Selecting a ticket marks it for inclusion in the batch.
var $results_cells = $(".trac-query-results tr td.id", $table);
if ($results_cells.length > 0) {
$(".trac-query-heading tr th.id", $table).before($('<th class="sel">'));
$results_cells.each(function() {
var tId = $(this).text().substring(1);
$(this).before(
$('<td class="sel">').append(
$('<input type="checkbox" name="selected_ticket" />').attr({
title: babel.format(_("Select ticket #%(id)s for modification"), {id: tId}),
value: tId
})
)
);
});
$(".trac-query-summary tr td.id", $table).before($('<td>'));
$("tbody tr .trac-colspan", $table).each(function () {
$(this).attr('colspan', $(this).attr('colspan') + 1);
});
}
// Add a Select All checkbox at the top of the column.
$table.addSelectAllCheckboxes();
// Prevent submit of form if required items are not selected.
$("#batchmod_submit").disableSubmit("input[name='selected_ticket']")
.click(function() {
var valid = true;
// Remove existing validation messages.
$(".batchmod_required").remove();
// Check that each radio property has something selected.
getDisabledBatchOptions().each(function() {
var propertyName = $(this).val();
if (properties[propertyName].type == "radio") {
var isChecked = false;
var inputName = getBatchInputName(propertyName);
$("[name=" + inputName + "]").each(function() {
isChecked = isChecked || $(this).is(':checked');
});
if (!isChecked) {
// Select the last label in the row to add the error message.
$("[name=" + inputName + "] ~ label:last")
.after('<span class="batchmod_required">Required</span>');
valid = false;
}
}
});
return valid;
});
// Create an array of selected items on form submit.
$("form#batchmod_form").submit(function() {
var selectedTix = [];
$("input[name=selected_ticket]:checked").each(function() {
selectedTix.push(this.value);
});
$("input[name=selected_tickets]").val(selectedTix);
});
// Collapse the form by default
$("#batchmod_fieldset").toggleClass("collapsed");
// Add the new batch modify field when the user selects one from the
// dropdown.
$("#add_batchmod_field").change(function() {
if (this.selectedIndex < 1) {
return;
}
// Trac has a properties object that has information about each property
// that filters could be built with. We use it here to add batchmod
// fields.
var propertyName = this.options[this.selectedIndex].value;
var property = properties[propertyName];
var tr = $("<tr>").attr('id', 'batchmod_' + propertyName);
// Add the remove button
tr.append($('<td>')
.append($('<div class="inlinebuttons">')
.append($('<input type="button" value="–">')
.click(function() {
$('#batchmod_' + propertyName).remove();
$($.htmlFormat("#add_batchmod_field option[value='$1']",
propertyName)).enable();
})
)
)
);
// Add the header row.
tr.append($('<th scope="row">')
.append(createLabel(property.label + ":", // FIXME: french l10n
getBatchInputName(propertyName)))
);
// Add the input element.
var batchInput = createBatchInput(propertyName, property);
tr.append(batchInput[0]);
// New rows are added in the same order as listed in the dropdown.
// This is the same behavior as the filters.
var insertionPoint = null;
for (var i = this.selectedIndex + 1; i < this.options.length; i++) {
if (insertionPoint === null && this.options[i].disabled) {
insertionPoint = $("#batchmod_" + this.options[i].value);
}
}
if (insertionPoint === null) {
insertionPoint = $("#add_batchmod_field_row");
}
insertionPoint.before(tr);
// Disable each element from the option list when it is selected.
this.options[this.selectedIndex].disabled = true;
if (batchInput[1])
batchInput[1].focus();
this.selectedIndex = 0;
});
// Make the form visible
$("#batchmod_form").show();
}
})(jQuery);