Skip to content

Instantly share code, notes, and snippets.

@Beaglefoot
Created December 14, 2019 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Beaglefoot/0edf5dac8faed93b61e04874911f176e to your computer and use it in GitHub Desktop.
Save Beaglefoot/0edf5dac8faed93b61e04874911f176e to your computer and use it in GitHub Desktop.
Simple TreeSelect (with vanillaTS)
console.clear();
enum FoldIcon {
folded = '+',
unfolded = '-'
}
interface IData {
id: number;
title: string;
children: IData[];
}
const data: IData[] = [
{
id: 1,
title: 'Afroaves',
children: [
{
id: 2,
title: 'Accipitrimorphae',
children: [
{ id: 3, title: 'Cathartiformes', children: [] },
{ id: 4, title: 'Accipitriformes', children: [] }
]
},
{
id: 5,
title: 'Coraciimorphae',
children: [
{
id: 6,
title: 'Cavitaves',
children: [
{
id: 7,
title: 'Eucavitaves',
children: []
},
{ id: 4, title: 'Leptosomiformes', children: [] }
]
},
{ id: 4, title: 'Coliiformes', children: [] }
]
},
{ id: 5, title: 'Strigiformes', children: [] }
]
}
];
class TreeNode {
public id: number;
public parent?: TreeNode;
private title: string;
private _isFolded: boolean;
private children: TreeNode[];
private domElement: HTMLElement;
private foldIcon: HTMLElement;
private onSelect: (ids: number[], title: string, event: MouseEvent) => void;
constructor({
id,
title,
children,
onSelect,
parent
}: {
id: number;
title: string;
children: IData[];
parent?: TreeNode;
onSelect: (ids: number[], title: string, event: MouseEvent) => void;
}) {
this.id = id;
this.title = title;
this.parent = parent;
this.onSelect = onSelect;
this._isFolded = true;
this.children = children.map(
child => new TreeNode({ ...child, onSelect, parent: this })
);
this.foldIcon = this.getFoldIcon();
this.domElement = this.getDomElement();
}
public appendTo(element: HTMLElement): void {
element.appendChild(this.domElement);
}
public removeFrom(element: HTMLElement): void {
element.removeChild(this.domElement);
}
private get isFolded(): boolean {
return this._isFolded;
}
private set isFolded(state: boolean) {
this._isFolded = state;
if (state) {
this.foldIcon.textContent = FoldIcon.folded;
this.children.forEach(child => {
child.removeFrom(this.domElement);
});
} else {
this.foldIcon.textContent = FoldIcon.unfolded;
this.children.forEach(child => {
child.domElement.classList.add('tree-select__node-inner-fold');
child.appendTo(this.domElement);
});
}
}
private getDomElement(): HTMLElement {
const domElement = document.createElement('div');
domElement.appendChild(this.foldIcon);
domElement.appendChild(this.getTitleElement());
domElement.classList.add('tree-select__node');
domElement.addEventListener('click', (event: MouseEvent): void => {
event.stopPropagation();
this.onSelect(this.getIdsBranch(), this.title, event);
});
return domElement;
}
private getTitleElement(): HTMLElement {
const title = document.createElement('span');
title.textContent = this.title;
title.classList.add('tree-select__title');
return title;
}
private getFoldIcon(): HTMLElement {
const foldIcon = document.createElement('span');
foldIcon.appendChild(document.createTextNode(FoldIcon.folded));
foldIcon.classList.add('tree-select__node-fold');
if (!this.children.length) {
foldIcon.classList.add('tree-select__node-fold--invisible');
} else {
foldIcon.addEventListener('click', this.handleClick);
}
return foldIcon;
}
private handleClick = (event: MouseEvent): void => {
event.stopPropagation();
this.isFolded = !this.isFolded;
};
private getIdsBranch(): number[] {
const ids = [];
let treeNode: TreeNode | undefined = this;
while (treeNode) {
ids.unshift(treeNode.id);
treeNode = treeNode.parent;
}
return ids;
}
}
class SelectField {
private domElement: HTMLElement;
private _selectionText = '';
private placeholder = 'Select';
private icon: HTMLElement;
constructor(onClick: (event: MouseEvent) => void) {
this.domElement = this.getDomElement(onClick);
this.icon = this.getSelectIcon();
this.domElement.appendChild(this.icon);
}
public set text(newText: string) {
this._selectionText = newText;
this.domElement.textContent = this._selectionText;
this.domElement.classList.remove('tree-select__select-field--deemphasize');
this.domElement.appendChild(this.icon);
}
public appendTo(element: HTMLElement) {
element.appendChild(this.domElement);
}
public onOpen(): void {
this.icon.classList.add('tree-select__select-icon--down');
}
public onClose(): void {
this.icon.classList.remove('tree-select__select-icon--down');
}
private getDomElement(onClick: (event: MouseEvent) => void): HTMLElement {
const domElement = document.createElement('div');
domElement.classList.add(
'tree-select__select-field',
'tree-select__select-field--deemphasize'
);
domElement.textContent = this.placeholder;
domElement.addEventListener('click', onClick);
return domElement;
}
private getSelectIcon(): HTMLElement {
const icon = document.createElement('div');
icon.classList.add('tree-select__select-icon');
return icon;
}
}
class TreeSelect {
public selectField: SelectField;
private data: IData[];
private domElement: HTMLElement;
private providedOnSelect: (ids: number[], title: string, event: Event) => void;
private _isOpened = false;
private dropDown: HTMLElement = this.getDropDown();
constructor(data: IData[], providedOnSelect: (ids: number[], title: string, event: Event) => void) {
this.data = data;
this.providedOnSelect = providedOnSelect;
this.selectField = new SelectField(this.toggleDropDown);
this.domElement = this.getDomElement();
this.attachTreeNodes(this.dropDown);
}
public appendTo(element: HTMLElement) {
element.appendChild(this.domElement);
}
private getDomElement(): HTMLElement {
const domElement = document.createElement('div');
domElement.classList.add('tree-select');
this.selectField.appendTo(domElement);
return domElement;
}
private onSelect = (
ids: number[],
title: string,
event: MouseEvent
): void => {
this.providedOnSelect(ids, title, event.target);
this.selectField.text = title;
this.isOpened = false;
};
private getTreeNode({ id, title, children }: IData): TreeNode {
return new TreeNode({
id,
title,
children,
onSelect: this.onSelect
});
}
private attachTreeNodes(element: HTMLElement): void {
this.data
.map(this.getTreeNode, this)
.forEach(treeNode => treeNode.appendTo(element));
}
private getDropDown(): HTMLElement {
const dropDown = document.createElement('div');
dropDown.classList.add('tree-select__dropdown');
return dropDown;
}
private get isOpened(): boolean {
return this._isOpened;
}
private set isOpened(value: boolean) {
this._isOpened = value;
if (value) {
this.domElement.appendChild(this.dropDown);
this.selectField.onOpen();
} else {
this.domElement.removeChild(this.dropDown);
this.selectField.onClose();
}
}
private toggleDropDown = (): void => {
this.isOpened = !this.isOpened;
};
}
const treeSelect = new TreeSelect(data, console.log);
treeSelect.appendTo(document.body);
const lorem = document.createElement('div');
lorem.textContent =
'"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."';
document.body.appendChild(lorem);
.tree-select {
position: relative;
width: 200px;
padding: 0 5px;
color: #333;
user-select: none;
}
.tree-select__select-field {
position: relative;
border-radius: 5px;
padding: 0.2em 0.4em;
margin: 0 -5px;
cursor: pointer;
}
.tree-select__select-field:hover {
border-color: skyblue;
}
.tree-select__select-field--deemphasize {
color: #bbb;
}
.tree-select__select-icon {
border-right: 1px solid #bbb;
border-bottom: 1px solid #bbb;
position: absolute;
top: 50%;
right: 16px;
width: 0.5em;
height: 0.5em;
transform: translateY(-50%) rotate(-45deg);
transition: transform 0.2s;
}
.tree-select__select-icon--down {
transform: translateY(-75%) rotate(45deg);
}
.tree-select__dropdown {
position: absolute;
left: 0;
top: calc(100% + 2px);
background: #fff;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.25);
min-width: 100%;
transition: opacity 0.5s;
}
.tree-select__select-field,
.tree-select__dropdown {
box-sizing: border-box;
border: 1px solid #bbb;
border-radius: 5px;
padding: 0.2em 0.4em;
}
.tree-select__title {
cursor: pointer;
}
.tree-select__title:hover {
color: gold;
text-shadow: 0px 0px 1px black;
}
.tree-select__node-fold {
padding: 5px;
font-family: monospace;
font-size: 16px;
vertical-align: text-bottom;
cursor: pointer;
}
.tree-select__node-fold:hover {
color: gold;
text-shadow: 0px 0px 1px black;
}
.tree-select__node-fold--invisible {
visibility: hidden;
}
.tree-select__node-inner-fold {
margin-left: calc(1em);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment