| 
									
										
											  
											
												WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.
* Remove debug output
* In-progress conversion of player-options to Jinja rendering
* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors
* Un-alphabetize options, add default group name for item/location Option classes, implement more option types
* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options
* Support all remaining option types
* Rendering improvements and CSS fixes for prettiness
* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS
* Minor CSS tweaks, as recommended by the designer
* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.
* Fix labels, add JS handling for TextChoice
* Make option groups collapsable
* PEP8 current option_groups progress (#2604)
* Make the python more PEP8 and remove unneeded imports
* remove LocationSet from `Item & Location Options` group
* It's ugly, but YAML generation is working
* Stop generating JSON files for player-options pages
* Do not include ItemDict entries whose values are zero
* Properly format yaml output
* Save options when form is submitted, load options on page load
* Fix options being omitted from the page if a group has an even number of options
* Implement generate-game, escape option descriptions
* Fix "randomize" checkboxes not properly setting YAML options to "random"
* Add a separator between item/location groups and items/locations in their respective lists
* Implement option presets
* Fix docs to detail what actually ended up happening
* implement option groups on webworld to allow dev sorting (#2616)
* Force extremely long item/location/option names with no spaces to text-wrap
* Fix "randomize" button being too wide in single-column display, change page header to include game name
* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name
* Un-break weighted-options, add option group names to weighted options
* Nuke weighted-options. Set up framework to rebuild it in Jinja.
* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds
* Support Toggle, DefaultOnToggle, and Choice options in weighted-options
* Implement expand/collapse without JS for worlds and option groups
* Properly style set options
* Implement Range and NamedRange. Also, CSS is hard.
* Add support for remaining option types. JS and backend still forthcoming.
* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.
* Support showing/hiding game divs based on range value for game
* Add support for adding/deleting range rows
* Save settings to localStorage on form submission
* Save deleted options on form submission
* Break weighted-options into a per-game page.
- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options
* Implement loading previous settings from localStorage on page load if JS is enabled
* Weighted options can now generate YAML files and single-player games
* options pages now respect option visibility settings for simple and complex pages
* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page
* Fix instance of AutoWorld not having access to proper `random`
* Catch instances of frozenset along with set
* Restore word-wrap in tooltips
* Fix word wrap in player-options labels
* Add `dedent` filter to help with formatting tooltips in player-options
* Do not change the ordering of keys when printing yaml files
* Move necessary import out of conditional statement
* Expand only the first option group by default on both options pages
* Respect option visibility when generating yaml template files
* Swap to double quotes
* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links
* Strip newlines and spaces after applying dedent filter
* Fix documentation for option groups
* Update site map
* Update various docs
* Sort OptionSet lists alphabetically
* Minor style tweak
* Fix extremely long text overflowing tooltips
* Convert player-options to use CSS grid instead of tables
* Do not display link to weighted-options page on supported games if the options page is an external link
* Update worlds/AutoWorld.py
Bugfix by @alwaysintreble
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
* Fix NamedRange options not being properly set if a preset it loaded
* Move option-presets route into options.py
* Include preset name in YAML if not "default" and not "custom"
* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes
* Fix not disabling custom inputs when the randomize button is clicked
* Only sort OptionList and OptionSet valid_keys if they are unordered
* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column
* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold
* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs
---------
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
											
										 
											2024-05-18 00:11:57 -04:00
										 |  |  | let presets = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | window.addEventListener('load', async () => { | 
					
						
							|  |  |  |   // Load settings from localStorage, if available
 | 
					
						
							|  |  |  |   loadSettings(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Fetch presets if available
 | 
					
						
							|  |  |  |   await fetchPresets(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to range inputs
 | 
					
						
							|  |  |  |   document.querySelectorAll('input[type=range]').forEach((range) => { | 
					
						
							|  |  |  |     const optionName = range.getAttribute('id'); | 
					
						
							|  |  |  |     range.addEventListener('change', () => { | 
					
						
							|  |  |  |       document.getElementById(`${optionName}-value`).innerText = range.value; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Handle updating named range selects to "custom" if appropriate
 | 
					
						
							|  |  |  |       const select = document.querySelector(`select[data-option-name=${optionName}]`); | 
					
						
							|  |  |  |       if (select) { | 
					
						
							|  |  |  |         let updated = false; | 
					
						
							|  |  |  |         select?.childNodes.forEach((option) => { | 
					
						
							|  |  |  |           if (option.value === range.value) { | 
					
						
							|  |  |  |             select.value = range.value; | 
					
						
							|  |  |  |             updated = true; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         if (!updated) { | 
					
						
							|  |  |  |           select.value = 'custom'; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to named range selects
 | 
					
						
							|  |  |  |   document.querySelectorAll('.named-range-container select').forEach((select) => { | 
					
						
							|  |  |  |     const optionName = select.getAttribute('data-option-name'); | 
					
						
							|  |  |  |     select.addEventListener('change', (evt) => { | 
					
						
							|  |  |  |       document.getElementById(optionName).value = evt.target.value; | 
					
						
							|  |  |  |       document.getElementById(`${optionName}-value`).innerText = evt.target.value; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to randomize checkboxes
 | 
					
						
							|  |  |  |   document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => { | 
					
						
							|  |  |  |     const optionName = checkbox.getAttribute('data-option-name'); | 
					
						
							|  |  |  |     checkbox.addEventListener('change', () => { | 
					
						
							|  |  |  |       const optionInput = document.getElementById(optionName); | 
					
						
							|  |  |  |       const namedRangeSelect = document.querySelector(`select[data-option-name=${optionName}]`); | 
					
						
							|  |  |  |       const customInput = document.getElementById(`${optionName}-custom`); | 
					
						
							|  |  |  |       if (checkbox.checked) { | 
					
						
							|  |  |  |         optionInput.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |         namedRangeSelect?.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |         if (customInput) { | 
					
						
							|  |  |  |           customInput.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         optionInput.removeAttribute('disabled'); | 
					
						
							|  |  |  |         namedRangeSelect?.removeAttribute('disabled'); | 
					
						
							|  |  |  |         if (customInput) { | 
					
						
							|  |  |  |           customInput.removeAttribute('disabled'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to TextChoice input[type=text]
 | 
					
						
							|  |  |  |   document.querySelectorAll('.text-choice-container input[type=text]').forEach((input) => { | 
					
						
							|  |  |  |     const optionName = input.getAttribute('data-option-name'); | 
					
						
							|  |  |  |     input.addEventListener('input', () => { | 
					
						
							|  |  |  |       const select = document.getElementById(optionName); | 
					
						
							|  |  |  |       const optionValues = []; | 
					
						
							|  |  |  |       select.childNodes.forEach((option) => optionValues.push(option.value)); | 
					
						
							|  |  |  |       select.value = (optionValues.includes(input.value)) ? input.value : 'custom'; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to TextChoice select
 | 
					
						
							|  |  |  |   document.querySelectorAll('.text-choice-container select').forEach((select) => { | 
					
						
							|  |  |  |     const optionName = select.getAttribute('id'); | 
					
						
							|  |  |  |     select.addEventListener('change', () => { | 
					
						
							|  |  |  |       document.getElementById(`${optionName}-custom`).value = ''; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Update the "Option Preset" select to read "custom" when changes are made to relevant inputs
 | 
					
						
							|  |  |  |   const presetSelect = document.getElementById('game-options-preset'); | 
					
						
							|  |  |  |   document.querySelectorAll('input, select').forEach((input) => { | 
					
						
							|  |  |  |     if ( // Ignore inputs which have no effect on yaml generation
 | 
					
						
							|  |  |  |       (input.id === 'player-name') || | 
					
						
							|  |  |  |       (input.id === 'game-options-preset') || | 
					
						
							|  |  |  |       (input.classList.contains('group-toggle')) || | 
					
						
							|  |  |  |       (input.type === 'submit') | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     input.addEventListener('change', () => { | 
					
						
							|  |  |  |       presetSelect.value = 'custom'; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Handle changes to presets select
 | 
					
						
							|  |  |  |   document.getElementById('game-options-preset').addEventListener('change', choosePreset); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Save settings to localStorage when form is submitted
 | 
					
						
							|  |  |  |   document.getElementById('options-form').addEventListener('submit', (evt) => { | 
					
						
							|  |  |  |     const playerName = document.getElementById('player-name'); | 
					
						
							|  |  |  |     if (!playerName.value.trim()) { | 
					
						
							|  |  |  |       evt.preventDefault(); | 
					
						
							|  |  |  |       window.scrollTo(0, 0); | 
					
						
							|  |  |  |       showUserMessage('You must enter a player name!'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     saveSettings(); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Save all settings to localStorage
 | 
					
						
							|  |  |  | const saveSettings = () => { | 
					
						
							|  |  |  |   const options = { | 
					
						
							|  |  |  |     inputs: {}, | 
					
						
							|  |  |  |     checkboxes: {}, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  |   document.querySelectorAll('input, select').forEach((input) => { | 
					
						
							|  |  |  |     if (input.type === 'submit') { | 
					
						
							|  |  |  |       // Ignore submit inputs
 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     else if (input.type === 'checkbox') { | 
					
						
							|  |  |  |       options.checkboxes[input.id] = input.checked; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     else { | 
					
						
							|  |  |  |       options.inputs[input.id] = input.value | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   const game = document.getElementById('player-options').getAttribute('data-game'); | 
					
						
							|  |  |  |   localStorage.setItem(game, JSON.stringify(options)); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Load all options from localStorage
 | 
					
						
							|  |  |  | const loadSettings = () => { | 
					
						
							|  |  |  |   const game = document.getElementById('player-options').getAttribute('data-game'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const options = JSON.parse(localStorage.getItem(game)); | 
					
						
							|  |  |  |   if (options) { | 
					
						
							|  |  |  |     if (!options.inputs || !options.checkboxes) { | 
					
						
							|  |  |  |       localStorage.removeItem(game); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Restore value-based inputs and selects
 | 
					
						
							|  |  |  |     Object.keys(options.inputs).forEach((key) => { | 
					
						
							|  |  |  |       try{ | 
					
						
							|  |  |  |         document.getElementById(key).value = options.inputs[key]; | 
					
						
							|  |  |  |         const rangeValue = document.getElementById(`${key}-value`); | 
					
						
							|  |  |  |         if (rangeValue) { | 
					
						
							|  |  |  |           rangeValue.innerText = options.inputs[key]; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         console.error(`Unable to restore value to input with id ${key}`); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Restore checkboxes
 | 
					
						
							|  |  |  |     Object.keys(options.checkboxes).forEach((key) => { | 
					
						
							|  |  |  |       try{ | 
					
						
							|  |  |  |         if (options.checkboxes[key]) { | 
					
						
							|  |  |  |           document.getElementById(key).setAttribute('checked', '1'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         console.error(`Unable to restore value to input with id ${key}`); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Ensure any input for which the randomize checkbox is checked by default, the relevant inputs are disabled
 | 
					
						
							|  |  |  |   document.querySelectorAll('.randomize-checkbox').forEach((checkbox) => { | 
					
						
							|  |  |  |     const optionName = checkbox.getAttribute('data-option-name'); | 
					
						
							|  |  |  |     if (checkbox.checked) { | 
					
						
							|  |  |  |       const input = document.getElementById(optionName); | 
					
						
							|  |  |  |       if (input) { | 
					
						
							|  |  |  |         input.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const customInput = document.getElementById(`${optionName}-custom`); | 
					
						
							|  |  |  |       if (customInput) { | 
					
						
							|  |  |  |         customInput.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Fetch the preset data for this game and apply the presets if localStorage indicates one was previously chosen | 
					
						
							|  |  |  |  * @returns {Promise<void>} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const fetchPresets = async () => { | 
					
						
							|  |  |  |   const response = await fetch('option-presets'); | 
					
						
							|  |  |  |   presets = await response.json(); | 
					
						
							|  |  |  |   const presetSelect = document.getElementById('game-options-preset'); | 
					
						
							|  |  |  |   presetSelect.removeAttribute('disabled'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const game = document.getElementById('player-options').getAttribute('data-game'); | 
					
						
							|  |  |  |   const presetToApply = localStorage.getItem(`${game}-preset`); | 
					
						
							|  |  |  |   const playerName = localStorage.getItem(`${game}-player`); | 
					
						
							|  |  |  |   if (presetToApply) { | 
					
						
							|  |  |  |     localStorage.removeItem(`${game}-preset`); | 
					
						
							|  |  |  |     presetSelect.value = presetToApply; | 
					
						
							|  |  |  |     applyPresets(presetToApply); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (playerName) { | 
					
						
							|  |  |  |     document.getElementById('player-name').value = playerName; | 
					
						
							|  |  |  |     localStorage.removeItem(`${game}-player`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Clear the localStorage for this game and set a preset to be loaded upon page reload | 
					
						
							|  |  |  |  * @param evt | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const choosePreset = (evt) => { | 
					
						
							|  |  |  |   if (evt.target.value === 'custom') { return; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const game = document.getElementById('player-options').getAttribute('data-game'); | 
					
						
							|  |  |  |   localStorage.removeItem(game); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   localStorage.setItem(`${game}-player`, document.getElementById('player-name').value); | 
					
						
							|  |  |  |   if (evt.target.value !== 'default') { | 
					
						
							|  |  |  |     localStorage.setItem(`${game}-preset`, evt.target.value); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   document.querySelectorAll('#options-form input, #options-form select').forEach((input) => { | 
					
						
							|  |  |  |     if (input.id === 'player-name') { return; } | 
					
						
							|  |  |  |     input.removeAttribute('value'); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   window.location.replace(window.location.href); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const applyPresets = (presetName) => { | 
					
						
							|  |  |  |   // Ignore the "default" preset, because it gets set automatically by Jinja
 | 
					
						
							|  |  |  |   if (presetName === 'default') { | 
					
						
							|  |  |  |     saveSettings(); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!presets[presetName]) { | 
					
						
							|  |  |  |     console.error(`Unknown preset ${presetName} chosen`); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const preset = presets[presetName]; | 
					
						
							|  |  |  |   Object.keys(preset).forEach((optionName) => { | 
					
						
							|  |  |  |     const optionValue = preset[optionName]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Handle List and Set options
 | 
					
						
							|  |  |  |     if (Array.isArray(optionValue)) { | 
					
						
							|  |  |  |       document.querySelectorAll(`input[type=checkbox][name=${optionName}]`).forEach((checkbox) => { | 
					
						
							|  |  |  |         if (optionValue.includes(checkbox.value)) { | 
					
						
							|  |  |  |           checkbox.setAttribute('checked', '1'); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           checkbox.removeAttribute('checked'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Handle Dict options
 | 
					
						
							|  |  |  |     if (typeof(optionValue) === 'object' && optionValue !== null) { | 
					
						
							|  |  |  |       const itemNames = Object.keys(optionValue); | 
					
						
							|  |  |  |       document.querySelectorAll(`input[type=number][data-option-name=${optionName}]`).forEach((input) => { | 
					
						
							|  |  |  |         const itemName = input.getAttribute('data-item-name'); | 
					
						
							|  |  |  |         input.value = (itemNames.includes(itemName)) ? optionValue[itemName] : 0 | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Identify all possible elements
 | 
					
						
							|  |  |  |     const normalInput = document.getElementById(optionName); | 
					
						
							|  |  |  |     const customInput = document.getElementById(`${optionName}-custom`); | 
					
						
							|  |  |  |     const rangeValue = document.getElementById(`${optionName}-value`); | 
					
						
							|  |  |  |     const randomizeInput = document.getElementById(`random-${optionName}`); | 
					
						
							|  |  |  |     const namedRangeSelect = document.getElementById(`${optionName}-select`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // It is possible for named ranges to use name of a value rather than the value itself. This is accounted for here
 | 
					
						
							|  |  |  |     let trueValue = optionValue; | 
					
						
							|  |  |  |     if (namedRangeSelect) { | 
					
						
							|  |  |  |       namedRangeSelect.querySelectorAll('option').forEach((opt) => { | 
					
						
							|  |  |  |         if (opt.innerText.startsWith(optionValue)) { | 
					
						
							|  |  |  |           trueValue = opt.value; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       namedRangeSelect.value = trueValue; | 
					
						
							| 
									
										
										
										
											2024-10-17 09:40:46 -07:00
										 |  |  |       // It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom"
 | 
					
						
							|  |  |  |       if (namedRangeSelect.selectedIndex == -1) | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         namedRangeSelect.value = "custom"; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
											  
											
												WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.
* Remove debug output
* In-progress conversion of player-options to Jinja rendering
* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors
* Un-alphabetize options, add default group name for item/location Option classes, implement more option types
* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options
* Support all remaining option types
* Rendering improvements and CSS fixes for prettiness
* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS
* Minor CSS tweaks, as recommended by the designer
* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.
* Fix labels, add JS handling for TextChoice
* Make option groups collapsable
* PEP8 current option_groups progress (#2604)
* Make the python more PEP8 and remove unneeded imports
* remove LocationSet from `Item & Location Options` group
* It's ugly, but YAML generation is working
* Stop generating JSON files for player-options pages
* Do not include ItemDict entries whose values are zero
* Properly format yaml output
* Save options when form is submitted, load options on page load
* Fix options being omitted from the page if a group has an even number of options
* Implement generate-game, escape option descriptions
* Fix "randomize" checkboxes not properly setting YAML options to "random"
* Add a separator between item/location groups and items/locations in their respective lists
* Implement option presets
* Fix docs to detail what actually ended up happening
* implement option groups on webworld to allow dev sorting (#2616)
* Force extremely long item/location/option names with no spaces to text-wrap
* Fix "randomize" button being too wide in single-column display, change page header to include game name
* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name
* Un-break weighted-options, add option group names to weighted options
* Nuke weighted-options. Set up framework to rebuild it in Jinja.
* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds
* Support Toggle, DefaultOnToggle, and Choice options in weighted-options
* Implement expand/collapse without JS for worlds and option groups
* Properly style set options
* Implement Range and NamedRange. Also, CSS is hard.
* Add support for remaining option types. JS and backend still forthcoming.
* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.
* Support showing/hiding game divs based on range value for game
* Add support for adding/deleting range rows
* Save settings to localStorage on form submission
* Save deleted options on form submission
* Break weighted-options into a per-game page.
- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options
* Implement loading previous settings from localStorage on page load if JS is enabled
* Weighted options can now generate YAML files and single-player games
* options pages now respect option visibility settings for simple and complex pages
* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page
* Fix instance of AutoWorld not having access to proper `random`
* Catch instances of frozenset along with set
* Restore word-wrap in tooltips
* Fix word wrap in player-options labels
* Add `dedent` filter to help with formatting tooltips in player-options
* Do not change the ordering of keys when printing yaml files
* Move necessary import out of conditional statement
* Expand only the first option group by default on both options pages
* Respect option visibility when generating yaml template files
* Swap to double quotes
* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links
* Strip newlines and spaces after applying dedent filter
* Fix documentation for option groups
* Update site map
* Update various docs
* Sort OptionSet lists alphabetically
* Minor style tweak
* Fix extremely long text overflowing tooltips
* Convert player-options to use CSS grid instead of tables
* Do not display link to weighted-options page on supported games if the options page is an external link
* Update worlds/AutoWorld.py
Bugfix by @alwaysintreble
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
* Fix NamedRange options not being properly set if a preset it loaded
* Move option-presets route into options.py
* Include preset name in YAML if not "default" and not "custom"
* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes
* Fix not disabling custom inputs when the randomize button is clicked
* Only sort OptionList and OptionSet valid_keys if they are unordered
* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column
* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold
* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs
---------
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
											
										 
											2024-05-18 00:11:57 -04:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Handle options whose presets are "random"
 | 
					
						
							|  |  |  |     if (optionValue === 'random') { | 
					
						
							|  |  |  |       normalInput.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |       randomizeInput.setAttribute('checked', '1'); | 
					
						
							|  |  |  |       if (customInput) { | 
					
						
							|  |  |  |         customInput.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (rangeValue) { | 
					
						
							|  |  |  |         rangeValue.innerText = normalInput.value; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (namedRangeSelect) { | 
					
						
							|  |  |  |         namedRangeSelect.setAttribute('disabled', '1'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Handle normal (text, number, select, etc.) and custom inputs (custom inputs exist with TextChoice only)
 | 
					
						
							|  |  |  |     normalInput.value = trueValue; | 
					
						
							|  |  |  |     normalInput.removeAttribute('disabled'); | 
					
						
							|  |  |  |     randomizeInput.removeAttribute('checked'); | 
					
						
							|  |  |  |     if (customInput) { | 
					
						
							|  |  |  |       document.getElementById(`${optionName}-custom`).removeAttribute('disabled'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (rangeValue) { | 
					
						
							|  |  |  |       rangeValue.innerText = trueValue; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   saveSettings(); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const showUserMessage = (text) => { | 
					
						
							|  |  |  |   const userMessage = document.getElementById('user-message'); | 
					
						
							|  |  |  |   userMessage.innerText = text; | 
					
						
							|  |  |  |   userMessage.addEventListener('click', hideUserMessage); | 
					
						
							|  |  |  |   userMessage.style.display = 'block'; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const hideUserMessage = () => { | 
					
						
							|  |  |  |   const userMessage = document.getElementById('user-message'); | 
					
						
							|  |  |  |   userMessage.removeEventListener('click', hideUserMessage); | 
					
						
							|  |  |  |   userMessage.style.display = 'none'; | 
					
						
							|  |  |  | }; |