Created
December 14, 2019 17:12
-
-
Save Beaglefoot/0edf5dac8faed93b61e04874911f176e to your computer and use it in GitHub Desktop.
Simple TreeSelect (with vanillaTS)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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