224 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			224 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
								 | 
							
								let deletedOptions = {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								window.addEventListener('load', () => {
							 | 
						||
| 
								 | 
							
								  const worldName = document.querySelector('#weighted-options').getAttribute('data-game');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Generic change listener. Detecting unique qualities and acting on them here reduces initial JS initialisation time
							 | 
						||
| 
								 | 
							
								  // and handles dynamically created elements
							 | 
						||
| 
								 | 
							
								  document.addEventListener('change', (evt) => {
							 | 
						||
| 
								 | 
							
								    // Handle updates to range inputs
							 | 
						||
| 
								 | 
							
								    if (evt.target.type === 'range') {
							 | 
						||
| 
								 | 
							
								      // Update span containing range value. All ranges have a corresponding `{rangeId}-value` span
							 | 
						||
| 
								 | 
							
								      document.getElementById(`${evt.target.id}-value`).innerText = evt.target.value;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // If the changed option was the name of a game, determine whether to show or hide that game's div
							 | 
						||
| 
								 | 
							
								      if (evt.target.id.startsWith('game||')) {
							 | 
						||
| 
								 | 
							
								        const gameName = evt.target.id.split('||')[1];
							 | 
						||
| 
								 | 
							
								        const gameDiv = document.getElementById(`${gameName}-container`);
							 | 
						||
| 
								 | 
							
								        if (evt.target.value > 0) {
							 | 
						||
| 
								 | 
							
								          gameDiv.classList.remove('hidden');
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          gameDiv.classList.add('hidden');
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Generic click listener
							 | 
						||
| 
								 | 
							
								  document.addEventListener('click', (evt) => {
							 | 
						||
| 
								 | 
							
								    // Handle creating new rows for Range options
							 | 
						||
| 
								 | 
							
								    if (evt.target.classList.contains('add-range-option-button')) {
							 | 
						||
| 
								 | 
							
								      const optionName = evt.target.getAttribute('data-option');
							 | 
						||
| 
								 | 
							
								      addRangeRow(optionName);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // Handle deleting range rows
							 | 
						||
| 
								 | 
							
								    if (evt.target.classList.contains('range-option-delete')) {
							 | 
						||
| 
								 | 
							
								      const targetRow = document.querySelector(`tr[data-row="${evt.target.getAttribute('data-target')}"]`);
							 | 
						||
| 
								 | 
							
								      setDeletedOption(
							 | 
						||
| 
								 | 
							
								        targetRow.getAttribute('data-option-name'),
							 | 
						||
| 
								 | 
							
								        targetRow.getAttribute('data-value'),
							 | 
						||
| 
								 | 
							
								      );
							 | 
						||
| 
								 | 
							
								      targetRow.parentElement.removeChild(targetRow);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Listen for enter presses on inputs intended to add range rows
							 | 
						||
| 
								 | 
							
								  document.addEventListener('keydown', (evt) => {
							 | 
						||
| 
								 | 
							
								    if (evt.key === 'Enter') {
							 | 
						||
| 
								 | 
							
								      evt.preventDefault();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (evt.key === 'Enter' && evt.target.classList.contains('range-option-value')) {
							 | 
						||
| 
								 | 
							
								      const optionName = evt.target.getAttribute('data-option');
							 | 
						||
| 
								 | 
							
								      addRangeRow(optionName);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Detect form submission
							 | 
						||
| 
								 | 
							
								  document.getElementById('weighted-options-form').addEventListener('submit', (evt) => {
							 | 
						||
| 
								 | 
							
								    // Save data to localStorage
							 | 
						||
| 
								 | 
							
								    const weightedOptions = {};
							 | 
						||
| 
								 | 
							
								    document.querySelectorAll('input[name]').forEach((input) => {
							 | 
						||
| 
								 | 
							
								      const keys = input.getAttribute('name').split('||');
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Determine keys
							 | 
						||
| 
								 | 
							
								      const optionName = keys[0] ?? null;
							 | 
						||
| 
								 | 
							
								      const subOption = keys[1] ?? null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Ensure keys exist
							 | 
						||
| 
								 | 
							
								      if (!weightedOptions[optionName]) { weightedOptions[optionName] = {}; }
							 | 
						||
| 
								 | 
							
								      if (subOption && !weightedOptions[optionName][subOption]) {
							 | 
						||
| 
								 | 
							
								        weightedOptions[optionName][subOption] = null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (subOption) { return weightedOptions[optionName][subOption] = determineValue(input); }
							 | 
						||
| 
								 | 
							
								      if (optionName) { return weightedOptions[optionName] = determineValue(input); }
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    localStorage.setItem(`${worldName}-weights`, JSON.stringify(weightedOptions));
							 | 
						||
| 
								 | 
							
								    localStorage.setItem(`${worldName}-deletedOptions`, JSON.stringify(deletedOptions));
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Remove all deleted values as specified by localStorage
							 | 
						||
| 
								 | 
							
								  deletedOptions = JSON.parse(localStorage.getItem(`${worldName}-deletedOptions`) || '{}');
							 | 
						||
| 
								 | 
							
								  Object.keys(deletedOptions).forEach((optionName) => {
							 | 
						||
| 
								 | 
							
								    deletedOptions[optionName].forEach((value) => {
							 | 
						||
| 
								 | 
							
								      const targetRow = document.querySelector(`tr[data-row="${value}-row"]`);
							 | 
						||
| 
								 | 
							
								      targetRow.parentElement.removeChild(targetRow);
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Populate all settings from localStorage on page initialisation
							 | 
						||
| 
								 | 
							
								  const previousSettingsJson = localStorage.getItem(`${worldName}-weights`);
							 | 
						||
| 
								 | 
							
								  if (previousSettingsJson) {
							 | 
						||
| 
								 | 
							
								    const previousSettings = JSON.parse(previousSettingsJson);
							 | 
						||
| 
								 | 
							
								    Object.keys(previousSettings).forEach((option) => {
							 | 
						||
| 
								 | 
							
								      if (typeof previousSettings[option] === 'string') {
							 | 
						||
| 
								 | 
							
								        return document.querySelector(`input[name="${option}"]`).value = previousSettings[option];
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      Object.keys(previousSettings[option]).forEach((value) => {
							 | 
						||
| 
								 | 
							
								        const input = document.querySelector(`input[name="${option}||${value}"]`);
							 | 
						||
| 
								 | 
							
								        if (!input?.type) {
							 | 
						||
| 
								 | 
							
								          return console.error(`Unable to populate option with name ${option}||${value}.`);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        switch (input.type) {
							 | 
						||
| 
								 | 
							
								          case 'checkbox':
							 | 
						||
| 
								 | 
							
								            input.checked = (parseInt(previousSettings[option][value], 10) === 1);
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								          case 'range':
							 | 
						||
| 
								 | 
							
								            input.value = parseInt(previousSettings[option][value], 10);
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								          case 'number':
							 | 
						||
| 
								 | 
							
								            input.value = previousSettings[option][value].toString();
							 | 
						||
| 
								 | 
							
								            break;
							 | 
						||
| 
								 | 
							
								          default:
							 | 
						||
| 
								 | 
							
								            console.error(`Found unsupported input type: ${input.type}`);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								    });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const addRangeRow = (optionName) => {
							 | 
						||
| 
								 | 
							
								  const inputQuery = `input[type=number][data-option="${optionName}"].range-option-value`;
							 | 
						||
| 
								 | 
							
								  const inputTarget = document.querySelector(inputQuery);
							 | 
						||
| 
								 | 
							
								  const newValue = inputTarget.value;
							 | 
						||
| 
								 | 
							
								  if (!/^-?\d+$/.test(newValue)) {
							 | 
						||
| 
								 | 
							
								    alert('Range values must be a positive or negative integer!');
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  inputTarget.value = '';
							 | 
						||
| 
								 | 
							
								  const tBody = document.querySelector(`table[data-option="${optionName}"].range-rows tbody`);
							 | 
						||
| 
								 | 
							
								  const tr = document.createElement('tr');
							 | 
						||
| 
								 | 
							
								  tr.setAttribute('data-row', `${optionName}-${newValue}-row`);
							 | 
						||
| 
								 | 
							
								  tr.setAttribute('data-option-name', optionName);
							 | 
						||
| 
								 | 
							
								  tr.setAttribute('data-value', newValue);
							 | 
						||
| 
								 | 
							
								  const tdLeft = document.createElement('td');
							 | 
						||
| 
								 | 
							
								  tdLeft.classList.add('td-left');
							 | 
						||
| 
								 | 
							
								  const label = document.createElement('label');
							 | 
						||
| 
								 | 
							
								  label.setAttribute('for', `${optionName}||${newValue}`);
							 | 
						||
| 
								 | 
							
								  label.innerText = newValue.toString();
							 | 
						||
| 
								 | 
							
								  tdLeft.appendChild(label);
							 | 
						||
| 
								 | 
							
								  tr.appendChild(tdLeft);
							 | 
						||
| 
								 | 
							
								  const tdMiddle = document.createElement('td');
							 | 
						||
| 
								 | 
							
								  tdMiddle.classList.add('td-middle');
							 | 
						||
| 
								 | 
							
								  const range = document.createElement('input');
							 | 
						||
| 
								 | 
							
								  range.setAttribute('type', 'range');
							 | 
						||
| 
								 | 
							
								  range.setAttribute('min', '0');
							 | 
						||
| 
								 | 
							
								  range.setAttribute('max', '50');
							 | 
						||
| 
								 | 
							
								  range.setAttribute('value', '0');
							 | 
						||
| 
								 | 
							
								  range.setAttribute('id', `${optionName}||${newValue}`);
							 | 
						||
| 
								 | 
							
								  range.setAttribute('name', `${optionName}||${newValue}`);
							 | 
						||
| 
								 | 
							
								  tdMiddle.appendChild(range);
							 | 
						||
| 
								 | 
							
								  tr.appendChild(tdMiddle);
							 | 
						||
| 
								 | 
							
								  const tdRight = document.createElement('td');
							 | 
						||
| 
								 | 
							
								  tdRight.classList.add('td-right');
							 | 
						||
| 
								 | 
							
								  const valueSpan = document.createElement('span');
							 | 
						||
| 
								 | 
							
								  valueSpan.setAttribute('id', `${optionName}||${newValue}-value`);
							 | 
						||
| 
								 | 
							
								  valueSpan.innerText = '0';
							 | 
						||
| 
								 | 
							
								  tdRight.appendChild(valueSpan);
							 | 
						||
| 
								 | 
							
								  tr.appendChild(tdRight);
							 | 
						||
| 
								 | 
							
								  const tdDelete = document.createElement('td');
							 | 
						||
| 
								 | 
							
								  const deleteSpan = document.createElement('span');
							 | 
						||
| 
								 | 
							
								  deleteSpan.classList.add('range-option-delete');
							 | 
						||
| 
								 | 
							
								  deleteSpan.classList.add('js-required');
							 | 
						||
| 
								 | 
							
								  deleteSpan.setAttribute('data-target', `${optionName}-${newValue}-row`);
							 | 
						||
| 
								 | 
							
								  deleteSpan.innerText = '❌';
							 | 
						||
| 
								 | 
							
								  tdDelete.appendChild(deleteSpan);
							 | 
						||
| 
								 | 
							
								  tr.appendChild(tdDelete);
							 | 
						||
| 
								 | 
							
								  tBody.appendChild(tr);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Remove this option from the set of deleted options if it exists
							 | 
						||
| 
								 | 
							
								  unsetDeletedOption(optionName, newValue);
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Determines the value of an input element, or returns a 1 or 0 if the element is a checkbox
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * @param {object} input - The input element.
							 | 
						||
| 
								 | 
							
								 * @returns {number} The value of the input element.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								const determineValue = (input) => {
							 | 
						||
| 
								 | 
							
								  switch (input.type) {
							 | 
						||
| 
								 | 
							
								    case 'checkbox':
							 | 
						||
| 
								 | 
							
								      return (input.checked ? 1 : 0);
							 | 
						||
| 
								 | 
							
								    case 'range':
							 | 
						||
| 
								 | 
							
								      return parseInt(input.value, 10);
							 | 
						||
| 
								 | 
							
								    default:
							 | 
						||
| 
								 | 
							
								      return input.value;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Sets the deleted option value for a given world and option name.
							 | 
						||
| 
								 | 
							
								 * If the world or option does not exist, it creates the necessary entries.
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * @param {string} optionName - The name of the option.
							 | 
						||
| 
								 | 
							
								 * @param {*} value - The value to be set for the deleted option.
							 | 
						||
| 
								 | 
							
								 * @returns {void}
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								const setDeletedOption = (optionName, value) => {
							 | 
						||
| 
								 | 
							
								  deletedOptions[optionName] = deletedOptions[optionName] || [];
							 | 
						||
| 
								 | 
							
								  deletedOptions[optionName].push(`${optionName}-${value}`);
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Removes a specific value from the deletedOptions object.
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * @param {string} optionName - The name of the option.
							 | 
						||
| 
								 | 
							
								 * @param {*} value - The value to be removed
							 | 
						||
| 
								 | 
							
								 * @returns {void}
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								const unsetDeletedOption = (optionName, value) => {
							 | 
						||
| 
								 | 
							
								  if (!deletedOptions.hasOwnProperty(optionName)) { return; }
							 | 
						||
| 
								 | 
							
								  if (deletedOptions[optionName].includes(`${optionName}-${value}`)) {
							 | 
						||
| 
								 | 
							
								    deletedOptions[optionName].splice(deletedOptions[optionName].indexOf(`${optionName}-${value}`), 1);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  if (deletedOptions[optionName].length === 0) {
							 | 
						||
| 
								 | 
							
								    delete deletedOptions[optionName];
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								};
							 |