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

100% Statements 144/144
100% Branches 26/26
100% Functions 5/5
100% Lines 140/140

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 1412x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 208x 34x 34x 174x 208x 310x 310x 118x 118x 118x 310x 56x 208x 42x 42x 208x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 90x 90x 16x 16x 90x 90x 14x 14x 14x 14x 14x 90x 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 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 432x 432x 432x 284x 432x 148x 148x 432x  
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) {
	effect(() => {
		if (get_value) {
			select_option(select, untrack(get_value));
		}
 
		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;
	}
}