[TASK] Migrate Suggest Wizard to jQuery plugin 27/41427/5
authorBenjamin Mack <benni@typo3.org>
Thu, 16 Jul 2015 10:50:57 +0000 (12:50 +0200)
committerSusanne Moog <typo3@susannemoog.de>
Sat, 18 Jul 2015 17:38:33 +0000 (19:38 +0200)
Resolves: #68347
Releases: master
Change-Id: Ie51935ba3f6acbd33cab51e090fdc2aaeb573752
Reviewed-on: http://review.typo3.org/41427
Reviewed-by: Benjamin Kott <info@bk2k.info>
Tested-by: Benjamin Kott <info@bk2k.info>
Reviewed-by: Susanne Moog <typo3@susannemoog.de>
Tested-by: Susanne Moog <typo3@susannemoog.de>
Build/Resources/Public/Less/Component/autocomplete.less [new file with mode: 0644]
Build/Resources/Public/Less/_minimal.less
typo3/sysext/backend/Classes/Form/FormEngine.php
typo3/sysext/backend/Classes/Form/Wizard/SuggestWizard.php
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineSuggest.js [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js [deleted file]
typo3/sysext/t3skin/Resources/Public/Css/backend.css

diff --git a/Build/Resources/Public/Less/Component/autocomplete.less b/Build/Resources/Public/Less/Component/autocomplete.less
new file mode 100644 (file)
index 0000000..8b2d974
--- /dev/null
@@ -0,0 +1,53 @@
+//
+// Autocomplete
+// ============
+//
+
+
+//
+// Variables
+//
+@autocomplete-border:                   #ddd;
+@autocomplete-border-radius:            2px;
+@autocomplete-results-bg:               #fff;
+@autocomplete-zindex:                   @zindex-dropdown;
+@autocomplete-suggestion-link-hover-bg: #fafafa;
+
+
+//
+// Component
+//
+.autocomplete {
+       position: relative;
+}
+.autocomplete-results {
+       z-index: @autocomplete-zindex;
+       position: absolute;
+       margin: 5px 0;
+       top: 100%;
+       right: 0;
+       border: 1px solid @autocomplete-border;
+       border-radius: @autocomplete-border-radius;
+       background-color: @autocomplete-results-bg;
+       overflow: hidden;
+       box-shadow: 0 1px 0 0 rgba(0,0,0,0.25);
+}
+
+.autocomplete-suggestion {
+       border-top: 1px solid @autocomplete-border;
+       &:first-child {
+               border-top: none;
+       }
+}
+.autocomplete-suggestion-link {
+       padding: 5px 13px 5px 28px;
+       display: block;
+       text-decoration: none;
+       &:hover {
+               background-color: @autocomplete-suggestion-link-hover-bg;
+               text-decoration: none;
+       }
+}
+.autocomplete-info {
+       padding: 5px 15px;
+}
\ No newline at end of file
index 1af4b9d..9c40319 100644 (file)
@@ -69,6 +69,7 @@
 //
 // Custom Components
 //
+@import "Component/autocomplete.less";
 @import "Component/avatar.less";
 @import "Component/callout.less";
 
index 30aa048..aba0ff4 100644 (file)
@@ -1273,10 +1273,6 @@ class FormEngine {
                                $this->loadJavascriptLib('sysext/core/Resources/Public/JavaScript/Contrib/placeholders.jquery.min.js');
                        }
 
-                       // @todo: remove scriptaclous once suggest & flex form foo is moved to RequireJS, see #55575
-                       $pageRenderer->loadScriptaculous();
-                       $this->loadJavascriptLib('sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js');
-
                        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Filelist/FileListLocalisation');
                        $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DragUploader');
 
index 45cca04..ef2f97a 100644 (file)
@@ -26,11 +26,6 @@ use TYPO3\CMS\Lang\LanguageService;
 class SuggestWizard {
 
        /**
-        * @var string
-        */
-       protected $cssClass = 'typo3-TCEforms-suggest';
-
-       /**
         * Renders an ajax-enabled text field. Also adds required JS
         *
         * @param string $fieldname The fieldname in the form
@@ -42,8 +37,6 @@ class SuggestWizard {
         */
        public function renderSuggestSelector($fieldname, $table, $field, array $row, array $config) {
                $languageService = $this->getLanguageService();
-               $containerCssClass = $this->cssClass . ' ' . $this->cssClass . '-position-right';
-               $suggestId = 'suggest-' . $table . '-' . $field . '-' . $row['uid'];
                $isFlexFormField = $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex';
                if ($isFlexFormField) {
                        $fieldPattern = 'data[' . $table . '][' . $row['uid'] . '][';
@@ -51,17 +44,7 @@ class SuggestWizard {
                        $flexformField = substr($flexformField, 0, -1);
                        $field = str_replace(array(']['), '|', $flexformField);
                }
-               $selector = '
-               <div class="' . $containerCssClass . '" id="' . $suggestId . '">
-                       <div class="input-group">
-                               <span class="input-group-addon"><i class="fa fa-search"></i></span>
-                               <input type="search" id="' . htmlspecialchars($fieldname) . 'Suggest" value="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord') . '" class="form-control ' . $this->cssClass . '-search" />
-                               <div class="' . $this->cssClass . '-indicator" style="display: none;" id="' . htmlspecialchars($fieldname) . 'SuggestIndicator">
-                                       <span class="fa fa-spin fa-spinner" title="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:alttext.suggestSearching', TRUE) . '"></span>
-                               </div>
-                               <div class="' . $this->cssClass . '-choices" style="display: none;" id="' . htmlspecialchars($fieldname) . 'SuggestChoices"></div>
-                       </div>
-               </div>';
+
                // Get minimumCharacters from TCA
                $minChars = 0;
                if (isset($config['fieldConf']['config']['wizards']['suggest']['default']['minimumCharacters'])) {
@@ -80,20 +63,30 @@ class SuggestWizard {
                }
 
                $jsRow = '';
-               if ($isFlexFormField && !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
+               if ($isFlexFormField || !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
                        // Ff we have a new record, we hand that row over to JS.
                        // This way we can properly retrieve the configuration of our wizard
                        // if it is shown in a flexform
                        $jsRow = serialize($row);
                }
 
-               // Replace "-" with ucwords for the JS object name
-               $jsObj = str_replace(' ', '', ucwords(str_replace(array('-', '.'), ' ', GeneralUtility::strtolower($suggestId))));
-               $selector .=
-                       '<script type="text/javascript">' . LF .
-                               'var ' . $jsObj . ' = new TCEForms.Suggest("' . $fieldname . '", "' . $table . '", "' . $field . '", "' . $row['uid'] . '", ' . $row['pid'] . ', ' . $minChars . ', "' . $type . '", ' . GeneralUtility::quoteJSvalue($jsRow) . ');' . LF .
-                               $jsObj . '.defaultValue = "' . GeneralUtility::slashJS($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord')) . '";' . LF .
-                       '</script>' . LF;
+               $selector = '
+               <div class="autocomplete t3-form-suggest-container">
+                       <div class="input-group">
+                               <span class="input-group-addon"><i class="fa fa-search"></i></span>
+                               <input type="search" class="t3-form-suggest form-control"
+                                       placeholder="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord') . '"
+                                       data-fieldname="' . $fieldname . '"
+                                       data-table="' . $table . '"
+                                       data-field="' . $field . '"
+                                       data-uid="' . $row['uid'] . '"
+                                       data-pid="' . $row['pid'] . '"
+                                       data-fieldtype="' . $type . '"
+                                       data-minchars="' . $minChars .'"
+                                       data-recorddata="' . htmlspecialchars($jsRow) .'"
+                               />
+                       </div>
+               </div>';
 
                return $selector;
        }
@@ -217,16 +210,10 @@ class SuggestWizard {
                $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
                $maxItems = min(count($resultRows), $maxItems);
 
-               $rowIdSuffix = '-' . $table . '-' . $uid . '-' . $field;
-               $listItems = $this->createListItemsFromResultRow($resultRows, $maxItems, $rowIdSuffix);
+               $listItems = $this->createListItemsFromResultRow($resultRows, $maxItems);
 
-               if (!empty($listItems)) {
-                       $list = implode('', $listItems);
-               } else {
-                       $list = '<li class="suggest-noresults"><i>' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noRecordFound') . '</i></li>';
-               }
-               $list = '<ul class="' . $this->cssClass . '-resultlist">' . $list . '</ul>';
-               $ajaxObj->addContent(0, $list);
+               $ajaxObj->setContent($listItems);
+               $ajaxObj->setContentFormat('json');
        }
 
        /**
@@ -354,7 +341,7 @@ class SuggestWizard {
         * @param string $rowIdSuffix
         * @return array
         */
-       protected function createListItemsFromResultRow(array $resultRows, $maxItems, $rowIdSuffix) {
+       protected function createListItemsFromResultRow(array $resultRows, $maxItems) {
                if (empty($resultRows)) {
                        return array();
                }
@@ -370,11 +357,8 @@ class SuggestWizard {
 
                // put together the selector entries
                for ($i = 0; $i < $maxItems; ++$i) {
-                       $row = $resultRows[$rowsSort[$i]];
-                       $rowId = $row['table'] . '-' . $row['uid'] . $rowIdSuffix;
-                       $listItems[] = '<li' . ($row['class'] !== '' ? ' class="' . $row['class'] . '"' : '') . ' id="' . $rowId . '"' . ($row['style'] !== '' ? ' style="' . $row['style'] . '"' : '') . '>' . $row['sprite'] . $row['text'] . '</li>';
+                       $listItems[] = $resultRows[$rowsSort[$i]];
                }
-
                return $listItems;
        }
 
index 95d82a0..b72d025 100644 (file)
@@ -764,6 +764,11 @@ define('TYPO3/CMS/Backend/FormEngine', ['jquery'], function ($) {
                                $('.t3js-clearable').clearable();
                        });
                }
+               if ($('.t3-form-suggest').length) {
+                       require(['TYPO3/CMS/Backend/FormEngineSuggest'], function(Suggest) {
+                               Suggest($('.t3-form-suggest'));
+                       });
+               }
                // apply DatePicker to all date time fields
                require(['TYPO3/CMS/Backend/DateTimePicker'], function(DateTimePicker) {
                        DateTimePicker.initialize();
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineSuggest.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngineSuggest.js
new file mode 100644 (file)
index 0000000..4cc6f22
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Functionality to load suggest functionality
+ */
+define('TYPO3/CMS/Backend/FormEngineSuggest', ['jquery', 'jquery/autocomplete'], function ($) {
+       var initialize = function($searchField) {
+               var $containerElement = $searchField.closest('.t3-form-suggest-container');
+               var table = $searchField.data('table'),
+                       field = $searchField.data('field'),
+                       uid = $searchField.data('uid'),
+                       pid = $searchField.data('pid'),
+                       newRecordRow = $searchField.data('recorddata'),
+                       minimumCharacters = $searchField.data('minchars'),
+                       url = TYPO3.settings.ajaxUrls['t3lib_TCEforms_suggest::searchRecord']
+                                 + '&table=' + table
+                                 + '&field=' + field
+                                 + '&uid=' + uid
+                                 + '&pid=' + pid
+                                 + '&newRecordRow=' + newRecordRow;
+
+               $searchField.autocomplete({
+                       // ajax options
+                       serviceUrl: url,
+                       paramName: 'value',
+                       dataType: 'json',
+                       minChars: minimumCharacters,
+                       groupBy: 'typeLabel',
+                       containerClass: 'autocomplete-results',
+                       appendTo: $containerElement,
+                       forceFixPosition: false,
+                       preserveInput: true,
+                       showNoSuggestionNotice: true,
+                       noSuggestionNotice: '<div class="autocomplete-info">No results</div>',
+                       minLength: minimumCharacters,
+                       // put the AJAX results in the right format
+                       transformResult: function(response) {
+                               return {
+                                       suggestions: $.map(response, function(dataItem) {
+                                               return { value: dataItem.text, data: dataItem };
+                                       })
+                               };
+                       },
+                       // Rendering of each item
+                       formatResult: function(suggestion, value) {
+                               return '<a class="autocomplete-suggestion-link" href="#" data-label="' + suggestion.data.label + '" data-table="' + suggestion.data.table + '" data-uid="' + suggestion.data.uid + '">' +
+                                               suggestion.data.sprite + suggestion.data.text +
+                                       '</a>';
+                       },
+                       onSearchComplete: function() {
+                               $containerElement.addClass('open');
+                       },
+                       beforeRender: function(container) {
+                               // Unset height, width and z-index again, should be fixed by the plugin at a later point
+                               container.attr('style', '');
+                               $containerElement.addClass('open');
+                       },
+                       onHide: function() {
+                               $containerElement.removeClass('open');
+                       }
+               });
+
+               // set up the events
+               $containerElement.on('click', '.autocomplete-suggestion-link', function(evt) {
+                       evt.preventDefault();
+                       var insertData = '';
+                       if ($searchField.data('fieldtype') == 'select') {
+                               insertData = $(this).data('uid');
+                       } else {
+                               insertData = $(this).data('table') + '_' + $(this).data('uid');
+                       }
+
+                       var formEl = $searchField.data('fieldname');
+                       setFormValueFromBrowseWin(formEl, insertData, $(this).data('label'), $(this).data('label'));
+                       TBE_EDITOR.fieldChanged(table, uid, field, formEl);
+               });
+       };
+
+       /**
+        * return a function that gets DOM elements that are checked if suggest is already initialized
+        */
+       return function(selectorElements) {
+               $(selectorElements).each(function(key, el) {
+                       if (!$(el).data('t3-suggest-initialized')) {
+                               initialize($(el));
+                               $(el).data('t3-suggest-initialized', true);
+                       }
+               });
+       };
+});
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js b/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.tceforms_suggest.js
deleted file mode 100644 (file)
index 6ec3f1c..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-/**
- * Class for JS handling of suggest fields in TCEforms.
- */
-if (!TCEForms) {
-       var TCEForms = {};
-}
-
-TCEForms.Suggest = Class.create({
-       objectId: '',
-       suggestField: '',
-       suggestResultList: '',
-       minimumCharacters: 2,
-       defaultValue: '',
-       fieldType: '',
-
-       /**
-        * Wrapper for script.aculo.us' Autocompleter functionality: Assigns a new autocompletion object to
-        * the input field of a given Suggest selector.
-        *
-        * @param  string  objectId  The ID of the object to assign the completer to
-        * @param  string  table     The table of the record which is currently edited
-        * @param  string  field     The field which is currently edited
-        * @param  integer uid       The uid of the record which is currently edited
-        * @param  integer pid       The pid of the record which is currently edited
-        * @param  integer minimumCharacters the minimum characters that is need to trigger the initial search
-        * @param  string  fieldType The TCA type of the field (e.g. group, select, ...)
-        * @param string newRecordRow JSON encoded new content element. Only set when new record is inside flexform
-        */
-       initialize: function(objectId, table, field, uid, pid, minimumCharacters, fieldType, newRecordRow) {
-               this.objectId = objectId;
-               this.suggestField = objectId + 'Suggest';
-               this.suggestResultList = objectId + 'SuggestChoices';
-               this.fieldType = fieldType;
-
-               new Ajax.Autocompleter(this.suggestField, this.suggestResultList, TYPO3.settings.ajaxUrls['t3lib_TCEforms_suggest::searchRecord'], {
-                               paramName: 'value',
-                               minChars: (minimumCharacters ? minimumCharacters : this.minimumCharacters),
-                               updateElement: this.addElementToList.bind(this),
-                               parameters: 'table=' + table + '&field=' + field + '&uid=' + uid + '&pid=' + pid + '&newRecordRow=' + newRecordRow,
-                               indicator: objectId + 'SuggestIndicator'
-                       }
-               );
-
-               $(this.suggestField).observe('focus', this.checkDefaultValue.bind(this));
-               $(this.suggestField).observe('keydown', this.checkDefaultValue.bind(this));
-       },
-
-       /**
-        * check for default value of the input field and set it to empty once somebody wants to type something in
-        */
-       checkDefaultValue: function() {
-               if ($(this.suggestField).value == this.defaultValue) {
-                       $(this.suggestField).value = '';
-               }
-       },
-
-       /**
-        * Adds an element to the list of items in a group selector.
-        *
-        * @param  object  item  The item to add to the list (usually an li element)
-        * @return void
-        */
-       addElementToList: function(item) {
-               if (item.className.indexOf('noresults') == -1) {
-                       var arr = item.id.split('-');
-                       var ins_table = arr[0];
-                       var ins_uid = arr[1];
-                       var ins_uid_string = (this.fieldType == 'select') ? ins_uid : (ins_table + '_' + ins_uid);
-                       var rec_table = arr[2];
-                       var rec_uid = arr[3];
-                       var rec_field = arr[4];
-
-                       var formEl = this.objectId;
-
-                       var suggestLabelNode = Element.select(item, '.suggest-label')[0];
-                       var label = suggestLabelNode.textContent ? suggestLabelNode.textContent : suggestLabelNode.innerText;
-                       var suggestLabelTitleNode = Element.select(suggestLabelNode, '[title]')[0];
-                       var title = suggestLabelTitleNode ? suggestLabelTitleNode.readAttribute('title') : '';
-
-                       setFormValueFromBrowseWin(formEl, ins_uid_string, label, title);
-                       TBE_EDITOR.fieldChanged(rec_table, rec_uid, rec_field, formEl);
-
-                       $(this.suggestField).value = this.defaultValue;
-               }
-       }
-});
index 1b8af8a..d25bc53 100644 (file)
@@ -7347,6 +7347,39 @@ button.close {
     bottom: 20px;
   }
 }
+.autocomplete {
+  position: relative;
+}
+.autocomplete-results {
+  z-index: 1000;
+  position: absolute;
+  margin: 5px 0;
+  top: 100%;
+  right: 0;
+  border: 1px solid #dddddd;
+  border-radius: 2px;
+  background-color: #ffffff;
+  overflow: hidden;
+  box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.25);
+}
+.autocomplete-suggestion {
+  border-top: 1px solid #dddddd;
+}
+.autocomplete-suggestion:first-child {
+  border-top: none;
+}
+.autocomplete-suggestion-link {
+  padding: 5px 13px 5px 28px;
+  display: block;
+  text-decoration: none;
+}
+.autocomplete-suggestion-link:hover {
+  background-color: #fafafa;
+  text-decoration: none;
+}
+.autocomplete-info {
+  padding: 5px 15px;
+}
 .avatar {
   position: relative;
   display: inline-block;