1
0
mirror of https://github.com/vrana/adminer.git synced 2025-08-29 17:19:52 +02:00

Add drag-n-drop moving of rows in table selection filter

This commit is contained in:
Peter Knut
2024-10-14 00:49:57 +02:00
parent 7997331b77
commit b9cdf52ec5
6 changed files with 228 additions and 91 deletions

View File

@@ -393,100 +393,123 @@ class Adminer {
echo "</table>\n";
}
/** Print columns box in select
* @param array result of selectColumnsProcess()[0]
* @param array selectable columns
* @return null
*/
function selectColumnsPrint($select, $columns) {
/**
* Prints columns box in select filter.
*
* @param array $select result of selectColumnsProcess()[0]
* @param array $columns selectable columns
*/
function selectColumnsPrint(array $select, array $columns) {
global $functions, $grouping;
print_fieldset("select", lang('Select'), $select);
print_fieldset("select", lang('Select'), $select, true);
$_GET["columns"][""] = [];
$i = 0;
$select[""] = array();
foreach ($select as $key => $val) {
$val = $_GET["columns"][$key];
foreach ($_GET["columns"] as $key => $val) {
if ($key != "" && $val["col"] == "") continue;
$column = select_input(
" name='columns[$i][col]'",
"name='columns[$i][col]'",
$columns,
$val["col"],
($key !== "" ? "selectFieldChange" : "selectAddRow")
$key !== "" ? "selectFieldChange" : "selectAddRow"
);
echo "<div>" . ($functions || $grouping ? "<select name='columns[$i][fun]'>"
. optionlist(array(-1 => "") + array_filter(array(lang('Functions') => $functions, lang('Aggregation') => $grouping)), $val["fun"]) . "</select>"
. on_help("getTarget(event).value && getTarget(event).value.replace(/ |\$/, '(') + ')'", 1)
. script("qsl('select').onchange = function () { helpClose();" . ($key !== "" ? "" : " qsl('select, input', this.parentNode).onchange();") . " };", "")
. "($column)" : $column)
. " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>"
. script('qsl(".icon").onclick = selectRemoveRow;', "")
. "</div>\n";
echo "<div ", ($key != "" ? "" : "class='no-sort'"), ">",
"<span class='jsonly handle'>=</span>";
if ($functions || $grouping) {
echo "<select name='columns[$i][fun]'>",
optionlist([-1 => ""] + array_filter([lang('Functions') => $functions, lang('Aggregation') => $grouping]), $val["fun"]),
"</select>",
on_help("getTarget(event).value && getTarget(event).value.replace(/ |\$/, '(') + ')'", 1),
script("qsl('select').onchange = (event) => { helpClose();" . ($key !== "" ? "" : " qsl('select, input:not(.remove)', event.target.parentNode).onchange();") . " };", ""),
"($column)";
} else {
echo $column;
}
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>",
script("qsl('#fieldset-select .remove').onclick = selectRemoveRow;", ""),
"</div>\n";
$i++;
}
echo "</div></fieldset>\n";
echo "</div>", script("initSortable('#fieldset-select');"), "</fieldset>\n";
}
/** Print search box in select
* @param array result of selectSearchProcess()
* @param array selectable columns
* @param array
* @return null
*/
function selectSearchPrint($where, $columns, $indexes) {
/**
* Prints search box in select.
*
* @param array $where result of selectSearchProcess()
* @param array $columns selectable columns
*/
function selectSearchPrint(array $where, array $columns, array $indexes) {
print_fieldset("search", lang('Search'), $where);
foreach ($indexes as $i => $index) {
if ($index["type"] == "FULLTEXT") {
echo "<div>(<i>" . implode("</i>, <i>", array_map('h', $index["columns"])) . "</i>) AGAINST";
echo " <input type='search' name='fulltext[$i]' value='" . h($_GET["fulltext"][$i]) . "'>";
echo script("qsl('input').oninput = selectFieldChange;", "");
echo checkbox("boolean[$i]", 1, isset($_GET["boolean"][$i]), "BOOL");
echo "</div>\n";
echo "<div>(<i>" . implode("</i>, <i>", array_map('h', $index["columns"])) . "</i>) AGAINST",
" <input type='search' name='fulltext[$i]' value='" . h($_GET["fulltext"][$i]) . "'>",
script("qsl('input').oninput = selectFieldChange;", ""),
checkbox("boolean[$i]", 1, isset($_GET["boolean"][$i]), "BOOL"),
"</div>\n";
}
}
$change_next = "this.parentNode.firstChild.onchange();";
foreach (array_merge((array) $_GET["where"], array(array())) as $i => $val) {
if (!$val || ("$val[col]$val[val]" != "" && in_array($val["op"], $this->operators))) {
echo "<div>" . select_input(
" name='where[$i][col]'",
$columns,
$val["col"],
($val ? "selectFieldChange" : "selectAddRow"),
"(" . lang('anywhere') . ")"
);
echo html_select("where[$i][op]", $this->operators, $val["op"], $change_next);
echo "<input type='search' name='where[$i][val]' value='" . h($val["val"]) . "'>";
echo script("mixin(qsl('input'), {oninput: function () { $change_next }, onkeydown: selectSearchKeydown, onsearch: selectSearchSearch});", "");
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>";
echo script('qsl(".icon").onclick = selectRemoveRow;', "");
echo "</div>\n";
echo "<div>",
select_input(
" name='where[$i][col]'",
$columns,
$val["col"],
($val ? "selectFieldChange" : "selectAddRow"),
"(" . lang('anywhere') . ")"
),
html_select("where[$i][op]", $this->operators, $val["op"], $change_next),
"<input type='search' name='where[$i][val]' value='" . h($val["val"]) . "'>",
script("mixin(qsl('input'), {oninput: function () { $change_next }, onkeydown: selectSearchKeydown, onsearch: selectSearchSearch});", ""),
" <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>",
script('qsl("#fieldset-search .remove").onclick = selectRemoveRow;', ""),
"</div>\n";
}
}
echo "</div></fieldset>\n";
}
/** Print order box in select
* @param array result of selectOrderProcess()
* @param array selectable columns
* @param array
* @return null
*/
function selectOrderPrint($order, $columns, $indexes) {
print_fieldset("sort", lang('Sort'), $order);
/**
* Prints order box in select filter.
*
* @param array $order result of selectOrderProcess()
* @param array $columns selectable columns
*/
function selectOrderPrint(array $order, array $columns, array $indexes) {
print_fieldset("sort", lang('Sort'), $order, true);
$_GET["order"][""] = "";
$i = 0;
foreach ((array) $_GET["order"] as $key => $val) {
if ($val != "") {
echo "<div>" . select_input(" name='order[$i]'", $columns, $val, "selectFieldChange");
echo checkbox("desc[$i]", 1, isset($_GET["desc"][$key]), lang('descending'));
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>";
echo script('qsl(".icon").onclick = selectRemoveRow;', "");
echo "</div>\n";
$i++;
}
if ($key != "" && $val == "") continue;
echo "<div ", ($key != "" ? "" : "class='no-sort'"), ">",
"<span class='jsonly handle'>=</span>",
select_input("name='order[$i]'", $columns, $val, $key !== "" ? "selectFieldChange" : "selectAddRow"),
checkbox("desc[$i]", 1, isset($_GET["desc"][$key]), lang('descending')),
" <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>",
script('qsl("#fieldset-sort .remove").onclick = selectRemoveRow;', ""),
"</div>\n";
$i++;
}
echo "<div>" . select_input(" name='order[$i]'", $columns, "", "selectAddRow");
echo checkbox("desc[$i]", 1, false, lang('descending'));
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>";
echo script('qsl(".icon").onclick = selectRemoveRow;', "");
echo "</div>\n";
echo "</div></fieldset>\n";
echo "</div>", script("initSortable('#fieldset-sort');"), "</fieldset>\n";
}
/** Print limit box in select

View File

@@ -235,7 +235,7 @@ function html_select($name, $options, $value = "", $onchange = true, $labelled_b
*/
function select_input($attrs, $options, $value = "", $onchange = "", $placeholder = "") {
$tag = ($options ? "select" : "input");
return "<$tag$attrs" . ($options
return "<$tag $attrs" . ($options
? "><option value=''>$placeholder" . optionlist($options, $value, true) . "</select>"
: " size='10' value='" . h($value) . "' placeholder='$placeholder'>"
) . ($onchange ? script("qsl('$tag').onchange = $onchange;", "") : ""); //! use oninput for input
@@ -254,14 +254,13 @@ function confirm($message = "", $selector = "qsl('input')") {
* @param string
* @param string
* @param bool
* @return null
*/
function print_fieldset($id, $legend, $visible = false) {
function print_fieldset($id, $legend, $visible = false, $sortable = false) {
echo "<fieldset><legend>";
echo "<a href='#fieldset-$id'>$legend</a>";
echo script("qsl('a').onclick = partial(toggle, 'fieldset-$id');", "");
echo "</legend>";
echo "<div id='fieldset-$id'" . ($visible ? "" : " class='hidden'") . ">\n";
echo "<div id='fieldset-$id' class='" . ($visible ? "" : "hidden") . ($sortable ? " sortable" : "") . "'>\n";
}
/** Return class='active' if $bold is true

View File

@@ -71,6 +71,12 @@ input::placeholder { color: #000; opacity: 0.4; }
.logout { margin-top: .5em; position: absolute; top: 0; right: 0; }
.loadmore { margin-left: 1ex; }
.tables-filter { padding: .8em 1em 0; }
.handle { display: inline-block; width: 18px; height: 18px; padding-right: 5px; vertical-align: middle; overflow: hidden; font-size: 130%; text-align: center; line-height: 16px; opacity: 0.2; cursor: grab; }
.sortable { position: relative; }
.sortable .no-sort .handle { opacity: 0; cursor: default; }
.placeholder { opacity: 0; }
.dragging { position: absolute; z-index: 1; }
.dragging .handle { cursor: grabbing; }
/* .edit used in designs */
#menu { position: absolute; margin: 10px 0 0; padding: 0 0 30px 0; top: 2em; left: 0; width: 19em; }
#menu p, #logins, #tables { padding: .8em 1em; margin: 0; border-bottom: 1px solid #ccc; }
@@ -88,7 +94,7 @@ input::placeholder { color: #000; opacity: 0.4; }
#schema .references { position: absolute; }
#tables-filter, #database-select, #scheme-select { width: 100%; }
#help { position: absolute; border: 1px solid #999; background: #eee; padding: 5px; font-family: monospace; z-index: 1; }
#fieldset-select div:last-child > .icon, #fieldset-search div:last-child > .icon, #fieldset-sort div:last-child > .icon { display: none; }
#fieldset-select div:last-child > .remove, #fieldset-search div:last-child > .remove, #fieldset-sort div:last-child > .remove { display: none; }
.rtl h2 { margin: 0 -18px 20px 0; }
.rtl p, .rtl table, .rtl .error, .rtl .message { margin: 1em 0 0 20px; }

View File

@@ -1,4 +1,15 @@
/**
* Returns the element found by given identifier.
*
* @param {string} id
* @param {?HTMLElement} context Defaults to document.
* @return {?HTMLElement}
*/
function gid(id, context = null) {
return (context || document).getElementById(id);
}
/** Get first element by selector
* @param string
* @param [HTMLElement] defaults to document
@@ -73,13 +84,15 @@ function alterClass(el, className, enable) {
}
}
/** Toggle visibility
* @param string
* @return boolean false
*/
/**
* Toggles visibility of element with ID.
*
* @param {string} id
* @return {boolean} Always false.
*/
function toggle(id) {
var el = qs('#' + id);
el.className = (el.className === 'hidden' ? '' : 'hidden');
gid(id).classList.toggle("hidden");
return false;
}
@@ -450,14 +463,15 @@ function menuOut() {
/**
* Adds row in select fieldset.
*
* @param {Event} event
* @this HTMLSelectElement
*/
function selectAddRow() {
function selectAddRow(event) {
const field = this;
const row = cloneNode(field.parentNode);
field.onchange = selectFieldChange;
field.onchange();
field.onchange(event);
const selects = qsa('select', row);
for (const select of selects) {
@@ -486,13 +500,19 @@ function selectAddRow() {
button.onclick = selectRemoveRow;
}
field.parentNode.parentNode.appendChild(row);
const parent = field.parentNode.parentNode;
if (parent.classList.contains("sortable")) {
initSortableRow(field.parentElement);
}
parent.appendChild(row);
}
/**
* Removes a row in select fieldset.
*
* @this HTMLInputElement
* @return {boolean} Always false.
*/
function selectRemoveRow() {
const row = this.parentNode;
@@ -522,6 +542,95 @@ function selectSearchSearch() {
}
}
// Sorting.
(function() {
let placeholderRow, draggingRow, nextRow;
let startY, maxY;
/**
* Initializes sortable list of DIV elements.
*
* @param {string} parentId
*/
window.initSortable = function(parentSelector) {
const parent = qs(parentSelector);
if (!parent) return;
for (const row of parent.children) {
if (!row.classList.contains("no-sort")) {
initSortableRow(row);
}
}
};
/**
* Initializes one row of sortable parent.
*
* @param {HTMLElement} row
*/
window.initSortableRow = function(row) {
row.classList.remove("no-sort");
const handle = qs(".handle", row);
handle.addEventListener("mousedown", (event) => {
event.preventDefault();
const parent = row.parentNode;
startY = event.clientY - row.offsetTop;
maxY = parent.offsetHeight - row.offsetHeight;
placeholderRow = row.cloneNode(true);
placeholderRow.classList.add("placeholder");
placeholderRow.style.top = (event.clientY - startY) + "px";
parent.insertBefore(placeholderRow, row);
draggingRow = row;
draggingRow.classList.add("dragging");
parent.insertBefore(draggingRow, parent.firstChild);
nextRow = placeholderRow.nextElementSibling;
updateSorting(event);
window.addEventListener("mousemove", updateSorting);
window.addEventListener("mouseup", () => {
draggingRow.classList.remove("dragging");
parent.insertBefore(draggingRow, placeholderRow);
placeholderRow.remove();
window.removeEventListener("mousemove", updateSorting);
}, { once: true });
});
};
function updateSorting(event) {
let top = Math.min(Math.max(event.clientY - startY, 0), maxY);
draggingRow.style.top = top + "px";
let sibling;
if (top > placeholderRow.offsetTop + placeholderRow.offsetHeight / 2) {
sibling = !nextRow.classList.contains("no-sort") ? nextRow.nextElementSibling : nextRow;
} else if (top + placeholderRow.offsetHeight < placeholderRow.offsetTop + placeholderRow.offsetHeight / 2) {
sibling = placeholderRow.previousElementSibling;
} else {
sibling = nextRow;
}
if (sibling !== nextRow) {
const parent = placeholderRow.parentNode;
nextRow = sibling;
if (sibling) {
parent.insertBefore(placeholderRow, nextRow);
} else {
parent.appendChild(placeholderRow);
}
}
}
})();
/** Toggles column context menu

View File

@@ -224,11 +224,11 @@ ORDER BY ORDINAL_POSITION", null, "") as $row) { //! requires MySQL 5
return $val;
}
function selectColumnsPrint($select, $columns) {
function selectColumnsPrint(array $select, array $columns) {
// can allow grouping functions by indexes
}
function selectSearchPrint($where, $columns, $indexes) {
function selectSearchPrint(array $where, array $columns, array $indexes) {
$where = (array) $_GET["where"];
echo '<fieldset id="fieldset-search"><legend>' . lang('Search') . "</legend><div>\n";
$keys = array();
@@ -265,7 +265,7 @@ ORDER BY ORDINAL_POSITION", null, "") as $row) { //! requires MySQL 5
echo "<div><select name='where[$i][col]'><option value=''>(" . lang('anywhere') . ")" . optionlist($columns, $val["col"], true) . "</select>";
echo html_select("where[$i][op]", array(-1 => "") + $this->operators, $val["op"]);
echo "<input type='search' name='where[$i][val]' value='" . h($val["val"]) . "'>" . script("mixin(qsl('input'), {onkeydown: selectSearchKeydown, onsearch: selectSearchSearch});", "");
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>" . script('qsl(".icon").onclick = selectRemoveRow;', "");
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>" . script('qsl("#fieldset-search .remove").onclick = selectRemoveRow;', "");
echo "</div>\n";
$i++;
}
@@ -275,13 +275,13 @@ ORDER BY ORDINAL_POSITION", null, "") as $row) { //! requires MySQL 5
echo html_select("where[$i][op]", array(-1 => "") + $this->operators);
echo "<input type='search' name='where[$i][val]'>";
echo script("mixin(qsl('input'), {onchange: function () { this.parentNode.firstChild.onchange(); }, onsearch: selectSearchSearch});");
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>";
echo script('qsl(".icon").onclick = selectRemoveRow;', "");
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>";
echo script('qsl("#fieldset-search .remove").onclick = selectRemoveRow;', "");
echo "</div>";
echo "</div></fieldset>\n";
}
function selectOrderPrint($order, $columns, $indexes) {
function selectOrderPrint(array $order, array $columns, array $indexes) {
//! desc
$orders = array();
foreach ($indexes as $key => $index) {

View File

@@ -250,17 +250,17 @@ class AdminerPlugin extends Adminer {
return $this->_applyPlugin(__FUNCTION__, $args);
}
function selectColumnsPrint($select, $columns) {
function selectColumnsPrint(array $select, array $columns) {
$args = func_get_args();
return $this->_applyPlugin(__FUNCTION__, $args);
}
function selectSearchPrint($where, $columns, $indexes) {
function selectSearchPrint(array $where, array $columns, array $indexes) {
$args = func_get_args();
return $this->_applyPlugin(__FUNCTION__, $args);
}
function selectOrderPrint($order, $columns, $indexes) {
function selectOrderPrint(array $order, array $columns, array $indexes) {
$args = func_get_args();
return $this->_applyPlugin(__FUNCTION__, $args);
}