Commit 4c365887 authored by Carsten  Rose's avatar Carsten Rose
Browse files

#4918 / Drag'n'Drop reorder elements

DRAGANDDROP.md, PROTOCOL.md: Doc for "drag'n' drop" implementation.
dragAndDrop.php: API endpoint
DragAndDrop.php: Class for implementing drag'n' drop functionality.
Link.php: implement new renderMode=8 - returning only the sip.
QuickFormQuery.php: New entry point for processing "drag'n' drop".
parent a0e682ea
# Drag And Drop
## Sort
Initialize a dnd container by adding the class "qfq-dnd"
Set container object class to `class="qfq-dnd qfq-dnd-sort"`.
Add the data elements: `data-dnd-api="url"` and `data-dnd-key="key"`
For the children inside of the container (just the first children):
add `data-dnd-id` to a reference you can handle (probably record id).
Request will be sent containing following GET variables:
* dragId = `data-dnd-id` of the dragged object,
* dragPosition = client internal old position of the dragged object.
* setTo = "after" or "before",
* hoverId = `data-dnd-id` id of the element the dragged element is now hovering, meaning before or after.
* hoverPosition = client internal position of currently hovered element.
......@@ -73,11 +73,11 @@ the Client by adding following name/value pairs to the response JSON
Stream
{
"status": "error",
...
"field-name": "<field name>",
"field-message": "<message>",
...
"status": "error",
...
"field-name": "<field name>",
"field-message": "<message>",
...
}
Only one validation failure per request can be reported to Client.
......@@ -465,7 +465,7 @@ Request Method
: GET
URL Parameters
: `s=<SIP>` (form, r)
: `s=<SIP>` (form, r)
: `action=lock`, `action=extend`, `action=release>`
: `recordHashMd5=<value of hidden form element 'recordHashMd5'>`
......@@ -489,24 +489,28 @@ On `"conflict"` the Client opens the alert as modal dialog (user can't change an
form' button.
On `"conflict_allow_force"` the Client opens the alert non-modal (default).
## Drag And Drop
Initialize a dnd container by adding the class "qfq-dnd"
### Sorting
Set container object class to "qfq-dnd qfq-dnd-sort".
Add the data elements: data-dnd-api="url" and data-dnd-key="key"
For the childrens inside of the container (just the first children):
add data-dnd-id to a reference you can handle (probably SQL id)
### Drag And Drop (sort)
Request will be sent containing following information:
{"id":2,"value":50,"position":5,"setTo":"after","otherId":4}
Request
: api/dragAndDrop.php
dragId = id of the dragged object
dragPosition = counted original position of the dragged object
setTo = "after" or "before"
hoverId = id of the element the dragged element is now hovering, meaning before or after.
hoverPosition = Position of currently hovered element.
Request Method
: GET
URL Parameters:
: `s=<SIP>` (`form=<formname>`)
:
: `dragId=<data-dnd-id of dragged element>`
: `dragPosition=<client internal position (numbering) of element before dragging>`
: `setTo=before`, `setTo=after`
: `hoverId=<data-dnd-id of dragged element>`
: `hoverPosition=<client internal position (numbering) of element after dragging>`
Server Response
: The response contains at least a [Minimal Response]. In addition, a
[HTML Element Response] (need to be defined) may be included.
## Glossary
......
......@@ -13,6 +13,10 @@ use qfq;
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/QuickFormQuery.php');
//require_once(__DIR__ . '/../qfq/exceptions/UserFormException.php');
//require_once(__DIR__ . '/../qfq/exceptions/CodeException.php');
//require_once(__DIR__ . '/../qfq/exceptions/DbException.php');
//require_once(__DIR__ . '/../qfq/exceptions/ErrorHandler.php');
/**
......@@ -55,24 +59,14 @@ try {
$data = $qfq->dragAndDrop();
// $answer[API_REDIRECT] = $qfq->getForwardMode($answer[API_REDIRECT_URL]);
$answer[API_STATUS] = API_ANSWER_STATUS_SUCCESS;
$answer[API_MESSAGE] = 'load: success';
$answer[API_FORM_UPDATE] = $data[API_FORM_UPDATE];
$answer[API_ELEMENT_UPDATE] = $data[API_ELEMENT_UPDATE];
$answer[API_MESSAGE] = 'reorder: success';
// $answer[API_FORM_UPDATE] = $data[API_FORM_UPDATE];
// $answer[API_ELEMENT_UPDATE] = $data[API_ELEMENT_UPDATE];
// unset($answer[API_FORM_UPDATE][API_ELEMENT_UPDATE]);
} catch (qfq\UserFormException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
$val = Store::getVar(SYSTEM_FORM_ELEMENT, STORE_SYSTEM);
if ($val !== false)
$answer[API_FIELD_NAME] = $val;
$val = Store::getVar(SYSTEM_FORM_ELEMENT_MESSAGE, STORE_SYSTEM);
if ($val !== false)
$answer[API_FIELD_MESSAGE] = $val;
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\DbException $e) {
......
......@@ -853,6 +853,9 @@ const F_NEW_BUTTON_GLYPH_ICON = SYSTEM_NEW_BUTTON_GLYPH_ICON;
const F_ENTER_AS_SUBMIT = SYSTEM_ENTER_AS_SUBMIT;
const F_DRAG_AND_DROP_ORDER_SQL = 'dragAndDropOrderSql';
const F_ORDER_INTERVAL = 'orderInterval';
const F_ORDER_COLUMN = 'orderColumn';
const F_ORDER_COLUMN_NAME = 'ord';
// FORM_ELEMENT_STATI
const FE_MODE_SHOW = 'show';
......@@ -1423,3 +1426,14 @@ const EXCEPTION_STACKTRACE = 'Stacktrace';
const EXCEPTION_TABLE_CLASS = 'table table-hover qfq-table-80';
// Drag And Drop
const DND_DRAG_ID = 'dragId';
const DND_DRAG_POSITION = 'dragPosition';
const DND_SET_TO = 'setTo';
const DND_SET_TO_BEFORE = 'before';
const DND_SET_TO_AFTER = 'after';
const DND_HOVER_ID = 'hoverId';
const DND_HOVER_POSITION = 'hoverPosition';
const DND_COLUMN_ID = 'id';
const DND_COLUMN_ORD = 'ord';
const DND_COLUMN_ORD_NEW = 'ordNew';
......@@ -493,8 +493,8 @@ class QuickFormQuery
case FORM_DRAG_AND_DROP:
$formAction->elements($recordId, $this->feSpecAction, FE_TYPE_BEFORE_LOAD);
$draganddrop = new DragAndDrop();
$draganddrop->process($this->formSpec[F_TABLE_NAME], $this->formSpec[F_DRAG_AND_DROP_ORDER_SQL]);
$dragAndDrop = new DragAndDrop();
$dragAndDrop->process($this->formSpec);
$formAction->elements($recordId, $this->feSpecAction, FE_TYPE_AFTER_LOAD);
break;
......
......@@ -10,11 +10,13 @@ namespace qfq;
//use TYPO3\CMS\Core\FormProtection\Exception;
require_once(__DIR__ . '/../store/Sip.php');
require_once(__DIR__ . '/../store/Session.php');
//require_once(__DIR__ . '/../store/Sip.php');
//require_once(__DIR__ . '/../store/Session.php');
require_once(__DIR__ . '/../store/Store.php');
require_once(__DIR__ . '/../Constants.php');
require_once(__DIR__ . '/../helper/Ldap.php');
//require_once(__DIR__ . '/../helper/Ldap.php');
require_once(__DIR__ . '/../database/Database.php');
require_once(__DIR__ . '/../Evaluate.php');
/**
......@@ -29,140 +31,133 @@ class DragAndDrop {
private $db = null;
/**
* @var array
* @var Store
*/
private $vars = array();
private $store = null;
/**
* @var Evaluate instantiated class
*/
protected $evaluate = null; // copy of the loaded form
/**
* @return array|int
* @param array $formSpec F_TABLE_NAME, F_DRAG_AND_DROP_ORDER_SQL, F_DRAG_AND_DROP_INTERVAL
* @param bool|false $phpUnit
*
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function process($tableName, $dragAndDropOrderSql) {
$arr = array();
$dbIndex = DB_INDEX_DEFAULT; //TODO hier muss noch die aktuelle DB ermittelt werden (kann im iform angegeben sein)
$sipClass = new Sip();
$sipVars = $sipClass->getVarsFromSip($this->vars[TYPEAHEAD_API_SIP]);
// Check for an optional given dbIndex: '[<int>]SELECT ...'
$sql = $sipVars[FE_TYPEAHEAD_SQL];
if ($sql[0] === '[') {
$pos = strpos($sql, ']');
$dbIndex = substr($sql, 1, $pos - 1);
$sipVars[FE_TYPEAHEAD_SQL] = substr($sql, $pos + 1);
}
public function __construct(array $formSpec = array(), $phpUnit = false) {
$dbIndex = DB_INDEX_DEFAULT; //TODO hier muss noch die aktuelle DB ermittelt werden (kann im Form angegeben sein) - Gerade im Formular FORM Editor genau testen!
$this->db = new Database($dbIndex);
if (isset($sipVars[FE_TYPEAHEAD_SQL])) {
if($this->vars[TYPEAHEAD_API_PREFETCH] == '') {
$arr = $this->typeAheadSql($sipVars, $this->vars[TYPEAHEAD_API_QUERY]);
} else {
$arr = $this->typeAheadSqlPrefetch($sipVars, $this->vars[TYPEAHEAD_API_PREFETCH]);
}
} elseif (isset($sipVars[FE_LDAP_SERVER])) {
$ldap = new Ldap();
if ($this->vars[TYPEAHEAD_API_PREFETCH] == '') {
$mode = MODE_LDAP_MULTI;
$key = $this->vars[TYPEAHEAD_API_QUERY];
} else {
$mode = MODE_LDAP_PREFETCH;
$key = $this->vars[TYPEAHEAD_API_PREFETCH];
}
$arr = $ldap->process($sipVars, $key, $mode);
}
return $arr;
$this->store = Store::getInstance('', $phpUnit);
$this->evaluate = new Evaluate($this->store, $this->db);
}
/**
* Do a wildcard search on the prepared statement $config[FE_TYPEAHEAD_SQL].
* All '?' will be replaced by '%$value%'.
* If there is no 'LIMIT x' defined, append it.
* Returns an dict array [ API_TYPEAHEAD_KEY => key, API_TYPEAHEAD_VALUE => value ]
*
* @param array $config
* @param string $value
*
* @return array
* @return array|int
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function typeAheadSql(array $config, $value) {
$values = array();
$sql = $config[FE_TYPEAHEAD_SQL];
$value = '%' . $value . '%';
$cnt = substr_count($sql, '?');
public function process() {
if ($cnt == 0) {
throw new UserFormException("Missing at least one '?' in " . FE_TYPEAHEAD_SQL);
if (empty($formSpec[F_DRAG_AND_DROP_ORDER_SQL])) {
throw new UserFormException('Missing definition of: ' . F_DRAG_AND_DROP_ORDER_SQL, ERROR_MISSING_DEFINITON);
}
for ($ii = 0; $ii < $cnt; $ii++) {
$values[] = $value;
}
$dragId = $this->store->getVar(DND_DRAG_ID, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
// $dragPosition = $this->store->getVar(DND_DRAG_POSITION, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
$setTo = $this->store->getVar(DND_SET_TO, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
$hoverId = $this->store->getVar(DND_HOVER_ID, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
// $hoverPosition = $this->store->getVar(DND_HOVER_POSITION, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);
if (!$this->db->hasLimit($sql)) {
$sql .= ' LIMIT ' . $config[FE_TYPEAHEAD_LIMIT];
}
$arr = $this->db->sql($sql, ROW_REGULAR, $values);
if ($arr == false || count($arr) == 0) {
$orderInterval = empty($formSpec[F_ORDER_INTERVAL]) ? 1 : $formSpec[F_ORDER_INTERVAL];
$orderColumn = empty($formSpec[F_ORDER_COLUMN]) ? F_ORDER_COLUMN_NAME : $formSpec[F_ORDER_COLUMN];
$rows = $this->evaluate->parse($formSpec[F_DRAG_AND_DROP_ORDER_SQL]);
if (!is_array($rows)) {
return array();
}
return $this->db->makeArrayDict($arr, TYPEAHEAD_SQL_KEY_NAME, API_TYPEAHEAD_VALUE, API_TYPEAHEAD_KEY, API_TYPEAHEAD_VALUE);
$this->reorder($rows, $dragId, $setTo, $hoverId, $orderColumn, $orderInterval, $formSpec[F_TABLE_NAME]);
return $arr;
}
/**
* Returns a dict array [ API_TYPEAHEAD_KEY => key, API_TYPEAHEAD_VALUE => value ] with the prefetch result
*
* @param array $config
* @param string $key
*
* @return array
* @param array $rows
* @param $dragId
* @param $setTo
* @param $hoverId
* @param $orderColumn
* @param $orderInterval
* @param $tableName
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
private function reorder(array $rows, $dragId, $setTo, $hoverId, $orderColumn, $orderInterval, $tableName) {
$ord = $orderInterval;
$ordDragOld = -1;
private function typeAheadSqlPrefetch(array $config, $key) {
$keys = array();
// Reorder. Get index for 'drag' and 'hoover'
foreach ($rows as $key => $row) {
$sql = $config[FE_TYPEAHEAD_SQL_PREFETCH];
if ($config[FE_TYPEAHEAD_SQL_PREFETCH] == '') {
throw new UserFormException("Missing definition for `" . FE_TYPEAHEAD_SQL_PREFETCH . "`", ERROR_MISSING_TYPE_AHEAD_SQL_PREFETCH);
}
// the dragged element: skip old position.
if ($rows[DND_COLUMN_ID] == $dragId) {
$ordDragOld = $rows[DND_COLUMN_ORD];
continue;
}
$cnt = substr_count($sql, '?');
if ($cnt == 0) {
throw new UserFormException("Missing at least one '?' in " . FE_TYPEAHEAD_SQL_PREFETCH);
// the dragged element: new position.
if ($rows[DND_COLUMN_ID] == $hoverId) {
switch ($setTo) {
case DND_SET_TO_BEFORE:
$this->setNewOrder($tableName, $orderColumn, $dragId, $ordDragOld, $ord);
$ord += $orderInterval;
$this->setNewOrder($tableName, $orderColumn, $key, $rows[DND_COLUMN_ORD], $ord);
break;
case DND_SET_TO_AFTER:
$this->setNewOrder($tableName, $orderColumn, $key, $rows[DND_COLUMN_ORD], $ord);
$ord += $orderInterval;
$this->setNewOrder($tableName, $orderColumn, $dragId, $ordDragOld, $ord);
break;
default:
throw new CodeException('Unkown setTo string', $setTo, ERROR_UNKNOWN_TOKEN);
}
} else {
$this->setNewOrder($tableName, $orderColumn, $key, $rows[DND_COLUMN_ORD], $ord);
}
$ord += $orderInterval;
}
}
for ($ii = 0; $ii < $cnt; $ii++) {
$keys[] = $key;
}
/**
* @param $tableName
* @param $orderColumn
* @param $id
* @param $ordOld
* @param $ordNew
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
private function setNewOrder($tableName, $orderColumn, $id, $ordOld, $ordNew) {
$arr = $this->db->sql($sql, ROW_REGULAR, $keys);
if ($arr == false || count($arr) == 0) {
return array();
if ($ordNew == $ordOld) {
return;
}
// return first result as key-value pair (concatenate columns)
$value = '';
foreach($arr[0] AS $name => $column) {
$value .= $column;
}
return [ [ API_TYPEAHEAD_KEY => $key, API_TYPEAHEAD_VALUE => $value ] ];
$this->db->sql("UPDATE ? SET ?=? WHERE id=?", [$tableName, $orderColumn, $ordNew, $id]);
}
}
\ No newline at end of file
......@@ -262,6 +262,7 @@ class Link {
* 4: <a href=url>Text</a>
* 5: text
* 6: url
* 8: SIP only - 's=badcaffee1234'
*
* r=render mode, u=url, t:text and/or image.
*
......@@ -308,10 +309,15 @@ class Link {
$this->renderControl[7][1][0] = 6;
$this->renderControl[7][1][1] = 6;
$this->renderControl[8][0][0] = 0;
$this->renderControl[8][0][1] = 0;
$this->renderControl[8][1][0] = 8;
$this->renderControl[8][1][1] = 8;
}
/**
* In render mode 3,4,5 there is no '<a href ...>'. Nevertheless, tooltip and BS Button should be displaye.
* In render mode 3,4,5 there is no '<a href ...>'. Nevertheless, tooltip and BS Button should be displayed.
* Do this by applying a '<span>' attribute around the text.
*
* @param array $vars
......@@ -421,10 +427,10 @@ class Link {
case '22':
case '23':
case '24':
//TODO: Alter Code, umstellen auf JS Client von RO. Vorlage koennte 'Delete' in Subrecord sein.
$link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',sip:'',forward:'" .
$vars[NAME_PAGE] . "'});\" " . $vars[NAME_LINK_CLASS] . ">" . $vars[NAME_TEXT] . "</a>";
break;
//TODO: Alter Code, umstellen auf JS Client von RO. Vorlage koennte 'Delete' in Subrecord sein.
$link = "<a href=\"javascript: void(0);\" onClick=\"var del = new FR.Delete({recordId:'',sip:'',forward:'" .
$vars[NAME_PAGE] . "'});\" " . $vars[NAME_LINK_CLASS] . ">" . $vars[NAME_TEXT] . "</a>";
break;
// 5: plain text, no <span> around
case '5':
......@@ -442,7 +448,9 @@ class Link {
case '26':
throw new UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
break;
case '8':
$link = substr($vars[FINAL_HREF],12); // strip 'index.php?s='
break;
default:
throw new UserReportException ("Mode not implemented. internal render mode=$mode", ERROR_UNKNOWN_MODE);
......@@ -1321,7 +1329,7 @@ EOF;
// if ($vars[NAME_BOOTSTRAP_BUTTON] == '0') {
// $vars[NAME_EXTRA_CONTENT_WRAP] = '<button type="button" ' . $attributes . $onClick . '>';
// } else {
$vars[NAME_EXTRA_CONTENT_WRAP] = '<span ' . $attributes . $onClick . '>';
$vars[NAME_EXTRA_CONTENT_WRAP] = '<span ' . $attributes . $onClick . '>';
$vars[NAME_BOOTSTRAP_BUTTON] = '0';
// }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment