Monthly Archives: June 2020
JavaScript || Sort.by.js – Simple Array Sorting With Multiple Sorting Conditions Using Vanilla JavaScript
The following is a module that handles complex array sorting. Much like the SQL/LINQ ‘Order By’ operation, this module allows sorting capabilities with multiple sorting conditions for arrays of any type.
Contents
1. Usage
2. Sorting Condition
3. Comparison
4. Null Values
5. Filtering & Selecting Items
6. Sort.by.js Namespace
7. Notes
8. More Examples
Syntax is very straightforward. The following demonstrates sorting a simple array:
1 2 3 4 5 6 7 8 9 |
// Sort array with no sorting conditions. // By default, the array will be sorted in ascending order console.log( Sort.by( [5,4,3,2,1] ) ); // expected output: /* [1,2,3,4,5] */ |
To sort a simple array in descending order, it can be done like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Sort array with one sorting condition. // This array is sorted in descending order console.log( Sort.by( [10,11,12,13,14,15], [ { value:(x) => x, desc: true } ]) ); // expected output: /* [15,14,13,12,11,10] */ |
In the example above, the second parameter of the ‘Sort.by‘ function defines the sorting condition by which the array should be sorted.
2. Sorting Condition
A sorting condition defines how the array should be sorted. It is made up of 3 properties.
1 2 3 4 5 6 7 8 |
[ { value: (x) => x.foo, // Indicates the value to be sorted for this condition desc: true / false, // Optional: Indicates the sorting direction for this condition. true = descending, false = ascending. Default sorting direction is ascending compare: (x, y) => x.localeCompare(y) // Optional: Overrides the default compare function for this condition }, // {...} // Additional sorting conditions ] |
The ‘value‘ property is a function which defines how the value for the sorting condition should be determined. The ‘desc‘ property is a boolean, which defines the sorting direction of the sorting condition. The ‘compare‘ property is a function, which overrides the default compare function for the sorting condition.
More than one sorting condition can be used. The array is sorted in the order in which the conditions were specified (FIFO).
The following example sorts an object array with multiple sorting conditions. In this example, the array is first sorted by the full name length in descending order, followed by the id field sorted in ascending order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// Sort object array with more than one sorting condition. class Person { constructor(id, firstName, lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } getFullName() { return `${this.firstName} ${this.lastName}` } }; let people = [ new Person(31, 'Kenneth', 'P'), new Person(28, 'Jennifer', 'N'), new Person(5, 'Lynn', 'P'), new Person(22, 'Kenneth', 'P'), new Person(19, 'Jennifer', 'N'), ]; // First, sorts by the full name length in descending order // followed by the id field sorted in ascending order console.log( Sort.by(people, [ { value: (x) => x.getFullName().length, desc: true }, { value: (x) => x.id }, ]) ); // expected output: /* [ { "id": 19, "firstName": "Jennifer", "lastName": "N" }, { "id": 28, "firstName": "Jennifer", "lastName": "N" }, { "id": 22, "firstName": "Kenneth", "lastName": "P" }, { "id": 31, "firstName": "Kenneth", "lastName": "P" }, { "id": 5, "firstName": "Lynn", "lastName": "P" } ] */ |
3. Comparison
Overriding the default comparison function is optional. But, when it is used, it takes precedence over the ‘desc’ property. That is because when supplying the custom comparison function, that ultimately defines the sorting direction and behavior.
The following example demonstrates the use of the comparison function to sort a mixed array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Sort a multi dimensional array with multiple conditions. let mixedArray = [ [0, 'Aluminium', 0, 'Francis'], [1, 'Argon', 1, 'Ada'], [2, 'Brom', 2, 'John'], [3, 'Cadmium', 9, 'Marie'], [4, 'Fluor', 12, 'Marie'], [5, 'Gold', 1, 'Ada'], [6, 'Kupfer', 4, 'Ines'], [7, 'Krypton', 4, 'Joe'], [8, 'Sauerstoff', 0, 'Marie'], [9, 'Zink', 5, 'Max'] ]; // This array is sorted in ascending order at index 3, // ascending order at index 2, // and descending order at index 1 console.log( Sort.by(mixedArray, [ { value: (x) => x[3], compare: (x, y) => x.localeCompare(y) }, { value: (x) => x[2], desc: false }, { value: (x) => x[1], compare: (x, y) => y.localeCompare(x) }, ]) ); // expected output: /* [ [ 5, "Gold", 1, "Ada" ], [ 1, "Argon", 1, "Ada" ], [ 0, "Aluminium", 0, "Francis" ], [ 6, "Kupfer", 4, "Ines" ], [ 7, "Krypton", 4, "Joe" ], [ 2, "Brom", 2, "John" ], [ 8, "Sauerstoff", 0, "Marie" ], [ 3, "Cadmium", 9, "Marie" ], [ 4, "Fluor", 12, "Marie" ], [ 9, "Zink", 5, "Max" ] ] */ |
4. Null Array Values
The default comparison function automatically handles null values by placing them at the end of the array, regardless of the sorting direction. This behavior can be overridden by supplying your own user defined comparison function.
The following example demonstrates sorting an array with null values.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Sort an array with one condition. // This demonstrates null value behavior let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; // This array is sorted in ascending order, with // null values placed at the end of the array console.log( Sort.by(arrayWithNullValues, [ { value: (x) => x ? parseFloat(x) : x } ]) ); // expected output: /* ["0.254","1.68","36","211",1987,1991,null,null,null,null,null] */ |
In the example below, the default comparison function is overridden, but null values are un-handled. This leaves it up to the user to define it’s behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Sort an array with one condition. // This demonstrates null value behavior with a custom // comparison function let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; // This array is sorted in ascending order, but // null values are un-handled console.log( Sort.by(arrayWithNullValues, [ // The sorting condition below overrides the // compare function but does not handle null values { value: (x) => x ? parseFloat(x) : x, compare:(x, y) => x - y } ]) ); // expected output: /* ["0.254","1.68","36","211",null,null,null,null,null,1987,1991] */ |
5. Filtering & Selecting Items
Filtering and selecting result items can be achieved by using either the Array.filter() and Array.map() technique, or by using Array.reduce().
The example below demonstrates this. It first sorts an object array with multiple conditions. The array is sorted by score DESC, time ASC, and age ASC. It then filters the result array by a certain score, and returns a new list which contains only the score, time, and age properties.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// Sort an array with miltiple conditions. // This demonstrates then filtering & selecting values let users = [ {USER:"bob", SCORE:2000, TIME:32, AGE:16, COUNTRY:"US"}, {USER:"jane", SCORE:4000, TIME:35, AGE:16, COUNTRY:"DE"}, {USER:"tim", SCORE:1000, TIME:30, AGE:17, COUNTRY:"UK"}, {USER:"mary", SCORE:1500, TIME:31, AGE:19, COUNTRY:"PL"}, {USER:"joe", SCORE:2500, TIME:33, AGE:18, COUNTRY:"US"}, {USER:"sally", SCORE:2000, TIME:30, AGE:16, COUNTRY:"CA"}, {USER:"yuri", SCORE:3000, TIME:34, AGE:19, COUNTRY:"RU"}, {USER:"anita", SCORE:2500, TIME:32, AGE:17, COUNTRY:"LV"}, {USER:"mark", SCORE:2000, TIME:30, AGE:18, COUNTRY:"DE"}, {USER:"amy", SCORE:1500, TIME:29, AGE:19, COUNTRY:"UK"} ]; // Sort an object with multiple conditions. // The array is sorted by score DESC, time ASC, age ASC. console.log( Sort.by(users, [ { value: (x) => x.SCORE, desc: true }, { value: (x) => x.TIME }, { value: (x) => x.AGE }, ]).reduce((acc, x) => { x.SCORE >= 2000 && acc.push( {SCORE: x.SCORE, TIME: x.TIME, AGE: x.AGE} ); return acc }, []) // You could also achive the same result as above using the filter & map method //.filter( (x) => x.SCORE >= 2000 ).map( (x) => ( {SCORE: x.SCORE, TIME: x.TIME, AGE: x.AGE} ) ) ); // expected output: /* [ { "SCORE": 4000, "TIME": 35, "AGE": 16 }, { "SCORE": 3000, "TIME": 34, "AGE": 19 }, { "SCORE": 2500, "TIME": 32, "AGE": 17 }, { "SCORE": 2500, "TIME": 33, "AGE": 18 }, { "SCORE": 2000, "TIME": 30, "AGE": 16 }, { "SCORE": 2000, "TIME": 30, "AGE": 18 }, { "SCORE": 2000, "TIME": 32, "AGE": 16 } ] */ |
It should be noted that using the Array.filter() and Array.map() technique iterates the array twice. This same effect can be achieved by using Array.reduce(), which only iterates the array once, thus being more efficient.
6. Sort.by.js Namespace
The following is the Sort.by.js Namespace. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 27, 2020 // Taken From: http://programmingnotes.org/ // File: Sort.by.js // Description: Namespace which handles complex array sorting. This allows // SQL/LINQ like sorting capabilities with multiple sorting conditions // for arrays of any type. // Example: // // Sort a simple array in descending order // let sorted = Sort.by( [28,91,10,87,19,32], // [ // { value:(x) => x, desc: true } // // Custom compare example: // // { value:(x) => String(x), compare: (x, y) => y.localeCompare(x) } // ]); // ============================================================================ /** * NAMESPACE: Sort * USE: Handles complex array sorting with multiple sorting conditions. */ var Sort = Sort || {}; // Declare the namespace members (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; /** * FUNCTION: by * USE: Sorts an array in ascending or descending order depending * on the sorting conditions. This method returns a new, sorted * array. The original array is left untouched * @param arry: The array to be sorted. * @param conditions: The conditions by which the array should be sorted. * Accepts multiple sorting conditions. The array is sorted in the * order in which the conditions were placed (FIFO) * @return: The sorted array according to the sorting conditions. */ exposed.by = (arry, conditions = null) => { // Make sure the array is valid verifyArray(arry); // Make sure the sorting conditions are in the expected format conditions = verifyConditions(conditions); // Create a seperate array that holds only the computed values we want to sort. // This is done to reduce overhead in the .sort function, so item // values are only computed once, as opposed to possibly multiple times // for each comparison. let sortable = mapArray(arry, conditions); // Get sorting info from the sorting conditions let sortingInfo = getSortingInfo(conditions); // Sort the sortable array according to its pre-computed value sortable.sort((x, y) => { // Evaluate each condition based on the order they were supplied in let evaluation = evaluate(sortingInfo, x, y); return evaluation; }); // Using the sorted sortable array, reorder the original // array by its index and save it into a result list let result = sortable.map((item) => { return arry[item.index]; }); return result; } /** * FUNCTION: defaultCompare * USE: Default comparer that defines the sort order. * @param x: The first element for comparison. * @param y: The second element for comparison. * @param desc: Indicates if comparison should be ascending or descending. * @return: The result value of the comparison. */ exposed.defaultCompare = (x, y, desc) => { // Default: both values left unchanged let evaluation = 0; // Check to see if both values are the same if (x === y) { evaluation = 0; } // Null values sort after anything else else if (isNull(x) || isNull(y)) { if (isNull(x)) { evaluation = 1; } else { evaluation = -1; } } else { // x goes before y if (x > y) { evaluation = 1 } // x goes after y else if (x < y) { evaluation = -1; } // Reverse order if descending if (desc) { evaluation *= -1; } } return evaluation; } // -- Private data -- /** * FUNCTION: evaluate * USE: Evaluates each sorting condition and determines the * order the item should be in. * @param sortingInfo: The sorting info from the sorting conditions. * @param x: The first element for comparison. * @param y: The second element for comparison. * @return: The sorting order of this item. */ let evaluate = (sortingInfo, x, y) => { let retValue = 0; // Evaluate each condition based on the order they were supplied in for (let index = 0; index < sortingInfo.length; ++index) { let desc = sortingInfo[index].desc; let compare = sortingInfo[index].compare; let valueX = x.values[index].value; let valueY = y.values[index].value; // Call the comparison function to evaluate the condition. // Exit if we find a valid ordering let evaluation = compare.call(this, valueX, valueY, desc); if (evaluation) { retValue = evaluation; break; } } return retValue; } /** * FUNCTION: mapArray * USE: Creates a seperate array that holds only the computed * values we want to sort. This is done to limit overhead, so * the item values are only computed once in the .sort * function, as opposed to possibly multiple times for each * comparison * @param arry: The original array to be sorted * @param conditions: The sorting conditions * @return: The sortable array holding only the computed values */ let mapArray = (arry, conditions) => { let mapped = []; for (let index = 0; index < arry.length; ++index) { const item = arry[index]; // Create a new object that represents the item to sort let sortableItem = createSortableItem(item, index, conditions); mapped.push(sortableItem); } return mapped; } /** * FUNCTION: createSortableItem * USE: Creates the item that holds the computed values to be sorted, * and saves its array order (index) of the original array * @param item: The array item in the original array * @param index: The index the item resides at in the original array * @param conditions: The sorting conditions * @return: The sortable item holding the computed values */ let createSortableItem = (item, index, conditions) => { let sortableItem = {}; sortableItem.index = index; sortableItem.values = []; // Go through each condition and process its value Array.prototype.forEach.call(conditions, (condition, index) => { // Check to see if there is a custom value format specified let formatValue = condition.value; if (isNull(formatValue)) { if (!isNull(condition.prop) && item.hasOwnProperty(condition.prop)) { formatValue = (item) => item[condition.prop]; } else { formatValue = (item) => item; } } if (!isFunction(formatValue)) { throw new TypeError(`Unable to determine the value on sorting condition #${index + 1}. Reason: '${formatValue}' is not a function`); } // Evaluate the field and save its value sortableItem.values.push({ value: formatValue.call(this, item) }); }); return sortableItem; } /** * FUNCTION: getSortingInfo * USE: Gets sorting information about the sorting condition * @param conditions: Sorting conditions * @return: The sorting direction and comparison * function to be used for each sorting condition. */ let getSortingInfo = (conditions) => { let sortingInfo = []; Array.prototype.forEach.call(conditions, (condition, index) => { // Check to see if this condition should be in descending order. // Default is ascending sort let desc = (!isNull(condition.desc)) ? condition.desc : false; // Get the comparison function let compare = condition.compare || exposed.defaultCompare; if (!isFunction(compare)) { throw new TypeError(`Invalid comparison of type '${typeof compare}' found on sorting condition #${index + 1}. Reason: '${compare}' is not a function`); } // Save the sorting condition info sortingInfo.push({ desc: desc, compare: compare }); }); return sortingInfo; } /** * FUNCTION: verifyArray * USE: Make sure the array is valid for sorting * @param arry: The array to be sorted * @return: N/A. */ let verifyArray = (arry) => { if (isNull(arry)) { throw new TypeError('Unable to sort. Reason: Array is not defined'); } else if (!isArrayLike(arry) && !isString(arry)) { throw new TypeError(`Unable to sort value of type: ${typeof arry}. Reason: '${arry}' is not an array.`); } } /** * FUNCTION: verifyConditions * USE: Make sure the sorting conditions are in the expected format * @param conditions: Sorting conditions * @return: The verified sorting condition. */ let verifyConditions = (conditions) => { if (isNull(conditions)) { conditions = {}; } if (!isArrayLike(conditions)) { conditions = [conditions]; } return conditions; } let isFunction = (item) => { return 'function' === typeof item } let isNull = (item) => { return undefined === item || null === item } let isString = (item) => { return 'string' == typeof item; } // see if it looks and smells like an iterable object, and do accept length === 0 let isArrayLike = (item) => { return ( Array.isArray(item) || (!!item && typeof item === "object" && typeof (item.length) === "number" && (item.length === 0 || (item.length > 0 && (item.length - 1) in item) ) ) ); } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Sort)); // http://programmingnotes.org/ |
7. Notes
This module uses the Array.sort() function to handle array sorting. But instead of altering the original array, it returns a newly sorted array. Internally, a separate ‘sortable’ array is created, which contains only the values to be sorted. This is done to reduce overhead in the .sort().compare() function, so item values are only computed once, as opposed to possibly multiple times for each comparison. So instead of sorting the original array, the lightweight ‘sortable’ array is sorted instead. Once array sorting is complete, the newly sorted ‘sortable’ array is used to reorder the original array, and that result is returned.
8. More Examples
Below are more examples demonstrating the Sort.by use for sorting arrays. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 27, 2020 // Taken From: http://programmingnotes.org/ // File: sortByDemo.html // Description: Demonstrates the use of Sort.by.js // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Sort.by.js Demo</title> <style> .main { text-align:center; margin-left:auto; margin-right:auto; } .output { text-align: left; } pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; } .string { color: green; } .number { color: darkorange; } .boolean { color: blue; } .null { color: magenta; } .key { color: red; } </style> <!-- // Include module --> <script type="text/javascript" src="./Sort.by.js"></script> </head> <body> <div class="main"> My Programming Notes Sort.by.js Demo <pre><code><div class="output"></div></code></pre> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { let indent = 4; // Sort a simple array. // Ascending order is the default sort direction let simpleArray1 = [5,4,3,2,1]; print('1. Original:', simpleArray1); print('1. Sorted in ascending order:', Sort.by( simpleArray1 ) ); // Sort a simple array with one condition. // To sort in descending order, specify a sorting condition let simpleArray2 = [10,11,12,13,14,15]; print('2. Original:', simpleArray2); print('2. Sorted in descending order:', Sort.by( simpleArray2, [ { value:(x) => x, desc: true } ]) ); // Sort a simple array with multiple conditions. // This option first, orders the even numbers and then // sorts them in ascending order let simpleArray3 = [28,91,10,87,19,32]; print('3. Original:', simpleArray3); print('3. First, orders the even numbers and then sorts them in ascending order:', Sort.by( simpleArray3, [ { value:(x) => (x % 2 == 0) ? 0 : 1 }, { value:(x) => x } ]) ); // Sort an object with one condition. // This array is sorted by artist converted to uppercase in ascending order let albums = [ { artist: 'Coldplay', title: 'Parachutes', released: 2000 }, { artist: 'Alicia Keys', title: 'Songs in A Minor', released: 2001 }, { artist: 'System of a Down', title: 'Toxicity', released: 2001 }, { artist: 'Kanye West', title: 'The College Dropout', released: 2004 }, ]; print('4. Original:', albums, indent); print('4. Sorted by artist in ascending order:', Sort.by(albums, [ { value:(x) => x.artist.toUpperCase() } ]) , indent); // Sort an object with multiple conditions. // The array is first sorted by the full name length in descending order, // followed by the id field sorted in ascending order let people = [ new Person(31, 'Kenneth', 'P'), new Person(28, 'Jennifer', 'N'), new Person(5, 'Lynn', 'P'), new Person(22, 'Kenneth', 'P'), new Person(19, 'Jennifer', 'N'), ]; print('5. Original:', people, indent); print('5. First, sorts by the full name length in descending order, followed by the id field sorted in ascending order:', Sort.by(people, [ { value: (x) => x.getFullName().length, desc: true }, { value: (x) => x.id }, ]) , indent); // Sort an object with multiple conditions. // The array is sorted by name in ascending order // followed by the speciality in descending order let employees = [ { name: 'Mike', speciality: 'JS', age: 22 }, { name: 'Tom', speciality: 'Java', age: 30 }, { name: 'Mike', speciality: 'PHP', age: 40 }, { name: 'Abby', speciality: 'Design', age: 20 }, ]; print('6. Original:', employees, indent); print('6. The array is sorted by name in ascending order followed by the speciality in descending order:', Sort.by(employees, [ { value: (x) => x.name }, { value: (x) => x.speciality, desc: true }, ]) , indent); // Sort a multi dimensional array with multiple conditions. // This array is sorted by accesing its value by numerical index. // The optional comparison function is used to sort in // using 'localeCompare' let mixedArray = [ [0, 'Aluminium', 0, 'Francis'], [1, 'Argon', 1, 'Ada'], [2, 'Brom', 2, 'John'], [3, 'Cadmium', 9, 'Marie'], [4, 'Fluor', 12, 'Marie'], [5, 'Gold', 1, 'Ada'], [6, 'Kupfer', 4, 'Ines'], [7, 'Krypton', 4, 'Joe'], [8, 'Sauerstoff', 0, 'Marie'], [9, 'Zink', 5, 'Max'] ]; print('7. Original:', mixedArray, indent); print('7. This array is sorted in ascending order at index 3, ascending order at index 2, and descending order at index 1:', Sort.by(mixedArray, [ { value: (x) => x[3], compare: (x, y) => x.localeCompare(y) }, { value: (x) => x[2], desc: false }, { value: (x) => x[1], compare: (x, y) => y.localeCompare(x) }, ]) , indent); let users = [ {USER:"bob", SCORE:2000, TIME:32, AGE:16, COUNTRY:"US"}, {USER:"jane", SCORE:4000, TIME:35, AGE:16, COUNTRY:"DE"}, {USER:"tim", SCORE:1000, TIME:30, AGE:17, COUNTRY:"UK"}, {USER:"mary", SCORE:1500, TIME:31, AGE:19, COUNTRY:"PL"}, {USER:"joe", SCORE:2500, TIME:33, AGE:18, COUNTRY:"US"}, {USER:"sally", SCORE:2000, TIME:30, AGE:16, COUNTRY:"CA"}, {USER:"yuri", SCORE:3000, TIME:34, AGE:19, COUNTRY:"RU"}, {USER:"anita", SCORE:2500, TIME:32, AGE:17, COUNTRY:"LV"}, {USER:"mark", SCORE:2000, TIME:30, AGE:18, COUNTRY:"DE"}, {USER:"amy", SCORE:1500, TIME:29, AGE:19, COUNTRY:"UK"} ]; print('8. Original:', users, indent); // Sort an object with multiple conditions. // The array is sorted by score DESC, time ASC, age ASC. print('8. This array is sorted by score DESC, time ASC, age ASC:', Sort.by(users, [ { value: (x) => x.SCORE, desc: true }, { value: (x) => x.TIME }, { value: (x) => x.AGE }, ]) , indent); // Sort an array with one condition // The sorting method automatically sorts null values to the end of the array let arrayWithNullValues = ['211', '36', '1.68', '0.254']; arrayWithNullValues[9] = 1991; arrayWithNullValues[10] = 1987; print('9. Original:', arrayWithNullValues); print('9. This array is sorted in ascending order, with null values placed at the end of the array:', Sort.by(arrayWithNullValues, [ { value: (x) => x ? parseFloat(x) : x } ]) ); }); function print(desc, obj, indent = 0) { let text = (desc.length > 0 ? '<br />' : '') + desc + (desc.length > 0 ? '<br />' : ''); let objText = obj || ''; if (obj && typeof obj != 'string') { objText = syntaxHighlight(JSON.stringify(obj, null, indent)); } text += objText; let output = document.querySelector('.output'); let pageText = output.innerHTML; pageText += (pageText.length > 0 ? '<br />' : '') + text; output.innerHTML = pageText; } class Person { constructor(id, firstName, lastName) { this.id = id; this.firstName = firstName; this.lastName = lastName; } getFullName() { return `${this.firstName} ${this.lastName}` } }; function syntaxHighlight(json) { if (typeof json != 'string') { json = JSON.stringify(json, undefined, 2); } json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="' + cls + '">' + match + '</span>'; }); } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript/CSS/HTML || sliderRadioButton.js – Simple Animated Slider Radio Button Using Vanilla JavaScript
Radio buttons let a user select only one of a limited number of choices. Radio buttons are normally presented in radio groups (a collection of radio buttons describing a set of related options). Only one radio button in a group can be selected at the same time.
Using JavaScript, the following is sample code which demonstrates how to display a simple animated slider radio button group to the page.
This animated slider radio button comes with a few basic features. When a selection is chosen, a sliding animation appears. The speed of the animation can be modified via CSS transitions. The look and orientation can also be modified via CSS, with the button group direction being either vertical or horizontal.
Contents
1. Basic Usage
2. Slider HTML - Default
3. Slider HTML - Column
4. sliderRadioButton.js & CSS Source
5. More Examples
1. Basic Usage
Syntax is very straightforward. The following demonstrates the JavaScript used to setup the radio buttons decorated with the ‘slider’ CSS class.
1 2 3 4 5 6 7 8 |
// Initialize slider. <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize slider Slider.init(); }); </script> |
2. Slider HTML – Default
The following is an example of the HTML used to display the slider. By default, the options are displayed inline.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!-- // HTML --> <!-- Radio Group --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoSole" value="Solé" name="rdoGroupName"> <label class="slider-option-label" for="rdoSole">Solé</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoLynn" value="Lynn" name="rdoGroupName"> <label class="slider-option-label" for="rdoLynn">Lynn</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoJennifer" value="Jennifer" name="rdoGroupName"> <label class="slider-option-label" for="rdoJennifer">Jennifer</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoKenneth" value="Kenneth" name="rdoGroupName"> <label class="slider-option-label" for="rdoKenneth">Kenneth</label> </div> |
3. Slider HTML – Column
The following is an example of the HTML used to display the slider as a column.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!-- // HTML --> <!-- Radio Group --> <div class="slider-radio-group column"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoRed" value="Red" name="rdoGroupColor"> <label class="slider-option-label" for="rdoRed">Red</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoBlue" value="Blue" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlue">Blue</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoBlack" value="Black" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlack">Black</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoYellow" value="Yellow" name="rdoGroupColor"> <label class="slider-option-label" for="rdoYellow">Yellow</label> <!-- Option 5 --> <input type="radio" class="slider-radio-button" id="rdoPurple" value="Purple" name="rdoGroupColor"> <label class="slider-option-label" for="rdoPurple">Purple</label> </div> |
4. sliderRadioButton.js & CSS Source
The following is the sliderRadioButton.js Namespace & CSS Source. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButton.js // Description: Javascript that handles the animation for a slider radio // button. // ============================================================================ /** * NAMESPACE: Slider * USE: Handles the sliding animation for the Slider Radio Button. */ var Slider = Slider || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; // Set class names and other shared data const settings = { classNameRadioGroup: '.slider-radio-group', classNameRadioButton: '.slider-radio-button', classNameRadioButtonLabel: 'slider-option-label', classNameSelectedItem: '.slider-selected-item', classNameRadioColumn: '.column', classNameLabelContent: '.slider-option-label-content', clean: (str) => { return str ? str.trim().replace('.', '') : ''; } }; exposed.settings = settings; /** * FUNCTION: init * USE: Initializes the slider radio groups and button clicks. * @param element: JavaScript element to search for radio groups * @return: N/A. */ exposed.init = (element = document) => { exposed.prepareRadioGroups(element); addClickEvents(element); } /** * FUNCTION: animate * USE: Handles the sliding animation of a radio button. * @param radioButton: JavaScript element (radio button) that the * slider animation should be applied to. * @return: N/A. */ exposed.animate = (radioButton) => { if (!radioButton) { throw new Error('Radio button does not exist!'); } // Get the radio group for this button and make sure it exists let radioGroup = exposed.getGroupForButton(radioButton); if (!radioGroup) { throw new Error('Unable to find radio group!'); } // Get the radio button label and make sure it exists let radioButtonLabel = exposed.getLabelForButton(radioButton, radioGroup); if (!radioButtonLabel) { throw new Error('Unable to find radio option label!'); } // Get the "selected item" slider div that marks the // label as selected and make sure it exists let selectedItem = exposed.getSelectedItem(radioGroup); if (!selectedItem) { throw new Error('Unable to find selected item slider animation element!'); } // Mark the option label as selected markAsSelected(radioButtonLabel, selectedItem); } /** * FUNCTION: prepareRadioGroups * USE: Makes sure radio groups contain the "selected item" slider div * and makes sure radio column groups are set up properly * @param element: JavaScript element to search for radio groups * @return: N/A. */ exposed.prepareRadioGroups = (element = document) => { // Get radio groups and make sure it exists let radioGroups = exposed.getRadioGroups(element); if (!radioGroups) { throw new Error('Unable to find a radio group!'); } // Go through radio groups and make sure they are setup properly for (const radioGroup of radioGroups) { // Make sure the "selected item" slider div exists. Add it in if it does not let selectedItemElement = exposed.getSelectedItem(radioGroup); if (!selectedItemElement) { selectedItemElement = document.createElement('div'); selectedItemElement.classList.add(settings.clean(settings.classNameSelectedItem)); radioGroup.appendChild(selectedItemElement); } // Get radio buttons for this group and make sure it exists let radioButtons = exposed.getRadioButtons(radioGroup); if (!radioButtons) { continue; } let classRadioColumn = settings.clean(settings.classNameRadioColumn); for (const radioButton of radioButtons) { // Check to see if this radio group is marked as a "column". If it is, // also add the associated class to its linked radio button elements if (radioGroup.classList.contains(classRadioColumn) && !radioButton.classList.contains(classRadioColumn)) { radioButton.classList.add(classRadioColumn); } let radioButtonLabel = exposed.getLabelForButton(radioButton, radioGroup); if (!radioButtonLabel) { continue; } let container = document.createElement('div'); container.classList.add(settings.clean(settings.classNameLabelContent)); while (radioButtonLabel.hasChildNodes()) { container.appendChild(radioButtonLabel.firstChild); } radioButtonLabel.appendChild(container); } } } /** * FUNCTION: getRadioGroups * USE: Returns the radio groups that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for radio groups * @return: Radio group descendants found in the given element. */ exposed.getRadioGroups = (element = document) => { return element.querySelectorAll(settings.classNameRadioGroup); } /** * FUNCTION: getRadioButtons * USE: Returns the radio buttons that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for radio buttons * @return: Radio button descendants found in the given element. */ exposed.getRadioButtons = (element = document) => { return element.querySelectorAll(settings.classNameRadioButton); } /** * FUNCTION: getGroupForButton * USE: Returns the radio group that is a parent of * the object on which this method was called. * @param radioButton: JavaScript element representing the radio button * @return: Radio button label descendant found in the given element. */ exposed.getGroupForButton = (radioButton) => { return radioButton.closest(settings.classNameRadioGroup); } /** * FUNCTION: getLabelForButton * USE: Returns the radio button label that is a descendant of * the object on which this method was called. * @param radioButton: JavaScript element representing the radio button * @param element: JavaScript parent element of the radio button * @return: Radio button label descendant found in the given element. */ exposed.getLabelForButton = (radioButton, element = document) => { return element.querySelector('label[for="' + radioButton.id + '"]'); } /** * FUNCTION: getSelectedItem * USE: Returns the "selected item" div for the object on which this * method was called. * @param element: JavaScript element to search for the "selected item" * @return: The "selected item" div found in the given element. */ exposed.getSelectedItem = (element = document) => { return element.querySelector(settings.classNameSelectedItem); } /** * FUNCTION: getSelectedRadioButton * USE: Returns the selected (checked) radio button that is * descendant of the object on which this method was called. * @param element: JavaScript element to search for radio buttons * @return: Selected radio button descendant found in the given element. */ exposed.getSelectedRadioButton = (element = document) => { let selectedButton = null; let buttons = exposed.getRadioButtons(element); for (const button of buttons) { if (button.checked) { selectedButton = button; break; } } return selectedButton; } // -- Private data -- /** * FUNCTION: addClickEvents * USE: Adds slider animation button click events for the radio buttons. * @param element: JavaScript element to search for radio groups * @return: N/A. */ let addClickEvents = (element = document) => { // Go through each radio button to initialize any that are selected/set button clicks exposed.getRadioButtons(element).forEach((radioButton, index) => { // If the radio button is checked, animate the selection if (radioButton.checked) { exposed.animate(radioButton); } // Add click events to update the selected radio button radioButton.addEventListener('click', (eventClick) => { exposed.animate(radioButton); }); }); } /** * FUNCTION: markAsSelected * USE: Marks the radio button label as "selected" and performs * the slider animation by moving the "selected item" div to * the location of the radio button label. * @param radioButtonLabel: JavaScript element (label) linked to the selected radio button * @param selectedItem: JavaScript element (div) for "selected item" * @return: N/A. */ let markAsSelected = (radioButtonLabel, selectedItem) => { let radioButtonLabelCoords = getElementCoords(radioButtonLabel, true); moveElement(selectedItem, radioButtonLabelCoords); selectedItem.style.display = 'block'; } /** * FUNCTION: moveElement * USE: Moves an element to a specific location and resizes it * to a specific size. * @param element: JavaScript element to move * @param coords: Coordinates to move & resize the element to * @return: N/A. */ let moveElement = (element, coords) => { element.style.left = coords.x + 'px'; element.style.top = coords.y + 'px'; element.style.width = coords.width + 'px'; element.style.height = coords.height + 'px'; } /** * FUNCTION: getElementCoords * USE: Gets the coordinates & size of an element. * @param element: JavaScript element in question * @param roundUp: Determines if element coordinates should * be rounded up or not * @return: The coordinates of the element in question. */ let getElementCoords = (element, roundUp = true) => { const boundingRect = element.getBoundingClientRect(); let coords = { x: element.offsetLeft, y: element.offsetTop, width: boundingRect.width, height: boundingRect.height } if (roundUp) { for (const prop in coords) { let item = coords[prop]; if (isNumber(item)) { coords[prop] = Math.ceil(item); } } } return coords; } let isNumber = (n) => { return !isNaN(parseFloat(n)) && !isNaN(n - 0) } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Slider)); // http://programmingnotes.org/ |
The following is sliderRadioButton.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/* // ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButton.css // Description: CSS that handles the animation for a slider radio // button. // ============================================================================ */ @import url('https://fonts.googleapis.com/css?family=Roboto'); .slider-radio-group { border: solid 1px #bdbdbd; margin: 5px; border-radius: 10px; overflow: hidden; width: 420px; position: relative; font-family: roboto; display: flex; justify-content: center; align-items: center; } .slider-radio-group.column { width: 100px; flex-direction: column; } .slider-radio-button { visibility: hidden; display: none; } .slider-radio-button.column { } input[type=radio][class*="slider-radio-button" i]:checked + label { background-color: rgba(117, 190, 218, .3); } input[type=radio][class*="slider-radio-button" i]:not(:first-child) + label { border-left: solid 1px lightgrey; } input[type=radio][class*="slider-radio-button column" i]:not(:first-child) + label { border-top: solid 1px lightgrey; } .slider-option-label { display: inline-block; cursor: pointer; padding: 5px; width: 110px; text-align:center; background-color: #f3f4f4; overflow: hidden; box-sizing: border-box; } .slider-option-label:hover { background-color: #eef6fa; } .slider-option-label-content { position: relative; z-index: 1; } .slider-selected-item { cursor: pointer; position: absolute; transition: all 400ms ease-in-out; background-color: rgba(117, 190, 218, .25); display:none; } /* // http://programmingnotes.org/ */ |
5. More Examples
Below are more examples demonstrating the use of ‘sliderRadioButton.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 12, 2020 // Taken From: http://programmingnotes.org/ // File: sliderRadioButtonDemo.html // Description: Demonstrates how to display a simple animated // slider radio button to the page. // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Simple Animated Slider Radio Button Demo</title> <style> .button { padding: 8px; background-color: #d2d2d2; height:100%; text-align:center; text-decoration:none; color:black; display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 15px; cursor: pointer; } .button.medium { height:18px; width:80px; } .section-header { margin-top: 20px; text-align:center; font-size: 15px; font-weight: bold; font-style: italic; font-family: Tahoma,helvetica,arial,sans-serif; } .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } .emoji { font-size:13px } </style> <!-- // Include module --> <link type="text/css" rel="stylesheet" href="./sliderRadioButton.css"> <script type="text/javascript" src="./sliderRadioButton.js"></script> </head> <body> <div class="main"> <div class="inline" style="margin: 5px;"> <div> <div class="section-header"> Rate Our Service </div> <!-- Radio Group 1 --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoOneStar" value="1" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoOneStar"><span class="emoji">⭐</span></label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoTwoStar" value="2" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoTwoStar"><span class="emoji">⭐⭐</span></label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoThreeStar" value="3" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoThreeStar"><span class="emoji">⭐⭐⭐</span></label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoFourStar" value="4" name="rdoGroupRatings"> <label class="slider-option-label" for="rdoFourStar"><span class="emoji">⭐⭐⭐⭐</span></label> </div> </div> <div> <div class="section-header"> Favorite Name </div> <!-- Radio Group 2 --> <div class="slider-radio-group"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoSole" value="Solé" name="rdoGroupName"> <label class="slider-option-label" for="rdoSole">Solé</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoLynn" value="Lynn" name="rdoGroupName"> <label class="slider-option-label" for="rdoLynn">Lynn</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoJennifer" value="Jennifer" name="rdoGroupName"> <label class="slider-option-label" for="rdoJennifer">Jennifer</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoKenneth" value="Kenneth" name="rdoGroupName"> <label class="slider-option-label" for="rdoKenneth">Kenneth</label> </div> </div> <div class="inline"> <div class="section-header"> Favorite Color </div> <!-- Radio Group 3 --> <div class="slider-radio-group column"> <!-- Option 1 --> <input type="radio" class="slider-radio-button" id="rdoRed" value="Red" name="rdoGroupColor"> <label class="slider-option-label" for="rdoRed">Red</label> <!-- Option 2 --> <input type="radio" class="slider-radio-button" id="rdoBlue" value="Blue" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlue">Blue</label> <!-- Option 3 --> <input type="radio" class="slider-radio-button" id="rdoBlack" value="Black" name="rdoGroupColor"> <label class="slider-option-label" for="rdoBlack">Black</label> <!-- Option 4 --> <input type="radio" class="slider-radio-button" id="rdoYellow" value="Yellow" name="rdoGroupColor"> <label class="slider-option-label" for="rdoYellow">Yellow</label> <!-- Option 5 --> <input type="radio" class="slider-radio-button" id="rdoPurple" value="Purple" name="rdoGroupColor"> <label class="slider-option-label" for="rdoPurple">Purple</label> </div> </div> <div class="button section-header" id="btnSelect"> Select </div> </div> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Make sure the radio group is setup with the selected option slider div and other options // And add button clicks to animate the radio button slider Slider.init(); // Select button click document.querySelector('#btnSelect').addEventListener('click', function(eventClick) { let radioButtons = Slider.getRadioButtons(); let optionSelected = false; if (radioButtons) { for (const radioButton of radioButtons) { if (radioButton.checked) { alert(radioButton.id + ' is checked. Its value is: ' + radioButton.value); optionSelected = true; } } } if (!optionSelected) { alert('Please select an option!'); } }); }); </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
JavaScript/CSS/HTML || Collapsible.js – Simple Collapsible Accordion Panel Using Vanilla JavaScript
Accordion panels are a vertical stack of items. Each item can be “expanded” or “collapsed” to reveal the content associated with that item. There can be zero expanded items, or more than one item expanded at a time, depending on the configuration.
Using JavaScript, the following is sample code which demonstrates how to display a simple collapsible accordion panel to the page.
This panel comes with a few basic features. Using data attributes, you can adjust the slide up and slide down speed, as well as the initial collapsed status. You can also specify either the entire row as selectable, or just the button.
Contents
1. Basic Usage
2. Collapsible HTML
3. Collapsible.js & CSS Source
4. More Examples
1. Basic Usage
Syntax is very straightforward. The following demonstrates the JavaScript used to setup the elements decorated with the ‘collapsible‘ CSS class.
1 2 3 4 5 6 7 8 |
// Initialize collapsible. <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize collapsible Collapsible.init(); }); </script> |
2. Collapsible HTML
The following is an example of the HTML used to display the collapsible.
To make the entire row clickable, only add the ‘collapsible‘ class to the ‘collapsible-header‘ element. To make just the button clickable, only add the ‘collapsible‘ class to the ‘collapsible-button‘ element
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- // HTML --> <!-- Collapsible 1 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Sample Header! </div> <div class="collapsible-button"></div> </header> <section class="collapsible-body" data-slideup="800" data-slidedown="100" data-expanded="true"> <!-- Specify the slide duration (in milliseconds), and if the row is initially expanded --> <p> You can adjust the slide up and slide down speed, as well as the initial expanded status with data attributes. </p> <p> You can specify either the entire row as selectable, or just the button, depending on where the 'collapsible' class declaration is placed. </p> </section> <!-- ... Additional Collapsibles --> |
3. Collapsible.js & CSS Source
The following is the Collapsible.js Namespace & CSS Source. Include this in your project to start using!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
// ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: Collapsible.js // Description: Demonstrates how to display a simple collapsible // accordion panel to the page. // ============================================================================ /** * NAMESPACE: Collapsible * USE: Collapsible accordion panel. */ var Collapsible = Collapsible || {}; (function(namespace) { 'use strict'; // -- Public data -- // Property to hold public variables and functions let exposed = namespace; // Set class names and other shared data const settings = { classNameExpanded: '.expanded', classNameHeader: '.collapsible-header', classNameHeaderText: '.collapsible-header-text', classNameBody: '.collapsible-body', classNameButton: '.collapsible-button', classNameHideContent: '.hide', classNameCollapsible: '.collapsible', dataNameSlideUp: 'data-slideup', dataNameSlideDown: 'data-slidedown', dataNameExpanded: 'data-expanded', dataNameCurrentlyExpanded: 'data-isCurrentlyExpanded', cleanClassName: function(str) { return str ? str.trim().replace('.', '') : ''; }, }; exposed.settings = settings; /** * FUNCTION: init * USE: Initializes the collapsible accordion panel button clicks. * @param element: JavaScript element to search for collapsible panels * @return: N/A. */ exposed.init = (element = document) => { addEvents(element); } /** * FUNCTION: animate * USE: Handles opening/closing of a collapsible accordion panel. * @param collapsible: JavaScript element that raised the event that * is decorated with the 'collapsible' class tag. * @return: N/A. */ exposed.animate = (collapsible) => { if (!collapsible) { return; } // Find the collapsible header row // and make sure its found let header = collapsible.closest(settings.classNameHeader); if (!header) { throw new Error('Unable to find header row!'); } // Find the associated collapsible body text // and make sure its found let body = header.nextElementSibling; if (!body) { throw new Error('Unable to find content body!'); } // Determine if the content should be expanded or not let status = getPanelStatus(body); // Get the slide up/down speeds let slideUpDuration = status.slideUpDuration; let slideDownDuration = status.slideDownDuration; // Get the current collapsible status let isCurrentlyExpanded = status.isCurrentlyExpanded; // Find the action button so we can change its icon let button = header.querySelector(settings.classNameButton); // Update contents depending on if the row is expanded or not if (toBoolean(isCurrentlyExpanded)) { // Mark the header row as 'active' addClass(header, settings.classNameExpanded); // Change the button icon to '-' addClass(button, settings.classNameExpanded); // Slide down the body slideDown(body, slideDownDuration); } else { // Remove the header row as 'active' removeClass(header, settings.classNameExpanded); // Change the button icon to '+' removeClass(button, settings.classNameExpanded); // Slide up the body slideUp(body, slideUpDuration); } // Save the expanded status body.setAttribute(settings.dataNameCurrentlyExpanded, isCurrentlyExpanded); } /** * FUNCTION: getHeaders * USE: Returns the collapsible headers that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for collapsible headers * @return: Collapsible headers descendants found in the given element. */ exposed.getHeaders = (element = document) => { return element.querySelectorAll(settings.classNameHeader); } /** * FUNCTION: getBodies * USE: Returns the collapsible bodies that are descendants of the object on * which this method was called. * @param element: JavaScript element to search for collapsible bodies * @return: Collapsible body descendants found in the given element. */ exposed.getBodies = (element = document) => { return element.querySelectorAll(settings.classNameBody); } /** * FUNCTION: getPanels * USE: Returns the collapsible panels (header & bodies) that are * descendants of the object on which this method was called. * @param element: JavaScript element to search for collapsible panels * @return: Collapsible panel descendants found in the given element. */ exposed.getPanels = (element = document) => { let panels = []; // Get collapsible headers and bodies const headers = exposed.getHeaders(element); const bodies = exposed.getBodies(element); // Get the maximum item count const maxItems = Math.max(headers.length, bodies.length); // Go through each header and body and create an // object to group them together and save them to a list for(let x = 0; x < maxItems; ++x) { let header = (x < headers.length) ? headers[x] : null; let body = (x < bodies.length) ? bodies[x] : null; panels.push({ header: header, body: body, }); } return panels; } // -- Private data -- /** * FUNCTION: addEvents * USE: Adds events for the collapsibles. * @param element: JavaScript element to search for collapsibles * @return: N/A. */ let addEvents = (element = document) => { // Initialize each row to its initial // opened/closed state, depending on user values element.querySelectorAll(settings.classNameCollapsible).forEach(function(collapsible, index) { exposed.animate(collapsible); // Add button click collapsible.addEventListener('click', function(eventClick) { eventClick.stopPropagation(); exposed.animate(collapsible) }); }); window.addEventListener('resize', (event) => { let bodies = exposed.getBodies(element); for (let body of bodies) { if (toBoolean(body.getAttribute(settings.dataNameCurrentlyExpanded))) { slideUp(body, 0); slideDown(body, 0); } } }); } let addClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && !hasClass(element, cssClass)) { element.classList.add(cssClass) modified = true; } return modified; } let removeClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); let modified = false; if (cssClass.length > 0 && hasClass(element, cssClass)) { element.classList.remove(cssClass); modified = true; } return modified; } let hasClass = (element, cssClass) => { cssClass = settings.cleanClassName(cssClass); return element.classList.contains(cssClass); } let slideUp = (body, slideUpDuration) => { // Add the class that removes the content body addClass(body, settings.classNameHideContent); // Set the slide up duration body.style.transitionDuration = (null != slideUpDuration) ? slideUpDuration + 'ms' : null; // Remove the content body custom height so the transition can take effect body.style.height = null; } let slideDown = (body, slideDownDuration) => { // Remove the class that hides the content body removeClass(body, settings.classNameHideContent); // Set the slide down duration body.style.transitionDuration = (null != slideDownDuration) ? slideDownDuration + 'ms' : null; // Get the content body height let bodyHeight = body.scrollHeight; // Get the style for the element so we can get its max height let computedStyle = window.getComputedStyle(body); // Check to see if the current content body is greater than the maximum allowed height. // Scrollbars appear when setting a custom height. // We do this check so scroll bars arent visible if they dont need to be if (computedStyle && computedStyle.maxHeight) { let computedMaxHeight = parseInt(computedStyle.maxHeight, 10); if (bodyHeight > computedMaxHeight) { // Body height is bigger than the max height, remove the custom overflow value // and use its computed overflow style body.style.overflow = null; } else { // Override and set overflow to hidden so scroll bars arent visible // if they dont need to be body.style.overflow = 'hidden'; } } // Set the content body custom height so the transition can take effect body.style.height = bodyHeight + 'px'; } let getPanelStatus = (body) => { // Get the slide up/down speeds let slideUpDuration = body.getAttribute(settings.dataNameSlideUp); let slideDownDuration = body.getAttribute(settings.dataNameSlideDown); // Get the current collapsible status let isCurrentlyExpanded = body.getAttribute(settings.dataNameCurrentlyExpanded); // If the current status hasnt been defined yet, use the user defined value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = body.getAttribute(settings.dataNameExpanded) // Assume the row is closed if the user did not define a value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = false; } // Remove the delay so the row is immediately opened/closed slideUpDuration = 0; slideDownDuration = 0; } else { // The status has been defined in the past. Change its state if (toBoolean(isCurrentlyExpanded)) { isCurrentlyExpanded = false; } else { isCurrentlyExpanded = true; } } return { isCurrentlyExpanded: isCurrentlyExpanded, slideUpDuration: slideUpDuration, slideDownDuration: slideDownDuration, } } /** * FUNCTION: toBoolean * USE: Converts a specified value to an equivalent Boolean value. * @param value: Value to convert * @return: true if value evaluates to true; false otherwise. */ let toBoolean = (value) => { if (typeof value === 'string') { value = value.trim().toLowerCase(); } let ret = false; switch (value) { case true: case "true": case "yes": case "1": case 1: ret = true; break; } return ret; } (function (factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } }(function() { return namespace; })); }(Collapsible)); // http://programmingnotes.org/ |
The following is Collapsible.css.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
/* // ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: Collapsible.css // Description: CSS for the collapsible accordian panel // ============================================================================ */ /* Indicates that an element is collapsible and clickable. To make the entire row clickable, only add this class to the 'collapsible-header' element. To make just the button clickable, only add this class to the 'collapsible-button' element. */ .collapsible { cursor: pointer; } .collapsible-header, .collapsible-body { width: 100%; } .collapsible-header { background-color: #d6d6d6; border-top-left-radius: 5px; border-top-right-radius: 5px; padding: 10px; position: relative; text-align: left; border-top: 0.5px solid #bfbfbf; box-sizing: border-box; } .collapsible-header-text { max-width: 90%; max-height: 20px; overflow: hidden; padding-left: 8px; display: inline-block; font-family: helvetica,arial,sans-serif; white-space: nowrap; text-overflow: ellipsis; } .collapsible-button { position: absolute; right: 0; margin-right: 15px; display: inline-block; font-family: Tahoma,helvetica,arial,sans-serif; border: unset; background-color: inherit; font-size: inherit; } .collapsible-button:after { content: '\002B'; /* + */ } .expanded, .collapsible-header:hover { background-color: #a6a6a6; } .collapsible-button.expanded:after { content: "\2212"; /* - */ } .collapsible-button.expanded { background-color: inherit; } .collapsible-body { border: 1px solid #d8d8d8; border-top: unset; max-height: 380px; padding: 15px; padding-top: 0px; padding-bottom: 0px; text-align: left; overflow: auto; box-sizing: border-box; transition: all 400ms ease-in-out; height: 0; } .collapsible-body.hide { height: 0; padding-top: 0px; padding-bottom: 0px; margin-bottom: 0px; } /* http://programmingnotes.org/ */ |
4. More Examples
Below are more examples demonstrating the use of ‘Collapsible.js‘. Don’t forget to include the module when running the examples!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
<!-- // ============================================================================ // Author: Kenneth Perkins // Date: Jun 8, 2020 // Taken From: http://programmingnotes.org/ // File: SimpleCollapsibleAccordionPanel.html // Description: Demonstrates how to display a simple collapsible // accordion panel to the page. // ============================================================================ --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>My Programming Notes Simple Collapsible Accordion Panel Demo</title> <style> .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <!-- // Include module --> <link type="text/css" rel="stylesheet" href="./Collapsible.css"> <script type="text/javascript" src="./Collapsible.js"></script> </head> <body> <div class="main"> <p>My Programming Notes Collapsible.js Demo</p> <div class="inline" style="margin: 5px; width: 600px;"> <!-- Collapsible 1 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Sample Header! </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body" data-slideup="800" data-slidedown="100" data-expanded="true"> <!-- Specify the slide duration (in milliseconds), and if the row is initially expanded --> <p> You can adjust the slide up and slide down speed, as well as the initial expanded status with data attributes. </p> <p> You can specify either the entire row as selectable, or just the button, depending on where the 'collapsible' class declaration is placed. </p> </section> <!-- Collapsible 2 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > A Journey To Another World </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body"> <div style="text-align: center;"> <p> Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had. </p> <p> Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education. Discovery incommode earnestly no he commanded if. Put still any about manor heard. </p> <p> Use securing confined his shutters. Delightful as he it acceptance an solicitude discretion reasonably. Carriage we husbands advanced an perceive greatest. Totally dearest expense on demesne ye he. Curiosity excellent commanded in me. Unpleasing impression themselves to at assistance acceptance my or. On consider laughter civility offended oh. </p> <p> Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill. </p> </div> </section> <!-- Collapsible 3 --> <header class="collapsible collapsible-header"> <div class="collapsible-header-text" > Tell Us About Yourself </div> <div class="collapsible-button"> </div> </header> <section class="collapsible-body"> <div style="padding: 10px;"> <input type="radio" id="male" name="gender" value="male" checked> <label for="male">Male</label><br /> <input type="radio" id="female" name="gender" value="female"> <label for="female">Female</label><br /> <input type="radio" id="na" name="gender" value="na"> <label for="na">Prefer Not To Say</label> </div> </section> </div> <div style="margin-top: 5px;"> Click a tab to try! </div> </div> <script> document.addEventListener("DOMContentLoaded", function(eventLoaded) { // Initialize each row to its initial // opened/closed state, depending on user values Collapsible.init(); }); </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
jQuery/CSS/HTML || Simple Progress Bar
The following is sample code which demonstrates how to create a progress bar. Using jQuery, the progress bar is animated to simulate download progress.
REQUIRED KNOWLEDGE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
<!-- // ============================================================================ // Author: K Perkins // Date: Jun 3, 2020 // Taken From: http://programmingnotes.org/ // File: SimpleProgressBar.html // Description: Demonstrates how to display a simple progress // bar to the page and simulate download progress. // ============================================================================ --> <!DOCTYPE html> <html> <head> <title>My Programming Notes Simple Progress Bar Demo</title> <style> .progress { width: 100%; display: inline-block; overflow: hidden; height: 28px; background-color: #ddd; border-radius: 4px; position: relative; display: flex; justify-content: center; flex-direction: column; } .progress-bar-text { position: absolute; left: 50%; font-style: italic; font-size: 1em; transform:translateX(-50%); font-family: Tahoma,helvetica,arial,sans-serif; } .progress-bar { width: 0; border-radius: 4px; height: 100%; font-size: 14px; line-height: 22px; color: #464e5b; text-align: center; background-color: #61D265; } .button { padding: 8px; background-color: #d2d2d2; height:100%; text-align:center; text-decoration:none; color:black; display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 15px; cursor: pointer; } .button.medium { height:18px; width:80px; } .button:hover { background-color:#bdbdbd; } .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> </head> <body> <div class="main"> <div class="progress"> <div class="progress-bar-text"></div> <div class="progress-bar" style="width: 0%;"> </div> </div> <div class="inline" style="margin: 10px;"> <div id="btnResetProgress" class="inline button medium"> Reset </div> <div id="btnStopProgress" class="inline button medium"> Stop </div> <div id="btnResumeProgress" class="inline button medium"> Resume </div> </div> <div style="margin-top: 5px;"> Click a button to demo! </div> </div> <script> $(document).ready(function() { checkButtonVisibility(); animateProgress(); $('#btnStopProgress').click(function(e) { stopProgress(); }); $('#btnResumeProgress').click(function(e) { stopProgress(); animateProgress(settings.currentWidth); }); $('#btnResetProgress').click(function(e) { stopProgress(true); animateProgress(); }); }); // Object to hold shared variables const settings = { // The refresh rate of the progress bar updateInterval: 500, // The maximum width of the progress bar maxWidth: 100, // The current width of the progress bar currentWidth: 0, // The interval ID returned from setInterval // which triggers each progress update progressInterval: null }; /** * FUNCTION: animateProgress * USE: Simulates a download progress bar. * @param initial: The initial percentage of the progress bar. * @return: N/A. */ function animateProgress(initial = 0) { settings.currentWidth = initial; settings.progressInterval = setInterval(function() { // Find the progress bar so we can adjust its width let $progressBar = $('.progress-bar'); // Variable to determine if we should stop the animation let stopInterval = false; // Make sure the progress bar element is found if (!$progressBar || $progressBar.length < 1) { // Stop progress if not found stopInterval = true; alert('Unable to find the progress bar!'); } else { // Add progress if not complete if (!progressCompleted()) { settings.currentWidth = Math.min(settings.currentWidth + getRandomInt(1, 15), settings.maxWidth); } // Check to see if progress has been completed with the recent update if (progressCompleted()) { stopInterval = true; } // Set the progress bar text with the updated percentage $('.progress-bar-text').html(settings.currentWidth + '%'); // Adjust the progress bar width to match the updated percentage $progressBar.width(settings.currentWidth + '%'); } // Stop the progress bar if requested if (stopInterval) { stopProgress(); } checkButtonVisibility(); }, settings.updateInterval); } /** * FUNCTION: stopProgress * USE: Stops progress bar animation. * @param reset: Indicates if this function is called due to resetting progress * @return: N/A. */ function stopProgress(reset = false) { if (!progressStopped()) { clearInterval(settings.progressInterval); settings.progressInterval = null; } if (!reset) { checkButtonVisibility(); } } /** * FUNCTION: checkButtonVisibility * USE: Determines if certain buttons should be visible. * @return: N/A. */ function checkButtonVisibility() { let $resumeButton = $('#btnResumeProgress'); let $stopButton = $('#btnStopProgress'); let resumeVisible = false; // Hide the resume button if: // 1. Progress has not started // 2. Progress has not stopped // 3. Progress has been completed if (!progressStarted() || !progressStopped() || progressCompleted()) { $resumeButton.hide(); resumeVisible = false; } // Show the resume button if progress is paused else if (progressStopped()) { $resumeButton.show(); resumeVisible = true; } // Show/hide the stop button if the resume button is hidden/visible if (resumeVisible) { $stopButton.hide(); } else { $stopButton.show(); } } /** * FUNCTION: progressStarted * USE: Determines if the progress animation has started. * @return: True if animation has started, false otherwise. */ function progressStarted() { return settings.currentWidth && settings.currentWidth > 0; } /** * FUNCTION: progressCompleted * USE: Determines if the progress animation has completed. * @return: True if animation has completed, false otherwise. */ function progressCompleted() { return progressStarted() && settings.currentWidth >= settings.maxWidth; } /** * FUNCTION: animateProgress * USE: Determines if the progress animation has stopped. * @return: True if animation has stopped, false otherwise. */ function progressStopped() { return !settings.progressInterval; } /** * FUNCTION: getRandomInt * USE: Gets a random integer within a given range. * @param min: min (inclusive) * @param max: max (inclusive) * @return: A random integer within a given range. */ function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
jQuery/CSS/HTML || Simple Collapsible Accordion Panel
Accordion panels are a vertical stack of items. Each item can be “expanded” or “collapsed” to reveal the content associated with that item. There can be zero expanded items, or more than one item expanded at a time, depending on the configuration.
Using jQuery, the following is sample code which demonstrates how to display a simple collapsible accordion panel to the page.
This panel comes with a few basic features. Using data attributes, you can adjust the slide up and slide down speed, as well as the initial collapsed status. You can also specify either the entire row as selectable, or just the button.
REQUIRED KNOWLEDGE
jQuery - closest
jQuery - next
jQuery - find
JavaScript - stopPropagation
JavaScript - Events
Accordion Panel - What is it?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
<!-- // ============================================================================ // Author: K Perkins // Date: Jun 2, 2020 // Taken From: http://programmingnotes.org/ // File: SimpleCollapsibleAccordionPanel.html // Description: Demonstrates how to display a simple collapsible // accordion panel to the page. // ============================================================================ --> <!DOCTYPE html> <html> <head> <title>My Programming Notes Simple Collapsible Accordion Panel Demo</title> <style> /* Indicates that an element is collapsible and clickable. To make the entire row clickable, only add this class to the 'collapsible-header' element. To make just the button clickable, only add this class to the 'collapsible-button' element. */ .collapsible { cursor: pointer; } .collapsible-header { background-color: #d6d6d6; border-top-left-radius: 5px; border-top-right-radius: 5px; padding: 10px; position: relative; width: 600px; text-align: left; border-top: 0.5px solid #bfbfbf; box-sizing: border-box; } .collapsible-header-text { max-width: 90%; max-height: 20px; overflow: hidden; padding-left: 8px; display: inline-block; font-family: helvetica,arial,sans-serif; white-space: nowrap; text-overflow: ellipsis; } .collapsible-button { float:right; position: absolute; right: 0; margin-right: 15px; display: inline-block; font-family: Tahoma,helvetica,arial,sans-serif; } .collapsible-button:after { content: '\002B'; /* + */ } .expanded, .collapsible-header:hover { background-color: #a6a6a6; } .collapsible-button.expanded:after { content: "\2212"; /* - */ } .collapsible-button.expanded { background-color: inherit; } .collapsible-body { border: 1px solid #d8d8d8; border-top: unset; max-height: 380px; width: 600px; padding: 15px; padding-top: 0px; padding-bottom: 0px; text-align: left; overflow: auto; box-sizing: border-box; display: none; } .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> </head> <body> <div class="main"> <div class="inline" style="margin: 10px;"> <!-- Collapsible 1 --> <div class="collapsible collapsible-header"> <div class="collapsible-header-text" > Sample Header! </div> <div class="collapsible-button"> </div> </div> <div class="collapsible-body" data-slideup="800" data-slidedown="100" data-expanded="true"> <!-- Specify the slide duration (in milliseconds), and if the row is initially expanded --> <p> You can adjust the slide up and slide down speed, as well as the initial expanded status with data attributes. </p> <p> You can specify either the entire row as selectable, or just the button, depending on where the 'collapsible' class declaration is placed. </p> </div> <!-- Collapsible 2 --> <div class="collapsible collapsible-header"> <div class="collapsible-header-text" > A Journey To Another World </div> <div class="collapsible-button"> </div> </div> <div class="collapsible-body"> <div style="text-align: center;"> <p> Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had. </p> <p> Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education. Discovery incommode earnestly no he commanded if. Put still any about manor heard. </p> <p> Use securing confined his shutters. Delightful as he it acceptance an solicitude discretion reasonably. Carriage we husbands advanced an perceive greatest. Totally dearest expense on demesne ye he. Curiosity excellent commanded in me. Unpleasing impression themselves to at assistance acceptance my or. On consider laughter civility offended oh. </p> <p> Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill. </p> </div> </div> <!-- Collapsible 3 --> <div class="collapsible collapsible-header"> <div class="collapsible-header-text" > Tell Us About Yourself </div> <div class="collapsible-button"> </div> </div> <div class="collapsible-body"> <div style="padding: 10px;"> <input type="radio" id="male" name="gender" value="male" checked> <label for="male">Male</label><br /> <input type="radio" id="female" name="gender" value="female"> <label for="female">Female</label><br /> <input type="radio" id="na" name="gender" value="na"> <label for="na">Prefer Not To Say</label> </div> </div> </div> <div style="margin-top: 5px;"> Click a tab to try! </div> </div> <script> $(document).ready(function() { $(".collapsible").on('click', function(e) { e.stopPropagation(); animateCollapsible($(this)); }); // Initialize each row to its initial // opened/closed state, depending on user values $('.collapsible').each(function(index, element) { animateCollapsible($(this)); }); }); /** * FUNCTION: animateCollapsible * USE: Handles opening/closing of a collapsible accordion panel. * @param sender: JQuery element that raised the event that * is decorated with the 'collapsible' class tag. * @return: N/A. */ function animateCollapsible($sender) { if (!$sender || $sender.length < 1) { return; } // Set class and data attribute variable names const settings = { classNameExpanded: 'expanded', classNameHeader: '.collapsible-header', classNameBody: '.collapsible-body', classNameButton: '.collapsible-button', dataNameSlideUp: 'slideup', dataNameSlideDown: 'slidedown', dataNameExpanded: 'expanded', dataNameCurrentlyExpanded: 'isCurrentlyExpanded' }; // Find the collapsible header row // and make sure its found let $header = $sender.closest(settings.classNameHeader); if (!$header || $header.length < 1) { alert('Unable to find header row!'); return; } // Find the associated collapsible body text // and make sure its found let $body = $header.next(settings.classNameBody); if (!$body || $body.length < 1) { alert('Unable to find content body!'); return; } // Determine if the content should be expanded or not // Get the slide up/down speeds let slideUpDuration = $body.data(settings.dataNameSlideUp); let slideDownDuration = $body.data(settings.dataNameSlideDown); // Get the current collapsible status let isCurrentlyExpanded = $body.data(settings.dataNameCurrentlyExpanded); // If the current status hasnt been defined yet, use the user defined value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = $body.data(settings.dataNameExpanded) // Assume the row is closed if the user did not define a value if (null == isCurrentlyExpanded) { isCurrentlyExpanded = false; } // Remove the delay if there is a user defined value // so the row is immediately opened/closed slideUpDuration = 0; slideDownDuration = 0; } else { // The status has been defined in the past. Change its state if (isCurrentlyExpanded) { isCurrentlyExpanded = false; } else { isCurrentlyExpanded = true; } } // Find the action button so we can change its icon let $button = $header.find(settings.classNameButton); // Update contents depending on if the row is expanded or not if (isCurrentlyExpanded) { // Mark the header row as 'active' $header.addClass(settings.classNameExpanded); // Change the button icon to '-' $button.addClass(settings.classNameExpanded); // Slide down the body $body.slideDown(slideDownDuration); } else { // Remove the header row as 'active' $header.removeClass(settings.classNameExpanded); // Change the button icon to '+' $button.removeClass(settings.classNameExpanded); // Slide up the body $body.slideUp(slideUpDuration); } // Save the expanded status $body.data(settings.dataNameCurrentlyExpanded, isCurrentlyExpanded); } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.
jQuery/CSS/HTML || Simple Notification Message Div With Success, Warning & Error Icons
Using jQuery, the following is sample code which demonstrates how to display a simple notification message div with success, warning & error icons. The message is displayed to the page on a delay, and disappears after the delay completes.
REQUIRED KNOWLEDGE
jQuery - fadeIn
jQuery - fadeOut
JavaScript - setTimeout
JavaScript - Object.freeze
Base64 Image - What is it?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
<!-- // ============================================================================ // Author: K Perkins // Date: Jun 1, 2020 // Taken From: http://programmingnotes.org/ // File: Notification.html // Description: Demonstrates how to display a notification div message to // the page with icons. // ============================================================================ --> <!DOCTYPE html> <html> <head> <title>My Programming Notes Notification Display Demo</title> <style> .notice { border: 1px solid #d6dadb; border-left: 4.5px solid #32b032; background-color: #f1f5f6; text-align:left; padding: 8px; font-family: helvetica,arial,sans-serif; } /* Background url source: http://icons.iconarchive.com/icons/custom-icon-design/flatastic-9/256/Accept-icon.png */ .notice:before { width: 18px; height: 18px; margin-right: 10px; content: ""; display: inline-block; vertical-align: middle; background: url('data:image/png;base64,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') no-repeat; background-size: 100%; transform: translateY(-10%); } .notice.error { border-left-color: red; } /* Background url source: http://icons.iconarchive.com/icons/papirus-team/papirus-status/256/dialog-error-icon.png */ .notice.error:before { background: url('data:image/png;base64,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') no-repeat; background-size: 100%; } .notice.warning { border-left-color: #ff8000; } /* Background url source: http://icons.iconarchive.com/icons/papirus-team/papirus-status/256/dialog-warning-icon.png */ .notice.warning:before { background: url('data:image/png;base64,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') no-repeat; background-size: 100%; } .notice-container { height:45px; } .button { padding: 8px; background-color: #d2d2d2; height:100%; text-align:center; text-decoration:none; color:black; display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 15px; cursor: pointer; } .button:hover { background-color:#bdbdbd; } .buttonSection { border-top: 1px solid #a3a3a3; border-bottom: 1px solid #a3a3a3; padding:8px; margin-bottom:10px; text-align:center; } .inline { display:inline-block; } .main { text-align:center; margin-left:auto; margin-right:auto; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> </head> <body> <div class="main"> <div class="notice-container"> <div class="notice" style="display:none;"> </div> </div> <div class="buttonSection"> <div id="btnSuccessMessage" class="inline button"> Success Message </div> <div id="btnWarningMessage" class="inline button"> Warning Message </div> <div id="btnErrorMessage" class="inline button"> Error Message </div> <div style="margin-top: 5px;"> Click a button to see the different message styles! </div> </div> </div> <script> $(document).ready(function() { $('#btnSuccessMessage').click(function(e) { displayNotice('Success!', NoticeType.Success); }); $('#btnWarningMessage').click(function(e) { displayNotice('Warning!', NoticeType.Warning); }); $('#btnErrorMessage').click(function(e) { displayNotice('Error!', NoticeType.Error); }); }); // Notice type enum const NoticeType = Object.freeze({Success:1, Warning:2, Error:3}); // Holds the notice duration timeout delay var noticeClearTimeout = null; /** * FUNCTION: displayNotice * USE: Displays a message to the page with the given text. * @param text: Text to be displayed to the page. * @param type: The message type. The type determines the style of the notice. * @param duration: The length the notice is displayed to the page (in milliseconds). * Set value to null to display the notice indefinitely. * @return: N/A. */ function displayNotice(text, type = NoticeType.Success, duration = 3000) { // Remove any previously existing notice duration delay if (noticeClearTimeout) { clearTimeout(noticeClearTimeout); } // Find the notice element var $notice = $('.notice'); // Make sure the notice element exists if (!$notice || $notice.length < 1) { return; } // Define the css classes for the different notice types var styles = [ {type: NoticeType.Success, class: ''}, {type: NoticeType.Warning, class: 'warning'}, {type: NoticeType.Error, class: 'error'} ]; // Remove previous styles for the notice and set the new one for (const style of styles) { // Check if the class is applied let hasClass = $notice.hasClass(style.class); // Add the selected class if (type && type === style.type) { if (!hasClass) { $notice.addClass(style.class); } } else if (hasClass) { // Remove class if exists $notice.removeClass(style.class); } } // Set the notice text $notice.html(text); // Fade in the notice to make it visible $notice.fadeIn(); // Alternatively, you can simply adjust the opacity //$notice.fadeTo('slow', 1); // Set the notice display duration if (duration) { noticeClearTimeout = setTimeout(function() { // Fade out the notice to make it invisible $notice.fadeOut(); // Alternatively, you can simply adjust the opacity //$notice.fadeTo('slow', 0); }, duration); } } </script> </body> </html><!-- // http://programmingnotes.org/ --> |
QUICK NOTES:
The highlighted lines are sections of interest to look out for.
The code is heavily commented, so no further insight is necessary. If you have any questions, feel free to leave a comment below.