1
0
mirror of https://github.com/vrana/adminer.git synced 2025-08-31 01:59:59 +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"; echo "</table>\n";
} }
/** Print columns box in select /**
* @param array result of selectColumnsProcess()[0] * Prints columns box in select filter.
* @param array selectable columns *
* @return null * @param array $select result of selectColumnsProcess()[0]
*/ * @param array $columns selectable columns
function selectColumnsPrint($select, $columns) { */
function selectColumnsPrint(array $select, array $columns) {
global $functions, $grouping; global $functions, $grouping;
print_fieldset("select", lang('Select'), $select);
print_fieldset("select", lang('Select'), $select, true);
$_GET["columns"][""] = [];
$i = 0; $i = 0;
$select[""] = array();
foreach ($select as $key => $val) { foreach ($_GET["columns"] as $key => $val) {
$val = $_GET["columns"][$key]; if ($key != "" && $val["col"] == "") continue;
$column = select_input( $column = select_input(
" name='columns[$i][col]'", "name='columns[$i][col]'",
$columns, $columns,
$val["col"], $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>" echo "<div ", ($key != "" ? "" : "class='no-sort'"), ">",
. on_help("getTarget(event).value && getTarget(event).value.replace(/ |\$/, '(') + ')'", 1) "<span class='jsonly handle'>=</span>";
. script("qsl('select').onchange = function () { helpClose();" . ($key !== "" ? "" : " qsl('select, input', this.parentNode).onchange();") . " };", "")
. "($column)" : $column) if ($functions || $grouping) {
. " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>" echo "<select name='columns[$i][fun]'>",
. script('qsl(".icon").onclick = selectRemoveRow;', "") optionlist([-1 => ""] + array_filter([lang('Functions') => $functions, lang('Aggregation') => $grouping]), $val["fun"]),
. "</div>\n"; "</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++; $i++;
} }
echo "</div></fieldset>\n";
echo "</div>", script("initSortable('#fieldset-select');"), "</fieldset>\n";
} }
/** Print search box in select /**
* @param array result of selectSearchProcess() * Prints search box in select.
* @param array selectable columns *
* @param array * @param array $where result of selectSearchProcess()
* @return null * @param array $columns selectable columns
*/ */
function selectSearchPrint($where, $columns, $indexes) { function selectSearchPrint(array $where, array $columns, array $indexes) {
print_fieldset("search", lang('Search'), $where); print_fieldset("search", lang('Search'), $where);
foreach ($indexes as $i => $index) { foreach ($indexes as $i => $index) {
if ($index["type"] == "FULLTEXT") { if ($index["type"] == "FULLTEXT") {
echo "<div>(<i>" . implode("</i>, <i>", array_map('h', $index["columns"])) . "</i>) AGAINST"; 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]) . "'>"; " <input type='search' name='fulltext[$i]' value='" . h($_GET["fulltext"][$i]) . "'>",
echo script("qsl('input').oninput = selectFieldChange;", ""); script("qsl('input').oninput = selectFieldChange;", ""),
echo checkbox("boolean[$i]", 1, isset($_GET["boolean"][$i]), "BOOL"); checkbox("boolean[$i]", 1, isset($_GET["boolean"][$i]), "BOOL"),
echo "</div>\n"; "</div>\n";
} }
} }
$change_next = "this.parentNode.firstChild.onchange();"; $change_next = "this.parentNode.firstChild.onchange();";
foreach (array_merge((array) $_GET["where"], array(array())) as $i => $val) { foreach (array_merge((array) $_GET["where"], array(array())) as $i => $val) {
if (!$val || ("$val[col]$val[val]" != "" && in_array($val["op"], $this->operators))) { if (!$val || ("$val[col]$val[val]" != "" && in_array($val["op"], $this->operators))) {
echo "<div>" . select_input( echo "<div>",
" name='where[$i][col]'", select_input(
$columns, " name='where[$i][col]'",
$val["col"], $columns,
($val ? "selectFieldChange" : "selectAddRow"), $val["col"],
"(" . lang('anywhere') . ")" ($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"]) . "'>"; html_select("where[$i][op]", $this->operators, $val["op"], $change_next),
echo script("mixin(qsl('input'), {oninput: function () { $change_next }, onkeydown: selectSearchKeydown, onsearch: selectSearchSearch});", ""); "<input type='search' name='where[$i][val]' value='" . h($val["val"]) . "'>",
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>"; script("mixin(qsl('input'), {oninput: function () { $change_next }, onkeydown: selectSearchKeydown, onsearch: selectSearchSearch});", ""),
echo script('qsl(".icon").onclick = selectRemoveRow;', ""); " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon remove' title='" . h(lang('Remove')) . "' alt='x'>",
echo "</div>\n"; script('qsl("#fieldset-search .remove").onclick = selectRemoveRow;', ""),
"</div>\n";
} }
} }
echo "</div></fieldset>\n"; echo "</div></fieldset>\n";
} }
/** Print order box in select /**
* @param array result of selectOrderProcess() * Prints order box in select filter.
* @param array selectable columns *
* @param array * @param array $order result of selectOrderProcess()
* @return null * @param array $columns selectable columns
*/ */
function selectOrderPrint($order, $columns, $indexes) { function selectOrderPrint(array $order, array $columns, array $indexes) {
print_fieldset("sort", lang('Sort'), $order); print_fieldset("sort", lang('Sort'), $order, true);
$_GET["order"][""] = "";
$i = 0; $i = 0;
foreach ((array) $_GET["order"] as $key => $val) { foreach ((array) $_GET["order"] as $key => $val) {
if ($val != "") { if ($key != "" && $val == "") continue;
echo "<div>" . select_input(" name='order[$i]'", $columns, $val, "selectFieldChange");
echo checkbox("desc[$i]", 1, isset($_GET["desc"][$key]), lang('descending')); echo "<div ", ($key != "" ? "" : "class='no-sort'"), ">",
echo " <input type='image' src='../adminer/static/cross.gif' class='jsonly icon' title='" . h(lang('Remove')) . "' alt='x'>"; "<span class='jsonly handle'>=</span>",
echo script('qsl(".icon").onclick = selectRemoveRow;', ""); select_input("name='order[$i]'", $columns, $val, $key !== "" ? "selectFieldChange" : "selectAddRow"),
echo "</div>\n"; checkbox("desc[$i]", 1, isset($_GET["desc"][$key]), lang('descending')),
$i++; " <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 "</div>", script("initSortable('#fieldset-sort');"), "</fieldset>\n";
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";
} }
/** Print limit box in select /** 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 = "") { function select_input($attrs, $options, $value = "", $onchange = "", $placeholder = "") {
$tag = ($options ? "select" : "input"); $tag = ($options ? "select" : "input");
return "<$tag$attrs" . ($options return "<$tag $attrs" . ($options
? "><option value=''>$placeholder" . optionlist($options, $value, true) . "</select>" ? "><option value=''>$placeholder" . optionlist($options, $value, true) . "</select>"
: " size='10' value='" . h($value) . "' placeholder='$placeholder'>" : " size='10' value='" . h($value) . "' placeholder='$placeholder'>"
) . ($onchange ? script("qsl('$tag').onchange = $onchange;", "") : ""); //! use oninput for input ) . ($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 string * @param string
* @param bool * @param bool
* @return null
*/ */
function print_fieldset($id, $legend, $visible = false) { function print_fieldset($id, $legend, $visible = false, $sortable = false) {
echo "<fieldset><legend>"; echo "<fieldset><legend>";
echo "<a href='#fieldset-$id'>$legend</a>"; echo "<a href='#fieldset-$id'>$legend</a>";
echo script("qsl('a').onclick = partial(toggle, 'fieldset-$id');", ""); echo script("qsl('a').onclick = partial(toggle, 'fieldset-$id');", "");
echo "</legend>"; 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 /** 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; } .logout { margin-top: .5em; position: absolute; top: 0; right: 0; }
.loadmore { margin-left: 1ex; } .loadmore { margin-left: 1ex; }
.tables-filter { padding: .8em 1em 0; } .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 */ /* .edit used in designs */
#menu { position: absolute; margin: 10px 0 0; padding: 0 0 30px 0; top: 2em; left: 0; width: 19em; } #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; } #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; } #schema .references { position: absolute; }
#tables-filter, #database-select, #scheme-select { width: 100%; } #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; } #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 h2 { margin: 0 -18px 20px 0; }
.rtl p, .rtl table, .rtl .error, .rtl .message { margin: 1em 0 0 20px; } .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 /** Get first element by selector
* @param string * @param string
* @param [HTMLElement] defaults to document * @param [HTMLElement] defaults to document
@@ -73,13 +84,15 @@ function alterClass(el, className, enable) {
} }
} }
/** Toggle visibility /**
* @param string * Toggles visibility of element with ID.
* @return boolean false *
*/ * @param {string} id
* @return {boolean} Always false.
*/
function toggle(id) { function toggle(id) {
var el = qs('#' + id); gid(id).classList.toggle("hidden");
el.className = (el.className === 'hidden' ? '' : 'hidden');
return false; return false;
} }
@@ -450,14 +463,15 @@ function menuOut() {
/** /**
* Adds row in select fieldset. * Adds row in select fieldset.
* *
* @param {Event} event
* @this HTMLSelectElement * @this HTMLSelectElement
*/ */
function selectAddRow() { function selectAddRow(event) {
const field = this; const field = this;
const row = cloneNode(field.parentNode); const row = cloneNode(field.parentNode);
field.onchange = selectFieldChange; field.onchange = selectFieldChange;
field.onchange(); field.onchange(event);
const selects = qsa('select', row); const selects = qsa('select', row);
for (const select of selects) { for (const select of selects) {
@@ -486,13 +500,19 @@ function selectAddRow() {
button.onclick = selectRemoveRow; 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. * Removes a row in select fieldset.
* *
* @this HTMLInputElement * @this HTMLInputElement
* @return {boolean} Always false.
*/ */
function selectRemoveRow() { function selectRemoveRow() {
const row = this.parentNode; 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 /** Toggles column context menu

View File

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

View File

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