1import { Controller } from '@hotwired/stimulus'; 2import Sortable from 'sortablejs'; 3import { patch, post } from '@rails/request.js'; 4import { devWarn } from 'helpers/logger'; 5import { generateUniqueId } from 'helpers/unique_id'; 6import { SortableState } from 'states/sortable_state'; 7 8/** 9 * @domain ë ì´ìì ì ì´ 10 * @concern ëëê·¸ ì¤ ëë¡ì¼ë¡ 리ì¤í¸ ìì ë³ê²½ 11 * @description Sortable.jsë¡ ë¦¬ì¤í¸ í목ì ëëê·¸ ì¤ ëë¡ ì¬ì ë ¬ê³¼ ì¶ê°ë¥¼ ì²ë¦¬íê³ , ë³ê²½ë ìì를 ìë²ì ë¹ë기 ì ì¥íë 컨í¸ë¡¤ë¬ 12 */ 13export default class extends Controller { 14 static values = { 15 updateUrl: String, 16 animation: { default: 0, type: Number }, 17 group: Object, 18 sort: Boolean, 19 disabled: { default: false, type: Boolean }, 20 }; 21 22 static targets = ['draggable', 'handle']; 23 static classes = ['chosen', 'drag', 'ghost', 'bodyDrag']; 24 25 connect() { 26 // ëëê·¸ ì¤ì´ë©´ ì°ê²°ì ì¤ë¨ 27 if (SortableState.dragging) { 28 devWarn('connect skipped due to drag in progress'); 29 return; 30 } 31 32 if (this.sortable) { 33 devWarn('Sortable already initialized, disconnecting first'); 34 return; 35 } 36 37 this.sortable = Sortable.create(this.element, this.getOptions()); 38 39 this.newIdCounter = 0; // ì í목ì ëí ê³ ì ID ì¹´ì´í° ì´ê¸°í 40 41 this.element.addEventListener('dragstart', this.handleDragStart); // ëëê·¸ ìì ì´ë²¤í¸ 리ì¤ë ë±ë¡ 42 43 devWarn('Connecting Sortable:', this.element); 44 } 45 46 disconnect() { 47 // ëëê·¸ ì¤ì´ë©´ ì°ê²°ì ì¤ë¨ 48 if (SortableState.dragging) { 49 devWarn('disconnect skipped due to drag in progress'); 50 return; 51 } 52 53 if (this.sortable) { 54 devWarn('Disconnecting Sortable:', this.element); 55 this.sortable.destroy(); 56 this.sortable = null; 57 } else { 58 devWarn('No Sortable instance to disconnect:', this.element); 59 } 60 61 this.element.removeEventListener('dragstart', this.handleDragStart); 62 } 63 64 getOptions() { 65 const options = { 66 animation: this.animationValue, 67 draggable: "[data-sortable-target='draggable']", 68 handle: "[data-sortable-target='handle']", 69 onUpdate: this.onUpdate.bind(this), 70 onAdd: this.onAdd.bind(this), 71 onStart: this.onStart.bind(this), 72 onEnd: this.onEnd.bind(this), 73 // fallbackOnBody: true ì¤ì ì ëëê·¸ ì¤ ìì±ëë ghost ìì를 <body>ë¡ ì´ëìì¼, 74 // ë ì´ìì ì¶©ë ìì´ ìì ì ì¸ ëëê·¸ í¨ê³¼ë¥¼ ì ê³µí기 ìí ìµì ì ëë¤. 75 // 76 // â ì¤ì²©(nested) 리ì¤í¸ íê²½ììë íìì ì¼ë¡ ì¤ì í´ì¼ íë©°, 77 // ì¤ì²©ë 리ì¤í¸ ê° ëëê·¸ ì ghost ìì¹ ì¤ë¥ë ê¹ë¹¡ì íìì ë°©ì§í©ëë¤. 78 // 79 // â ë¹ì¤ì²©(single-level) 리ì¤í¸ììë ì¬ì© ê°ë¥íë©°, 80 // ëëê·¸ ììê° overflow, z-index ë± ë ì´ìì ì ì½ ìì´ ìì°ì¤ë½ê² ë³´ì´ê² ëìì¤ëë¤. 81 // 82 // â ï¸ ë¨, ghost ììê° bodyì ë¶ê¸° ë문ì 83 // absolute/fixed 컨í ì´ë ë´ë¶ììë ìì¹ ë³´ì ì´ íìí ì ìì¼ë©°, 84 // ì¼ë¶ ë ì´ììììë ììì¹ ëª»í ìì¹ ì¤ë¥ë ì±ë¥ ì´ìê° ë°ìí ì ììµëë¤. 85 // 86 // â¤ ê²°ë¡ ì ì¼ë¡, fallbackOnBodyë ì¤ì²© ì¬ë¶ì ê´ê³ìì´ ì¬ì© ê°ë¥íë, 87 // ë ì´ìì 구조ì ë°ë¼ ëìì ë°ëì í ì¤í¸ í ì¬ì©íë ê²ì´ ì¢ìµëë¤. 88 fallbackOnBody: true, 89 disabled: this.hasDisabledValue ? this.disabledValue : false, 90 forceFallback: false, 91 }; 92 93 if (this.hasGhostClass) options.ghostClass = this.ghostClass; 94 if (this.hasChosenClass) options.chosenClass = this.chosenClass; 95 if (this.hasDragClass) options.dragClass = this.dragClass; 96 if (this.groupValue) options.group = this.groupValue; 97 if (this.hasSortValue) options.sort = this.sortValue; 98 return options; 99 } 100 101 async onUpdate(event) { 102 try { 103 const urlStr = this.updateUrlValue; 104 if (!urlStr) return; 105 106 const url = new URL(urlStr, document.baseURI); 107 const formData = this._buildFormData(); 108 109 await patch(url, { 110 body: formData, 111 responseKind: 'turbo-stream', 112 }); 113 } catch (e) { 114 devWarn(`Update request failed: ${e}`); 115 } 116 } 117 118 async onAdd(event) { 119 try { 120 const item = event.item; 121 const urlStr = item.dataset.sortableAddUrl; 122 if (!urlStr) return; 123 124 if (!item.id) { 125 item.id = generateUniqueId(this.newIdCounter++); 126 } 127 128 // ìë² ì ì¥ ì¤ íì 129 item.dataset.saving = 'true'; 130 131 // ìë² ì ì¥ ì ìë ì ë ¬ ë§ê¸°
132 this.sortable.option('sort', false); 133 134 const url = new URL(urlStr, document.baseURI); 135 const formData = this._buildFormData(); 136 formData.append('item_id', item.id); 137 138 await post(url, { 139 body: formData, 140 responseKind: 'turbo-stream', 141 }); 142 143 // ìë² ì ì¥ ìë£ 144 delete item.dataset.saving; 145 146 // 모ë ì í목 ì ì¥ ìë£ëë©´ ì ë ¬ íì© 147 const hasSaving = this.draggableTargets.some((el) => el.dataset.saving === 'true'); 148 if (!hasSaving) this.sortable.option('sort', true); 149 } catch (e) { 150 devWarn(`Add request failed: ${e}`); 151 152 const item = event.item; 153 if (item && item.parentNode) { 154 item.parentNode.removeChild(item); 155 } 156 alert('í목 ì¶ê°ì ì¤í¨íìµëë¤. ì ì í ë¤ì ìëí´ì£¼ì¸ì.'); 157 158 const hasSaving = this.draggableTargets.some((el) => el.dataset.saving === 'true'); 159 if (!hasSaving) this.sortable.option('sort', true); 160 } 161 } 162 163 // ë¶ëª¨ ID + íì¬ ë¦¬ì¤í¸ ì draggable ID ìì§ì ë´ë¹íë í¬í¼ í¨ì 164 _buildFormData() { 165 const formData = new FormData(); 166 167 // 컨í ì´ë ë´ë¶ draggable ëìì sortableItemId ìì§ 168 for (const element of this.draggableTargets) { 169 const id = element.dataset.sortableItemId; 170 if (!id) { 171 devWarn('Sortable item is missing data-sortable-item-id attribute.'); 172 continue; 173 } 174 formData.append('sortable_ids[]', id); 175 } 176 177 return formData; 178 } 179 180 onStart(event) { 181 const hasSavingItems = this.draggableTargets.some((el) => el.dataset.saving === 'true'); 182 183 if (hasSavingItems) { 184 devWarn('ì ë ¬ ë¶ê°: ì í목 ì¶ê° ì²ë¦¬ ì¤'); 185 alert('ì í목ì ì ì¥ ì¤ì ëë¤. ì ì í ë¤ì ìëí´ì£¼ì¸ì.'); 186 event.preventDefault(); 187 return false; 188 } 189 190 SortableState.dragging = true; 191 192 if (this.hasBodyDragClass) { 193 document.body.classList.add(...this.bodyDragClasses); 194 } 195 } 196 197 onEnd() { 198 SortableState.dragging = false; 199 200 if (this.hasBodyDragClass) { 201 document.body.classList.remove(...this.bodyDragClasses); 202 } 203 } 204 205 handleDragStart = (e) => { 206 const target = e.target.closest("[data-sortable-target='draggable']"); 207 if (!target) return; 208 209 const dt = e.dataTransfer; 210 if (!dt) return; 211 212 const dragImage = this.createDragPreview(); 213 214 dragImage.style.position = 'absolute'; 215 dragImage.style.top = '-9999px'; 216 dragImage.style.left = '-9999px'; 217 dragImage.style.pointerEvents = 'none'; 218 219 document.body.appendChild(dragImage); 220 221 dt.setDragImage(dragImage, 20, 20); 222 223 setTimeout(() => dragImage.remove(), 0); 224 }; 225 226 createDragPreview() { 227 const el = document.createElement('div'); 228 229 el.className = ` 230 w-[140px] h-[40px] 231 flex items-center justify-center gap-2 232 border-2 border-dashed border-slate-300 233 rounded-lg 234 bg-slate-50 235 text-slate-500 text-xs font-medium 236 shadow-md 237 `; 238 239 el.innerHTML = ` 240 <svg class="w-4 h-4 text-slate-400" viewBox="0 0 20 20" fill="currentColor"> 241 <circle cx="6" cy="6" r="1.5"/> 242 <circle cx="6" cy="10" r="1.5"/> 243 <circle cx="6" cy="14" r="1.5"/> 244 <circle cx="10" cy="6" r="1.5"/> 245 <circle cx="10" cy="10" r="1.5"/> 246 <circle cx="10" cy="14" r="1.5"/> 247 </svg> 248 <span>Block</span> 249 `; 250 251 return el; 252 } 253}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.