Commit 9dcff2c7 authored by Carsten  Rose's avatar Carsten Rose
Browse files

#4918 / Drag'n'Drop reorder elements

Manual.rst: usage of drag and drop.
parent 9738b34e
......@@ -6225,49 +6225,75 @@ Drag and drop
Sort/order elements
^^^^^^^^^^^^^^^^
QFQ supports sort/order elements via HTML5 drag and drop together with QFQ API/DB calls.
Display the list of elements via a regular QFQ content record. Output: ::
<div class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s=badcaffee1234">
<div class="anyClass" id="<uniq1>" data-dnd-id="55">
Numbero Uno
</div>
<div class="anyClass" id="<uniq2>" data-dnd-id="18">
Numbero Deux
</div>
<div class="anyClass" id="<uniq3>" data-dnd-id="27">
Numbero Tre
</div>
<div class="anyClass" id="<uniq4>" data-dnd-id="534">
Numbero Quattro
</div>
<div class="anyClass" id="<uniq5>" data-dnd-id="67">
Numbero Cinge
</div>
<div class="anyClass" id="<uniq6>" data-dnd-id="101">
Numbero Siesta
</div>
</div>
An example QFQ report which generates those HTML: ::
Manually sorting and ordering of elements via HTML5 drag and drop is supported via QFQ JS/API/DB. Any sortable element
should be represented by a database record with an order column.
* Display: the records will be displayed via QFQ/report.
* Update: updates of the order column are managed by a specific 'drag and drop' definition Form.
Part 1: Display list
''''''''''''''''''''
Display the list of elements via a regular QFQ content record. All 'drag and drop' elements have to be nested by an HTML
element:
* With `class="qfq-dnd-sort"`.
* With an automatically SIP protected form name: `{{'<form name>' AS _data-dnd-api}}`
* Only direct children of such element can be dragged.
* Every children needs a unique identifier `data-dnd-id="<unique>"`. Typically this is the corresponding record id.
* The record needs a dedicated order column, which will be updated through API calls in time.
The final HTML output: ::
<div class="qfq-dnd-sort" data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s=badcaffee1234">
<div class="anyClass" id="<uniq1>" data-dnd-id="55">
Numbero Uno
</div>
<div class="anyClass" id="<uniq2>" data-dnd-id="18">
Numbero Deux
</div>
<div class="anyClass" id="<uniq3>" data-dnd-id="27">
Numbero Tre
</div>
</div>
A typical QFQ report which generates those HTML: ::
10 {
sql = SELECT '<div class="anyClass" id="sort-', n.id,'" data-dnd-api="', n.id,'">' , n.id, ' / ', n.note, ' / ', n.ord, '</div>'
sql = SELECT '<div id="anytag-', n.id,'" data-dnd-id="', n.id,'">' , n.note, '</div>'
FROM Note AS n
WHERE n.grId=4
ORDER BY n.ord
head = <div class-"qfq-dnd" data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php" data-dnd-key="{{'U:form=draganddrop|s|r:8' AS _link}}">
head = <div class="qfq-dnd-sort" {{'<form name>' AS _data-dnd-api}}>
tail = </div>
}
Part 2: Update database
'''''''''''''''''''''''
A dedicated `Form`, without any `FormElements`, is needed to define the database update definition.
Examples
--------
Fields:
* Name: <custom formname>
* Table: <table with the element records>
* Parameter:
+=====================================+================================================================================+
| orderInterval = <number> | Optional. By default '10'. Might be any number > 0. |
+-------------------------------------+--------------------------------------------------------------------------------+
| orderColumn = <column name> | Optional. By default 'ord'. |
+-------------------------------------+--------------------------------------------------------------------------------+
| dragAndDropOrderSql = | Query which selects the same records as the report in the |
| {{!SELECT n.id, n.ord FROM Note AS n ORDER BY n.ord}} | same order! Inconsistencies results in sort differences |
+---------------------------------------------------------+------------------------------------------------------------+
Report Examples
---------------
The following section gives some examples of typical reports
......
......@@ -86,6 +86,8 @@ abstract class AbstractBuildForm {
* @param array $feSpecAction
* @param array $feSpecNative
* @param array $db
* @throws CodeException
* @throws UserFormException
*/
public function __construct(array $formSpec, array $feSpecAction, array $feSpecNative, array $db = null) {
$this->formSpec = $formSpec;
......@@ -172,6 +174,8 @@ abstract class AbstractBuildForm {
* @param array $latestFeSpecNative
* @return array|string $mode=LOAD_FORM: The whole form as HTML, $mode=FORM_UPDATE: array of all
* formElement.dynamicUpdate-yes values/states
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function process($mode, $htmlElementNameIdZero = false, $latestFeSpecNative = array()) {
......@@ -247,6 +251,8 @@ abstract class AbstractBuildForm {
*
* @param string $mode
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function head($mode = FORM_LOAD) {
$html = '';
......@@ -276,6 +282,8 @@ abstract class AbstractBuildForm {
*
* @return string String: <a href="?pageId&sip=....">Edit</a> <small>[sip:..., r:..., urlparam:...,
* ...]</small>
* @throws CodeException
* @throws UserFormException
*/
public function createFormEditorUrl($form, $recordId, array $param = array()) {
......@@ -338,6 +346,8 @@ abstract class AbstractBuildForm {
* Build MD5 from the current record. Return HTML Input element.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function buildInputRecordHashMd5() {
......@@ -357,6 +367,9 @@ abstract class AbstractBuildForm {
* @param $recordId
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function buildRecordHashMd5($tableName, $recordId) {
$record = array();
......@@ -372,6 +385,8 @@ abstract class AbstractBuildForm {
* Create HTML Input vars to detect bot automatic filling of forms.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function getHoneypotVars() {
$html = '';
......@@ -442,6 +457,9 @@ abstract class AbstractBuildForm {
* See: https://www.w3.org/wiki/HTML/Elements/form#HTML_Attributes
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
*/
public function getEncType() {
......@@ -457,6 +475,10 @@ abstract class AbstractBuildForm {
* @param array|string $value
*
* @return array|string
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function processReportSyntax($value) {
......@@ -502,6 +524,10 @@ abstract class AbstractBuildForm {
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
*
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
public function elements($recordId, $filter = FORM_ELEMENTS_NATIVE, $feIdContainer = 0, array &$json,
$modeCollectFe = FLAG_DYNAMIC_UPDATE, $htmlElementNameIdZero = false,
......@@ -672,6 +698,10 @@ abstract class AbstractBuildForm {
* @param array $formElement
*
* @return array
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function prepareFillStoreFireLdap(array $formElement) {
......@@ -767,6 +797,8 @@ abstract class AbstractBuildForm {
* save/update.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function prepareT3VarsForSave() {
......@@ -802,6 +834,8 @@ abstract class AbstractBuildForm {
* Get all elements from STORE_ADDITIONAL_FORM_ELEMENTS and return them as a string.
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function buildAdditionalFormElements() {
......@@ -816,6 +850,8 @@ abstract class AbstractBuildForm {
* @param array $json
*
* @return string <input type='hidden' name='s' value='<sip>'>
* @throws CodeException
* @throws UserFormException
*/
public function buildHiddenSip(array &$json) {
......@@ -921,6 +957,7 @@ abstract class AbstractBuildForm {
* @param array $feMode
*
* @return array
* @throws UserFormException
*/
private function getJsonFeMode($feMode) {
......@@ -970,6 +1007,7 @@ abstract class AbstractBuildForm {
* @param string $addClass
*
* @return string
* @throws CodeException
*/
public function buildLabel($htmlFormElementName, $label, $addClass = '') {
$attributes = Support::doAttribute('for', $htmlFormElementName);
......@@ -985,6 +1023,8 @@ abstract class AbstractBuildForm {
*
* @param $toolTipNew
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function deriveNewRecordUrlFromExistingSip(&$toolTipNew) {
......@@ -1048,6 +1088,8 @@ abstract class AbstractBuildForm {
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE
*
* @return string complete rendered HTML input element.
* @throws CodeException
* @throws UserFormException
*/
public function buildInput(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
$textarea = '';
......@@ -1192,6 +1234,7 @@ abstract class AbstractBuildForm {
* @param array $formElement
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function typeAheadBuildParam(array &$formElement) {
......@@ -1297,6 +1340,7 @@ abstract class AbstractBuildForm {
* @param bool $flagOmitEmpty
*
* @return string
* @throws CodeException
*/
private function getAttributeList(array $formElement, array $attributeList, $flagOmitEmpty = true) {
$attribute = '';
......@@ -1315,6 +1359,7 @@ abstract class AbstractBuildForm {
*
* @param bool $cssDisable
* @return string
* @throws CodeException
* @throws UserFormException
*/
private function getAttributeFeMode($feMode, $cssDisable = true) {
......@@ -1371,6 +1416,7 @@ abstract class AbstractBuildForm {
* @param string $mode FORM_LOAD | FORM_UPDATE | FORM_SAVE*
*
* @return string
* @throws CodeException
* @throws UserFormException
*/
public function buildCheckbox(array $formElement, $htmlFormElementName, $value, array &$json, $mode = FORM_LOAD) {
......@@ -1426,6 +1472,7 @@ abstract class AbstractBuildForm {
* @param array $itemKey
* @param array $itemValue
*
* @throws CodeException
* @throws UserFormException
*/
public function getKeyValueListFromSqlEnumSpec(array $formElement, array &$itemKey, array &$itemValue) {
......
......@@ -383,7 +383,9 @@ class BuildFormBootstrap extends AbstractBuildForm {
* @param array $json
* @return string
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function buildPillNavigation($mode = FORM_LOAD, array $pillArray, array &$json) {
$pillButton = '';
......
......@@ -547,6 +547,7 @@ const SYSTEM_DOWNLOAD_POPUP = 'hasDownloadPopup'; // Marker which is set to 'tru
const DOWNLOAD_POPUP_REQUEST = 'true';
const DOWNLOAD_POPUP_REPLACE_TEXT = '#downloadPopupReplaceText#';
const DOWNLOAD_POPUP_REPLACE_TITLE = '#downloadPopupReplaceTitle#';
const SYSTEM_DRAG_AND_DROP_JS = 'hasDragAndDropJS';
const SYSTEM_PARAMETER_LANGUAGE_FIELD_NAME = 'parameterLanguageFieldName';
const CSS_REQUIRED = 'required-field';
......@@ -648,6 +649,7 @@ const MODE_LDAP_MULTI = 'ldapMulti';
// api/save.php, api/delete.php, api/load.php
const API_DELETE_PHP = 'delete.php';
const API_DOWNLOAD_PHP = 'download.php';
const API_DRAG_AND_DROP_PHP = 'dragAndDrop.php';
const API_STATUS = 'status';
const API_MESSAGE = 'message';
......@@ -1437,3 +1439,4 @@ const DND_HOVER_POSITION = 'hoverPosition';
const DND_COLUMN_ID = 'id';
const DND_COLUMN_ORD = 'ord';
const DND_COLUMN_ORD_NEW = 'ordNew';
const DND_DATA_DND_API = 'data-dnd-api';
\ No newline at end of file
......@@ -197,6 +197,59 @@ class Evaluate {
return $result;
}
/**
* @param $arrToken
* @param $dbIndex
* @param $foundInStore
* @return string
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
private function inlineLink($arrToken, $dbIndex, &$foundInStore){
$token = OnString::trimQuote(trim(implode(' ', $arrToken)));
if ($this->link === null) {
$this->link = new Link($this->store->getSipInstance(), $dbIndex);
}
$foundInStore = TOKEN_FOUND_AS_COLUMN;
return $this->link->renderLink($token);
}
/**
* @param $arrToken
* @param $dbIndex
* @param $foundInStore
* @return string
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
private function inlineDataDndApi($arrToken, $dbIndex, &$foundInStore){
$token = OnString::trimQuote(trim(implode(' ', $arrToken)));
if(empty($token)){
throw new UserReportException('Missing form name for "data-dnd-api"', ERROR_MISSING_FORM);
}
if ($this->link === null) {
$this->link = new Link($this->store->getSipInstance(), $dbIndex);
}
$foundInStore = TOKEN_FOUND_AS_COLUMN;
$s = $this->link->renderLink('U:form=' . $token . '|s|r:8');
// Flag to add DND JS code later on.
$this->store->setVar(SYSTEM_DRAG_AND_DROP_JS, 'true', STORE_SYSTEM);
// data-dnd-api="typo3conf/ext/qfq/qfq/api/dragAndDrop.php?s={{'U:form=<formnname>|s|r:8' AS _link}}"
return DND_DATA_DND_API . '="' . API_DIR . '/' . API_DRAG_AND_DROP_PHP . '?s=' . $s . '"';
}
/**
* Tries to substitute $token.
* Token might be:
......@@ -244,7 +297,7 @@ class Evaluate {
$sqlMode = ROW_REGULAR;
}
// Extract token: check if this is a 'SQL Statement', 'link'
// Extract token: check if this is a 'variable', 'SQL Statement', 'link', 'data-dnd-api'
$arrToken = explode(' ', $token);
// Variable Type 'SQL Statement'
......@@ -254,28 +307,22 @@ class Evaluate {
return $this->dbArray[$dbIndex]->sql($token, $sqlMode);
}
// Variable Type '... AS LINK'
// Variable Type '... AS _link', '... as data-dnd-api'
$countToken = count($arrToken);
if ($countToken > 2 && strcasecmp($arrToken[$countToken - 2], 'as') == 0) {
$type = OnString::stripFirstCharIf('_', $arrToken[$countToken - 1]);
if (strcasecmp($type, 'link') == 0) {
array_pop($arrToken); // remove 'link' | 'data-dnd-api'
array_pop($arrToken); // remove 'as'
array_pop($arrToken); // remove 'link'
array_pop($arrToken); // remove 'as'
$token = trim(implode($arrToken));
// $str = OnString::trimQuote(substr($token, 0, strlen($token) - 8)); // strlen('_as_link')=8
$str = OnString::trimQuote($token);
if ($this->link === null) {
$this->link = new Link($this->store->getSipInstance(), $dbIndex);
}
if (strcasecmp($type, COLUMN_LINK ) == 0) {
return($this->inlineLink($arrToken, $dbIndex,$foundInStore));
}
$foundInStore = TOKEN_FOUND_AS_COLUMN;
if($type == DND_DATA_DND_API){
return($this->inlineDataDndApi($arrToken, $dbIndex,$foundInStore));
return $this->link->renderLink($str);
}
}
......
......@@ -62,8 +62,7 @@ require_once(__DIR__ . '/form/DragAndDrop.php');
* Class Qfq
* @package qfq
*/
class QuickFormQuery
{
class QuickFormQuery {
/**
* @var \qfq\Store instantiated class
......@@ -128,8 +127,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
public function __construct(array $t3data = array(), $phpUnit = false)
{
public function __construct(array $t3data = array(), $phpUnit = false) {
$this->phpUnit = $phpUnit;
......@@ -198,8 +196,7 @@ class QuickFormQuery
* @throws CodeException
* @throws UserFormException
*/
public function getForwardMode()
{
public function getForwardMode() {
$forwardPage = $this->formSpec[F_FORWARD_PAGE];
......@@ -227,8 +224,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
public function process()
{
public function process() {
$html = '';
if ($this->store->getVar(TYPO3_DEBUG_SHOW_BODY_TEXT, STORE_TYPO3) === 'yes') {
......@@ -244,6 +240,11 @@ class QuickFormQuery
$html .= $this->getModalCode();
}
// Only needed if there are 'drag and drop' elements.
if ($this->store->getVar(SYSTEM_DRAG_AND_DROP_JS, STORE_SYSTEM) == 'true') {
$html .= $this->getDragAndDropCode();
}
$class = $this->store->getVar(SYSTEM_CSS_CLASS_QFQ_CONTAINER, STORE_SYSTEM);
if ($class) {
$html = Support::wrapTag("<div class='$class'>", $html);
......@@ -255,8 +256,7 @@ class QuickFormQuery
/**
* Determine the name of the language parameter field, which has to be taken to fill language specific defintions.
*/
private function setParameterLanguageFieldName()
{
private function setParameterLanguageFieldName() {
$typo3PageLanguage = $this->store->getVar(TYPO3_PAGE_LANGUAGE, STORE_TYPO3);
if (empty($typo3PageLanguage)) {
......@@ -290,8 +290,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
private function doForm($formMode)
{
private function doForm($formMode) {
$data = '';
$foundInStore = '';
......@@ -536,8 +535,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
private function setForwardModePage()
{
private function setForwardModePage() {
if ('url' != substr($this->formSpec[F_FORWARD_MODE], 0, 3)) {
return false;
......@@ -595,8 +593,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
private function pasteClipboard($formId, FormAction $formAction)
{
private function pasteClipboard($formId, FormAction $formAction) {
if (!$this->isPasteRecord()) {
return;
......@@ -621,8 +618,7 @@ class QuickFormQuery
/**
* @return bool true if there is at least one paste record, else false.
*/
private function isPasteRecord()
{
private function isPasteRecord() {
foreach ($this->feSpecAction as $formElement) {
if ($formElement[FE_TYPE] == FE_TYPE_PASTE) {
......@@ -645,8 +641,7 @@ class QuickFormQuery
* @throws CodeException
* @throws UserFormException
*/
private function buildNSetReloadUrl(array $formSpec, $recordId)
{
private function buildNSetReloadUrl(array $formSpec, $recordId) {
$formSpec[F_FORWARD_MODE] = API_ANSWER_REDIRECT_URL_SKIP_HISTORY;
......@@ -684,8 +679,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
private function loadFormSpecification($mode, $recordId, &$foundInStore = '')
{
private function loadFormSpecification($mode, $recordId, &$foundInStore = '') {
// formName
if (false === ($formName = $this->getFormName($mode, $foundInStore))) {
......@@ -826,8 +820,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
public function getNativeFormElements($sql, array $param, $formSpec)
{
public function getNativeFormElements($sql, array $param, $formSpec) {
$feSpecNative = $this->dbArray[$this->dbIndexQfq]->sql($sql, ROW_REGULAR, $param);
......@@ -867,8 +860,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
private function explodeTemplateGroupElements(array $elements)
{
private function explodeTemplateGroupElements(array $elements) {
$new = array();
// No FormElements or no NAME_TG_COPIES column: nothing to do, return.
......@@ -924,8 +916,7 @@ class QuickFormQuery
* @throws UserFormException
* @throws UserReportException
*/
public function getFormName($mode, &$foundInStore = '')
{
public function getFormName($mode, &$foundInStore = '') {
$dummy = array();
switch ($mode) {
......@@ -964,8 +955,7 @@ class QuickFormQuery
* @throws CodeException
* @throws UserFormException
*/
private function modeCleanFormConfig($mode, array $form)
{
private function modeCleanFormConfig($mode, array $form) {
switch ($mode) {
case FORM_DELETE:
......@@ -996,8 +986,7 @@ class QuickFormQuery
* @throws CodeException
* @throws UserFormException
*/
private function syncSystemFormConfig(array $formSpec)
{
private function syncSystemFormConfig(array $formSpec) {
$keys = [F_BS_COLUMNS,
F_BS_LABEL_COLUMNS,
......@@ -1067,8 +1056,7 @@ class QuickFormQuery
* @throws CodeException
* @throws UserFormException
*/
private function initForm(array $formSpec)
{
private function initForm(array $formSpec) {
Support::setIfNotSet($formSpec, F_EXTRA_DELETE_FORM, '');
Support::setIfNotSet($formSpec, F_SUBMIT_BUTTON_TEXT, '');
......@@ -1109,8 +1097,7 @@ class QuickFormQuery
* @throws \qfq\UserFormException
* @internal param $foundInStore
*/
private function validateForm($formNameFoundInStore, $formMode)