diff --git a/.eslintrc.json b/.eslintrc.json index 5055cc4..4b6b5a0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,7 @@ "plugin:prettier/recommended", "plugin:compat/recommended" ], - "plugins": ["prettier"], + "plugins": ["prettier", "sort-class-members"], "env": { "es6": true, "browser": true @@ -37,6 +37,23 @@ "array": false, "object": true } + ], + "curly": ["error", "all"], + "newline-before-return": "error", + "sort-class-members/sort-class-members": [ + 2, + { + "order": [ + "[static-properties]", + "[static-methods]", + "[properties]", + "[conventional-private-properties]", + "constructor", + "[methods]", + "[conventional-private-methods]" + ], + "accessorPairPositioning": "getThenSet" + } ] }, "overrides": [ diff --git a/package-lock.json b/package-lock.json index 9fe0740..575cc4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3629,9 +3629,9 @@ } }, "eslint": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.5.1.tgz", - "integrity": "sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -3641,9 +3641,9 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.2", + "eslint-utils": "^1.4.3", "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.1", + "espree": "^6.1.2", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -3653,7 +3653,7 @@ "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", + "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -3679,15 +3679,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, "import-fresh": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", @@ -3698,21 +3689,6 @@ "resolve-from": "^4.0.0" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3754,9 +3730,9 @@ } }, "eslint-config-prettier": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.4.0.tgz", - "integrity": "sha512-YrKucoFdc7SEko5Sxe4r6ixqXPDP1tunGw91POeZTTRKItf/AMFYt/YLEQtZMkR2LVpAVhcAcZgcWpm1oGPW7w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", + "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -3990,6 +3966,12 @@ "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-sort-class-members": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sort-class-members/-/eslint-plugin-sort-class-members-1.6.0.tgz", + "integrity": "sha512-7+FEtnxeK0vtoPERAU7gtxpr//NsYYrPvMoDRIoqP6u999xbY7Lpgjr9KmalfXIOTuVxvRNgK7vJhAGVRi/z4A==", + "dev": true + }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", @@ -6619,78 +6601,87 @@ "dev": true }, "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", "dev": true, "requires": { - "ansi-escapes": "^3.2.0", + "ansi-escapes": "^4.2.1", "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", + "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", "run-async": "^2.2.0", "rxjs": "^6.4.0", - "string-width": "^2.1.0", + "string-width": "^4.1.0", "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "dev": true, + "requires": { + "type-fest": "^0.5.2" + } }, "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" } }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" } }, "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "requires": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, @@ -6704,24 +6695,14 @@ } }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" } }, "strip-ansi": { @@ -6731,15 +6712,13 @@ "dev": true, "requires": { "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } } + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "dev": true } } }, @@ -8396,9 +8375,9 @@ } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "mimic-response": { @@ -8588,9 +8567,9 @@ "dev": true }, "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, "nan": { diff --git a/package.json b/package.json index 34a475d..f9272ae 100644 --- a/package.json +++ b/package.json @@ -63,14 +63,15 @@ "chai": "^4.2.0", "csso-cli": "^3.0.0", "cypress": "3.5.0", - "eslint": "^6.5.1", + "eslint": "^6.6.0", "eslint-config-airbnb-base": "^14.0.0", - "eslint-config-prettier": "^6.4.0", + "eslint-config-prettier": "^6.5.0", "eslint-loader": "^3.0.2", "eslint-plugin-compat": "3.3.0", "eslint-plugin-cypress": "^2.7.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-sort-class-members": "^1.6.0", "express": "^4.16.4", "husky": "^3.0.9", "jsdom": "^15.2.0", diff --git a/src/scripts/choices.js b/src/scripts/choices.js index b8d7213..0fc5a19 100644 --- a/src/scripts/choices.js +++ b/src/scripts/choices.js @@ -351,11 +351,13 @@ class Choices { highlightAll() { this._store.items.forEach(item => this.highlightItem(item)); + return this; } unhighlightAll() { this._store.items.forEach(item => this.unhighlightItem(item)); + return this; } @@ -431,6 +433,7 @@ class Choices { const values = this._store.activeItems.reduce((selectedItems, item) => { const itemValue = valueOnly ? item.value : item; selectedItems.push(itemValue); + return selectedItems; }, []); @@ -446,6 +449,7 @@ class Choices { } items.forEach(value => this._setChoiceOrItem(value)); + return this; } @@ -539,12 +543,14 @@ class Choices { label = 'label', replaceChoices = false, ) { - if (!this.initialised) + if (!this.initialised) { throw new ReferenceError( `setChoices was called on a non-initialized instance of Choices`, ); - if (!this._isSelectElement) + } + if (!this._isSelectElement) { throw new TypeError(`setChoices can't be used with INPUT based Choices`); + } if (typeof value !== 'string' || !value) { throw new TypeError( @@ -558,10 +564,11 @@ class Choices { } if (!Array.isArray(choicesArrayOrFetcher)) { - if (typeof choicesArrayOrFetcher !== 'function') + if (typeof choicesArrayOrFetcher !== 'function') { throw new TypeError( `.setChoices must be called either with array of choices with a function resulting into Promise of array of choices`, ); + } // it's a choices fetcher requestAnimationFrame(() => this._handleLoadingState(true)); @@ -571,21 +578,26 @@ class Choices { return fetcher .then(data => this.setChoices(data, value, label, replaceChoices)) .catch(err => { - if (!this.config.silent) console.error(err); + if (!this.config.silent) { + console.error(err); + } }) .then(() => this._handleLoadingState(false)) .then(() => this); } // function returned something else than promise, let's check if it's an array of choices - if (!Array.isArray(fetcher)) + if (!Array.isArray(fetcher)) { throw new TypeError( `.setChoices first argument function must return either array of choices or Promise, got: ${typeof fetcher}`, ); + } + // recursion with results, it's sync and choices were cleared already return this.setChoices(fetcher, value, label, false); } this.containerOuter.removeLoadingState(); + const addGroupsAndChoices = groupOrChoice => { if (groupOrChoice.choices) { this._addGroup({ @@ -615,10 +627,13 @@ class Choices { clearChoices() { this._store.dispatch(clearChoices()); + + return this; } clearStore() { this._store.dispatch(clearAll()); + return this; } @@ -769,6 +784,7 @@ class Choices { if (this._isSelectOneElement) { return choice.groupId === group.id; } + return ( choice.groupId === group.id && (this.config.renderSelectedChoices === 'always' || !choice.selected) @@ -833,6 +849,7 @@ class Choices { } else { acc.normalChoices.push(choice); } + return acc; }, { placeholderChoices: [], normalChoices: [] }, @@ -980,7 +997,9 @@ class Choices { // If we are clicking on an option const { id } = element.dataset; const choice = this._store.getChoiceById(id); - if (!choice) return; + if (!choice) { + return; + } const passedKeyCode = activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null; const hasActiveDropdown = this.dropdown.isActive; @@ -1921,6 +1940,7 @@ class Choices { } const { templates, classNames } = this.config; + return templates[template].call(this, classNames, ...args); } @@ -2068,7 +2088,9 @@ class Choices { }); // If sorting is enabled or the user is searching, filter choices - if (this.config.shouldSort) allChoices.sort(filter); + if (this.config.shouldSort) { + allChoices.sort(filter); + } // Determine whether there is a selected choice const hasSelectedChoice = allChoices.some(choice => choice.selected); @@ -2212,6 +2234,7 @@ class Choices { return elements.reduce( (instances, element) => { instances.push(new Choices(element, config)); + return instances; }, [this], diff --git a/src/scripts/choices.test.js b/src/scripts/choices.test.js index d08afb9..5b1075c 100644 --- a/src/scripts/choices.test.js +++ b/src/scripts/choices.test.js @@ -15,16 +15,16 @@ describe('choices', () => { }); }; + beforeEach(() => { + passedElement = document.createElement('input'); + passedElement.type = 'text'; + passedElement.className = 'js-choices'; + document.body.appendChild(passedElement); + + instance = new Choices(passedElement); + }); + describe('public methods', () => { - beforeEach(() => { - passedElement = document.createElement('input'); - passedElement.type = 'text'; - passedElement.className = 'js-choices'; - document.body.appendChild(passedElement); - - instance = new Choices(passedElement); - }); - afterEach(() => { output = null; instance = null; @@ -798,6 +798,29 @@ describe('choices', () => { }); }); + describe('clearChoices', () => { + let storeDispatchStub; + + beforeEach(() => { + storeDispatchStub = stub(); + instance._store.dispatch = storeDispatchStub; + + output = instance.clearChoices(); + }); + + afterEach(() => { + instance._store.dispatch.reset(); + }); + + returnsInstance(output); + + it('dispatches clearChoices action', () => { + expect(storeDispatchStub.lastCall.args[0]).to.eql({ + type: ACTION_TYPES.CLEAR_CHOICES, + }); + }); + }); + describe('clearStore', () => { let storeDispatchStub; @@ -930,6 +953,7 @@ describe('choices', () => { expect(inst).to.eq(choice); fetcherCalled = true; await new Promise(resolve => setTimeout(resolve, 1000)); + return [ { label: 'l1', value: 'v1', customProperties: 'prop1' }, { label: 'l2', value: 'v2', customProperties: 'prop2' }, @@ -1440,7 +1464,9 @@ describe('choices', () => { }); }); }); + }); + describe('private methods', () => { describe('_createGroupsFragment', () => { let _createChoicesFragmentStub; const choices = [ diff --git a/src/scripts/components/dropdown.js b/src/scripts/components/dropdown.js index b914a6b..e1f61a8 100644 --- a/src/scripts/components/dropdown.js +++ b/src/scripts/components/dropdown.js @@ -15,6 +15,7 @@ export default class Dropdown { this.position = Math.ceil( this.dimensions.top + window.pageYOffset + this.element.offsetHeight, ); + return this.position; } @@ -35,6 +36,7 @@ export default class Dropdown { this.element.classList.add(this.classNames.activeState); this.element.setAttribute('aria-expanded', 'true'); this.isActive = true; + return this; } @@ -47,6 +49,7 @@ export default class Dropdown { this.element.classList.remove(this.classNames.activeState); this.element.setAttribute('aria-expanded', 'false'); this.isActive = false; + return this; } } diff --git a/src/scripts/components/input.js b/src/scripts/components/input.js index a5d59cb..f6a7a41 100644 --- a/src/scripts/components/input.js +++ b/src/scripts/components/input.js @@ -25,14 +25,14 @@ export default class Input { this.element.placeholder = placeholder; } - set value(value) { - this.element.value = value; - } - get value() { return sanitise(this.element.value); } + set value(value) { + this.element.value = value; + } + addEventListeners() { this.element.addEventListener('paste', this._onPaste); this.element.addEventListener('input', this._onInput, { diff --git a/src/scripts/components/wrapped-element.js b/src/scripts/components/wrapped-element.js index 04b2588..b02fea4 100644 --- a/src/scripts/components/wrapped-element.js +++ b/src/scripts/components/wrapped-element.js @@ -56,7 +56,8 @@ export default class WrappedElement { this.element.removeAttribute('data-choice'); // Re-assign values - this is weird, I know - this.element.value = this.element.value; + // @todo Figure out why we need to do this + this.element.value = this.element.value; // eslint-disable-line no-self-assign } enable() { diff --git a/src/scripts/components/wrapped-input.js b/src/scripts/components/wrapped-input.js index 0630937..8fdf5c6 100644 --- a/src/scripts/components/wrapped-input.js +++ b/src/scripts/components/wrapped-input.js @@ -6,6 +6,10 @@ export default class WrappedInput extends WrappedElement { this.delimiter = delimiter; } + get value() { + return this.element.value; + } + set value(items) { const itemValues = items.map(({ value }) => value); const joinedValues = itemValues.join(this.delimiter); @@ -13,8 +17,4 @@ export default class WrappedInput extends WrappedElement { this.element.setAttribute('value', joinedValues); this.element.value = joinedValues; } - - get value() { - return this.element.value; - } } diff --git a/src/scripts/lib/delegate-events.js b/src/scripts/lib/delegate-events.js new file mode 100644 index 0000000..cbe865f --- /dev/null +++ b/src/scripts/lib/delegate-events.js @@ -0,0 +1,45 @@ +window.delegateEvent = (function delegateEvent() { + let events; + let addedListenerTypes; + if (typeof events === 'undefined') { + events = new Map(); + } + if (typeof addedListenerTypes === 'undefined') { + addedListenerTypes = []; + } + + function _callback(event) { + const type = events.get(event.type); + + if (!type) { + return; + } + + type.forEach(fn => fn(event)); + } + + return { + add: function add(type, fn) { + // Cache list of events. + if (events.has(type)) { + events.get(type).push(fn); + } else { + events.set(type, [fn]); + } + // Setup events. + if (addedListenerTypes.indexOf(type) === -1) { + document.documentElement.addEventListener(type, _callback, true); + addedListenerTypes.push(type); + } + }, + remove: function remove(type, fn) { + if (!events.get(type)) { + return; + } + events.set(type, events.get(type).filter(item => item !== fn)); + if (!events.get(type).length) { + addedListenerTypes.splice(addedListenerTypes.indexOf(type), 1); + } + }, + }; +})(); diff --git a/src/scripts/lib/utils.js b/src/scripts/lib/utils.js index d0fe6a8..e35995a 100644 --- a/src/scripts/lib/utils.js +++ b/src/scripts/lib/utils.js @@ -36,6 +36,7 @@ export const wrap = (element, wrapper = document.createElement('div')) => { } else { element.parentNode.appendChild(wrapper); } + return wrapper.appendChild(element); }; @@ -92,6 +93,7 @@ export const sanitise = value => { export const strToEl = (() => { const tmpEl = document.createElement('div'); + return str => { const cleanedInput = str.trim(); tmpEl.innerHTML = cleanedInput; @@ -133,6 +135,7 @@ export const dispatchEvent = (element, type, customArgs = null) => { export const getWindowHeight = () => { const { body } = document; const html = document.documentElement; + return Math.max( body.scrollHeight, body.offsetHeight, diff --git a/src/scripts/reducers/choices.js b/src/scripts/reducers/choices.js index 266b063..8f10516 100644 --- a/src/scripts/reducers/choices.js +++ b/src/scripts/reducers/choices.js @@ -33,6 +33,7 @@ export default function choices(state = defaultState, action) { return state.map(obj => { const choice = obj; choice.active = action.active; + return choice; }); } @@ -45,6 +46,7 @@ export default function choices(state = defaultState, action) { if (choice.id === parseInt(action.choiceId, 10)) { choice.selected = true; } + return choice; }); } @@ -61,6 +63,7 @@ export default function choices(state = defaultState, action) { if (choice.id === parseInt(action.choiceId, 10)) { choice.selected = false; } + return choice; }); } @@ -76,8 +79,10 @@ export default function choices(state = defaultState, action) { choice.active = action.results.some(({ item, score }) => { if (item.id === choice.id) { choice.score = score; + return true; } + return false; }); @@ -89,6 +94,7 @@ export default function choices(state = defaultState, action) { return state.map(obj => { const choice = obj; choice.active = action.active; + return choice; }); } diff --git a/src/scripts/reducers/items.js b/src/scripts/reducers/items.js index a119404..2a520f9 100644 --- a/src/scripts/reducers/items.js +++ b/src/scripts/reducers/items.js @@ -23,6 +23,7 @@ export default function items(state = defaultState, action) { return newState.map(obj => { const item = obj; item.highlighted = false; + return item; }); } @@ -34,6 +35,7 @@ export default function items(state = defaultState, action) { if (item.id === action.id) { item.active = false; } + return item; }); } @@ -44,6 +46,7 @@ export default function items(state = defaultState, action) { if (item.id === action.id) { item.highlighted = action.highlighted; } + return item; }); } diff --git a/src/scripts/store/store.js b/src/scripts/store/store.js index caef472..e283544 100644 --- a/src/scripts/store/store.js +++ b/src/scripts/store/store.js @@ -125,6 +125,7 @@ export default class Store { const hasActiveOptions = choices.some( choice => choice.active === true && choice.disabled === false, ); + return isActive && hasActiveOptions; }, []); } @@ -145,8 +146,10 @@ export default class Store { getChoiceById(id) { if (id) { const n = parseInt(id, 10); + return this.activeChoices.find(choice => choice.id === n); } + return false; } diff --git a/src/scripts/templates.js b/src/scripts/templates.js index 20bf676..a20788e 100644 --- a/src/scripts/templates.js +++ b/src/scripts/templates.js @@ -16,13 +16,24 @@ export const TEMPLATES = /** @type {Templates} */ ({ const div = Object.assign(document.createElement('div'), { className: containerOuter, }); + div.dataset.type = passedElementType; - if (dir) div.dir = dir; - if (isSelectOneElement) div.tabIndex = 0; + + if (dir) { + div.dir = dir; + } + + if (isSelectOneElement) { + div.tabIndex = 0; + } + if (isSelectElement) { div.setAttribute('role', searchEnabled ? 'combobox' : 'listbox'); - if (searchEnabled) div.setAttribute('aria-autocomplete', 'list'); + if (searchEnabled) { + div.setAttribute('aria-autocomplete', 'list'); + } } + div.setAttribute('aria-haspopup', 'true'); div.setAttribute('aria-expanded', 'false'); @@ -63,20 +74,31 @@ export const TEMPLATES = /** @type {Templates} */ ({ className: item, innerHTML: label, }); + Object.assign(div.dataset, { item: '', id, value, customProperties, }); - if (active) div.setAttribute('aria-selected', 'true'); - if (disabled) div.setAttribute('aria-disabled', 'true'); - if (isPlaceholder) div.classList.add(placeholder); + if (active) { + div.setAttribute('aria-selected', 'true'); + } + + if (disabled) { + div.setAttribute('aria-disabled', 'true'); + } + + if (isPlaceholder) { + div.classList.add(placeholder); + } div.classList.add(highlighted ? highlightedState : itemSelectable); if (removeItemButton) { - if (disabled) div.classList.remove(itemSelectable); + if (disabled) { + div.classList.remove(itemSelectable); + } div.dataset.deletable = ''; /** @todo This MUST be localizable, not hardcoded! */ const REMOVE_ITEM_TEXT = 'Remove item'; @@ -99,8 +121,12 @@ export const TEMPLATES = /** @type {Templates} */ ({ const div = Object.assign(document.createElement('div'), { className: list, }); - if (!isSelectOneElement) div.setAttribute('aria-multiselectable', 'true'); + + if (!isSelectOneElement) { + div.setAttribute('aria-multiselectable', 'true'); + } div.setAttribute('role', 'listbox'); + return div; }, @@ -108,15 +134,26 @@ export const TEMPLATES = /** @type {Templates} */ ({ const div = Object.assign(document.createElement('div'), { className: `${group} ${disabled ? itemDisabled : ''}`, }); + div.setAttribute('role', 'group'); - Object.assign(div.dataset, { group: '', id, value }); - if (disabled) div.setAttribute('aria-disabled', 'true'); + + Object.assign(div.dataset, { + group: '', + id, + value, + }); + + if (disabled) { + div.setAttribute('aria-disabled', 'true'); + } + div.appendChild( Object.assign(document.createElement('div'), { className: groupHeading, innerHTML: value, }), ); + return div; }, @@ -140,17 +177,22 @@ export const TEMPLATES = /** @type {Templates} */ ({ disabled ? itemDisabled : itemSelectable } ${isPlaceholder ? placeholder : ''}`, }); + div.setAttribute('role', groupId > 0 ? 'treeitem' : 'option'); + Object.assign(div.dataset, { choice: '', id, value, selectText, }); + if (disabled) { div.dataset.choiceDisabled = ''; div.setAttribute('aria-disabled', 'true'); - } else div.dataset.choiceSelectable = ''; + } else { + div.dataset.choiceSelectable = ''; + } return div; }, @@ -162,21 +204,30 @@ export const TEMPLATES = /** @type {Templates} */ ({ autocapitalize: 'off', spellcheck: false, }); + inp.setAttribute('role', 'textbox'); inp.setAttribute('aria-autocomplete', 'list'); inp.setAttribute('aria-label', placeholderValue); + return inp; }, dropdown({ list, listDropdown }) { const div = document.createElement('div'); + div.classList.add(list, listDropdown); div.setAttribute('aria-expanded', 'false'); + return div; }, notice({ item, itemChoice, noResults, noChoices }, innerHTML, type = '') { const classes = [item, itemChoice]; - if (type === 'no-choices') classes.push(noChoices); - else if (type === 'no-results') classes.push(noResults); + + if (type === 'no-choices') { + classes.push(noChoices); + } else if (type === 'no-results') { + classes.push(noResults); + } + return Object.assign(document.createElement('div'), { innerHTML, className: classes.join(' '), @@ -184,8 +235,12 @@ export const TEMPLATES = /** @type {Templates} */ ({ }, option({ label, value, customProperties, active, disabled }) { const opt = new Option(label, value, false, active); - if (customProperties) opt.dataset.customProperties = customProperties; + + if (customProperties) { + opt.dataset.customProperties = customProperties; + } opt.disabled = disabled; + return opt; }, });