mini_tree 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +136 -0
- data/app/controllers/mini_trees_controller.rb +40 -0
- data/app/javascript/controllers/tree_controller.js +252 -0
- data/app/views/application/_mini_tree_title.html.erb +1 -0
- data/app/views/mini_trees/_index.html.erb +34 -0
- data/app/views/mini_trees/_item.html.erb +27 -0
- data/config/routes.rb +3 -0
- data/lib/mini_tree/engine.rb +4 -0
- data/lib/mini_tree/utils.rb +69 -0
- data/lib/mini_tree/version.rb +6 -0
- data/lib/mini_tree/version.rb.bak +5 -0
- data/lib/mini_tree.rb +2 -0
- metadata +130 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cfcc12efe875d58a64a005b8bcf01c4287521f929dafe941e55abfe0774bd64e
|
|
4
|
+
data.tar.gz: 6314c4eecfbe0c7cf0df0b2db8e952515bb3bba14158c94aeb250eb89f5763e4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 264d652696e5c73cfda4b8b6e7094e9b8596d9ba30bf71be3361712845a41929fab44017fda6d6b9834b7c8eecf73ab3c5ca74469def6cb35119dcd1698a21d5
|
|
7
|
+
data.tar.gz: f3139e7a2cd685933b07341c2503aa3ba553c0279cfd6e52d41fed823f1d3eb7e8a0351f83bd7a29c6aa40280e2494133e6f74a9fab2c9aa0c77b03c01b71b0f
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2025 Dittmar Krall (www.matiq.com)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# UNDER CONSTRUCTION
|
|
2
|
+
|
|
3
|
+
# MiniTree
|
|
4
|
+
|
|
5
|
+
[](https://rubygems.org/gems/mini_tree)
|
|
6
|
+
[](https://rubygems.org/gems/mini_tree)
|
|
7
|
+
[](https://github.com/matique/mini_tree/actions/workflows/rake.yml)
|
|
8
|
+
[](https://github.com/standardrb/standard)
|
|
9
|
+
[](http://choosealicense.com/licenses/mit/)
|
|
10
|
+
|
|
11
|
+
_MiniTree_ is a Rails gem to display and handle a treeview.
|
|
12
|
+
It supports moving/reordering items in the treeview,
|
|
13
|
+
collapsing/expanding of a subtree
|
|
14
|
+
and creating/deleting them.
|
|
15
|
+
|
|
16
|
+
Items in the treeview can be enhanced with links
|
|
17
|
+
to trigger actions.
|
|
18
|
+
|
|
19
|
+
_MiniTree_ requires just a basic Rails system.
|
|
20
|
+
Specifically, besides Stimulus no other Javascript package
|
|
21
|
+
(i.e jQuery) is expected.
|
|
22
|
+
Configuration is absent.
|
|
23
|
+
|
|
24
|
+
_MiniTree_ includes a javascript component to handle
|
|
25
|
+
the view on the client side
|
|
26
|
+
as well as code for the server side.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
Some preparation for the usage is required:
|
|
32
|
+
- an additional database table storing the tree structure
|
|
33
|
+
- a _legend_ method in the model
|
|
34
|
+
- a call to _MiniTree_ to initialize the tree structure
|
|
35
|
+
- calls in the model to _miniTree_ during creation, update
|
|
36
|
+
and deletion of an item
|
|
37
|
+
|
|
38
|
+
~~~Ruby
|
|
39
|
+
# ./app/models/<model>.rb
|
|
40
|
+
class <model> < ApplicationRecord
|
|
41
|
+
def legend = "#{name} #{id}" # an example
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ./app/models/<model>_tree.rb
|
|
45
|
+
class <model>Tree < ApplicationRecord
|
|
46
|
+
include MiniTree::Utils
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ./db/migrate/<nnn>_create_<model>_trees.rb
|
|
50
|
+
class Create<Model>Trees < ActiveRecord::Migration[8.0]
|
|
51
|
+
def change
|
|
52
|
+
create_table :<model>_trees do |t|
|
|
53
|
+
t.string :legend
|
|
54
|
+
t.integer :parent_id, index: true
|
|
55
|
+
t.integer :position, null: false, default: 0
|
|
56
|
+
t.boolean :collapsed, default: false
|
|
57
|
+
t.string :kind
|
|
58
|
+
|
|
59
|
+
t.timestamps
|
|
60
|
+
end
|
|
61
|
+
# add_foreign_key :items, :items, column: :parent_id
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
You may specify your view of an item in the treeview:
|
|
67
|
+
~~~Ruby
|
|
68
|
+
# ./app/views/mini_trees/_mini_tree_title.html.erb
|
|
69
|
+
# id and legend are defined
|
|
70
|
+
<%= link_to "action", edit_<model>(id:), class: 'button' %>
|
|
71
|
+
<%= legend %>
|
|
72
|
+
~~~
|
|
73
|
+
|
|
74
|
+
## Refresh
|
|
75
|
+
|
|
76
|
+
~~~Ruby
|
|
77
|
+
<Model>Tree.refresh
|
|
78
|
+
<Model>Tree.refresh_item(<id>, <legend>)
|
|
79
|
+
<Model>Tree.create_item(<id>, <legend>)
|
|
80
|
+
<Model>Tree.del_item(<id>)
|
|
81
|
+
~~~
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
~~~Ruby
|
|
86
|
+
# Example
|
|
87
|
+
<% list = <Model>Tree.all %>
|
|
88
|
+
<%= render "mini_trees/index", locals: {list:} %>
|
|
89
|
+
~~~
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
As usual:
|
|
95
|
+
|
|
96
|
+
~~~~ruby
|
|
97
|
+
# Gemfile
|
|
98
|
+
...
|
|
99
|
+
gem "mini_tree"
|
|
100
|
+
...
|
|
101
|
+
~~~~
|
|
102
|
+
|
|
103
|
+
and run "bundle install".
|
|
104
|
+
|
|
105
|
+
Furthermore, copy manually *app/javascript/controllers/tree_controller.js*
|
|
106
|
+
from the _gem mini-tree_
|
|
107
|
+
into your own _app/javascript/controllers/_ directory.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## System dependencies
|
|
111
|
+
|
|
112
|
+
This software has been developed and tested with:
|
|
113
|
+
- Ubuntu 24.04
|
|
114
|
+
- Ruby 3.4.7
|
|
115
|
+
- Rails 8.1.0
|
|
116
|
+
|
|
117
|
+
See also:
|
|
118
|
+
- ./.github/workflows/rake.yml
|
|
119
|
+
|
|
120
|
+
No particular system dependency is known,
|
|
121
|
+
i.e. _mini_tree_ is expected to run on other systems without trouble.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## Curious
|
|
125
|
+
|
|
126
|
+
There are quite a lot of TreeViews available.
|
|
127
|
+
If you are curious you may search in particular for:
|
|
128
|
+
- jqTree
|
|
129
|
+
- sortableJS
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Copyright (c) 2025 Dittmar Krall (www.matiq.com),
|
|
135
|
+
released as open source under the terms of the
|
|
136
|
+
[MIT license](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class MiniTreesController < ApplicationController
|
|
2
|
+
def sync
|
|
3
|
+
init_klasses(params[:owner])
|
|
4
|
+
id = params[:id]
|
|
5
|
+
value = params[:value]
|
|
6
|
+
|
|
7
|
+
case params[:function]
|
|
8
|
+
when "order"
|
|
9
|
+
client_order = JSON.parse(params[:order])
|
|
10
|
+
client_order.each_with_index do |pair, index|
|
|
11
|
+
id, parent_id = pair
|
|
12
|
+
row = @tree.find(id.to_i)
|
|
13
|
+
row.update!(position: index, parent_id: parent_id.to_i)
|
|
14
|
+
end
|
|
15
|
+
when "leaf2node"
|
|
16
|
+
row = @tree.find_by(id: value.to_i)
|
|
17
|
+
row.update! kind: "node"
|
|
18
|
+
when "node2leaf"
|
|
19
|
+
row = @tree.find_by(id: value.to_i)
|
|
20
|
+
row.update! kind: "leaf"
|
|
21
|
+
when "toggle"
|
|
22
|
+
id = id.to_i
|
|
23
|
+
row = @tree.find_by(id:)
|
|
24
|
+
row.update! collapsed: value
|
|
25
|
+
else
|
|
26
|
+
raise "MiniTreeView: unknown function <#{params}>"
|
|
27
|
+
end
|
|
28
|
+
# head :ok
|
|
29
|
+
render json: {status: "ok"}
|
|
30
|
+
rescue RuntimeError => e
|
|
31
|
+
# head :bad_request
|
|
32
|
+
render json: {status: "error", message: e.message}, status: :unprocessable_entity
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def init_klasses(name)
|
|
38
|
+
@tree = Kernel.const_get("#{name}Tree")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["item", "top"]
|
|
5
|
+
static values = {owner: String}
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.expanded = "\u25BE" // down triangle
|
|
9
|
+
this.collapsed = "\u25b8" // right triangle
|
|
10
|
+
this.itemTargets.forEach(item => this.attach(item))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
attach(item) {
|
|
14
|
+
item.addEventListener("dragstart", e => this.dragStart(e, item))
|
|
15
|
+
item.addEventListener("dragend", e => this.dragEnd(e, item))
|
|
16
|
+
item.addEventListener("dragover", e => this.dragOver(e, item))
|
|
17
|
+
item.addEventListener("drop", e => this.drop(e, item))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dragStart(e, el) {
|
|
21
|
+
e.stopPropagation()
|
|
22
|
+
|
|
23
|
+
// ev.dataTransfer.setData("text/plain", "")
|
|
24
|
+
this.dragged = el
|
|
25
|
+
e.dataTransfer.effectAllowed = "move"
|
|
26
|
+
el.classList.add("dragging")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
dragEnd(e) {
|
|
30
|
+
e.stopPropagation()
|
|
31
|
+
this.dragged.classList.remove("dragging")
|
|
32
|
+
this.dragged = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dragOver(e, el) {
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
drop(e) {
|
|
40
|
+
e.stopPropagation()
|
|
41
|
+
|
|
42
|
+
const dragged = this.dragged
|
|
43
|
+
const target = e.currentTarget
|
|
44
|
+
// this.testUtilities(target)
|
|
45
|
+
|
|
46
|
+
if (dragged == target) {
|
|
47
|
+
this.doGrouping(target)
|
|
48
|
+
} else {
|
|
49
|
+
this.drop2(e, dragged, target)
|
|
50
|
+
this.sendOrder(target)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
drop2(e, dragged, target) {
|
|
55
|
+
const children = target.lastElementChild
|
|
56
|
+
if (this.isNode(target)) {
|
|
57
|
+
if (this.isNodeEmpty(target)) {
|
|
58
|
+
children.appendChild(dragged)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rect = target.getBoundingClientRect()
|
|
64
|
+
const offset = e.clientY - rect.top
|
|
65
|
+
const threshold = (rect.height / 3) * 2
|
|
66
|
+
|
|
67
|
+
const target2 = (offset > threshold) ? target.nextSibling : target
|
|
68
|
+
target.parentNode.insertBefore(dragged, target2)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
doGrouping(elem) {
|
|
72
|
+
if (this.isNode(elem)) {
|
|
73
|
+
if (!this.isNodeEmpty(elem)) {
|
|
74
|
+
throw "MiniTreeView: Node has children; can't be converted to leaf"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.toLeaf(elem)
|
|
78
|
+
} else {
|
|
79
|
+
this.toNode(elem)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toLeaf(elem) {
|
|
84
|
+
var row = elem.firstElementChild
|
|
85
|
+
const newRow = this.create("span", {className: "toggle-space"})
|
|
86
|
+
|
|
87
|
+
this.removeUl(elem)
|
|
88
|
+
row.removeChild(row.firstElementChild)
|
|
89
|
+
row.insertBefore(newRow, row.children[0])
|
|
90
|
+
|
|
91
|
+
this.sync({owner: this.ownerValue, function: "node2leaf",
|
|
92
|
+
value: elem.dataset.itemId})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
toNode(elem) {
|
|
96
|
+
var row = elem.firstElementChild
|
|
97
|
+
const newRow = this.create("button", {className: "toggle-btn",
|
|
98
|
+
type: "button", textContent: this.expanded})
|
|
99
|
+
|
|
100
|
+
this.removeUl(elem)
|
|
101
|
+
const ulRow2 = this.create("ul", {className: "nested"})
|
|
102
|
+
elem.insertBefore(ulRow2, elem.children[1])
|
|
103
|
+
row.removeChild(row.firstElementChild)
|
|
104
|
+
row.insertBefore(newRow, row.children[0])
|
|
105
|
+
|
|
106
|
+
this.sync({owner: this.ownerValue, function: "leaf2node",
|
|
107
|
+
value: elem.dataset.itemId})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
removeUl(elem) {
|
|
111
|
+
const ulRow = elem.children[1]
|
|
112
|
+
if (ulRow) elem.removeChild(ulRow)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
toggle(e) {
|
|
116
|
+
const li = e.currentTarget.closest(".tree-item")
|
|
117
|
+
const id = li.dataset.itemId
|
|
118
|
+
const list = li.querySelector(".nested")
|
|
119
|
+
if (!list) return
|
|
120
|
+
|
|
121
|
+
const btn = li.querySelector(".toggle-btn")
|
|
122
|
+
const hide = list.classList.toggle("hidden")
|
|
123
|
+
if (btn) btn.textContent = hide ? this.collapsed : this.expanded
|
|
124
|
+
this.sync({owner: this.ownerValue, function: "toggle",
|
|
125
|
+
id: id, value: hide})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
///////////////////////// Utilities ////////////////////////////////
|
|
129
|
+
|
|
130
|
+
testUtilities(el) {
|
|
131
|
+
console.log("***** testUtilities *****", el)
|
|
132
|
+
console.log("isLeaf", this.isLeaf(el))
|
|
133
|
+
console.log("isNode", this.isNode(el))
|
|
134
|
+
console.log("isNodeEmpty", this.isNodeEmpty(el))
|
|
135
|
+
console.log("parent", this.parent(el))
|
|
136
|
+
console.log("parent_id", this.parent_id(el))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
create(tag, attrs) {
|
|
140
|
+
var elem = document.createElement(tag)
|
|
141
|
+
Object.assign(elem, attrs)
|
|
142
|
+
return elem
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isLeaf(elem) {
|
|
146
|
+
const el = elem.firstElementChild.firstElementChild
|
|
147
|
+
return el.tagName == "SPAN"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isNode(elem) {
|
|
151
|
+
return !this.isLeaf(elem)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isNodeEmpty(elem) {
|
|
155
|
+
const el = elem.lastElementChild
|
|
156
|
+
if (el.childElementCount == 0) return true
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parent(elem) {
|
|
161
|
+
return elem.parentElement.closest("li")
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parent_id(elem) {
|
|
165
|
+
elem = this.parent(elem)
|
|
166
|
+
if (!elem) return null
|
|
167
|
+
return elem.dataset.itemId
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
///////////////////////// AJAX ////////////////////////////////
|
|
171
|
+
|
|
172
|
+
flatted = (elem) => {
|
|
173
|
+
const result = []
|
|
174
|
+
var id = elem.dataset.itemId
|
|
175
|
+
var parent_id = this.parent_id(elem.parentElement)
|
|
176
|
+
|
|
177
|
+
if (id && id != 0) { result.push([id, parent_id]) }
|
|
178
|
+
for (var elem2 of elem.children) {
|
|
179
|
+
result.push(...this.flatted(elem2))
|
|
180
|
+
}
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
sendOrder(el) {
|
|
185
|
+
const flat = this.flatted(this.topTarget.parentElement)
|
|
186
|
+
this.sync({owner: this.ownerValue, function: "order",
|
|
187
|
+
order: JSON.stringify(flat)})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async sync(params) {
|
|
191
|
+
const url = "/mini_trees/sync"
|
|
192
|
+
const token = document.querySelector("meta[name=csrf-token]").content
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"X-CSRF-Token": token
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify(params)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new Error(`Response status: ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// const result = await response.json();
|
|
208
|
+
// console.log(result);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(error.message);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
// over(e, el) {
|
|
218
|
+
//console.log("over")
|
|
219
|
+
// e.preventDefault()
|
|
220
|
+
// if (this.dragged === el) return
|
|
221
|
+
// const rect = el.getBoundingClientRect()
|
|
222
|
+
// const offset = e.clientY - rect.top
|
|
223
|
+
// const third = rect.height / 3
|
|
224
|
+
//
|
|
225
|
+
// if (offset < third) {
|
|
226
|
+
// el.parentNode.insertBefore(this.placeholder, el)
|
|
227
|
+
// } else if (offset > 2 * third) {
|
|
228
|
+
// el.parentNode.insertBefore(this.placeholder, el.nextSibling)
|
|
229
|
+
// } else {
|
|
230
|
+
// let ul = el.querySelector(".nested")
|
|
231
|
+
// if (!ul) {
|
|
232
|
+
// ul = document.createElement("ul")
|
|
233
|
+
// ul.className = "nested"
|
|
234
|
+
// el.appendChild(ul)
|
|
235
|
+
// }
|
|
236
|
+
// ul.classList.remove("hidden")
|
|
237
|
+
// ul.appendChild(this.placeholder)
|
|
238
|
+
// }
|
|
239
|
+
// }
|
|
240
|
+
//
|
|
241
|
+
// removePlaceholder() {
|
|
242
|
+
// this.placeholder?.remove()
|
|
243
|
+
// }
|
|
244
|
+
//
|
|
245
|
+
//
|
|
246
|
+
// // rails javascript no jquery no sortablejs use stimulus sortable treeview
|
|
247
|
+
// // expand collapse support
|
|
248
|
+
// // nested reordering
|
|
249
|
+
//
|
|
250
|
+
//
|
|
251
|
+
// // https://wiki.selfhtml.org/wiki/JavaScript/Drag_%26_Drop
|
|
252
|
+
// // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= legend %>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.mini-tree {
|
|
3
|
+
xbackground: yellow;
|
|
4
|
+
.mini-tree-view, .nested {
|
|
5
|
+
list-style: none;
|
|
6
|
+
padding-left: 1rem;
|
|
7
|
+
}
|
|
8
|
+
.tree-row {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 0.5rem;
|
|
11
|
+
}
|
|
12
|
+
.toggle-btn {
|
|
13
|
+
background: transparent;
|
|
14
|
+
border: none;
|
|
15
|
+
}
|
|
16
|
+
.dragging { opacity: 0.5; }
|
|
17
|
+
.hidden { display: none; }
|
|
18
|
+
.toggle-btn, .toggle-space { width: 1rem; }
|
|
19
|
+
.tree-row:hover { background: rgba(0,0,0,0.05); }
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
|
|
23
|
+
<%
|
|
24
|
+
list = locals[:list]
|
|
25
|
+
owner = list.first.class.name[0..-5]
|
|
26
|
+
%>
|
|
27
|
+
<div class="mini-tree" data-item-id="0"
|
|
28
|
+
data-controller="tree" data-tree-owner-value="<%= owner %>">
|
|
29
|
+
<ul class="mini-tree-view" data-tree-target="top">
|
|
30
|
+
<%= render partial: "mini_trees/item",
|
|
31
|
+
collection: list.where(parent_id: 0).order(:position)
|
|
32
|
+
%>
|
|
33
|
+
</ul>
|
|
34
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<%
|
|
2
|
+
icon = (item.collapsed ? "\u25b8" : "\u25be").html_safe
|
|
3
|
+
klass = item.collapsed ? "nested hidden" : "nested"
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<li class="tree-item" data-tree-target="item"
|
|
7
|
+
data-item-id="<%= item.id %>" draggable="true">
|
|
8
|
+
<div class="tree-row" data-action="click->tree#toggle">
|
|
9
|
+
<% if item.kind == "node" %>
|
|
10
|
+
<button class="toggle-btn" type="button"><%= icon %></button>
|
|
11
|
+
<% else %>
|
|
12
|
+
<span class="toggle-space"></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<span class="title">
|
|
15
|
+
<%= render partial: "mini_tree_title",
|
|
16
|
+
locals: {id: item.id, legend: item.legend}
|
|
17
|
+
%>
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<% if item.kind == "node" %>
|
|
22
|
+
<ul class="<%= klass %>">
|
|
23
|
+
<%= render partial: "mini_trees/item", collection: item.children %>
|
|
24
|
+
</ul>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
</li>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module MiniTree::Utils
|
|
2
|
+
def self.included(base)
|
|
3
|
+
base.extend(ClassMethods)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def children
|
|
7
|
+
self.class.where(parent_id: id).order(:position)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_s
|
|
11
|
+
"position <#{position}> parent_id <#{parent_id}> #{kind} \"#{legend}\""
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def refresh
|
|
16
|
+
missing_cnt = delete_cnt = refresh_cnt = 0
|
|
17
|
+
owner_class = Kernel.const_get(name[0..-5])
|
|
18
|
+
owner_ids = owner_class.all.pluck(:id)
|
|
19
|
+
ids = all.pluck(:id)
|
|
20
|
+
|
|
21
|
+
(owner_ids - ids).each { |id|
|
|
22
|
+
legend = owner_class.find_by(id:).legend
|
|
23
|
+
create_item(id, legend)
|
|
24
|
+
missing_cnt += 1
|
|
25
|
+
}
|
|
26
|
+
(ids - owner_ids).each { |id|
|
|
27
|
+
del_item(id)
|
|
28
|
+
delete_cnt += 1
|
|
29
|
+
}
|
|
30
|
+
(owner_ids & ids).each { |id|
|
|
31
|
+
refresh_item(id, owner_class.find_by(id:).legend)
|
|
32
|
+
refresh_cnt += 1
|
|
33
|
+
}
|
|
34
|
+
[missing_cnt, delete_cnt, refresh_cnt]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def refresh_item(id, legend)
|
|
38
|
+
find_by(id:).update!(legend:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_item(id, legend)
|
|
42
|
+
create! id:, legend:, parent_id: 0, position: id, kind: "leaf"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def del_item(id)
|
|
46
|
+
where(id:).delete_all
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# used by tests
|
|
50
|
+
def flat
|
|
51
|
+
res = []
|
|
52
|
+
sorted.each { |item|
|
|
53
|
+
res << [item.id, item.parent_id]
|
|
54
|
+
}
|
|
55
|
+
res
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print
|
|
59
|
+
puts "*** #{name}.all ***"
|
|
60
|
+
sorted.each_with_index { |item, index|
|
|
61
|
+
puts "#{index}: #{item}"
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sorted
|
|
66
|
+
all.order(:position)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/mini_tree.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mini_tree
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dittmar Krall
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: stimulus-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: combustion
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: minitest
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: minitest-spec-rails
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: |2
|
|
83
|
+
A simple TreeView Rails 8+ gem based on Stimulus
|
|
84
|
+
(no jQuery is required).
|
|
85
|
+
A server side handling of the tree is included.
|
|
86
|
+
The client side display of the usual legend can be
|
|
87
|
+
adapted to the user's requirements to include links.
|
|
88
|
+
email:
|
|
89
|
+
- dittmar.krall@gmail.com
|
|
90
|
+
executables: []
|
|
91
|
+
extensions: []
|
|
92
|
+
extra_rdoc_files:
|
|
93
|
+
- MIT-LICENSE
|
|
94
|
+
- README.md
|
|
95
|
+
files:
|
|
96
|
+
- MIT-LICENSE
|
|
97
|
+
- README.md
|
|
98
|
+
- app/controllers/mini_trees_controller.rb
|
|
99
|
+
- app/javascript/controllers/tree_controller.js
|
|
100
|
+
- app/views/application/_mini_tree_title.html.erb
|
|
101
|
+
- app/views/mini_trees/_index.html.erb
|
|
102
|
+
- app/views/mini_trees/_item.html.erb
|
|
103
|
+
- config/routes.rb
|
|
104
|
+
- lib/mini_tree.rb
|
|
105
|
+
- lib/mini_tree/engine.rb
|
|
106
|
+
- lib/mini_tree/utils.rb
|
|
107
|
+
- lib/mini_tree/version.rb
|
|
108
|
+
- lib/mini_tree/version.rb.bak
|
|
109
|
+
homepage: https://github.com/matique/mini_tree
|
|
110
|
+
licenses:
|
|
111
|
+
- MIT
|
|
112
|
+
metadata: {}
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - "~>"
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '3'
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubygems_version: 3.6.9
|
|
128
|
+
specification_version: 4
|
|
129
|
+
summary: MiniTree a simple TreeView Rails 8+ gem.
|
|
130
|
+
test_files: []
|