Skip to content

Commit

Permalink
added multiselect
Browse files Browse the repository at this point in the history
  • Loading branch information
tesohi committed Jan 16, 2024
1 parent 46cac1e commit d5d3202
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 4 deletions.
8 changes: 4 additions & 4 deletions src/components/event/EventWindowHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { EventListArrowNav } from './EventListNavigation';
import useEventsDataStore from '../../hooks/useEventsDataStore';
import { isEventsStore } from '../../helpers/stores';
import { EventsIntervalInput } from './EventsIntervalInput';
import Select from '../util/Select';
import { useBooksStore } from '../../hooks/useBooksStore';
import { SearchDirection } from '../../models/search/SearchDirection';
import MultiSelect from '../util/MultiSelect';

function EventWindowHeader() {
const eventStore = useWorkspaceEventStore();
Expand Down Expand Up @@ -58,15 +58,15 @@ function EventWindowHeader() {
</div>
<EventsIntervalInput />
</div>
<Select
<MultiSelect
className='event-window-header__scope'
options={booksStore.scopeList}
onChange={scope => {
if (scope) {
eventStore.applyScope(scope);
eventStore.applyScope(scope[0]);
}
}}
selected={eventStore.scope || ''}
selected={eventStore.scope ? [eventStore.scope] : []}
/>
{eventDataStore.isLoading && (
<div className='event-window-header__loader'>
Expand Down
169 changes: 169 additions & 0 deletions src/components/util/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/** ****************************************************************************
* Copyright 2020-2020 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
***************************************************************************** */

import React, { useState, useRef, useEffect } from 'react';
import '../../styles/multiselect.scss';
import { ModalPortal } from './Portal';
import { raf } from '../../helpers/raf';
import { useOutsideClickListener } from '../../hooks';

interface Props<T> {
className?: string;
options: Array<T>;
onChange: (selected: Array<T>) => void;
selected?: Array<T>;
}

function MultiSelect<T>({ options, onChange, selected = [], className = '' }: Props<T>) {
const [selectedValues, setSelectedValues] = useState<T[]>(selected || []);
const [isOpen, setIsOpen] = useState(false);
const [isAllSelected, setIsAllSelected] = useState(false);
const [partialSelect, setPartialSelect] = useState(0);
const dropdownRef = useRef<HTMLDivElement>(null);
const multiselectBodyRef = useRef<HTMLDivElement>(null);
const indeterminateCheckboxRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (selectedValues.length === 0) {
setIsAllSelected(false);
setPartialSelect(0);
} else if (selectedValues.length === options.length) {
setIsAllSelected(true);
setPartialSelect(2);
} else {
setIsAllSelected(false);
setPartialSelect(1);
}
}, [selectedValues, options.length]);

useEffect(() => {
if (indeterminateCheckboxRef.current) {
indeterminateCheckboxRef.current.indeterminate = partialSelect === 1;
}
}, [partialSelect]);

const handleSelectAll = () => {
if (!isAllSelected) {
setSelectedValues([...options]);
} else {
setSelectedValues([]);
}
};

const handlePartialSelect = () => {
if (partialSelect === 0 || partialSelect === 1) {
setSelectedValues([...options]);
} else {
setSelectedValues([]);
}
};

const handleSelect = (value: T) => {
const newSelectedValues = selectedValues.includes(value)
? selectedValues.filter(v => v !== value)
: [...selectedValues, value];

setSelectedValues(newSelectedValues);
if (onChange) {
onChange(newSelectedValues);
}
};

const toggleDropdown = () => setIsOpen(!isOpen);

React.useLayoutEffect(() => {
if (isOpen) {
raf(() => {
if (multiselectBodyRef.current && dropdownRef.current) {
const { left, bottom } = dropdownRef.current.getBoundingClientRect();
const clientWidth = document.documentElement.clientWidth;

let calculatedWidth = multiselectBodyRef.current.clientWidth;
const leftPosition = left;

if (left + calculatedWidth > clientWidth) {
calculatedWidth = clientWidth - left - 10;
}

multiselectBodyRef.current.style.left = `${leftPosition}px`;
multiselectBodyRef.current.style.top = `${bottom}px`;
multiselectBodyRef.current.style.width = `${calculatedWidth}px`;
}
}, 2);
}
}, [isOpen]);

useOutsideClickListener(
multiselectBodyRef,
(e: MouseEvent) => {
if (
e.target instanceof Element &&
!multiselectBodyRef.current?.contains(e.target) &&
!dropdownRef.current?.contains(e.target)
) {
setIsOpen(false);
}
},
isOpen,
);

return (
<div className={`multiselect ${className}`}>
<div ref={dropdownRef} className='dropdown-header' onClick={toggleDropdown}>
<div className='dropdown-header__text'>
{selectedValues.length > 0 ? selectedValues.join(', ') : ''}
</div>
</div>
<ModalPortal isOpen={isOpen}>
<div ref={multiselectBodyRef} className='dropdown-menu'>
<ul className='dropdown-list'>
<li className='dropdown-item' onClick={handlePartialSelect}>
<input
type='checkbox'
className='indeterminate-checkbox'
checked={partialSelect === 2}
ref={indeterminateCheckboxRef}
readOnly
/>
{partialSelect === 0 ? '0 Selected' : `${selectedValues.length} Selected`}
</li>

<li className='dropdown-item' onClick={handleSelectAll}>
<input type='checkbox' checked={isAllSelected} readOnly />
Select All
</li>

{options.map(option => (
<li
key={String(option)}
className={`dropdown-item ${selectedValues.includes(option) ? 'selected' : ''}`}
onClick={() => handleSelect(option)}>
<input
type='checkbox'
checked={selectedValues.includes(option)}
onChange={() => handleSelect(option)}
/>
{option}
</li>
))}
</ul>
</div>
</ModalPortal>
</div>
);
}

export default MultiSelect;
107 changes: 107 additions & 0 deletions src/styles/multiselect.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/******************************************************************************
* Copyright 2020-2020 Exactpro (Exactpro Systems Limited)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/

@import './common/vars';

.multiselect {
border: 1px solid #ccc;
padding: 0;
border-radius: 4px;
position: relative;
cursor: pointer;
user-select: none;
color: $primaryTextColor;
font-size: 14px;
}

.dropdown-header {
border-bottom: 1px solid #eee;
background: {
image: url(../../resources/icons/arr1-down.svg);
color: transparent;
repeat: no-repeat;
size: 15px;
position-x: 95%;
position-y: 50%;
}
width: 150px;
height: 24px;
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 4px;

&__text {
width: 130px;
height: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}

.dropdown-menu {
position: absolute;
top: 102%;
left: 0;
right: 0;
z-index: 1000;
padding: 5px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
max-height: 400px;
min-width: min-content;
width: fit-content;
max-width: 600px;
overflow-y: auto;
overflow-x: hidden;
@include scrollbar();
}

.dropdown-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: stretch;
}

.dropdown-item {
padding: 2px 5px 2px 2px;
cursor: pointer;
display: flex;
align-items: center;
border-radius: 4px;
white-space: nowrap;
}
.dropdown-item:first-child {
margin-bottom: 5px;
}

.dropdown-item input[type='checkbox'] {
margin-right: 5px;
}

.dropdown-item.selected {
background-color: #f0f0f0;
}

.dropdown-item:hover {
background-color: #e9e9e9;
}

0 comments on commit d5d3202

Please sign in to comment.