Source
21/46
39 |
40 | | File | 44 |Identifier | 45 |Document | 46 | 47 | 48 | 49 |
| src/tree.js | 55 |Tree | 56 |45 %21/46 | 57 | 58 | 59 | 60 |
├── docs ├── jsdoc │ ├── lint.json │ ├── image │ │ ├── search.png │ │ ├── esdoc-logo-mini.png │ │ ├── esdoc-logo-mini-black.png │ │ ├── badge.svg │ │ └── manual-badge.svg │ ├── script │ │ ├── patch-for-local.js │ │ ├── manual.js │ │ ├── pretty-print.js │ │ ├── inherited-summary.js │ │ ├── inner-link.js │ │ ├── test-summary.js │ │ ├── search.js │ │ ├── search_index.js │ │ └── prettify │ │ │ ├── Apache-License-2.0.txt │ │ │ └── prettify.js │ ├── coverage.json │ ├── css │ │ ├── identifiers.css │ │ ├── source.css │ │ ├── test.css │ │ ├── github.css │ │ ├── search.css │ │ ├── prettify-tomorrow.css │ │ ├── manual.css │ │ └── style.css │ ├── badge.svg │ ├── source.html │ ├── index.html │ ├── identifiers.html │ ├── typedef │ │ └── index.html │ ├── file │ │ └── src │ │ │ ├── defaults.js.html │ │ │ ├── icons.js.html │ │ │ ├── utils.js.html │ │ │ └── tree.js.html │ ├── variable │ │ └── index.html │ └── ast │ │ └── source │ │ └── icons.js.json ├── index.html ├── index.css └── code.js ├── .gitignore ├── example.api.json ├── images ├── tree.afdesign ├── partial.svg ├── open.svg └── closed.svg ├── scripts ├── cleanDocs.js ├── images.js ├── live.js └── compile.js ├── .esdoc.json ├── src ├── indicator.js ├── defaults.js ├── icons.js ├── utils.js ├── input.js └── tree.js ├── package.json ├── LICENSE └── README.md /docs/jsdoc/lint.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /example.api.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "type": "RootDoc" 4 | } -------------------------------------------------------------------------------- /images/tree.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/tree/HEAD/images/tree.afdesign -------------------------------------------------------------------------------- /docs/jsdoc/image/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/tree/HEAD/docs/jsdoc/image/search.png -------------------------------------------------------------------------------- /docs/jsdoc/image/esdoc-logo-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/tree/HEAD/docs/jsdoc/image/esdoc-logo-mini.png -------------------------------------------------------------------------------- /docs/jsdoc/image/esdoc-logo-mini-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfig/tree/HEAD/docs/jsdoc/image/esdoc-logo-mini-black.png -------------------------------------------------------------------------------- /scripts/cleanDocs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const dir = 'docs/jsdoc' 4 | 5 | async function clean() { 6 | console.log(`Cleaning director ${dir}...`) 7 | await fs.emptyDir(dir) 8 | process.exit(0) 9 | } 10 | 11 | clean() -------------------------------------------------------------------------------- /docs/jsdoc/script/patch-for-local.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (location.protocol === 'file:') { 3 | var elms = document.querySelectorAll('a[href="./"]'); 4 | for (var i = 0; i < elms.length; i++) { 5 | elms[i].href = './index.html'; 6 | } 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
23 |
24 |
25 | | File | 44 |Identifier | 45 |Document | 46 | 47 | 48 | 49 |
| src/tree.js | 55 |Tree | 56 |45 %21/46 | 57 | 58 | 59 | 60 |
23 |
24 |
25 | Vanilla drag-and-drop UI tree
39 |I needed a tree components for my tools. Most of the available visual tree APIs require vue or react. And so yy-tree was created.
40 |const data = {
41 | children: [
42 | { name: 'fruits', children: [
43 | { name: 'apples', children: [] },
44 | { name: 'oranges', children: [
45 | { name: 'tangerines', children: [] },
46 | { name: 'mandarins', children: [] },
47 | { name: 'pomelo', children: [] },
48 | { name: 'blood orange', children: [] },
49 | ] }
50 | ]},
51 | { name: 'vegetables', children: [
52 | { name: 'brocolli', children: [] },
53 | ] },
54 | ]
55 | }
56 |
57 | const tree = new Tree(data, { parent: document.body })
58 |
59 | https://davidfig.github.io/tree/
60 |https://davidfig.github.io/tree/jsdoc/
61 |npm i yy-tree
62 | MIT License 63 | (c) 2021 YOPEY YOPEY LLC by David Figatner
64 |
23 |
24 |
25 | | summary | ||
| 51 | public 52 | 53 | 54 | 55 | 56 | | 57 |
58 |
59 |
66 | 60 | C 61 | 62 | 63 | Tree 64 | 65 |
67 |
68 |
69 |
70 |
71 | |
72 | 73 | 74 | 75 | | 76 |
| 79 | public 80 | 81 | 82 | 83 | 84 | | 85 |
86 |
87 |
94 | 88 | T 89 | 90 | 91 | Tree~TreeData: Object 92 | 93 |
95 |
96 |
97 |
98 |
99 | |
100 | 101 | 102 | 103 | | 104 |
23 |
24 |
25 | | Static Public Summary | ||
| 45 | public 46 | 47 | 48 | 49 | 50 | | 51 |
52 |
53 |
60 | 54 | 55 | 56 | 57 | Tree~TreeData: Object 58 | 59 |
61 |
62 |
63 |
64 |
65 | |
66 | 67 | 68 | 69 | | 70 |
| Name | Type | Attribute | Description |
| children | 109 |TreeData[] | 110 |111 | | 112 | |
| name | 115 |string | 116 |117 | | 118 | |
| parent | 121 |parent | 122 |
|
123 | if not provided then will traverse tree to find parent 124 | |
125 |
23 |
24 |
25 | export const defaults = {
54 | children: 'children',
55 | parent: 'parent',
56 | name: 'name',
57 | data: 'data',
58 | expanded: 'expanded',
59 | move: true,
60 | indentation: 20,
61 | nameStyles: {
62 | padding: '0.5em 1em',
63 | margin: '0.1em',
64 | background: 'rgba(230,230,230)',
65 | userSelect: 'none',
66 | cursor: ['grab', '-webkit-grab', 'pointer'],
67 | width: '100px'
68 | },
69 | threshold: 10,
70 | indicatorStyles: {
71 | background: 'rgb(150,150,255)',
72 | height: '5px',
73 | width: '100px',
74 | padding: '0 1em'
75 | },
76 | expandStyles: {
77 | width: '15px',
78 | height: '15px',
79 | cursor: 'pointer'
80 | },
81 | holdTime: 1000,
82 | expandOnClick: true,
83 | dragOpacity: 0.75
84 | }
85 |
86 |
23 |
24 |
25 | export const icons={closed:'<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><rect id="closed" x="0" y="0" width="100" height="100" style="fill:none;"/><rect x="10" y="10" width="80" height="80" style="fill:none;stroke:#000;stroke-width:2px;"/><path d="M25,50l50,0" style="fill:none;stroke:#000;stroke-width:2px;"/><path d="M50,75l0,-50" style="fill:none;stroke:#000;stroke-width:2px;"/></svg>',open:'<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><rect id="open" x="0" y="0" width="100" height="100" style="fill:none;"/><rect x="10" y="10" width="80" height="80" style="fill:none;stroke:#000;stroke-width:2px;"/><path d="M25,50l50,0" style="fill:none;stroke:#000;stroke-width:2px;"/></svg>'}
54 |
55 |
23 |
24 |
25 | /**
54 | * measure distance between two points
55 | * @param {number} x1
56 | * @param {number} y1
57 | * @param {number} x2
58 | * @param {number} y2
59 | */
60 | export function distance(x1, y1, x2, y2) {
61 | return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
62 | }
63 |
64 | /**
65 | * find shortest distance from point to HTMLElement's bounding box
66 | * from: https://gamedev.stackexchange.com/questions/44483/how-do-i-calculate-distance-between-a-point-and-an-axis-aligned-rectangle
67 | * @param {number} x
68 | * @param {number} y
69 | * @param {HTMLElement} element
70 | */
71 | export function distancePointElement(px, py, element) {
72 | const pos = toGlobal(element)
73 | const width = element.offsetWidth
74 | const height = element.offsetHeight
75 | const x = pos.x + width / 2
76 | const y = pos.y + height / 2
77 | const dx = Math.max(Math.abs(px - x) - width / 2, 0)
78 | const dy = Math.max(Math.abs(py - y) - height / 2, 0)
79 | return dx * dx + dy * dy
80 | }
81 |
82 | /**
83 | * determine whether the mouse is inside an element
84 | * @param {HTMLElement} dragging
85 | * @param {HTMLElement} element
86 | */
87 | export function inside(x, y, element) {
88 | const pos = toGlobal(element)
89 | const x1 = pos.x
90 | const y1 = pos.y
91 | const w1 = element.offsetWidth
92 | const h1 = element.offsetHeight
93 | return x >= x1 && x <= x1 + w1 && y >= y1 && y <= y1 + h1
94 | }
95 |
96 | /**
97 | * determines global location of a div
98 | * from https://stackoverflow.com/a/26230989/1955997
99 | * @param {HTMLElement} e
100 | * @returns {PointLike}
101 | */
102 | export function toGlobal(e) {
103 | const box = e.getBoundingClientRect()
104 |
105 | const body = document.body
106 | const docEl = document.documentElement
107 |
108 | const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop
109 | const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft
110 |
111 | const clientTop = docEl.clientTop || body.clientTop || 0
112 | const clientLeft = docEl.clientLeft || body.clientLeft || 0
113 |
114 | const top = box.top + scrollTop - clientTop
115 | const left = box.left + scrollLeft - clientLeft
116 |
117 | return { y: Math.round(top), x: Math.round(left) }
118 | }
119 |
120 | /**
121 | * @typedef {object} PointLike
122 | * @property {number} x
123 | * @property {number} y
124 | */
125 |
126 | /**
127 | * combines options and default options
128 | * @param {object} options
129 | * @param {object} defaults
130 | * @returns {object} options+defaults
131 | */
132 | export function options(options, defaults) {
133 | options = options || {}
134 | for (let option in defaults) {
135 | options[option] = typeof options[option] !== 'undefined' ? options[option] : defaults[option]
136 | }
137 | return options
138 | }
139 |
140 | /**
141 | * set a style on an element
142 | * @param {HTMLElement} element
143 | * @param {string} style
144 | * @param {(string|string[])} value - single value or list of possible values (test each one in order to see if it works)
145 | */
146 | export function style(element, style, value) {
147 | if (Array.isArray(value)) {
148 | for (let entry of value) {
149 | element.style[style] = entry
150 | if (element.style[style] === entry) {
151 | break
152 | }
153 | }
154 | } else {
155 | element.style[style] = value
156 | }
157 | }
158 |
159 | /**
160 | * calculate percentage of overlap between two boxes
161 | * from https://stackoverflow.com/a/21220004/1955997
162 | * @param {number} xa1
163 | * @param {number} ya1
164 | * @param {number} xa2
165 | * @param {number} xa2
166 | * @param {number} xb1
167 | * @param {number} yb1
168 | * @param {number} xb2
169 | * @param {number} yb2
170 | */
171 | export function percentage(xa1, ya1, xa2, ya2, xb1, yb1, xb2, yb2) {
172 | const sa = (xa2 - xa1) * (ya2 - ya1)
173 | const sb = (xb2 - xb1) * (yb2 - yb1)
174 | const si = Math.max(0, Math.min(xa2, xb2) - Math.max(xa1, xb1)) * Math.max(0, Math.min(ya2, yb2) - Math.max(ya1, yb1))
175 | const union = sa + sb - si
176 | if (union !== 0) {
177 | return si / union
178 | } else {
179 | return 0
180 | }
181 | }
182 |
183 | export function removeChildren(element) {
184 | while (element.firstChild) {
185 | element.firstChild.remove()
186 | }
187 | }
188 |
189 | export function html(options) {
190 | options = options || {}
191 | const object = document.createElement(options.type || 'div')
192 | if (options.parent) {
193 | options.parent.appendChild(object)
194 | }
195 | if (options.defaultStyles) {
196 | styles(object, options.defaultStyles)
197 | }
198 | if (options.styles) {
199 | styles(object, options.styles)
200 | }
201 | if (options.html) {
202 | object.innerHTML = options.html
203 | }
204 | if (options.id) {
205 | object.id = options.id
206 | }
207 | return object
208 | }
209 |
210 | export function styles(object, styles) {
211 | for (let style in styles) {
212 | if (Array.isArray(styles[style])) {
213 | for (let entry of styles[style]) {
214 | object.style[style] = entry
215 | if (object.style[style] === entry) {
216 | break
217 | }
218 | }
219 | } else {
220 | object.style[style] = styles[style]
221 | }
222 | }
223 | }
224 |
225 | export function getChildIndex(parent, child) {
226 | let index = 0
227 | for (let entry of parent.children) {
228 | if (entry === child) {
229 | return index
230 | }
231 | index++
232 | }
233 | return -1
234 | }
235 |
236 |
23 |
24 |
25 | | Static Public Summary | ||
| 59 | public 60 | 61 | 62 | 63 | 64 | | 65 |
66 |
67 |
74 | 68 | 69 | 70 | 71 | defaults: {"children": string, "parent": string, "name": string, "data": string, "expanded": string, "move": boolean, "indentation": number, "nameStyles": *, "threshold": number, "indicatorStyles": *, "expandStyles": *, "holdTime": number, "expandOnClick": boolean, "dragOpacity": number} 72 | 73 |
75 |
76 |
77 |
78 |
79 | |
80 | 81 | 82 | 83 | | 84 |
| 87 | public 88 | 89 | 90 | 91 | 92 | | 93 |
94 |
102 |
103 |
104 |
105 |
106 |
107 | |
108 | 109 | 110 | 111 | | 112 |
import {defaults} from 'yy-tree/src/defaults.js'import {icons} from 'yy-tree/src/icons.js'=h&&(b+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(x){E.console&&console.log(x&&x.stack||x)}}var E=window,C=["break,continue,do,else,for,if,return,while"], 34 | F=[[C,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"], 35 | O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"], 36 | F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,function,get,implements,instanceof,interface,let,null,set,undefined,var,with,yield,Infinity,NaN"],Q=[C,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[C,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],C=[C,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"], 37 | S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=y({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,C],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),I={};t(X,["default-code"]);t(G([],[["pln",/^[^]+/],["dec", 38 | /^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^
23 |
24 |
25 | import Events from 'eventemitter3'
40 | import clicked from 'clicked'
41 |
42 | import { Input } from './input'
43 | import { defaults, styleDefaults } from './defaults'
44 | import * as utils from './utils'
45 | import { icons } from './icons'
46 |
47 | export class Tree extends Events {
48 | /**
49 | * Create Tree
50 | * @param {TreeData} tree - data for tree (see readme for description)
51 | * @param {object} [options]
52 | * @param {(HTMLElement|string)} [options.element] if a string then document.querySelector(element); if empty, it creates an element
53 | * @param {(HTMLElement|string)} [options.parent] appends the element to this parent (if string then document.querySelector(parent))
54 | * @param {boolean} [options.move=true] drag tree to rearrange
55 | * @param {boolean} [options.select=true] click to select node (if false then nodes are not selected and tree.selection is always null)
56 | * @param {number} [options.indentation=20] number of pixels to indent for each level
57 | * @param {number} [options.threshold=10] number of pixels to move to start a drag
58 | * @param {number} [options.holdTime=2000] number of milliseconds to press and hold name before editing starts (set to 0 to disable)
59 | * @param {boolean} [options.expandOnClick=true] expand and collapse node on click without drag except (will select before expanding if select=true)
60 | * @param {number} [options.dragOpacity=0.75] opacity setting for dragged item
61 | * @param {string} [options.prefixClassName=yy-tree] first part of className for all DOM objects (e.g., yy-tree, yy-tree-indicator)
62 | * @param {boolean} [options.addStyles=true] attaches a style sheet with default and overridden styles; set to false to use your own stylesheet
63 | * @param {object} [styles]
64 | * @param {string[]} [styles.nameStyles] use these to override individual styles for the name (will be included in the attached stylesheet)
65 | * @param {string[]} [styles.contentStyles] use these to override individual styles for the content (will be included in the attached stylesheet)
66 | * @param {string[]} [styles.indicatorStyles] use these to override individual styles for the move-line indicator (will be included in the attached stylesheet)
67 | * @param {string[]} [styles.selectedStyles] use these to override individual styles for the selected item (will be included in the attached stylesheet)
68 | * @fires render
69 | * @fires clicked
70 | * @fires expand
71 | * @fires collapse
72 | * @fires name-change
73 | * @fires move
74 | * @fires move-pending
75 | * @fires update
76 | */
77 | constructor(tree, options, styles) {
78 | super()
79 | this._options = utils.options(options, defaults)
80 | this._input = new Input(this)
81 | if (typeof this._options.element === 'undefined') {
82 | /**
83 | * Main div holding tree
84 | * @type {HTMLElement}
85 | */
86 | this.element = document.createElement('div')
87 | } else {
88 | this.element = utils.el(this._options.element)
89 | }
90 | if (this._options.parent) {
91 | utils.el(this._options.parent).appendChild(this.element)
92 | }
93 | this.element.classList.add(this.prefixClassName)
94 | this.element.data = tree
95 | if (this._options.addStyles !== false) {
96 | this._addStyles(styles)
97 | }
98 | this.update()
99 | }
100 |
101 | /**
102 | * Selected data
103 | * @type {*}
104 | */
105 | get selection() {
106 | return this._selection.data
107 | }
108 | set selection(data) {
109 | }
110 |
111 | /**
112 | * className's prefix (e.g., "yy-tree"-content)
113 | * @type {string}
114 | */
115 | get prefixClassName() {
116 | return this._options.prefixClassName
117 | }
118 | set prefixClassName(value) {
119 | if (value !== this._options.prefixClassName) {
120 | this._options.prefixClassName = value
121 | this.update()
122 | }
123 | }
124 |
125 | /**
126 | * indentation for tree
127 | * @type {number}
128 | */
129 | get indentation() {
130 | return this._options.indentation
131 | }
132 | set indentation(value) {
133 | if (value !== this._options.indentation) {
134 | this._options.indentation = value
135 | this._input._indicatorMarginLeft = value + 'px'
136 | this.update()
137 | }
138 | }
139 |
140 | /**
141 | * number of milliseconds to press and hold name before editing starts (set to 0 to disable)
142 | * @type {number}
143 | */
144 | get holdTime() {
145 | return this._options.holdTime
146 | }
147 | set holdTime(value) {
148 | if (value !== this._options.holdTime) {
149 | this._options.holdTime = value
150 | }
151 | }
152 |
153 | /**
154 | * whether tree may be rearranged
155 | * @type {boolean}
156 | */
157 | get move() {
158 | return this._options.move
159 | }
160 | set move(value) {
161 | this._options.move = value
162 | }
163 |
164 | /**
165 | * expand and collapse node on click without drag except (will select before expanding if select=true)
166 | * @type {boolean}
167 | */
168 | get expandOnClick() {
169 | return this._options.expandOnClick
170 | }
171 | set expandOnClick(value) {
172 | this._options.expandOnClick = value
173 | }
174 |
175 | /**
176 | * click to select node (if false then nodes are not selected and tree.selection is always null)
177 | * @type {boolean}
178 | */
179 | get select() {
180 | return this._options.select
181 | }
182 | set select(value) {
183 | this._options.select = value
184 | }
185 |
186 | /**
187 | * opacity setting for dragged item
188 | * @type {number}
189 | */
190 | get dragOpacity() {
191 | return this._options.dragOpacity
192 | }
193 | set dragOpacity(value) {
194 | this._options.dragOpacity = value
195 | }
196 |
197 | _leaf(data, level) {
198 | const leaf = utils.html({ className: `${this.prefixClassName}-leaf` })
199 | leaf.isLeaf = true
200 | leaf.data = data
201 | leaf.content = utils.html({ parent: leaf, className: `${this.prefixClassName}-content` })
202 | leaf.style.marginLeft = this.indentation + 'px'
203 | leaf.icon = utils.html({
204 | parent: leaf.content,
205 | html: data.expanded ? icons.open : icons.closed,
206 | className: `${this.prefixClassName}-expand`
207 | })
208 | leaf.name = utils.html({ parent: leaf.content, html: data.name, className: `${this.prefixClassName}-name` })
209 | leaf.name.addEventListener('mousedown', e => this._input._down(e))
210 | leaf.name.addEventListener('touchstart', e => this._input._down(e))
211 | for (let child of data.children) {
212 | const add = this._leaf(child, level + 1)
213 | add.data.parent = data
214 | leaf.appendChild(add)
215 | if (!data.expanded) {
216 | add.style.display = 'none'
217 | }
218 | }
219 | if (this._getChildren(leaf, true).length === 0) {
220 | this._hideIcon(leaf)
221 | }
222 | clicked(leaf.icon, () => this.toggleExpand(leaf))
223 | this.emit('render', leaf, this)
224 | return leaf
225 | }
226 |
227 | _getChildren(leaf, all) {
228 | leaf = leaf || this.element
229 | const children = []
230 | for (let child of leaf.children) {
231 | if (child.isLeaf && (all || child.style.display !== 'none')) {
232 | children.push(child)
233 | }
234 | }
235 | return children
236 | }
237 |
238 | _hideIcon(leaf) {
239 | if (leaf.isLeaf) {
240 | leaf.icon.style.opacity = 0
241 | leaf.icon.style.cursor = 'unset'
242 | }
243 | }
244 |
245 | _showIcon(leaf) {
246 | if (leaf.isLeaf) {
247 | leaf.icon.style.opacity = 1
248 | leaf.icon.style.cursor = this._options.cursorExpand
249 | }
250 | }
251 |
252 | /** Expands all leaves */
253 | expandAll() {
254 | this._expandChildren(this.element)
255 | }
256 |
257 | _expandChildren(leaf) {
258 | for (let child of this._getChildren(leaf, true)) {
259 | this.expand(child)
260 | this._expandChildren(child)
261 | }
262 | }
263 |
264 | /** Collapses all leaves */
265 | collapseAll() {
266 | this._collapseChildren(this)
267 | }
268 |
269 | _collapseChildren(leaf) {
270 | for (let child of this._getChildren(leaf, true)) {
271 | this.collapse(child)
272 | this._collapseChildren(child)
273 | }
274 | }
275 |
276 | /**
277 | * Toggles a leaf
278 | * @param {HTMLElement} leaf
279 | */
280 | toggleExpand(leaf) {
281 | if (leaf.icon.style.opacity !== '0') {
282 | if (leaf.data.expanded) {
283 | this.collapse(leaf)
284 | } else {
285 | this.expand(leaf)
286 | }
287 | }
288 | }
289 |
290 | /**
291 | * Expands a leaf
292 | * @param {HTMLElement} leaf
293 | */
294 | expand(leaf) {
295 | if (leaf.isLeaf) {
296 | const children = this._getChildren(leaf, true)
297 | if (children.length) {
298 | for (let child of children) {
299 | child.style.display = 'block'
300 | }
301 | leaf.data.expanded = true
302 | leaf.icon.innerHTML = icons.open
303 | this.emit('expand', leaf, this)
304 | this.emit('update', leaf, this)
305 | }
306 | }
307 | }
308 |
309 | /**
310 | * Collapses a leaf
311 | * @param {HTMLElement} leaf
312 | */
313 | collapse(leaf) {
314 | if (leaf.isLeaf) {
315 | const children = this._getChildren(leaf, true)
316 | if (children.length) {
317 | for (let child of children) {
318 | child.style.display = 'none'
319 | }
320 | leaf.data.expanded = false
321 | leaf.icon.innerHTML = icons.closed
322 | this.emit('collapse', leaf, this)
323 | this.emit('update', leaf, this)
324 | }
325 | }
326 | }
327 |
328 | /** call this after tree's data has been updated outside of this library */
329 | update() {
330 | const scroll = this.element.scrollTop
331 | utils.removeChildren(this.element)
332 | for (let leaf of this.element.data.children) {
333 | const add = this._leaf(leaf, 0)
334 | add.data.parent = this.element.data
335 | this.element.appendChild(add)
336 | }
337 | this.element.scrollTop = scroll + 'px'
338 | }
339 |
340 | /**
341 | * edit the name entry using the data
342 | * @param {object} data element of leaf
343 | */
344 | editData(data) {
345 | const children = this._getChildren(null, true)
346 | for (let child of children) {
347 | if (child.data === data) {
348 | this.edit(child)
349 | }
350 | }
351 | }
352 |
353 | /**
354 | * edit the name entry using the created element
355 | * @param {HTMLElement} leaf
356 | */
357 | edit(leaf) {
358 | this._editing = leaf
359 | this._editInput = utils.html({ parent: this._editing.name.parentNode, type: 'input', className: `${this.prefixClassName}-name` })
360 | const computed = window.getComputedStyle(this._editing.name)
361 | this._editInput.style.boxSizing = 'content-box'
362 | this._editInput.style.fontFamily = computed.getPropertyValue('font-family')
363 | this._editInput.style.fontSize = computed.getPropertyValue('font-size')
364 | this._editInput.value = this._editing.name.innerText
365 | this._editInput.setSelectionRange(0, this._editInput.value.length)
366 | this._editInput.focus()
367 | this._editInput.addEventListener('update', () => {
368 | this.nameChange(this._editing, this._editInput.value)
369 | this._holdClose()
370 | })
371 | this._editInput.addEventListener('keyup', (e) => {
372 | if (e.code === 'Escape') {
373 | this._holdClose()
374 | }
375 | if (e.code === 'Enter') {
376 | this.nameChange(this._editing, this._editInput.value)
377 | this._holdClose()
378 | }
379 | })
380 | this._editing.name.style.display = 'none'
381 | this._target = null
382 | }
383 |
384 | _holdClose() {
385 | if (this._editing) {
386 | this._editInput.remove()
387 | this._editing.name.style.display = 'block'
388 | this._editing = this._editInput = null
389 | }
390 | }
391 |
392 | /**
393 | * change the name of a leaf
394 | * @param {HTMLElement} leaf
395 | * @param {string} name
396 | */
397 | nameChange(leaf, name) {
398 | leaf.data.name = this._input.value
399 | leaf.name.innerHTML = name
400 | this.emit('name-change', leaf, this._input.value, this)
401 | this.emit('update', leaf, this)
402 | }
403 |
404 | /**
405 | * Find a leaf based using its data
406 | * @param {object} leaf
407 | * @param {HTMLElement} [root=this.element]
408 | */
409 | getLeaf(leaf, root = this.element) {
410 | this.findInTree(root, data => data === leaf)
411 | }
412 |
413 | /**
414 | * call the callback function on each node; returns the node if callback === true
415 | * @param {*} leaf data
416 | * @param {function} callback
417 | */
418 | findInTree(leaf, callback) {
419 | for (const child of leaf.children) {
420 | if (callback(child)) {
421 | return child
422 | }
423 | const find = this.findInTree(child, callback)
424 | if (find) {
425 | return find
426 | }
427 | }
428 | }
429 |
430 | _getFirstChild(element, all) {
431 | const children = this._getChildren(element, all)
432 | if (children.length) {
433 | return children[0]
434 | }
435 | }
436 |
437 | _getLastChild(element, all) {
438 | const children = this._getChildren(element, all)
439 | if (children.length) {
440 | return children[children.length - 1]
441 | }
442 | }
443 |
444 | _getParent(element) {
445 | element = element.parentNode
446 | while (element.style.display === 'none') {
447 | element = element.parentNode
448 | }
449 | return element
450 | }
451 |
452 | _addStyles(userStyles) {
453 | const styles = utils.options(userStyles, styleDefaults)
454 | let s = `.${this.prefixClassName}-name{`
455 | for (const key in styles.nameStyles) {
456 | s += `${key}:${styles.nameStyles[key]};`
457 | }
458 | s += `}.${this.prefixClassName}-content{`
459 | for (const key in styles.contentStyles) {
460 | s += `${key}:${styles.contentStyles[key]};`
461 | }
462 | s += `}.${this.prefixClassName}-indicator{`
463 | for (const key in styles.indicatorStyles) {
464 | s += `${key}:${styles.indicatorStyles[key]};`
465 | }
466 | s += `}.${this.prefixClassName}-expand{`
467 | for (const key in styles.expandStyles) {
468 | s += `${key}:${styles.expandStyles[key]};`
469 | }
470 | s += `}.${this.prefixClassName}-select{`
471 | for (const key in styles.selectStyles) {
472 | s += `${key}:${styles.selectStyles[key]};`
473 | }
474 | s + '}'
475 | const style = document.createElement('style')
476 | style.innerHTML = s
477 | document.head.appendChild(style)
478 | }
479 | }
480 |
481 | /**
482 | * @typedef {Object} Tree~TreeData
483 | * @property {TreeData[]} children
484 | * @property {string} name
485 | * @property {parent} [parent] if not provided then will traverse tree to find parent
486 | */
487 |
488 | /**
489 | * trigger when expand is called either through UI interaction or Tree.expand()
490 | * @event Tree~expand
491 | * @type {object}
492 | * @property {HTMLElement} tree element
493 | * @property {Tree} Tree
494 | */
495 |
496 | /**
497 | * trigger when collapse is called either through UI interaction or Tree.expand()
498 | * @event Tree~collapse
499 | * @type {object}
500 | * @property {HTMLElement} tree element
501 | * @property {Tree} Tree
502 | */
503 |
504 | /**
505 | * trigger when name is change either through UI interaction or Tree.nameChange()
506 | * @event Tree~name-change
507 | * @type {object}
508 | * @property {HTMLElement} tree element
509 | * @property {string} name
510 | * @property {Tree} Tree
511 | */
512 |
513 | /**
514 | * trigger when a leaf is picked up through UI interaction
515 | * @event Tree~move-pending
516 | * @type {object}
517 | * @property {HTMLElement} tree element
518 | * @property {Tree} Tree
519 | */
520 |
521 | /**
522 | * trigger when a leaf's location is changed
523 | * @event Tree~move
524 | * @type {object}
525 | * @property {HTMLElement} tree element
526 | * @property {Tree} Tree
527 | */
528 |
529 | /**
530 | * trigger when a leaf is clicked and not dragged or held
531 | * @event Tree~clicked
532 | * @type {object}
533 | * @property {HTMLElement} tree element
534 | * @property {UIEvent} event
535 | * @property {Tree} Tree
536 | */
537 |
538 | /**
539 | * trigger when a leaf is changed (i.e., moved, name-change)
540 | * @event Tree~update
541 | * @type {object}
542 | * @property {HTMLElement} tree element
543 | * @property {Tree} Tree
544 | */
545 |
546 | /**
547 | * trigger when a leaf's div is created
548 | * @event Tree~render
549 | * @type {object}
550 | * @property {HTMLElement} tree element
551 | * @property {Tree} Tree
552 | */
553 |
554 |