├── .gitignore ├── _config.yml ├── favicon.ico ├── webfonts ├── fa-brands-400.ttf ├── fa-solid-900.ttf ├── fa-brands-400.woff2 ├── fa-regular-400.ttf ├── fa-regular-400.woff2 ├── fa-solid-900.woff2 ├── fa-v4compatibility.ttf └── fa-v4compatibility.woff2 ├── style.css ├── css ├── tooltip.min.css ├── treeSortable.css └── all.min.css ├── index.html ├── js ├── tooltip.min.js ├── script.js └── treeSortable.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/favicon.ico -------------------------------------------------------------------------------- /webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamed/treeSortable/HEAD/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | #tree-2, 2 | #tree-level-1 { 3 | padding: 0.1em 0; 4 | list-style: none; 5 | margin: 0; 6 | } 7 | 8 | .wrapper { 9 | display: flex; 10 | gap: 20px; 11 | margin-top: 100px; 12 | } 13 | 14 | .wrapper ul { 15 | width: 100%; 16 | 17 | border-radius: 6px; 18 | padding: 20px; 19 | } 20 | -------------------------------------------------------------------------------- /css/tooltip.min.css: -------------------------------------------------------------------------------- 1 | .b-tooltip { 2 | border: 3px solid #fff; 3 | display: inline-block; 4 | font-size: 0.875em; 5 | padding: 0.75em; 6 | position: absolute; 7 | text-align: center; 8 | z-index: 999999; 9 | } 10 | .b-tooltip-light { 11 | background: #eaeaea; 12 | color: #242424; 13 | } 14 | .b-tooltip-dark { 15 | background: #000; 16 | color: #fff; 17 | } 18 | 19 | .wrap { 20 | margin: 0 auto; 21 | width: 800px; 22 | } 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tree Sortable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /js/tooltip.min.js: -------------------------------------------------------------------------------- 1 | Tooltip = function (t) { 2 | function e(t, e, o, a) { 3 | var i, 4 | s, 5 | r = t.getBoundingClientRect(); 6 | switch ((console.log(a), o)) { 7 | case 'left': 8 | (i = parseInt(r.left) - n - e.offsetWidth), parseInt(r.left) - e.offsetWidth < 0 && (i = n); 9 | break; 10 | case 'right': 11 | (i = r.right + n), 12 | parseInt(r.right) + e.offsetWidth > document.documentElement.clientWidth && 13 | (i = document.documentElement.clientWidth - e.offsetWidth - n); 14 | break; 15 | default: 16 | case 'center': 17 | i = parseInt(r.left) + (t.offsetWidth - e.offsetWidth) / 2; 18 | } 19 | switch (a) { 20 | case 'center': 21 | s = (parseInt(r.top) + parseInt(r.bottom)) / 2 - e.offsetHeight / 2; 22 | break; 23 | case 'bottom': 24 | s = parseInt(r.bottom) + n; 25 | break; 26 | default: 27 | case 'top': 28 | s = parseInt(r.top) - e.offsetHeight - n; 29 | } 30 | (i = 0 > i ? parseInt(r.left) : i), 31 | (s = 0 > s ? parseInt(r.bottom) + n : s), 32 | (e.style.left = i + 'px'), 33 | (e.style.top = s + pageYOffset + 'px'); 34 | } 35 | var o = t.theme || 'dark', 36 | a = t.delay || 0, 37 | n = t.distance || 10; 38 | document.body.addEventListener('mouseover', function (t) { 39 | if (t.target.hasAttribute('data-tooltip')) { 40 | var a = document.createElement('div'); 41 | (a.className = 'b-tooltip b-tooltip-' + o), 42 | (a.innerHTML = t.target.getAttribute('data-tooltip')), 43 | document.body.appendChild(a); 44 | var n = t.target.getAttribute('data-position') || 'center top', 45 | i = n.split(' ')[0]; 46 | (posVertical = n.split(' ')[1]), e(t.target, a, i, posVertical); 47 | } 48 | }), 49 | document.body.addEventListener('mouseout', function (t) { 50 | t.target.hasAttribute('data-tooltip') && 51 | setTimeout(function () { 52 | document.body.removeChild(document.querySelector('.b-tooltip')); 53 | }, a); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /js/script.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | const dataLeft = [ 3 | { 4 | id: 1, 5 | parent_id: 0, 6 | title: 'Branch 1', 7 | level: 1, 8 | }, 9 | { 10 | id: 2, 11 | parent_id: 1, 12 | title: 'Branch 1', 13 | level: 2, 14 | }, 15 | { 16 | id: 3, 17 | parent_id: 1, 18 | title: 'Branch 3', 19 | level: 2, 20 | }, 21 | { 22 | id: 4, 23 | parent_id: 3, 24 | title: 'Branch 4', 25 | level: 3, 26 | }, 27 | { 28 | id: 5, 29 | parent_id: 3, 30 | title: 'Branch 5', 31 | level: 3, 32 | }, 33 | { 34 | id: 6, 35 | parent_id: 1, 36 | title: 'Branch 6', 37 | level: 2, 38 | }, 39 | { 40 | id: 7, 41 | parent_id: 0, 42 | title: 'Branch 7', 43 | level: 1, 44 | }, 45 | { 46 | id: 8, 47 | parent_id: 1, 48 | title: 'Branch 8', 49 | level: 2, 50 | }, 51 | { 52 | id: 9, 53 | parent_id: 1, 54 | title: 'Branch 9', 55 | level: 2, 56 | }, 57 | { 58 | id: 10, 59 | parent_id: 9, 60 | title: 'Branch 10', 61 | level: 3, 62 | }, 63 | ]; 64 | 65 | const dataRight = [ 66 | { 67 | id: 1, 68 | parent_id: 0, 69 | title: 'Item 1', 70 | level: 1, 71 | }, 72 | { 73 | id: 2, 74 | parent_id: 1, 75 | title: 'Item 4', 76 | level: 2, 77 | }, 78 | { 79 | id: 3, 80 | parent_id: 0, 81 | title: 'Item 2', 82 | level: 1, 83 | }, 84 | { 85 | id: 4, 86 | parent_id: 3, 87 | title: 'Item 3', 88 | level: 2, 89 | }, 90 | ]; 91 | 92 | const leftTreeId = '#left-tree'; 93 | const leftSortable = new TreeSortable({ 94 | treeSelector: leftTreeId, 95 | }); 96 | const $leftTree = $(leftTreeId); 97 | const $content = dataLeft.map(leftSortable.createBranch); 98 | $leftTree.html($content); 99 | leftSortable.run(); 100 | 101 | const delay = () => { 102 | return new Promise(resolve => { 103 | setTimeout(() => { 104 | resolve(); 105 | }, 1000); 106 | }); 107 | }; 108 | 109 | leftSortable.onSortCompleted(async (event, ui) => { 110 | await delay(); 111 | console.log('left tree', ui.item); 112 | }); 113 | 114 | leftSortable.addListener('click', '.add-child', function (event, instance) { 115 | event.preventDefault(); 116 | instance.addChildBranch($(event.target)); 117 | }); 118 | 119 | leftSortable.addListener('click', '.add-sibling', function (event, instance) { 120 | event.preventDefault(); 121 | instance.addSiblingBranch($(event.target)); 122 | }); 123 | 124 | leftSortable.addListener('click', '.remove-branch', function (event, instance) { 125 | event.preventDefault(); 126 | 127 | const confirm = window.confirm('Are you sure you want to delete this branch?'); 128 | if (!confirm) { 129 | return; 130 | } 131 | instance.removeBranch($(event.target)); 132 | }); 133 | 134 | const rightTreeId = '#right-tree'; 135 | const rightSortable = new TreeSortable({ 136 | treeSelector: rightTreeId, 137 | }); 138 | const $rightTree = $(rightTreeId); 139 | const $rightContent = dataRight.map(rightSortable.createBranch); 140 | $rightTree.html($rightContent); 141 | rightSortable.run(); 142 | rightSortable.onSortCompleted(async (event, ui) => { 143 | await delay(); 144 | console.log('right tree', ui.item); 145 | }); 146 | 147 | rightSortable.addListener('click', '.add-child', function (event, instance) { 148 | instance.addChildBranch($(event.target)); 149 | }); 150 | rightSortable.addListener('click', '.add-sibling', function (event, instance) { 151 | instance.addSiblingBranch($(event.target)); 152 | }); 153 | 154 | tippy('[data-tippy-content]'); 155 | }); 156 | -------------------------------------------------------------------------------- /css/treeSortable.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::after, 3 | ::before { 4 | box-sizing: border-box; 5 | } 6 | html { 7 | -moz-tab-size: 4; 8 | tab-size: 4; 9 | line-height: 1.15; 10 | -webkit-text-size-adjust: 100%; 11 | } 12 | 13 | body, 14 | input, 15 | select, 16 | textarea, 17 | button, 18 | #admin .redactor-styles { 19 | font-family: 'Inter', system-ui, -apple-system, segoe ui, Roboto, Helvetica, Arial, sans-serif, apple color emoji, 20 | segoe ui emoji; 21 | } 22 | hr { 23 | height: 0; 24 | color: inherit; 25 | } 26 | abbr[title] { 27 | text-decoration: underline dotted; 28 | } 29 | b, 30 | strong { 31 | font-weight: bolder; 32 | } 33 | code, 34 | kbd, 35 | pre, 36 | samp { 37 | font-family: ui-monospace, SFMono-Regular, Consolas, liberation mono, Menlo, monospace; 38 | font-size: 1em; 39 | } 40 | small { 41 | font-size: 80%; 42 | } 43 | sub, 44 | sup { 45 | font-size: 75%; 46 | line-height: 0; 47 | position: relative; 48 | vertical-align: baseline; 49 | } 50 | sub { 51 | bottom: -0.25em; 52 | } 53 | sup { 54 | top: -0.5em; 55 | } 56 | table { 57 | text-indent: 0; 58 | border-color: inherit; 59 | } 60 | button, 61 | input, 62 | optgroup, 63 | select, 64 | textarea { 65 | font-family: inherit; 66 | font-size: 100%; 67 | line-height: 1.15; 68 | margin: 0; 69 | } 70 | button, 71 | select { 72 | text-transform: none; 73 | } 74 | [type='button'], 75 | [type='reset'], 76 | [type='submit'], 77 | button { 78 | -webkit-appearance: button; 79 | } 80 | ::-moz-focus-inner { 81 | border-style: none; 82 | padding: 0; 83 | } 84 | :-moz-focusring { 85 | outline: 1px dotted ButtonText; 86 | } 87 | :-moz-ui-invalid { 88 | box-shadow: none; 89 | } 90 | legend { 91 | padding: 0; 92 | } 93 | progress { 94 | vertical-align: baseline; 95 | } 96 | ::-webkit-inner-spin-button, 97 | ::-webkit-outer-spin-button { 98 | height: auto; 99 | } 100 | [type='search'] { 101 | -webkit-appearance: textfield; 102 | outline-offset: -2px; 103 | } 104 | ::-webkit-search-decoration { 105 | -webkit-appearance: none; 106 | } 107 | ::-webkit-file-upload-button { 108 | -webkit-appearance: button; 109 | font: inherit; 110 | } 111 | summary { 112 | display: list-item; 113 | } 114 | ul { 115 | padding: 0.1em 0; 116 | list-style: none; 117 | margin: 0; 118 | } 119 | 120 | body { 121 | padding: 0; 122 | margin: 0; 123 | background: #f4f6f9; 124 | font-family: 'Inter', system-ui, -apple-system, segoe ui, Roboto, Helvetica, Arial, sans-serif, apple color emoji, 125 | segoe ui emoji; 126 | } 127 | 128 | .container { 129 | max-width: 1200px; 130 | margin: 0 auto; 131 | } 132 | 133 | .form-input { 134 | width: 100%; 135 | border: 1px solid #ddd; 136 | outline: none; 137 | padding: 8px; 138 | border-radius: 6px; 139 | } 140 | 141 | .button { 142 | background: #fff; 143 | border: none; 144 | outline: none; 145 | cursor: pointer; 146 | transition: 0.3s; 147 | padding: 5px; 148 | width: 20px; 149 | height: 20px; 150 | } 151 | 152 | .button:hover { 153 | transform: scale(1.2); 154 | } 155 | 156 | .tree-branch .branch-editor { 157 | display: none; 158 | } 159 | 160 | .tree-branch { 161 | margin-bottom: 0; 162 | position: relative; 163 | user-select: none; 164 | max-height: 58px; 165 | } 166 | 167 | .tree-branch > .contents .branch-wrapper { 168 | display: flex; 169 | align-items: center; 170 | justify-content: space-between; 171 | width: 100%; 172 | background: #fff; 173 | border: 1px solid #fff; 174 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.15); 175 | border-radius: 3px; 176 | min-height: 20px; 177 | max-width: 450px; 178 | width: 100%; 179 | position: relative; 180 | padding: 10px 15px; 181 | height: auto; 182 | gap: 12px; 183 | 184 | line-height: 2.3076923; 185 | overflow: hidden; 186 | word-wrap: break-word; 187 | } 188 | 189 | .tree-branch > .contents .branch-wrapper .left-sidebar { 190 | display: flex; 191 | gap: 12px; 192 | align-items: center; 193 | max-width: 280px; 194 | width: 100%; 195 | } 196 | 197 | .right-sidebar { 198 | opacity: 0; 199 | transition: 0.3s; 200 | } 201 | 202 | .branch-wrapper:hover .right-sidebar { 203 | opacity: 1; 204 | } 205 | 206 | .tree-branch > .contents .branch-wrapper .left-sidebar { 207 | cursor: pointer; 208 | } 209 | 210 | .tree-branch > .contents { 211 | clear: both; 212 | line-height: 1.5; 213 | position: relative; 214 | margin: 10px 0 0; 215 | } 216 | 217 | .contents .branch-drag-handler { 218 | cursor: move; 219 | } 220 | 221 | .branch-drag-handler .icon { 222 | color: #504e4e; 223 | margin-right: 5px; 224 | } 225 | 226 | .sortable-placeholder { 227 | border: 1px dashed rgb(63, 63, 63); 228 | height: 35px; 229 | max-width: 450px; 230 | width: 100%; 231 | margin-top: 10px; 232 | } 233 | 234 | .tree-branch.ui-sortable-helper .contents { 235 | margin-top: 0; 236 | } 237 | 238 | .tree-branch.ui-sortable-helper .children-bus .contents { 239 | margin-top: 10px; 240 | } 241 | 242 | .tree-branch .children-bus:empty { 243 | display: none; 244 | } 245 | 246 | .tree-branch { 247 | margin-left: var(--tree-sortable-branch-left-shift); 248 | } 249 | .children-bus { 250 | margin-left: var(--tree-sortable-children-left-shift); 251 | } 252 | 253 | .ui-sortable-placeholder { 254 | margin-left: var(--tree-sortable-branch-left-shift); 255 | } 256 | 257 | .branch-path { 258 | display: block; 259 | position: absolute; 260 | width: var(--tree-sortable-depth); 261 | min-height: 72px; 262 | bottom: 50%; 263 | left: -12px; 264 | border: 2px solid #565656; 265 | border-top: 0; 266 | border-right: 0; 267 | padding: 4px 0 0; 268 | padding-top: 3px; 269 | border-bottom-left-radius: 6px; 270 | z-index: -1; 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tree Sortable 2 | A drag and drop list item sorting and level changing library. 3 | 4 | ![treeSortableBanner](https://user-images.githubusercontent.com/5783354/163663867-9a404565-1550-4eac-afb7-a1f50fddd7c8.gif) 5 | 6 | ## Motivation 7 | The `jQuery-ui's` sortable plugin allows us to sort items vertically or horizontally. But sometimes we need some items to make a child of another item and also need to sort a parent with all of the children items. 8 | This is the reason why I've created this library for managing the parent-children relationship of list items and fully by drag and drop. 9 | 10 | ## Installation 11 | Download the required files from the [Github](https://github.com/ahamed/treeSortable/archive/refs/heads/master.zip). Use the `treeSortable.js` and `treeSortable.css` files to your project. 12 | You also need to use `jQuery` and `jQuery-ui` libraries. 13 | 14 | In the `` tag use the css files. 15 | ```html 16 | 17 | 18 | ``` 19 | 20 | Before the `` tag 21 | ```html 22 | 23 | 24 | 25 | ``` 26 | 27 | --- 28 | 29 | ## Usage 30 | Create a `ul` element with some ID attribute. 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | Then generate an array of object with the exact structure defined below. 37 | In the data structure the ordering is important, the tree will be rendered according to this order. 38 | 39 | ```js 40 | const data = [ 41 | { 42 | id: 1, 43 | level: 1, 44 | parent_id: 0, 45 | title: 'Branch 1' 46 | }, 47 | { 48 | id: 2, 49 | level: 2, 50 | parent_id: 1, 51 | title: 'Branch 2' 52 | }, 53 | . 54 | . 55 | . 56 | ] 57 | ``` 58 | 59 | Now we are gonna create an instance of the tree sortable. And we can pass an options object as the constructor of the `TreeSortable`. 60 | 61 | 62 | ```js 63 | const sortable = new TreeSortable({ 64 | treeSelector: '#tree' 65 | }); 66 | ``` 67 | 68 | Now create the HTML content of the tree from the `data` we've declared earlier, and append the `$content` to the `