Commit b20cd90a authored by bbaer's avatar bbaer
Browse files

Merge remote-tracking branch 'origin/dragAndDrop' into dragAndDrop

parents 54a46bec 5783f8c0
......@@ -1203,7 +1203,7 @@ Bug Fixes
* Dynamic Update has been broken since implementing of 'element-update' (#3180). Now both methods, 'element-update' and 'form-update' should be fine.
* qfq-bs.css.less: Fixed problem with 'typeahead input elements' not expanded to Bootstrap column width. Changed
Layout/Design Typeahead drop-down box. Add hoover for the drop-down box with a blue background
Layout/Design Typeahead drop-down box. Add hover for the drop-down box with a blue background
* AbstractBuildForm.php: #3374 - textarea elements now contains 'maxlength' attribute.
* BuildFormBootstrap.php: wrapping of optional 'submitButtonText' now done with the 'per form' values.
* typeahead.php: if there is an exception, the message body is sent as regular 'content' for the drop-down box. At the
......
# 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.
Example: http://something/bla?dragId=uno&dragPosition=1&setTo=before&hoverId=tre&hoverPosition=3
\ No newline at end of file
......@@ -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
......
......@@ -6218,8 +6218,95 @@ E.g.::
10.sql = SELECT "p:home&r=0|t:Home|c:qfq-100 qfq-left" AS _pagev
Examples
--------
Drag and drop
-------------
Sort/order elements
^^^^^^^^^^^^^^^^^^^
Manually sorting and ordering of elements via `HTML5 drag and drop` is supported via QFQ. Any sortable element
should be represented by a database record with an order column. If the elements are unordered, they will be ordered after
the first manual move of an element.
Functionality is divided into:
* Display list: the records will be displayed via QFQ/report.
* Update database: 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=<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 id="anytag-', n.id,'" data-dnd-id="', n.id,'">' , n.note, '</div>'
FROM Note AS n
ORDER BY n.ord
head = <div class="qfq-dnd-sort" {{'form=dndSortNote&grId=28' AS _data-dnd-api}}>
tail = </div>
}
Part 2: Update database
'''''''''''''''''''''''
A dedicated `Form`, without any `FormElements`, is needed to define the database update definition.
Fields:
* Name: <custom form name> - used in Part 1 in the `_data-dnd-api` variable.
* Table: <table with the element records> - used to the update the records specified by `dragAndDropOrderSql`.
* Parameter:
+-------------------------------------+--------------------------------------------------------------------------------+
| Attribute | Description |
+=====================================+================================================================================+
| orderInterval = <number> | Optional. By default '10'. Might be any number > 0. |
+-------------------------------------+--------------------------------------------------------------------------------+
| orderColumn = <column name> | Optional. By default 'ord'. |
+-------------------------------------+--------------------------------------------------------------------------------+
| dragAndDropOrderSql = | Query to selects the *same* records as the report in the |
| {{!SELECT n.id AS id, n.ord AS ord FROM Note AS n | same *order!* Inconsistencies results in sort differences. |
| ORDER BY n.ord}} | The columns `id` and `ord` are *mandatory.* |
+-------------------------------------------------------+--------------------------------------------------------------+
The form related to the example of part 1: ::
Form.name: dndSortNote
Form.table: Note
Form.parameter: orderInterval = 1
Form.parameter: orderColumn = ord
Form.parameter: dragAndDropOrderSql = {{!SELECT n.id AS id, n.ord AS ord FROM Note AS n WHERE n.grId={{grId:S0}} ORDER BY n.ord}}
Report Examples
---------------
The following section gives some examples of typical reports
......
......@@ -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) {
......
......@@ -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 = '';
......@@ -595,6 +597,7 @@ class BuildFormBootstrap extends AbstractBuildForm {
fileDeleteUrl: '$apiDir/file.php?$actionDelete'
});
var qfqRecordList = new QfqNS.QfqRecordList('$apiDeletePhp');
})
</script>
......
......@@ -248,6 +248,7 @@ const ERROR_DOWNLOAD_NO_FILES = 1701;
const ERROR_DOWNLOAD_NOTHING_TO_DO = 1702;
const ERROR_DOWNLOAD_UNEXPECTED_MIME_TYPE = 1703;
const ERROR_DOWNLOAD_UNEXPECTED_NUMBER_OF_SOURCES = 1704;
const ERROR_DOWNLOAD_FILE_NOT_READABLE = 1705;
// KeyValueParser
const ERROR_KVP_VALUE_HAS_NO_KEY = 1900;
......@@ -547,6 +548,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 +650,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';
......@@ -853,6 +856,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 +1429,15 @@ 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';
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:' . $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=<form name>[&paramX=<any value>]|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
// Extract token: check if this is a 'variable', 'SQL Statement', 'link', 'data-dnd-api'
$arrToken = explode(' ', $token);
// Variable Type 'SQL Statement'
......@@ -254,23 +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) {
$str = OnString::trimQuote(substr($token, 0, strlen($token) - 8)); // strlen('_as_link')=8
array_pop($arrToken); // remove 'link' | 'data-dnd-api'
array_pop($arrToken); // remove 'as'
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);
}
}
......
......@@ -44,6 +44,7 @@ require_once(__DIR__ . '/BodytextParser.php');
require_once(__DIR__ . '/Delete.php');
require_once(__DIR__ . '/form/FormAction.php');
require_once(__DIR__ . '/form/Dirty.php');
require_once(__DIR__ . '/form/DragAndDrop.php');
/*
* Form will be called
......@@ -61,8 +62,7 @@ require_once(__DIR__ . '/form/Dirty.php');
* Class Qfq
* @package qfq
*/
class QuickFormQuery
{
class QuickFormQuery {
/**
* @var \qfq\Store instantiated class
......@@ -127,8 +127,7 @@ class QuickFormQuery