| import * as d3 from 'd3'; |
|
|
| export type SettingsDropdownOption<T> = { |
| value: T; |
| html: string; |
| }; |
|
|
| |
| const SHARED = { |
| container: 'settings-dropdown', |
| btn: 'settings-dropdown-btn', |
| menu: 'settings-dropdown-menu', |
| option: 'settings-dropdown-option', |
| } as const; |
|
|
| export type CreateSettingsDropdownOptions<T> = { |
| container: d3.Selection<Element | d3.BaseType, unknown, HTMLElement, unknown>; |
| classPrefix: string; |
| options: SettingsDropdownOption<T>[]; |
| dataAttr: string; |
| bodyClickNamespace: string; |
| onSelect: (value: T) => void; |
| }; |
|
|
| export type SettingsDropdown<T> = { |
| updateCurrent: (value: T) => void; |
| dispose: () => void; |
| }; |
|
|
| |
| |
| |
| export function createSettingsDropdown<T extends string>( |
| config: CreateSettingsDropdownOptions<T> |
| ): SettingsDropdown<T> { |
| const { container, classPrefix, options, dataAttr, bodyClickNamespace, onSelect } = config; |
| const containerClass = `${classPrefix}-dropdown-container`; |
| const currentBtnClass = `${classPrefix}-current-btn`; |
| const menuClass = `${classPrefix}-dropdown-menu`; |
| const optionClass = `${classPrefix}-option`; |
|
|
| container.html(''); |
| const dropdownContainer = container.append('div').attr('class', `${containerClass} ${SHARED.container}`); |
| const currentButton = dropdownContainer.append('button').attr('class', `${currentBtnClass} ${SHARED.btn}`).attr('type', 'button'); |
| const dropdownMenu = dropdownContainer.append('div').attr('class', `${menuClass} ${SHARED.menu}`); |
|
|
| options.forEach(({ value, html }) => { |
| const option = dropdownMenu |
| .append('button') |
| .attr('class', `${optionClass} ${optionClass}-${value} ${SHARED.option}`) |
| .attr(dataAttr, value) |
| .attr('type', 'button') |
| .html(html); |
| option.on('click', function (event: MouseEvent) { |
| event.stopPropagation(); |
| if (d3.select(this).classed('active')) return; |
| onSelect(d3.select(this).attr(dataAttr) as T); |
| closeDropdown(); |
| }); |
| }); |
|
|
| const updateCurrent = (value: T) => { |
| const opt = options.find((o) => o.value === value); |
| if (opt) currentButton.html(opt.html); |
| dropdownMenu.selectAll(`.${optionClass}`).classed('active', function () { |
| return d3.select(this).attr(dataAttr) === value; |
| }); |
| }; |
|
|
| let isOpen = false; |
| const openDropdown = () => { |
| isOpen = true; |
| dropdownMenu.classed('open', true); |
| currentButton.classed('active', true); |
| }; |
| const closeDropdown = () => { |
| isOpen = false; |
| dropdownMenu.classed('open', false); |
| currentButton.classed('active', false); |
| }; |
|
|
| currentButton.on('click', (event: MouseEvent) => { |
| event.stopPropagation(); |
| if (isOpen) closeDropdown(); |
| else openDropdown(); |
| }); |
|
|
| |
| const bodyClickHandler = (event: MouseEvent) => { |
| if (!isOpen) return; |
| const target = event.target as HTMLElement; |
| const containerNode = dropdownContainer.node(); |
| |
| if (containerNode && !containerNode.contains(target)) { |
| closeDropdown(); |
| } |
| }; |
| |
| setTimeout(() => { |
| document.addEventListener('click', bodyClickHandler, true); |
| }, 0); |
|
|
| return { |
| updateCurrent, |
| dispose: () => { |
| document.removeEventListener('click', bodyClickHandler, true); |
| container.selectAll('*').remove(); |
| }, |
| }; |
| } |
|
|