/** * This file handles the scripting for the Reflectance Calculator. * There are 3 parts to it: * 1 - The sidebar filters * 2 - The layers table * 3 - The ZingChart chart * * Values from the sidebar and layers are converted into url search-params format (key/value - `?foo=bar&baz=biz`) * and sent via a Fetch GET request to the server API endpoint where: * 1 - It returns the data as series values for rendering in the chart (if the 'Plot' button pressed) * 2 - It returns the data as a .txt file for download (if the 'Download' button pressed) * * When a user successfully adds a filter/layer combination that returns data, we store that config information * in session storage. If the user refreshes the page or comes back to it, the filter/layers will start * with that data instead of the default values. * * Please see the `init()` function to get started. */ // CONFIG // ----------------------------- // Elements let root = document.querySelector('reflectance-calculator'); let buttons = { parent: document.querySelector('[refcalc-buttons]'), // Buttons stored after load - 'Mobile' buttons cloned from matching base buttons in DOM download: null, downloadMobile: null, plot: null, plotMobile: null, }; let chart = { render: document.querySelector('refcalc-chart-render'), plotting: document.querySelector('refcalc-layer-plotting'), error: document.querySelector('refcalc-layer-error'), } let filters = { angle: document.querySelector('[name="plot-angle"]'), end: document.querySelector('[name="plot-end"]'), polarization: document.querySelectorAll('[name="plot-polarization"]'), spacing: document.querySelector('[name="plot-spacing"]'), start: document.querySelector('[name="plot-start"]'), type: document.querySelectorAll('[name="plot-type"]'), }; let layer = { parent: document.querySelector('refcalc-layer'), medium: document.querySelector('[data-calc="medium"]'), newLayers: document.querySelector('[data-calc="new-layers"]'), thickness: document.querySelector('[data-calc="thickness"]'), substrate: document.querySelector('[data-calc="substrate"]'), }; let reflectanceIndex = { dialog: document.querySelector('[data-calc="reflectance-index-dialog"]'), input: document.querySelector('[data-calc="reflectance-index-input"]'), no: document.querySelector('[data-calc="reflectance-index-no"]'), yes: document.querySelector('[data-calc="reflectance-index-yes"]'), }; // API Endpoints let plotApiUrl = '/wp-json/reflectance-calculator/query'; let downloadApiUrl = '/wp-json/reflectance-calculator/file'; // Config let materialRefractiveIndexLabel = 'Enter Refractive Index...'; let materialTypes = [materialRefractiveIndexLabel,'Acrylic','Ag','Air','Al','Al10Ga90As','Al20Ga80As','Al2O3','Al32Ga68As','Al42Ga58As','Al49Ga51As','Al59Ga41As','Al70Ga30As','Al80Ga20As','Al90Ga10As','AlAs','AlCu','AlN','AlSb','Au','BK7','CaF2','CdTe','Cellulose','Co','CoSi2','Cr','Cu','GaAs','GaN','GaP','GaSb','Ge','HfO2','InAs','InP','InSb','Insulator','ITO','KCl','MgF2','MgO','Mo','Nb','Ni','PbS','PbSe','PET','Polyacrylate','Polyethylene','Pt','Quartz','Rh','Si','Si3N4','SiO','SiO2','Styrene','Styrene','Ta','Ta2O5','Ti','TiN','TiO2','TiSi2','W','ZrO2',]; let materialOptions = materialTypes.map(type => ``); let thicknessUOMs = { A: { multiplier: 10000, symbol: 'Å' }, nm: { multiplier: 1000, symbol: 'nm', default: true }, um: { multiplier: 1, symbol: 'µm' }, kA: { multiplier: 10, symbol: 'kÅ' }, uin: { multiplier: 39.3701, symbol: 'µin' }, mils: { multiplier: 0.0393701, symbol: 'mils' }, mm: { multiplier: 0.001, symbol: 'mm' }, 'in': { multiplier: 0.0000393701, symbol: 'in' } } // Layer let mediumStart = 'Air'; let substrateStart = 'Si'; let materialTypeDropdownIndex = 'data-index'; // Session Storage let sessionStorageVariable = 'reflectance-calculator-filters'; // Chart let chartId = 'reflectance-calculator-chart'; let chartColors = { black: '#0c0c12', darkGrey: '#b6b6b7', lightGrey: '#e7e3e8', lightPurple: '#fcf6fd', purple: '#aa1dd5', }; let chartRenderShared = { width: '100%', height: '100%', output: 'canvas', } // INIT // ----------------------------- async function init() { // Add init attribute to assist JS-enabled styling initForStyling(); // Init the starting values in the plot sidebar initPlotFilters(); // Init the starting items for the layers table initLayers(); // Clone 'Plot' and 'Download' buttons for mobile initButtonClones(); // Handle 'Plot' Button Events await initPlotButtons(); // Handle 'Download' Button Events await initDownloadButtons(); // Init chart section await initChart(); } await init(); // Comment out if imported. See export below. // --- /** * Add init attribute to assist JS-enabled styling (show the calc differently if JS enabled or not) */ function initForStyling() { root?.setAttribute('init', ''); } /** * Init the starting values in the filters sidebar. If the user previously used a set of filters that returned data, * those filter settings were saved in session storage and we'll use them if the page is refreshed or reloaded. * If not, then we'll populate the filter items with a predetermined set of filter values. * @returns none */ function initPlotFilters() { let {angle, end, polarization, spacing, start, type} = filters; // If url has `?key=value&key=value` filters use them (urlFilters) // If not, but user previous had a successful chart render, use the filters from session storage (sessionFilters) // Finally, if the user's first page load of the session, populate the filters/layers/chart with starting, default data let urlFilters = getFiltersFromUrl(); let sessionFilters = getFiltersFromSessionStorage(); let userFilters = urlFilters && urlFilters.size ? urlFilters : sessionFilters; // Use previous user choice if matching session data found if (userFilters) { // Input angle.value = userFilters.get('angle'); end.value = userFilters.get('wmax'); spacing.value = userFilters.get('wstep'); start.value = userFilters.get('wmin'); // Radio let matchingPolarizationRadio = getMatchingRadio(polarization, userFilters.get('pol')); if (matchingPolarizationRadio) matchingPolarizationRadio.checked = true; let matchingTypeRadio = getMatchingRadio(type, userFilters.get('sptype')); if (matchingTypeRadio) matchingTypeRadio.checked = true; } // Defaults, no user session values found else { // Input angle.value = 0; end.value = 1000; spacing.value = 1; start.value = 200; // Radio polarization[0].checked = true; type[0].checked = true; } } /** * Init the layer section's table items */ function initLayers() { let {medium, thickness, substrate} = layer; // Init 'Reflective Index' Overlay (dialog) // --- // This is shown when selecting 'Enter Refractive Index...' choice from a 'Material Type' layer dropdown function handleLayerMaterialDropdownChange(e) { let isRefractiveIndexOption = e.target.value === materialRefractiveIndexLabel; if (!isRefractiveIndexOption || !reflectanceIndex.dialog) return; // When showing the overlay, if there was a previously-added value, set it as the input's value before showing // Otherwise, make sure the input value is cleared out. let selectedOptionValue = Array.from( e.target.querySelectorAll('option') ).filter(option => option.selected)[0].value; let setInputValue = selectedOptionValue > 0 ? selectedOptionValue : 0; if (reflectanceIndex.input) reflectanceIndex.input.value = setInputValue; // Add the 'data-index="X"' value to the dialog so it can find which dropdown launched it reflectanceIndex.dialog.setAttribute(materialTypeDropdownIndex, e.target.getAttribute(materialTypeDropdownIndex)); // Show overlay to enter value reflectanceIndex.dialog.showModal(); // Focus the input field reflectanceIndex.input.focus(); } // Add 'Cancel' overlay button click event reflectanceIndex.no?.addEventListener('click', e => { reflectanceIndex.dialog?.close() }); // Add 'Ok' overlay button click event reflectanceIndex.yes?.addEventListener('click', e => { // Get the calling 'Material Type' select dropdown by reading the `data-index="X"` value from the dialog. The matching dropdown will have the same attribute/value let callingDropdown = document.querySelector(`select[${materialTypeDropdownIndex}="${reflectanceIndex.dialog.getAttribute(materialTypeDropdownIndex)}"]`); // Add the value from the overlay's input as a new option (selected) in the calling ${ materialDropdownOptionsWithStartingOption.join('') }`; thicknessCell.innerHTML = ``; insertTarget.insertAdjacentElement(insertLocation, rowClone); // Add 'Material' dropdown change event (will need to remove this listener when row is removed to avoid memory leak) let materialSelect = materialCell.querySelector('select'); if (materialSelect) materialSelect.addEventListener('change', handleLayerMaterialDropdownChange); // Renumber the 'Layer Number' column let renumberRefEl = insertTarget.getAttribute('data-calc') === 'new-layers' ? insertTarget : insertTarget.closest('[data-calc="new-layers"]'); let rows = renumberRefEl.querySelectorAll('tr'); rows?.forEach((row, index) => setSequentialLayerNumber(row, index)); } // 'Add' buttons added in more than one location so we want to keep button markup consistent function addButtonMarkup() { return ``; } // Set sequential number to target element function setSequentialLayerNumber(el, index) { if (!el || index < 0) return; let layerNumber = el.querySelector('span') let materialTypeDropdown = el.querySelector('select') layerNumber.textContent = index + 1; // Update 'Layer Number' column text materialTypeDropdown.setAttribute(materialTypeDropdownIndex, index+2); // Update '