From b5ed0b313a8745fd70fa843e4c07017f31cd93d2 Mon Sep 17 00:00:00 2001 From: Jim Cottrell Date: Tue, 19 Nov 2019 00:13:08 -0700 Subject: [PATCH] Restore support for Select2 data format in custom select control (#4712) Credit to @jimcottrell. Refs: #4413 --- modules/system/assets/ui/docs/select.md | 64 +++++++++++--- modules/system/assets/ui/js/select.js | 33 +++---- modules/system/assets/ui/storm-min.js | 7 +- tests/js/cases/system/ui.select.test.js | 111 ++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 26 deletions(-) create mode 100644 tests/js/cases/system/ui.select.test.js diff --git a/modules/system/assets/ui/docs/select.md b/modules/system/assets/ui/docs/select.md index 2b13f08d7..871698389 100644 --- a/modules/system/assets/ui/docs/select.md +++ b/modules/system/assets/ui/docs/select.md @@ -71,24 +71,68 @@ Use the `data-handler` attribute to source the select options from an AJAX handl > ``` -The AJAX handler should return results as an array. +The AJAX handler should return results in the [Select2 data format](https://select2.org/data-sources/formats). ```php public function onGetOptions() { - $results = [ - [ - 'id' => 1, - 'text' => 'Foobar', - ], - ... + return [ + 'results' => [ + [ + 'id' => 1, + 'text' => 'Foo' + ], + [ + 'id' => 2, + 'text' => 'Bar' + ] + ... + ] ]; - - return ['result' => $results]; } ``` -Due to the fact that JavaScript reorders numeric keys when interpreting the JSON data received by the AJAX handler, we suggest the method above for defining `results`. Support for the original `results` array format is however retained to ensure backwards compatibility. +Or a more full-featured example: + +```php +public function onGetOptions() +{ + return [ + 'results' => [ + [ + 'id' => 1, + 'text' => 'Foo', + 'disabled' => true + ], + [ + 'id' => 2, + 'text' => 'Bar', + 'selected' => true + ], + [ + 'text' => 'Group', + 'children' => [ + [ + 'id' => 3, + 'text' => 'Child 1' + ], + [ + 'id' => 4, + 'text' => 'Child 2' + ] + ... + ] + ] + ... + ], + 'pagination' => [ + 'more' => true + ] + ]; +} +``` + +The results array can be assigned to either the `result` or `results` key. As an alternative to the Select2 format, results can also be provided as an associative array (also assigned to either key). Due to the fact that JavaScript does not guarantee the order of object properties, we suggest the method above for defining results. ```php public function onGetOptions() diff --git a/modules/system/assets/ui/js/select.js b/modules/system/assets/ui/js/select.js index 86a3afe86..a7c6c5190 100644 --- a/modules/system/assets/ui/js/select.js +++ b/modules/system/assets/ui/js/select.js @@ -87,23 +87,26 @@ return $request }, processResults: function (data, params) { - var results = data.result; - var options = []; + var results = data.result || data.results, + options = [] - for (var i in results) { - if (results.hasOwnProperty(i)) { - var isObject = i != null && i.constructor.name === 'Object'; - - options.push({ - id: isObject ? results[i].id : i, - text: isObject ? results[i].text : results[i], - }); - }; - }; + delete(data.result) + if (results[0] && typeof(results[0]) === 'object') { // Pass through Select2 format + options = results + } + else { // Key-value map + for (var i in results) { + if (results.hasOwnProperty(i)) { + options.push({ + id: i, + text: results[i], + }) + } + } + } - return { - results: options, - }; + data.results = options + return data }, dataType: 'json' } diff --git a/modules/system/assets/ui/storm-min.js b/modules/system/assets/ui/storm-min.js index f3fe8efe3..23f2c4ebe 100644 --- a/modules/system/assets/ui/storm-min.js +++ b/modules/system/assets/ui/storm-min.js @@ -3514,7 +3514,12 @@ if($element.hasClass('select-hide-selected')){extraOptions.dropdownCssClass+=' s var source=$element.data('handler');if(source){extraOptions.ajax={transport:function(params,success,failure){var $request=$element.request(source,{data:params.data}) $request.done(success) $request.fail(failure) -return $request},processResults:function(data,params){var results=data.result;var options=[];for(var i in results){if(results.hasOwnProperty(i)){var isObject=i!=null&&i.constructor.name==='Object';options.push({id:isObject?results[i].id:i,text:isObject?results[i].text:results[i],});};};return{results:options,};},dataType:'json'}} +return $request},processResults:function(data,params){var results=data.result||data.results,options=[] +delete(data.result) +if(results[0]&&typeof(results[0])==='object'){options=results} +else{for(var i in results){if(results.hasOwnProperty(i)){options.push({id:i,text:results[i],})}}} +data.results=options +return data},dataType:'json'}} var separators=$element.data('token-separators') if(separators){extraOptions.tags=true extraOptions.tokenSeparators=separators.split('|') diff --git a/tests/js/cases/system/ui.select.test.js b/tests/js/cases/system/ui.select.test.js new file mode 100644 index 000000000..17f854789 --- /dev/null +++ b/tests/js/cases/system/ui.select.test.js @@ -0,0 +1,111 @@ +import { assert } from 'chai' +import fakeDom from 'helpers/fakeDom' + +describe('modules/system/assets/ui/js/select.js', function () { + describe('AJAX processResults function', function () { + let dom, + window, + processResults, + keyValResultFormat = { + value1: 'text1', + value2: 'text2' + }, + select2ResultFormat = [ + { + id: 'value1', + text: 'text1', + disabled: true + }, + { + id: 'value2', + text: 'text2', + selected: false + } + ] + + this.timeout(1000) + + beforeEach((done) => { + // Load framework.js and select.js in the fake DOM + dom = fakeDom(` + + + + + `) + + window = dom.window + + window.selectScript.onload = () => { + window.jQuery.fn.select2 = function(options) { + processResults = options.ajax.processResults + done() + } + } + }) + + afterEach(() => { + window.close() + }) + + it('supports a key-value mapping on the "result" key', function () { + let result = processResults({ result: keyValResultFormat }) + assert.deepEqual(result, { results: [ + { + id: 'value1', + text: 'text1' + }, + { + id: 'value2', + text: 'text2' + } + ]}) + }) + + it('supports a key-value mapping on the "results" key', function() { + let result = processResults({ results: keyValResultFormat }) + assert.deepEqual(result, { results: [ + { + id: 'value1', + text: 'text1' + }, + { + id: 'value2', + text: 'text2' + } + ]}) + }) + + it('passes through other data provided with key-value mapping', function() { + let result = processResults({ result: keyValResultFormat, other1: 1, other2: '2' }) + assert.include(result, { other1: 1, other2: '2'}) + }) + + it('supports the Select2 result format on the "result" key', function() { + let result = processResults({ result: select2ResultFormat }) + assert.deepEqual(result, { results: select2ResultFormat }) + }) + + it('passes through the Select2 result format on the "results" key', function() { + let result = processResults({ results: select2ResultFormat }) + assert.deepEqual(result, { results: select2ResultFormat }) + }) + + it('passes through other data provided with Select2 results format', function() { + let result = processResults({ results: select2ResultFormat, pagination: { more: true }, other: 'value' }) + assert.deepInclude(result, { pagination: { more: true }, other: 'value' }) + }) + + it('passes through the Select2 format with a group as the first entry', function() { + let data = [ + { + text: 'Label', + children: select2ResultFormat + } + ] + + let result = processResults({ results: data }) + assert.deepEqual(result, { results: data }) + }) + }) +})