Skip to content

Commit 56cb169

Browse files
authored
feat: add new item (#92)
* assign dynamically rendered list to this.sortableList * refactor how data items get mapped by introducing addMappingProxyToItem() * some refactoring around types etc. * code reorder * add addNewItem() to the DataEngine class * add missing trailing commas * add addNewItem() to the NestedSort class * update to samples to include controls for adding new items
1 parent e0d2198 commit 56cb169

File tree

8 files changed

+333
-73
lines changed

8 files changed

+333
-73
lines changed

dev/mapped-data-driven-list.html

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@
1313
<div class="container">
1414
<h1>A mapped data-driven list</h1>
1515

16-
<p>The main goal is to create a tree-like list of nested items. You should be able to achieve that by simply dragging and dropping the items using your mouse. Touch screens are not supported yet! 😐</p>
16+
<p>The main goal is to create a tree-like list of nested items. You should be able to achieve that by simply dragging and dropping the items using your mouse.</p>
17+
18+
<input type="checkbox" id="property-mapping" checked onchange="updateList()">
19+
<label for="property-mapping">Use property mapping</label>
20+
21+
<div style="margin: 20px 0;">
22+
<input type="checkbox" id="new-item-position">
23+
<label for="new-item-position">New item should sit at the end of the list</label>
24+
<br>
25+
<input type="button" value="Add New Item" id="add-new-item" style="margin-top: 5px;">
26+
</div>
1727

1828
<div class="sample-wrap">
1929
<div id="draggable"></div>
@@ -24,38 +34,79 @@ <h1>A mapped data-driven list</h1>
2434
<!-- Scripts -->
2535
<script src="../dist/nested-sort.umd.min.js"></script>
2636
<script>
27-
(function() {
28-
const data = [
29-
{ item_id: 1, item_title: 'One', position: 5 },
30-
{ item_id: 11, item_title: 'One-One', item_parent: 1, position: 1 },
31-
{ item_id: 2, item_title: 'Two', position: 1 },
32-
{ item_id: 3, item_title: 'Three', position: 2 },
33-
{ item_id: 1121, item_title: 'One-One-Two-One', item_parent: 112, position: 2 },
34-
{ item_id: 1123, item_title: 'One-One-Two-Three', item_parent: 112, position: 4 },
35-
{ item_id: 12, item_title: 'One-Two', item_parent: 1, position: 2 },
36-
{ item_id: 111, item_title: 'One-One-One', item_parent: 11, position: 1 },
37-
{ item_id: 112, item_title: 'One-One-Two', item_parent: 11, position: 2 },
38-
]
39-
40-
new NestedSort({
37+
const logData = (data) => {
38+
const resultWrap = document.querySelector('.result-wrap')
39+
resultWrap.innerHTML = JSON.stringify(data, null, 2)
40+
}
41+
let data = [
42+
{ id: 1, text: 'One', order: 5 },
43+
{ id: 11, text: 'One-One', parent: 1, order: 1 },
44+
{ id: 2, text: 'Two', order: 1 },
45+
{ id: 3, text: 'Three', order: 2 },
46+
{ id: 1121, text: 'One-One-Two-One', parent: 112, order: 2 },
47+
{ id: 1123, text: 'One-One-Two-Three', parent: 112, order: 4 },
48+
{ id: 12, text: 'One-Two', parent: 1, order: 2 },
49+
{ id: 111, text: 'One-One-One', parent: 11, order: 1 },
50+
{ id: 112, text: 'One-One-Two', parent: 11, order: 2 },
51+
]
52+
const propertyMap = {
53+
id: 'item_id',
54+
parent: 'item_parent',
55+
text: 'item_title',
56+
order: 'position',
57+
}
58+
59+
document.getElementById('add-new-item').addEventListener('click', () => {
60+
if (!window.ns) return
61+
62+
const item_title = prompt('New item name:')
63+
if (!item_title) return
64+
65+
const item_id = item_title.toLowerCase().replaceAll(' ', '-')
66+
if (document.querySelector(`#draggable [data-id="${item_id}"]`)) {
67+
return alert('This is a duplicate item! Try another one please.')
68+
}
69+
70+
const usePropertyMapping = document.getElementById('property-mapping').checked
71+
const asLastChild = document.getElementById('new-item-position').checked
72+
73+
const item = usePropertyMapping
74+
? { [propertyMap.id]: item_id, [propertyMap.text]: item_title }
75+
: { id: item_id, text: item_title }
76+
77+
const { data } = window.ns.addNewItem({ item, asLastChild })
78+
logData(data)
79+
})
80+
81+
function updateList() {
82+
if (window.ns) {
83+
window.ns.destroy()
84+
}
85+
86+
const usePropertyMapping = document.getElementById('property-mapping').checked
87+
88+
window.ns = new NestedSort({
4189
actions: {
4290
onDrop: function (data) {
43-
const resultWrap = document.querySelector('.result-wrap')
44-
resultWrap.innerHTML = JSON.stringify(data, null, ' ')
91+
logData(data)
4592
console.log(data)
4693
}
4794
},
48-
data: data,
49-
propertyMap: {
50-
id: 'item_id',
51-
parent: 'item_parent',
52-
text: 'item_title',
53-
order: 'position',
54-
},
95+
data: usePropertyMapping
96+
? data.map(({ id, text, order, parent }) => ({
97+
[propertyMap.id]: id,
98+
[propertyMap.parent]: parent,
99+
[propertyMap.text]: text,
100+
[propertyMap.order]: order,
101+
}))
102+
: [...data],
103+
propertyMap: usePropertyMapping ? propertyMap : undefined,
55104
el: '#draggable',
56105
droppingEdge: 5
57106
})
58-
})()
107+
}
108+
109+
updateList()
59110
</script>
60111
</body>
61112
</html>

dev/server-rendered-list.html

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,29 @@ <h1>A server-rendered list</h1>
2121
<input type="checkbox" id="property-mapping" onchange="updateList()">
2222
<label for="property-mapping">Use property mapping (affects the list data structure logged to the console after each drag and drop)</label>
2323

24+
<div style="margin: 20px 0;">
25+
<input type="checkbox" id="new-item-position">
26+
<label for="new-item-position">New item should sit at the end of the list</label>
27+
<br>
28+
<input type="button" value="Add New Item" id="add-new-item" style="margin-top: 5px;">
29+
</div>
30+
2431
<div class="sample-wrap">
2532
<ul id="draggable">
26-
<li data-id="1">Topic 1</li>
27-
<li data-id="2">Topic 2</li>
28-
<li data-id="3">Topic 3
29-
<ul data-id="3">
30-
<li data-id="31">Topic 3-1</li>
31-
<li data-id="32">Topic 3-2</li>
32-
<li data-id="33">Topic 3-3</li>
33+
<li data-id="topic-1">Topic 1</li>
34+
<li data-id="topic-2">Topic 2</li>
35+
<li data-id="topic-3">Topic 3
36+
<ul data-id="topic-3">
37+
<li data-id="topic-31">Topic 3-1</li>
38+
<li data-id="topic-32">Topic 3-2</li>
39+
<li data-id="topic-33">Topic 3-3</li>
3340
</ul>
3441
</li>
35-
<li data-id="4">Topic 4</li>
36-
<li data-id="5">Topic 5</li>
37-
<li data-id="6">Topic 6</li>
38-
<li data-id="7">Topic 7</li>
39-
<li data-id="8">Topic 8</li>
42+
<li data-id="topic-4">Topic 4</li>
43+
<li data-id="topic-5">Topic 5</li>
44+
<li data-id="topic-6">Topic 6</li>
45+
<li data-id="topic-7">Topic 7</li>
46+
<li data-id="topic-8">Topic 8</li>
4047
</ul>
4148

4249
<pre class="result-wrap"></pre>
@@ -46,21 +53,49 @@ <h1>A server-rendered list</h1>
4653
<!-- Scripts -->
4754
<script src="../dist/nested-sort.umd.js"></script>
4855
<script>
56+
const logData = (data) => {
57+
const resultWrap = document.querySelector('.result-wrap')
58+
resultWrap.innerHTML = JSON.stringify(data, null, 2)
59+
}
60+
61+
document.getElementById('add-new-item').addEventListener('click', () => {
62+
if (!window.ns) return
63+
64+
const item_name = prompt('New item name:')
65+
if (!item_name) return
66+
67+
const item_id = item_name.toLowerCase().replaceAll(' ', '-')
68+
if (document.querySelector(`#draggable [data-id="${item_id}"]`)) {
69+
return alert('This is a duplicate item! Try another one please.')
70+
}
71+
72+
const usePropertyMapping = document.getElementById('property-mapping').checked
73+
const asLastChild = document.getElementById('new-item-position').checked
74+
75+
const item = usePropertyMapping
76+
? { item_id, item_name }
77+
: { id: item_id, text: item_name }
78+
79+
const { data } = window.ns.addNewItem({ item, asLastChild })
80+
logData(data)
81+
})
82+
4983
function updateList() {
5084
if (window.ns) window.ns.destroy()
5185

5286
const usePropertyMapping = document.getElementById('property-mapping').checked
5387
window.ns = new NestedSort({
5488
actions: {
5589
onDrop: function (data) {
56-
const resultWrap = document.querySelector('.result-wrap')
57-
resultWrap.innerHTML = JSON.stringify(data, null, ' ')
90+
logData(data)
5891
console.log(`data ${usePropertyMapping ? 'with' : 'without'} property mapping`, data)
5992
}
6093
},
6194
propertyMap: usePropertyMapping ? {
6295
id: 'item_id',
6396
order: 'position',
97+
parent: 'child_of',
98+
text: 'item_name',
6499
} : undefined,
65100
el: '#draggable',
66101
droppingEdge: 5

src/data-engine.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AddNewItemArgs,
23
DataEngineOptions,
34
DataItem,
45
ListElement,
@@ -11,8 +12,9 @@ class DataEngine {
1112
data: Array<DataItem>
1213
sortedData: Array<DataItem>
1314
sortedDataDomArray: Array<HTMLElement>
14-
propertyMap: Partial<PropertyMap>
15+
propertyMap: PropertyMap
1516
renderListItem: RenderListItemFn
17+
boundGetItemPropProxyName: (prop: string | symbol) => string
1618

1719
/**
1820
* @constructor
@@ -23,25 +25,25 @@ class DataEngine {
2325
this.sortedDataDomArray = []
2426
this.propertyMap = propertyMap
2527
this.renderListItem = renderListItem
28+
this.boundGetItemPropProxyName = this.getItemPropProxyName.bind(this)
2629

2730
this.maybeTransformData()
2831
}
2932

33+
addMappingProxyToItem(item: DataItem): DataItem {
34+
return new Proxy(item, {
35+
get: (target, prop, receiver) => {
36+
return Reflect.get(target, this.boundGetItemPropProxyName(prop), receiver)
37+
},
38+
})
39+
}
40+
3041
maybeTransformData(): void {
3142
if (!Object.keys(this.propertyMap).length || !Array.isArray(this.data)) return
32-
33-
const getItemPropProxyName = this.getItemPropProxyName.bind(this)
34-
35-
this.data = this.data.map(item => {
36-
return new Proxy(item, {
37-
get(target, prop, receiver) {
38-
return Reflect.get(target, getItemPropProxyName(prop), receiver)
39-
},
40-
})
41-
})
43+
this.data = this.data.map(this.addMappingProxyToItem.bind(this))
4244
}
4345

44-
getItemPropProxyName(prop: string): string {
46+
getItemPropProxyName(prop: string | symbol): string | symbol {
4547
if (Object.prototype.hasOwnProperty.call(this.propertyMap, prop)) {
4648
return this.propertyMap[prop]
4749
}
@@ -57,21 +59,26 @@ class DataEngine {
5759

5860
const topLevelItems = items
5961
.filter(a => this.isTopLevelItem(a))
60-
.sort((a, b) => a.order && b.order ? a.order - b.order : 0)
62+
.sort((a, b) => {
63+
return a.order && b.order ? a.order - b.order : 0
64+
})
6165

6266
const childItems = items
6367
.filter(a => !this.isTopLevelItem(a))
6468
.reduce((groups, item) => {
69+
if (!item.parent) return groups
6570
if (Object.prototype.hasOwnProperty.call(groups, item.parent)) {
6671
groups[item.parent].push(item)
6772
} else {
6873
groups[item.parent] = [item]
6974
}
7075
return groups
71-
}, {}) as Array<DataItem>
76+
}, {} as Record<string, DataItem[]>)
7277

7378
Object.keys(childItems).forEach(parentId => {
74-
childItems[parentId].sort((a, b) => a.order && b.order ? a.order - b.order : 0)
79+
childItems[parentId].sort((a, b) => {
80+
return a.order && b.order ? a.order - b.order : 0
81+
})
7582
})
7683

7784
this.sortedData = [
@@ -82,6 +89,14 @@ class DataEngine {
8289
return this.sortedData
8390
}
8491

92+
addNewItem({ item, asLastChild = false }: AddNewItemArgs): HTMLElement {
93+
const mappedItem = this.addMappingProxyToItem(item)
94+
if (Array.isArray(this.data)) {
95+
this.data[asLastChild ? 'push' : 'unshift'](mappedItem)
96+
}
97+
return this.createItemElement(mappedItem)
98+
}
99+
85100
createItemElement(item: Partial<DataItem>, nodeName = 'li'): HTMLElement {
86101
const { id, text } = item
87102
const el = document.createElement(nodeName)
@@ -138,6 +153,8 @@ class DataEngine {
138153

139154
while (processedItems.length !== this.sortListItems().length) {
140155
processedItems = this.sortedData.reduce((processedItems, item) => {
156+
if (!item.id) return processedItems
157+
141158
const id = item.id.toString()
142159
if (processedItems.includes(id)) return processedItems
143160

@@ -173,7 +190,7 @@ class DataEngine {
173190
})
174191
}
175192

176-
render(): Node {
193+
render(): HTMLOListElement {
177194
const list = document.createElement('ol')
178195
this.getListItemsDom().forEach((listItem: HTMLElement) => list.appendChild(listItem))
179196
return list

src/main.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import DataEngine from './data-engine'
22
import {
33
Actions,
4+
AddNewItemArgs,
45
ClassNames,
56
ClassNamesList,
67
Cursor,
@@ -11,6 +12,7 @@ import {
1112
ListElement,
1213
ListInterface,
1314
ListTagName,
15+
MappedDataItem,
1416
Options,
1517
PlaceholderMaintenanceActions,
1618
PropertyMap,
@@ -119,6 +121,7 @@ class NestedSort {
119121
const list = this.getDataEngine().render()
120122
this.wrapper.innerHTML = ''
121123
this.wrapper.appendChild(list)
124+
this.sortableList = list
122125
}
123126

124127
getListTagName(): ListTagName {
@@ -440,6 +443,20 @@ class NestedSort {
440443
this.placeholderInUse = this.placeholderList.cloneNode(true) as HTMLElement
441444
return this.placeholderInUse
442445
}
446+
447+
addNewItem({ item, asLastChild = false }: AddNewItemArgs): { data: MappedDataItem[] } {
448+
const listItem = this.getDataEngine()
449+
.addNewItem({
450+
item,
451+
asLastChild,
452+
})
453+
listItem.setAttribute('draggable', String(this.initialised))
454+
this.getSortableList()?.[asLastChild ? 'append' : 'prepend'](listItem)
455+
456+
return {
457+
data: this.getDataEngine().convertDomToData(this.getSortableList()),
458+
}
459+
}
443460
}
444461

445462
export default NestedSort

0 commit comments

Comments
 (0)