svelte
Version:
Cybernetically enhanced web apps
147 lines (129 loc) • 3.95 kB
JavaScript
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;
}
}