django-fast-treenode 2.1.4__py3-none-any.whl → 3.0.0__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-3.0.0.dist-info/METADATA +203 -0
- django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
- treenode/admin/__init__.py +2 -7
- treenode/admin/admin.py +138 -209
- treenode/admin/changelist.py +21 -39
- treenode/admin/exporter.py +170 -0
- treenode/admin/importer.py +171 -0
- treenode/admin/mixin.py +291 -0
- treenode/apps.py +42 -20
- treenode/cache.py +192 -303
- treenode/forms.py +45 -65
- treenode/managers/__init__.py +4 -20
- treenode/managers/managers.py +216 -0
- treenode/managers/queries.py +233 -0
- treenode/managers/tasks.py +167 -0
- treenode/models/__init__.py +8 -5
- treenode/models/decorators.py +54 -0
- treenode/models/factory.py +44 -68
- treenode/models/mixins/__init__.py +2 -1
- treenode/models/mixins/ancestors.py +44 -20
- treenode/models/mixins/children.py +33 -26
- treenode/models/mixins/descendants.py +33 -22
- treenode/models/mixins/family.py +25 -15
- treenode/models/mixins/logical.py +23 -21
- treenode/models/mixins/node.py +162 -104
- treenode/models/mixins/properties.py +22 -16
- treenode/models/mixins/roots.py +59 -15
- treenode/models/mixins/siblings.py +46 -43
- treenode/models/mixins/tree.py +212 -153
- treenode/models/mixins/update.py +154 -0
- treenode/models/models.py +365 -0
- treenode/settings.py +28 -0
- treenode/static/{treenode/css → css}/tree_widget.css +1 -1
- treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
- treenode/static/css/treenode_tabs.css +51 -0
- treenode/static/js/lz-string.min.js +1 -0
- treenode/static/{treenode/js → js}/tree_widget.js +9 -23
- treenode/static/js/treenode_admin.js +531 -0
- treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
- treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
- treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- treenode/static/vendors/jquery-ui/index.html +297 -0
- treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
- treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
- treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
- treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
- treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
- treenode/static/vendors/jquery-ui/package.json +82 -0
- treenode/templates/admin/treenode_changelist.html +25 -0
- treenode/templates/admin/treenode_import_export.html +85 -0
- treenode/templates/admin/treenode_rows.html +57 -0
- treenode/tests.py +3 -0
- treenode/urls.py +6 -27
- treenode/utils/__init__.py +0 -15
- treenode/utils/db/__init__.py +7 -0
- treenode/utils/db/compiler.py +114 -0
- treenode/utils/db/db_vendor.py +50 -0
- treenode/utils/db/service.py +84 -0
- treenode/utils/db/sqlcompat.py +60 -0
- treenode/utils/db/sqlquery.py +70 -0
- treenode/version.py +2 -2
- treenode/views/__init__.py +5 -0
- treenode/views/autoapi.py +91 -0
- treenode/views/autocomplete.py +52 -0
- treenode/views/children.py +41 -0
- treenode/views/common.py +23 -0
- treenode/views/crud.py +209 -0
- treenode/views/search.py +48 -0
- treenode/widgets.py +27 -44
- django_fast_treenode-2.1.4.dist-info/METADATA +0 -166
- django_fast_treenode-2.1.4.dist-info/RECORD +0 -63
- treenode/admin/mixins.py +0 -302
- treenode/managers/adjacency.py +0 -205
- treenode/managers/closure.py +0 -278
- treenode/models/adjacency.py +0 -342
- treenode/models/classproperty.py +0 -27
- treenode/models/closure.py +0 -122
- treenode/static/treenode/js/.gitkeep +0 -1
- treenode/static/treenode/js/treenode_admin.js +0 -131
- treenode/templates/admin/export_success.html +0 -26
- treenode/templates/admin/tree_node_changelist.html +0 -19
- treenode/templates/admin/tree_node_export.html +0 -27
- treenode/templates/admin/tree_node_import.html +0 -45
- treenode/templates/admin/tree_node_import_report.html +0 -32
- treenode/templates/widgets/tree_widget.css +0 -23
- treenode/utils/aid.py +0 -46
- treenode/utils/base16.py +0 -38
- treenode/utils/base36.py +0 -37
- treenode/utils/db.py +0 -116
- treenode/utils/exporter.py +0 -196
- treenode/utils/importer.py +0 -328
- treenode/utils/radix.py +0 -61
- treenode/views.py +0 -184
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_fast_treenode-2.1.4.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
- /treenode/static/{treenode → css}/.gitkeep +0 -0
- /treenode/static/{treenode/css → js}/.gitkeep +0 -0
@@ -0,0 +1,365 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
The TreeNode Model
|
4
|
+
|
5
|
+
This module defines an abstract base model `TreeNodeModel` that
|
6
|
+
implements hierarchical data storage using the Adjacency Table method.
|
7
|
+
It integrates with a Closure Table for optimized tree operations.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Implements a basic tree representation as an Adjacency List with parent-child
|
11
|
+
relationships.
|
12
|
+
- Combines the Adjacency List method with a Materialized Path for efficient
|
13
|
+
ancestor and descendant queries.
|
14
|
+
- Provides a caching mechanism to optimize performance.
|
15
|
+
- Includes methods for tree traversal, manipulation, and serialization.
|
16
|
+
|
17
|
+
Version: 3.0.0
|
18
|
+
Author: Timur Kady
|
19
|
+
Email: timurkady@yandex.com
|
20
|
+
|
21
|
+
Причём с абсолютной поддержкой SQL-очередей, deferred execution,
|
22
|
+
кастомной сортировки и крутой архитектурой без лишнего дублирования.
|
23
|
+
"""
|
24
|
+
|
25
|
+
from __future__ import annotations
|
26
|
+
|
27
|
+
from collections import deque
|
28
|
+
from django.db import models, connection
|
29
|
+
from itertools import islice
|
30
|
+
from django.db.models.signals import pre_save, post_save
|
31
|
+
from django.utils.translation import gettext_lazy as _
|
32
|
+
|
33
|
+
from . import mixins as mx
|
34
|
+
from .factory import TreeNodeModelBase
|
35
|
+
from ..utils.db import ModelSQLService, SQLQueue
|
36
|
+
from ..managers import TreeNodeManager, TreeQueryManager, TreeTaskManager
|
37
|
+
from ..cache import treenode_cache as cache
|
38
|
+
from ..settings import SEGMENT_LENGTH, BASE
|
39
|
+
from ..signals import disable_signals
|
40
|
+
|
41
|
+
|
42
|
+
class TreeNodeModel(
|
43
|
+
mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
|
44
|
+
mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
|
45
|
+
mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
|
46
|
+
mx.TreeNodePropertiesMixin, mx.TreeNodeRootsMixin,
|
47
|
+
mx.TreeNodeSiblingsMixin, mx.TreeNodeTreeMixin, mx.RawSQLMixin,
|
48
|
+
models.Model, metaclass=TreeNodeModelBase):
|
49
|
+
"""
|
50
|
+
Abstract tree node model.
|
51
|
+
|
52
|
+
Implements hierarchical storage using the adjacency table method.
|
53
|
+
For performance improvements, it has additional Materialized Path
|
54
|
+
attributes.
|
55
|
+
"""
|
56
|
+
|
57
|
+
parent = models.ForeignKey(
|
58
|
+
'self',
|
59
|
+
null=True,
|
60
|
+
blank=True,
|
61
|
+
on_delete=models.CASCADE,
|
62
|
+
related_name='children',
|
63
|
+
verbose_name=_('parent')
|
64
|
+
)
|
65
|
+
|
66
|
+
# Node order among siblings
|
67
|
+
priority = models.PositiveIntegerField(
|
68
|
+
default=0, verbose_name=_('priority')
|
69
|
+
)
|
70
|
+
|
71
|
+
# Node materialized path
|
72
|
+
_path = models.TextField(default='', editable=False)
|
73
|
+
# Maybe in the future, sometime...
|
74
|
+
# _path_order = models.PositiveBigIntegerField(default=0, editable=False)
|
75
|
+
# Node nesting depth
|
76
|
+
_depth = models.PositiveIntegerField(default=0, editable=False)
|
77
|
+
|
78
|
+
# Field for display in the Admin Class
|
79
|
+
display_field = None
|
80
|
+
|
81
|
+
class SortingChoices(models.TextChoices):
|
82
|
+
"""Sorting Direction Class."""
|
83
|
+
|
84
|
+
ASC = "ASC", _("Ascending order")
|
85
|
+
DESC = "DESC", _("Descending order")
|
86
|
+
|
87
|
+
# Sorting field
|
88
|
+
sorting_field = 'priority'
|
89
|
+
# Sorting direction
|
90
|
+
sorting_direction = SortingChoices.ASC
|
91
|
+
# Model API protection flag via mandatory authorization via login
|
92
|
+
api_login_required = False
|
93
|
+
|
94
|
+
# Defaul manager
|
95
|
+
objects = TreeNodeManager()
|
96
|
+
# Task manager
|
97
|
+
tasks = TreeTaskManager()
|
98
|
+
# Query manager
|
99
|
+
query = TreeQueryManager()
|
100
|
+
|
101
|
+
# SQL Queue
|
102
|
+
sqlq = SQLQueue()
|
103
|
+
|
104
|
+
# DB Service
|
105
|
+
db = ModelSQLService()
|
106
|
+
|
107
|
+
class Meta:
|
108
|
+
"""Tree Meta Class."""
|
109
|
+
|
110
|
+
indexes = [
|
111
|
+
# models.Index(fields=["_path_order"]),
|
112
|
+
models.Index(fields=["parent", "priority"]),
|
113
|
+
models.Index(fields=["_depth", "priority"]),
|
114
|
+
]
|
115
|
+
abstract = True
|
116
|
+
|
117
|
+
# -----------------------------------------------------------------
|
118
|
+
#
|
119
|
+
# General Methods
|
120
|
+
#
|
121
|
+
# -----------------------------------------------------------------
|
122
|
+
|
123
|
+
def __str__(self):
|
124
|
+
"""Return a human-readable string representation of an object."""
|
125
|
+
field = getattr(type(self), 'display_field', None)
|
126
|
+
if field and hasattr(self, field):
|
127
|
+
return str(getattr(self, field))
|
128
|
+
return f'Node {self.pk}'
|
129
|
+
|
130
|
+
def clear_cache(self):
|
131
|
+
"""Clear cache for this node only."""
|
132
|
+
cache.invalidate(self._meta.label)
|
133
|
+
|
134
|
+
# Generation methods ------------------------------------------
|
135
|
+
|
136
|
+
def generate_path(self) -> str:
|
137
|
+
"""Build _path based on priorities of ancestors."""
|
138
|
+
segment = f"{self.priority:0{SEGMENT_LENGTH}X}"
|
139
|
+
return segment if self.parent is None else f"{self.parent._path}.{segment}" # noqa: D501
|
140
|
+
|
141
|
+
# Modification methods ----------------------------------------
|
142
|
+
|
143
|
+
def delete(self, cascade=True):
|
144
|
+
"""Delete a node and clears the cache.
|
145
|
+
|
146
|
+
If cascade=False, then:
|
147
|
+
- If the node is not a root, its children are "lifted" to the parent,
|
148
|
+
- If the node is a root (parent is None), its children become
|
149
|
+
the new roots.
|
150
|
+
"""
|
151
|
+
if not cascade:
|
152
|
+
self.db.reassign_children(self.id, self.parent_id)
|
153
|
+
|
154
|
+
# Delete the node itself
|
155
|
+
super().delete()
|
156
|
+
# Update subtree
|
157
|
+
self._update_path(self.parent_id)
|
158
|
+
self.sqlq.flush()
|
159
|
+
# Clead cache
|
160
|
+
self.clear_cache()
|
161
|
+
|
162
|
+
# Saving and Udating methods ----------------------------------
|
163
|
+
|
164
|
+
def save(self, *args, **kwargs):
|
165
|
+
"""
|
166
|
+
Save or update the node.
|
167
|
+
|
168
|
+
Method save() acts as a fast controller. All heavy-lifting is delegated
|
169
|
+
to SQL queue. Queries are deterministic and ordered. Foelds of priority,
|
170
|
+
_path, _depth are updated in one pass. Performance is close to maximum.
|
171
|
+
"""
|
172
|
+
model = self._meta.model
|
173
|
+
|
174
|
+
# Send signal pre_save
|
175
|
+
pre_save.send(
|
176
|
+
sender=model,
|
177
|
+
instance=self,
|
178
|
+
raw=False,
|
179
|
+
using=self._state.db,
|
180
|
+
update_fields=kwargs.get("update_fields", None)
|
181
|
+
)
|
182
|
+
|
183
|
+
# Routing
|
184
|
+
is_new = False
|
185
|
+
is_shift = False
|
186
|
+
is_move = False
|
187
|
+
|
188
|
+
if self.pk:
|
189
|
+
state = self.get_db_state()
|
190
|
+
if state:
|
191
|
+
is_shift = self.priority != state["priority"]
|
192
|
+
is_move = self.parent_id != state["parent_id"]
|
193
|
+
if is_move:
|
194
|
+
self._meta.model.tasks.add("update", state["parent_id"])
|
195
|
+
else:
|
196
|
+
print("TreeNodeModel error: oject not found in DB! WTF, MF!")
|
197
|
+
else:
|
198
|
+
is_new = True
|
199
|
+
|
200
|
+
# New node ----------------------------------
|
201
|
+
if is_new:
|
202
|
+
# Step 1. Set pk
|
203
|
+
self.pk = self.db.get_next_id()
|
204
|
+
|
205
|
+
# Step 2. Set priority
|
206
|
+
if self.priority is None:
|
207
|
+
self.priority = BASE - 1
|
208
|
+
|
209
|
+
# Step 3. Set initial values
|
210
|
+
self._path = self.generate_path()
|
211
|
+
self._depth = self._path.count('.')
|
212
|
+
|
213
|
+
"""
|
214
|
+
# Move node ---------------------------------
|
215
|
+
elif is_move:
|
216
|
+
# Reserved
|
217
|
+
pass
|
218
|
+
|
219
|
+
# Shift node --------------------------------
|
220
|
+
elif is_shift:
|
221
|
+
# Reserved
|
222
|
+
pass
|
223
|
+
|
224
|
+
else:
|
225
|
+
# Reserved
|
226
|
+
pass
|
227
|
+
"""
|
228
|
+
|
229
|
+
if is_new or is_move or is_shift:
|
230
|
+
# Step 1: Shift siblings
|
231
|
+
if (is_new or is_move) and (self.priority is not None):
|
232
|
+
self._shift_siblings_forward()
|
233
|
+
# Step 2: Update paths for the new parent -> sqlq
|
234
|
+
self._meta.model.tasks.add("update", self.parent_id)
|
235
|
+
# Step 3: Clear model cache
|
236
|
+
self.clear_cache()
|
237
|
+
|
238
|
+
# Disable signals
|
239
|
+
with (disable_signals(pre_save, model),
|
240
|
+
disable_signals(post_save, model)):
|
241
|
+
super().save(*args, **kwargs)
|
242
|
+
|
243
|
+
if is_new or is_move or is_shift:
|
244
|
+
# Run sql
|
245
|
+
# self.sqlq.flush()
|
246
|
+
setattr(model, 'is_dry', True)
|
247
|
+
|
248
|
+
# Send signal post_save
|
249
|
+
post_save.send(sender=model, instance=self, created=is_new)
|
250
|
+
|
251
|
+
# Debug
|
252
|
+
# self.check_tree_integrity()
|
253
|
+
|
254
|
+
# Auxiliary methods -------------------------------------------
|
255
|
+
|
256
|
+
def get_db_state(self):
|
257
|
+
"""Read paren and priority from DB."""
|
258
|
+
with connection.cursor() as cursor:
|
259
|
+
cursor.execute(
|
260
|
+
f"SELECT priority, parent_id FROM {self._meta.db_table} WHERE id = %s", # noqa: D501
|
261
|
+
[self.pk]
|
262
|
+
)
|
263
|
+
row = cursor.fetchone()
|
264
|
+
if row:
|
265
|
+
return {"priority": row[0], "parent_id": row[1]}
|
266
|
+
return None
|
267
|
+
|
268
|
+
# Maybe in the future, sometime...
|
269
|
+
# def encode_path_order(self) -> int:
|
270
|
+
# """Encode path."""
|
271
|
+
# segments = self._path.split(".")
|
272
|
+
# value = 0
|
273
|
+
# for i, segment in enumerate(reversed(segments)):
|
274
|
+
# value += int(segment, 16) * (BASE ** i)
|
275
|
+
# return value
|
276
|
+
|
277
|
+
# def decode_path_order(self) -> str:
|
278
|
+
# """Decode path."""
|
279
|
+
# segments = []
|
280
|
+
# path_order = self._path_order
|
281
|
+
# while path_order:
|
282
|
+
# path_order, rem = divmod(path_order, BASE)
|
283
|
+
# segments.insert(0, f"{rem:0{SEGMENT_LENGTH}X}")
|
284
|
+
# return ".".join(segments) if segments else "0" * SEGMENT_LENGTH
|
285
|
+
|
286
|
+
@staticmethod
|
287
|
+
def chunked(iterable, size):
|
288
|
+
"""Split an iterable into chunks of a given size."""
|
289
|
+
it = iter(iterable)
|
290
|
+
return iter(lambda: list(islice(it, size)), [])
|
291
|
+
|
292
|
+
def check_tree_integrity(self, verbose: bool = True) -> list[str]:
|
293
|
+
"""
|
294
|
+
Check tree consistency (_path, _depth and priority).
|
295
|
+
|
296
|
+
Returns a list of errors (if any).
|
297
|
+
:param verbose: If True - prints errors to console.
|
298
|
+
How to use:
|
299
|
+
root.check_tree_integrity()
|
300
|
+
"""
|
301
|
+
model = self._meta.model
|
302
|
+
errors = []
|
303
|
+
queue = deque([self])
|
304
|
+
|
305
|
+
while queue:
|
306
|
+
node = queue.popleft()
|
307
|
+
node.refresh_from_db()
|
308
|
+
|
309
|
+
# Проверка _depth
|
310
|
+
expected_depth = node._path.count(".")
|
311
|
+
if node._depth != expected_depth:
|
312
|
+
errors.append(
|
313
|
+
f"[DEPTH] id={node.pk} _depth={node._depth} ≠ path depth={expected_depth} / parent={node.parent_id}" # noqa: D501
|
314
|
+
)
|
315
|
+
|
316
|
+
# Проверка generate_path
|
317
|
+
expected_path = ".".join(
|
318
|
+
f"{n.priority:0{SEGMENT_LENGTH}X}"
|
319
|
+
for n in node.get_ancestors(include_self=True)
|
320
|
+
)
|
321
|
+
if node._path != expected_path:
|
322
|
+
errors.append(
|
323
|
+
f"[PATH] id={node.pk} _path={node._path} ≠ expected={expected_path} / parent={node.parent_id}" # noqa: D501
|
324
|
+
)
|
325
|
+
|
326
|
+
# Проверка уникальности priority среди сиблингов
|
327
|
+
siblings = model.objects.filter(
|
328
|
+
parent=node.parent).only("pk", "priority")
|
329
|
+
priorities = [s.priority for s in siblings]
|
330
|
+
if len(priorities) != len(set(priorities)):
|
331
|
+
errors.append(
|
332
|
+
f"[PRIORITY] Duplicate priorities in siblings of id={node.pk} parent={node.parent_id}" # noqa: D501
|
333
|
+
)
|
334
|
+
|
335
|
+
queue.extend(model.objects.filter(parent=node))
|
336
|
+
|
337
|
+
if verbose and errors:
|
338
|
+
print("Tree integrity check failed:")
|
339
|
+
for err in errors:
|
340
|
+
print(" -", err)
|
341
|
+
elif verbose:
|
342
|
+
print("Tree integrity: OK ✅")
|
343
|
+
|
344
|
+
return errors
|
345
|
+
|
346
|
+
# ------------------------------------------------------------------
|
347
|
+
#
|
348
|
+
# Prived properties
|
349
|
+
#
|
350
|
+
# -----------------------------------------------------------------
|
351
|
+
|
352
|
+
@property
|
353
|
+
def _parent_id(self):
|
354
|
+
"""Lazy initialization of _parent_id."""
|
355
|
+
if not hasattr(self, "_self_parent_id"):
|
356
|
+
setattr(self, "_self_parent_id", self.parent_id)
|
357
|
+
return self._self_parent_id
|
358
|
+
|
359
|
+
@_parent_id.setter
|
360
|
+
def _parent_id(self, value):
|
361
|
+
"""Setter for _parent_id."""
|
362
|
+
setattr(self, "_self_parent_id", value)
|
363
|
+
|
364
|
+
|
365
|
+
# The End
|
treenode/settings.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNodeModel and TreeCache settings.
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.conf import settings
|
11
|
+
|
12
|
+
|
13
|
+
CACHE_LIMIT = getattr(settings, "TREENODE_CACHE_LIMIT", 100) * 1024 * 1024
|
14
|
+
|
15
|
+
# The length on Materialized Path segment
|
16
|
+
SEGMENT_LENGTH = getattr(settings, "TREENODE_SEGMENT_LENGTH", 3)
|
17
|
+
|
18
|
+
# Serialization dictionary: hexadecimal encoding, fixed segment size
|
19
|
+
SEGMENT_BASE = 16
|
20
|
+
|
21
|
+
# Nubber children per one tree node
|
22
|
+
BASE = SEGMENT_BASE ** SEGMENT_LENGTH # 4096
|
23
|
+
|
24
|
+
|
25
|
+
TREENODE_PAD_CHAR = getattr(settings, "TREENODE_PAD_CHAR", "'0'")
|
26
|
+
|
27
|
+
|
28
|
+
# The End
|
@@ -10,8 +10,9 @@ Features:
|
|
10
10
|
- Supports both light and dark themes.
|
11
11
|
- Smooth hover effects and animations.
|
12
12
|
- Consistent layout adjustments for better UI interaction.
|
13
|
+
- Visual feedback drag-n-drop operations.
|
13
14
|
|
14
|
-
Version:
|
15
|
+
Version: 3.0.0
|
15
16
|
Author: Timur Kady
|
16
17
|
Email: timurkady@yandex.com
|
17
18
|
|
@@ -97,7 +98,7 @@ Email: timurkady@yandex.com
|
|
97
98
|
line-height: 10px;
|
98
99
|
padding: 1px;
|
99
100
|
cursor: ns-resize;
|
100
|
-
opacity: 0.
|
101
|
+
opacity: 0.75;
|
101
102
|
}
|
102
103
|
|
103
104
|
.treenode-drag-handle:hover {
|
@@ -111,3 +112,43 @@ Email: timurkady@yandex.com
|
|
111
112
|
.treenode-wrapper {
|
112
113
|
display: inline-block;
|
113
114
|
}
|
115
|
+
|
116
|
+
tr.treenode-placeholder td {
|
117
|
+
background-color: #eef;
|
118
|
+
border-top: 1px solid var(--hairline-color);
|
119
|
+
border-bottom: 1px solid var(--hairline-color);
|
120
|
+
border-left: 0px;
|
121
|
+
margin: 0;
|
122
|
+
padding: 0;
|
123
|
+
height: 30px;
|
124
|
+
}
|
125
|
+
|
126
|
+
tr.target-as-child{
|
127
|
+
border-left: 4px solid #4caf50;
|
128
|
+
transition: border-left 0.2s ease;
|
129
|
+
}
|
130
|
+
|
131
|
+
tr.target-as-child td {
|
132
|
+
background-color: #d9fbe3 !important;
|
133
|
+
transition: background 0.2s ease;
|
134
|
+
}
|
135
|
+
|
136
|
+
tr.flash-insert td {
|
137
|
+
animation: flash-green 0.6s ease-in-out;
|
138
|
+
}
|
139
|
+
|
140
|
+
@keyframes flash-green {
|
141
|
+
0% { background-color: #dbffe0; }
|
142
|
+
100% { background-color: transparent; }
|
143
|
+
}
|
144
|
+
|
145
|
+
tr.dragging td {
|
146
|
+
opacity: 0.6;
|
147
|
+
background-color: #f8f8f8;
|
148
|
+
}
|
149
|
+
|
150
|
+
.column-treenode_field {
|
151
|
+
width: 100%;
|
152
|
+
}
|
153
|
+
|
154
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
.tabs {
|
2
|
+
list-style: none;
|
3
|
+
padding: 0 !important;
|
4
|
+
margin:0 !important;
|
5
|
+
display: flex;
|
6
|
+
gap: 0em;
|
7
|
+
border-bottom: 1px solid var(--border-color);
|
8
|
+
}
|
9
|
+
|
10
|
+
.tab {
|
11
|
+
padding: 0.5em 1em;
|
12
|
+
cursor: pointer;
|
13
|
+
color: var(--object-tools-fg);
|
14
|
+
background: var(--object-tools-bg);
|
15
|
+
border-radius: 4px;
|
16
|
+
}
|
17
|
+
|
18
|
+
.tab.active {
|
19
|
+
background: var(--primary);
|
20
|
+
font-weight: bold;
|
21
|
+
}
|
22
|
+
|
23
|
+
.tab-content {
|
24
|
+
border: 1px solid var(--border-color);
|
25
|
+
padding: 1em;
|
26
|
+
border-radius: 4px;
|
27
|
+
margin-top: 1em;
|
28
|
+
}
|
29
|
+
|
30
|
+
.tabs li.tab {
|
31
|
+
list-style-type: none;
|
32
|
+
margin: 0;
|
33
|
+
border-radius: 5px 5px 0 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
.tab-content a.button {
|
37
|
+
display: inline-block;
|
38
|
+
text-align: center;
|
39
|
+
padding: 10px 10px !important;
|
40
|
+
width: 110px;
|
41
|
+
|
42
|
+
}
|
43
|
+
|
44
|
+
.tab-content form > .form-row {
|
45
|
+
height: 60px;
|
46
|
+
}
|
47
|
+
|
48
|
+
.tab-content .button.default {
|
49
|
+
display: inline-block;
|
50
|
+
width: 130px;
|
51
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);
|
@@ -36,19 +36,9 @@ Email: timurkady@yandex.com
|
|
36
36
|
// Get URL for AJAX and model data from data attributes
|
37
37
|
var ajaxUrl = $select.data('url');
|
38
38
|
var ajaxUrlChildren = $select.data('url-children');
|
39
|
-
var
|
40
|
-
if (typeof forwardData === "string" && forwardData.trim().length > 0) {
|
41
|
-
try {
|
42
|
-
forwardData = JSON.parse(forwardData.replace(/"/g, '"'));
|
43
|
-
} catch (e) {
|
44
|
-
console.error("Invalid JSON in data-forward:", forwardData, e);
|
45
|
-
forwardData = {};
|
46
|
-
}
|
47
|
-
} else {
|
48
|
-
forwardData = {};
|
49
|
-
}
|
39
|
+
var model = $select.attr("data-model");
|
50
40
|
|
51
|
-
var selectedId = $select.
|
41
|
+
var selectedId = $select.val();
|
52
42
|
if (selectedId === undefined) {
|
53
43
|
selectedId = "";
|
54
44
|
}
|
@@ -60,9 +50,8 @@ Email: timurkady@yandex.com
|
|
60
50
|
$display: $display,
|
61
51
|
ajaxUrl: ajaxUrl,
|
62
52
|
urlChildren: ajaxUrlChildren,
|
63
|
-
model:
|
53
|
+
model: model,
|
64
54
|
selectedId: selectedId,
|
65
|
-
mode: selectedId ? 'selected' : 'default'
|
66
55
|
};
|
67
56
|
|
68
57
|
$widget.data('widgetData', widgetData);
|
@@ -77,17 +66,14 @@ Email: timurkady@yandex.com
|
|
77
66
|
|
78
67
|
// Method of loading data via AJAX
|
79
68
|
loadData: function (widgetData, searchQuery) {
|
80
|
-
var params = {
|
69
|
+
var params = {
|
70
|
+
model: widgetData.model,
|
71
|
+
select_id: widgetData.selectedId
|
72
|
+
};
|
81
73
|
if (searchQuery) {
|
82
74
|
params.q = searchQuery;
|
83
|
-
|
84
|
-
|
85
|
-
params.select_id = widgetData.selectedId;
|
86
|
-
widgetData.mode = 'selected';
|
87
|
-
} else {
|
88
|
-
widgetData.mode = 'default';
|
89
|
-
}
|
90
|
-
|
75
|
+
}
|
76
|
+
|
91
77
|
$.ajax({
|
92
78
|
url: widgetData.ajaxUrl,
|
93
79
|
data: params,
|