forked from MapComplete/MapComplete
		
	Looking at https://github.com/dcastil/tailwind-merge/blob/v1.13.1/docs/when-and-how-to-use-it.md#how-to-use-it I placed the "winning" class at the end for twMerge
		
			
				
	
	
		
			152 lines
		
	
	
	
		
			3.8 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
			
		
		
	
	
			152 lines
		
	
	
	
		
			3.8 KiB
		
	
	
	
		
			Svelte
		
	
	
	
	
	
| <script lang="ts">
 | |
|   import { twJoin } from "tailwind-merge"
 | |
|   import { Store, Stores, UIEventSource } from "../../../Logic/UIEventSource"
 | |
| 
 | |
|   /**
 | |
|    * Given the available floors, shows an elevator to pick a single one
 | |
|    *
 | |
|    * This is but the input element, the logic of handling the filter is in 'LevelSelector'
 | |
|    */
 | |
|   export let floors: Store<string[]>
 | |
|   export let value: UIEventSource<string>
 | |
| 
 | |
|   const HEIGHT = 40
 | |
| 
 | |
|   let initialIndex = Math.max(0, floors?.data?.findIndex((f) => f === value?.data) ?? 0)
 | |
|   let index: UIEventSource<number> = new UIEventSource<number>(initialIndex)
 | |
|   let forceIndex: number | undefined = undefined
 | |
|   let top = Math.max(0, initialIndex) * HEIGHT
 | |
|   let elevator: HTMLImageElement
 | |
| 
 | |
|   let mouseDown = false
 | |
| 
 | |
|   let container: HTMLElement
 | |
| 
 | |
|   $: {
 | |
|     if (top > 0 || forceIndex !== undefined) {
 | |
|       index.setData(closestFloorIndex())
 | |
|       value.setData(floors.data[forceIndex ?? closestFloorIndex()])
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function unclick() {
 | |
|     mouseDown = false
 | |
|   }
 | |
| 
 | |
|   function click() {
 | |
|     mouseDown = true
 | |
|   }
 | |
| 
 | |
|   function closestFloorIndex() {
 | |
|     return Math.min(floors.data.length - 1, Math.max(0, Math.round(top / HEIGHT)))
 | |
|   }
 | |
| 
 | |
|   function onMove(e: { movementY: number }) {
 | |
|     if (mouseDown) {
 | |
|       forceIndex = undefined
 | |
|       const containerY = container.clientTop
 | |
|       const containerMax = containerY + (floors.data.length - 1) * HEIGHT
 | |
|       top = Math.min(Math.max(0, top + e.movementY), containerMax)
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let momentum = 0
 | |
| 
 | |
|   function stabilize() {
 | |
|     // Automatically move the elevator to the closes floor
 | |
|     if (mouseDown) {
 | |
|       return
 | |
|     }
 | |
|     const target = (forceIndex ?? index.data) * HEIGHT
 | |
|     let diff = target - top
 | |
|     if (diff > 1) {
 | |
|       diff /= 3
 | |
|     }
 | |
|     const sign = Math.sign(diff)
 | |
|     momentum = momentum + sign
 | |
|     let diffR = Math.min(Math.abs(momentum), forceIndex !== undefined ? 9 : 3, Math.abs(diff))
 | |
|     momentum = Math.sign(momentum) * Math.min(diffR, Math.abs(momentum))
 | |
|     top += sign * diffR
 | |
|     if (index.data === forceIndex) {
 | |
|       forceIndex = undefined
 | |
|     }
 | |
|     top = Math.max(top, 0)
 | |
|   }
 | |
| 
 | |
|   Stores.Chronic(50).addCallback((_) => stabilize())
 | |
|   floors.addCallback((floors) => {
 | |
|     forceIndex = floors.findIndex((s) => s === value.data)
 | |
|   })
 | |
| 
 | |
|   let image: HTMLImageElement
 | |
|   $: {
 | |
|     if (image) {
 | |
|       let lastY = 0
 | |
|       image.ontouchstart = (e: TouchEvent) => {
 | |
|         mouseDown = true
 | |
|         lastY = e.changedTouches[0].clientY
 | |
|       }
 | |
|       image.ontouchmove = (e) => {
 | |
|         const y = e.changedTouches[0].clientY
 | |
|         console.log(y)
 | |
|         const movementY = y - lastY
 | |
|         lastY = y
 | |
|         onMove({ movementY })
 | |
|       }
 | |
|       image.ontouchend = unclick
 | |
|     }
 | |
|   }
 | |
| </script>
 | |
| 
 | |
| <div
 | |
|   bind:this={container}
 | |
|   class="relative"
 | |
|   style={`height: calc(${HEIGHT}px * ${$floors.length}); width: 96px`}
 | |
| >
 | |
|   <div class="absolute right-0 h-full w-min">
 | |
|     {#each $floors as floor, i}
 | |
|       <button
 | |
|         style={`height: ${HEIGHT}px; width: ${HEIGHT}px`}
 | |
|         class={twJoin(
 | |
|           "content-box m-0 flex items-center justify-center border-2 border-gray-300",
 | |
|           i === (forceIndex ?? $index) && "selected"
 | |
|         )}
 | |
|         on:click={() => {
 | |
|           forceIndex = i
 | |
|         }}
 | |
|       >
 | |
|         {floor}
 | |
|       </button>
 | |
|     {/each}
 | |
|   </div>
 | |
| 
 | |
|   <div style={`width: ${HEIGHT}px`}>
 | |
|     <img
 | |
|       bind:this={image}
 | |
|       class="draggable"
 | |
|       draggable="false"
 | |
|       on:mousedown={click}
 | |
|       src="./assets/svg/elevator.svg"
 | |
|       style={`top: ${top}px;`} />
 | |
|   </div>
 | |
| </div>
 | |
| 
 | |
| <svelte:window on:mousemove={onMove} on:mouseup={unclick} />
 | |
| 
 | |
| <style>
 | |
|   .draggable {
 | |
|     user-select: none;
 | |
|     cursor: move;
 | |
|     position: absolute;
 | |
|     user-drag: none;
 | |
| 
 | |
|     height: 72px;
 | |
|     margin-top: -15px;
 | |
|     margin-bottom: -15px;
 | |
|     margin-left: -18px;
 | |
|     -webkit-user-drag: none;
 | |
|     -moz-user-select: none;
 | |
|     -webkit-user-select: none;
 | |
|     -ms-user-select: none;
 | |
|   }
 | |
| </style>
 |