django-fast-treenode 2.0.11__py3-none-any.whl → 2.1.1__py3-none-any.whl
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.
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/LICENSE +2 -2
- django_fast_treenode-2.1.1.dist-info/METADATA +158 -0
- django_fast_treenode-2.1.1.dist-info/RECORD +64 -0
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +9 -0
- treenode/admin/admin.py +295 -0
- treenode/admin/changelist.py +65 -0
- treenode/admin/mixins.py +302 -0
- treenode/apps.py +12 -1
- treenode/cache.py +2 -2
- treenode/forms.py +8 -10
- treenode/managers/__init__.py +21 -0
- treenode/managers/adjacency.py +203 -0
- treenode/managers/closure.py +278 -0
- treenode/models/__init__.py +2 -1
- treenode/models/adjacency.py +343 -0
- treenode/models/classproperty.py +3 -0
- treenode/models/closure.py +23 -24
- treenode/models/factory.py +12 -2
- treenode/models/mixins/__init__.py +23 -0
- treenode/models/mixins/ancestors.py +65 -0
- treenode/models/mixins/children.py +81 -0
- treenode/models/mixins/descendants.py +66 -0
- treenode/models/mixins/family.py +63 -0
- treenode/models/mixins/logical.py +68 -0
- treenode/models/mixins/node.py +210 -0
- treenode/models/mixins/properties.py +156 -0
- treenode/models/mixins/roots.py +96 -0
- treenode/models/mixins/siblings.py +99 -0
- treenode/models/mixins/tree.py +344 -0
- treenode/signals.py +26 -0
- treenode/static/treenode/css/tree_widget.css +201 -31
- treenode/static/treenode/css/treenode_admin.css +48 -41
- treenode/static/treenode/js/tree_widget.js +269 -131
- treenode/static/treenode/js/treenode_admin.js +131 -171
- treenode/templates/admin/tree_node_changelist.html +6 -0
- treenode/templates/admin/treenode_ajax_rows.html +7 -0
- treenode/tests/tests.py +488 -0
- treenode/urls.py +10 -6
- treenode/utils/__init__.py +2 -0
- treenode/utils/aid.py +46 -0
- treenode/utils/base16.py +38 -0
- treenode/utils/base36.py +3 -1
- treenode/utils/db.py +116 -0
- treenode/utils/exporter.py +2 -0
- treenode/utils/importer.py +0 -1
- treenode/utils/radix.py +61 -0
- treenode/version.py +2 -2
- treenode/views.py +118 -43
- treenode/widgets.py +91 -43
- django_fast_treenode-2.0.11.dist-info/METADATA +0 -698
- django_fast_treenode-2.0.11.dist-info/RECORD +0 -42
- treenode/admin.py +0 -439
- treenode/docs/Documentation +0 -636
- treenode/managers.py +0 -419
- treenode/models/proxy.py +0 -669
- {django_fast_treenode-2.0.11.dist-info → django_fast_treenode-2.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,344 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Tree Mixin
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
import json
|
11
|
+
from django.db import models, transaction
|
12
|
+
from collections import OrderedDict
|
13
|
+
from django.core.serializers.json import DjangoJSONEncoder
|
14
|
+
|
15
|
+
from ...cache import cached_method
|
16
|
+
|
17
|
+
|
18
|
+
class TreeNodeTreeMixin(models.Model):
|
19
|
+
"""TreeNode Tree Mixin."""
|
20
|
+
|
21
|
+
class Meta:
|
22
|
+
"""Moxin Meta Class."""
|
23
|
+
|
24
|
+
abstract = True
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def dump_tree(cls, instance=None):
|
28
|
+
"""
|
29
|
+
Return an n-dimensional dictionary representing the model tree.
|
30
|
+
|
31
|
+
Introduced for compatibility with other packages.
|
32
|
+
"""
|
33
|
+
return cls.get_tree(cls, instance)
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
@cached_method
|
37
|
+
def get_tree(cls, instance=None):
|
38
|
+
"""
|
39
|
+
Return an n-dimensional dictionary representing the model tree.
|
40
|
+
|
41
|
+
If instance is passed, returns a subtree rooted at instance (using
|
42
|
+
get_descendants_queryset), if not passed, builds a tree for all nodes
|
43
|
+
(loads all records in one query).
|
44
|
+
"""
|
45
|
+
# If instance is passed, we get all its descendants (including itself)
|
46
|
+
if instance:
|
47
|
+
queryset = instance.get_descendants_queryset(include_self=True)\
|
48
|
+
.annotate(depth=models.Max("parents_set__depth"))
|
49
|
+
else:
|
50
|
+
# Load all records at once
|
51
|
+
queryset = cls.objects.all()
|
52
|
+
|
53
|
+
# Dictionary for quick access to nodes by id and list for iteration
|
54
|
+
nodes_by_id = {}
|
55
|
+
nodes_list = []
|
56
|
+
|
57
|
+
# Loop through all nodes using an iterator
|
58
|
+
for node in queryset.iterator(chunk_size=1000):
|
59
|
+
# Create a dictionary for the node.
|
60
|
+
# In Python 3.7+, the standard dict preserves insertion order.
|
61
|
+
# We'll stick to the order:
|
62
|
+
# id, 'tn_parent', 'tn_priority', 'level', then the rest of
|
63
|
+
# the fields. Сlose the dictionary with the fields 'level', 'path',
|
64
|
+
# 'children'
|
65
|
+
node_dict = OrderedDict()
|
66
|
+
node_dict['id'] = node.id
|
67
|
+
node_dict['tn_parent'] = node.tn_parent_id
|
68
|
+
node_dict['tn_priority'] = node.tn_priority
|
69
|
+
node_dict['level'] = node.get_depth()
|
70
|
+
node_dict['path'] = node.get_breadcrumbs('tn_priority')
|
71
|
+
|
72
|
+
# Add the rest of the model fields.
|
73
|
+
# Iterate over all the fields obtained via _meta.get_fields()
|
74
|
+
for field in node._meta.get_fields():
|
75
|
+
# Skipping fields that are already added or not required
|
76
|
+
# (e.g. tn_closure or virtual links)
|
77
|
+
if field.name in [
|
78
|
+
'id', 'tn_parent', 'tn_priority', 'tn_closure',
|
79
|
+
'children']:
|
80
|
+
continue
|
81
|
+
|
82
|
+
try:
|
83
|
+
value = getattr(node, field.name)
|
84
|
+
except Exception:
|
85
|
+
value = None
|
86
|
+
|
87
|
+
# If the field is many-to-many, we get a list of IDs of
|
88
|
+
# related objects
|
89
|
+
if hasattr(value, 'all'):
|
90
|
+
value = list(value.all().values_list('id', flat=True))
|
91
|
+
|
92
|
+
node_dict[field.name] = value
|
93
|
+
|
94
|
+
# Adding a materialized path
|
95
|
+
node_dict['path'] = None
|
96
|
+
# Adding a nesting level
|
97
|
+
node_dict['level'] = None
|
98
|
+
# We initialize the list of children, which we will then fill
|
99
|
+
# when assembling the tree
|
100
|
+
node_dict['children'] = []
|
101
|
+
|
102
|
+
# Save the node both in the list and in the dictionary by id
|
103
|
+
# for quick access
|
104
|
+
nodes_by_id[node.id] = node_dict
|
105
|
+
nodes_list.append(node_dict)
|
106
|
+
|
107
|
+
# Build a tree: assign each node a list of its children
|
108
|
+
tree = []
|
109
|
+
for node_dict in nodes_list:
|
110
|
+
parent_id = node_dict['tn_parent']
|
111
|
+
# If there is a parent and it is present in nodes_by_id, then
|
112
|
+
# add the current node to the list of its children
|
113
|
+
if parent_id and parent_id in nodes_by_id:
|
114
|
+
parent_node = nodes_by_id[parent_id]
|
115
|
+
parent_node['children'].append(node_dict)
|
116
|
+
else:
|
117
|
+
# If there is no parent, this is the root node of the tree
|
118
|
+
tree.append(node_dict)
|
119
|
+
|
120
|
+
return tree
|
121
|
+
|
122
|
+
@classmethod
|
123
|
+
def get_tree_json(cls, instance=None):
|
124
|
+
"""Represent the tree as a JSON-compatible string."""
|
125
|
+
tree = cls.dump_tree(instance)
|
126
|
+
return DjangoJSONEncoder().encode(tree)
|
127
|
+
|
128
|
+
@classmethod
|
129
|
+
def load_tree(cls, tree_data):
|
130
|
+
"""
|
131
|
+
Load a tree from a list of dictionaries.
|
132
|
+
|
133
|
+
Loaded nodes are synchronized with the database: existing records
|
134
|
+
are updated, new ones are created.
|
135
|
+
Each dictionary must contain the 'id' key to identify the record.
|
136
|
+
"""
|
137
|
+
|
138
|
+
def flatten_tree(nodes, model_fields):
|
139
|
+
"""
|
140
|
+
Recursively traverse the tree and generate lists of nodes.
|
141
|
+
|
142
|
+
Each node in the list is a copy of the dictionary without
|
143
|
+
the service keys 'children', 'level', 'path'.
|
144
|
+
"""
|
145
|
+
flat = []
|
146
|
+
for node in nodes:
|
147
|
+
# Create a copy of the dictionary so as not to affect
|
148
|
+
# the original tree
|
149
|
+
node_copy = node.copy()
|
150
|
+
children = node_copy.pop('children', [])
|
151
|
+
# Remove temporary/service fields that are not related to
|
152
|
+
# the model
|
153
|
+
for key in list(node_copy.keys() - model_fields):
|
154
|
+
del node_copy[key]
|
155
|
+
flat.append(node_copy)
|
156
|
+
# Recursively add all children
|
157
|
+
flat.extend(flatten_tree(children, model_fields))
|
158
|
+
return flat
|
159
|
+
|
160
|
+
# Get a flat list of nodes (from root to leaf)
|
161
|
+
model_fields = [field.name for field in cls._meta.get_fields()]
|
162
|
+
flat_nodes = flatten_tree(tree_data, model_fields)
|
163
|
+
|
164
|
+
# Load all ids for a given model from the database to minimize
|
165
|
+
# the number of database requests
|
166
|
+
existing_ids = set(cls.objects.values_list('id', flat=True))
|
167
|
+
|
168
|
+
# Lists for nodes to update and create
|
169
|
+
nodes_to_update = []
|
170
|
+
nodes_to_create = []
|
171
|
+
|
172
|
+
# Determine which model fields should be updated (excluding 'id')
|
173
|
+
# This assumes that all model fields are used in serialization
|
174
|
+
# (excluding service ones, like children)
|
175
|
+
field_names = model_fields.remove('id')
|
176
|
+
|
177
|
+
# Iterate over each node from the flat list
|
178
|
+
for node_data in flat_nodes:
|
179
|
+
# Collect data for creation/update.
|
180
|
+
# There is already an 'id' in node_data, so we can distinguish
|
181
|
+
# an existing entry from a new one.
|
182
|
+
data = {k: v for k, v in node_data.items()
|
183
|
+
if k in field_names or k == 'id'}
|
184
|
+
|
185
|
+
# Handle the foreign key tn_parent.
|
186
|
+
# If the value is None, then there is no parent, otherwise
|
187
|
+
# the parent id is expected.
|
188
|
+
# Additional checks can be added here if needed.
|
189
|
+
if 'tn_parent' in data and data['tn_parent'] is None:
|
190
|
+
data['tn_parent'] = None
|
191
|
+
|
192
|
+
# Если id уже есть в базе, то будем обновлять запись,
|
193
|
+
# иначе создаем новую.
|
194
|
+
if data['id'] in existing_ids:
|
195
|
+
nodes_to_update.append(cls(**data))
|
196
|
+
else:
|
197
|
+
nodes_to_create.append(cls(**data))
|
198
|
+
|
199
|
+
# Perform operations in a transaction to ensure data integrity.
|
200
|
+
with transaction.atomic():
|
201
|
+
# bulk_create – creating new nodes
|
202
|
+
if nodes_to_create:
|
203
|
+
cls.objects.bulk_create(nodes_to_create, batch_size=1000)
|
204
|
+
# bulk_update – updating existing nodes.
|
205
|
+
# When bulk_update, we must specify a list of fields to update.
|
206
|
+
if nodes_to_update:
|
207
|
+
cls.objects.bulk_update(
|
208
|
+
nodes_to_update,
|
209
|
+
fields=field_names,
|
210
|
+
batch_size=1000
|
211
|
+
)
|
212
|
+
cls.clear_cache()
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def load_tree_json(cls, json_str):
|
216
|
+
"""
|
217
|
+
Decode a JSON string into a dictionary.
|
218
|
+
|
219
|
+
Takes a JSON-compatible string and decodes it into a tree structure.
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
tree_data = json.loads(json_str)
|
223
|
+
except json.JSONDecodeError as e:
|
224
|
+
raise ValueError(f"Error decoding JSON: {e}")
|
225
|
+
|
226
|
+
cls.load_tree(tree_data)
|
227
|
+
|
228
|
+
@classmethod
|
229
|
+
@cached_method
|
230
|
+
def get_tree_display(cls, instance=None, symbol="—"):
|
231
|
+
"""Get a multiline string representing the model tree."""
|
232
|
+
# If instance is passed, we get all its descendants (including itself)
|
233
|
+
if instance:
|
234
|
+
queryset = instance.get_descendants_queryset(include_self=True)\
|
235
|
+
.prefetch_related("tn_children")\
|
236
|
+
.annotate(depth=models.Max("parents_set__depth"))\
|
237
|
+
.order_by("depth", "tn_parent", "tn_priority")
|
238
|
+
else:
|
239
|
+
queryset = cls.objects.all()\
|
240
|
+
.prefetch_related("tn_children")\
|
241
|
+
.annotate(depth=models.Max("parents_set__depth"))\
|
242
|
+
.order_by("depth", "tn_parent", "tn_priority")
|
243
|
+
# Convert queryset to list for indexed access
|
244
|
+
nodes = list(queryset)
|
245
|
+
sorted_nodes = cls._sort_node_list(nodes)
|
246
|
+
result = []
|
247
|
+
for node in sorted_nodes:
|
248
|
+
# Insert an indent proportional to the depth of the node
|
249
|
+
indent = symbol * node.depth
|
250
|
+
result.append(indent + str(node))
|
251
|
+
return result
|
252
|
+
|
253
|
+
@classmethod
|
254
|
+
@cached_method
|
255
|
+
def get_tree_annotated(cls):
|
256
|
+
"""
|
257
|
+
Get an annotated list from a tree branch.
|
258
|
+
|
259
|
+
Something like this will be returned:
|
260
|
+
[
|
261
|
+
(a, {'open':True, 'close':[], 'level': 0})
|
262
|
+
(ab, {'open':True, 'close':[], 'level': 1})
|
263
|
+
(aba, {'open':True, 'close':[], 'level': 2})
|
264
|
+
(abb, {'open':False, 'close':[], 'level': 2})
|
265
|
+
(abc, {'open':False, 'close':[0,1], 'level': 2})
|
266
|
+
(ac, {'open':False, 'close':[0], 'level': 1})
|
267
|
+
]
|
268
|
+
|
269
|
+
All nodes are ordered by materialized path.
|
270
|
+
This can be used with a template like this:
|
271
|
+
|
272
|
+
{% for item, info in annotated_list %}
|
273
|
+
{% if info.open %}
|
274
|
+
<ul><li>
|
275
|
+
{% else %}
|
276
|
+
</li><li>
|
277
|
+
{% endif %}
|
278
|
+
|
279
|
+
{{ item }}
|
280
|
+
|
281
|
+
{% for close in info.close %}
|
282
|
+
</li></ul>
|
283
|
+
{% endfor %}
|
284
|
+
{% endfor %}
|
285
|
+
|
286
|
+
"""
|
287
|
+
# Load the tree with the required preloads and depth annotation.
|
288
|
+
queryset = cls.objects.all()\
|
289
|
+
.prefetch_related("tn_children")\
|
290
|
+
.annotate(depth=models.Max("parents_set__depth"))\
|
291
|
+
.order_by("depth", "tn_parent", "tn_priority")
|
292
|
+
# Convert queryset to list for indexed access
|
293
|
+
nodes = list(queryset)
|
294
|
+
total_nodes = len(nodes)
|
295
|
+
sorted_nodes = cls._sort_node_list(nodes)
|
296
|
+
|
297
|
+
result = []
|
298
|
+
|
299
|
+
for i, node in enumerate(sorted_nodes):
|
300
|
+
# Get the value to display the node
|
301
|
+
value = str(node)
|
302
|
+
# Determine if there are descendants (use prefetch_related to avoid
|
303
|
+
# additional queries)
|
304
|
+
value_open = len(node.tn_children.all()) > 0
|
305
|
+
level = node.depth
|
306
|
+
|
307
|
+
# Calculate the "close" field
|
308
|
+
if i + 1 < total_nodes:
|
309
|
+
next_node = nodes[i + 1]
|
310
|
+
depth_diff = level - next_node.depth
|
311
|
+
# If the next node is at a lower level, then some open
|
312
|
+
# levels need to be closed
|
313
|
+
value_close = list(range(next_node.depth, level)
|
314
|
+
) if depth_diff > 0 else []
|
315
|
+
else:
|
316
|
+
# For the last node, close all open levels
|
317
|
+
value_close = list(range(0, level + 1))
|
318
|
+
|
319
|
+
result.append(
|
320
|
+
(value, {
|
321
|
+
"open": value_open,
|
322
|
+
"close": value_close,
|
323
|
+
"level": level
|
324
|
+
})
|
325
|
+
)
|
326
|
+
return result
|
327
|
+
|
328
|
+
@classmethod
|
329
|
+
@transaction.atomic
|
330
|
+
def update_tree(cls):
|
331
|
+
"""Rebuilds the closure table."""
|
332
|
+
# Clear cache
|
333
|
+
cls.clear_cache()
|
334
|
+
cls.closure_model.delete_all()
|
335
|
+
objs = list(cls.objects.all())
|
336
|
+
cls.closure_model.objects.bulk_create(objs, batch_size=1000)
|
337
|
+
|
338
|
+
@classmethod
|
339
|
+
def delete_tree(cls):
|
340
|
+
"""Delete the whole tree for the current node class."""
|
341
|
+
cls.clear_cache()
|
342
|
+
cls.objects.all().delete()
|
343
|
+
|
344
|
+
# The end
|
treenode/signals.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Signals Module
|
4
|
+
|
5
|
+
Version: 2.1.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from contextlib import contextmanager
|
11
|
+
|
12
|
+
|
13
|
+
@contextmanager
|
14
|
+
def disable_signals(signal, sender):
|
15
|
+
"""Temporarily disable execution of signal generation."""
|
16
|
+
# Save current signal handlers
|
17
|
+
old_receivers = signal.receivers[:]
|
18
|
+
signal.receivers = []
|
19
|
+
try:
|
20
|
+
yield
|
21
|
+
finally:
|
22
|
+
# Restore handlers
|
23
|
+
signal.receivers = old_receivers
|
24
|
+
|
25
|
+
|
26
|
+
# Tne End
|
@@ -11,52 +11,222 @@ Features:
|
|
11
11
|
- Custom styling for search fields and selection indicators.
|
12
12
|
- Enhances usability in tree-based data selection.
|
13
13
|
|
14
|
+
Main styles:
|
15
|
+
.tree-widget-display: styles the display area of the selected item, adding
|
16
|
+
padding, borders, and background.
|
17
|
+
.tree-dropdown-arrow: styles the dropdown arrow.
|
18
|
+
.tree-widget-dropdown: defines the style of the dropdown, including positioning,
|
19
|
+
size, and shadows.
|
20
|
+
.tree-search-wrapper: decorates the search area inside the dropdown.
|
21
|
+
.tree-search: styles the search input.
|
22
|
+
.tree-clear-button: button to clear the search input.
|
23
|
+
.tree-list and .tree-node: styles for the list and its items.
|
24
|
+
.expand-button: button to expand child elements.
|
25
|
+
|
26
|
+
Dark theme:
|
27
|
+
Uses the .dark-theme class to apply dark styles. Changes the background,
|
28
|
+
borders, and text color for the corresponding elements. Ensures comfortable use
|
29
|
+
of the widget in dark mode.
|
30
|
+
|
14
31
|
Version: 2.0.0
|
15
32
|
Author: Timur Kady
|
16
33
|
Email: timurkady@yandex.com
|
34
|
+
|
17
35
|
*/
|
18
36
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
37
|
+
.form-row.field-tn_parent {
|
38
|
+
position: relative;
|
39
|
+
overflow: visible !important;
|
40
|
+
z-index: auto;
|
41
|
+
}
|
42
|
+
|
43
|
+
.tree-widget {
|
44
|
+
position: relative;
|
45
|
+
width: 100%;
|
46
|
+
font-family: Arial, sans-serif;
|
47
|
+
}
|
48
|
+
|
49
|
+
.tree-widget-display {
|
50
|
+
display: flex;
|
51
|
+
align-items: center;
|
52
|
+
justify-content: space-between;
|
53
|
+
border: 1px solid #aaa;
|
54
|
+
border-radius: 4px;
|
55
|
+
background-color: #fff;
|
56
|
+
cursor: pointer;
|
57
|
+
transition: border-color 0.2s;
|
58
|
+
}
|
59
|
+
|
60
|
+
.tree-widget-display:hover {
|
61
|
+
border-color: #333;
|
62
|
+
}
|
63
|
+
|
64
|
+
.tree-dropdown-arrow {
|
65
|
+
font-size: 0.8em;
|
66
|
+
color: #888;
|
67
|
+
display: inline-block;
|
68
|
+
height: 100%;
|
69
|
+
background-color: lightgrey;
|
70
|
+
padding: 8px;
|
71
|
+
margin: 0;
|
72
|
+
border-radius: 0 4px 4px 0;
|
73
|
+
}
|
74
|
+
|
75
|
+
.tree-widget-dropdown {
|
76
|
+
display: none;
|
77
|
+
position: absolute;
|
78
|
+
top: 100%;
|
79
|
+
left: 0;
|
80
|
+
width: 100%;
|
81
|
+
max-height: 242px;
|
82
|
+
overflow-y: auto;
|
83
|
+
border: 1px solid #aaa;
|
84
|
+
border-top: none;
|
85
|
+
border-radius: 0 3px 3px 0;
|
86
|
+
background-color: #fff;
|
87
|
+
z-index: 1000;
|
88
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
89
|
+
}
|
90
|
+
|
91
|
+
.tree-search-wrapper {
|
92
|
+
display: flex;
|
93
|
+
align-items: center;
|
94
|
+
padding: 6px;
|
95
|
+
border-bottom: 1px solid #ddd;
|
96
|
+
background-color: #f9f9f9;
|
97
|
+
}
|
98
|
+
|
99
|
+
.tree-search-icon {
|
100
|
+
margin-right: 6px;
|
101
|
+
font-size: 1.3em;
|
102
|
+
}
|
103
|
+
|
104
|
+
.tree-search {
|
105
|
+
flex-grow: 1;
|
106
|
+
padding: 6px;
|
107
|
+
border: 1px solid #ccc;
|
108
|
+
border-radius: 4px;
|
109
|
+
font-size: 1em;
|
110
|
+
}
|
111
|
+
|
112
|
+
.tree-search-clear{
|
113
|
+
border: none;
|
114
|
+
border-radius: 3px;
|
115
|
+
background: none;
|
116
|
+
font-size: 1.8em;
|
117
|
+
cursor: pointer;
|
118
|
+
}
|
119
|
+
|
120
|
+
.tree-clear-button {
|
121
|
+
background: none;
|
122
|
+
border: none;
|
123
|
+
font-size: 1.2em;
|
124
|
+
color: #888;
|
125
|
+
cursor: pointer;
|
126
|
+
margin-left: 6px;
|
127
|
+
}
|
128
|
+
|
129
|
+
.tree-list {
|
130
|
+
list-style: none;
|
131
|
+
margin: 0 !important;
|
132
|
+
padding: 0 !important;
|
133
|
+
}
|
134
|
+
|
135
|
+
.tree-node {
|
136
|
+
display: block !important;
|
137
|
+
padding: 6px 0px !important;
|
138
|
+
cursor: pointer;
|
139
|
+
transition: background-color 0.2s;
|
140
|
+
}
|
141
|
+
|
142
|
+
.tree-node:hover {
|
143
|
+
background-color: #f0f0f0;
|
144
|
+
}
|
145
|
+
|
146
|
+
.tree-node[data-level="1"] {
|
147
|
+
padding-left: 20px;
|
148
|
+
}
|
149
|
+
|
150
|
+
.tree-node[data-level="2"] {
|
151
|
+
padding-left: 40px;
|
152
|
+
}
|
153
|
+
|
154
|
+
.expand-button {
|
155
|
+
display: inline-block;
|
156
|
+
width: 18px;
|
157
|
+
height: 18px;
|
158
|
+
background: var(--button-bg);
|
159
|
+
color: var(--button-fg);
|
160
|
+
border-radius: 3px;
|
161
|
+
border: none;
|
162
|
+
margin: 0px 5px;
|
163
|
+
cursor: pointer;
|
164
|
+
font-size: 12px;
|
165
|
+
line-height: 18px;
|
166
|
+
padding: 0px;
|
167
|
+
opacity: 0.8;
|
168
|
+
}
|
169
|
+
|
170
|
+
.no-expand {
|
171
|
+
display: inline-block;
|
172
|
+
width: 18px;
|
173
|
+
height: 18px;
|
174
|
+
border: none;
|
175
|
+
margin: 0px 5px;
|
176
|
+
}
|
177
|
+
|
178
|
+
.selected-node {
|
179
|
+
margin: 6px 12px;
|
180
|
+
}
|
181
|
+
|
182
|
+
/* Тёмная тема */
|
183
|
+
.dark-theme .tree-widget-display {
|
184
|
+
background-color: #333;
|
185
|
+
border-color: #555;
|
186
|
+
color: #eee;
|
187
|
+
}
|
188
|
+
|
189
|
+
.dark-theme .tree-dropdown-arrow {
|
190
|
+
color: #ccc;
|
191
|
+
}
|
192
|
+
|
193
|
+
.dark-theme .tree-widget-dropdown {
|
194
|
+
background-color: #444;
|
195
|
+
border-color: #555;
|
196
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
|
197
|
+
}
|
198
|
+
|
199
|
+
.dark-theme .tree-search-wrapper {
|
200
|
+
background-color: #555;
|
201
|
+
border-bottom-color: #666;
|
202
|
+
}
|
203
|
+
|
204
|
+
.dark-theme .tree-search {
|
205
|
+
background-color: #666;
|
206
|
+
border-color: #777;
|
207
|
+
color: #eee;
|
24
208
|
}
|
25
209
|
|
26
|
-
|
27
|
-
|
28
|
-
background-color: transparent !important;
|
29
|
-
color: #ddd !important;
|
210
|
+
.dark-theme .tree-search::placeholder {
|
211
|
+
color: #ccc;
|
30
212
|
}
|
31
213
|
|
32
|
-
|
33
|
-
|
34
|
-
.select2-dropdown.dark-theme .select2-results__option--selected {
|
35
|
-
background-color: #555 !important;
|
36
|
-
color: #fff !important;
|
214
|
+
.dark-theme .tree-clear-button {
|
215
|
+
color: #ccc;
|
37
216
|
}
|
38
217
|
|
39
|
-
|
40
|
-
|
41
|
-
background-color: #2c2c2c !important;
|
42
|
-
color: #ddd !important;
|
43
|
-
border: 1px solid #444 !important;
|
44
|
-
outline: none !important;
|
218
|
+
.dark-theme .tree-node {
|
219
|
+
color: #eee;
|
45
220
|
}
|
46
221
|
|
47
|
-
|
48
|
-
|
49
|
-
background-color: #2c2c2c !important;
|
50
|
-
border: 1px solid #444 !important;
|
51
|
-
color: #ddd !important;
|
222
|
+
.dark-theme .tree-node:hover {
|
223
|
+
background-color: #555;
|
52
224
|
}
|
53
225
|
|
54
|
-
|
55
|
-
|
56
|
-
border-color: #ddd transparent transparent transparent !important;
|
226
|
+
.dark-theme .expand-button {
|
227
|
+
color: #ccc;
|
57
228
|
}
|
58
229
|
|
59
|
-
|
60
|
-
|
61
|
-
color: #ddd !important;
|
230
|
+
.dark-theme .selected-node {
|
231
|
+
color: #eee;
|
62
232
|
}
|