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

100% Statements 146/146
100% Branches 25/25
100% Functions 5/5
100% Lines 142/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 211x 34x 34x 177x 211x 316x 316x 118x 118x 118x 316x 59x 211x 43x 43x 211x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 92x 92x 92x 18x 18x 92x 92x 92x 15x 15x 15x 15x 15x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 92x 2x 2x 2x 2x 2x 2x 2x 2x 74x 74x 74x 30x 30x 30x 30x 4x 30x 26x 26x 26x 26x 30x 30x 74x 74x 74x 74x 144x 144x 144x 144x 144x 14x 14x 14x 14x 14x 14x 14x 144x 144x 144x 144x 74x 74x 74x 74x 74x 2x 2x 2x 2x 2x 2x 34x 34x 76x 76x 76x 34x 2x 2x 438x 438x 438x 290x 438x 148x 148x 438x  
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;
	}
}