diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-22 17:52:48 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-22 17:58:09 +0800 |
| commit | 0a1d70dd6017164e13386997b7b03eab47045e0a (patch) | |
| tree | 289214c209ca8a399139b75f70934485079acd89 | |
| parent | 516aab9de3d6ad5b52da75666ce9f6531c913be6 (diff) | |
feat: animation for creating draggable block
* rename class name: draggable -> draggable-part
* code refactor: separate logic about working with plain-draggable
* get original rect before creating blocks
| -rw-r--r-- | src/Layout.mjs | 172 | ||||
| -rw-r--r-- | src/css/dumbymap.css | 6 |
2 files changed, 100 insertions, 78 deletions
diff --git a/src/Layout.mjs b/src/Layout.mjs index 2deef01..8a9f316 100644 --- a/src/Layout.mjs +++ b/src/Layout.mjs | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | import PlainDraggable from 'plain-draggable' | 1 | import PlainDraggable from 'plain-draggable' |
| 2 | import { onRemove } from './utils' | 2 | import { onRemove, animateRectTransition } from './utils' |
| 3 | 3 | ||
| 4 | export class Layout { | 4 | export class Layout { |
| 5 | constructor(options = {}) { | 5 | constructor(options = {}) { |
| @@ -32,6 +32,7 @@ export class SideBySide extends Layout { | |||
| 32 | containment: { left: '25%', top: 0, right: '75%', height: 0 }, | 32 | containment: { left: '25%', top: 0, right: '75%', height: 0 }, |
| 33 | }) | 33 | }) |
| 34 | draggable.draggableCursor = "grab" | 34 | draggable.draggableCursor = "grab" |
| 35 | |||
| 35 | draggable.onDrag = (pos) => { | 36 | draggable.onDrag = (pos) => { |
| 36 | handle.style.transform = 'unset' | 37 | handle.style.transform = 'unset' |
| 37 | resizeByLeft(pos.left) | 38 | resizeByLeft(pos.left) |
| @@ -51,16 +52,82 @@ export class SideBySide extends Layout { | |||
| 51 | export class Overlay extends Layout { | 52 | export class Overlay extends Layout { |
| 52 | name = "overlay" | 53 | name = "overlay" |
| 53 | 54 | ||
| 54 | enterHandler = (dumbymap) => { | 55 | saveLeftTopAsData = (element) => { |
| 55 | const container = dumbymap.htmlHolder | 56 | const { left, top } = element.getBoundingClientRect() |
| 56 | const moveIntoDraggable = (block) => { | 57 | element.setAttribute('data-left', left) |
| 58 | element.setAttribute('data-top', top) | ||
| 59 | } | ||
| 60 | |||
| 61 | addDraggable = (element) => { | ||
| 62 | // Add draggable part | ||
| 63 | const draggablePart = document.createElement('div') | ||
| 64 | element.appendChild(draggablePart) | ||
| 65 | draggablePart.className = 'draggable-part' | ||
| 66 | draggablePart.innerHTML = '<div class="handle">\u2630</div>' | ||
| 67 | draggablePart.title = 'Use middle-click to remove block' | ||
| 68 | draggablePart.onmouseup = (e) => { | ||
| 69 | // Hide block with middle click | ||
| 70 | if (e.button === 1) { | ||
| 71 | element.classList.add('hide') | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | // Add draggable instance | ||
| 76 | const { left, top } = element.getBoundingClientRect() | ||
| 77 | const draggable = new PlainDraggable(element, { | ||
| 78 | top: top, | ||
| 79 | left: left, | ||
| 80 | handle: draggablePart, | ||
| 81 | snap: { x: { step: 20 }, y: { step: 20 } }, | ||
| 82 | }) | ||
| 83 | |||
| 84 | // FIXME use pure CSS to hide utils | ||
| 85 | const utils = element.querySelector('.utils') | ||
| 86 | const siblings = Array.from(element.parentElement.querySelectorAll(':scope > *')) | ||
| 87 | draggable.onDragStart = () => { | ||
| 88 | utils.style.opacity = 0 | ||
| 89 | element.classList.add('drag') | ||
| 90 | // Remove z-index from previous dragged | ||
| 91 | siblings.forEach(e => e.style.removeProperty('z-index')) | ||
| 92 | } | ||
| 93 | |||
| 94 | draggable.onDragEnd = () => { | ||
| 95 | utils.style = '' | ||
| 96 | element.classList.remove('drag') | ||
| 97 | // Ensuer last dragged block always on top | ||
| 98 | element.style.zIndex = '9000' | ||
| 99 | } | ||
| 100 | |||
| 101 | // Reposition draggable instance when resized | ||
| 102 | new ResizeObserver(() => { | ||
| 103 | try { | ||
| 104 | draggable.position(); | ||
| 105 | } catch (_) { | ||
| 106 | null | ||
| 107 | } | ||
| 108 | }).observe(element); | ||
| 109 | |||
| 110 | // Callback for remove | ||
| 111 | onRemove(element, () => { | ||
| 112 | draggable.remove() | ||
| 113 | }) | ||
| 114 | } | ||
| 115 | |||
| 116 | enterHandler = ({ htmlHolder, blocks }) => { | ||
| 117 | |||
| 118 | // FIXME It is weird rect from this method and this scope are different... | ||
| 119 | blocks.forEach(this.saveLeftTopAsData) | ||
| 120 | |||
| 121 | // Create draggable blocks and set each position by previous one | ||
| 122 | let [left, top] = [20, 20] | ||
| 123 | blocks.forEach(block => { | ||
| 124 | const originLeft = Number(block.getAttribute('data-left')) | ||
| 125 | const originTop = Number(block.getAttribute('data-top')) | ||
| 126 | |||
| 57 | // Create draggable block | 127 | // Create draggable block |
| 58 | const draggableBlock = document.createElement('div') | 128 | const wrapper = document.createElement('div') |
| 59 | draggableBlock.classList.add('draggable-block') | 129 | wrapper.classList.add('draggable-block') |
| 60 | draggableBlock.innerHTML = ` | 130 | wrapper.innerHTML = ` |
| 61 | <div class="draggable"> | ||
| 62 | <div class="handle">\u2630</div> | ||
| 63 | </div> | ||
| 64 | <div class="utils"> | 131 | <div class="utils"> |
| 65 | <div id="close">\u274C</div> | 132 | <div id="close">\u274C</div> |
| 66 | <div id="plus-font-size" ">\u2795</div> | 133 | <div id="plus-font-size" ">\u2795</div> |
| @@ -68,82 +135,39 @@ export class Overlay extends Layout { | |||
| 68 | </div> | 135 | </div> |
| 69 | ` | 136 | ` |
| 70 | 137 | ||
| 71 | // Add draggable part | 138 | wrapper.appendChild(block) |
| 72 | const draggablePart = draggableBlock.querySelector('.draggable') | 139 | htmlHolder.appendChild(wrapper) |
| 73 | draggablePart.title = 'Use middle-click to remove block' | 140 | wrapper.style.left = left + "px" |
| 74 | draggablePart.onmouseup = (e) => { | 141 | wrapper.style.top = top + "px" |
| 75 | if (e.button === 1) { | 142 | const { width } = wrapper.getBoundingClientRect() |
| 76 | // Hide block with middle click | 143 | left += width + 30 |
| 77 | draggableBlock.setAttribute("data-state", "hide") | 144 | if (left > window.innerWidth) { |
| 78 | } | 145 | top += 200 |
| 146 | left = left % window.innerWidth | ||
| 79 | } | 147 | } |
| 80 | 148 | ||
| 81 | // Set elements | 149 | animateRectTransition( |
| 82 | draggableBlock.appendChild(draggablePart) | 150 | wrapper, |
| 83 | draggableBlock.appendChild(block) | 151 | { left: originLeft, top: originTop }, |
| 84 | container.appendChild(draggableBlock) | 152 | { resume: true, duration: 500 } |
| 85 | 153 | ) | |
| 86 | // Add draggable instance | 154 | .finished |
| 87 | const draggableInstance = new PlainDraggable(draggableBlock, { | 155 | .finally(() => this.addDraggable(wrapper)) |
| 88 | handle: draggablePart, | ||
| 89 | snap: { x: { step: 20 }, y: { step: 20 } }, | ||
| 90 | }) | ||
| 91 | 156 | ||
| 92 | // Close button | 157 | // Close button |
| 93 | draggableBlock.querySelector('#close').onclick = () => { | 158 | wrapper.querySelector('#close').onclick = () => { |
| 94 | draggableBlock.classList.add('hide') | 159 | wrapper.classList.add('hide') |
| 95 | } | 160 | } |
| 96 | // Plus/Minus font-size of content | 161 | // Plus/Minus font-size of content |
| 97 | draggableBlock.querySelector('#plus-font-size').onclick = () => { | 162 | wrapper.querySelector('#plus-font-size').onclick = () => { |
| 98 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 | 163 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 |
| 99 | block.style.fontSize = `${fontSize + 0.1}rem` | 164 | block.style.fontSize = `${fontSize + 0.1}rem` |
| 100 | } | 165 | } |
| 101 | draggableBlock.querySelector('#minus-font-size').onclick = () => { | 166 | wrapper.querySelector('#minus-font-size').onclick = () => { |
| 102 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 | 167 | const fontSize = parseFloat(getComputedStyle(block).fontSize) / 16 |
| 103 | block.style.fontSize = `${fontSize - 0.1}rem` | 168 | block.style.fontSize = `${fontSize - 0.1}rem` |
| 104 | } | 169 | } |
| 105 | 170 | }) | |
| 106 | // FIXME use pure CSS to hide utils | ||
| 107 | const utils = draggableBlock.querySelector('.utils') | ||
| 108 | draggableInstance.onDragStart = () => { | ||
| 109 | utils.style.opacity = 0 | ||
| 110 | draggableBlock.classList.add('drag') | ||
| 111 | } | ||
| 112 | draggableInstance.onDragEnd = () => { | ||
| 113 | utils.style = '' | ||
| 114 | draggableBlock.classList.remove('drag') | ||
| 115 | } | ||
| 116 | |||
| 117 | // Reposition draggable instance when resized | ||
| 118 | new ResizeObserver(() => { | ||
| 119 | try { | ||
| 120 | draggableInstance.position(); | ||
| 121 | } catch (_) { | ||
| 122 | null | ||
| 123 | } | ||
| 124 | }).observe(draggableBlock); | ||
| 125 | |||
| 126 | // Callback for remove | ||
| 127 | onRemove(draggableBlock, () => { | ||
| 128 | draggableInstance.remove() | ||
| 129 | }) | ||
| 130 | |||
| 131 | return draggableInstance | ||
| 132 | } | ||
| 133 | |||
| 134 | // Create draggable blocks and set each position by previous one | ||
| 135 | let [x, y] = [0, 0] | ||
| 136 | dumbymap.blocks.map(moveIntoDraggable) | ||
| 137 | .forEach(draggable => { | ||
| 138 | draggable.left = x | ||
| 139 | draggable.top = y | ||
| 140 | const rect = draggable.element.getBoundingClientRect() | ||
| 141 | x += rect.width + 30 | ||
| 142 | if (x > window.innerWidth) { | ||
| 143 | y += 200 | ||
| 144 | x = x % window.innerWidth | ||
| 145 | } | ||
| 146 | }) | ||
| 147 | } | 171 | } |
| 148 | 172 | ||
| 149 | leaveHandler = ({ htmlHolder, blocks }) => { | 173 | leaveHandler = ({ htmlHolder, blocks }) => { |
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index e04b6bc..cb692ae 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
| @@ -262,8 +262,6 @@ | |||
| 262 | font-size: 0.8rem; | 262 | font-size: 0.8rem; |
| 263 | pointer-events: auto; | 263 | pointer-events: auto; |
| 264 | 264 | ||
| 265 | transition: visibility 1s; | ||
| 266 | |||
| 267 | .dumby-block { | 265 | .dumby-block { |
| 268 | overflow: auto; | 266 | overflow: auto; |
| 269 | margin: 0; | 267 | margin: 0; |
| @@ -305,7 +303,7 @@ | |||
| 305 | } | 303 | } |
| 306 | } | 304 | } |
| 307 | 305 | ||
| 308 | .draggable { | 306 | .draggable-part { |
| 309 | display: block; | 307 | display: block; |
| 310 | overflow: clip; | 308 | overflow: clip; |
| 311 | width: 100%; | 309 | width: 100%; |
| @@ -337,7 +335,7 @@ | |||
| 337 | visibility: hidden; | 335 | visibility: hidden; |
| 338 | } | 336 | } |
| 339 | 337 | ||
| 340 | &:has(.draggable:hover, .utils:hover), | 338 | &:has(.draggable-part:hover, .utils:hover), |
| 341 | &.drag { | 339 | &.drag { |
| 342 | .dumby-block { | 340 | .dumby-block { |
| 343 | color: gray; | 341 | color: gray; |