From 6e049ecd5ed6febe8516bb1ae099efd7717656a7 Mon Sep 17 00:00:00 2001
From: Carsten  Rose <carsten.rose@math.uzh.ch>
Date: Tue, 25 Feb 2020 23:29:41 +0100
Subject: [PATCH] Refs #10145: QFQ fills data-typeahead-initial-suggestion

---
 Documentation/Manual.rst                      | 14 ++++++--
 extension/Classes/Core/AbstractBuildForm.php  | 20 +++++++----
 extension/Classes/Core/Constants.php          |  4 +--
 extension/Classes/Core/Database/Database.php  | 35 ++++++++++---------
 .../Classes/Core/Helper/HelperFormElement.php |  1 -
 extension/Classes/Core/Helper/OnArray.php     | 23 ++++++++++++
 extension/Classes/Core/Helper/Support.php     |  4 +--
 .../Classes/Core/Store/FillStoreForm.php      |  2 +-
 8 files changed, 71 insertions(+), 32 deletions(-)

diff --git a/Documentation/Manual.rst b/Documentation/Manual.rst
index e9793b271..2f787953d 100644
--- a/Documentation/Manual.rst
+++ b/Documentation/Manual.rst
@@ -3227,6 +3227,8 @@ See also at specific *FormElement* definitions.
 |                        |        | (defaults to 00:00:00 if none entered).                                                                  |
 +------------------------+--------+----------------------------------------------------------------------------------------------------------+
 | typeAheadLimit,        | string | See `input-typeahead`_                                                                                   |
+| typeAheadInitial       |        |                                                                                                          |
+| Suggestion,            |        |                                                                                                          |
 | typeAheadMinLength,    |        |                                                                                                          |
 | typeAheadSql,          |        |                                                                                                          |
 | typeAheadSqlPrefetch,  |        |                                                                                                          |
@@ -3540,7 +3542,7 @@ configured).
 Configuration via Form / FormElement
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-All of the `typeAhead*` (except `typeAheadLdap`) and `ldap*` parameter can be specified either in
+All of the `typeAhead*` (except `typeAheadLdap`, `typeAheadInitialSuggestion`) and `ldap*` parameter can be specified either in
 *Form.parameter* or in *FormElement.parameter*.
 
 SQL
@@ -3548,7 +3550,7 @@ SQL
 
 * *FormElement.parameter*:
 
-  * *typeAheadSql* = `SELECT ... AS 'id', ... AS 'value' FROM ... WHERE name LIKE ? OR firstName LIKE ? LIMIT 100`
+  * *typeAheadSql* = ``SELECT ... AS 'id', ... AS 'value' FROM ... WHERE name LIKE ? OR firstName LIKE ? LIMIT 100``
 
     * If there is only one column in the SELECT statement, that one will be used and there is no dict (key/value pair).
     * If there is no column `id` or no column `value`, then the first column becomes `id` and the second column becomes `value`.
@@ -3556,11 +3558,17 @@ SQL
     * The value, typed by the user, will be replaced on all places where a `?` appears.
     * All `?` will be automatically surrounded by '%'. Therefore wildcard search is implemented: `... LIKE '%<?>%' ...`
 
-  * *typeAheadSqlPrefetch* = `SELECT firstName, ' ', lastName FROM Person WHERE id = ?`
+  * *typeAheadSqlPrefetch* = ``SELECT firstName, ' ', lastName FROM Person WHERE id = ?``
 
     * If the query returns several results, only the first one is returned and displayed.
     * If the query selects multiple columns, the columns are concatenated.
 
+  * *typeAheadInitialSuggestion* = ``{{!SELECT fr.id AS id, fr.name AS value FROM Fruit AS fr}}``
+
+    * Shows suggestions when the input element gets the focus, before the user starts to type anything.
+    * If given, *typeAheadMinLength* will be set to 0.
+    * Limit the number of rows via SQL ``... LIMIT ...`` clause.
+
 LDAP
 ;;;;
 
diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php
index 6127c2203..060a0aa84 100644
--- a/extension/Classes/Core/AbstractBuildForm.php
+++ b/extension/Classes/Core/AbstractBuildForm.php
@@ -737,7 +737,8 @@ abstract class AbstractBuildForm {
 
         // The following 'FormElement.parameter' will never be used during load (fe.type='upload'). FE_PARAMETER has been already expanded.
         $skip = [FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
-            FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT];
+            FE_FILL_STORE_VAR, FE_FILE_DOWNLOAD_BUTTON, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE,
+            FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION];
 
         // get current data record
         $primaryKey = $this->formSpec[F_PRIMARY_KEY];
@@ -1335,6 +1336,7 @@ abstract class AbstractBuildForm {
             }
         }
 
+        // Check if typeAhead[Tag] needs to build
         if ('' != ($typeAheadUrlParam = $this->typeAheadBuildParam($formElement))) {
 
             if (empty($formElement[FE_INPUT_TYPE])) {
@@ -1345,6 +1347,14 @@ abstract class AbstractBuildForm {
                 $formElement[FE_INPUT_AUTOCOMPLETE] = 'off'; // typeahead behaves better with 'autocomplete'='off'
             }
 
+            // Collect typeAhead initial suggestion
+            if (!empty($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION])) {
+//                $formElement[FE_TYPEAHEAD_MINLENGTH] = 0; // If a suggestion is defined: minLength becomes automatically 0
+                $arr = $this->evaluate->parse($formElement[FE_TYPEAHEAD_INITIAL_SUGGESTION]);
+                $arr = $this->dbArray[$this->dbIndexData]->makeArrayDict($arr, TYPEAHEAD_SQL_KEY_NAME, API_TYPEAHEAD_VALUE, API_TYPEAHEAD_KEY, API_TYPEAHEAD_VALUE);
+                $attribute .= Support::doAttribute(DATA_TYPEAHEAD_INITIAL_SUGGESTION, json_encode($arr));
+            }
+
             $class .= ' ' . CLASS_TYPEAHEAD;
             $dataSip = $this->sip->queryStringToSip($typeAheadUrlParam, RETURN_SIP);
             $attribute .= Support::doAttribute(DATA_TYPEAHEAD_SIP, $dataSip);
@@ -1364,12 +1374,8 @@ abstract class AbstractBuildForm {
                 $formElement[FE_INPUT_TYPE] = 'hidden';
 
                 // Client: TAG handling expects the '$value' as a JSON string.
-                $kk = KeyValueStringParser::parse($value, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, KVP_IF_VALUE_EMPTY_COPY_KEY);
-                $jj = '';
-                foreach ($kk as $arrKey => $arrValue) {
-                    $jj .= ',' . json_encode(["key" => $arrKey, "value" => $arrValue]);
-                }
-                $value = '[' . substr($jj, 1) . ']';
+                $arr = KeyValueStringParser::parse($value, PARAM_KEY_VALUE_DELIMITER, PARAM_LIST_DELIMITER, KVP_IF_VALUE_EMPTY_COPY_KEY);
+                $value = OnArray::arrayToQfqJson($arr);
             }
         }
 
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index b9f905c46..ec8f54f37 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -859,6 +859,7 @@ const DATA_ENABLE_SAVE_BUTTON = 'data-enable-save-button';
 const DATA_TYPEAHEAD_LIMIT = 'data-typeahead-limit';
 const DATA_TYPEAHEAD_MINLENGTH = 'data-typeahead-minlength';
 const DATA_TYPEAHEAD_PEDANTIC = 'data-typeahead-pedantic';
+const DATA_TYPEAHEAD_INITIAL_SUGGESTION = 'data-typeahead-initial-suggestion';
 const DATA_TYPEAHEAD_TAG = 'data-typeahead-tags';
 const DATA_TYPEAHEAD_TAG_DELIMITER = 'data-typeahead-tag-delimiters';
 
@@ -870,8 +871,6 @@ const CLASS_FORM_ELEMENT_EDIT = 'qfq-form-element-edit';
 const CLASS_FORM_ELEMENT_AUTO_GROW = 'qfq-auto-grow';
 const ATTRIBUTE_DATA_MAX_HEIGHT = 'data-max-height';
 
-
-
 // BuildForm
 const SYMBOL_NEW = 'new';
 const SYMBOL_EDIT = 'edit';
@@ -1240,6 +1239,7 @@ const FE_LDAP_USE_BIND_CREDENTIALS = F_LDAP_USE_BIND_CREDENTIALS;
 const FE_TYPEAHEAD_LIMIT = F_TYPEAHEAD_LIMIT;
 const FE_TYPEAHEAD_MINLENGTH = F_TYPEAHEAD_MINLENGTH;
 const FE_TYPEAHEAD_PEDANTIC = F_TYPEAHEAD_PEDANTIC;
+const FE_TYPEAHEAD_INITIAL_SUGGESTION = 'typeAheadInitialSuggestion';
 const FE_TYPEAHEAD_TAG = 'typeAheadTag';
 const FE_TYPEAHEAD_TAG_DELIMITER = 'typeAheadTagDelimiter';
 const FE_TYPEAHEAD_GLUE_INSERT = 'typeAheadGlueInsert';
diff --git a/extension/Classes/Core/Database/Database.php b/extension/Classes/Core/Database/Database.php
index 12e3ff723..07ef4bd8d 100644
--- a/extension/Classes/Core/Database/Database.php
+++ b/extension/Classes/Core/Database/Database.php
@@ -8,13 +8,13 @@
 
 namespace IMATHUZH\Qfq\Core\Database;
 
- 
-use IMATHUZH\Qfq\Core\Store\Store;
-use IMATHUZH\Qfq\Core\Helper\Logger;
+
 use IMATHUZH\Qfq\Core\Helper\BindParam;
+use IMATHUZH\Qfq\Core\Helper\HelperFile;
 use IMATHUZH\Qfq\Core\Helper\HelperFormElement;
+use IMATHUZH\Qfq\Core\Helper\Logger;
 use IMATHUZH\Qfq\Core\Helper\OnArray;
-use IMATHUZH\Qfq\Core\Helper\HelperFile;
+use IMATHUZH\Qfq\Core\Store\Store;
 
 /**
  * Class Database
@@ -932,10 +932,12 @@ class Database {
     }
 
     /**
-     * $arr will be converted to a two column array with keys $keyName1 and $keyName2.
-     * $arr might contain one or more columns.
-     * Only when $keyName1 and $keyName2 exist, those will be used. Else the first column becomes $keyName1 and the
-     * second becomes $keyName2. If there is only one column, that column will be doubled.
+     * $arr = [ 0 => [ $srcColumn1 => $value0_1, $srcColumn2 => $value0_2 ], 1 => [ $srcColumn1 => $value1_1, $srcColumn2 => $value1_2 ], ...]
+     *
+     * $arr will be converted to a two column array with keys $destColumn1 and $destColumn2.
+     * If $destColumn1 or $destColumn2 is empty, take $srcColumn1, $srcColumn2 as names.
+     * $arr might contain one or more columns. Only the first two columns are used.
+     * If there is only one column, that column will be doubled.
      *
      * @param array $arr
      * @param string $srcColumn1
@@ -947,6 +949,11 @@ class Database {
      */
     public function makeArrayDict(array $arr, $srcColumn1, $srcColumn2, $destColumn1 = '', $destColumn2 = '') {
 
+        if ($arr == array() || $arr === null) {
+            return array();
+        }
+
+        // Set defaults
         if ($destColumn1 == '') {
             $destColumn1 = $srcColumn1;
         }
@@ -955,12 +962,7 @@ class Database {
             $destColumn2 = $srcColumn2;
         }
 
-        $new = array();
-
-        if ($arr == array() || $arr === null) {
-            return array();
-        }
-
+        // Set final column names
         $row = $arr[0];
         $keys = array_keys($row);
         if (count($row) < 2) {
@@ -974,9 +976,11 @@ class Database {
             $column2 = $keys[1];
         }
 
+        $new = array();
         $row = array_shift($arr);
         while (null !== $row) {
             $new[] = [$destColumn1 => $row[$column1], $destColumn2 => $row[$column2]];
+//            $new[] = [$destColumn1 => htmlentities($row[$column1], ENT_QUOTES), $destColumn2 => htmlentities($row[$column2], ENT_QUOTES)];
             $row = array_shift($arr);
         }
 
@@ -1028,8 +1032,7 @@ class Database {
 
         try {
             $this->playMultiQuery($query);
-        }
-        catch (\CodeException $e) {
+        } catch (\CodeException $e) {
             throw new \CodeException("Error playing $filename", ERROR_PLAY_SQL_FILE);
         }
 
diff --git a/extension/Classes/Core/Helper/HelperFormElement.php b/extension/Classes/Core/Helper/HelperFormElement.php
index 030ed969b..33c8dcd15 100644
--- a/extension/Classes/Core/Helper/HelperFormElement.php
+++ b/extension/Classes/Core/Helper/HelperFormElement.php
@@ -718,7 +718,6 @@ EOF;
                 $attribute .= Support::doAttribute('required', 'required');
                 break;
             case FE_MODE_READONLY:
-//                $attribute .= Support::doAttribute($feMode, $feMode);
                 $attribute .= Support::doAttribute('disabled', 'disabled');
                 break;
             default:
diff --git a/extension/Classes/Core/Helper/OnArray.php b/extension/Classes/Core/Helper/OnArray.php
index 17a8d5358..7e9be89d1 100644
--- a/extension/Classes/Core/Helper/OnArray.php
+++ b/extension/Classes/Core/Helper/OnArray.php
@@ -427,4 +427,27 @@ class OnArray {
     public static function getMd5(array $data) {
         return md5(implode($data));
     }
+
+    /**
+     * Converts a one dimensional array to JSON array. The 'key' and 'value' will names are hardcoded:
+     *
+     * Return: [ { 'key': $key[0], 'value': $value[0] }, { 'key': $key[1], 'value': $value[2] }, ... ]
+     *
+     * @param array $arr
+     * @param bool $flagHtmlEntity true|false
+     * @return string
+     */
+    public static function arrayToQfqJson(array $arr, $flagHtmlEntity = false) {
+
+        $json = '';
+        foreach ($arr as $arrKey => $arrValue) {
+            if ($flagHtmlEntity) {
+                $arrKey = htmlentities($arrKey, ENT_QUOTES);
+                $arrValue = htmlentities($arrValue, ENT_QUOTES);
+            }
+            $json .= ',' . json_encode(["key" => $arrKey, "value" => $arrValue]);
+        }
+
+        return '[' . substr($json, 1) . ']';
+    }
 }
\ No newline at end of file
diff --git a/extension/Classes/Core/Helper/Support.php b/extension/Classes/Core/Helper/Support.php
index 33f22db34..915642a7e 100644
--- a/extension/Classes/Core/Helper/Support.php
+++ b/extension/Classes/Core/Helper/Support.php
@@ -292,8 +292,8 @@ class Support {
      *
      * TinyMCE: Encoding JS Attributes (keys & values) for TinyMCE needs to be encapsulated in '&quot;' instead of '\"'.
      *
-     * @param        $str
-     * @param string $modeEscape
+     * @param string $str
+     * @param string $modeEscape ESCAPE_WITH_BACKSLASH | ESCAPE_WITH_HTML_QUOTE
      *
      * @return string
      * @throws \CodeException
diff --git a/extension/Classes/Core/Store/FillStoreForm.php b/extension/Classes/Core/Store/FillStoreForm.php
index 75c894eb0..144726696 100644
--- a/extension/Classes/Core/Store/FillStoreForm.php
+++ b/extension/Classes/Core/Store/FillStoreForm.php
@@ -175,7 +175,7 @@ class FillStoreForm {
 
         // The following will never be used during load (fe.type='upload').
         $skip = [FE_SLAVE_ID, FE_SQL_UPDATE, FE_SQL_INSERT, FE_SQL_DELETE, FE_SQL_AFTER, FE_SQL_BEFORE, FE_PARAMETER,
-            FE_VALUE, FE_FILL_STORE_VAR, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT];
+            FE_VALUE, FE_FILL_STORE_VAR, FE_TYPEAHEAD_GLUE_INSERT, FE_TYPEAHEAD_GLUE_DELETE, FE_TYPEAHEAD_TAG_INSERT, FE_TYPEAHEAD_INITIAL_SUGGESTION];
 
         $html = '';
         $newValues = array();
-- 
GitLab