UNPKG

svelte

Version:

Cybernetically enhanced web apps

257 lines (221 loc) • 7.82 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'; import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js'; import { async_mode_flag } from '../../../../flags/index.js'; /** * Sets the `selected` attribute on an option so form reset can restore it. * @param {HTMLOptionElement} option * @param {boolean} selected */ export function set_selected(option, selected) { if (selected) { if (!option.hasAttribute('selected')) option.setAttribute('selected', ''); } else { option.removeAttribute('selected'); } } /** * Sets the options a form reset should restore. The first call selects * them if nothing has set a value, later calls leave the current selection alone. * @param {HTMLSelectElement} select * @param {any} value */ export function set_default_select_value(select, value) { var mounting = !('__defaultValue' in select); // @ts-expect-error if (!mounting && select.__defaultValue === value) return; // @ts-expect-error select.__defaultValue = value; apply_default_select_value(select, !mounting || '__value' in select); } /** * Marks the options matching `__defaultValue` as selected. Without `preserve` * a newly matching option gets selected, as an inserted `<option selected>` would. * @param {HTMLSelectElement} select * @param {boolean} preserve */ function apply_default_select_value(select, preserve) { // @ts-expect-error var value = select.__defaultValue; var multiple = select.multiple; var values = multiple ? value ?? [] : null; if (multiple && !is_array(values)) return; var index = select.selectedIndex; var selected = preserve && multiple ? new Set(select.selectedOptions) : null; for (var option of select.options) { var option_value = get_option_value(option); set_selected( option, multiple ? /** @type {any[]} */ (values).includes(option_value) : is(option_value, value) ); } if (!preserve) return; if (selected !== null) { for (option of select.options) { var was_selected = selected.has(option); if (option.selected !== was_selected) option.selected = was_selected; } } else if (select.selectedIndex !== index) { select.selectedIndex = index; } } /** * 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 } } /** * Sets up a mutation observer to sync the current selection * and default to the dom when the options change, for example * when they are inside an `#each` block. Called once per `<select>`, * by the compiled output or by `attribute_effect` for spreads. * @param {HTMLSelectElement} select */ export function init_select(select) { var observer = new MutationObserver((entries) => { // Mutations related to `<selectedcontent>` can never affect the option list. // Reacting to them could revert a user-initiated selection change, because the // records are delivered as soon as any listener returns (e.g. a delegated `input` // handler), which can happen before the `change` handler has updated `__value` if (entries.every(is_selectedcontent_mutation)) return; if ('__defaultValue' in select) { apply_default_select_value(select, false); } if ('__value' in select) { 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 batches = new WeakSet(); 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); // @ts-ignore select.__value = value; if (current_batch !== null) { batches.add(current_batch); } }); // 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(); if (select === document.activeElement) { // In sync mode render effects are executed during tree traversal -> needs current_batch // In async mode render effects are flushed once batch resolved, at which point current_batch is null -> needs previous_batch var batch = /** @type {Batch} */ (async_mode_flag ? previous_batch : current_batch); // Don't update the <select> if it is focused. We can get here if, for example, // an update is deferred because of async work depending on the select: // // <select bind:value={selected}>...</select> // <p>{await find(selected)}</p> if (batches.has(batch)) { return; } } 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; }); } /** @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; } } /** * Returns `true` if the mutation stems from the browser mirroring the selected * option's content into `<selectedcontent>`, or from us replacing the * `<selectedcontent>` element with a clone of itself * @param {MutationRecord} entry */ function is_selectedcontent_mutation(entry) { if (/** @type {Element} */ (entry.target).closest('selectedcontent') !== null) { return true; } if (entry.type === 'childList') { var nodes = [...entry.addedNodes, ...entry.removedNodes]; return nodes.length > 0 && nodes.every((node) => node.nodeName === 'SELECTEDCONTENT'); } return false; }