All files / src/internal/client/dom/elements/bindings select.js

99.31% Statements 145/146
100% Branches 24/24
100% Functions 5/5
99.29% Lines 141/142

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 1432x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 205x 34x 34x 171x 205x 306x 306x 112x 112x 112x 306x 59x 205x 43x 43x 205x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 90x 90x 90x 18x 18x 90x 90x 90x 15x 15x 15x 15x 15x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x 90x   90x 90x 90x 2x 2x 2x 2x 2x 2x 2x 2x 72x 72x 72x 28x 28x 28x 28x 4x 28x 24x 24x 24x 24x 28x 28x 72x 72x 72x 72x 138x 138x 138x 138x 138x 14x 14x 14x 14x 14x 14x 14x 138x 138x 138x 138x 72x 72x 72x 72x 72x 2x 2x 2x 2x 2x 2x 34x 34x 76x 76x 76x 34x 2x 2x 426x 426x 426x 276x 426x 150x 150x 426x  
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_value
 * @param {(value: unknown) => void} update
 * @returns {void}
 */
export function bind_select_value(select, get_value, update) {
	var mounting = true;
 
	listen_to_event_and_reset_event(select, 'change', () => {
		/** @type {unknown} */
		var value;
 
		if (select.multiple) {
			value = [].map.call(select.querySelectorAll(':checked'), get_option_value);
		} else {
			/** @type {HTMLOptionElement | null} */
			var selected_option = select.querySelector(':checked');
			value = selected_option && get_option_value(selected_option);
		}
 
		update(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_value();
		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);
				update(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;
	}
}