UNPKG

svelte

Version:

Cybernetically enhanced web apps

139 lines (120 loc) 3.81 kB
import { effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; import * as w from '../../../warnings.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) * @template V * @param {HTMLSelectElement} select * @param {V} value * @param {boolean} mounting */ export function select_option(select, value, mounting = false) { if (select.multiple) { // If value is null or undefined, keep the selection as is if (value == undefined) { return; } // If not an array, warn and keep the selection as is if (!is_array(value)) { return w.select_multiple_invalid_value(); } // Otherwise, update the selection for (var option of select.options) { option.selected = value.includes(get_option_value(option)); } return; } for (option of select.options) { var option_value = get_option_value(option); if (is(option_value, value)) { option.selected = true; return; } } if (!mounting || value !== undefined) { select.selectedIndex = -1; // no option should be selected } } /** * Selects the correct option(s) if `value` is given, * and then sets up a mutation observer to sync the * current selection to the dom when it changes. Such * changes could for example occur when options are * inside an `#each` block. * @param {HTMLSelectElement} select */ export function init_select(select) { var observer = new MutationObserver(() => { // @ts-ignore select_option(select, select.__value); // Deliberately don't update the potential binding value, // the model should be preserved unless explicitly changed }); observer.observe(select, { // Listen to option element changes childList: true, subtree: true, // because of <optgroup> // Listen to option element value attribute changes // (doesn't get notified of select value changes, // because that property is not reflected as an attribute) attributes: true, attributeFilter: ['value'] }); teardown(() => { observer.disconnect(); }); } /** * @param {HTMLSelectElement} select * @param {() => unknown} get * @param {(value: unknown) => void} set * @returns {void} */ export function bind_select_value(select, get, set = get) { var mounting = true; listen_to_event_and_reset_event(select, 'change', (is_reset) => { var query = is_reset ? '[selected]' : ':checked'; /** @type {unknown} */ var value; if (select.multiple) { value = [].map.call(select.querySelectorAll(query), get_option_value); } else { /** @type {HTMLOptionElement | null} */ var selected_option = select.querySelector(query) ?? // will fall back to first non-disabled option if no option is selected select.querySelector('option:not([disabled])'); value = selected_option && get_option_value(selected_option); } set(value); }); // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated effect(() => { var value = get(); select_option(select, value, mounting); // Mounting and value undefined -> take selection from dom if (mounting && value === undefined) { /** @type {HTMLOptionElement | null} */ var selected_option = select.querySelector(':checked'); if (selected_option !== null) { value = get_option_value(selected_option); set(value); } } // @ts-ignore select.__value = value; mounting = false; }); init_select(select); } /** @param {HTMLOptionElement} option */ function get_option_value(option) { // __value only exists if the <option> has a value attribute if ('__value' in option) { return option.__value; } else { return option.value; } }