github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/static_source/admin/src/views/Dashboard/editor/ViewCard.vue (about) 1 <script setup lang="ts"> 2 import {computed, onMounted, onUnmounted, onUpdated, PropType, ref,} from "vue"; 3 import {Card, CardItem, Core, EventContextMenu, eventBus} from "@/views/Dashboard/core"; 4 import debounce from 'lodash.debounce' 5 import Moveable from 'vue3-moveable' 6 import {deepFlat} from "@daybrush/utils"; 7 import {VueSelecto} from "vue3-selecto"; 8 import {CardItemName} from "@/views/Dashboard/card_items"; 9 import {UUID} from "uuid-generator-ts"; 10 import {KeystrokeCaptureViewer} from "@/views/Dashboard/components"; 11 import {useAppStore} from "@/store/modules/app"; 12 13 const appStore = useAppStore() 14 15 const currentID = ref('') 16 const cardRef = ref(null) 17 18 const eventHandler = (event: string, args: any[]) => { 19 switch (event) { 20 case 'selectedCardItem': 21 if (!currentCard.value.active) { 22 return 23 } 24 const itemIndex = args 25 if (itemIndex === -1 || !currentCard.value.items.length || !currentCard.value.itemList[itemIndex]) { 26 setSelectedTargets([]) 27 return 28 } 29 const target = currentCard.value.itemList[itemIndex]; 30 // target.classList.add("selected"); 31 setSelectedTargets([target]); 32 break; 33 case 'unselectedCardItem': 34 if (currentCard.value.active) { 35 return 36 } 37 setSelectedTargets([]); 38 break; 39 } 40 } 41 42 onMounted(() => { 43 const uuid = new UUID() 44 currentID.value = uuid.getDashFreeUUID() 45 46 currentCard.value.document = cardRef.value 47 currentCard.value.updateItemList() 48 49 eventBus.subscribe(['selectedCardItem', 'unselectedCardItem'], eventHandler) 50 }) 51 52 onUnmounted(() => { 53 eventBus.unsubscribe(['selectedCardItem', 'unselectedCardItem'], eventHandler) 54 }) 55 56 onUpdated(() => { 57 58 }) 59 60 // --------------------------------- 61 // common 62 // --------------------------------- 63 64 const zoom = ref(1); 65 66 67 const props = defineProps({ 68 core: { 69 type: Object as PropType<Core>, 70 }, 71 card: { 72 type: Object as PropType<Nullable<Card>>, 73 default: () => null 74 }, 75 }) 76 77 const reloadKey = ref(0); 78 const reload = debounce(() => { 79 reloadKey.value += 1 80 }, 100) 81 82 83 const currentCard = computed({ 84 get(): Card { 85 return props.card as Card 86 }, 87 set(val: Card) { 88 } 89 }) 90 91 const hover = ref(false) 92 93 // --------------------------------- 94 // component methods 95 // --------------------------------- 96 const getCardItemName = (item: CardItem): string => { 97 //todo: check if item disabled 98 return CardItemName(item.type); 99 } 100 101 const selectCardItem = (itemIndex: number) => { 102 if (!currentCard.value.active) { 103 props.core?.onSelectedCard(currentCard.value.id) 104 eventBus.emit('selectedCard', currentCard.value.id) 105 } 106 107 currentCard.value.selectedItem = itemIndex; 108 // if (itemIndex === -1 || !currentCard.value.items.length || !currentCard.value.items[itemIndex]) { 109 // targets.value = []; 110 // } else { 111 // targets.value = [currentCard.value.items[itemIndex].target]; 112 // } 113 // eventBus.emit('unselectedCardItem') 114 } 115 116 const onDrag = ({target, transform, beforeTranslate, left, top}: any) => { 117 const classes = target.className.split(' '); 118 target.style.transform = transform; 119 for (const cl of classes) { 120 if (cl.includes('item-index-')) { 121 const index = parseInt(cl.replace("item-index-", "")) 122 currentCard.value.items[index].transform = transform; 123 } 124 } 125 } 126 127 const setSelectedTargets = (target) => { 128 selectoRef.value.setSelectedTargets(deepFlat(target)); 129 targets.value = target; 130 }; 131 132 const onResize = ({target, width, height, clientX, clientY}: any) => { 133 width = Math.round(width); 134 height = Math.round(height); 135 136 if (currentCard.value.selectedItem > -1) { 137 currentCard.value.items[currentCard.value.selectedItem].width = width; 138 currentCard.value.items[currentCard.value.selectedItem].height = height; 139 } 140 target.style.width = `${width}px`; 141 target.style.height = `${height}px`; 142 } 143 144 const onRotate = ({target, transform, beforeRotate, clientX, clientY}: any) => { 145 if (currentCard.value.selectedItem > -1) { 146 currentCard.value.items[currentCard.value.selectedItem].transform = transform; 147 } 148 target.style.transform = transform; 149 } 150 151 const targets = ref([]); 152 const moveableRef = ref(null); 153 154 const onSnap = e => { 155 // console.log(e.guidelines, e.elements); 156 }; 157 158 // --------------------------------- 159 // selecto methods 160 // --------------------------------- 161 162 const onDragGroup = ({events}) => { 163 events.forEach(ev => { 164 const classes = ev.target.className.split(' '); 165 ev.target.style.transform = ev.transform; 166 for (const cl of classes) { 167 if (cl.includes('item-index-')) { 168 const index = parseInt(cl.replace("item-index-", "")) 169 currentCard.value.items[index].transform = ev.transform; 170 } 171 } 172 }); 173 }; 174 175 const onRenderGroup = e => { 176 e.events.forEach(ev => { 177 ev.target.style.transform = ev.transform; 178 }); 179 }; 180 181 const onClickGroup = e => { 182 if (!e.moveableTarget) { 183 setSelectedTargets([]); 184 selectCardItem(-1); 185 return; 186 } 187 if (e.isTrusted) { 188 selectoRef.value.clickTarget(e.inputEvent, e.moveableTarget); 189 } 190 }; 191 192 // --------------------------------- 193 // group methods 194 // --------------------------------- 195 196 //todo add group 197 // https://daybrush.com/moveable/storybook/index.html?path=/story/combination-with-other-components--components-selecto-with-multiple-group 198 // https://daybrush.com/moveable/storybook/index.html?path=/story/combination-with-other-components--components-selecto 199 200 const selectoRef = ref(null); 201 202 const onSelect = (e) => { 203 e.added.forEach(el => { 204 el.classList.add("selected"); 205 }); 206 e.removed.forEach(el => { 207 el.classList.remove("selected"); 208 }); 209 } 210 211 const onSelectEnd = (e) => { 212 const { 213 isDragStartEnd, 214 isClick, 215 added, 216 removed, 217 inputEvent, 218 selected, 219 } = e; 220 const moveable = moveableRef.value; 221 if (e.isDragStart) { 222 e.inputEvent.preventDefault(); 223 moveable.waitToChangeTarget().then(() => { 224 // moveable.dragStart(e.inputEvent); 225 }); 226 } 227 targets.value = selected; 228 if (selected && selected.length == 1) { 229 const classes = selected[0].className.split(' '); 230 for (const cl of classes) { 231 if (cl.includes('item-index-')) { 232 const index = parseInt(cl.replace("item-index-", "")) 233 selectCardItem(index) 234 } 235 } 236 } 237 238 if (selected && selected.length == 0) { 239 selectCardItem(-1) 240 } 241 }; 242 243 const onDragStart = (e) => { 244 const moveable = moveableRef.value; 245 const target = e.inputEvent.target; 246 const flatted = targets.value.flat(3); 247 if (moveable.isMoveableElement(target) 248 || flatted.some(t => t === target || t && t.contains(target)) 249 ) { 250 e.stop(); 251 } 252 }; 253 254 const getCardStyle = () => { 255 const style = { 256 transform: `scale(${zoom.value})`, 257 } 258 if (currentCard.value?.template) { 259 style['background-color'] = 'inherit' 260 } else { 261 if (currentCard.value?.background) { 262 style['background-color'] = currentCard.value.background 263 } else { 264 if (currentCard.value?.backgroundAdaptive) { 265 style['background-color'] = appStore.isDark ? '#232324' : '#F5F7FA' 266 } 267 } 268 } 269 270 return style 271 } 272 273 const onContextMenu = (e: MouseEvent, owner: 'card' | 'cardItem', cardItemId?: number) => { 274 e.preventDefault(); 275 e.stopPropagation(); 276 eventBus.emit('eventContextMenu', { 277 event: e, 278 owner: owner, 279 tabId: currentCard.value.dashboardTabId, 280 cardId: currentCard.value.id, 281 cardItemId: cardItemId, 282 } as EventContextMenu) 283 } 284 285 defineOptions({ 286 inheritAttrs: false 287 }) 288 </script> 289 290 <template> 291 292 <div 293 class="item-card elements selecto-area prevent-select" 294 ref="cardRef" 295 :class="[{'active': currentCard.active}, `class-${currentCard.currentID}`]" 296 :key="reloadKey" 297 :style="getCardStyle()" 298 @mouseover="hover = true" 299 @touchstart="hover = true" 300 @mouseleave="hover = false" 301 @mouseout="hover = false" 302 @contextmenu="onContextMenu($event, 'card')" 303 > 304 <div class="card-label">active</div> 305 306 <KeystrokeCaptureViewer :card="currentCard" :core="core" :hover="hover"/> 307 308 <component 309 v-for="(item, index) in currentCard.items" 310 :key="index" 311 class="movable" 312 :style="item.position" 313 v-bind:class="['item-index-'+index, 'item-id-'+item.id]" 314 :is="getCardItemName(item)" 315 :item="item" 316 :core="core" 317 :editor="true" 318 @contextmenu="onContextMenu($event, 'cardItem', item.id)" 319 /> 320 321 <Moveable 322 ref="moveableRef" 323 :target="targets" 324 @drag="onDrag" 325 @dragGroup="onDragGroup" 326 @renderGroup="onRenderGroup" 327 @clickGroup="onClickGroup" 328 @resize="onResize" 329 @rotate="onRotate" 330 @onSnap="onSnap" 331 v-bind="currentCard.settings()" 332 /> 333 334 <VueSelecto 335 ref="selectoRef" 336 :rootContainer="cardRef" 337 :selectableTargets="['.class-'+currentCard.currentID+' .movable']" 338 :hitRate="0" 339 :selectByClick="true" 340 :selectFromInside="false" 341 :toggleContinueSelect="['shift']" 342 :ratio="0" 343 @dragStart="onDragStart" 344 @selectEnd="onSelectEnd" 345 @select="onSelect" 346 /> 347 </div> 348 349 </template> 350 351 <style lang="less"> 352 .movable { 353 position: absolute; 354 } 355 356 .card-label { 357 display: none; 358 position: absolute; 359 top: 18px; 360 right: -17px; 361 width: 55px; 362 height: 20px; 363 background: #4af; 364 padding: 0 6px; 365 transform: rotate(90deg); 366 z-index: 9999; 367 opacity: 0.5; 368 color: #eeeeee; 369 } 370 371 .item-card { 372 position: relative; 373 overflow: hidden; 374 width: 100%; 375 height: 100%; 376 377 &.active { 378 .card-label { 379 display: inherit; 380 } 381 } 382 } 383 </style>