diff --git a/index.html b/index.html index efcc355baa5f069a0b62b9d85a3962c98da8ba0e..940013fd4ea084b59ce18b4e04f3615fbf3ac0ce 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ <meta name="description" content="" /> <meta name="author" content="" /> <title>Dashboard - iCity Bosch</title> - <link href="css/styles.css" rel="stylesheet" /> + <link href="css/thirdparty/styles.css" rel="stylesheet" /> <link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" @@ -53,16 +53,17 @@ <!-- Bootstrap dashboard template --> <script - defer src="https://code.jquery.com/jquery-3.5.1.slim.min.js" crossorigin="anonymous" ></script> <script - defer src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous" ></script> - <script defer src="js/thirdparty/scripts.js"></script> + <script src="js/thirdparty/scripts.js"></script> + + <!-- vanillaSelectBox --> + <link href="css/thirdparty/vanillaSelectBox.css" rel="stylesheet" /> <!-- Custom JS --> @@ -133,7 +134,7 @@ <div id="drop-down--bldg-parent"> <span><strong>Building</strong></span> <div class="nowrap"> - <select id="drop-down--bldg"></select> + <select id="drop-down--bldg" multiple></select> </div> </div> <br /> @@ -155,7 +156,6 @@ <span><strong>Chart type</strong></span> <div class="nowrap"> <select id="drop-down--chart-type"> - <option>--Select--</option> <option>Line</option> <option>Heatmap</option> </select> diff --git a/public/css/styles.css b/public/css/thirdparty/styles.css similarity index 100% rename from public/css/styles.css rename to public/css/thirdparty/styles.css diff --git a/public/css/thirdparty/vanillaSelectBox.css b/public/css/thirdparty/vanillaSelectBox.css new file mode 100644 index 0000000000000000000000000000000000000000..b12a75766cb800577ead7694bbf72b44de188f14 --- /dev/null +++ b/public/css/thirdparty/vanillaSelectBox.css @@ -0,0 +1,271 @@ +.hidden-search { + display: none !important; +} + + +li[data-parent].closed{ + display:none !important; +} + +li[data-parent].open:not(.hidden-search){ + display:block !important; +} + +.vsb-menu{ + cursor:pointer; + z-index:1000; + display:block; + visibility: hidden; + position:absolute;/*Don't change*/ + border:1px solid #B2B2B2; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0,0,0,.15); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + border-radius:4px; + font-size : 11px; +} + +.vsb-js-search-zone{ + position:absolute;/*Don't change*/ + z-index:1001; + width: 80%; + min-height:1.8em; + padding: 2px; + background-color: #fff; +} + +.vsb-js-search-zone input{ + border: 1px solid grey; + margin-left: 2px; + width: 96%; + border-radius: 4px; + height: 25px !important; +} + +.vsb-main{ + position: relative;/*Don't change*/ + display: inline-block; + vertical-align: middle; + text-align:left; +} + +.vsb-menu li:hover { + background: linear-gradient(#f5f5f5, #e8e8e8); +} + +.vsb-menu ul{ + user-select:none; + list-style:none; + white-space: nowrap; + margin:0px; + margin-top:4px; + padding-left:10px; + padding-right:10px; + padding-bottom:3px; + color: #333; + cursor:pointer; + overflow-y:auto; +} + +li.disabled{ + cursor:not-allowed; + opacity:0.3; + background-color: #999; +} + +li.overflow{ + cursor:not-allowed; + opacity:0.3; + background-color: #999; +} + +li.short{ + overflow:hidden; + text-overflow: ellipsis; +} + +.vsb-main button{ + min-width: 120px; + border-radius: 0; + width: 100%; + text-align: left; + z-index: 1; + color: #333; + background: white !important; + border: 1px solid #999 !important; + line-height:20px; + font-size:14px; + padding:6px 12px; +} + +.vsb-main button.disabled{ + cursor:not-allowed; + opacity:0.65; +} + +.vsb-main .title { + margin-right: 6px; + user-select:none; +} + +.vsb-main li:hover { + background: linear-gradient(#f5f5f5, #e8e8e8); +} + +.vsb-main ul{ + white-space: nowrap; +} + +.vsb-menu li { + font-size: 14px; + background-color: #fff; + min-height:1.4em; + padding: 0.2em 2em 0.2em 1em; +} + +.vsb-menu li.grouped-option b { + display: inline-block; + font-size: 15px; + margin-left:10px; + transform: translate(-18px); +} + +.vsb-menu li.grouped-option.open span { + display: inline-block; + font-size: inherit; + margin-top:-2px; + height: 8px; + width: 8px; + transform: translate(-38px) rotate(45deg); + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu li.grouped-option.closed span { + display: inline-block; + font-size: inherit; + height: 8px; + width: 8px; + transform: translate(-38px) rotate(-45deg); + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu li.grouped-option i { + display: inline-block; + font-size: inherit; + float:left; + font-weight:bold; + margin-left:22px; + margin-right:2px; + height: 11px; + width: 8px; + border : 1px solid; + border-radius : 3px; + padding: 1px 3px 2px 3px; + margin-top:0px; + color:black; +} + +.vsb-menu li.grouped-option.checked i::after { + content: ""; + display: inline-block; + font-size: inherit; + color: #333; + float:left; + margin-left:0px; + display: inline-block; + transform: rotate(45deg); + height: 8px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; +} + +.vsb-menu :not(.multi) li.active { + margin-left:7px; +} + +.vsb-menu :not(.multi) li.active::before { + content: ""; + display: inline-block; + font-size: inherit; + margin-left:-18px; + transform: rotate(45deg); + height: 10px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; + border-radius:2px; +} + +.vsb-menu .multi li { + font-size: 14px; + background-color: #fff; + min-height:1.4em; + padding: 0.2em 2em 0.2em 26px; +} + +.vsb-menu .multi li.grouped-option { + font-size: 15px; + padding-left: 5px; +} + + +.vsb-menu .multi li.grouped-option:hover { + font-weight: bold; + text-decoration: underline; + color:rgb(52, 31, 112); +} + +.vsb-menu .multi li:not(.grouped-option)::before{ + content: ""; + display: inline-block; + font-size: inherit; + float:left; + font-weight:bold; + margin-left:-22px; + margin-right:2px; + border : 1px solid; + border-radius : 3px; + padding : 7px; + margin-top:0px; + color:black; +} + +.vsb-menu .multi li:not(.grouped-option).active::after { + content: ""; + display: inline-block; + font-size: inherit; + color: #333; + float:left; + margin-left:-18px; + display: inline-block; + transform: rotate(45deg); + margin-top:1px; + height: 8px; + width: 5px; + border-bottom: 3px solid black; + border-right: 3px solid black; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + + +li[data-parent]{ + padding-left: 50px !important; +} + diff --git a/public/js/dropDownList.js b/public/js/dropDownList.js index 815c3375a28b07467c89a8a615a40fe6a3a101cc..b59951573304eef9acfbd78ea359a479a8bb0fcb 100644 --- a/public/js/dropDownList.js +++ b/public/js/dropDownList.js @@ -27,8 +27,10 @@ import { hideLoadingSpinner, } from "./src_modules/loadingIndicator.mjs"; +import { vanillaSelectBox } from "./thirdparty/vanillaSelectBox.mjs"; + const buildingsAvailableSensorsArr = [ - ["--Select--", "", ""], + // ["--Select--", "", ""], ["Bau 101", "Vorlauftemperatur", "15 min"], ["Bau 101", "Vorlauftemperatur", "60 min"], @@ -132,27 +134,42 @@ const makeDropDown = function (dataArr, filtersAsArray, targetElement) { * Use the `makeDropDown` function to create the first two levels of the linked drop down lists * @returns {undefined} */ -const applyDropDown = function () { +const applyDropDownLevelOneTwo = function () { const selectLevel1Value = document.querySelector("#drop-down--bldg").value; - const selectLevel2 = document.querySelector("#drop-down--sensor"); + const selectLevel2DOMString = "#drop-down--sensor"; + const selectLevel2 = document.querySelector(selectLevel2DOMString); makeDropDown(buildingsAvailableSensorsArr, [selectLevel1Value], selectLevel2); - applyDropDown2(); + applyDropDownLevelThree(); + + // Create our dropdown list using `vanillaSelectBox` + new vanillaSelectBox(selectLevel2DOMString, { + "placeHolder": "--Select--", + }); }; /** * Use the `makeDropDown` function to create the third level of the linked drop down lists * @returns {undefined} */ -const applyDropDown2 = function () { +const applyDropDownLevelThree = function () { const selectLevel1Value = document.querySelector("#drop-down--bldg").value; const selectLevel2Value = document.querySelector("#drop-down--sensor").value; - const selectLevel3 = document.querySelector("#drop-down--sampling-rate"); + const selectLevel3DOMString = "#drop-down--sampling-rate"; + const selectLevel3 = document.querySelector(selectLevel3DOMString); makeDropDown( buildingsAvailableSensorsArr, [selectLevel1Value, selectLevel2Value], selectLevel3 ); + + // Create our dropdown list using `vanillaSelectBox` + new vanillaSelectBox(selectLevel3DOMString, { + "placeHolder": "--Select--", + }); + + // Create our fourth level dropdown + styleFourthLevelDropDown(); }; /** @@ -160,21 +177,53 @@ const applyDropDown2 = function () { * @returns {undefined} */ const populateFirstLevelDropDown = function () { - const el = document.querySelector("#drop-down--bldg"); + const selectLevel1DOMString = "#drop-down--bldg"; + const selectLevel1 = document.querySelector(selectLevel1DOMString); const uniqueList = getUniqueValues(buildingsAvailableSensorsArr, 0); - populateDropDown(el, uniqueList); + populateDropDown(selectLevel1, uniqueList); + + // Create our dropdown list using `vanillaSelectBox`; supports the selection of multiple options + new vanillaSelectBox(selectLevel1DOMString, { + "disableSelectAll": true, + "maxSelect": 3, + "placeHolder": "--Select--", + "search": false, + }); +}; + +/** + * Use the `vanillaDropDown` library to style the fourth level drop down list + * + * @returns {undefined} + */ +const styleFourthLevelDropDown = function () { + const selectLevel4DOMString = "#drop-down--chart-type"; + + // Create our dropdown list using `vanillaSelectBox` + new vanillaSelectBox(selectLevel4DOMString, { + "placeHolder": "--Select--", + }); }; document .querySelector("#drop-down--bldg") - .addEventListener("change", applyDropDown); + .addEventListener("change", applyDropDownLevelOneTwo); document .querySelector("#drop-down--sensor") - .addEventListener("change", applyDropDown2); + .addEventListener("change", applyDropDownLevelThree); + +/** + * Callback function that wraps the logic of populating the linked drop down lists. + * Will run on `DOMContentLoaded` event + * + * @returns {undefined} + */ +const afterDocumentLoads = function () { + populateFirstLevelDropDown(); + applyDropDownLevelOneTwo(); +}; -// These functions run after "DOMContentLoaded" event -populateFirstLevelDropDown(); -applyDropDown(); +document.addEventListener("DOMContentLoaded", afterDocumentLoads); /** * Get the values from the currently selected options in the linked drop dpwn lists diff --git a/public/js/src_modules/calculateTemperatureDiff.mjs b/public/js/src_modules/calculateTemperatureDiff.mjs index 292252843794a109f8522b8cdad5e314a4eabe86..b429303342217684dabf1b0ca3126ad1b5b189e8 100644 --- a/public/js/src_modules/calculateTemperatureDiff.mjs +++ b/public/js/src_modules/calculateTemperatureDiff.mjs @@ -1,11 +1,14 @@ "use strict"; +import { checkForAndDeleteUniqueObservationsFromLargerArray } from "./chartHelpers.mjs"; + import { getMetadataPlusObservationsFromSingleOrMultipleDatastreams } from "./fetchData.mjs"; import { extractPhenomenonNameFromDatastreamName } from "./fetchedDataProcessing.mjs"; /** * Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL) + * @async * @param {String} baseUrl Base URL of the STA server * @param {Object} urlParams The URL parameters to be sent together with the GET request * @param {String} buildingId The building ID as a string @@ -40,21 +43,47 @@ export const calculateVorlaufMinusRuecklaufTemperature = async function ( [metadataVorlauf, metadataRuecklauf], ] = observationsPlusMetadata; + // Compare the lengths of the observations arrays for VL and RL, + // delete the unique observation(s), if necessary + const [vorlaufTemperatureObsFinalArr, ruecklaufTemperatureObsFinalArr] = + vorlaufTemperatureObsArr.length === ruecklaufTemperatureObsArr.length + ? [vorlaufTemperatureObsArr, ruecklaufTemperatureObsArr] + : checkForAndDeleteUniqueObservationsFromLargerArray( + vorlaufTemperatureObsArr, + ruecklaufTemperatureObsArr + ); + // Extract the temperature values - const vorlaufTemperatureValues = vorlaufTemperatureObsArr.map( + const vorlaufTemperatureValues = vorlaufTemperatureObsFinalArr.map( (vlTempObs) => vlTempObs[1] ); - const ruecklaufTemperatureValues = ruecklaufTemperatureObsArr.map( + const ruecklaufTemperatureValues = ruecklaufTemperatureObsFinalArr.map( (rlTempObs) => rlTempObs[1] ); // The arrays have equal length, we need only use one of them for looping // Resulting array contains the following pairs (timestamp + dT) const vorlaufMinusRuecklaufTemperatureObs = vorlaufTemperatureObsArr.map( - (vlTempObs, i) => [ - vlTempObs[0], // timestamp - vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[i], - ] + (vlTempObs, i) => { + // Use timestamp from VL, since is equal to that of RL + const timestamp = vlTempObs[0]; + + // Case 1: One of the observation values is `null`, + // no need to calculate temperature difference + if ( + vorlaufTemperatureValues[i] === null || + ruecklaufTemperatureValues[i] === null + ) { + return [timestamp, null]; + } + + // Case 2: Neither of the observation values is `null`, + // calculate temperature difference + return [ + timestamp, + vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[i], + ]; + } ); // From Vorlauf metadata, extract `name` and `unitOfMeasurement` diff --git a/public/js/src_modules/chartHelpers.mjs b/public/js/src_modules/chartHelpers.mjs index 5b61a426cba4a61a28f80c585f3c0ef135c7e72b..1ebd5b81f3cdca892ed9e9cb717d62642cf47877 100644 --- a/public/js/src_modules/chartHelpers.mjs +++ b/public/js/src_modules/chartHelpers.mjs @@ -8,6 +8,173 @@ const chartExportOptions = { }, }; +/** + * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other + * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations + * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations + * @returns {Array} An array of timestamps missing from either set of observations + */ +const getSymmetricDifferenceBetweenArrays = function ( + obsTimestampArrayOne, + obsTimestampArrayTwo +) { + return obsTimestampArrayOne + .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) + .concat( + obsTimestampArrayTwo.filter( + (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) + ) + ); +}; + +/** + * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other + * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations + * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations + * @returns {Array} An array of the indexes of the missing observations + */ +const getIndexesOfUniqueObservations = function ( + uniqueTimestampsArr, + largerObsTimestampArr +) { + return uniqueTimestampsArr.map((index) => + largerObsTimestampArr.indexOf(index) + ); +}; + +/** + * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other + * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations + * @param {Array} largerObsArr The larger array of observations (timestamp + value) + * @returns {Array} The larger array with the unique indexes removed + */ +const removeUniqueObservationsFromLargerArray = function ( + uniqueIndexesArr, + largerObsArr +) { + // Create a reversed copy of the indexes array, so that the larger index is removed first + const reversedUniqueIndexesArr = uniqueIndexesArr.reverse(); + + // Create a copy the larger observation array, will be modified in place + const processedLargerObsArr = largerObsArr; + + reversedUniqueIndexesArr.forEach((index) => { + if (index > -1) { + processedLargerObsArr.splice(index, 1); + } + }); + + return processedLargerObsArr; +}; + +/** + * Compares the length of two input arrays to determine the larger one + * @param {Array} firstArr First input array + * @param {Array} secondArr Second input array + * @returns {Array} The larger array + */ +const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { + if (firstArr.length === secondArr.length) return; + + if (firstArr.length > secondArr.length) return firstArr; + + if (firstArr.length < secondArr.length) return secondArr; +}; + +/** + * Compares the length of two input arrays to determine the smaller one + * @param {Array} firstArr First input array + * @param {Array} secondArr Second input array + * @returns {Array} The smaller array + */ +const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { + if (firstArr.length === secondArr.length) return; + + if (firstArr.length < secondArr.length) return firstArr; + + if (firstArr.length > secondArr.length) return secondArr; +}; + +/** + * Utility function for deleting the unique observations from a larger array + * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API + * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API + * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths + */ +const deleteUniqueObservationsFromLargerArray = function ( + obsArrayOne, + obsArrayTwo +) { + // Create arrays with timestamps only + const obsArrayOneTimestamp = obsArrayOne.map( + (obsTimeValue) => obsTimeValue[0] + ); + const obsArrayTwoTimestamp = obsArrayTwo.map( + (obsTimeValue) => obsTimeValue[0] + ); + + const missingTimestamp = getSymmetricDifferenceBetweenArrays( + obsArrayOneTimestamp, + obsArrayTwoTimestamp + ); + + // Determine the larger observation timestamp array + const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays( + obsArrayOneTimestamp, + obsArrayTwoTimestamp + ); + + // Indexes of the missing observations + const indexesMissingObsArr = getIndexesOfUniqueObservations( + missingTimestamp, + biggerObsTimestampArr + ); + + // Determine the larger observation array + const biggerObsArr = getLargerArrayBetweenTwoInputArrays( + obsArrayOne, + obsArrayTwo + ); + + // Determine the smaller observation array + const smallerObsArr = getSmallerArrayBetweenTwoInputArrays( + obsArrayOne, + obsArrayTwo + ); + + // Remove the missing observation from the larger array of observations + const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray( + indexesMissingObsArr, + biggerObsArr + ); + + return [modifiedBiggerObsArr, smallerObsArr]; +}; + +/** + * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained + * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API + * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API + * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths + */ +const checkForAndDeleteUniqueObservationsFromLargerArray = function ( + obsArrayOne, + obsArrayTwo +) { + if (obsArrayOne.length === obsArrayTwo.length) return; + + // Case 1: obsArrayOne.length < obsArrayTwo.length + if (obsArrayOne.length < obsArrayTwo.length) { + const [biggerObsArr, smallerObsArr] = + deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); + + return [smallerObsArr, biggerObsArr]; + } + + // Case 2: obsArrayOne.length > obsArrayTwo.length + return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); +}; + /** * Convert a hexadecimal color code obtained from the Highcharts object (`Highcharts.getOptions().colors`) to its equivalent RGB color code * @param {String} hexCode Input hex color code @@ -142,6 +309,7 @@ const removeTransparencyFromColor = function (rgbaColor) { export { chartExportOptions, + checkForAndDeleteUniqueObservationsFromLargerArray, createCombinedTextDelimitedByAmpersand, createCombinedTextDelimitedByComma, createFullTitleForLineOrColumnChart, diff --git a/public/js/src_modules/chartScatterPlot.mjs b/public/js/src_modules/chartScatterPlot.mjs index d690741037927090deb16d23b571e8fcb944784f..e1bd2143d4e744f5dcb0820f50f56872760d29e1 100644 --- a/public/js/src_modules/chartScatterPlot.mjs +++ b/public/js/src_modules/chartScatterPlot.mjs @@ -2,6 +2,7 @@ import { chartExportOptions, + checkForAndDeleteUniqueObservationsFromLargerArray, convertHexColorToRGBColor, createCombinedTextDelimitedByAmpersand, createCombinedTextDelimitedByComma, @@ -9,173 +10,6 @@ import { removeTransparencyFromColor, } from "./chartHelpers.mjs"; -/** - * Determines the timestamps that are missing from a smaller set of observations. Based on the comparison of two observation arrays, where one array is larger than the other - * @param {Array} obsTimestampArrayOne An array of timestamps for the first set of observations - * @param {Array} obsTimestampArrayTwo An array of timstamps for the second set of observations - * @returns {Array} An array of timestamps missing from either set of observations - */ -const getSymmetricDifferenceBetweenArrays = function ( - obsTimestampArrayOne, - obsTimestampArrayTwo -) { - return obsTimestampArrayOne - .filter((timestampOne) => !obsTimestampArrayTwo.includes(timestampOne)) - .concat( - obsTimestampArrayTwo.filter( - (timestampTwo) => !obsTimestampArrayOne.includes(timestampTwo) - ) - ); -}; - -/** - * Determines the indexes of timestamps that are unique to the larger set of observatiuons. Based on the comparison of two observation arrays, where one array is larger than the other - * @param {Array} uniqueTimestampsArr An array of timestamps unique to the larger set of observations - * @param {Array} largerObsTimestampArr An array of timestamps for the larger set of observations - * @returns {Array} An array of the indexes of the missing observations - */ -const getIndexesOfUniqueObservations = function ( - uniqueTimestampsArr, - largerObsTimestampArr -) { - return uniqueTimestampsArr.map((index) => - largerObsTimestampArr.indexOf(index) - ); -}; - -/** - * Removes observations (by modifying array in place) that are unique to a larger set of observations. Based on the comparison of two observation arrays, where one array is larger than the other - * @param {Array} uniqueIndexesArr An array of the indexes unique to the larger set of observations - * @param {Array} largerObsArr The larger array of observations (timestamp + value) - * @returns {Array} The larger array with the unique indexes removed - */ -const removeUniqueObservationsFromLargerArray = function ( - uniqueIndexesArr, - largerObsArr -) { - // Create a reversed copy of the indexes array, so that the larger index is removed first - const reversedUniqueIndexesArr = uniqueIndexesArr.reverse(); - - // Create a copy the larger observation array, will be modified in place - const processedLargerObsArr = largerObsArr; - - reversedUniqueIndexesArr.forEach((index) => { - if (index > -1) { - processedLargerObsArr.splice(index, 1); - } - }); - - return processedLargerObsArr; -}; - -/** - * Compares the length of two input arrays to determine the larger one - * @param {Array} firstArr First input array - * @param {Array} secondArr Second input array - * @returns {Array} The larger array - */ -const getLargerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { - if (firstArr.length === secondArr.length) return; - - if (firstArr.length > secondArr.length) return firstArr; - - if (firstArr.length < secondArr.length) return secondArr; -}; - -/** - * Compares the length of two input arrays to determine the smaller one - * @param {Array} firstArr First input array - * @param {Array} secondArr Second input array - * @returns {Array} The smaller array - */ -const getSmallerArrayBetweenTwoInputArrays = function (firstArr, secondArr) { - if (firstArr.length === secondArr.length) return; - - if (firstArr.length < secondArr.length) return firstArr; - - if (firstArr.length > secondArr.length) return secondArr; -}; - -/** - * Utility function for deleting the unique observations from a larger array - * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API - * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API - * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths - */ -const deleteUniqueObservationsFromLargerArray = function ( - obsArrayOne, - obsArrayTwo -) { - // Create arrays with timestamps only - const obsArrayOneTimestamp = obsArrayOne.map( - (obsTimeValue) => obsTimeValue[0] - ); - const obsArrayTwoTimestamp = obsArrayTwo.map( - (obsTimeValue) => obsTimeValue[0] - ); - - const missingTimestamp = getSymmetricDifferenceBetweenArrays( - obsArrayOneTimestamp, - obsArrayTwoTimestamp - ); - - // Determine the larger observation timestamp array - const biggerObsTimestampArr = getLargerArrayBetweenTwoInputArrays( - obsArrayOneTimestamp, - obsArrayTwoTimestamp - ); - - // Indexes of the missing observations - const indexesMissingObsArr = getIndexesOfUniqueObservations( - missingTimestamp, - biggerObsTimestampArr - ); - - // Determine the larger observation array - const biggerObsArr = getLargerArrayBetweenTwoInputArrays( - obsArrayOne, - obsArrayTwo - ); - - // Determine the smaller observation array - const smallerObsArr = getSmallerArrayBetweenTwoInputArrays( - obsArrayOne, - obsArrayTwo - ); - - // Remove the missing observation from the larger array of observations - const modifiedBiggerObsArr = removeUniqueObservationsFromLargerArray( - indexesMissingObsArr, - biggerObsArr - ); - - return [modifiedBiggerObsArr, smallerObsArr]; -}; - -/** - * Utility function for deleting the unique observations from a larger array AND ensuring the order of input arrays is maintained - * @param {Array} obsArrayOne Array of observations (timestamp + value) that is response from SensorThings API - * @param {Array} obsArrayTwo Array of observations (timestamp + value) that is response from SensorThings API - * @returns {Array} Two arrays of observations (timestamp + value) with matching timestamps and equal lengths - */ -const checkForAndDeleteUniqueObservationsFromLargerArray = function ( - obsArrayOne, - obsArrayTwo -) { - if (obsArrayOne.length === obsArrayTwo.length) return; - - // Case 1: obsArrayOne.length < obsArrayTwo.length - if (obsArrayOne.length < obsArrayTwo.length) { - const [biggerObsArr, smallerObsArr] = - deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); - - return [smallerObsArr, biggerObsArr]; - } - - // Case 2: obsArrayOne.length > obsArrayTwo.length - return deleteUniqueObservationsFromLargerArray(obsArrayOne, obsArrayTwo); -}; - /** * Extracts and combines observation values from two input observation arrays of equal length * @param {Array} obsArrayOne First set of N observations (timestamp + value) diff --git a/public/js/thirdparty/vanillaSelectBox.mjs b/public/js/thirdparty/vanillaSelectBox.mjs new file mode 100644 index 0000000000000000000000000000000000000000..391efe9e5ae32fb4d1114640683cd69e61c454f6 --- /dev/null +++ b/public/js/thirdparty/vanillaSelectBox.mjs @@ -0,0 +1,1332 @@ +/* +Copyright (C) Philippe Meyer 2019-2021 +Distributed under the MIT License + +vanillaSelectBox : v0.75 : Remote search ready + local search modification : when a check on optgroup checks children only + if they not excluded from search. +vanillaSelectBox : v0.72 : Remote search (WIP) bugfix [x] Select all duplicated +vanillaSelectBox : v0.71 : Remote search (WIP) better code +vanillaSelectBox : v0.70 : Remote search (WIP) for users to test +vanillaSelectBox : v0.65 : Two levels: bug fix : groups are checked/unchecked when check all/uncheck all is clicked +vanillaSelectBox : v0.64 : Two levels: groups are now checkable to check/uncheck the children options +vanillaSelectBox : v0.63 : Two levels: one click on the group selects / unselects children +vanillaSelectBox : v0.62 : New option: maxOptionWidth set a maximum width for each option for narrow menus +vanillaSelectBox : v0.61 : New option: maxSelect, set a maximum to the selectable options in a multiple choice menu +vanillaSelectBox : v0.60 : Two levels: Optgroups are now used to show two level dropdowns +vanillaSelectBox : v0.59 : Bug fix : search box was overlapping first item in single selects +vanillaSelectBox : v0.58 : Bug fixes +vanillaSelectBox : v0.57 : Bug fix (minWidth option not honored) +vanillaSelectBox : v0.56 : The multiselect checkboxes are a little smaller, maxWidth option is now working + added minWidth option as well + The button has now a style attribute to protect its appearance +vanillaSelectBox : v0.55 : All attributes from the original select options are copied to the selectBox element +vanillaSelectBox : v0.54 : if all the options of the select are selected by the user then the check all checkbox is checked +vanillaSelectBox : v0.53 : if all the options of the select are selected then the check all checkbox is checked +vanillaSelectBox : v0.52 : Better support of select('all') => command is consistent with checkbox and selecting / deselecting while searching select / uncheck only the found items +vanillaSelectBox : v0.51 : Translations for select all/clear all + minor css corrections + don't select disabled items +vanillaSelectBox : v0.50 : PR by jaguerra2017 adding a select all/clear all check button + optgroup support ! +vanillaSelectBox : v0.41 : Bug corrected, the menu content was misplaced if a css transform was applied on a parent +vanillaSelectBox : v0.40 : A click on one selectBox close the other opened boxes +vanillaSelectBox : v0.35 : You can enable and disable items +vanillaSelectBox : v0.30 : The menu stops moving around on window resize and scroll + z-index in order of creation for multiple instances +vanillaSelectBox : v0.26 : Corrected bug in stayOpen mode with disable() function +vanillaSelectBox : v0.25 : New option stayOpen, and the dropbox is no longer a dropbox but a nice multi-select +previous version : v0.24 : corrected bug affecting options with more than one class +https://github.com/PhilippeMarcMeyer/vanillaSelectBox +*/ + +let VSBoxCounter = function () { + let count = 0; + let instances = []; + return { + set: function (instancePtr) { + instances.push({ offset: ++count, ptr: instancePtr }); + return instances[instances.length - 1].offset; + }, + remove: function (instanceNr) { + let temp = instances.filter(function (x) { + return x.offset != instanceNr; + }) + instances = temp.splice(0); + }, + closeAllButMe: function (instanceNr) { + instances.forEach(function (x) { + if (x.offset != instanceNr) { + x.ptr.closeOrder(); + } + }); + } + }; +}(); + +function vanillaSelectBox(domSelector, options) { + let self = this; + this.instanceOffset = VSBoxCounter.set(self); + this.domSelector = domSelector; + this.root = document.querySelector(domSelector); + this.rootToken = null; + this.main; + this.button; + this.title; + this.isMultiple = this.root.hasAttribute("multiple"); + this.multipleSize = this.isMultiple && this.root.hasAttribute("size") ? parseInt(this.root.getAttribute("size")) : -1; + this.isOptgroups = false; + this.currentOptgroup = 0; + this.drop; + this.top; + this.left; + this.options; + this.listElements; + this.isDisabled = false; + this.search = false; + this.searchZone = null; + this.inputBox = null; + this.disabledItems = []; + this.ulminWidth = 140; + this.ulmaxWidth = 280; + this.ulminHeight = 25; + this.maxOptionWidth = Infinity; + this.maxSelect = Infinity; + this.isInitRemote = false; + this.isSearchRemote = false; + this.onInit = null; + this.onSearch = null; // if isRemote is true : a user defined function that loads more options from the back + this.onInitSize = null; + this.forbidenAttributes = ["class", "selected", "disabled", "data-text", "data-value", "style"]; + this.forbidenClasses = ["active", "disabled"]; + this.userOptions = { + maxWidth: 500, + minWidth: -1, + maxHeight: 400, + translations: { "all": "All", "items": "items", "selectAll": "Select All", "clearAll": "Clear All" }, + search: false, + placeHolder: "", + stayOpen: false, + disableSelectAll: false + } + if (options) { + if (options.maxWidth != undefined) { + this.userOptions.maxWidth = options.maxWidth; + } + if (options.minWidth != undefined) { + this.userOptions.minWidth = options.minWidth; + } + if (options.maxHeight != undefined) { + this.userOptions.maxHeight = options.maxHeight; + } + if (options.translations != undefined) { + for (var property in options.translations) { + if (options.translations.hasOwnProperty(property)) { + if (this.userOptions.translations[property]) { + this.userOptions.translations[property] = options.translations[property]; + } + } + } + } + if (options.placeHolder != undefined) { + this.userOptions.placeHolder = options.placeHolder; + } + if (options.search != undefined) { + this.search = options.search; + } + if (options.remote != undefined && options.remote) { + + // user defined onInit function + if (options.remote.onInit!= undefined && typeof options.remote.onInit === 'function') { + this.onInit = options.remote.onInit; + this.isInitRemote = true; + } + if (options.remote.onInitSize != undefined) { + this.onInitSize = options.remote.onInitSize; + if (this.onInitSize < 3) this.onInitSize = 3; + } + // user defined remote search function + if (options.remote.onSearch != undefined && typeof options.remote.onSearch === 'function') { + this.onSearch = options.remote.onSearch; + this.isSearchRemote = true; + } + } + + if (options.stayOpen != undefined) { + this.userOptions.stayOpen = options.stayOpen; + } + + if (options.disableSelectAll != undefined) { + this.userOptions.disableSelectAll = options.disableSelectAll; + } + + if (options.maxSelect != undefined && !isNaN(options.maxSelect) && options.maxSelect >= 1) { + this.maxSelect = options.maxSelect; + this.userOptions.disableSelectAll = true; + } + + if (options.maxOptionWidth != undefined && !isNaN(options.maxOptionWidth) && options.maxOptionWidth >= 20) { + this.maxOptionWidth = options.maxOptionWidth; + this.ulminWidth = options.maxOptionWidth + 60; + this.ulmaxWidth = options.maxOptionWidth + 60; + } + } + + this.closeOrder = function () { + let self = this; + if (!self.userOptions.stayOpen) { + self.drop.style.visibility = "hidden"; + if (self.search) { + self.inputBox.value = ""; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("hide"); + }); + } + } + } + + this.getCssArray = function (selector) { + // Why inline css ? To protect the button display from foreign css files + let cssArray = []; + if (selector === ".vsb-main button") { + cssArray = [ + { "key": "min-width", "value": "120px" }, + { "key": "border-radius", "value": "0" }, + { "key": "width", "value": "100%" }, + { "key": "text-align", "value": "left" }, + { "key": "z-index", "value": "1" }, + { "key": "color", "value": "#333" }, + { "key": "background", "value": "white !important" }, + { "key": "border", "value": "1px solid #999 !important" }, + { "key": "line-height", "value": "20px" }, + { "key": "font-size", "value": "14px" }, + { "key": "padding", "value": "6px 12px" } + ] + } + + return cssArrayToString(cssArray); + + function cssArrayToString(cssList) { + let list = ""; + cssList.forEach(function (x) { + list += x.key + ":" + x.value + ";"; + }); + return list; + } + } + + this.init = function () { + let self = this; + if (self.isInitRemote) { + self.onInit("",self.onInitSize) + .then(function (data) { + self.buildSelect(data); + self.createTree(); + }); + } else { + self.createTree(); + } + } + + this.createTree = function () { + + this.rootToken = self.domSelector.replace(/[^A-Za-z0-9]+/, "") + this.root.style.display = "none"; + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + already.remove(); + } + this.main = document.createElement("div"); + this.root.parentNode.insertBefore(this.main, this.root.nextSibling); + this.main.classList.add("vsb-main"); + this.main.setAttribute("id", "btn-group-" + this.rootToken); + this.main.style.marginLeft = this.main.style.marginLeft; + if (self.userOptions.stayOpen) { + this.main.style.minHeight = (this.userOptions.maxHeight + 10) + "px"; + } + + if (self.userOptions.stayOpen) { + this.button = document.createElement("div"); + } else { + this.button = document.createElement("button"); + var cssList = self.getCssArray(".vsb-main button"); + this.button.setAttribute("style", cssList); + } + this.button.style.maxWidth = this.userOptions.maxWidth + "px"; + if (this.userOptions.minWidth !== -1) { + this.button.style.minWidth = this.userOptions.minWidth + "px"; + } + + this.main.appendChild(this.button); + this.title = document.createElement("span"); + this.button.appendChild(this.title); + this.title.classList.add("title"); + let caret = document.createElement("span"); + this.button.appendChild(caret); + + caret.classList.add("caret"); + caret.style.position = "absolute"; + caret.style.right = "8px"; + caret.style.marginTop = "8px"; + + if (self.userOptions.stayOpen) { + caret.style.display = "none"; + this.title.style.paddingLeft = "20px"; + this.title.style.fontStyle = "italic"; + this.title.style.verticalAlign = "20%"; + } + + this.drop = document.createElement("div"); + this.main.appendChild(this.drop); + this.drop.classList.add("vsb-menu"); + this.drop.style.zIndex = 2000 - this.instanceOffset; + this.ul = document.createElement("ul"); + this.drop.appendChild(this.ul); + + this.ul.style.maxHeight = this.userOptions.maxHeight + "px"; + this.ul.style.minWidth = this.ulminWidth + "px"; + this.ul.style.maxWidth = this.ulmaxWidth + "px"; + this.ul.style.minHeight = this.ulminHeight + "px"; + if (this.isMultiple) { + this.ul.classList.add("multi"); + if (!self.userOptions.disableSelectAll) { + let selectAll = document.createElement("option"); + selectAll.setAttribute("value", 'all'); + selectAll.innerText = self.userOptions.translations.selectAll; + this.root.insertBefore(selectAll, (this.root.hasChildNodes()) + ? this.root.childNodes[0] + : null); + } + } + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + + if (this.search) { + this.searchZone = document.createElement("div"); + this.ul.appendChild(this.searchZone); + this.searchZone.classList.add("vsb-js-search-zone"); + this.searchZone.style.zIndex = 2001 - this.instanceOffset; + this.inputBox = document.createElement("input"); + this.searchZone.appendChild(this.inputBox); + this.inputBox.setAttribute("type", "text"); + this.inputBox.setAttribute("id", "search_" + this.rootToken); + if (this.maxOptionWidth < Infinity) { + this.searchZone.style.maxWidth = self.maxOptionWidth + 30 + "px"; + this.inputBox.style.maxWidth = self.maxOptionWidth + 30 + "px"; + } + + var para = document.createElement("p"); + this.ul.appendChild(para); + para.style.fontSize = "12px"; + para.innerHTML = " "; + this.ul.addEventListener("scroll", function (e) { + var y = this.scrollTop; + self.searchZone.parentNode.style.top = y + "px"; + }); + } + + this.options = document.querySelectorAll(this.domSelector + " > option"); + Array.prototype.slice.call(this.options).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let originalAttrs; + if (x.hasAttributes()) { + originalAttrs = Array.prototype.slice.call(x.attributes) + .filter(function (a) { + return self.forbidenAttributes.indexOf(a.name) === -1 + }); + } + let classes = x.getAttribute("class"); + if (classes) { + classes = classes + .split(" ") + .filter(function (c) { + return self.forbidenClasses.indexOf(c) === -1 + }); + } else { + classes = []; + } + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + let isDisabled = x.hasAttribute("disabled"); + + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + + if (originalAttrs !== undefined) { + originalAttrs.forEach(function (a) { + li.setAttribute(a.name, a.value); + }); + } + + classes.forEach(function (x) { + li.classList.add(x); + }); + + if (self.maxOptionWidth < Infinity) { + li.classList.add("short"); + li.style.maxWidth = self.maxOptionWidth + "px"; + } + + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = ","; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + if (isDisabled) { + li.classList.add("disabled"); + } + li.appendChild(document.createTextNode(" " + text)); + }); + + if (document.querySelector(this.domSelector + ' optgroup') !== null) { + this.isOptgroups = true; + //this.isRemote = false;// debug + this.options = document.querySelectorAll(this.domSelector + " option"); + let groups = document.querySelectorAll(this.domSelector + ' optgroup'); + Array.prototype.slice.call(groups).forEach(function (group) { + let groupOptions = group.querySelectorAll('option'); + let li = document.createElement("li"); + let span = document.createElement("span"); + let iCheck = document.createElement("i"); + let labelElement = document.createElement("b"); + let dataWay = group.getAttribute("data-way"); + if (!dataWay) dataWay = "closed"; + if (!dataWay || (dataWay !== "closed" && dataWay !== "open")) dataWay = "closed"; + li.appendChild(span); + li.appendChild(iCheck); + self.ul.appendChild(li); + li.classList.add('grouped-option'); + li.classList.add(dataWay); + self.currentOptgroup++; + let optId = self.rootToken + "-opt-" + self.currentOptgroup; + li.id = optId; + li.appendChild(labelElement); + labelElement.appendChild(document.createTextNode(group.label)); + li.setAttribute("data-text", group.label); + self.ul.appendChild(li); + + Array.prototype.slice.call(groupOptions).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let classes = x.getAttribute("class"); + if (classes) { + classes = classes.split(" "); + } + else { + classes = []; + } + classes.push(dataWay); + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + li.setAttribute("data-parent", optId); + if (classes.length != 0) { + classes.forEach(function (x) { + li.classList.add(x); + }); + } + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = ","; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + li.appendChild(document.createTextNode(text)); + }) + }) + } + + if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = self.userOptions.translations.items || "items" + selectedTexts = nrActives + " " + wordForItems; + } + } + if (self.isMultiple) { + self.title.innerHTML = selectedTexts; + } + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + this.listElements = this.drop.querySelectorAll("li:not(.grouped-option)"); + if (self.search) { + self.inputBox.addEventListener("keyup", function (e) { + let searchValue = e.target.value.toUpperCase(); + let searchValueLength = searchValue.length; + let nrFound = 0; + let nrChecked = 0; + let selectAll = null; + if (self.isSearchRemote) { + if (searchValueLength == 0) { + self.remoteSearchIntegrate(null); + } else if (searchValueLength >= 3) { + self.onSearch(searchValue) + .then(function (data) { + self.remoteSearchIntegrate(data); + }); + } + } else { + if (searchValueLength < 3) { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.getAttribute('data-value') === 'all') { + selectAll = x; + } else { + x.classList.remove("hidden-search"); + nrFound++; + nrChecked += x.classList.contains('active'); + } + }); + } else { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.getAttribute('data-value') !== 'all') { + let text = x.getAttribute("data-text").toUpperCase(); + if (text.indexOf(searchValue) === -1 && x.getAttribute('data-value') !== 'all') { + x.classList.add("hidden-search"); + } else { + nrFound++; + x.classList.remove("hidden-search"); + nrChecked += x.classList.contains('active'); + } + } else { + selectAll = x; + } + }); + } + if (selectAll) { + if (nrFound === 0) { + selectAll.classList.add('disabled'); + } else { + selectAll.classList.remove('disabled'); + } + if (nrChecked !== nrFound) { + selectAll.classList.remove("active"); + selectAll.innerText = self.userOptions.translations.selectAll; + selectAll.setAttribute('data-selected', 'false') + } else { + selectAll.classList.add("active"); + selectAll.innerText = self.userOptions.translations.clearAll; + selectAll.setAttribute('data-selected', 'true') + } + } + } + + }); // + } + + if (self.userOptions.stayOpen) { + self.drop.style.visibility = "visible"; + self.drop.style.boxShadow = "none"; + self.drop.style.minHeight = (this.userOptions.maxHeight + 10) + "px"; + self.drop.style.position = "relative"; + self.drop.style.left = "0px"; + self.drop.style.top = "0px"; + self.button.style.border = "none"; + } else { + this.main.addEventListener("click", function (e) { + if (self.isDisabled) return; + self.drop.style.left = self.left + "px"; + self.drop.style.top = self.top + "px"; + self.drop.style.visibility = "visible"; + document.addEventListener("click", docListener); + e.preventDefault(); + e.stopPropagation(); + if (!self.userOptions.stayOpen) { + VSBoxCounter.closeAllButMe(self.instanceOffset); + } + }); + } + + this.drop.addEventListener("click", function (e) { + if (self.isDisabled) return; + if (e.target.tagName === 'INPUT') return; + let isShowHideCommand = e.target.tagName === 'SPAN'; + let isCheckCommand = e.target.tagName === 'I'; + let liClicked = e.target.parentElement; + if (!liClicked.hasAttribute("data-value")) { + if (liClicked.classList.contains("grouped-option")) { + if (!isShowHideCommand && !isCheckCommand) return; + let oldClass, newClass; + if (isCheckCommand) { // check or uncheck children + self.checkUncheckFromParent(liClicked); + } else { //open or close + if (liClicked.classList.contains("open")) { + oldClass = "open" + newClass = "closed" + } else { + oldClass = "closed" + newClass = "open" + } + liClicked.classList.remove(oldClass); + liClicked.classList.add(newClass); + let theChildren = self.drop.querySelectorAll("[data-parent='" + liClicked.id + "']"); + theChildren.forEach(function (x) { + x.classList.remove(oldClass); + x.classList.add(newClass); + }) + } + return; + } + } + let choiceValue = e.target.getAttribute("data-value"); + let choiceText = e.target.getAttribute("data-text"); + let className = e.target.getAttribute("class"); + + if (className && className.indexOf("disabled") != -1) { + return; + } + + if (className && className.indexOf("overflow") != -1) { + return; + } + + if (choiceValue === 'all') { + if (e.target.hasAttribute('data-selected') + && e.target.getAttribute('data-selected') === 'true') { + self.setValue('none') + } else { + self.setValue('all'); + } + return; + } + + if (!self.isMultiple) { + self.root.value = choiceValue; + self.title.textContent = choiceText; + if (className) { + self.title.setAttribute("class", className + " title"); + } else { + self.title.setAttribute("class", "title"); + } + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("active"); + }); + if (choiceText != "") { + e.target.classList.add("active"); + } + self.privateSendChange(); + if (!self.userOptions.stayOpen) { + docListener(); + } + } else { + let wasActive = false; + if (className) { + wasActive = className.indexOf("active") != -1; + } + if (wasActive) { + e.target.classList.remove("active"); + } else { + e.target.classList.add("active"); + } + if (e.target.hasAttribute("data-parent")) { + self.checkUncheckFromChild(e.target); + } + + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + for (let i = 0; i < self.options.length; i++) { + nrAll++; + if (self.options[i].value == choiceValue) { + self.options[i].selected = !wasActive; + } + if (self.options[i].selected) { + nrActives++; + selectedTexts += sep + self.options[i].textContent; + sep = ","; + } + } + if (nrAll == nrActives) { + let wordForAll = self.userOptions.translations.all || "all"; + selectedTexts = wordForAll; + } else if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = self.userOptions.translations.items || "items" + selectedTexts = nrActives + " " + wordForItems; + } + } + self.title.textContent = selectedTexts; + self.checkSelectMax(nrActives); + self.checkUncheckAll(); + self.privateSendChange(); + } + e.preventDefault(); + e.stopPropagation(); + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + }); + function docListener() { + document.removeEventListener("click", docListener); + self.drop.style.visibility = "hidden"; + if (self.search) { + self.inputBox.value = ""; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + x.classList.remove("hidden-search"); + }); + } + } + } + this.init(); + this.checkUncheckAll(); +} + +vanillaSelectBox.prototype.buildSelect = function (data) { + let self = this; + if(data == null || data.length < 1) return; + if(!self.isOptgroups){ + self.isOptgroups = data[0].parent != undefined && data[0].parent != ""; + } + + if(self.isOptgroups){ + let groups = {}; + data = data.filter(function(x){ + return x.parent != undefined && x.parent != ""; + }); + + data.forEach(function (x) { + if(!groups[x.parent]){ + groups[x.parent] = true; + } + + }); + for (let group in groups) { + let anOptgroup = document.createElement("optgroup"); + anOptgroup.setAttribute("label", group); + + options = data.filter(function(x){ + return x.parent == group; + }); + options.forEach(function (x) { + let anOption = document.createElement("option"); + anOption.value = x.value; + anOption.text = x.text; + if(x.selected){ + anOption.setAttribute("selected",true) + } + anOptgroup.appendChild(anOption); + }); + self.root.appendChild(anOptgroup); + } + }else{ + data.forEach(function (x) { + let anOption = document.createElement("option"); + anOption.value = x.value; + anOption.text = x.text; + if(x.selected){ + anOption.setAttribute("selected",true) + } + self.root.appendChild(anOption); + }); + } +} + +vanillaSelectBox.prototype.remoteSearchIntegrate = function (data) { + let self = this; + + if (data == null || data.length == 0) { + let dataChecked = self.optionsCheckedToData(); + if(dataChecked) + data = dataChecked.slice(0); + self.remoteSearchIntegrateIt(data); + } else { + let dataChecked = self.optionsCheckedToData(); + if (dataChecked.length > 0){ + for (var i = data.length - 1; i >= 0; i--) { + if(dataChecked.indexOf(data[i].id) !=-1){ + data.slice(i,1); + } + } + } + data = data.concat(dataChecked); + + self.remoteSearchIntegrateIt(data); + } +} + +vanillaSelectBox.prototype.optionsCheckedToData = function () { + let self = this; + let dataChecked = []; + let treeOptions = self.ul.querySelectorAll("li.active:not(.grouped-option)"); + let keepParents = {}; + if (treeOptions) { + Array.prototype.slice.call(treeOptions).forEach(function (x) { + let oneData = {"value":x.getAttribute("data-value"),"text":x.getAttribute("data-text"),"selected":true}; + if(oneData.value !== "all"){ + if(self.isOptgroups){ + let parentId = x.getAttribute("data-parent"); + if(keepParents[parentId]!=undefined){ + oneData.parent = keepParents[parentId]; + }else{ + let parentPtr = self.ul.querySelector("#"+parentId); + let parentName = parentPtr.getAttribute("data-text"); + keepParents[parentId] = parentName; + oneData.parent = parentName; + } + } + dataChecked.push(oneData); + } + + }); + } + return dataChecked; +} + +vanillaSelectBox.prototype.removeOptionsNotChecked = function (data) { + let self = this; + let minimumSize = self.onInitSize; + let newSearchSize = data == null ? 0 : data.length; + let presentSize = self.root.length; + if (presentSize + newSearchSize > minimumSize) { + let maxToRemove = presentSize + newSearchSize - minimumSize - 1; + let removed = 0; + for (var i = self.root.length - 1; i >= 0; i--) { + if (self.root.options[i].selected == false) { + if (removed <= maxToRemove) { + removed++; + self.root.remove(i); + } + } + } + } +} + +vanillaSelectBox.prototype.remoteSearchIntegrateIt = function (data) { + let self = this; + if (data == null || data.length == 0) return; + while(self.root.firstChild) + self.root.removeChild(self.root.firstChild); + + self.buildSelect(data); + self.reloadTree(); +} + +vanillaSelectBox.prototype.reloadTree = function () { + let self = this; + let lis = self.ul.querySelectorAll("li"); + if (lis != null) { + for (var i = lis.length - 1; i >= 0; i--) { + if (lis[i].getAttribute('data-value') !== 'all') { + self.ul.removeChild(lis[i]); + } + } + } + + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + + if (self.isOptgroups) { + if (document.querySelector(self.domSelector + ' optgroup') !== null) { + self.options = document.querySelectorAll(this.domSelector + " option"); + let groups = document.querySelectorAll(this.domSelector + ' optgroup'); + Array.prototype.slice.call(groups).forEach(function (group) { + let groupOptions = group.querySelectorAll('option'); + let li = document.createElement("li"); + let span = document.createElement("span"); + let iCheck = document.createElement("i"); + let labelElement = document.createElement("b"); + let dataWay = group.getAttribute("data-way"); + if (!dataWay) dataWay = "closed"; + if (!dataWay || (dataWay !== "closed" && dataWay !== "open")) dataWay = "closed"; + li.appendChild(span); + li.appendChild(iCheck); + self.ul.appendChild(li); + li.classList.add('grouped-option'); + li.classList.add(dataWay); + self.currentOptgroup++; + let optId = self.rootToken + "-opt-" + self.currentOptgroup; + li.id = optId; + li.appendChild(labelElement); + labelElement.appendChild(document.createTextNode(group.label)); + li.setAttribute("data-text", group.label); + self.ul.appendChild(li); + + Array.prototype.slice.call(groupOptions).forEach(function (x) { + let text = x.textContent; + let value = x.value; + let classes = x.getAttribute("class"); + if (classes) { + classes = classes.split(" "); + } + else { + classes = []; + } + classes.push(dataWay); + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + li.setAttribute("data-parent", optId); + if (classes.length != 0) { + classes.forEach(function (x) { + li.classList.add(x); + }); + } + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = ","; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + li.appendChild(document.createTextNode(text)); + }) + }) + } + self.listElements = this.drop.querySelectorAll("li:not(.grouped-option)"); + } else { + + self.options = self.root.querySelectorAll("option"); + Array.prototype.slice.call(self.options).forEach(function (x) { + let text = x.textContent; + let value = x.value; + if (value != "all") { + let originalAttrs; + if (x.hasAttributes()) { + originalAttrs = Array.prototype.slice.call(x.attributes) + .filter(function (a) { + return self.forbidenAttributes.indexOf(a.name) === -1 + }); + } + let classes = x.getAttribute("class"); + if (classes) { + classes = classes + .split(" ") + .filter(function (c) { + return self.forbidenClasses.indexOf(c) === -1 + }); + } else { + classes = []; + } + let li = document.createElement("li"); + let isSelected = x.hasAttribute("selected"); + + let isDisabled = x.disabled; + + self.ul.appendChild(li); + li.setAttribute("data-value", value); + li.setAttribute("data-text", text); + + if (originalAttrs !== undefined) { + originalAttrs.forEach(function (a) { + li.setAttribute(a.name, a.value); + }); + } + + classes.forEach(function (x) { + li.classList.add(x); + }); + + if (self.maxOptionWidth < Infinity) { + li.classList.add("short"); + li.style.maxWidth = self.maxOptionWidth + "px"; + } + + if (isSelected) { + nrActives++; + selectedTexts += sep + text; + sep = ","; + li.classList.add("active"); + if (!self.isMultiple) { + self.title.textContent = text; + if (classes.length != 0) { + classes.forEach(function (x) { + self.title.classList.add(x); + }); + } + } + } + if (isDisabled) { + li.classList.add("disabled"); + } + li.appendChild(document.createTextNode(" " + text)); + } + }); + } + +} + +vanillaSelectBox.prototype.disableItems = function (values) { + let self = this; + let foundValues = []; + if (vanillaSelectBox_type(values) == "string") { + values = values.split(","); + } + + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) != -1) { + foundValues.push(x.value); + x.setAttribute("disabled", ""); + } + }); + } + Array.prototype.slice.call(self.listElements).forEach(function (x) { + let val = x.getAttribute("data-value"); + if (foundValues.indexOf(val) != -1) { + x.classList.add("disabled"); + } + }); +} + +vanillaSelectBox.prototype.enableItems = function (values) { + let self = this; + let foundValues = []; + if (vanillaSelectBox_type(values) == "string") { + values = values.split(","); + } + + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) != -1) { + foundValues.push(x.value); + x.removeAttribute("disabled"); + } + }); + } + + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (foundValues.indexOf(x.getAttribute("data-value")) != -1) { + x.classList.remove("disabled"); + } + }); +} + +vanillaSelectBox.prototype.checkSelectMax = function (nrActives) { + let self = this; + if (self.maxSelect == Infinity || !self.isMultiple) return; + if (self.maxSelect <= nrActives) { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + if (!x.classList.contains('disabled') && !x.classList.contains('active')) { + x.classList.add("overflow"); + } + } + }); + } else { + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.classList.contains('overflow')) { + x.classList.remove("overflow"); + } + }); + } +} + +vanillaSelectBox.prototype.checkUncheckFromChild = function (liClicked) { + let self = this; + let parentId = liClicked.getAttribute('data-parent'); + let parentLi = document.getElementById(parentId); + if (!self.isMultiple) return; + let listElements = self.drop.querySelectorAll("li"); + let childrenElements = Array.prototype.slice.call(listElements).filter(function (el) { + return el.hasAttribute("data-parent") && el.getAttribute('data-parent') == parentId && !el.classList.contains('hidden-search') ; + }); + let nrChecked = 0; + let nrCheckable = childrenElements.length; + if (nrCheckable == 0) return; + childrenElements.forEach(function (el) { + if (el.classList.contains('active')) nrChecked++; + }); + if (nrChecked === nrCheckable || nrChecked === 0) { + if (nrChecked === 0) { + parentLi.classList.remove("checked"); + } else { + parentLi.classList.add("checked"); + } + } else { + parentLi.classList.remove("checked"); + } +} + +vanillaSelectBox.prototype.checkUncheckFromParent = function (liClicked) { + let self = this; + let parentId = liClicked.id; + if (!self.isMultiple) return; + let listElements = self.drop.querySelectorAll("li"); + let childrenElements = Array.prototype.slice.call(listElements).filter(function (el) { + return el.hasAttribute("data-parent") && el.getAttribute('data-parent') == parentId && !el.classList.contains('hidden-search'); + }); + let nrChecked = 0; + let nrCheckable = childrenElements.length; + if (nrCheckable == 0) return; + childrenElements.forEach(function (el) { + if (el.classList.contains('active')) nrChecked++; + }); + if (nrChecked === nrCheckable || nrChecked === 0) { + //check all or uncheckAll : just do the opposite + childrenElements.forEach(function (el) { + var event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + el.dispatchEvent(event); + }); + if (nrChecked === 0) { + liClicked.classList.add("checked"); + } else { + liClicked.classList.remove("checked"); + } + } else { + //check all + liClicked.classList.remove("checked"); + childrenElements.forEach(function (el) { + if (!el.classList.contains('active')) { + var event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, false); + el.dispatchEvent(event); + } + }); + } +} + +vanillaSelectBox.prototype.checkUncheckAll = function () { + let self = this; + if (!self.isMultiple) return; + let nrChecked = 0; + let nrCheckable = 0; + let checkAllElement = null; + if (self.listElements == null) return; + Array.prototype.slice.call(self.listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + if (x.getAttribute('data-value') === 'all') { + checkAllElement = x; + } + if (x.getAttribute('data-value') !== 'all' + && !x.classList.contains('hidden-search') + && !x.classList.contains('disabled')) { + nrCheckable++; + nrChecked += x.classList.contains('active'); + } + } + }); + + if (checkAllElement) { + if (nrChecked === nrCheckable) { + // check the checkAll checkbox + checkAllElement.classList.add("active"); + checkAllElement.innerText = self.userOptions.translations.clearAll; + checkAllElement.setAttribute('data-selected', 'true') + } else if (nrChecked === 0) { + // uncheck the checkAll checkbox + self.title.textContent = self.userOptions.placeHolder; + checkAllElement.classList.remove("active"); + checkAllElement.innerText = self.userOptions.translations.selectAll; + checkAllElement.setAttribute('data-selected', 'false') + } + } +} + +vanillaSelectBox.prototype.setValue = function (values) { + let self = this; + let listElements = self.drop.querySelectorAll("li"); + + if (values == null || values == undefined || values == "") { + self.empty(); + } else { + if (self.isMultiple) { + if (vanillaSelectBox_type(values) == "string") { + if (values === "all") { + values = []; + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + let value = x.getAttribute('data-value'); + if (value !== 'all') { + if (!x.classList.contains('hidden-search') && !x.classList.contains('disabled')) { + values.push(x.getAttribute('data-value')); + } + // already checked (but hidden by search) + if (x.classList.contains('active')) { + if (x.classList.contains('hidden-search') || x.classList.contains('disabled')) { + values.push(value); + } + } + } + } else if (x.classList.contains('grouped-option')) { + x.classList.add("checked"); + } + }); + } else if (values === "none") { + values = []; + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.hasAttribute('data-value')) { + let value = x.getAttribute('data-value'); + if (value !== 'all') { + if (x.classList.contains('active')) { + if (x.classList.contains('hidden-search') || x.classList.contains('disabled')) { + values.push(value); + } + } + } + } else if (x.classList.contains('grouped-option')) { + x.classList.remove("checked"); + } + }); + } else { + values = values.split(","); + } + } + let foundValues = []; + if (vanillaSelectBox_type(values) == "array") { + Array.prototype.slice.call(self.options).forEach(function (x) { + if (values.indexOf(x.value) !== -1) { + x.selected = true; + foundValues.push(x.value); + } else { + x.selected = false; + } + }); + let selectedTexts = "" + let sep = ""; + let nrActives = 0; + let nrAll = 0; + Array.prototype.slice.call(listElements).forEach(function (x) { + nrAll++; + if (foundValues.indexOf(x.getAttribute("data-value")) != -1) { + x.classList.add("active"); + nrActives++; + selectedTexts += sep + x.getAttribute("data-text"); + sep = ","; + } else { + x.classList.remove("active"); + } + }); + if (nrAll == nrActives) { + let wordForAll = self.userOptions.translations.all || "all"; + selectedTexts = wordForAll; + } else if (self.multipleSize != -1) { + if (nrActives > self.multipleSize) { + let wordForItems = self.userOptions.translations.items || "items" + selectedTexts = nrActives + " " + wordForItems; + } + } + self.title.textContent = selectedTexts; + self.privateSendChange(); + } + self.checkUncheckAll(); + } else { + let found = false; + let text = ""; + let classNames = "" + Array.prototype.slice.call(listElements).forEach(function (x) { + if (x.getAttribute("data-value") == values) { + x.classList.add("active"); + found = true; + text = x.getAttribute("data-text") + } else { + x.classList.remove("active"); + } + }); + Array.prototype.slice.call(self.options).forEach(function (x) { + if (x.value == values) { + x.selected = true; + className = x.getAttribute("class"); + if (!className) className = ""; + } else { + x.selected = false; + } + }); + if (found) { + self.title.textContent = text; + if (self.userOptions.placeHolder != "" && self.title.textContent == "") { + self.title.textContent = self.userOptions.placeHolder; + } + if (className != "") { + self.title.setAttribute("class", className + " title"); + } else { + self.title.setAttribute("class", "title"); + } + } + } + } +} + +vanillaSelectBox.prototype.privateSendChange = function () { + let event = document.createEvent('HTMLEvents'); + event.initEvent('change', true, false); + this.root.dispatchEvent(event); +} + +vanillaSelectBox.prototype.empty = function () { + Array.prototype.slice.call(this.listElements).forEach(function (x) { + x.classList.remove("active"); + }); + Array.prototype.slice.call(this.options).forEach(function (x) { + x.selected = false; + }); + this.title.textContent = ""; + if (this.userOptions.placeHolder != "" && this.title.textContent == "") { + this.title.textContent = this.userOptions.placeHolder; + } + this.checkUncheckAll(); + this.privateSendChange(); +} + +vanillaSelectBox.prototype.destroy = function () { + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + VSBoxCounter.remove(this.instanceOffset); + already.remove(); + this.root.style.display = "inline-block"; + } +} +vanillaSelectBox.prototype.disable = function () { + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + button = already.querySelector("button") + if (button) button.classList.add("disabled"); + this.isDisabled = true; + } +} +vanillaSelectBox.prototype.enable = function () { + let already = document.getElementById("btn-group-" + this.rootToken); + if (already) { + button = already.querySelector("button") + if (button) button.classList.remove("disabled"); + this.isDisabled = false; + } +} + +vanillaSelectBox.prototype.showOptions = function () { + console.log(this.userOptions); +} +// Polyfills for IE +if (!('remove' in Element.prototype)) { + Element.prototype.remove = function () { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; +} + +function vanillaSelectBox_type(target) { + const computedType = Object.prototype.toString.call(target); + const stripped = computedType.replace("[object ", "").replace("]", ""); + const lowercased = stripped.toLowerCase(); + return lowercased; +} + +export { vanillaSelectBox }