renusify 2.4.4 → 2.5.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.
@@ -12,7 +12,7 @@
12
12
  width: 40px;
13
13
  height: 40px;
14
14
  position: absolute;
15
- transition: 2s all map.get(base.$transition, 'swing');
15
+ transition: 0.5s all map.get(base.$transition, 'swing');
16
16
  @include mixins.rtl() {
17
17
  right: -40px;
18
18
  }
@@ -25,7 +25,7 @@
25
25
  height: 100%;
26
26
  overflow: auto;
27
27
  position: absolute;
28
- transition: .5s all map.get(base.$transition, 'swing');
28
+ transition: .3s all map.get(base.$transition, 'swing');
29
29
  background-color: var(--color-sheet);
30
30
  color: var(--color-on-sheet);
31
31
  @include mixins.rtl() {
@@ -42,7 +42,7 @@
42
42
  height: 100vh;
43
43
  z-index: map.get(base.$z-index, 'important');
44
44
  background-color: var(--color-overlay);
45
- transition: 1s width map.get(base.$transition, 'swing');
45
+ transition: .1s width map.get(base.$transition, 'swing');
46
46
  overflow: hidden;
47
47
  @include mixins.rtl() {
48
48
  right: 0;
@@ -56,7 +56,7 @@
56
56
  width: 100vw;
57
57
  }
58
58
  .toolbar-side {
59
- transition: 2s all map.get(base.$transition, 'swing');
59
+ transition: 0.3s all map.get(base.$transition, 'swing');
60
60
  @include mixins.rtl() {
61
61
  right: 0;
62
62
  }
@@ -106,7 +106,7 @@
106
106
  }
107
107
 
108
108
  .menu-list {
109
- transition: 1s map.get(base.$transition, 'swing');
109
+ transition: 0.3s map.get(base.$transition, 'swing');
110
110
  min-width: 200px;
111
111
  max-height: 500px;
112
112
  overflow-y: auto;
@@ -129,7 +129,7 @@
129
129
  position: relative;
130
130
 
131
131
  .toolbar-childs {
132
- transition: 1s map.get(base.$transition, 'swing');
132
+ transition: 0.3s map.get(base.$transition, 'swing');
133
133
  position: absolute;
134
134
  top: 40px;
135
135
  max-height: 0;
@@ -85,7 +85,7 @@ emits:['update:modelValue','change'],
85
85
  width: 100%;
86
86
  height: 100%;
87
87
  border-radius: 10px;
88
- background-color: var(--color-sheet-container);
88
+ background-color: var(--color-border);
89
89
  }
90
90
 
91
91
  .switch-dot {
@@ -1,18 +1,13 @@
1
1
  <template>
2
2
  <div v-intersect.[modifier]="check" :class="{
3
3
  [$r.prefix+'img']:true,
4
- 'img-hoverZoom':hoverZoom,'img-hoverDark':hoverDark,'img-hoverTitle':hoverTitle}"
4
+ 'img-hoverZoom':hoverZoom,'img-hoverDark':hoverDark,'img-hover-preview':preview}"
5
5
  ref="rImg">
6
- <div v-if="hoverTitle" class="title-3 color-white-text img-title w-full pa-1"
7
- :class="{
8
- 'title-hs':titleHs,
9
- 'title-hc':titleHc,
10
- 'title-he':titleHe,
11
- 'title-vs':titleVs,
12
- 'title-vc':titleVc,
13
- 'title-ve':titleVe
14
- }"
15
- >{{ alt }}
6
+ <div v-if="preview" class="img-preview w-full h-full d-flex v-center h-center"
7
+ >
8
+ <r-btn icon @click="show_preview=true">
9
+ <r-icon v-html="$r.icons.eye"></r-icon>
10
+ </r-btn>
16
11
  </div>
17
12
  <img v-if="load &&!isSvg" ref="img" :alt="alt" :height="size.height>0?size.height:'auto'" :src="link" :style="{'height':size.height>0?undefined:'auto',
18
13
  'width':size.width>0?undefined:'auto'
@@ -21,6 +16,9 @@
21
16
  draggable="false"/>
22
17
  <svg-img v-else-if="load &&isSvg&&link" :link="link" :size="size">
23
18
  </svg-img>
19
+ <teleport v-if="preview&&show_preview" :to="`.${$r.prefix}app`">
20
+ <preview-img :src="preview" @close="show_preview=false"></preview-img>
21
+ </teleport>
24
22
  </div>
25
23
  </template>
26
24
  <script>
@@ -28,12 +26,16 @@ import {defineAsyncComponent} from 'vue'
28
26
 
29
27
  export default {
30
28
  name: 'r-img',
31
- components: {SvgImg:defineAsyncComponent(()=>import('./svgImg.vue'))},
29
+ components: {
30
+ SvgImg: defineAsyncComponent(() => import('./svgImg.vue')),
31
+ previewImg: defineAsyncComponent(() => import('./preview.vue')),
32
+ },
32
33
  props: {
33
34
  src: {
34
35
  type: String,
35
36
  required: true
36
37
  },
38
+ preview: String,
37
39
  alt: {
38
40
  type: String,
39
41
  required: true
@@ -55,19 +57,13 @@ export default {
55
57
  autoSize: Boolean,
56
58
  hoverZoom: Boolean,
57
59
  hoverDark: Boolean,
58
- hoverTitle: Boolean,
59
- titleHs: Boolean,
60
- titleHc: Boolean,
61
- titleHe: Boolean,
62
- titleVs: Boolean,
63
- titleVc: Boolean,
64
- titleVe: Boolean,
65
60
  isSvg: Boolean,
66
61
  svgCache: {type: Number, default: 86400},
67
62
  wPH: Number
68
63
  },
69
64
  data() {
70
65
  return {
66
+ show_preview: false,
71
67
  load: false,
72
68
  view: false,
73
69
  modifier: this.lazy !== 'no' ? 'once' : 'pre',
@@ -180,6 +176,7 @@ export default {
180
176
  }
181
177
  </script>
182
178
  <style lang="scss">
179
+ @use "sass:map";
183
180
  @use "../../style/variables/base";
184
181
 
185
182
  .#{base.$prefix}img {
@@ -211,44 +208,65 @@ export default {
211
208
  }
212
209
  }
213
210
 
214
- &.img-hoverTitle {
211
+ &.img-hover-preview {
215
212
  &:hover {
216
- .img-title {
213
+ .img-preview {
214
+ background: rgba(0, 0, 0, 0.5);
217
215
  max-width: 100%;
218
216
  }
219
217
  }
220
218
  }
221
219
 
222
- .img-title {
220
+ .img-preview {
221
+ transition: 0.1s all ease;
223
222
  position: absolute;
224
223
  z-index: 1;
224
+ top: 0;
225
+ left: 0;
225
226
  max-width: 0;
226
227
  overflow: hidden;
228
+ }
229
+ }
227
230
 
228
- &.title-hs {
229
- text-align: start;
230
- }
231
-
232
- &.title-hc {
233
- text-align: center;
234
- }
235
-
236
- &.title-he {
237
- text-align: end;
238
- }
231
+ .#{base.$prefix}img-preview-container {
232
+ width: 100vw;
233
+ height: 100vh;
234
+ overflow: hidden;
235
+ position: fixed;
236
+ touch-action: none;
237
+ background: var(--color-overlay);
238
+ backdrop-filter: blur(3px) grayscale(30%);
239
+ z-index: map.get(base.$z-index, 'important');
240
+ top: 0;
241
+ left: 0;
239
242
 
240
- &.title-vs {
241
- top: 10px
242
- }
243
+ .image-wrapper {
244
+ position: absolute;
245
+ will-change: transform;
246
+ }
243
247
 
244
- &.title-vc {
245
- top: 50%
246
- }
248
+ .image-wrapper img {
249
+ display: block;
250
+ }
247
251
 
248
- &.title-ve {
249
- bottom: 10px
250
- }
252
+ .controls {
253
+ position: absolute;
254
+ bottom: 10px;
255
+ left: 50%;
256
+ transform: translateX(-50%);
257
+ background: rgba(0, 0, 0, 0.5);
258
+ color: white;
259
+ padding: 5px 10px;
260
+ border-radius: 5px;
261
+ display: flex;
262
+ gap: 10px;
263
+ align-items: center;
264
+ z-index: 2;
265
+ }
251
266
 
267
+ .controls button {
268
+ padding: 2px 8px;
269
+ cursor: pointer;
252
270
  }
253
271
  }
254
272
  </style>
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div ref="container"
3
+ :class="{ [$r.prefix+'img-preview-container']:true}">
4
+ <div
5
+ :style="wrapperStyle"
6
+ class="image-wrapper"
7
+ @mousedown="startDrag"
8
+ @touchstart="startDrag"
9
+ @wheel.prevent="handleWheel"
10
+ >
11
+ <span :style="imageHolderStyle">
12
+ <img
13
+ :src="src"
14
+ :style="imageStyle"
15
+ alt="Preview image"
16
+ @load="handleImageLoad"
17
+ >
18
+ </span>
19
+ </div>
20
+
21
+ <div v-if="showControls" class="controls">
22
+ <button @click="zoomIn">
23
+ <r-icon v-html="$r.icons.plus"></r-icon>
24
+ </button>
25
+ <button @click="zoomOut">
26
+ <r-icon v-html="$r.icons.minus"></r-icon>
27
+ </button>
28
+ <button class="font-weight-bold" @click="rotate">↻</button>
29
+ <button @click="reset">R</button>
30
+ <button @click="$emit('close',true)">
31
+ <r-icon v-html="$r.icons.close"></r-icon>
32
+ </button>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
39
+
40
+ export default {
41
+ props: {
42
+ src: {
43
+ type: String,
44
+ required: true
45
+ },
46
+ maxScale: {
47
+ type: Number,
48
+ default: 5
49
+ },
50
+ minScale: {
51
+ type: Number,
52
+ default: 0.1
53
+ },
54
+ showControls: {
55
+ type: Boolean,
56
+ default: true
57
+ }
58
+ },
59
+
60
+ setup(props) {
61
+ const container = ref(null);
62
+ const scale = ref(1);
63
+ const rotation = ref(0);
64
+ const position = ref({x: 0, y: 0});
65
+ const isDragging = ref(false);
66
+ const dragStart = ref({x: 0, y: 0});
67
+ const naturalSize = ref({width: 0, height: 0});
68
+ const containerSize = ref({width: 0, height: 0});
69
+
70
+ const wrapperStyle = computed(() => ({
71
+ transform: `translate(${position.value.x}px, ${position.value.y}px)`,
72
+ cursor: isDragging.value ? 'grabbing' : 'grab'
73
+ }));
74
+
75
+ const imageStyle = computed(() => ({
76
+ transform: `scale(${scale.value})`,
77
+ transformOrigin: '0 0',
78
+ width: naturalSize.value.width ? `${naturalSize.value.width}px` : 'auto',
79
+ height: naturalSize.value.height ? `${naturalSize.value.height}px` : 'auto',
80
+ display: 'block'
81
+ }));
82
+ const imageHolderStyle = computed(() => ({
83
+ transform: `rotate(${rotation.value}deg)`,
84
+ transformOrigin: 'center center',
85
+ width: naturalSize.value.width ? `${naturalSize.value.width * scale.value}px` : 'auto',
86
+ height: naturalSize.value.height ? `${naturalSize.value.height * scale.value}px` : 'auto',
87
+ display: 'block'
88
+ }));
89
+
90
+ const handleImageLoad = (event) => {
91
+ naturalSize.value = {
92
+ width: event.target.naturalWidth,
93
+ height: event.target.naturalHeight
94
+ };
95
+ updateContainerSize();
96
+ centerImage();
97
+ };
98
+
99
+ const updateContainerSize = () => {
100
+ if (container.value) {
101
+ containerSize.value = {
102
+ width: container.value.clientWidth,
103
+ height: container.value.clientHeight
104
+ };
105
+ }
106
+ };
107
+
108
+ const centerImage = () => {
109
+ if (!container.value || !naturalSize.value.width) return;
110
+
111
+ const widthRatio = containerSize.value.width / naturalSize.value.width;
112
+ const heightRatio = containerSize.value.height / naturalSize.value.height;
113
+ const initialScale = Math.min(widthRatio, heightRatio, 1); // Don't scale up initially
114
+
115
+ scale.value = initialScale;
116
+ rotation.value = 0;
117
+
118
+ position.value = {
119
+ x: (containerSize.value.width - naturalSize.value.width * scale.value) / 2,
120
+ y: (containerSize.value.height - naturalSize.value.height * scale.value) / 2
121
+ };
122
+ };
123
+
124
+ const rotate = () => {
125
+ if (!container.value) return;
126
+ rotation.value = (rotation.value + 22.5) % 360;
127
+ };
128
+
129
+ const zoom = (factor, clientX, clientY) => {
130
+ if (!container.value) return;
131
+
132
+ const oldScale = scale.value;
133
+ scale.value = Math.min(
134
+ Math.max(scale.value * factor, props.minScale),
135
+ props.maxScale
136
+ );
137
+
138
+ const rect = container.value.getBoundingClientRect();
139
+ const containerX = clientX - rect.left;
140
+ const containerY = clientY - rect.top;
141
+
142
+ const imageX = (containerX - position.value.x) / oldScale;
143
+ const imageY = (containerY - position.value.y) / oldScale;
144
+
145
+ position.value.x = containerX - imageX * scale.value;
146
+ position.value.y = containerY - imageY * scale.value;
147
+ };
148
+
149
+ const zoomIn = () => {
150
+ if (!container.value) return;
151
+ const rect = container.value.getBoundingClientRect();
152
+ zoom(1.2, rect.left + rect.width / 2, rect.top + rect.height / 2);
153
+ };
154
+
155
+ const zoomOut = () => {
156
+ if (!container.value) return;
157
+ const rect = container.value.getBoundingClientRect();
158
+ zoom(0.8, rect.left + rect.width / 2, rect.top + rect.height / 2);
159
+ };
160
+
161
+ const reset = () => {
162
+ centerImage();
163
+ };
164
+
165
+ const startDrag = (e) => {
166
+ if (isDragging.value) return
167
+ isDragging.value = true;
168
+ const clientX = e.clientX || e.touches[0].clientX;
169
+ const clientY = e.clientY || e.touches[0].clientY;
170
+
171
+ dragStart.value = {
172
+ x: clientX - position.value.x,
173
+ y: clientY - position.value.y
174
+ };
175
+
176
+ e.preventDefault();
177
+ };
178
+
179
+ const handleDrag = (e) => {
180
+ if (!isDragging.value) return;
181
+
182
+ const clientX = e.clientX || (e.touches && e.touches[0].clientX);
183
+ const clientY = e.clientY || (e.touches && e.touches[0].clientY);
184
+
185
+ if (clientX === undefined || clientY === undefined) return;
186
+
187
+ position.value = {
188
+ x: clientX - dragStart.value.x,
189
+ y: clientY - dragStart.value.y
190
+ };
191
+ e.preventDefault();
192
+ };
193
+
194
+ const endDrag = () => {
195
+ isDragging.value = false;
196
+ };
197
+
198
+ const handleWheel = (e) => {
199
+ const factor = e.deltaY < 0 ? 1.1 : 0.9;
200
+ zoom(factor, e.clientX, e.clientY);
201
+ };
202
+
203
+ onMounted(() => {
204
+ window.addEventListener('mousemove', handleDrag);
205
+ window.addEventListener('mouseup', endDrag);
206
+ window.addEventListener('touchmove', handleDrag, {passive: false});
207
+ window.addEventListener('touchend', endDrag);
208
+ window.addEventListener('resize', updateContainerSize);
209
+ });
210
+
211
+ onBeforeUnmount(() => {
212
+ window.removeEventListener('mousemove', handleDrag);
213
+ window.removeEventListener('mouseup', endDrag);
214
+ window.removeEventListener('touchmove', handleDrag);
215
+ window.removeEventListener('touchend', endDrag);
216
+ window.removeEventListener('resize', updateContainerSize);
217
+ });
218
+
219
+ return {
220
+ container,
221
+ scale,
222
+ rotation,
223
+ position,
224
+ wrapperStyle,
225
+ imageStyle,
226
+ imageHolderStyle,
227
+ handleImageLoad,
228
+ startDrag,
229
+ handleWheel,
230
+ zoomIn,
231
+ zoomOut,
232
+ reset,
233
+ rotate
234
+ };
235
+ }
236
+ };
237
+ </script>
@@ -7,61 +7,83 @@
7
7
  @touchend="touchend"
8
8
  @touchmove="touchmove"
9
9
  @touchstart="dragstart"
10
+ :aria-expanded="!closed"
11
+ :aria-label="item.title || `Item ${item[keyProp]}`"
12
+ role="listitem"
10
13
  >
11
14
  <div
12
15
  class="nestable-item-content"
13
16
  @mouseenter="onMouseEnter"
14
17
  @mouseleave="onMouseLeave"
15
18
  @mousemove="onMouseMove"
19
+ tabindex="0"
20
+ @keydown="onKeyDown"
16
21
  >
17
22
  <r-btn-confirm
18
23
  v-if="editable"
19
- class="color-error-text"
24
+ :aria-label="`Delete ${item.title || 'item'}`"
20
25
  icon
21
26
  text
22
27
  @click="del(item)"
28
+ class="color-error-text mr-2"
23
29
  >
24
30
  <r-icon v-html="$r.icons.delete"></r-icon>
25
31
  </r-btn-confirm>
26
- <r-btn v-if="hasChildren" icon text @click="closed=!closed">
27
- <r-icon v-html="closed?$r.icons.plus:$r.icons.minus"></r-icon>
32
+
33
+ <r-btn
34
+ v-if="hasChildren"
35
+ :aria-label="closed ? 'Expand' : 'Collapse'"
36
+ class="mr-2"
37
+ icon
38
+ text
39
+ @click="toggleChildren"
40
+ >
41
+ <r-icon v-html="closed ? $r.icons.plus : $r.icons.minus"></r-icon>
28
42
  </r-btn>
29
- <r-btn v-else icon text></r-btn>
30
- <slot :item="item">{{ item }}</slot>
43
+
44
+ <r-btn
45
+ v-else
46
+ aria-hidden="true"
47
+ class="mr-2"
48
+ icon
49
+ text
50
+ ></r-btn>
51
+
52
+ <slot :item="item">{{ item.title || `Item ${item[keyProp]}` }}</slot>
31
53
  </div>
32
- <div v-if="hasChildren&&!closed" class="nestable-list ms-5">
33
- <template
34
- v-for="(child, childIndex) in item[childrenProp]"
35
- :key="childIndex"
54
+
55
+ <transition name="nestable-children">
56
+ <div
57
+ v-if="hasChildren && !closed"
58
+ class="nestable-list ms-5"
59
+ role="group"
36
60
  >
37
- <NestableItem
38
- :childrenProp="childrenProp"
39
- :dragItem="dragItem"
40
- :index="childIndex"
41
- :item="child"
42
- :key-prop="keyProp"
43
- is-child
44
- @delete="$emit('delete', $event)"
45
- @drag-start="$emit('drag-start', $event)"
46
- @touch-move="$emit('touch-move', $event)"
47
- @mouse-enter="$emit('mouse-enter', $event)"
48
- @drag-end="$emit('drag-end', $event)"
61
+ <template
62
+ v-for="(child, childIndex) in item[childrenProp]"
63
+ :key="child[keyProp]"
49
64
  >
50
- <template v-slot="{ item }">
51
- <r-btn-confirm
52
- v-if="editable"
53
- class="color-error-text"
54
- icon
55
- text
56
- @click="del(item)"
57
- >
58
- <r-icon v-html="$r.icons.delete"></r-icon>
59
- </r-btn-confirm>
60
- <slot :item="item" itemscope>{{ item }}</slot>
61
- </template>
62
- </NestableItem>
63
- </template>
64
- </div>
65
+ <NestableItem
66
+ :childrenProp="childrenProp"
67
+ :dragItem="dragItem"
68
+ :editable="editable"
69
+ :index="childIndex"
70
+ :item="child"
71
+ :keyProp="keyProp"
72
+ :maxDepth="maxDepth"
73
+ is-child
74
+ @delete="$emit('delete', $event)"
75
+ @drag-start="$emit('drag-start', $event)"
76
+ @touch-move="$emit('touch-move', $event)"
77
+ @mouse-enter="$emit('mouse-enter', $event)"
78
+ @drag-end="$emit('drag-end', $event)"
79
+ >
80
+ <template v-slot="{ item }">
81
+ <slot :item="item" itemscope>{{ item.title || `Item ${item[keyProp]}` }}</slot>
82
+ </template>
83
+ </NestableItem>
84
+ </template>
85
+ </div>
86
+ </transition>
65
87
  </div>
66
88
  </template>
67
89
 
@@ -88,12 +110,14 @@ export default {
88
110
  },
89
111
  childrenProp: String,
90
112
  dragItem: Object,
113
+ maxDepth: Number,
91
114
  },
92
115
  data() {
93
116
  return {
94
117
  breakPoint: null,
95
118
  moveDown: false,
96
- closed: false
119
+ closed: false,
120
+ isKeyboardNavigating: false
97
121
  };
98
122
  },
99
123
  computed: {
@@ -110,28 +134,65 @@ export default {
110
134
  },
111
135
 
112
136
  itemClasses() {
113
- const isDragging = this.isDragging ? ["is-dragging"] : [];
114
-
115
- return [
137
+ const classes = [
116
138
  "nestable-item",
117
139
  `nestable-item-${this.item[this.keyProp]}`,
118
- ...isDragging,
119
140
  ];
141
+
142
+ if (this.isDragging) {
143
+ classes.push("is-dragging");
144
+ }
145
+
146
+ if (this.isKeyboardNavigating) {
147
+ classes.push("is-focused");
148
+ }
149
+
150
+ return classes;
120
151
  },
152
+
153
+ depth() {
154
+ return this.getDepth(this.item);
155
+ }
121
156
  },
122
157
  methods: {
158
+ getDepth(item, level = 0) {
159
+ if (!item[this.childrenProp] || item[this.childrenProp].length === 0) {
160
+ return level;
161
+ }
162
+
163
+ let maxDepth = level;
164
+ item[this.childrenProp].forEach(child => {
165
+ const childDepth = this.getDepth(child, level + 1);
166
+ if (childDepth > maxDepth) {
167
+ maxDepth = childDepth;
168
+ }
169
+ });
170
+
171
+ return maxDepth;
172
+ },
173
+
174
+ toggleChildren() {
175
+ this.closed = !this.closed;
176
+ },
177
+
123
178
  del(item) {
124
179
  this.$emit("delete", item);
125
180
  },
181
+
126
182
  dragstart(event) {
183
+ event.dataTransfer.setData('text/plain', this.item[this.keyProp]);
184
+ event.dataTransfer.effectAllowed = 'move';
127
185
  this.$emit("drag-start", [event, this.item]);
128
186
  },
187
+
129
188
  touchend(event) {
130
189
  this.$emit("drag-end", event);
131
190
  },
191
+
132
192
  touchmove(event) {
133
193
  this.$emit("touch-move", event);
134
194
  },
195
+
135
196
  onMouseEnter(event) {
136
197
  if (!this.dragItem) return;
137
198
 
@@ -140,12 +201,13 @@ export default {
140
201
  }
141
202
 
142
203
  this.moveDown = event.movementY > 0;
143
-
144
204
  this.breakPoint = event.target.getBoundingClientRect().height / 2;
145
205
  },
206
+
146
207
  onMouseLeave() {
147
208
  this.breakPoint = null;
148
209
  },
210
+
149
211
  onMouseMove(event) {
150
212
  if (!this.breakPoint) return;
151
213
 
@@ -156,10 +218,90 @@ export default {
156
218
 
157
219
  this.sendNotification(event);
158
220
  },
221
+
159
222
  sendNotification(event) {
160
223
  this.breakPoint = null;
161
224
  this.$emit("mouse-enter", [event, this.item]);
162
225
  },
163
- },
226
+
227
+ onKeyDown(event) {
228
+ // Handle keyboard navigation
229
+ switch (event.key) {
230
+ case 'ArrowUp':
231
+ this.navigate(-1);
232
+ break;
233
+ case 'ArrowDown':
234
+ this.navigate(1);
235
+ break;
236
+ case 'ArrowLeft':
237
+ if (!this.closed && this.hasChildren) {
238
+ this.closed = true;
239
+ }
240
+ break;
241
+ case 'ArrowRight':
242
+ if (this.closed && this.hasChildren) {
243
+ this.closed = false;
244
+ }
245
+ break;
246
+ case 'Enter':
247
+ case ' ':
248
+ if (this.hasChildren) {
249
+ this.closed = !this.closed;
250
+ }
251
+ break;
252
+ case 'Delete':
253
+ if (this.editable) {
254
+ this.del(this.item);
255
+ }
256
+ break;
257
+ default:
258
+ return;
259
+ }
260
+
261
+ event.preventDefault();
262
+ },
263
+
264
+ navigate(direction) {
265
+ // Implement keyboard navigation between items
266
+ this.isKeyboardNavigating = true;
267
+ // You'll need to implement actual navigation logic here
268
+ // This would involve finding the next/previous item in the tree
269
+ // and focusing it
270
+ }
271
+ }
164
272
  };
165
273
  </script>
274
+
275
+ <style lang="scss">
276
+ @use "../../style/variables/base";
277
+
278
+ .#{base.$prefix}nestable {
279
+ .nestable-item {
280
+ outline: none;
281
+
282
+ &.is-focused {
283
+ box-shadow: 0 0 0 2px #4a90e2;
284
+ }
285
+ }
286
+
287
+ .nestable-children-enter-active,
288
+ .nestable-children-leave-active {
289
+ transition: all 0.3s ease;
290
+ overflow: hidden;
291
+ }
292
+
293
+ .nestable-children-enter-from,
294
+ .nestable-children-leave-to {
295
+ opacity: 0;
296
+ transform: translateY(-10px);
297
+ max-height: 0;
298
+ }
299
+
300
+ .nestable-children-enter-to,
301
+ .nestable-children-leave-from {
302
+ opacity: 1;
303
+ transform: translateY(0);
304
+ max-height: 1000px;
305
+ }
306
+ }
307
+ </style>
@@ -1,7 +1,11 @@
1
1
  <template>
2
- <div ref="nestable" :class="`${$r.prefix}nestable`">
3
- <div class="nestable-list nestable-group">
4
- <template v-for="(item, index) in modelValue" :key="index">
2
+ <div
3
+ ref="nestable"
4
+ :class="`${$r.prefix}nestable`"
5
+ aria-label="Nestable list"
6
+ >
7
+ <div class="nestable-list nestable-group" role="list">
8
+ <template v-for="(item, index) in modelValue" :key="item[keyProp]">
5
9
  <nestable-item
6
10
  :childrenProp="childrenProp"
7
11
  :dragItem="dragItem"
@@ -9,6 +13,7 @@
9
13
  :index="index"
10
14
  :item="item"
11
15
  :keyProp="keyProp"
16
+ :maxDepth="maxDepth"
12
17
  @delete="del"
13
18
  @drag-start="dragstart"
14
19
  @touch-move="touchmove"
@@ -16,17 +21,31 @@
16
21
  @drag-end="onDragEnd"
17
22
  >
18
23
  <template v-slot="{ item }">
19
- <slot :item="item">{{ item }}</slot>
24
+ <slot :item="item">{{ item.title || `Item ${item[keyProp]}` }}</slot>
20
25
  </template>
21
26
  </nestable-item>
22
27
  </template>
28
+
29
+
23
30
  </div>
24
- <div v-if="editable" class="d-flex v-end w-full">
31
+
32
+ <div v-if="editable" class="d-flex v-end w-full mt-3">
25
33
  <slot :add="add" :form="form" name="form">
26
34
  <div class="pe-1 flex-grow-1">
27
- <r-text-input v-model="form.title" :label="labelInput"></r-text-input>
35
+ <r-text-input
36
+ ref="inputField"
37
+ v-model="form.title"
38
+ :label="labelInput"
39
+ @keydown.enter="form.title && add()"
40
+ ></r-text-input>
28
41
  </div>
29
- <r-btn :disabled="!form.title" class="color-success" icon @click="add">
42
+ <r-btn
43
+ :aria-label="'Add new item'"
44
+ :disabled="!form.title"
45
+ class="color-success"
46
+ icon
47
+ @click="add"
48
+ >
30
49
  <r-icon v-html="$r.icons.plus"></r-icon>
31
50
  </r-btn>
32
51
  </slot>
@@ -35,19 +54,25 @@
35
54
  </template>
36
55
 
37
56
  <script>
38
- import {defineAsyncComponent} from 'vue'
57
+ import {defineAsyncComponent, nextTick} from 'vue'
39
58
  import methods from "./methods.js";
40
59
  import editable from "./editable.js";
41
60
 
42
61
  export default {
43
62
  name: "r-nestable",
44
63
  components: {
45
- NestableItem:defineAsyncComponent(()=>import('./NestableItem.vue')),
64
+ NestableItem: defineAsyncComponent(() => import('./NestableItem.vue')),
46
65
  },
47
66
  mixins: [methods, editable],
48
67
  props: {
49
- labelInput: String,
50
- editable: Boolean,
68
+ labelInput: {
69
+ type: String,
70
+ default: "New item"
71
+ },
72
+ editable: {
73
+ type: Boolean,
74
+ default: false
75
+ },
51
76
  modelValue: {
52
77
  type: Array,
53
78
  default: () => [],
@@ -74,8 +99,8 @@ export default {
74
99
  itemsOld: null,
75
100
  dragItem: null,
76
101
  mouse: {
77
- last: {x: 0},
78
- shift: {x: 0},
102
+ last: {x: 0, y: 0},
103
+ shift: {x: 0, y: 0},
79
104
  },
80
105
  };
81
106
  },
@@ -86,17 +111,94 @@ export default {
86
111
  beforeUnmount() {
87
112
  this.stopTrackMouse();
88
113
  },
114
+ methods: {
115
+ // Override add method to focus input after adding
116
+ async add() {
117
+ let items = this.modelValue;
118
+ Object.assign(this.form, {
119
+ [this.keyProp]: this.lastId(items) + 1,
120
+ [this.childrenProp]: [],
121
+ })
122
+ items.push(this.form);
123
+ this.$emit("update:model-value", items);
124
+ this.form = {};
125
+
126
+ await nextTick();
127
+ this.$refs.inputField?.focus();
128
+ },
129
+
130
+ // Update mouse move for placeholder positioning
131
+ onMouseMove(event) {
132
+ event && event.preventDefault();
133
+
134
+ const {clientX, clientY} = this.getXandYFromEvent(event);
135
+
136
+ if (this.mouse.last.x === 0) {
137
+ this.mouse.last.x = clientX;
138
+ this.mouse.last.y = clientY;
139
+ }
140
+
141
+ const diffX = this.$r.rtl
142
+ ? this.mouse.last.x - clientX
143
+ : clientX - this.mouse.last.x;
144
+
145
+ if (
146
+ (diffX >= 0 && this.mouse.shift.x >= 0) ||
147
+ (diffX <= 0 && this.mouse.shift.x <= 0)
148
+ ) {
149
+ this.mouse.shift.x += diffX;
150
+ } else {
151
+ this.mouse.shift.x = 0;
152
+ }
153
+ this.mouse.last.x = clientX;
154
+ this.mouse.last.y = clientY;
155
+
156
+ if (Math.abs(this.mouse.shift.x) > this.threshold) {
157
+ if (this.mouse.shift.x > 0) {
158
+ this.tryIncreaseDepth(this.dragItem);
159
+ } else {
160
+ this.tryDecreaseDepth(this.dragItem);
161
+ }
162
+
163
+ this.mouse.shift.x = 0;
164
+ }
165
+ },
166
+
167
+ onDragEnd(event, isCancel = false) {
168
+ event?.preventDefault();
169
+
170
+ this.stopTrackMouse();
171
+ if (isCancel) {
172
+ this.dragRevert();
173
+ } else {
174
+ this.dragApply();
175
+ }
176
+
177
+ this.pathTo = null;
178
+ this.itemsOld = null;
179
+ this.dragItem = null;
180
+ },
181
+ }
89
182
  };
90
183
  </script>
184
+
91
185
  <style lang="scss">
92
186
  @use "../../style/variables/base";
93
187
 
94
188
  .#{base.$prefix}nestable {
189
+ position: relative;
190
+
95
191
  .nestable-handle {
96
192
  cursor: grab;
193
+ transition: transform 0.2s ease, opacity 0.2s ease;
194
+
195
+ &:active {
196
+ cursor: grabbing;
197
+ }
97
198
  }
98
199
 
99
200
  .is-dragging {
201
+ opacity: 0.5;
100
202
  cursor: grabbing !important;
101
203
  }
102
204
 
@@ -106,6 +208,19 @@ export default {
106
208
 
107
209
  .nestable-item-content {
108
210
  display: flex;
211
+ align-items: center;
212
+ padding: 8px 12px;
213
+ transition: background 0.2s ease;
214
+
215
+ &:hover {
216
+ opacity: 0.8;
217
+ }
218
+ }
219
+
220
+
221
+ .nestable-list {
222
+ position: relative;
223
+ transition: all 0.3s ease;
109
224
  }
110
225
  }
111
- </style>
226
+ </style>
@@ -44,7 +44,11 @@ export default {
44
44
  this.startTrackMouse();
45
45
 
46
46
  this.dragItem = item;
47
- this.itemsOld = this.modelValue;
47
+ this.itemsOld = JSON.parse(JSON.stringify(this.modelValue));
48
+
49
+ const {clientX, clientY} = this.getXandYFromEvent(event);
50
+ this.mouse.last = {x: clientX, y: clientY};
51
+ this.mouse.shift = {x: 0, y: 0};
48
52
 
49
53
  this.$nextTick(() => {
50
54
  this.onMouseMove(event);
@@ -119,6 +123,15 @@ export default {
119
123
  },
120
124
 
121
125
  moveItem({dragItem, pathFrom, pathTo}) {
126
+ if (JSON.stringify(pathFrom) === JSON.stringify(pathTo)) {
127
+ return;
128
+ }
129
+
130
+ const newDepth = pathTo.length + this.getItemDepth(dragItem);
131
+ if (newDepth > this.maxDepth) {
132
+ return;
133
+ }
134
+
122
135
  const realPathTo = this.getRealNextPath(pathFrom, pathTo);
123
136
 
124
137
  const removePath = this.getSplicePath(pathFrom, {
@@ -138,6 +151,12 @@ export default {
138
151
 
139
152
  this.pathTo = realPathTo;
140
153
  this.$emit("update:model-value", items);
154
+ this.$emit("change", {
155
+ movedItem: dragItem,
156
+ fromPath: pathFrom,
157
+ toPath: pathTo,
158
+ items
159
+ });
141
160
  },
142
161
  isEquals(x, y) {
143
162
  return x === y;
@@ -1,4 +1,4 @@
1
- import './style.scss'
1
+ import 'renusify/directive/animate/style.scss'
2
2
 
3
3
  function mounted(el, binding) {
4
4
  const modifiers = binding.modifiers || {}
@@ -17,15 +17,18 @@ function mounted(el, binding) {
17
17
  observer
18
18
  ) => {
19
19
  if (!el._observe) return
20
- const isIntersecting = Boolean(entries.find(entry => entry.isIntersecting))
21
-
20
+ if (el._observe.loading) return
21
+ const isIntersecting = entries[0].isIntersecting
22
22
  if (isIntersecting) {
23
+ el._observe.init = true
24
+ el._observe.loading = true
23
25
  setTimeout(() => {
24
26
  if (timing) {
25
27
  el.classList.add(timing)
26
28
  }
27
29
  el.classList.add('du-' + duration * 1000)
28
30
  el.classList.add('animate')
31
+ el._observe.loading = false
29
32
  }, delay)
30
33
 
31
34
  } else if(!modifiers.once) {
@@ -37,12 +40,13 @@ function mounted(el, binding) {
37
40
  if (el._observe.init && modifiers.once) {
38
41
  unmounted(el, binding)
39
42
  }
40
- else (el._observe.init = true)
41
- }, {threshold: 0})
43
+ }, {threshold: value.threshold || 0})
42
44
 
43
- el._observe = {init: false, observer}
45
+ el._observe = {init: false, loading: false, observer}
44
46
 
45
- observer.observe(el)
47
+ setTimeout(() => {
48
+ observer.observe(el)
49
+ }, 100)
46
50
  }
47
51
 
48
52
  function unmounted(el,binding) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "renusify",
3
- "version": "2.4.4",
3
+ "version": "2.5.0",
4
4
  "description": "Vue3 Framework",
5
5
  "keywords": [
6
6
  "vuejs",