@mixd-id/web-scaffold 0.1.230406042 → 0.1.230406043

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.230406042",
4
+ "version": "0.1.230406043",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -133,10 +133,7 @@ export default{
133
133
  default: ''
134
134
  },
135
135
 
136
- modelValue:{
137
- type: String,
138
- default: currentDate
139
- }
136
+ modelValue:String
140
137
 
141
138
  },
142
139
 
@@ -309,4 +306,4 @@ export default{
309
306
  @apply bg-primary;
310
307
  }
311
308
 
312
- </style>
309
+ </style>
@@ -1,23 +1,53 @@
1
1
  <template>
2
2
  <div :class="$style.comp" v-if="true || configLoaded">
3
3
 
4
- <div class="flex flex-row items-center gap-4 px-3">
5
- <div class="flex-1 flex flex-row gap-1">
6
- <h5>{{ title }}</h5>
7
- <button type="button">
8
- <svg width="16" height="16" class="fill-text-300 hover:fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"/></svg>
9
- </button>
4
+ <slot v-if="$slots.head" name="head"></slot>
5
+ <div v-else class="flex flex-row items-center gap-4">
6
+ <div class="flex flex-row gap-1">
7
+ <button type="button" class="flex flex-row gap-1 items-center">
8
+ <h3>{{ title }}</h3>
9
+ <svg width="16" height="16" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"/></svg>
10
+ </button>
11
+ <button type="button" @click="$refs.setting.open()">
12
+ <svg width="16" height="16" class="fill-text-300 hover:fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M452.515 237l31.843-18.382c9.426-5.441 13.996-16.542 11.177-27.054-11.404-42.531-33.842-80.547-64.058-110.797-7.68-7.688-19.575-9.246-28.985-3.811l-31.785 18.358a196.276 196.276 0 0 0-32.899-19.02V39.541a24.016 24.016 0 0 0-17.842-23.206c-41.761-11.107-86.117-11.121-127.93-.001-10.519 2.798-17.844 12.321-17.844 23.206v36.753a196.276 196.276 0 0 0-32.899 19.02l-31.785-18.358c-9.41-5.435-21.305-3.877-28.985 3.811-30.216 30.25-52.654 68.265-64.058 110.797-2.819 10.512 1.751 21.613 11.177 27.054L59.485 237a197.715 197.715 0 0 0 0 37.999l-31.843 18.382c-9.426 5.441-13.996 16.542-11.177 27.054 11.404 42.531 33.842 80.547 64.058 110.797 7.68 7.688 19.575 9.246 28.985 3.811l31.785-18.358a196.202 196.202 0 0 0 32.899 19.019v36.753a24.016 24.016 0 0 0 17.842 23.206c41.761 11.107 86.117 11.122 127.93.001 10.519-2.798 17.844-12.321 17.844-23.206v-36.753a196.34 196.34 0 0 0 32.899-19.019l31.785 18.358c9.41 5.435 21.305 3.877 28.985-3.811 30.216-30.25 52.654-68.266 64.058-110.797 2.819-10.512-1.751-21.613-11.177-27.054L452.515 275c1.22-12.65 1.22-25.35 0-38zm-52.679 63.019l43.819 25.289a200.138 200.138 0 0 1-33.849 58.528l-43.829-25.309c-31.984 27.397-36.659 30.077-76.168 44.029v50.599a200.917 200.917 0 0 1-67.618 0v-50.599c-39.504-13.95-44.196-16.642-76.168-44.029l-43.829 25.309a200.15 200.15 0 0 1-33.849-58.528l43.819-25.289c-7.63-41.299-7.634-46.719 0-88.038l-43.819-25.289c7.85-21.229 19.31-41.049 33.849-58.529l43.829 25.309c31.984-27.397 36.66-30.078 76.168-44.029V58.845a200.917 200.917 0 0 1 67.618 0v50.599c39.504 13.95 44.196 16.642 76.168 44.029l43.829-25.309a200.143 200.143 0 0 1 33.849 58.529l-43.819 25.289c7.631 41.3 7.634 46.718 0 88.037zM256 160c-52.935 0-96 43.065-96 96s43.065 96 96 96 96-43.065 96-96-43.065-96-96-96zm0 144c-26.468 0-48-21.532-48-48 0-26.467 21.532-48 48-48s48 21.533 48 48c0 26.468-21.532 48-48 48z"/></svg>
13
+ </button>
14
+
15
+ <Modal ref="setting" width="680" height="560">
16
+ <template #head>
17
+ <div class="relative">
18
+ <div class="absolute top-0 right-0 p-2">
19
+ <button type="button" class="p-2" @click="$refs.setting.close()">
20
+ <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
21
+ <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
22
+ </svg>
23
+ </button>
24
+ </div>
25
+ </div>
26
+ </template>
27
+ <ListViewSettings class="flex-1"
28
+ :config="config"
29
+ @change="load" />
30
+ </Modal>
10
31
  </div>
11
- <Textbox placeholder="Cari..." :clearable="true" @clear="clearSearch" v-model="preset.search"
12
- @keyup.enter="load">
13
32
 
33
+ <div class="flex-1">
34
+ <slot name="headerOpt"></slot>
35
+ </div>
36
+
37
+ <Textbox :placeholder="$t('Search...')" :clearable="true" @clear="clearSearch" v-model="preset.search"
38
+ @keyup.enter="load" :class="$style.searchBox">
39
+ <template #start>
40
+ <div class="pl-2">
41
+ <svg width="14" height="14" class="fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M508.5 481.6l-129-129c-2.3-2.3-5.3-3.5-8.5-3.5h-10.3C395 312 416 262.5 416 208 416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c54.5 0 104-21 141.1-55.2V371c0 3.2 1.3 6.2 3.5 8.5l129 129c4.7 4.7 12.3 4.7 17 0l9.9-9.9c4.7-4.7 4.7-12.3 0-17zM208 384c-97.3 0-176-78.7-176-176S110.7 32 208 32s176 78.7 176 176-78.7 176-176 176z"/></svg>
42
+ </div>
43
+ </template>
14
44
  </Textbox>
15
45
  </div>
16
46
 
17
47
  <VirtualTable ref="table1" :columns="presetColumns" :items="items" class="flex-1"
18
48
  @scroll-end="loadNext">
19
49
  <template v-for="column in presetColumns" #[colOf(column.key)]="{}">
20
- <div :class="getHeader(column)">
50
+ <div :class="getHeader(column)" @click="openColumnOptions(column.key, $event.target.closest('.' + $style.header))">
21
51
  <div>
22
52
  {{ column.label ?? column.key }}
23
53
  </div>
@@ -49,24 +79,38 @@
49
79
  </template>
50
80
  </VirtualTable>
51
81
 
52
- <Modal ref="setting" width="700" height="600">
53
- <template v-slot:head>
54
- <div class="relative p-5">
55
- <h3>Settings</h3>
56
- <div class="absolute top-0 right-0 p-2">
57
- <button type="button" class="p-2" @click="$refs.setting.close()">
58
- <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
59
- <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
60
- </svg>
61
- </button>
62
- </div>
63
- </div>
64
- </template>
65
- <ListViewSettings class="flex-1"
66
- ref="setting"
67
- :config="config"
68
- @change="load" />
69
- </Modal>
82
+ <ContextMenu ref="columnMenu" :dismiss="false">
83
+ <div class="flex-1 flex flex-col w-[270px] p-3">
84
+ <div class="flex flex-col">
85
+ <div class="flex flex-row gap-2 items-center">
86
+ <div class="p-2 text-text-300 flex-1">Sort By</div>
87
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('sort');$refs.columnMenu.close()">Sort Options</div>
88
+ </div>
89
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(1)">Sort Ascending</div>
90
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="setSortCurrent(2)">Sort Descending</div>
91
+ </div>
92
+ <div class="h-[1px] bg-text-50 my-2"></div>
93
+ <div class="flex flex-col">
94
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="hide">Hide</div>
95
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click="openPreset();$refs.columnMenu.close()">Column Options</div>
96
+ </div>
97
+ <div class="h-[1px] bg-text-50 my-2"></div>
98
+ <div class="flex flex-col">
99
+ <div class="flex flex-row gap-2 items-center">
100
+ <div class="p-2 text-text-300 flex-1">Filters</div>
101
+ <div class="text-primary cursor-pointer text-sm" @click="openPreset('filter');$refs.columnMenu.close()">Filter Options</div>
102
+ </div>
103
+ <div v-if="presetCurrentFilters.length > 0">
104
+ <ListPage1Filter v-if="preset.filters" v-for="filter in presetCurrentFilters"
105
+ :filter="filter" :column="config.columns[filter.key]"
106
+ @remove="removeFilter(filter)" @change="load" />
107
+ </div>
108
+ <div v-else>
109
+ <div class="p-2 cursor-pointer" :class="$style.hoverable" @click.stop="addCurrentFilter">Add Filter</div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </ContextMenu>
70
114
 
71
115
  </div>
72
116
  </template>
@@ -74,8 +118,11 @@
74
118
  <script>
75
119
 
76
120
  import throttle from "lodash/throttle";
121
+ import VirtualTable from "./VirtualTable.vue";
122
+
77
123
 
78
124
  export default{
125
+ components: {VirtualTable},
79
126
 
80
127
  emits: [ ],
81
128
 
@@ -92,6 +139,7 @@ export default{
92
139
  defaultValue: {}
93
140
  },
94
141
  configStore: String,
142
+ subscription: String,
95
143
 
96
144
  title: String
97
145
 
@@ -99,6 +147,38 @@ export default{
99
147
 
100
148
  methods: {
101
149
 
150
+ addCurrentFilter(){
151
+ this.addFilter(this.selectedColumn)
152
+ },
153
+
154
+ addFilter(key){
155
+
156
+ const column = this.config.columns[key]
157
+
158
+ if(!this.preset.filters){
159
+ this.preset.filters = []
160
+ }
161
+
162
+ let filters = [{}]
163
+ switch(column.type){
164
+ case 'date':
165
+ case 'enum':
166
+ filters = [{ value:[] }]
167
+ break
168
+ }
169
+
170
+ this.preset.filters.push({
171
+ enabled: true,
172
+ key: column.key,
173
+ label: column.label,
174
+ type: column.type,
175
+ typeParams: column.typeParams,
176
+ filters
177
+ })
178
+
179
+ this.presetFilterSelector = null
180
+ },
181
+
102
182
  clearSearch(){
103
183
  this.preset.search = ''
104
184
  this.load()
@@ -118,6 +198,12 @@ export default{
118
198
  .join(' ')
119
199
  },
120
200
 
201
+ hide(){
202
+ const idx = this.preset.columns.findIndex((_) => _.key === this.selectedColumn)
203
+ this.preset.columns[idx].visible = false
204
+ this.$refs.columnMenu.close()
205
+ },
206
+
121
207
  load(){
122
208
  if(!this.src) return
123
209
 
@@ -146,9 +232,12 @@ export default{
146
232
  },
147
233
 
148
234
  async loadConfig(){
235
+ if(!this.configStoreObj) return
236
+
149
237
  switch(this.configStoreObj.type){
150
238
  case 'socket':
151
- return this.socketEmit2(this.configStoreObj.src, { key:this.name })
239
+ return this.socketEmit2(this.configStoreObj.src,
240
+ { key:this.configStoreObj.name ?? 'ListView' })
152
241
  .then((config) => {
153
242
  Object.assign(this.config, config ?? {})
154
243
  this.configLoaded = true
@@ -156,30 +245,108 @@ export default{
156
245
  }
157
246
  },
158
247
 
248
+ onHooks(model, event, items){
249
+ console.log('onHooks', model, event, items)
250
+ if(model === this.subscriptionObj.model){
251
+ switch(event){
252
+
253
+ case 'create':
254
+ case 'update':
255
+ items.forEach((item) => {
256
+ this.$util.unshift(this.items, item)
257
+ })
258
+ break
259
+
260
+ case 'remove':
261
+ case 'destroy':
262
+ items.forEach((item) => {
263
+ const idx = this.items.findIndex((_) => _.id === item.id)
264
+ if(idx >= 0){
265
+ this.items.splice(idx, 1)
266
+ }
267
+ })
268
+ break
269
+ }
270
+ }
271
+ },
272
+
273
+ openColumnOptions(key, target){
274
+ this.selectedColumn = key
275
+ this.$refs.columnMenu.open(target)
276
+ },
277
+
278
+ openPreset(tab){
279
+ this.$refs.setting.open()
280
+ },
281
+
282
+ removeFilter(filter){
283
+ this.preset.filters.splice(this.preset.filters.indexOf(filter), 1)
284
+ this.load()
285
+ },
286
+
287
+ setSortCurrent(sortType){
288
+ this.preset.sorts = [
289
+ {
290
+ key: this.selectedColumn,
291
+ label: this.config.columns[this.selectedColumn].label,
292
+ type: sortType === 2 ? 'desc' : 'asc'
293
+ }
294
+ ]
295
+ this.load()
296
+ this.$refs.columnMenu.close()
297
+ },
298
+
159
299
  saveConfig: throttle(function() {
160
- if(!this.configLoaded) return
300
+ if(!this.configLoaded || !this.configStoreObj) return
161
301
  switch(this.configStoreObj.type){
162
302
  case 'socket':
163
- this.socketEmit2(this.configStoreObj.src, { key:this.name, config:this.config })
303
+ this.socketEmit2(this.configStoreObj.src,
304
+ { key:this.configStoreObj.name ?? 'ListView', config:this.config })
164
305
  break
165
306
  }
166
307
  }, 1000, { leading:true }),
167
308
 
309
+ subscribe(){
310
+ if(!this.subscriptionObj) return
311
+
312
+ const { type, method, model } = this.subscriptionObj
313
+
314
+ switch(type){
315
+
316
+ case 'socket':
317
+ this.socketEmit2(method, { name:model }).then()
318
+ break
319
+ }
320
+ }
321
+
168
322
  },
169
323
 
170
324
 
171
- inject: [ 'socketEmit2', 'toast', 'alert' ],
325
+ inject: [ 'socket', 'socketEmit2', 'toast', 'alert' ],
172
326
 
173
327
  computed: {
174
328
 
329
+ subscriptionObj(){
330
+ const splitted = ((this.subscription ?? '').toString()).split(':')
331
+ const splitted2 = (splitted[1] ?? '').split(',')
332
+ const obj = {
333
+ type: splitted[0],
334
+ method: splitted2[0],
335
+ model: splitted2[1],
336
+ }
337
+ return obj.type && obj.method && obj.model ? obj : null
338
+ },
339
+
175
340
  configStoreObj(){
176
341
  const splitted = ((this.configStore ?? '').toString()).split(':')
177
-
178
- return {
342
+ const splitted2 = (splitted[1] ?? '').split(',')
343
+ const obj = {
179
344
  type: splitted[0],
180
- src: splitted[1]
181
- }
182
- },
345
+ src: splitted2[0],
346
+ name: splitted2[1]
347
+ }
348
+ return obj.type && obj.src ? obj : null
349
+ },
183
350
 
184
351
  contentSlots(){
185
352
  const slots = {}
@@ -219,6 +386,11 @@ export default{
219
386
  return this.preset.columns
220
387
  },
221
388
 
389
+ presetCurrentFilters(){
390
+ if(!this.preset.filters) return []
391
+ return this.preset.filters.filter((_) => _.key === this.selectedColumn)
392
+ },
393
+
222
394
  },
223
395
 
224
396
  data(){
@@ -226,18 +398,29 @@ export default{
226
398
  items: [],
227
399
  hasNext: false,
228
400
  count: null,
229
- configLoaded: false
401
+ configLoaded: false,
402
+ selectedColumn: null
230
403
  }
231
404
  },
232
405
 
233
406
 
234
407
  mounted() {
235
- this.loadConfig().then(() => {
236
- this.load()
237
- })
408
+ this.socket.onAny(this.onHooks)
409
+
410
+ window.setTimeout(() => {
411
+ this.loadConfig().then(() => {
412
+ this.load()
413
+ })
414
+ }, 201)
415
+
416
+ this.subscribe()
238
417
  },
239
418
 
240
- watch: {
419
+ unmounted() {
420
+ this.socket.offAny(this.onHooks)
421
+ },
422
+
423
+ watch: {
241
424
 
242
425
  config: {
243
426
  deep: true,
@@ -265,4 +448,12 @@ export default{
265
448
  @apply text-ellipsis whitespace-nowrap overflow-x-hidden;
266
449
  }
267
450
 
451
+ .searchBox{
452
+ @apply w-[300px] max-w-[30%];
453
+ }
454
+
455
+ .hoverable{
456
+ @apply hover:bg-primary hover:text-white;
457
+ }
458
+
268
459
  </style>
@@ -1,15 +1,15 @@
1
1
  <template>
2
2
  <div :class="styleComp">
3
- <div>
3
+ <div class="border-r-[1px] border-text-50 bg-base-400">
4
4
 
5
5
  <div class="p-6">
6
6
  <h3>Presets</h3>
7
7
  <br />
8
8
  </div>
9
9
 
10
- <div class="flex-1 overflow-y-auto border-t-[1px] border-text-50">
10
+ <div class="flex-1 overflow-y-auto">
11
11
  <div v-for="(preset, idx) in config.presets"
12
- class="px-6 py-3 flex flex-row gap-2 items-center border-b-[1px] border-text-50 hover:bg-text-50 cursor-pointer">
12
+ class="px-6 py-3 flex flex-row gap-2 items-center hover:bg-text-50 cursor-pointer">
13
13
  <div class="px-2">
14
14
  <Checkbox :checked="idx === config.presetIdx" @change="selectPreset(preset)" />
15
15
  </div>
@@ -1,51 +1,214 @@
1
1
  <template>
2
+
2
3
  <div :class="$style.comp">
3
4
 
4
- <div :class="$style.pageGrid" ref="pageGrid">
5
- <div v-for="(widget, index) in widgets"
6
- :class="$style.column + ` col-span-${widget.span}`"
7
- :style="{ height:widget.height }"
8
- @mouseover="onMoverMouseOver">
9
- <div :class="$style.mover" class="hidden" @mousedown="(e) => onMoverStart(e, widget, index)"></div>
10
- <div :class="$style.resizeRight" @mousedown="(e) => onMouseDown(e, widget, 'right')"></div>
11
- <div :class="$style.resizeBottom" @mousedown="(e) => onMouseDown(e, widget, 'bottom')"></div>
12
- <div :class="$style.resizeBottomRight" @mousedown="(e) => onMouseDown(e, widget, 'bottom-right')"></div>
13
- <div v-if="widget.title">
14
- <h5>{{ widget.title }}</h5>
15
- </div>
16
- <component :is="widget.type" :="widget.props"></component>
5
+ <div v-if="useHeader" class="flex flex-row justify-end gap-8">
6
+ <div class="flex flex-row">
7
+ <Button @click="$refs.widgetSelector.open()" class="px-3">
8
+ <svg width="16" height="16" class="fill-white mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm144 276c0 6.6-5.4 12-12 12h-92v92c0 6.6-5.4 12-12 12h-56c-6.6 0-12-5.4-12-12v-92h-92c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h92v-92c0-6.6 5.4-12 12-12h56c6.6 0 12 5.4 12 12v92h92c6.6 0 12 5.4 12 12v56z"/></svg>
9
+ Widgets
10
+ </Button>
17
11
  </div>
12
+
13
+ <Switch v-model="editMode" />
18
14
  </div>
19
15
 
20
- <div class="col-span-1"></div>
21
- <div class="col-span-2"></div>
22
- <div class="col-span-3"></div>
23
- <div class="col-span-4"></div>
24
- <div class="col-span-5"></div>
25
- <div class="col-span-6"></div>
26
- <div class="col-span-7"></div>
27
- <div class="col-span-8"></div>
28
- <div class="col-span-9"></div>
29
- <div class="col-span-10"></div>
30
- <div class="col-span-11"></div>
16
+ <div :class="`${$style.pageGrid} ${gridClass}`" :style="pageGrid" ref="pageGrid">
17
+ <div v-for="(widget, index) in config.widgets"
18
+ :class="$style.column"
19
+ :style="columnStyle(widget)"
20
+ @mouseover="onMoverMouseOver">
21
+ <div v-if="editMode" :class="$style.mover" @mousedown="(e) => onMoverStart(e, widget, index)"></div>
22
+ <div v-if="editMode" :class="$style.resizeLeft" @mousedown="(e) => onMouseDown(e, widget, 'right')"></div>
23
+ <div v-if="editMode" :class="$style.resizeRight" @mousedown="(e) => onMouseDown(e, widget, 'right')"></div>
24
+ <div v-if="editMode" :class="$style.resizeBottom" @mousedown="(e) => onMouseDown(e, widget, 'bottom')"></div>
25
+ <div v-if="editMode" :class="$style.resizeTop" @mousedown="(e) => onMouseDown(e, widget, 'bottom')"></div>
26
+ <component v-if="widget.type" :is="widget.type" :="widget.props">
27
+ <template v-for="(_, slot) in $slots" #[slot]="{ item }">
28
+ <slot :name="slot" :item="item"></slot>
29
+ </template>
30
+ </component>
31
+ </div>
32
+ </div>
31
33
 
32
- <div class="col-span-12"></div>
34
+ <div class="hidden">
35
+ <div class="col-span-1"></div>
36
+ <div class="col-span-2"></div>
37
+ <div class="col-span-3"></div>
38
+ <div class="col-span-4"></div>
39
+ <div class="col-span-5"></div>
40
+ <div class="col-span-6"></div>
41
+ <div class="col-span-7"></div>
42
+ <div class="col-span-8"></div>
43
+ <div class="col-span-9"></div>
44
+ <div class="col-span-10"></div>
45
+ <div class="col-span-11"></div>
46
+ <div class="col-span-12"></div>
47
+ </div>
33
48
 
49
+ <div class="absolute">
50
+ <Modal ref="widgetSelector" width="420" height="560">
51
+ <template #head>
52
+ <div class="p-6 flex flex-col">
53
+ <div class="flex flex-row">
54
+ <h3 class="flex-1">Widgets</h3>
55
+ <button type="button" @click="$refs.widgetSelector.close()">
56
+ <svg width="19" height="19" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>
57
+ </button>
58
+ </div>
59
+ <div class="flex justify-center mb-4">
60
+ <Tabs :items="tabItems" v-model="tabIndex" />
61
+ </div>
62
+ </div>
63
+ </template>
64
+ <div v-if="tabIndex === 1" class="flex-1 flex flex-col divide-y divide-text-50">
65
+ <div v-for="widget in widgets" class="flex flex-row gap-4 p-4 px-6 hover:bg-primary-50">
66
+ <div class="w-[96px] h-[48px] border-[1px] border-text-50 bg-base-300"></div>
67
+ <div class="flex-1">
68
+ <strong>{{ widget.type ?? widget.title }}</strong>
69
+ </div>
70
+ <div class="grid grid-cols-2 gap-1">
71
+ <div @click="addWidget(widget, { col:3 })">3</div>
72
+ <div @click="addWidget(widget, { col:4 })">4</div>
73
+ <div @click="addWidget(widget, { col:6 })">6</div>
74
+ <div @click="addWidget(widget, { col:12 })">12</div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div v-else class="flex-1 flex">
79
+ <ListItem :items="config.widgets" @reorder="reorderWidget" class="flex-1" bodyClass="divide-y divide-text-50">
80
+ <template v-slot="{ item }">
81
+ <div class="flex flex-row items-center gap-2 px-3" :key="item">
82
+ <div class="p-2 cursor-move" data-reorder>
83
+ <svg width="19" height="19" class="fill-text-300" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
84
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.25 16C4.25 15.5858 4.58579 15.25 5 15.25H19C19.4142 15.25 19.75 15.5858 19.75 16C19.75 16.4142 19.4142 16.75 19 16.75H5C4.58579 16.75 4.25 16.4142 4.25 16Z"/>
85
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M4.25 8C4.25 7.58579 4.58579 7.25 5 7.25H19C19.4142 7.25 19.75 7.58579 19.75 8C19.75 8.41421 19.4142 8.75 19 8.75H5C4.58579 8.75 4.25 8.41421 4.25 8Z"/>
86
+ </svg>
87
+ </div>
88
+ <div class="flex-1">
89
+ <strong>{{ item.title ?? item.type }}</strong>
90
+ </div>
91
+ <div>
92
+ <button type="button" @click="removeWidget(item)">
93
+ <svg width="16" height="16" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"/></svg>
94
+ </button>
95
+ </div>
96
+ </div>
97
+ </template>
98
+ </ListItem>
99
+ </div>
100
+ </Modal>
101
+ </div>
34
102
  </div>
103
+
35
104
  </template>
36
105
 
37
106
  <script>
38
107
 
108
+ import throttle from "lodash/throttle";
109
+ import {parseBoolean} from "../utils/helpers.mjs";
110
+
39
111
  export default{
40
112
 
41
113
  props: {
42
-
43
- widgets: Array
44
-
114
+ config: Object,
115
+ configStore: String,
116
+ widgets: Array,
117
+ header: undefined,
118
+ gap: {
119
+ type: String,
120
+ default: '1rem'
121
+ },
122
+ grid: {
123
+ type: [ String, Number ],
124
+ default: 12
125
+ },
126
+ mode: {
127
+ type: String,
128
+ remark: 'page|screen, default:page'
129
+ },
130
+ gridClass: String
45
131
  },
46
132
 
47
133
  methods: {
48
134
 
135
+ columnStyle(widget){
136
+ let style = {
137
+ gridColumn: `span ${widget.col} / span ${widget.col}`,
138
+ }
139
+
140
+ switch(this.mode){
141
+
142
+ case 'screen':
143
+ Object.assign(style, {
144
+ gridRow: `span ${widget.row} / span ${widget.row}`,
145
+ })
146
+ break
147
+
148
+ case 'page':
149
+ default:
150
+ Object.assign(style, {
151
+ height:(widget.row * this.rowHeight) + 'px'
152
+ })
153
+ break
154
+ }
155
+
156
+ return style
157
+ },
158
+
159
+
160
+ addWidget(widget, props){
161
+ this.config.widgets.push({
162
+ ...widget,
163
+ ...(props ?? {})
164
+ })
165
+ this.$refs.widgetSelector.close()
166
+ },
167
+
168
+ reorderWidget(from, to){
169
+ this.config.widgets.splice(to, 0, this.config.widgets.splice(from, 1)[0])
170
+ },
171
+
172
+ removeWidget(widget){
173
+ const index = this.config.widgets.indexOf(widget)
174
+ if(index >= 0){
175
+ this.config.widgets.splice(index, 1)
176
+ }
177
+ },
178
+
179
+
180
+ toggleEditMode(state){
181
+ if(typeof state !== 'boolean')
182
+ this.editMode = state
183
+ else
184
+ this.editMode = !this.editMode
185
+ },
186
+
187
+
188
+ async loadConfig(){
189
+ if(!this.configStoreObj) return
190
+
191
+ switch(this.configStoreObj.type){
192
+ case 'socket':
193
+ return this.socketEmit2(this.configStoreObj.src,
194
+ { key:this.configStoreObj.name ?? 'pageBuilder' })
195
+ .then((config) => {
196
+ Object.assign(this.config, config ?? {})
197
+ })
198
+ }
199
+ },
200
+
201
+ saveConfig: throttle(function() {
202
+ if(!this.configStoreObj) return
203
+ switch(this.configStoreObj.type){
204
+ case 'socket':
205
+ this.socketEmit2(this.configStoreObj.src,
206
+ { key:this.configStoreObj.name ?? 'pageBuilder', config:this.config })
207
+ break
208
+ }
209
+ }, 1000, { leading:true }),
210
+
211
+
49
212
  onMouseDown(e, widget){
50
213
  this.curResize = {
51
214
  target: e.target.parentNode,
@@ -79,8 +242,8 @@ export default{
79
242
  if(span < 1) span = 1
80
243
  else if(span > 12) span = 12
81
244
 
82
- if(height < this.curResize.ySize) height = this.curResize.ySize
83
- else if(height > window.innerHeight) height = window.innerHeight
245
+ if(height < this.curResize.ySize) height = this.curResize.ySize
246
+ else if(height > window.innerHeight) height = window.innerHeight
84
247
 
85
248
  this.curResize.widget.span = span
86
249
  this.curResize.widget.height = height
@@ -92,12 +255,12 @@ export default{
92
255
  this.curResize = null
93
256
  },
94
257
 
95
- onMoverStart(e, widget, index){
258
+ onMoverStart(e, widget, index){
96
259
 
97
- const columnEl = e.target.parentNode;
98
- const rect = columnEl.getBoundingClientRect()
260
+ const columnEl = e.target.parentNode;
261
+ const rect = columnEl.getBoundingClientRect()
99
262
 
100
- const cloned = columnEl.cloneNode(true)
263
+ const cloned = columnEl.cloneNode(true)
101
264
  cloned.classList.add(this.$style.dragged)
102
265
  cloned.style.left = (Math.round(rect.x) + 16) + 'px'
103
266
  cloned.style.top = (Math.round(rect.y) + 16) + 'px'
@@ -105,73 +268,135 @@ export default{
105
268
  cloned.style.height = columnEl.clientHeight + 'px'
106
269
  this.$el.appendChild(cloned)
107
270
 
108
- this.curResize = {
109
- widget,
110
- index,
271
+ this.curResize = {
272
+ widget,
273
+ index,
111
274
  cloned,
112
275
  startX: e.clientX,
113
276
  startY: e.clientY
114
- }
277
+ }
115
278
 
116
- window.addEventListener('mousemove', this.onMoverMove)
117
- window.addEventListener('mouseup', this.onMoverUp)
279
+ window.addEventListener('mousemove', this.onMoverMove)
280
+ window.addEventListener('mouseup', this.onMoverUp)
118
281
  },
119
282
 
120
283
  swapArray(arr, i, j){
121
- const temp = arr[i];
122
- arr[i] = arr[j];
123
- arr[j] = temp;
124
- },
284
+ const temp = arr[i];
285
+ arr[i] = arr[j];
286
+ arr[j] = temp;
287
+ },
125
288
 
126
- onMoverMove(e){
127
- if(!this.curResize) return
289
+ onMoverMove(e){
290
+ if(!this.curResize) return
128
291
 
129
- e.preventDefault()
292
+ e.preventDefault()
130
293
 
131
294
  const cloned = this.curResize.cloned
132
295
  const distanceX = e.clientX - this.curResize.startX
133
296
  const distanceY = e.clientY - this.curResize.startY
134
- this.curResize.startX = e.clientX
297
+ this.curResize.startX = e.clientX
135
298
  this.curResize.startY = e.clientY
136
299
 
137
300
  cloned.style.left = (parseInt(cloned.style.left) + distanceX) + 'px'
138
301
  cloned.style.top = (parseInt(cloned.style.top) + distanceY) + 'px'
139
302
  },
140
303
 
141
- onMoverMouseOver(e){
142
- if(!this.curResize || !this.curResize.cloned) return
304
+ onMoverMouseOver(e){
305
+ if(!this.curResize || !this.curResize.cloned) return
306
+
307
+ e.preventDefault()
143
308
 
144
- const targetIdx = Array.prototype.indexOf.call(this.$refs.pageGrid.children, e.target.parentNode)
309
+ const column = e.target.closest('.' + this.$style.column)
145
310
 
146
- this.$refs.pageGrid.querySelectorAll('.' + this.$style.highlight).forEach(el => el.classList.remove(this.$style.highlight))
311
+ const targetIdx = Array.prototype.indexOf.call(this.$refs.pageGrid.children, column)
312
+
313
+ this.$refs.pageGrid.querySelectorAll('.' + this.$style.highlight).forEach(el => el.classList.remove(this.$style.highlight))
147
314
 
148
315
  if(targetIdx !== this.curResize.index){
149
- this.curResize.targetIdx = targetIdx
150
- e.target.classList.add(this.$style.highlight)
316
+ this.curResize.targetIdx = targetIdx
317
+ e.target.classList.add(this.$style.highlight)
151
318
  }
152
319
  },
153
320
 
154
- onMoverUp(){
155
- this.swapArray(this.widgets, this.curResize.targetIdx, this.curResize.index)
321
+ onMoverUp(){
322
+ this.swapArray(this.config.widgets, this.curResize.targetIdx, this.curResize.index)
323
+
324
+ window.removeEventListener('mousemove', this.onMoverMove)
325
+ window.removeEventListener('mouseup', this.onMoverUp)
326
+ this.$el.removeChild(this.curResize.cloned)
327
+ this.curResize = null
328
+
329
+ this.$refs.pageGrid.querySelectorAll('.' + this.$style.highlight).forEach(el => el.classList.remove(this.$style.highlight))
330
+ }
331
+
332
+ },
156
333
 
157
- window.removeEventListener('mousemove', this.onMoverMove)
158
- window.removeEventListener('mouseup', this.onMoverUp)
159
- this.$el.removeChild(this.curResize.cloned)
160
- this.curResize = null
161
334
 
162
- this.$refs.pageGrid.querySelectorAll('.' + this.$style.highlight).forEach(el => el.classList.remove(this.$style.highlight))
335
+ computed: {
336
+
337
+ configStoreObj(){
338
+ const splitted = ((this.configStore ?? '').toString()).split(':')
339
+ const splitted2 = (splitted[1] ?? '').split(',')
340
+ const obj = {
341
+ type: splitted[0],
342
+ src: splitted2[0],
343
+ name: splitted2[1]
344
+ }
345
+ return obj.type && obj.src ? obj : null
163
346
  },
164
347
 
165
- test(){
166
- this.swapArray(this.widgets, 4, 0)
348
+ useHeader(){
349
+ return parseBoolean(this.header)
350
+ },
351
+
352
+ pageGrid(){
353
+ const style = {
354
+ display: 'grid',
355
+ gridTemplateColumns: `repeat(${this.grid}, 1fr)`,
356
+ gridGap: this.gap
357
+ }
358
+
359
+ if(this.mode === 'screen'){
360
+ Object.assign(style, {
361
+ gridTemplateRows: `repeat(${this.grid}, 1fr)`
362
+ })
363
+ }
364
+
365
+ return style
167
366
  }
168
367
 
169
368
  },
170
369
 
171
370
  data(){
172
371
  return {
173
- curResize: null
372
+ curResize: null,
373
+ editMode: false,
374
+ tabItems: [
375
+ { text:"Add Widget", value:1 },
376
+ { text:"Widgets", value:2 }
377
+ ],
378
+ tabIndex: 1,
379
+ rowHeight: 0
174
380
  }
381
+ },
382
+
383
+ inject: [ 'socketEmit2' ],
384
+
385
+ mounted() {
386
+ this.loadConfig()
387
+
388
+ this.rowHeight = Math.round(window.innerHeight / parseInt(this.grid))
389
+ },
390
+
391
+ watch: {
392
+
393
+ config: {
394
+ handler(){
395
+ this.saveConfig()
396
+ },
397
+ deep: true
398
+ }
399
+
175
400
  }
176
401
 
177
402
  }
@@ -181,6 +406,7 @@ export default{
181
406
  <style module>
182
407
 
183
408
  .comp{
409
+ @apply flex flex-col gap-4;
184
410
  }
185
411
 
186
412
  .column{
@@ -188,20 +414,23 @@ export default{
188
414
  }
189
415
 
190
416
  .pageGrid{
191
- @apply grid grid-cols-12 gap-6;
417
+ @apply flex-1;
192
418
  }
193
419
  .pageGrid>*{
194
420
  @apply relative flex;
195
421
  }
196
422
 
423
+ .resizeLeft{
424
+ @apply absolute top-[-8px] left-[-8px] w-[4px] bottom-[-8px] bg-text-50 cursor-e-resize;
425
+ }
197
426
  .resizeRight{
198
- @apply absolute top-0 right-0 w-[4px] bottom-[6px] bg-text-50 cursor-e-resize;
427
+ @apply absolute top-[-8px] right-[-8px] w-[4px] bottom-[-8px] bg-text-50 cursor-e-resize;
199
428
  }
200
429
  .resizeBottom{
201
- @apply absolute bottom-0 left-0 right-[6px] h-[4px] bg-text-50 cursor-s-resize;
430
+ @apply absolute bottom-[-8px] left-[-8px] right-[-8px] h-[4px] bg-text-50 cursor-s-resize;
202
431
  }
203
- .resizeBottomRight{
204
- @apply absolute bottom-0 right-0 w-[4px] h-[4px] bg-text-50 cursor-se-resize;
432
+ .resizeTop{
433
+ @apply absolute top-[-8px] left-[-8px] right-[-8px] h-[4px] bg-text-50 cursor-s-resize;
205
434
  }
206
435
 
207
436
  .mover{
@@ -217,3 +446,4 @@ export default{
217
446
  }
218
447
 
219
448
  </style>
449
+
@@ -163,8 +163,12 @@ export default{
163
163
  const elHeight = parseInt(window.getComputedStyle(this.$el).height !== '0px' ?
164
164
  window.getComputedStyle(this.$el).height :
165
165
  window.getComputedStyle(this.$el).maxHeight)
166
+
167
+ if(isNaN(elHeight)) return
168
+
166
169
  this.itemHeight = parseInt(window.getComputedStyle(this.$refs.calc).height)
167
170
  this.maxVisibleItems = elHeight > 0 ? Math.ceil(elHeight / this.itemHeight) + 1 : this.items.length
171
+
168
172
  //console.log('Virtual scroll resize', { elHeight, itemHeight:this.itemHeight, maxVisibleItems:this.maxVisibleItems })
169
173
 
170
174
  if(this.itemHeight <= 0){
@@ -236,4 +240,4 @@ export default{
236
240
  top: -10000px;
237
241
  }
238
242
 
239
- </style>
243
+ </style>