diff --git a/README.md b/README.md index 01f3cc0..3dbb224 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,14 @@ With [NPM](https://www.npmjs.com/package/choices.js): ```zsh npm install choices.js --save ``` + With [Bower](https://bower.io/): ```zsh bower install choices.js --save ``` + Or include Choices directly: + ```html @@ -106,13 +109,7 @@ Or include Choices directly: loadingState: 'is-loading', }, callbackOnInit: null, - callbackOnAddItem: null, - callbackOnRemoveItem: null, - callbackOnHighlightItem: null, - callbackOnUnhighlightItem: null, callbackOnCreateTemplates: null, - callbackOnChange: null, - callbackOnSearch: null, }); ``` @@ -406,44 +403,6 @@ classNames: { **Usage:** Function to run once Choices initialises. -### callbackOnAddItem -**Type:** `Function` **Default:** `null` **Arguments:** `id, value, groupValue` - -**Input types affected:** `text`, `select-one`, `select-multiple` - -**Usage:** Function to run each time an item is added (programmatically or by the user). - -**Example:** - -```js -const example = new Choices(element, { - callbackOnAddItem: (id, value, groupValue) => { - // do something creative here... - }, -}; -``` - -### callbackOnRemoveItem -**Type:** `Function` **Default:** `null` **Arguments:** `id, value, groupValue` - -**Input types affected:** `text`, `select-one`, `select-multiple` - -**Usage:** Function to run each time an item is removed (programmatically or by the user). - -### callbackOnHighlightItem -**Type:** `Function` **Default:** `null` **Arguments:** `id, value, groupValue` - -**Input types affected:** `text`, `select-multiple` - -**Usage:** Function to run each time an item is highlighted. - -### callbackOnUnhighlightItem -**Type:** `Function` **Default:** `null` **Arguments:** `id, value, groupValue` - -**Input types affected:** `text`, `select-multiple` - -**Usage:** Function to run each time an item is unhighlighted. - ### callbackOnCreateTemplates **Type:** `Function` **Default:** `null` **Arguments:** `template` @@ -477,19 +436,73 @@ const example = new Choices(element, { }); ``` -### callbackOnChange -**Type:** `Function` **Default:** `null` **Arguments:** `value` +## Events +**Note:** Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object. + +**Example:** + +```js +const element = document.getElementById('example'); +const example = new Choices(element); + +element.addEventListener('addItem', function(event) { + // do something creative here... + console.log(event.detail.id); + console.log(event.detail.value); + console.log(event.detail.groupValue); +}, false); + +// or +const example = new Choices(document.getElementById('example')); + +example.passedElement.addEventListener('addItem', function(event) { + // do something creative here... + console.log(event.detail.id); + console.log(event.detail.value); + console.log(event.detail.groupValue); +}, false); + +``` + +### addItem +**Arguments:** `id, value, groupValue` **Input types affected:** `text`, `select-one`, `select-multiple` -**Usage:** Function to run each time an item is added/removed by a user. +**Usage:** Triggered each time an item is added (programmatically or by the user). -### callbackOnSearch -**Type:** `Function` **Default:** `null` **Arguments:** `value` +### removeItem +**Arguments:** `id, value, groupValue` -**Input types affected:** `select-one`, `select-multiple` +**Input types affected:** `text`, `select-one`, `select-multiple` -**Usage:** Function to run when a user types into an input to search choices. +**Usage:** Triggered each time an item is removed (programmatically or by the user). + +### highlightItem +**Arguments:** `id, value, groupValue` + +**Input types affected:** `text`, `select-multiple` + +**Usage:** Triggered each time an item is highlighted. + +### unhighlightItem +**Arguments:** `id, value, groupValue` + +**Input types affected:** `text`, `select-multiple` + +**Usage:** Triggered each time an item is unhighlighted. + +### change +**Arguments:** `value` + +**Input types affected:** `text`, `select-one`, `select-multiple` + +**Usage:** Triggered each time an item is added/removed **by a user**. + +### search +**Arguments:** `value` **Input types affected:** `select-one`, `select-multiple` + +**Usage:** Triggered when a user types into an input to search choices. ## Methods Methods can be called either directly or by chaining: diff --git a/assets/scripts/src/choices.js b/assets/scripts/src/choices.js index 0c28d4b..c6b858b 100644 --- a/assets/scripts/src/choices.js +++ b/assets/scripts/src/choices.js @@ -23,6 +23,7 @@ import { getWidthOfInput, sortByAlpha, sortByScore, + triggerEvent, } from './lib/utils.js'; import './lib/polyfills.js'; @@ -105,13 +106,7 @@ class Choices { loadingState: 'is-loading', }, callbackOnInit: null, - callbackOnAddItem: null, - callbackOnRemoveItem: null, - callbackOnHighlightItem: null, - callbackOnUnhighlightItem: null, callbackOnCreateTemplates: null, - callbackOnChange: null, - callbackOnSearch: null, }; // Merge options with user options @@ -223,8 +218,6 @@ class Choices { if (callback) { if (isType('Function', callback)) { callback.call(this); - } else { - console.error('callbackOnInit: Callback is not a function'); } } } @@ -449,24 +442,26 @@ class Choices { * @return {Object} Class instance * @public */ - highlightItem(item) { + highlightItem(item, runEvent = true) { if (!item) return; const id = item.id; const groupId = item.groupId; - const callback = this.config.callbackOnHighlightItem; + const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; + this.store.dispatch(highlightItem(id, true)); - // Run callback if it is a function - if (callback) { - if (isType('Function', callback)) { - const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; - if(group && group.value) { - callback.call(this, id, item.value, group.value); - } else { - callback.call(this, id, item.value) - } + if (runEvent) { + if(group && group.value) { + triggerEvent(this.passedElement, 'highlightItem', { + id, + value: item.value, + groupValue: group.value + }); } else { - console.error('callbackOnHighlightItem: Callback is not a function'); + triggerEvent(this.passedElement, 'highlightItem', { + id, + value: item.value, + }); } } @@ -483,22 +478,21 @@ class Choices { if (!item) return; const id = item.id; const groupId = item.groupId; - const callback = this.config.callbackOnUnhighlightItem; + const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; this.store.dispatch(highlightItem(id, false)); - // Run callback if it is a function - if (callback) { - if (isType('Function', callback)) { - const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; - if(group && group.value) { - callback.call(this, id, item.value, group.value); - } else { - callback.call(this, id, item.value) - } - } else { - console.error('callbackOnUnhighlightItem: Callback is not a function'); - } + if(group && group.value) { + triggerEvent(this.passedElement, 'unhighlightItem', { + id, + value: item.value, + groupValue: group.value + }); + } else { + triggerEvent(this.passedElement, 'unhighlightItem', { + id, + value: item.value, + }); } return this; @@ -580,15 +574,15 @@ class Choices { * @return {Object} Class instance * @public */ - removeHighlightedItems(runCallback = false) { + removeHighlightedItems(runEvent = false) { const items = this.store.getItemsFilteredByActive(); items.forEach((item) => { if (item.highlighted && item.active) { this._removeItem(item); // If this action was performed by the user - // run the callback - if (runCallback) { + // trigger the event + if (runEvent) { this._triggerChange(item.value); } } @@ -895,16 +889,10 @@ class Choices { */ _triggerChange(value) { if (!value) return; - const callback = this.config.callbackOnChange; - // Run callback if it is a function - if (callback) { - if (isType('Function', callback)) { - callback.call(this, value); - } else { - console.error('callbackOnChange: Callback is not a function'); - } - } + triggerEvent(this.passedElement, 'change', { + value + }); } /** @@ -1023,7 +1011,7 @@ class Choices { this._triggerChange(lastItem.value); } else { if (!hasHighlightedItems) { - this.highlightItem(lastItem); + this.highlightItem(lastItem, false); } this.removeHighlightedItems(true); } @@ -1173,7 +1161,6 @@ class Choices { if (!value) return; const choices = this.store.getChoices(); const hasUnactiveChoices = choices.some((option) => option.active !== true); - const callback = this.config.callbackOnSearch; // Run callback if it is a function if (this.input === document.activeElement) { @@ -1181,14 +1168,10 @@ class Choices { if (value && value.length > this.config.searchFloor) { // Filter available choices this._searchChoices(value); - // Run callback if it is a function - if (callback) { - if (isType('Function', callback)) { - callback.call(this, value); - } else { - console.error('callbackOnSearch: Callback is not a function'); - } - } + // Trigger search event + triggerEvent(this.passedElement, 'search', { + value, + }); } else if (hasUnactiveChoices) { // Otherwise reset choices to active this.isSearching = false; @@ -1853,7 +1836,12 @@ class Choices { const items = this.store.getItems(); const passedLabel = label || passedValue; const passedOptionId = parseInt(choiceId, 10) || -1; - const callback = this.config.callbackOnAddItem; + + // Get group if group ID passed + const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; + + // Generate unique id + const id = items ? items.length + 1 : 1; // If a prepended value has been passed, prepend it if (this.config.prependValue) { @@ -1865,27 +1853,24 @@ class Choices { passedValue += this.config.appendValue.toString(); } - // Generate unique id - const id = items ? items.length + 1 : 1; - this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId, groupId)); if (this.passedElement.type === 'select-one') { this.removeActiveItems(id); } - // Run callback if it is a function - if (callback) { - const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; - if (isType('Function', callback)) { - if(group && group.value) { - callback.call(this, id, passedValue, group.value); - } else { - callback.call(this, id, passedValue); - } - } else { - console.error('callbackOnAddItem: Callback is not a function'); - } + // Trigger change event + if(group && group.value) { + triggerEvent(this.passedElement, 'addItem', { + id, + value: passedValue, + groupValue: group.value, + }); + } else { + triggerEvent(this.passedElement, 'addItem', { + id, + value: passedValue, + }); } return this; @@ -1908,22 +1893,21 @@ class Choices { const value = item.value; const choiceId = item.choiceId; const groupId = item.groupId; - const callback = this.config.callbackOnRemoveItem; + const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; this.store.dispatch(removeItem(id, choiceId)); - // Run callback - if (callback) { - if (isType('Function', callback)) { - const group = groupId >= 0 ? this.store.getGroupById(groupId) : null; - if(group && group.value) { - callback.call(this, id, value, group.value); - } else { - callback.call(this, id, value); - } - } else { - console.error('callbackOnRemoveItem: Callback is not a function'); - } + if(group && group.value) { + triggerEvent(this.passedElement, 'removeItem', { + id, + value, + groupValue: group.value, + }); + } else { + triggerEvent(this.passedElement, 'removeItem', { + id, + value, + }); } return this; @@ -2100,6 +2084,7 @@ class Choices { if (callbackTemplate && isType('Function', callbackTemplate)) { userTemplates = callbackTemplate.call(this, strToEl); } + this.config.templates = extend(templates, userTemplates); } diff --git a/assets/scripts/src/lib/polyfills.js b/assets/scripts/src/lib/polyfills.js index f52a5f3..e45aed9 100644 --- a/assets/scripts/src/lib/polyfills.js +++ b/assets/scripts/src/lib/polyfills.js @@ -1,112 +1,129 @@ /* eslint-disable */ -// Production steps of ECMA-262, Edition 6, 22.1.2.1 -// Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from -if (!Array.from) { - Array.from = (function() { - var toStr = Object.prototype.toString; +(function () { + // Production steps of ECMA-262, Edition 6, 22.1.2.1 + // Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from + if (!Array.from) { + Array.from = (function() { + var toStr = Object.prototype.toString; - var isCallable = function(fn) { - return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; - }; + var isCallable = function(fn) { + return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; + }; - var toInteger = function(value) { - var number = Number(value); - if (isNaN(number)) { - return 0; - } - if (number === 0 || !isFinite(number)) { - return number; - } - return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); - }; + var toInteger = function(value) { + var number = Number(value); + if (isNaN(number)) { + return 0; + } + if (number === 0 || !isFinite(number)) { + return number; + } + return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); + }; - var maxSafeInteger = Math.pow(2, 53) - 1; + var maxSafeInteger = Math.pow(2, 53) - 1; - var toLength = function(value) { - var len = toInteger(value); - return Math.min(Math.max(len, 0), maxSafeInteger); - }; + var toLength = function(value) { + var len = toInteger(value); + return Math.min(Math.max(len, 0), maxSafeInteger); + }; - // The length property of the from method is 1. - return function from(arrayLike /*, mapFn, thisArg */ ) { - // 1. Let C be the this value. - var C = this; + // The length property of the from method is 1. + return function from(arrayLike /*, mapFn, thisArg */ ) { + // 1. Let C be the this value. + var C = this; - // 2. Let items be ToObject(arrayLike). - var items = Object(arrayLike); + // 2. Let items be ToObject(arrayLike). + var items = Object(arrayLike); - // 3. ReturnIfAbrupt(items). - if (arrayLike == null) { - throw new TypeError("Array.from requires an array-like object - not null or undefined"); - } - - // 4. If mapfn is undefined, then let mapping be false. - var mapFn = arguments.length > 1 ? arguments[1] : void undefined; - var T; - if (typeof mapFn !== 'undefined') { - // 5. else - // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. - if (!isCallable(mapFn)) { - throw new TypeError('Array.from: when provided, the second argument must be a function'); + // 3. ReturnIfAbrupt(items). + if (arrayLike == null) { + throw new TypeError("Array.from requires an array-like object - not null or undefined"); } - // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 2) { - T = arguments[2]; + // 4. If mapfn is undefined, then let mapping be false. + var mapFn = arguments.length > 1 ? arguments[1] : void undefined; + var T; + if (typeof mapFn !== 'undefined') { + // 5. else + // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. + if (!isCallable(mapFn)) { + throw new TypeError('Array.from: when provided, the second argument must be a function'); + } + + // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 2) { + T = arguments[2]; + } + } + + // 10. Let lenValue be Get(items, "length"). + // 11. Let len be ToLength(lenValue). + var len = toLength(items.length); + + // 13. If IsConstructor(C) is true, then + // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. + // 14. a. Else, Let A be ArrayCreate(len). + var A = isCallable(C) ? Object(new C(len)) : new Array(len); + + // 16. Let k be 0. + var k = 0; + // 17. Repeat, while k < len… (also steps a - h) + var kValue; + while (k < len) { + kValue = items[k]; + if (mapFn) { + A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); + } else { + A[k] = kValue; + } + k += 1; + } + // 18. Let putStatus be Put(A, "length", len, true). + A.length = len; + // 20. Return A. + return A; + }; + }()); + } + + // Reference: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/find + if (!Array.prototype.find) { + Array.prototype.find = function(predicate) { + 'use strict'; + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) { + return value; } } - - // 10. Let lenValue be Get(items, "length"). - // 11. Let len be ToLength(lenValue). - var len = toLength(items.length); - - // 13. If IsConstructor(C) is true, then - // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. - // 14. a. Else, Let A be ArrayCreate(len). - var A = isCallable(C) ? Object(new C(len)) : new Array(len); - - // 16. Let k be 0. - var k = 0; - // 17. Repeat, while k < len… (also steps a - h) - var kValue; - while (k < len) { - kValue = items[k]; - if (mapFn) { - A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); - } else { - A[k] = kValue; - } - k += 1; - } - // 18. Let putStatus be Put(A, "length", len, true). - A.length = len; - // 20. Return A. - return A; + return undefined; }; - }()); -} + } -// Reference: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/find -if (!Array.prototype.find) { - Array.prototype.find = function(predicate) { - 'use strict'; - if (this == null) { - throw new TypeError('Array.prototype.find called on null or undefined'); - } - if (typeof predicate !== 'function') { - throw new TypeError('predicate must be a function'); - } - var list = Object(this); - var length = list.length >>> 0; - var thisArg = arguments[1]; - var value; + function CustomEvent (event, params) { + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } - for (var i = 0; i < length; i++) { - value = list[i]; - if (predicate.call(thisArg, value, i, list)) { - return value; - } - } - return undefined; - }; -} \ No newline at end of file + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(); diff --git a/assets/scripts/src/lib/utils.js b/assets/scripts/src/lib/utils.js index 15e4aa7..e982acc 100644 --- a/assets/scripts/src/lib/utils.js +++ b/assets/scripts/src/lib/utils.js @@ -30,7 +30,7 @@ export const isNode = (o) => { return ( typeof Node === "object" ? o instanceof Node : o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string" - ); + ); }; /** @@ -42,7 +42,7 @@ export const isElement = (o) => { return ( typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string" - ); + ); }; /** @@ -58,7 +58,7 @@ export const extend = function() { * Merge one object into another * @param {Object} obj Object to merge into extended object */ - let merge = function(obj) { + let merge = function(obj) { for (let prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { // If deep merge and property is an object, merge properties @@ -91,7 +91,7 @@ export const extend = function() { */ export const whichTransitionEvent = function() { var t, - el = document.createElement("fakeelement"); + el = document.createElement("fakeelement"); var transitions = { "transition": "transitionend", @@ -113,7 +113,7 @@ export const whichTransitionEvent = function() { */ export const whichAnimationEvent = function() { var t, - el = document.createElement('fakeelement'); + el = document.createElement('fakeelement'); var animations = { 'animation': 'animationend', @@ -259,7 +259,7 @@ export const debounce = function(func, wait, immediate) { var timeout; return function() { var context = this, - args = arguments; + args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); @@ -459,6 +459,14 @@ export const getWidthOfInput = (input) => { return `${width}px`; }; +/** + * Sorting function for current and previous string + * @param {String} a Current value + * @param {String} b Next value + * @return {Number} -1 for after previous, + * 1 for before, + * 0 for same location + */ export const sortByAlpha = (a, b) => { const labelA = (a.label || a.value).toLowerCase(); const labelB = (b.label || b.value).toLowerCase(); @@ -468,6 +476,31 @@ export const sortByAlpha = (a, b) => { return 0; }; +/** + * Sort by numeric score + * @param {Object} a Current value + * @param {Object} b Next value + * @return {Number} -1 for after previous, + * 1 for before, + * 0 for same location + */ export const sortByScore = (a, b) => { return a.score - b.score; }; + +/** + * Trigger native event + * @param {NodeElement} element Element to trigger event on + * @param {String} type Type of event to trigger + * @param {Object} customArgs Data to pass with event + * @return {Object} Triggered event + */ +export const triggerEvent = (element, type, customArgs = null) => { + const event = new CustomEvent(type, { + detail: customArgs, + bubbles: true, + cancelable: true + }); + + return element.dispatchEvent(event); +}; diff --git a/index.html b/index.html index 8804897..e05632c 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ - + @@ -23,8 +23,8 @@ - - + +