@green-code-studio/objects-list 0.1.0

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.
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/objects-list.iml" filepath="$PROJECT_DIR$/.idea/objects-list.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$" />
5
+ <orderEntry type="inheritedJdk" />
6
+ <orderEntry type="sourceFolder" forTests="false" />
7
+ </component>
8
+ </module>
package/.idea/php.xml ADDED
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MessDetectorOptionsConfiguration">
4
+ <option name="transferred" value="true" />
5
+ </component>
6
+ <component name="PHPCSFixerOptionsConfiguration">
7
+ <option name="transferred" value="true" />
8
+ </component>
9
+ <component name="PHPCodeSnifferOptionsConfiguration">
10
+ <option name="highlightLevel" value="WARNING" />
11
+ <option name="transferred" value="true" />
12
+ </component>
13
+ <component name="PhpStanOptionsConfiguration">
14
+ <option name="transferred" value="true" />
15
+ </component>
16
+ <component name="PsalmOptionsConfiguration">
17
+ <option name="transferred" value="true" />
18
+ </component>
19
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@green-code-studio/objects-list",
3
+ "version": "0.1.0",
4
+ "main": "src/index.js",
5
+ "author": "Mateusz Krawczyk",
6
+ "license": "MIT"
7
+ }
@@ -0,0 +1,41 @@
1
+
2
+ import {t} from '../../i18n.xml';
3
+ export class ColumnFilter extends HTMLElement {
4
+ constructor(props) {
5
+ super(props);
6
+ this.select = this.addChild('select')
7
+ this.select.addChild('option', {value: 'none', text: t('filter.none')})
8
+ this.select.addChild('option', {value: 'equals', text: t('filter.equals')})
9
+ this.select.addChild('option', {value: 'less', text: t('filter.lessThan')})
10
+ this.select.addChild('option', {value: 'more', text: t('filter.greaterThan')})
11
+ this.select.addChild('option', {value: 'contains', text: t('filter.contains')})
12
+ this.select.addChild('option', {value: 'oneOf', text: t('filter.oneOf')})
13
+ this.select.addChild('option', {value: 'empty', text: t('filter.empty')})
14
+ this.input = this.addChild('input')
15
+ this.input.oninput = () => this.emit();
16
+ this.select.onchange = () => {
17
+ this.updateInput();
18
+ this.emit();
19
+ }
20
+ this.updateInput()
21
+ }
22
+
23
+ updateInput() {
24
+ if (this.select.value == 'none' || this.select.value == 'empty') {
25
+ this.input.style.display = 'none'
26
+ } else if (this.select.value == 'less' || this.select.value == 'more') {
27
+ this.input.style.display = ''
28
+ this.input.type = 'number';
29
+ } else {
30
+ this.input.style.display = ''
31
+ this.input.type = 'text';
32
+ }
33
+ }
34
+
35
+ emit() {
36
+ this.dispatchEvent(new CustomEvent('x-filter', {detail: {type: this.select.value, value: this.input.value}}))
37
+ }
38
+ }
39
+
40
+
41
+ customElements.define('column-filter', ColumnFilter);
@@ -0,0 +1,67 @@
1
+ import template from './ConfigPopup.mpts';
2
+ import {t} from '../../i18n.xml';
3
+ import {TableView} from "./tableView";
4
+ import {ListView} from "./listView";
5
+ import {ColumnFilter} from "./ColumnFilter";
6
+
7
+ export class ConfigPopup extends HTMLElement {
8
+ constructor(objectsList) {
9
+ super();
10
+ this.append(template({
11
+ t,
12
+ categories: [...objectsList.columns.groupBy(c => c.category)]
13
+ .map(([category, columns]) => ({name: category ?? '', columns}))
14
+ }))
15
+ this.querySelector('.mode').onchange = () => {
16
+ objectsList.infiniteScrollEnabled = this.querySelector('.mode').value == 'scrollMode'
17
+ objectsList.refresh()
18
+ }
19
+ this.querySelector('.view').onchange = () => {
20
+ if (this.querySelector('.view').value == 'tableView')
21
+ objectsList.insideViewClass = TableView
22
+ else
23
+ objectsList.insideViewClass = ListView
24
+ objectsList.refresh()
25
+ }
26
+ this.addEventListener('blur', () => {
27
+ if(!this.matches(':focus-within, :focus'))
28
+ this.remove();
29
+ })
30
+ this.querySelector('select').focus()
31
+ console.log('ssss')
32
+ for (const checkbox of this.querySelectorAll('table input[type="checkbox"]')) {
33
+ checkbox.checked = !objectsList.hiddenColumns.has(checkbox.dataset.name)
34
+ checkbox.onchange = () => {
35
+ if (checkbox.checked)
36
+ objectsList.hiddenColumns.delete(checkbox.dataset.name)
37
+ else
38
+ objectsList.hiddenColumns.add(checkbox.dataset.name)
39
+
40
+ objectsList.refresh()
41
+ }
42
+ }
43
+
44
+ for (const filterContainer of this.querySelectorAll('table .filter')) {
45
+ const filter=new ColumnFilter();
46
+ filterContainer.append(filter)
47
+ filter.addEventListener('x-filter', (e) => {
48
+ console.log('dddd',e)
49
+ objectsList.columnFilters.set(filterContainer.dataset.name, e.detail)
50
+ objectsList.refresh()
51
+ });
52
+ }
53
+ this.tabIndex = 0
54
+ setTimeout(()=>this.focus(), 1)
55
+ setTimeout(() => this.checkFocus(), 100)
56
+ }
57
+ checkFocus() {
58
+ console.log('checkFocus')
59
+ if (!this.matches(':focus-within, :focus'))
60
+ this.remove();
61
+
62
+ if(this.parentNode)
63
+ setTimeout(() => this.checkFocus(), 100)
64
+ }
65
+ }
66
+
67
+ customElements.define('config-popup', ConfigPopup);
@@ -0,0 +1,45 @@
1
+ <form>
2
+ <label>
3
+ <span>{{t('objectList.mode')}}</span>
4
+ <select class="mode">
5
+ <option value="paginationMode">{{t('objectList.paginationMode')}}</option>
6
+ <option value="scrollMode">{{t('objectList.scrollMode')}}</option>
7
+ </select>
8
+ </label>
9
+ <label>
10
+ <span>{{t('objectList.view')}}</span>
11
+ <select class="view">
12
+ <option value="tableView">{{t('objectList.tableView')}}</option>
13
+ <option value="listView">{{t('objectList.listView')}}</option>
14
+ </select>
15
+ </label>
16
+ <:foreach collection=categories item=category>
17
+ {{category.name}}
18
+
19
+ <table>
20
+ <thead>
21
+ <tr>
22
+ <th>{{t('objectList.columnEnabled')}}</th>
23
+ <th>{{t('objectList.columnName')}}</th>
24
+ <th>{{t('objectList.columnFilter')}}</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+
29
+ <:foreach collection=category.columns item=column>
30
+ <tr>
31
+ <td>
32
+ <input type="checkbox" data-name=column.dataName />
33
+ </td>
34
+ <td>
35
+ {{column.name}}
36
+ </td>
37
+ <td class="filter" data-name=column.dataName >
38
+
39
+ </td>
40
+ </tr>
41
+ </:foreach>
42
+ </tbody>
43
+ </table>
44
+ </:foreach>
45
+ </form>
@@ -0,0 +1,39 @@
1
+ export class AbstractView extends HTMLElement {
2
+ loadData(data, start, limit, infiniteScrollEnabled) {
3
+ this.style.setProperty('--height', (data.total * this.rowHeight) + 'px');
4
+ let top = 0;
5
+ let isOdd = true;
6
+ if (infiniteScrollEnabled) {
7
+ top = this.rowHeight * start;
8
+ isOdd = start % 2 == 0;
9
+ }
10
+ let newChildren = [];
11
+ for (let row of data.rows) {
12
+ let tr = this.generateRow(row);
13
+ tr.style.top = `${top}px`;
14
+ tr.classList.toggle('odd', isOdd)
15
+ tr.classList.toggle('even', !isOdd)
16
+ newChildren.push(tr);
17
+ top += this.rowHeight;
18
+ isOdd = !isOdd;
19
+ }
20
+ let oldChildren = Array.from(this.body.children);
21
+ for (let tr of oldChildren.filter(tr => !newChildren.includes(tr) && !tr.matches(':focus'))) {
22
+ tr.remove();
23
+ }
24
+
25
+ for (let tr of newChildren.filter(tr => !oldChildren.includes(tr))) {
26
+ this.body.appendChild(tr);
27
+ }
28
+ }
29
+
30
+ onScroll(e) {
31
+ if (this.onPaginationChanged) {
32
+ let start = Math.round(this.scrollTop / this.rowHeight);
33
+ let passedStart = Math.floor(start / 20) * 20 - 20;
34
+ if (passedStart < 0)
35
+ passedStart = 0;
36
+ this.onPaginationChanged(passedStart);
37
+ }
38
+ }
39
+ }
package/src/idsSet.js ADDED
@@ -0,0 +1,38 @@
1
+ export class IdsSet {
2
+ constructor() {
3
+ this.selected = new Set();
4
+ this.notSelected = new Set();
5
+ this.isSelectedAll = false;
6
+ }
7
+
8
+ clear() {
9
+ this.selected.clear();
10
+ this.notSelected.clear();
11
+ this.isSelectedAll = false;
12
+ }
13
+ has(id){
14
+ if(this.isSelectedAll){
15
+ return !this.notSelected.has(id?.toString())
16
+ }else{
17
+ return this.selected.has(id?.toString())
18
+ }
19
+ }
20
+ add(id){
21
+ if(this.isSelectedAll){
22
+ this.notSelected.delete(id?.toString())
23
+ }else{
24
+ this.selected.add(id?.toString())
25
+ }
26
+ }
27
+ delete(id){
28
+ if(this.isSelectedAll){
29
+ this.notSelected.add(id?.toString())
30
+ }else{
31
+ this.selected.delete(id?.toString())
32
+ }
33
+ }
34
+ selectAll(){
35
+ this.clear();
36
+ this.isSelectedAll=true;
37
+ }
38
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export {objectsList} from './objectsList'
2
+
@@ -0,0 +1,76 @@
1
+ import {ContextMenu} from "../contextMenu";
2
+ import {pageManager} from "../pageManager";
3
+ import {AbstractView} from "./abstractView";
4
+
5
+ export class ListView extends AbstractView {
6
+ constructor(objectsList) {
7
+ super();
8
+ this.objectsList = objectsList;
9
+ this.init();
10
+ window.dbgList = this;
11
+ }
12
+
13
+ init() {
14
+ this.body = this.addChild('.bodyContainer').addChild('.body');
15
+ this.addEventListener('scroll', this.onScroll.bind(this));
16
+ }
17
+
18
+ calcMaxVisibleItems(height) {
19
+ return Math.floor((height) / this.rowHeight);
20
+ }
21
+
22
+ get rowHeight() {
23
+ return Math.max(63, this.objectsList.visibleColumns.length * 21) + 20;
24
+ }
25
+
26
+ generateRow(data) {
27
+ let tr = this.body.querySelector(`.tr[data-row="${data.id}"]`);
28
+ if (!tr) {
29
+ tr = document.create('.tr');
30
+ tr.draggable = true;
31
+ }
32
+ tr.classList.toggle('selected', this.objectsList.selected.has(data.id))
33
+ this.fillRowContent(tr, data);
34
+ return tr;
35
+ }
36
+
37
+ fillRowContent(tr, data) {
38
+ tr.children.removeAll();
39
+ tr.addChild('.icon', {className: this.objectsList.icon});
40
+ for (let column of this.objectsList.visibleColumns) {
41
+ let keyValue = tr.addChild('.keyValue');
42
+ let key = keyValue.addChild('strong', {text: column.name + ': '});
43
+ let value = keyValue.addChild('span');
44
+ value.append((column.content?.call(column,data)||data[column.dataName]));
45
+ }
46
+ let actionsTd = tr.addChild('.td.actions');
47
+ let actions = this.objectsList.generateActions([data], 'row');
48
+ for (let action of actions) {
49
+ let actionButton = actionsTd.addChild(action.href ? 'a.button' : 'button', {
50
+ title: action.name
51
+ });
52
+ if (action.href) {
53
+ actionButton.href = action.href;
54
+ }
55
+ if (action.command) {
56
+ actionButton.onclick = action.command;
57
+ }
58
+ if (action.icon) {
59
+ actionButton.addChild('span', {classList: [action.icon]});
60
+ actionButton.append(action.name);
61
+ } else {
62
+ actionButton.textContent = action.name;
63
+ }
64
+ }
65
+
66
+
67
+ tr.dataset.row = data.id;
68
+ // tr.oncontextmenu = this.contextMenu.bind(this, tr);
69
+ // tr.onclick = this.trOnClick.bind(this, data);
70
+ // tr.ondblclick = this.trOnDblClick.bind(this, data, tr);
71
+ // tr.onkeydown = this.trOnKeyDown.bind(this, data, tr);
72
+ // tr.ondragstart = this.trOnDragStart.bind(this, data, tr);
73
+ }
74
+ }
75
+
76
+ customElements.define('list-view', ListView);
@@ -0,0 +1,202 @@
1
+ import {TableView} from "./tableView";
2
+ import {ListView} from "./listView";
3
+ import {t} from "../../i18n.xml";
4
+ import {PaginationButtons} from "./paginationButtons";
5
+ import {ContextMenu} from "../contextMenu";
6
+ import {ConcurencyLimiter} from "../utils/concurencyLimiter";
7
+ import {IdsSet} from "./idsSet";
8
+ import {ConfigPopup} from "./ConfigPopup";
9
+
10
+ export class ObjectsList extends HTMLElement {
11
+ constructor(datasource) {
12
+ super();
13
+ this.columns = [];
14
+ this.hiddenColumns = new Set()
15
+ this.columnFilters = new Map();
16
+ this.generateActions = () => [];
17
+ this.insideViewClass = TableView;
18
+ this.icon = 'icon-document';
19
+ this.loadConcurencyLimiter = new ConcurencyLimiter();
20
+ this.datasource = datasource;
21
+ this.datasource.onchange = () => this.refresh();
22
+ this.start = 0;
23
+ this.limit = 10
24
+ this.total = 0;
25
+ this.selected = new IdsSet();
26
+ this.selectedMain = null;
27
+ this.dataById = new Map();
28
+ this.initFoot();
29
+ this.addEventListener('contextmenu', e => this.showGlobalContextMenu(e));
30
+ addEventListener('resize', e => this.resize());
31
+ this.infiniteScrollEnabled = false;
32
+ }
33
+
34
+ get visibleColumns() {
35
+ return this.columns.filter(x => !this.hiddenColumns.has(x.dataName))
36
+ }
37
+
38
+ refreshLimit() {
39
+ this.limit = this.insideView.calcMaxVisibleItems(this.clientHeight - this.foot.clientHeight - 2);
40
+ if (this.limit < 1) this.limit = 1;
41
+ if (this.infiniteScrollEnabled) {
42
+ this.limit = Math.ceil(this.limit / 20) * 20 + 40;
43
+ } else {
44
+ this.start = Math.floor(Math.min(this.total, this.start) / this.limit) * this.limit;
45
+ }
46
+ }
47
+
48
+ async refresh() {
49
+ this.classList.toggle('infiniteScrollEnabled', this.infiniteScrollEnabled);
50
+
51
+ if (!this.insideView || !(this.insideView instanceof this.insideViewClass))
52
+ this.initInsideView();
53
+ else if (this.insideView.refreshHeader)
54
+ this.insideView.refreshHeader()
55
+
56
+ this.refreshLimit();
57
+ const refreshSymbol = Symbol();
58
+ this.lastRefreshSymbol = refreshSymbol;
59
+ await this.loadConcurencyLimiter.run(async () => {
60
+ if (this.lastRefreshSymbol != refreshSymbol) return;
61
+
62
+ let data = await this.datasource.get(this);
63
+ if (this.lastRefreshSymbol != refreshSymbol) return;
64
+
65
+ this.currentRows = data.rows;
66
+ this.fillDataById(data.rows);
67
+ this.total = data.total;
68
+ this.insideView.loadData(data, this.start, this.limit, this.infiniteScrollEnabled);
69
+ this.pagination.currentPage = Math.floor(this.start / this.limit);
70
+ this.pagination.totalPages = Math.ceil(this.total / this.limit);
71
+ this.pagination.render();
72
+ });
73
+ }
74
+
75
+ initInsideView() {
76
+ if (this.insideView) {
77
+ this.insideView.remove();
78
+ }
79
+ this.insideView = new this.insideViewClass(this);
80
+ this.insertBefore(this.insideView, this.foot);
81
+ this.insideView.refresh = () => this.refresh();
82
+ this.insideView.onPaginationChanged = (start, limit) => {
83
+ if (this.start != start) {
84
+ this.start = start;
85
+ //this.limit=limit;
86
+ this.refresh();
87
+ }
88
+ }
89
+ }
90
+
91
+ fillDataById(rows) {
92
+ for (let row of rows) {
93
+ this.dataById.set(parseInt(row.id), row);
94
+ }
95
+ }
96
+
97
+ getSelectedData() {
98
+ let data = Array.from(this.dataById).filter(([id, row]) => this.selected.has(id)).map(([id, row]) => row);
99
+ if (data.length > 0) return data;
100
+ else if (this.dataById[this.selectedMain])
101
+ return [this.dataById[this.selectedMain]]
102
+ else return [];
103
+ }
104
+
105
+ initFoot() {
106
+ this.foot = this.addChild('.foot');
107
+ let menuButton = this.foot.addChild('button.menuButton span.icon-settings');
108
+ menuButton.onclick = e => this.showConfigPopup();
109
+ this.pagination = new PaginationButtons();
110
+ this.pagination.onpageclick = (page) => {
111
+ this.start = page * this.limit;
112
+ this.refresh();
113
+ }
114
+ this.foot.append(this.pagination);
115
+ this.searchForm = this.foot.addChild('form', {className: 'search'});
116
+ this.searchForm.onsubmit = e => e.preventDefault();
117
+ const searchInput = this.searchForm.addChild('input', {
118
+ name: 'search',
119
+ type: 'search',
120
+ placeholder: t('objectList.search')
121
+ });
122
+ searchInput.oninput = e => {
123
+ this.start = 0;
124
+ this.refresh();
125
+ }
126
+
127
+ }
128
+
129
+ get search() {
130
+ if (!this.foot) return '';
131
+ const searchForm = this.foot.querySelector('.search');
132
+ if (!searchForm) return '';
133
+ return searchForm.search.value;
134
+ }
135
+
136
+ showGlobalContextMenu(e) {
137
+ let elements = [{
138
+ text: t('objectList.paginationMode'),
139
+ icon: 'icon-pagination',
140
+ onclick: () => {
141
+ this.infiniteScrollEnabled = false;
142
+ this.refresh();
143
+ }
144
+ }, {
145
+ text: t('objectList.scrollMode'),
146
+ icon: 'icon-scroll',
147
+ onclick: () => {
148
+ this.infiniteScrollEnabled = true;
149
+ this.refresh();
150
+ }
151
+ }, {
152
+ text: t('objectList.tableView'),
153
+ icon: 'icon-table',
154
+ onclick: () => {
155
+ this.insideViewClass = TableView;
156
+ this.refresh();
157
+ }
158
+ }, {
159
+ text: t('objectList.listView'),
160
+ icon: 'icon-table',
161
+ onclick: () => {
162
+ this.insideViewClass = ListView;
163
+ this.refresh();
164
+ }
165
+ }];
166
+ if (this.allowTableEdit) {
167
+ elements.push({
168
+ text: t('objectList.startMultiEdit'),
169
+ icon: 'icon-edit',
170
+ onclick: () => {
171
+ this.insideViewClass = TableView;
172
+ this.refresh();
173
+ this.insideView.startMultiEdit();
174
+ }
175
+ });
176
+ }
177
+ ContextMenu.openContextMenu(e, elements);
178
+ }
179
+
180
+ resize() {
181
+ let limit = this.limit;
182
+ this.refreshLimit();
183
+ if (limit != this.limit) {
184
+ this.refresh();
185
+ }
186
+ }
187
+
188
+ multiEditChanged(id, data, save = false) {
189
+ this.datasource.multiEditChanged(id, data, save);
190
+ }
191
+
192
+ showConfigPopup() {
193
+ if (this.querySelector('config-popup')) {
194
+ this.querySelector('config-popup').remove()
195
+ } else {
196
+ const popup = new ConfigPopup(this)
197
+ this.append(popup)
198
+ }
199
+ }
200
+ }
201
+
202
+ customElements.define('data-view', ObjectsList);
@@ -0,0 +1,71 @@
1
+ export class PaginationButtons extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.currentPage = 0;
5
+ this.totalPages = 0;
6
+ this.onpageclick = null;
7
+ }
8
+
9
+ render() {
10
+ let pagination = this.getPagination();
11
+ this.children.removeAll();
12
+ for (let pageNumber of pagination) {
13
+ if (pageNumber == null) {
14
+ this.addChild('span', {text: '...'});
15
+ } else {
16
+ let pageButton = this.addChild('button', {
17
+ text: pageNumber + 1,
18
+ className: pageNumber == this.currentPage ? 'active' : ''
19
+ });
20
+ pageButton.onclick = () => {
21
+ if (this.onpageclick) {
22
+ this.onpageclick(pageNumber);
23
+ }
24
+ this.currentPage = pageNumber;
25
+ this.render();
26
+ };
27
+ }
28
+ }
29
+ }
30
+
31
+ getPagination() {
32
+ const boxesCount = 7;
33
+ let boxes = [];
34
+ if (this.totalPages <= boxesCount) {
35
+ for (let i = 0; i < this.totalPages; i++) {
36
+ boxes.push(i);
37
+ }
38
+ } else if (this.currentPage <= (boxesCount - 1) / 2 - 1) {
39
+ for (let i = 0; i <= boxesCount - 3; i++) {
40
+ boxes.push(i);
41
+ }
42
+ boxes.push(null);
43
+ boxes.push(this.totalPages - 1);
44
+ } else if (this.currentPage >= this.totalPages - (boxesCount - 1) / 2 - 1) {
45
+ boxes.push(0);
46
+ boxes.push(null);
47
+ for (
48
+ let i = this.totalPages - Math.floor((boxesCount - 1) / 2) - 2;
49
+ i < this.totalPages;
50
+ i++
51
+ ) {
52
+ boxes.push(i);
53
+ }
54
+ } else {
55
+ boxes.push(0);
56
+ boxes.push(null);
57
+ for (
58
+ let i = this.currentPage - Math.floor((boxesCount - 5) / 2);
59
+ i <= this.currentPage + Math.ceil((boxesCount - 5) / 2);
60
+ i++
61
+ ) {
62
+ boxes.push(i);
63
+ }
64
+ boxes.push(null);
65
+ boxes.push(this.totalPages - 1);
66
+ }
67
+ return boxes;
68
+ }
69
+ }
70
+
71
+ customElements.define('pagination-buttons', PaginationButtons);
@@ -0,0 +1,403 @@
1
+ import {ContextMenu} from "../contextMenu";
2
+ import {pageManager} from "../pageManager";
3
+ import {AbstractView} from "./abstractView";
4
+ import {t} from "../../i18n.xml";
5
+
6
+ export class TableView extends AbstractView {
7
+ constructor(objectsList) {
8
+ super();
9
+ this.objectsList = objectsList;
10
+ this.init();
11
+ window.dbgTable = this;
12
+ }
13
+
14
+ init() {
15
+ this.head = this.addChild('.head');
16
+ this.refreshHeader();
17
+ this.body = this.addChild('.bodyContainer').addChild('.body');
18
+ this.setColumnsWidths();
19
+ addEventListener('resize', this.setColumnsWidths.bind(this))
20
+ addEventListener('copy', this.onCopy.bind(this));
21
+ this.addEventListener('scroll', this.onScroll.bind(this));
22
+ }
23
+
24
+ refreshHeader() {
25
+ while (this.head.firstChild) this.head.firstChild.remove();
26
+
27
+ this.head.addChild('.column.icon')
28
+ for (let column of this.objectsList.visibleColumns) {
29
+ let node = this.head.addChild('.column')
30
+ node.addChild('span.name', {text: column.name});
31
+ if (column.sortName) {
32
+ node.classList.add('ableToSort');
33
+ node.dataset.sortName = column.sortName
34
+ node.onclick = () => {
35
+ let sortName = column.sortName || x.dataset.value;
36
+ if (this.objectsList.sort && this.objectsList.sort.col === column.sortName) {
37
+ this.objectsList.sort.desc = !this.objectsList.sort.desc;
38
+ } else {
39
+ this.objectsList.sort = {col: column.sortName, desc: false};
40
+ }
41
+ if (!this.objectsList.infiniteScrollEnabled) {
42
+ this.objectsList.start = 0;
43
+ }
44
+ this.objectsList.refresh();
45
+ }
46
+ }
47
+ }
48
+ this.head.addChild('.column.actions')
49
+ }
50
+
51
+ loadData(data, start, limit, infiniteScrollEnabled) {
52
+ this.refreshSortIndicators();
53
+ super.loadData(data, start, limit, infiniteScrollEnabled)
54
+ this.setColumnsWidths();
55
+ }
56
+
57
+ get rowHeight() {
58
+ return parseFloat(window.getComputedStyle(this).getPropertyValue('--rowHeight') ?? 31);
59
+ }
60
+
61
+ generateRow(data) {
62
+ let tr = this.body.querySelector(`.tr[data-row="${data.id}"]`);
63
+ if (!tr) {
64
+ tr = document.create('.tr');
65
+ tr.draggable = true;
66
+ }
67
+ tr.classList.toggle('selected', this.objectsList.selected.has(data.id))
68
+ this.fillRowContent(tr, data);
69
+ return tr;
70
+ }
71
+
72
+ fillRowContent(tr, data) {
73
+ tr.lastData = data;
74
+ tr.children.removeAll();
75
+ tr.addChild('.td.icon', {className: this.objectsList.icon});
76
+ for (let column of this.objectsList.visibleColumns) {
77
+ let td = tr.addChild('.td');
78
+ if (this.multiEdit && column.dataName) {
79
+ tr.dataset.id = data.id;
80
+ td.addChild('input', {
81
+ data: {name: column.dataName},
82
+ value: data[column.dataName],
83
+ onclick: e => e.stopPropagation(),
84
+ onchange: () => this.multiEditChanged(tr)
85
+ });
86
+ } else {
87
+ td.append(column.content?.call(column, data) || data[column.dataName] || '');
88
+ }
89
+ }
90
+ let actionsTd = tr.addChild('.td.actions');
91
+ let actions = this.objectsList.generateActions([data], 'row');
92
+ if (this.multiEdit && data.__isMultirowEdited) {
93
+ actions = [
94
+ {
95
+ name: t("objectList.saveRow"),
96
+ icon: 'icon-save',
97
+ command: () => {
98
+ this.multiEditChanged(tr, true)
99
+ }
100
+ }
101
+ ]
102
+ }
103
+ for (let action of actions) {
104
+ let actionButton = actionsTd.addChild(action.href ? 'a.button' : 'button', {
105
+ title: action.name
106
+ });
107
+ actionButton.classList.add('action-' + (action.action ?? 'view'));
108
+
109
+ if (action.href) {
110
+ actionButton.href = action.href;
111
+ }
112
+ if (action.command) {
113
+ actionButton.onclick = action.command;
114
+ }
115
+ if (action.icon) {
116
+ actionButton.addChild('span', {classList: [action.icon]});
117
+ } else {
118
+ actionButton.textContent = action.name;
119
+ }
120
+ }
121
+
122
+
123
+ tr.dataset.row = data.id;
124
+ tr.oncontextmenu = this.contextMenu.bind(this, tr);
125
+ tr.onclick = this.trOnClick.bind(this, data);
126
+ tr.ondblclick = this.trOnDblClick.bind(this, data, tr);
127
+ tr.onkeydown = this.trOnKeyDown.bind(this, data, tr);
128
+ tr.ondragstart = this.trOnDragStart.bind(this, data, tr);
129
+ }
130
+
131
+ setColumnsWidths() {
132
+ const widths = this.calculateColumnsWidths();
133
+ for (let tr of this.body.children) {
134
+ for (let i = 0; i < widths.length; i++) {
135
+ tr.children[i].style.width = widths[i] + 'px';
136
+ }
137
+ }
138
+ let sum = 0;
139
+ for (let i = 0; i < widths.length; i++) {
140
+ let node = this.head.children[i];
141
+ if (node) {
142
+ if (i + 1 < widths.length)
143
+ node.style.width = widths[i] + 1 + 'px';
144
+ else
145
+ node.style.width = widths[i] + 'px';
146
+ node.style.left = sum + 'px';
147
+ }
148
+ sum += widths[i];
149
+ }
150
+ }
151
+
152
+ calculateColumnsWidths() {
153
+ let needed = [{base: 30, grow: 0}];
154
+
155
+ for (let column of this.objectsList.visibleColumns) {
156
+ needed.push({base: column.width || 10, grow: typeof (column.widthGrow) == "number" ? column.widthGrow : 1});
157
+ }
158
+ let actionWidth = Math.ceil(Array.from(this.querySelectorAll('.td.actions')).map(x => {
159
+ if (x.lastElementChild)
160
+ return x.lastElementChild.getBoundingClientRect().right - x.getBoundingClientRect().left + parseFloat(getComputedStyle(x).paddingRight);
161
+ else
162
+ return 0;
163
+ }).max());
164
+ needed.push({base: actionWidth, grow: 0});
165
+ let availableToGrow = this.clientWidth - needed.sum(x => x.base);
166
+ let sumGrow = needed.sum(x => x.grow);
167
+ if (availableToGrow > 0 && sumGrow > 0) {
168
+ return needed.map(x => x.base + x.grow / sumGrow * availableToGrow);
169
+ } else {
170
+ return needed.map(x => x.base);
171
+ }
172
+ }
173
+
174
+ refreshSortIndicators() {
175
+ this.head.querySelectorAll('[data-order]').forEach(x => delete x.dataset.order);
176
+ if (this.objectsList.sort)
177
+ this.head.querySelectorAll(`[data-sort-name="${this.objectsList.sort.col}"]`).forEach(x => x.dataset.order = this.objectsList.sort.desc ? 'desc' : 'asc');
178
+ }
179
+
180
+ contextMenu(tr, event) {
181
+ event.stopPropagation();
182
+ if (!this.objectsList.selected.has(tr.dataset.row)) {
183
+ this.objectsList.selected.clear();
184
+ this.objectsList.selected.add(tr.dataset.row)
185
+ this.objectsList.selectedMain = tr.dataset.row;
186
+ this.refreshSelectedClasses();
187
+ }
188
+ const rows = this.objectsList.getSelectedData();
189
+ let actions = this.objectsList.generateActions(rows, 'contextMenu');
190
+ let elements = actions.map(action => ({
191
+ text: action.name,
192
+ icon: action.icon,
193
+ class: action.action ? 'action-' + action.action : '',
194
+ onclick: action.command || (() => pageManager.goto(action.href))
195
+ }));
196
+ elements.push({
197
+ text: 'copy',
198
+ icon: 'icon-copy',
199
+ onclick: () => {
200
+ this.forceCopy(rows);
201
+ }
202
+ })
203
+ ContextMenu.openContextMenu(event, elements);
204
+ }
205
+
206
+ refreshSelectedClasses() {
207
+ for (const tr of this.body.children) {
208
+ tr.classList.toggle('selected', this.objectsList.selected.has(tr.dataset.row));
209
+ tr.classList.toggle('selectedMain', this.objectsList.selectedMain == tr.dataset.row);
210
+ if (this.objectsList.selectedMain == tr.dataset.row) {
211
+ tr.tabIndex = 1;
212
+ tr.focus();
213
+ getSelection().selectAllChildren(tr)
214
+ } else {
215
+ tr.tabIndex = -1;
216
+ }
217
+ }
218
+ }
219
+
220
+
221
+ trOnClick(row, e) {
222
+ const rowsIds = this.objectsList.currentRows.map(x => x.id);
223
+ console.log('click')
224
+ if (!e.ctrlKey) {
225
+ this.objectsList.selected.clear();
226
+ }
227
+
228
+ if (e.shiftKey) {
229
+ const mainIndex = rowsIds.indexOf(this.objectsList.selectedMain);
230
+ const clickedIndex = rowsIds.indexOf(row.id);
231
+ if (clickedIndex >= 0 && mainIndex >= 0)
232
+ rowsIds.slice(Math.min(mainIndex, clickedIndex), Math.max(mainIndex, clickedIndex) + 1).forEach(x => this.objectsList.selected.add(x));
233
+ } else {
234
+ if (this.objectsList.selected.has(row.id))
235
+ this.objectsList.selected.delete(row.id);
236
+ else
237
+ this.objectsList.selected.add(row.id);
238
+
239
+ this.objectsList.selectedMain = row.id;
240
+ }
241
+
242
+ this.refreshSelectedClasses();
243
+ }
244
+
245
+ trOnDblClick(row, tr, e) {
246
+ if (!this.objectsList.selected.has(tr.dataset.row)) {
247
+ this.objectsList.selected.clear();
248
+ this.objectsList.selected.add(tr.dataset.row)
249
+ this.objectsList.selectedMain = tr.dataset.row;
250
+ this.refreshSelectedClasses();
251
+ }
252
+ let action = this.objectsList.generateActions(this.objectsList.getSelectedData(), 'dblClick').find(x => x.main);
253
+ if (action) {
254
+ if (action.command) {
255
+ action.command();
256
+ } else if (action.href) {
257
+ pageManager.goto(action.href)
258
+ }
259
+ }
260
+ }
261
+
262
+ selectRange(start, end) {
263
+ const rowsIds = this.objectsList.currentRows.map(x => x.id);
264
+ let startIndex = rowsIds.indexOf(start);
265
+ let endIndex = rowsIds.indexOf(end);
266
+ if (endIndex < startIndex) {
267
+ let tmp = startIndex;
268
+ startIndex = endIndex;
269
+ endIndex = tmp;
270
+ }
271
+ for (let i = startIndex; i <= endIndex; i++) {
272
+ this.objectsList.selected.add(rowsIds[i]);
273
+ }
274
+ }
275
+
276
+ trOnKeyDown(row, tr, e) {
277
+ console.log('trOnKeyDown')
278
+ if (e.key === 'Enter') {
279
+ let action = this.objectsList.generateActions(this.objectsList.getSelectedData(), 'enter').find(x => x.main);
280
+ if (action) {
281
+ if (action.command) {
282
+ action.command();
283
+ } else if (action.href) {
284
+ pageManager.goto(action.href)
285
+ }
286
+ }
287
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
288
+ const rowsIds = this.objectsList.currentRows.map(x => x.id);
289
+ let index = rowsIds.indexOf(this.objectsList.selectedMain);
290
+ if (e.key === 'ArrowDown') {
291
+ if (index < rowsIds.length) index++;
292
+ else index = rowsIds.length - 1;
293
+ } else {
294
+ if (index > 0) index--;
295
+ }
296
+ const id = rowsIds[index];
297
+ if (!e.ctrlKey) {
298
+ this.objectsList.selected.clear();
299
+ }
300
+ if (e.shiftKey) {
301
+ if (!this.objectsList.selectedShiftStart) {
302
+ this.objectsList.selectedShiftStart = this.objectsList.selectedMain;
303
+ }
304
+ this.selectRange(this.objectsList.selectedShiftStart, id)
305
+ } else {
306
+ this.objectsList.selectedShiftStart = null;
307
+ if (!e.ctrlKey) {
308
+ this.objectsList.selected.add(id);
309
+ }
310
+ }
311
+ this.objectsList.selectedMain = id;
312
+ this.refreshSelectedClasses();
313
+ e.preventDefault();
314
+ } else if (e.key === ' ') {
315
+ if (this.objectsList.selected.has(this.objectsList.selectedMain))
316
+ this.objectsList.selected.delete(this.objectsList.selectedMain);
317
+ else
318
+ this.objectsList.selected.add(this.objectsList.selectedMain);
319
+
320
+ this.refreshSelectedClasses();
321
+ e.preventDefault();
322
+ } else if (e.key === 'a') {
323
+ if (e.ctrlKey)
324
+ this.objectsList.selected.selectAll();
325
+
326
+ this.refreshSelectedClasses();
327
+ e.preventDefault();
328
+ }
329
+ }
330
+
331
+ onCopy(e) {
332
+ if (this.copyForced) {
333
+ this.fillDataTransfer(e.clipboardData, this.copyForced);
334
+ e.preventDefault();
335
+ } else if (document.querySelector(':focus')?.findParent(x => x === this)) {
336
+ this.fillDataTransfer(e.clipboardData, this.objectsList.selected);
337
+ e.preventDefault();
338
+ }
339
+ }
340
+
341
+ trOnDragStart(row, oryginalTr, e) {
342
+ if (!this.objectsList.selected.has(row.id)) {
343
+ this.objectsList.selected.clear()
344
+ this.objectsList.selected.add(row.id)
345
+ this.refreshSelectedClasses();
346
+ }
347
+ this.fillDataTransfer(e.dataTransfer, this.objectsList.selected);
348
+ }
349
+
350
+ fillDataTransfer(dataTransfer, ids) {
351
+ const trs = Array.from(this.body.children).filter(tr => ids.has(tr.dataset.row));
352
+ let action = this.objectsList.generateActions(this.objectsList.getSelectedData(), 'dataTransfer').find(x => x.main);
353
+ if (action && action.href) {
354
+ dataTransfer.setData('text/uri-list', new URL(action.href, document.baseURI));
355
+ }
356
+ if (action && action.hrefArray) {
357
+ dataTransfer.setData('text/uri-list', action.hrefArray.map(x => new URL(x, document.baseURI)).map(x => x.toString()).join("\r\n#\r\n"));
358
+ }
359
+ dataTransfer.setData('text/html', this.generateTableHtml(trs));
360
+ dataTransfer.setData('text/plain', this.generateTableTextPlain(trs));
361
+
362
+ }
363
+
364
+ generateTableHtml(trs) {
365
+ const thead = '<thead><tr>' + Array.from(this.head.querySelectorAll('.column')).map(x => '<th>' + x.innerHTML + '</th>').join('') + '</tr></thead>';
366
+ const tbody = '<tbody>' + trs.map(tr => {
367
+ return '<tr>' + Array.from(tr.children).slice(1, -1).map(td => {
368
+ return '<td>' + td.innerHTML + '</td>';
369
+ }).join('') + '</tr>';
370
+ }).join('') + '</tbody>'
371
+ return '<table>' + thead + tbody + '</table>';
372
+ }
373
+
374
+ generateTableTextPlain(trs) {
375
+ const head = Array.from(this.head.querySelectorAll('.column')).map(x => x.textContent.replace(/\r\n/gm, ' ')).join("\t")
376
+ const body = trs.map(tr => Array.from(tr.children).slice(1).map(x => x.textContent.replace(/\r\n/gm, ' ')).join("\t")).join("\r\n")
377
+ return head + "\r\n" + body;
378
+ }
379
+
380
+ calcMaxVisibleItems(height) {
381
+ return Math.floor((height - this.head.clientHeight) / this.rowHeight);
382
+ }
383
+
384
+ forceCopy(rows) {
385
+ this.copyForced = new Set(rows.map(x => x.id));
386
+ document.execCommand("copy");
387
+ setTimeout(() => this.copyForced = null, 100);
388
+ }
389
+
390
+ startMultiEdit() {
391
+ this.multiEdit = true;
392
+ }
393
+
394
+ multiEditChanged(tr, save = false) {
395
+ let data = [...tr.querySelectorAll('[data-name]')].map(x => [x.dataset.name, x.value]);
396
+ let dataObj = Object.fromEntries(data);
397
+ this.objectsList.multiEditChanged(tr.dataset.id, dataObj, save);
398
+ this.fillRowContent(tr, {...tr.lastData, ...dataObj, __isMultirowEdited: true});
399
+ this.setColumnsWidths();
400
+ }
401
+ }
402
+
403
+ customElements.define('table-view', TableView);