UNPKG

svelte

Version:

Cybernetically enhanced web apps

147 lines (129 loc) 3.95 kB
import { effect } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import { untrack } from '../../../runtime.js'; import { is } from '../../../proxy.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) { if (select.multiple) { return select_options(select, value); } for (var 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. * @template V * @param {HTMLSelectElement} select * @param {() => V} [get_value] */ export function init_select(select, get_value) { let mounting = true; effect(() => { if (get_value) { select_option(select, untrack(get_value), mounting); } mounting = false; var observer = new MutationObserver(() => { // @ts-ignore var value = select.__value; select_option(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'] }); return () => { 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; }); // don't pass get_value, we already initialize it in the effect above init_select(select); } /** * @template V * @param {HTMLSelectElement} select * @param {V} value */ function select_options(select, value) { for (var option of select.options) { // @ts-ignore option.selected = ~value.indexOf(get_option_value(option)); } } /** @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; } }