django-fast-treenode 2.1.5__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.
Files changed (107) hide show
  1. django_fast_treenode-3.0.0.dist-info/METADATA +203 -0
  2. django_fast_treenode-3.0.0.dist-info/RECORD +90 -0
  3. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/WHEEL +1 -1
  4. treenode/admin/__init__.py +0 -5
  5. treenode/admin/admin.py +137 -208
  6. treenode/admin/changelist.py +21 -39
  7. treenode/admin/exporter.py +170 -0
  8. treenode/admin/importer.py +171 -0
  9. treenode/admin/mixin.py +291 -0
  10. treenode/apps.py +42 -20
  11. treenode/cache.py +192 -303
  12. treenode/forms.py +45 -65
  13. treenode/managers/__init__.py +4 -20
  14. treenode/managers/managers.py +216 -0
  15. treenode/managers/queries.py +233 -0
  16. treenode/managers/tasks.py +167 -0
  17. treenode/models/__init__.py +8 -5
  18. treenode/models/decorators.py +54 -0
  19. treenode/models/factory.py +44 -68
  20. treenode/models/mixins/__init__.py +2 -1
  21. treenode/models/mixins/ancestors.py +44 -20
  22. treenode/models/mixins/children.py +33 -26
  23. treenode/models/mixins/descendants.py +33 -22
  24. treenode/models/mixins/family.py +25 -15
  25. treenode/models/mixins/logical.py +23 -21
  26. treenode/models/mixins/node.py +162 -104
  27. treenode/models/mixins/properties.py +22 -16
  28. treenode/models/mixins/roots.py +59 -15
  29. treenode/models/mixins/siblings.py +46 -43
  30. treenode/models/mixins/tree.py +212 -153
  31. treenode/models/mixins/update.py +154 -0
  32. treenode/models/models.py +365 -0
  33. treenode/settings.py +28 -0
  34. treenode/static/{treenode/css → css}/tree_widget.css +1 -1
  35. treenode/static/{treenode/css → css}/treenode_admin.css +43 -2
  36. treenode/static/css/treenode_tabs.css +51 -0
  37. treenode/static/js/lz-string.min.js +1 -0
  38. treenode/static/{treenode/js → js}/tree_widget.js +9 -23
  39. treenode/static/js/treenode_admin.js +531 -0
  40. treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -0
  41. treenode/static/vendors/jquery-ui/LICENSE.txt +43 -0
  42. treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -0
  43. treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  44. treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  45. treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  46. treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  47. treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  48. treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  49. treenode/static/vendors/jquery-ui/index.html +297 -0
  50. treenode/static/vendors/jquery-ui/jquery-ui.css +438 -0
  51. treenode/static/vendors/jquery-ui/jquery-ui.js +5223 -0
  52. treenode/static/vendors/jquery-ui/jquery-ui.min.css +7 -0
  53. treenode/static/vendors/jquery-ui/jquery-ui.min.js +6 -0
  54. treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -0
  55. treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +5 -0
  56. treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -0
  57. treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +5 -0
  58. treenode/static/vendors/jquery-ui/package.json +82 -0
  59. treenode/templates/admin/treenode_changelist.html +25 -0
  60. treenode/templates/admin/treenode_import_export.html +85 -0
  61. treenode/templates/admin/treenode_rows.html +57 -0
  62. treenode/tests.py +3 -0
  63. treenode/urls.py +6 -27
  64. treenode/utils/__init__.py +0 -15
  65. treenode/utils/db/__init__.py +7 -0
  66. treenode/utils/db/compiler.py +114 -0
  67. treenode/utils/db/db_vendor.py +50 -0
  68. treenode/utils/db/service.py +84 -0
  69. treenode/utils/db/sqlcompat.py +60 -0
  70. treenode/utils/db/sqlquery.py +70 -0
  71. treenode/version.py +2 -2
  72. treenode/views/__init__.py +5 -0
  73. treenode/views/autoapi.py +91 -0
  74. treenode/views/autocomplete.py +52 -0
  75. treenode/views/children.py +41 -0
  76. treenode/views/common.py +23 -0
  77. treenode/views/crud.py +209 -0
  78. treenode/views/search.py +48 -0
  79. treenode/widgets.py +27 -44
  80. django_fast_treenode-2.1.5.dist-info/METADATA +0 -165
  81. django_fast_treenode-2.1.5.dist-info/RECORD +0 -63
  82. treenode/admin/mixins.py +0 -302
  83. treenode/managers/adjacency.py +0 -205
  84. treenode/managers/closure.py +0 -278
  85. treenode/models/adjacency.py +0 -342
  86. treenode/models/classproperty.py +0 -27
  87. treenode/models/closure.py +0 -122
  88. treenode/static/treenode/js/.gitkeep +0 -1
  89. treenode/static/treenode/js/treenode_admin.js +0 -131
  90. treenode/templates/admin/export_success.html +0 -26
  91. treenode/templates/admin/tree_node_changelist.html +0 -19
  92. treenode/templates/admin/tree_node_export.html +0 -27
  93. treenode/templates/admin/tree_node_import.html +0 -45
  94. treenode/templates/admin/tree_node_import_report.html +0 -32
  95. treenode/templates/widgets/tree_widget.css +0 -23
  96. treenode/utils/aid.py +0 -46
  97. treenode/utils/base16.py +0 -38
  98. treenode/utils/base36.py +0 -37
  99. treenode/utils/db.py +0 -116
  100. treenode/utils/exporter.py +0 -196
  101. treenode/utils/importer.py +0 -328
  102. treenode/utils/radix.py +0 -61
  103. treenode/views.py +0 -184
  104. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/licenses/LICENSE +0 -0
  105. {django_fast_treenode-2.1.5.dist-info → django_fast_treenode-3.0.0.dist-info}/top_level.txt +0 -0
  106. /treenode/static/{treenode → css}/.gitkeep +0 -0
  107. /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
@@ -34,7 +34,7 @@ Email: timurkady@yandex.com
34
34
 
35
35
  */
36
36
 
37
- .form-row.field-tn_parent {
37
+ .form-row.field-parent {
38
38
  position: relative;
39
39
  overflow: visible !important;
40
40
  z-index: auto;
@@ -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: 2.1.0
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.25;
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 forwardData = $select.attr("data-forward");
40
- if (typeof forwardData === "string" && forwardData.trim().length > 0) {
41
- try {
42
- forwardData = JSON.parse(forwardData.replace(/&quot;/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.data('selected'); // Получаем значение data-selected
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: forwardData.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 = {model: widgetData.model};
69
+ var params = {
70
+ model: widgetData.model,
71
+ select_id: widgetData.selectedId
72
+ };
81
73
  if (searchQuery) {
82
74
  params.q = searchQuery;
83
- widgetData.mode = 'search';
84
- } else if (widgetData.selectedId) {
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,