Skip to content

Commit f5000d1

Browse files
beryczboxed
authored andcommitted
reorderable EditTable
1 parent f6af38b commit f5000d1

5 files changed

Lines changed: 147 additions & 60 deletions

File tree

iommi/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from iommi.edit_table import (
1616
EditColumn,
1717
EditTable,
18+
register_edit_column_factory,
1819
)
1920
from iommi.form import (
2021
Field,
@@ -140,6 +141,7 @@ def inner(request, *args, **kwargs):
140141
'LAST',
141142
'MISSING',
142143
'register_factory',
144+
'register_edit_column_factory',
143145
'register_field_factory',
144146
'register_filter_factory',
145147
'register_column_factory',

iommi/edit_table.py

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
from collections import defaultdict
2+
import json
23
from typing import (
4+
Any,
35
Dict,
46
Optional,
57
Type,
8+
Union,
69
)
710

811
from django.db.models import QuerySet
912
from django.http import HttpResponseRedirect
10-
from django.template import (
11-
Context,
12-
Template,
13-
)
13+
from django.template import Context
1414
from django.utils.safestring import mark_safe
1515
from django.utils.translation import gettext_lazy
1616

17-
from iommi._web_compat import render_template
17+
from iommi._web_compat import (
18+
render_template,
19+
Template,
20+
)
1821
from iommi.action import (
1922
Action,
2023
Actions,
2124
group_actions,
2225
)
23-
from iommi.asset import Asset
26+
from iommi.sort_after import LAST
2427
from iommi.base import (
2528
MISSING,
2629
NOT_BOUND_MESSAGE,
@@ -46,6 +49,7 @@
4649
Field,
4750
Form,
4851
)
52+
from iommi.from_model import member_from_model
4953
from iommi.member import (
5054
bind_member,
5155
bind_members,
@@ -57,16 +61,32 @@
5761
refinable,
5862
EvaluatedRefinable,
5963
)
60-
from iommi.shortcut import with_defaults
64+
from iommi.shortcut import (
65+
Shortcut,
66+
with_defaults,
67+
)
6168
from iommi.struct import Struct
6269
from iommi.table import (
70+
_column_factory_by_field_type,
6371
Cell,
6472
Cells,
6573
Column,
6674
Table,
6775
)
6876
from iommi.evaluate import evaluate
6977

78+
from ._db_compat import base_defaults_factory
79+
80+
_edit_column_factory_by_field_type = {}
81+
82+
83+
def register_edit_column_factory(django_field_class, *, shortcut_name=MISSING, factory=MISSING, **kwargs):
84+
assert shortcut_name is not MISSING or factory is not MISSING
85+
if factory is MISSING:
86+
factory = Shortcut(call_target__attribute=shortcut_name, **kwargs)
87+
88+
_edit_column_factory_by_field_type[django_field_class] = factory
89+
7090

7191
class EditCell(Cell):
7292
def get_path(self):
@@ -158,6 +178,23 @@ class EditColumn(Column):
158178

159179
field: Field = Refinable()
160180

181+
@classmethod
182+
@dispatch(
183+
filter__call_target__attribute='from_model',
184+
bulk__call_target__attribute='from_model',
185+
)
186+
def from_model(cls, model=None, model_field_name=None, model_field=None, **kwargs):
187+
return member_from_model(
188+
cls=cls,
189+
model=model,
190+
factory_lookup={**_column_factory_by_field_type, **_edit_column_factory_by_field_type},
191+
factory_lookup_register_function=register_edit_column_factory,
192+
model_field_name=model_field_name,
193+
model_field=model_field,
194+
defaults_factory=base_defaults_factory,
195+
**kwargs,
196+
)
197+
161198
def on_refine_done(self):
162199
super(EditColumn, self).on_refine_done()
163200
self.field = None
@@ -220,10 +257,17 @@ def hardcoded(cls, **kwargs):
220257

221258
@classmethod
222259
@with_defaults(
260+
# TODO this would be better, but it doesn't work and idk why
261+
# header__template=Template('<th{{ header.attrs }}></th>'),
223262
sortable=False,
224-
# field__call_target__attribute='hidden', # TODO TypeError(Field object has no refinable attribute(s): "call_target".)
263+
# TODO
264+
# field__call_target__attribute='hidden',
265+
# call_target raises TypeError(Field object has no refinable attribute(s): "call_target".)
266+
# so "temporary" fix to set attrs__type=hidden (works for inputs only):
267+
field__input__attrs__type='hidden',
225268
field__include=True,
226-
display_name=gettext_lazy('Reorder'),
269+
cell__attrs__title=gettext_lazy('Drag and drop to reorder'),
270+
after=LAST,
227271
**{
228272
'cell__attrs__class__reordering-handle-cell': True,
229273
'field__input__attrs__data-reordering-value': True,
@@ -378,7 +422,7 @@ class EditTable(Table):
378422
form_class: Type[Form] = Refinable()
379423
parent_form: Optional[Form] = Refinable()
380424
edit_actions: Dict[str, Action] = RefinableMembers()
381-
reorderable: bool = EvaluatedRefinable()
425+
reorderable: Union[bool, Dict[str, Any], None] = EvaluatedRefinable()
382426

383427
class Meta:
384428
form_class = Form
@@ -404,8 +448,7 @@ class Meta:
404448
create_form = EMPTY
405449
container__children__text__template = 'iommi/table/edit_table_container.html'
406450

407-
reorderable = lambda table, **_: not table.sortable
408-
tbody__attrs = {'data-reorderable': lambda table, **_: evaluate(table.reorderable, **table.iommi_evaluate_parameters()) or None}
451+
reorderable = False
409452

410453
bulk__include = True
411454

@@ -508,6 +551,29 @@ def on_refine_done(self):
508551
if self.create_form is not None:
509552
self.create_form = self.create_form.refine_defaults(fields=declared_fields).refine_done()
510553

554+
def bind(self, *, parent=None, request=None):
555+
result = super(EditTable, self).bind(parent=parent, request=request)
556+
557+
if result is None:
558+
return result
559+
560+
reorder_handles = []
561+
for column_name, column in items(result.columns):
562+
if 'reorder_handle' in getattr(column, 'iommi_shortcut_stack', []):
563+
reorder_handles.append(column_name)
564+
if reorder_handles:
565+
assert len(reorder_handles) == 1, "You cannot have multiple EditColumn.reorder_handle in an EditTable!"
566+
if not result.reorderable:
567+
result.reorderable = True
568+
result.sortable = False
569+
if result.reorderable:
570+
result.tbody.attrs['data-reorderable'] = json.dumps(result.reorderable) if isinstance(result.reorderable, dict) else result.reorderable
571+
572+
if result.sortable and result.reorderable:
573+
raise NotImplementedError("sortable and reorderable cannot be used simultaneously")
574+
575+
return result
576+
511577
def on_bind(self) -> None:
512578
super(EditTable, self).on_bind()
513579
bind_members(self, name='edit_actions')
@@ -532,18 +598,6 @@ def on_bind(self) -> None:
532598

533599
self.tbody.children.text = _EditTable_Lazy_tbody(self)
534600

535-
reorder_handles = []
536-
for column_name, column in items(self.columns):
537-
if 'reorder_handle' in getattr(column, 'iommi_shortcut_stack', []):
538-
reorder_handles.append(column_name)
539-
if reorder_handles:
540-
assert len(reorder_handles) == 1, "You cannot have multiple EditColumn.reorder_handle in an EditTable!"
541-
self.reorderable = True
542-
self.sortable = False
543-
544-
if self.sortable and self.reorderable:
545-
raise ValueError("sortable and reorderable cannot be used simultaneously")
546-
547601
def is_valid(self):
548602
return not self.edit_errors and not self.create_errors
549603

iommi/static/css/iommi.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,9 @@ div[data-iommi-edit-table-delete-with="checkbox"] input[data-iommi-edit-table-de
5858
.reordering-handle-cell {
5959
cursor: move;
6060
cursor: -webkit-grabbing;
61+
width: 30px;
62+
}
63+
.reordering-handle-cell::after {
64+
content: "||";
6165
}
6266
/* /EditTable */

iommi/static/js/iommi.js

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -470,52 +470,63 @@ class IommiBase {
470470
}
471471
const {html} = await SELF.fetchJson(url);
472472

473-
let virtual_pk = parseInt(table.getAttribute('data-next-virtual-pk'), 10);
474-
virtual_pk -= 1;
475-
virtual_pk = virtual_pk.toString();
476-
table.setAttribute('data-next-virtual-pk', virtual_pk);
473+
let virtualPK = parseInt(table.getAttribute('data-next-virtual-pk'), 10);
474+
virtualPK -= 1;
475+
virtualPK = virtualPK.toString();
476+
table.setAttribute('data-next-virtual-pk', virtualPK);
477477

478478
let tpl = document.createElement('template');
479-
tpl.innerHTML = html.trim().replaceAll('#sentinel#', virtual_pk);
480-
let tbody_path = this.dataset.iommiEditTablePath === "" ? 'tbody' : `${this.dataset.iommiEditTablePath}__tbody`;
481-
const tbody = table.querySelector(`[data-iommi-path=${tbody_path}`);
479+
tpl.innerHTML = html.trim().replaceAll('#sentinel#', virtualPK);
480+
let tbodyPath = this.dataset.iommiEditTablePath === "" ? 'tbody' : `${this.dataset.iommiEditTablePath}__tbody`;
481+
const tbody = table.querySelector(`[data-iommi-path=${tbodyPath}`);
482482
tpl.content.childNodes.forEach((el) => {
483483
const appendedElement = tbody.appendChild(el);
484-
if (SELF.select2 && appendedElement.querySelector('.select2_enhance')) {
485-
SELF.select2.initAll(appendedElement);
486-
}
484+
appendedElement.dispatchEvent(
485+
new CustomEvent('iommi.editTable.newElement', {
486+
bubbles: true,
487+
detail: {tbody: tbody, virtualPK: virtualPK}
488+
})
489+
);
487490
});
488491
}
489492
);
490493
}
491494
}
492495

493496
class IommiSelect2 {
497+
defaultSelector = '.select2_enhance';
498+
494499
constructor() {
495500
const SELF = this;
496501
document.addEventListener('iommi.element.populated', (event) => {
497502
SELF.initAll(event.target);
498503
});
504+
505+
document.addEventListener('iommi.editTable.newElement', (event) => {
506+
if(event.target.querySelector(this.defaultSelector)) {
507+
SELF.initAll(event.target);
508+
}
509+
});
499510
}
500511

501512
onCompleteReadyState() {
502513
this.initAll();
503514
}
504515

505-
initAll(parent, selector, extra_options) {
516+
initAll(parent, selector, extraOptions) {
506517
const SELF = this;
507518
if (!parent) {
508519
parent = document;
509520
}
510521
if (!selector) {
511-
selector = '.select2_enhance';
522+
selector = this.defaultSelector;
512523
}
513524
$(selector, parent).each(function (_, x) {
514-
SELF.initOne(x, extra_options);
525+
SELF.initOne(x, extraOptions);
515526
});
516527
// Second time is a workaround because the table might resize on select2-ification
517528
$(selector, parent).each(function (_, x) {
518-
SELF.initOne(x, extra_options);
529+
SELF.initOne(x, extraOptions);
519530
});
520531
}
521532

@@ -549,7 +560,7 @@ class IommiSelect2 {
549560
}
550561
}
551562

552-
initOne(elem, extra_options) {
563+
initOne(elem, extraOptions) {
553564
let f = $(elem);
554565
let endpointPath = f.attr('data-choices-endpoint');
555566
let multiple = f.attr('multiple') !== undefined;
@@ -579,8 +590,8 @@ class IommiSelect2 {
579590
}
580591
}
581592
}
582-
if (extra_options) {
583-
$.extend(options, extra_options);
593+
if (extraOptions) {
594+
$.extend(options, extraOptions);
584595
}
585596
f.select2(options);
586597
f.on('change', function (e) {
@@ -594,53 +605,66 @@ class IommiSelect2 {
594605
}
595606

596607
class IommiReorderable {
608+
defaultSelector = '[data-reorderable]';
609+
597610
constructor() {
598611
const SELF = this;
599612
document.addEventListener('iommi.element.populated', (event) => {
600613
SELF.initAll(event.target);
601614
});
615+
616+
document.addEventListener('iommi.editTable.newElement', (event) => {
617+
const tbody = event.target.closest(this.defaultSelector);
618+
if(tbody) {
619+
SELF.recalculate(tbody);
620+
}
621+
});
602622
}
603623

604624
onCompleteReadyState() {
605625
this.initAll();
606626
}
607627

608-
initAll(parent, selector, extra_options) {
609-
const SELF = this;
628+
recalculate(tbody) {
629+
let index = 0;
630+
for (let item of tbody.children) {
631+
item.querySelector(tbody.dataset.reorderableFieldSelector).value = index;
632+
index += 1;
633+
}
634+
}
635+
636+
initAll(parent, selector, extraOptions) {
610637
if (!parent) {
611638
parent = document;
612639
}
613640
if (!selector) {
614-
selector = '[data-reorderable]';
641+
selector = this.defaultSelector;
615642
}
616-
parent.querySelectorAll(selector).forEach((el) => {this.initOne(el, extra_options)});
643+
parent.querySelectorAll(selector).forEach((el) => {this.initOne(el, extraOptions)});
617644
}
618645

619-
initOne(elem, extra_options) {
646+
initOne(elem, extraOptions) {
620647
const options = {
621648
animation: 150
622649
};
623650
if(elem.dataset.reorderableHandleSelector) {
624651
options.handle = elem.dataset.reorderableHandleSelector;
625652
}
626-
let required_options = {}
653+
let requiredOptions = {}
627654
if(elem.dataset.reorderable.startsWith("{")) {
628-
required_options = JSON.parse(elem.dataset.reorderable);
655+
requiredOptions = JSON.parse(elem.dataset.reorderable);
629656
}
657+
const SELF = this;
630658
if(elem.dataset.reorderableFieldSelector) {
631-
options.onUpdate = function (event) {
632-
let index = 0;
633-
for (let item of event.target.children) {
634-
item.querySelector(elem.dataset.reorderableFieldSelector).value = index;
635-
index += 1;
636-
}
659+
options.onUpdate = function(event) {
660+
SELF.recalculate(elem);
637661
}
638662
}
639663

640664
if (!elem.iommi) {
641665
elem.iommi = {};
642666
}
643-
elem.iommi.reorderable = new Sortable(elem, Object.assign(options, required_options, extra_options));
667+
elem.iommi.reorderable = new Sortable(elem, Object.assign(options, requiredOptions, extraOptions));
644668
}
645669
}
646670

0 commit comments

Comments
 (0)