react-native-layout-dnd 0.1.0-alpha.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.
Files changed (340) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +262 -0
  3. package/lib/module/adapters/coordinates.js +135 -0
  4. package/lib/module/adapters/coordinates.js.map +1 -0
  5. package/lib/module/adapters/scroll.js +81 -0
  6. package/lib/module/adapters/scroll.js.map +1 -0
  7. package/lib/module/adapters/virtualizedList.js +69 -0
  8. package/lib/module/adapters/virtualizedList.js.map +1 -0
  9. package/lib/module/components/DndProvider.js +1144 -0
  10. package/lib/module/components/DndProvider.js.map +1 -0
  11. package/lib/module/components/DragHandle.js +65 -0
  12. package/lib/module/components/DragHandle.js.map +1 -0
  13. package/lib/module/components/GridController.js +151 -0
  14. package/lib/module/components/GridController.js.map +1 -0
  15. package/lib/module/components/GridDragHandle.js +15 -0
  16. package/lib/module/components/GridDragHandle.js.map +1 -0
  17. package/lib/module/components/GridZoneView.js +59 -0
  18. package/lib/module/components/GridZoneView.js.map +1 -0
  19. package/lib/module/components/SortableGrid.js +305 -0
  20. package/lib/module/components/SortableGrid.js.map +1 -0
  21. package/lib/module/components/SortableList.js +714 -0
  22. package/lib/module/components/SortableList.js.map +1 -0
  23. package/lib/module/components/dndTypes.js +4 -0
  24. package/lib/module/components/dndTypes.js.map +1 -0
  25. package/lib/module/components/gridTypes.js +4 -0
  26. package/lib/module/components/gridTypes.js.map +1 -0
  27. package/lib/module/components/handleViewProps.js +13 -0
  28. package/lib/module/components/handleViewProps.js.map +1 -0
  29. package/lib/module/contracts.js +4 -0
  30. package/lib/module/contracts.js.map +1 -0
  31. package/lib/module/controller/approval.js +98 -0
  32. package/lib/module/controller/approval.js.map +1 -0
  33. package/lib/module/controller/coordinatorBase.js +93 -0
  34. package/lib/module/controller/coordinatorBase.js.map +1 -0
  35. package/lib/module/controller/dndSession.js +621 -0
  36. package/lib/module/controller/dndSession.js.map +1 -0
  37. package/lib/module/controller/dndState.js +103 -0
  38. package/lib/module/controller/dndState.js.map +1 -0
  39. package/lib/module/controller/gridAdapter.js +191 -0
  40. package/lib/module/controller/gridAdapter.js.map +1 -0
  41. package/lib/module/controller/gridState.js +101 -0
  42. package/lib/module/controller/gridState.js.map +1 -0
  43. package/lib/module/controller/locations.js +48 -0
  44. package/lib/module/controller/locations.js.map +1 -0
  45. package/lib/module/controller/previewPath.js +73 -0
  46. package/lib/module/controller/previewPath.js.map +1 -0
  47. package/lib/module/controller/sessionCore.js +222 -0
  48. package/lib/module/controller/sessionCore.js.map +1 -0
  49. package/lib/module/controller/useDndState.js +9 -0
  50. package/lib/module/controller/useDndState.js.map +1 -0
  51. package/lib/module/controller/useGridState.js +9 -0
  52. package/lib/module/controller/useGridState.js.map +1 -0
  53. package/lib/module/controller/useStoreState.js +15 -0
  54. package/lib/module/controller/useStoreState.js.map +1 -0
  55. package/lib/module/defaults.js +7 -0
  56. package/lib/module/defaults.js.map +1 -0
  57. package/lib/module/diagnostics.js +16 -0
  58. package/lib/module/diagnostics.js.map +1 -0
  59. package/lib/module/engine/gridPlacements.js +54 -0
  60. package/lib/module/engine/gridPlacements.js.map +1 -0
  61. package/lib/module/engine/index.js +19 -0
  62. package/lib/module/engine/index.js.map +1 -0
  63. package/lib/module/engine/layout.js +36 -0
  64. package/lib/module/engine/layout.js.map +1 -0
  65. package/lib/module/engine/layoutMove.js +326 -0
  66. package/lib/module/engine/layoutMove.js.map +1 -0
  67. package/lib/module/engine/layoutState.js +344 -0
  68. package/lib/module/engine/layoutState.js.map +1 -0
  69. package/lib/module/engine/list.js +172 -0
  70. package/lib/module/engine/list.js.map +1 -0
  71. package/lib/module/engine/listIndex.js +125 -0
  72. package/lib/module/engine/listIndex.js.map +1 -0
  73. package/lib/module/engine/move.js +216 -0
  74. package/lib/module/engine/move.js.map +1 -0
  75. package/lib/module/engine/movementPolicy.js +77 -0
  76. package/lib/module/engine/movementPolicy.js.map +1 -0
  77. package/lib/module/engine/orderedTarget.js +71 -0
  78. package/lib/module/engine/orderedTarget.js.map +1 -0
  79. package/lib/module/engine/pack.js +30 -0
  80. package/lib/module/engine/pack.js.map +1 -0
  81. package/lib/module/engine/rectangles.js +12 -0
  82. package/lib/module/engine/rectangles.js.map +1 -0
  83. package/lib/module/engine/sameLayout.js +29 -0
  84. package/lib/module/engine/sameLayout.js.map +1 -0
  85. package/lib/module/engine/search.js +159 -0
  86. package/lib/module/engine/search.js.map +1 -0
  87. package/lib/module/engine/spatial.js +27 -0
  88. package/lib/module/engine/spatial.js.map +1 -0
  89. package/lib/module/engine/spatialAppRotation.js +39 -0
  90. package/lib/module/engine/spatialAppRotation.js.map +1 -0
  91. package/lib/module/engine/spatialExchange.js +164 -0
  92. package/lib/module/engine/spatialExchange.js.map +1 -0
  93. package/lib/module/engine/spatialInsertion.js +135 -0
  94. package/lib/module/engine/spatialInsertion.js.map +1 -0
  95. package/lib/module/engine/spatialPolicy.js +41 -0
  96. package/lib/module/engine/spatialPolicy.js.map +1 -0
  97. package/lib/module/engine/spatialRotation.js +48 -0
  98. package/lib/module/engine/spatialRotation.js.map +1 -0
  99. package/lib/module/engine/validation.js +205 -0
  100. package/lib/module/engine/validation.js.map +1 -0
  101. package/lib/module/gesture/DndGestureGroup.js +268 -0
  102. package/lib/module/gesture/DndGestureGroup.js.map +1 -0
  103. package/lib/module/gesture/DndGestureHost.js +32 -0
  104. package/lib/module/gesture/DndGestureHost.js.map +1 -0
  105. package/lib/module/gesture/gestureConfig.js +81 -0
  106. package/lib/module/gesture/gestureConfig.js.map +1 -0
  107. package/lib/module/gesture/gestureGrouping.js +122 -0
  108. package/lib/module/gesture/gestureGrouping.js.map +1 -0
  109. package/lib/module/gesture/gestureHost.js +106 -0
  110. package/lib/module/gesture/gestureHost.js.map +1 -0
  111. package/lib/module/index.js +23 -0
  112. package/lib/module/index.js.map +1 -0
  113. package/lib/module/package.json +1 -0
  114. package/lib/module/pointer/dndTarget.js +101 -0
  115. package/lib/module/pointer/dndTarget.js.map +1 -0
  116. package/lib/module/pointer/geometry.js +363 -0
  117. package/lib/module/pointer/geometry.js.map +1 -0
  118. package/lib/module/pointer/layoutTarget.js +307 -0
  119. package/lib/module/pointer/layoutTarget.js.map +1 -0
  120. package/lib/module/pointer/listCellMotion.js +99 -0
  121. package/lib/module/pointer/listCellMotion.js.map +1 -0
  122. package/lib/module/pointer/listTarget.js +42 -0
  123. package/lib/module/pointer/listTarget.js.map +1 -0
  124. package/lib/module/pointer/motion.js +37 -0
  125. package/lib/module/pointer/motion.js.map +1 -0
  126. package/lib/module/runtime/dndPresentation.js +81 -0
  127. package/lib/module/runtime/dndPresentation.js.map +1 -0
  128. package/lib/module/runtime/dndRenderStore.js +129 -0
  129. package/lib/module/runtime/dndRenderStore.js.map +1 -0
  130. package/lib/module/runtime/dndRuntime.js +135 -0
  131. package/lib/module/runtime/dndRuntime.js.map +1 -0
  132. package/lib/module/runtime/dndSettlement.js +77 -0
  133. package/lib/module/runtime/dndSettlement.js.map +1 -0
  134. package/lib/module/runtime/gridRuntime.js +37 -0
  135. package/lib/module/runtime/gridRuntime.js.map +1 -0
  136. package/lib/module/types.js +4 -0
  137. package/lib/module/types.js.map +1 -0
  138. package/lib/typescript/package.json +1 -0
  139. package/lib/typescript/src/adapters/coordinates.d.ts +9 -0
  140. package/lib/typescript/src/adapters/coordinates.d.ts.map +1 -0
  141. package/lib/typescript/src/adapters/scroll.d.ts +49 -0
  142. package/lib/typescript/src/adapters/scroll.d.ts.map +1 -0
  143. package/lib/typescript/src/adapters/virtualizedList.d.ts +22 -0
  144. package/lib/typescript/src/adapters/virtualizedList.d.ts.map +1 -0
  145. package/lib/typescript/src/components/DndProvider.d.ts +4 -0
  146. package/lib/typescript/src/components/DndProvider.d.ts.map +1 -0
  147. package/lib/typescript/src/components/DragHandle.d.ts +4 -0
  148. package/lib/typescript/src/components/DragHandle.d.ts.map +1 -0
  149. package/lib/typescript/src/components/GridController.d.ts +21 -0
  150. package/lib/typescript/src/components/GridController.d.ts.map +1 -0
  151. package/lib/typescript/src/components/GridDragHandle.d.ts +8 -0
  152. package/lib/typescript/src/components/GridDragHandle.d.ts.map +1 -0
  153. package/lib/typescript/src/components/GridZoneView.d.ts +8 -0
  154. package/lib/typescript/src/components/GridZoneView.d.ts.map +1 -0
  155. package/lib/typescript/src/components/SortableGrid.d.ts +4 -0
  156. package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -0
  157. package/lib/typescript/src/components/SortableList.d.ts +4 -0
  158. package/lib/typescript/src/components/SortableList.d.ts.map +1 -0
  159. package/lib/typescript/src/components/dndTypes.d.ts +94 -0
  160. package/lib/typescript/src/components/dndTypes.d.ts.map +1 -0
  161. package/lib/typescript/src/components/gridTypes.d.ts +84 -0
  162. package/lib/typescript/src/components/gridTypes.d.ts.map +1 -0
  163. package/lib/typescript/src/components/handleViewProps.d.ts +6 -0
  164. package/lib/typescript/src/components/handleViewProps.d.ts.map +1 -0
  165. package/lib/typescript/src/contracts.d.ts +145 -0
  166. package/lib/typescript/src/contracts.d.ts.map +1 -0
  167. package/lib/typescript/src/controller/approval.d.ts +66 -0
  168. package/lib/typescript/src/controller/approval.d.ts.map +1 -0
  169. package/lib/typescript/src/controller/coordinatorBase.d.ts +37 -0
  170. package/lib/typescript/src/controller/coordinatorBase.d.ts.map +1 -0
  171. package/lib/typescript/src/controller/dndSession.d.ts +152 -0
  172. package/lib/typescript/src/controller/dndSession.d.ts.map +1 -0
  173. package/lib/typescript/src/controller/dndState.d.ts +27 -0
  174. package/lib/typescript/src/controller/dndState.d.ts.map +1 -0
  175. package/lib/typescript/src/controller/gridAdapter.d.ts +44 -0
  176. package/lib/typescript/src/controller/gridAdapter.d.ts.map +1 -0
  177. package/lib/typescript/src/controller/gridState.d.ts +18 -0
  178. package/lib/typescript/src/controller/gridState.d.ts.map +1 -0
  179. package/lib/typescript/src/controller/locations.d.ts +9 -0
  180. package/lib/typescript/src/controller/locations.d.ts.map +1 -0
  181. package/lib/typescript/src/controller/previewPath.d.ts +49 -0
  182. package/lib/typescript/src/controller/previewPath.d.ts.map +1 -0
  183. package/lib/typescript/src/controller/sessionCore.d.ts +106 -0
  184. package/lib/typescript/src/controller/sessionCore.d.ts.map +1 -0
  185. package/lib/typescript/src/controller/useDndState.d.ts +8 -0
  186. package/lib/typescript/src/controller/useDndState.d.ts.map +1 -0
  187. package/lib/typescript/src/controller/useGridState.d.ts +8 -0
  188. package/lib/typescript/src/controller/useGridState.d.ts.map +1 -0
  189. package/lib/typescript/src/controller/useStoreState.d.ts +16 -0
  190. package/lib/typescript/src/controller/useStoreState.d.ts.map +1 -0
  191. package/lib/typescript/src/defaults.d.ts +5 -0
  192. package/lib/typescript/src/defaults.d.ts.map +1 -0
  193. package/lib/typescript/src/diagnostics.d.ts +9 -0
  194. package/lib/typescript/src/diagnostics.d.ts.map +1 -0
  195. package/lib/typescript/src/engine/gridPlacements.d.ts +22 -0
  196. package/lib/typescript/src/engine/gridPlacements.d.ts.map +1 -0
  197. package/lib/typescript/src/engine/index.d.ts +28 -0
  198. package/lib/typescript/src/engine/index.d.ts.map +1 -0
  199. package/lib/typescript/src/engine/layout.d.ts +4 -0
  200. package/lib/typescript/src/engine/layout.d.ts.map +1 -0
  201. package/lib/typescript/src/engine/layoutMove.d.ts +46 -0
  202. package/lib/typescript/src/engine/layoutMove.d.ts.map +1 -0
  203. package/lib/typescript/src/engine/layoutState.d.ts +22 -0
  204. package/lib/typescript/src/engine/layoutState.d.ts.map +1 -0
  205. package/lib/typescript/src/engine/list.d.ts +68 -0
  206. package/lib/typescript/src/engine/list.d.ts.map +1 -0
  207. package/lib/typescript/src/engine/listIndex.d.ts +32 -0
  208. package/lib/typescript/src/engine/listIndex.d.ts.map +1 -0
  209. package/lib/typescript/src/engine/move.d.ts +10 -0
  210. package/lib/typescript/src/engine/move.d.ts.map +1 -0
  211. package/lib/typescript/src/engine/movementPolicy.d.ts +34 -0
  212. package/lib/typescript/src/engine/movementPolicy.d.ts.map +1 -0
  213. package/lib/typescript/src/engine/orderedTarget.d.ts +32 -0
  214. package/lib/typescript/src/engine/orderedTarget.d.ts.map +1 -0
  215. package/lib/typescript/src/engine/pack.d.ts +3 -0
  216. package/lib/typescript/src/engine/pack.d.ts.map +1 -0
  217. package/lib/typescript/src/engine/rectangles.d.ts +9 -0
  218. package/lib/typescript/src/engine/rectangles.d.ts.map +1 -0
  219. package/lib/typescript/src/engine/sameLayout.d.ts +4 -0
  220. package/lib/typescript/src/engine/sameLayout.d.ts.map +1 -0
  221. package/lib/typescript/src/engine/search.d.ts +28 -0
  222. package/lib/typescript/src/engine/search.d.ts.map +1 -0
  223. package/lib/typescript/src/engine/spatial.d.ts +16 -0
  224. package/lib/typescript/src/engine/spatial.d.ts.map +1 -0
  225. package/lib/typescript/src/engine/spatialAppRotation.d.ts +4 -0
  226. package/lib/typescript/src/engine/spatialAppRotation.d.ts.map +1 -0
  227. package/lib/typescript/src/engine/spatialExchange.d.ts +5 -0
  228. package/lib/typescript/src/engine/spatialExchange.d.ts.map +1 -0
  229. package/lib/typescript/src/engine/spatialInsertion.d.ts +5 -0
  230. package/lib/typescript/src/engine/spatialInsertion.d.ts.map +1 -0
  231. package/lib/typescript/src/engine/spatialPolicy.d.ts +9 -0
  232. package/lib/typescript/src/engine/spatialPolicy.d.ts.map +1 -0
  233. package/lib/typescript/src/engine/spatialRotation.d.ts +8 -0
  234. package/lib/typescript/src/engine/spatialRotation.d.ts.map +1 -0
  235. package/lib/typescript/src/engine/validation.d.ts +9 -0
  236. package/lib/typescript/src/engine/validation.d.ts.map +1 -0
  237. package/lib/typescript/src/gesture/DndGestureGroup.d.ts +33 -0
  238. package/lib/typescript/src/gesture/DndGestureGroup.d.ts.map +1 -0
  239. package/lib/typescript/src/gesture/DndGestureHost.d.ts +12 -0
  240. package/lib/typescript/src/gesture/DndGestureHost.d.ts.map +1 -0
  241. package/lib/typescript/src/gesture/gestureConfig.d.ts +15 -0
  242. package/lib/typescript/src/gesture/gestureConfig.d.ts.map +1 -0
  243. package/lib/typescript/src/gesture/gestureGrouping.d.ts +66 -0
  244. package/lib/typescript/src/gesture/gestureGrouping.d.ts.map +1 -0
  245. package/lib/typescript/src/gesture/gestureHost.d.ts +23 -0
  246. package/lib/typescript/src/gesture/gestureHost.d.ts.map +1 -0
  247. package/lib/typescript/src/index.d.ts +38 -0
  248. package/lib/typescript/src/index.d.ts.map +1 -0
  249. package/lib/typescript/src/pointer/dndTarget.d.ts +14 -0
  250. package/lib/typescript/src/pointer/dndTarget.d.ts.map +1 -0
  251. package/lib/typescript/src/pointer/geometry.d.ts +28 -0
  252. package/lib/typescript/src/pointer/geometry.d.ts.map +1 -0
  253. package/lib/typescript/src/pointer/layoutTarget.d.ts +43 -0
  254. package/lib/typescript/src/pointer/layoutTarget.d.ts.map +1 -0
  255. package/lib/typescript/src/pointer/listCellMotion.d.ts +43 -0
  256. package/lib/typescript/src/pointer/listCellMotion.d.ts.map +1 -0
  257. package/lib/typescript/src/pointer/listTarget.d.ts +7 -0
  258. package/lib/typescript/src/pointer/listTarget.d.ts.map +1 -0
  259. package/lib/typescript/src/pointer/motion.d.ts +20 -0
  260. package/lib/typescript/src/pointer/motion.d.ts.map +1 -0
  261. package/lib/typescript/src/runtime/dndPresentation.d.ts +37 -0
  262. package/lib/typescript/src/runtime/dndPresentation.d.ts.map +1 -0
  263. package/lib/typescript/src/runtime/dndRenderStore.d.ts +46 -0
  264. package/lib/typescript/src/runtime/dndRenderStore.d.ts.map +1 -0
  265. package/lib/typescript/src/runtime/dndRuntime.d.ts +138 -0
  266. package/lib/typescript/src/runtime/dndRuntime.d.ts.map +1 -0
  267. package/lib/typescript/src/runtime/dndSettlement.d.ts +18 -0
  268. package/lib/typescript/src/runtime/dndSettlement.d.ts.map +1 -0
  269. package/lib/typescript/src/runtime/gridRuntime.d.ts +54 -0
  270. package/lib/typescript/src/runtime/gridRuntime.d.ts.map +1 -0
  271. package/lib/typescript/src/types.d.ts +180 -0
  272. package/lib/typescript/src/types.d.ts.map +1 -0
  273. package/package.json +136 -0
  274. package/src/adapters/coordinates.ts +181 -0
  275. package/src/adapters/scroll.ts +147 -0
  276. package/src/adapters/virtualizedList.ts +112 -0
  277. package/src/components/DndProvider.tsx +1836 -0
  278. package/src/components/DragHandle.tsx +89 -0
  279. package/src/components/GridController.tsx +213 -0
  280. package/src/components/GridDragHandle.tsx +11 -0
  281. package/src/components/GridZoneView.tsx +57 -0
  282. package/src/components/SortableGrid.tsx +449 -0
  283. package/src/components/SortableList.tsx +1028 -0
  284. package/src/components/dndTypes.ts +102 -0
  285. package/src/components/gridTypes.ts +90 -0
  286. package/src/components/handleViewProps.ts +35 -0
  287. package/src/contracts.ts +149 -0
  288. package/src/controller/approval.ts +179 -0
  289. package/src/controller/coordinatorBase.ts +118 -0
  290. package/src/controller/dndSession.ts +1058 -0
  291. package/src/controller/dndState.ts +198 -0
  292. package/src/controller/gridAdapter.ts +268 -0
  293. package/src/controller/gridState.ts +187 -0
  294. package/src/controller/locations.ts +67 -0
  295. package/src/controller/previewPath.ts +90 -0
  296. package/src/controller/sessionCore.ts +390 -0
  297. package/src/controller/useDndState.ts +26 -0
  298. package/src/controller/useGridState.ts +22 -0
  299. package/src/controller/useStoreState.ts +27 -0
  300. package/src/defaults.ts +4 -0
  301. package/src/diagnostics.ts +23 -0
  302. package/src/engine/gridPlacements.ts +85 -0
  303. package/src/engine/index.ts +99 -0
  304. package/src/engine/layout.ts +29 -0
  305. package/src/engine/layoutMove.ts +401 -0
  306. package/src/engine/layoutState.ts +490 -0
  307. package/src/engine/list.ts +307 -0
  308. package/src/engine/listIndex.ts +174 -0
  309. package/src/engine/move.ts +274 -0
  310. package/src/engine/movementPolicy.ts +151 -0
  311. package/src/engine/orderedTarget.ts +131 -0
  312. package/src/engine/pack.ts +24 -0
  313. package/src/engine/rectangles.ts +18 -0
  314. package/src/engine/sameLayout.ts +50 -0
  315. package/src/engine/search.ts +174 -0
  316. package/src/engine/spatial.ts +36 -0
  317. package/src/engine/spatialAppRotation.ts +53 -0
  318. package/src/engine/spatialExchange.ts +212 -0
  319. package/src/engine/spatialInsertion.ts +157 -0
  320. package/src/engine/spatialPolicy.ts +48 -0
  321. package/src/engine/spatialRotation.ts +56 -0
  322. package/src/engine/validation.ts +358 -0
  323. package/src/gesture/DndGestureGroup.tsx +357 -0
  324. package/src/gesture/DndGestureHost.tsx +50 -0
  325. package/src/gesture/gestureConfig.ts +123 -0
  326. package/src/gesture/gestureGrouping.ts +210 -0
  327. package/src/gesture/gestureHost.tsx +163 -0
  328. package/src/index.ts +162 -0
  329. package/src/pointer/dndTarget.ts +213 -0
  330. package/src/pointer/geometry.ts +584 -0
  331. package/src/pointer/layoutTarget.ts +515 -0
  332. package/src/pointer/listCellMotion.ts +174 -0
  333. package/src/pointer/listTarget.ts +56 -0
  334. package/src/pointer/motion.ts +66 -0
  335. package/src/runtime/dndPresentation.ts +113 -0
  336. package/src/runtime/dndRenderStore.ts +246 -0
  337. package/src/runtime/dndRuntime.ts +311 -0
  338. package/src/runtime/dndSettlement.ts +117 -0
  339. package/src/runtime/gridRuntime.ts +73 -0
  340. package/src/types.ts +189 -0
@@ -0,0 +1,1058 @@
1
+ import type {
2
+ DndItem,
3
+ GridLayoutZone,
4
+ LayoutLocation,
5
+ LayoutState,
6
+ LayoutValidationIssue,
7
+ } from '../contracts';
8
+ import { samePlacement } from '../engine/gridPlacements';
9
+ import {
10
+ computeValidatedLayoutMove,
11
+ type GridItemLayout,
12
+ type LayoutMoveResult,
13
+ } from '../engine/layoutMove';
14
+ import { validateLayoutState } from '../engine/layoutState';
15
+ import { isCellSpan, isPositiveInteger, isRecord } from '../engine/validation';
16
+ import { DEFAULT_SEARCH_BUDGET } from '../defaults';
17
+ import {
18
+ resolveGridMovementPolicy,
19
+ validateGridMovementPolicy,
20
+ type GridMovementPolicy,
21
+ type ResolvedGridMovementPolicy,
22
+ } from '../engine/movementPolicy';
23
+ import type {
24
+ CancelReason,
25
+ DragStartEvent,
26
+ DropDecision,
27
+ GridDiagnosticEvent,
28
+ } from '../types';
29
+
30
+ import {
31
+ respondToValidatedDndProposal,
32
+ type DndProposal,
33
+ type DndValue,
34
+ } from './dndState';
35
+ import { now, reportCallbackError as reportFailure } from './coordinatorBase';
36
+ import { layoutLocation, sameLayoutLocation } from './locations';
37
+ import { PreviewPath } from './previewPath';
38
+ import {
39
+ type CandidateResult,
40
+ SessionCore,
41
+ validatedDecision,
42
+ } from './sessionCore';
43
+
44
+ export type DndCancelReason = CancelReason;
45
+ export type DndDropFailureReason =
46
+ | 'outside-zones'
47
+ | 'invalid-target'
48
+ | 'missing-grid-item-layout'
49
+ | 'invalid-grid-item-layout'
50
+ | 'unresolvable'
51
+ | 'search-budget'
52
+ | 'policy-rejected';
53
+ export type GridItemLayoutResolverArgs<T> = {
54
+ item: DndItem<T>;
55
+ from: LayoutLocation;
56
+ target: GridLayoutZone;
57
+ value: LayoutState<T>;
58
+ };
59
+ export type DndDragEndEvent<T> = DragStartEvent & { error?: unknown } & (
60
+ | { outcome: 'unchanged' }
61
+ | {
62
+ outcome: 'cancelled';
63
+ reason: DndCancelReason;
64
+ targetZoneId?: string;
65
+ policyReason?: string;
66
+ }
67
+ | {
68
+ outcome: 'proposed';
69
+ proposal: DndProposal<T>;
70
+ targetZoneId: string;
71
+ response: 'accepted' | 'rejected' | 'expired' | 'mismatch';
72
+ }
73
+ | {
74
+ outcome: 'proposed';
75
+ proposal: DndProposal<T>;
76
+ targetZoneId: string;
77
+ response: 'interrupted';
78
+ reason: DndCancelReason | 'response-missing' | 'change-handler-error';
79
+ }
80
+ );
81
+
82
+ export type DndSessionSnapshot<T> = {
83
+ value: DndValue<T> | null;
84
+ displayValue: DndValue<T> | null;
85
+ candidate: LayoutState<T> | null;
86
+ phase: 'idle' | 'dragging' | 'awaiting-response';
87
+ sessionId: string | null;
88
+ itemId: string | null;
89
+ sourceZoneId: string | null;
90
+ targetZoneId: string | null;
91
+ target: LayoutLocation | null;
92
+ seq: number;
93
+ validity: 'pending' | 'valid' | 'invalid';
94
+ disabled: boolean;
95
+ /** Span and placement captured once for each grid target at activation. */
96
+ gridItemLayouts: ReadonlyMap<string, GridItemLayout>;
97
+ movementPolicy: ResolvedGridMovementPolicy;
98
+ failureReason: DndDropFailureReason | null;
99
+ };
100
+
101
+ export type DndSessionOptions<T> = {
102
+ value: DndValue<T>;
103
+ onChange(next: DndValue<T>, proposal: DndProposal<T>): void;
104
+ canDrop?(proposal: DndProposal<T>): DropDecision;
105
+ onDragStart?(event: DragStartEvent): void;
106
+ onDragEnd?(event: DndDragEndEvent<T>): void;
107
+ onValidationError?(issues: LayoutValidationIssue[]): void;
108
+ /** Optional JS-only observation. Exceptions are ignored; omit to disable timing. */
109
+ onDiagnostic?(event: GridDiagnosticEvent): void;
110
+ responseTimeoutMs?: number;
111
+ disabled?: boolean;
112
+ movementPolicy?: GridMovementPolicy;
113
+ searchBudget?: number;
114
+ /** Required for list-to-grid; returning null disables that target for this session. */
115
+ getGridItemLayout?(
116
+ args: GridItemLayoutResolverArgs<T>,
117
+ ): GridItemLayout | null;
118
+ };
119
+
120
+ type ActiveSession<T> = DragStartEvent & {
121
+ baseRevision: number;
122
+ seq: number;
123
+ target: LayoutLocation | null;
124
+ /** Same-zone grid previews the next candidate builds on. */
125
+ path: PreviewPath<LayoutLocation, LayoutState<T>>;
126
+ proposal?: DndProposal<T>;
127
+ proposalKey?: string;
128
+ deadline?: number;
129
+ gridItemLayouts: Map<string, GridItemLayout>;
130
+ gridItemLayoutErrors: Map<
131
+ string,
132
+ 'missing-grid-item-layout' | 'invalid-grid-item-layout'
133
+ >;
134
+ movementPolicy: ResolvedGridMovementPolicy;
135
+ searchBudget: number;
136
+ responseTimeoutMs: number;
137
+ settingsKey: string;
138
+ getGridItemLayout: DndSessionOptions<T>['getGridItemLayout'];
139
+ resolving: boolean;
140
+ previewSeq: number | null;
141
+ previewInputVersion: number;
142
+ };
143
+
144
+ let instanceCounter = 0;
145
+
146
+ /** Capture structure by value so an in-place same-revision edit still invalidates a drag. */
147
+ function layoutKey<T>(value: LayoutState<T>): string {
148
+ return JSON.stringify([
149
+ value.revision,
150
+ value.items.map(item => item.id).sort(),
151
+ value.zones
152
+ .map(zone =>
153
+ zone.kind === 'list'
154
+ ? [zone.id, zone.kind, zone.orientation, [...zone.itemIds]]
155
+ : [
156
+ zone.id,
157
+ zone.kind,
158
+ zone.rows,
159
+ zone.columns,
160
+ zone.placements
161
+ .map(item => [
162
+ item.itemId,
163
+ item.position.row,
164
+ item.position.col,
165
+ item.span.rows,
166
+ item.span.cols,
167
+ item.placement,
168
+ ])
169
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
170
+ ],
171
+ )
172
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
173
+ ]);
174
+ }
175
+
176
+ function sameTarget(
177
+ first: LayoutLocation | null,
178
+ second: LayoutLocation | null,
179
+ ): boolean {
180
+ if (!first || !second) return first === second;
181
+ if (first.zoneId !== second.zoneId) return false;
182
+ return first.kind === 'list' && second.kind === 'list'
183
+ ? first.index === second.index
184
+ : first.kind === 'grid' &&
185
+ second.kind === 'grid' &&
186
+ first.position?.row === second.position?.row &&
187
+ first.position?.col === second.position?.col;
188
+ }
189
+
190
+ type PreviewStructure =
191
+ | 'state'
192
+ | 'zones'
193
+ | 'zone'
194
+ | 'placements'
195
+ | 'placement'
196
+ | 'coordinates'
197
+ | 'itemIds'
198
+ | 'target';
199
+
200
+ /** Compare generated layout containers without reading or copying opaque data. */
201
+ function samePreviewStructure(
202
+ first: unknown,
203
+ second: unknown,
204
+ structure: PreviewStructure,
205
+ ): boolean {
206
+ if (Object.is(first, second)) return true;
207
+ if (
208
+ first === null ||
209
+ second === null ||
210
+ typeof first !== 'object' ||
211
+ typeof second !== 'object' ||
212
+ Object.getPrototypeOf(first) !== Object.getPrototypeOf(second)
213
+ )
214
+ return false;
215
+ const keys = Reflect.ownKeys(first);
216
+ const otherKeys = Reflect.ownKeys(second);
217
+ return (
218
+ keys.length === otherKeys.length &&
219
+ keys.every((key, index) => {
220
+ if (key !== otherKeys[index]) return false;
221
+ const a = Object.getOwnPropertyDescriptor(first, key)!;
222
+ const b = Object.getOwnPropertyDescriptor(second, key)!;
223
+ if (!('value' in a) || !('value' in b) || a.enumerable !== b.enumerable)
224
+ return false;
225
+ let child: PreviewStructure | undefined;
226
+ if (structure === 'state' && key === 'zones') child = 'zones';
227
+ else if (structure === 'zone' && key === 'itemIds') child = 'itemIds';
228
+ else if (structure === 'zone' && key === 'placements')
229
+ child = 'placements';
230
+ else if (
231
+ (structure === 'placement' && (key === 'position' || key === 'span')) ||
232
+ (structure === 'target' && key === 'position')
233
+ )
234
+ child = 'coordinates';
235
+ else if (
236
+ (structure === 'zones' || structure === 'placements') &&
237
+ typeof key === 'string' &&
238
+ /^(0|[1-9]\d*)$/.test(key)
239
+ )
240
+ child = structure === 'zones' ? 'zone' : 'placement';
241
+ // Item arrays and extra metadata retain their exact identity. In
242
+ // particular, equality never traverses caller-owned item.data objects.
243
+ return child
244
+ ? samePreviewStructure(a.value, b.value, child)
245
+ : Object.is(a.value, b.value);
246
+ })
247
+ );
248
+ }
249
+
250
+ function sameVisibleSnapshot<T>(
251
+ first: DndSessionSnapshot<T>,
252
+ second: DndSessionSnapshot<T>,
253
+ ): boolean {
254
+ return (Object.keys(first) as Array<keyof DndSessionSnapshot<T>>).every(
255
+ key =>
256
+ key === 'seq' ||
257
+ (key === 'candidate' || key === 'displayValue'
258
+ ? samePreviewStructure(first[key], second[key], 'state')
259
+ : key === 'target'
260
+ ? samePreviewStructure(first[key], second[key], 'target')
261
+ : Object.is(first[key], second[key])),
262
+ );
263
+ }
264
+
265
+ function settingsKey<T>(options: DndSessionOptions<T>): string {
266
+ return JSON.stringify([
267
+ options.searchBudget ?? DEFAULT_SEARCH_BUDGET,
268
+ options.responseTimeoutMs ?? 2000,
269
+ resolveGridMovementPolicy(options.movementPolicy),
270
+ ]);
271
+ }
272
+
273
+ function copyMovementPolicy(
274
+ policy?: GridMovementPolicy,
275
+ ): ResolvedGridMovementPolicy {
276
+ const resolved = resolveGridMovementPolicy(policy);
277
+ return Object.freeze({
278
+ ...resolved,
279
+ candidateOrder: Object.freeze([...resolved.candidateOrder]),
280
+ pointer: Object.freeze({ ...resolved.pointer }),
281
+ });
282
+ }
283
+
284
+ function copyGridItemLayout(value: unknown): GridItemLayout | null {
285
+ if (
286
+ !isRecord(value) ||
287
+ !isCellSpan(value.span) ||
288
+ (value.placement !== undefined &&
289
+ value.placement !== 'insert' &&
290
+ value.placement !== 'exchange')
291
+ )
292
+ return null;
293
+ return Object.freeze({
294
+ span: Object.freeze({ rows: value.span.rows, cols: value.span.cols }),
295
+ ...(value.placement === undefined ? {} : { placement: value.placement }),
296
+ });
297
+ }
298
+
299
+ function checkOptions<T>(options: DndSessionOptions<T>) {
300
+ const checked = validateLayoutState(options.value);
301
+ const issues: LayoutValidationIssue[] = checked.valid
302
+ ? []
303
+ : [...checked.issues];
304
+ const policy = validateGridMovementPolicy(options.movementPolicy);
305
+ if (!policy.valid) issues.push(...policy.issues);
306
+ const budgetValid = isPositiveInteger(
307
+ options.searchBudget === undefined
308
+ ? DEFAULT_SEARCH_BUDGET
309
+ : options.searchBudget,
310
+ );
311
+ const timeoutValid = isPositiveInteger(options.responseTimeoutMs ?? 2000);
312
+ if (!budgetValid)
313
+ issues.push({
314
+ code: 'invalid-budget',
315
+ message: 'searchBudget must be a positive safe integer.',
316
+ });
317
+ if (
318
+ (options.responseTimeoutMs !== undefined &&
319
+ !isPositiveInteger(options.responseTimeoutMs)) ||
320
+ (options.disabled !== undefined && typeof options.disabled !== 'boolean') ||
321
+ typeof options.onChange !== 'function' ||
322
+ [
323
+ 'canDrop',
324
+ 'onDragStart',
325
+ 'onDragEnd',
326
+ 'onValidationError',
327
+ 'getGridItemLayout',
328
+ ].some(key => {
329
+ const callback = options[key as keyof DndSessionOptions<T>];
330
+ return callback !== undefined && typeof callback !== 'function';
331
+ })
332
+ ) {
333
+ issues.push({
334
+ code: 'invalid-configuration',
335
+ message: 'Session callbacks, disabled or response timeout are invalid.',
336
+ });
337
+ }
338
+ return {
339
+ layoutValid: checked.valid,
340
+ issues,
341
+ movementPolicy: copyMovementPolicy(
342
+ policy.valid ? options.movementPolicy : undefined,
343
+ ),
344
+ settingsKey:
345
+ policy.valid && budgetValid && timeoutValid ? settingsKey(options) : null,
346
+ };
347
+ }
348
+
349
+ function reportCallbackError(error: unknown): void {
350
+ reportFailure('LayoutDnD', error);
351
+ }
352
+
353
+ /** Give a stored preview the item objects of a same-layout recommit. */
354
+ function rebaseLayoutState<T>(
355
+ value: LayoutState<T>,
356
+ committed: LayoutState<T>,
357
+ ): LayoutState<T> {
358
+ return value.items === committed.items
359
+ ? value
360
+ : { ...value, items: committed.items };
361
+ }
362
+
363
+ /** Compare zone layouts by ID; revision and caller items are not layout. */
364
+ function sameLayoutZones<T>(
365
+ first: LayoutState<T>,
366
+ second: LayoutState<T>,
367
+ ): boolean {
368
+ if (first.zones.length !== second.zones.length) return false;
369
+ const zones = new Map(second.zones.map(zone => [zone.id, zone]));
370
+ return first.zones.every(zone => {
371
+ const other = zones.get(zone.id);
372
+ if (!other) return false;
373
+ if (zone === other) return true;
374
+ if (zone.kind === 'list')
375
+ return (
376
+ other.kind === 'list' &&
377
+ zone.itemIds.length === other.itemIds.length &&
378
+ zone.itemIds.every((id, index) => id === other.itemIds[index])
379
+ );
380
+ if (
381
+ other.kind !== 'grid' ||
382
+ zone.placements.length !== other.placements.length
383
+ )
384
+ return false;
385
+ const placements = new Map(
386
+ other.placements.map(item => [item.itemId, item]),
387
+ );
388
+ return zone.placements.every(item => {
389
+ const match = placements.get(item.itemId);
390
+ return match !== undefined && samePlacement(item, match);
391
+ });
392
+ });
393
+ }
394
+
395
+ /** Express a candidate built on an earlier preview relative to the committed value. */
396
+ function relativeToCommitted<T>(
397
+ committed: LayoutState<T>,
398
+ from: LayoutLocation,
399
+ result: Extract<LayoutMoveResult<T>, { status: 'ok' }>,
400
+ ): LayoutMoveResult<T> {
401
+ if (sameLayoutZones(committed, result.value))
402
+ return { ...result, value: committed, from, changed: false };
403
+ if (committed.revision === Number.MAX_SAFE_INTEGER)
404
+ return {
405
+ status: 'invalid',
406
+ issues: [
407
+ {
408
+ code: 'invalid-revision',
409
+ message: 'The layout revision cannot exceed a safe integer.',
410
+ },
411
+ ],
412
+ };
413
+ const revision = committed.revision + 1;
414
+ const value =
415
+ result.value.revision === revision && result.value.items === committed.items
416
+ ? result.value
417
+ : { ...result.value, revision, items: committed.items };
418
+ return { ...result, value, from, changed: true };
419
+ }
420
+
421
+ /** UI-independent normalized session; screen/scroll measurements stay in the adapter. */
422
+ export class DndSessionCoordinator<T> extends SessionCore<
423
+ DndValue<T>,
424
+ LayoutLocation,
425
+ DndSessionSnapshot<T>,
426
+ ActiveSession<T>,
427
+ DndProposal<T>
428
+ > {
429
+ private readonly instanceId = ++instanceCounter;
430
+ private sessionCounter = 0;
431
+ private options: DndSessionOptions<T>;
432
+ private lastLayoutKey: string | null;
433
+ private validationNotification: string | null = null;
434
+ private previewInputVersion = 0;
435
+ private operationVersion = 0;
436
+
437
+ constructor(options: DndSessionOptions<T>) {
438
+ const checked = checkOptions(options);
439
+ const lastValidValue = checked.layoutValid ? options.value : null;
440
+ super(
441
+ {
442
+ value: lastValidValue,
443
+ displayValue: lastValidValue,
444
+ candidate: null,
445
+ phase: 'idle',
446
+ sessionId: null,
447
+ itemId: null,
448
+ sourceZoneId: null,
449
+ targetZoneId: null,
450
+ target: null,
451
+ seq: 0,
452
+ validity: checked.issues.length ? 'invalid' : 'valid',
453
+ disabled: checked.issues.length > 0 || options.disabled === true,
454
+ gridItemLayouts: new Map(),
455
+ movementPolicy: checked.movementPolicy,
456
+ failureReason: null,
457
+ },
458
+ lastValidValue,
459
+ );
460
+ this.options = options;
461
+ this.lastLayoutKey = checked.layoutValid ? layoutKey(options.value) : null;
462
+ }
463
+
464
+ /** Every batch is one operation; a refresh may only fold into its own. */
465
+ protected override batchStarted(): void {
466
+ this.operationVersion++;
467
+ }
468
+
469
+ protected diagnosticObserver() {
470
+ return typeof this.options.onDiagnostic === 'function'
471
+ ? this.options.onDiagnostic
472
+ : undefined;
473
+ }
474
+
475
+ /** Commit owner state after render; ordinary scroll updates do not enter this contract. */
476
+ commit(options: DndSessionOptions<T>): void {
477
+ this.batch(() => {
478
+ this.disposed = false;
479
+ const previousValue = this.lastValidValue;
480
+ const checked = checkOptions(options);
481
+ const key = checked.layoutValid ? layoutKey(options.value) : null;
482
+ const layoutChanged = key !== this.lastLayoutKey;
483
+ if (
484
+ options.value !== this.options.value ||
485
+ options.canDrop !== this.options.canDrop
486
+ )
487
+ this.previewInputVersion++;
488
+ this.options = options;
489
+ this.lastLayoutKey = key;
490
+ if (checked.layoutValid) this.lastValidValue = options.value;
491
+ const disabled = checked.issues.length > 0 || options.disabled === true;
492
+ this.update({ value: this.lastValidValue, disabled });
493
+ const session = this.session;
494
+ const settingsChanged =
495
+ !!session &&
496
+ (checked.settingsKey !== session.settingsKey ||
497
+ options.getGridItemLayout !== session.getGridItemLayout);
498
+ if (!session) {
499
+ this.update({
500
+ displayValue: this.lastValidValue,
501
+ validity: disabled ? 'invalid' : 'valid',
502
+ ...(JSON.stringify(this.snapshot.movementPolicy) !==
503
+ JSON.stringify(checked.movementPolicy)
504
+ ? { movementPolicy: checked.movementPolicy }
505
+ : {}),
506
+ });
507
+ } else if (this.snapshot.phase === 'awaiting-response') {
508
+ const response = isRecord(options.value)
509
+ ? options.value.proposalResponse
510
+ : undefined;
511
+ const matches =
512
+ isRecord(response) &&
513
+ response.sessionId === session.sessionId &&
514
+ response.baseRevision === session.baseRevision &&
515
+ typeof response.accepted === 'boolean';
516
+ if (session.deadline !== undefined && now() >= session.deadline) {
517
+ this.settle(session, 'expired');
518
+ } else if (settingsChanged) {
519
+ this.interrupt(session, 'input-changed');
520
+ } else if (matches) {
521
+ // Acceptance is judged by layout and revision (the layout key). The
522
+ // owner may rebuild item objects or data in the accepting commit,
523
+ // as a store that derives a dock item from a page item does.
524
+ this.settle(
525
+ session,
526
+ response.accepted === false
527
+ ? 'rejected'
528
+ : checked.layoutValid && key === session.proposalKey
529
+ ? 'accepted'
530
+ : 'mismatch',
531
+ );
532
+ } else if (layoutChanged) {
533
+ this.interrupt(
534
+ session,
535
+ key === session.proposalKey ? 'response-missing' : 'input-changed',
536
+ );
537
+ } else if (disabled) {
538
+ this.interrupt(session, 'disabled');
539
+ }
540
+ } else if (layoutChanged) {
541
+ this.cancelWith('input-changed');
542
+ } else if (disabled) {
543
+ this.cancelWith('disabled');
544
+ } else if (settingsChanged) {
545
+ this.cancelWith('input-changed');
546
+ } else if (this.snapshot.displayValue === previousValue) {
547
+ this.update({ displayValue: this.lastValidValue });
548
+ }
549
+ const notification = checked.issues.length
550
+ ? JSON.stringify(checked.issues)
551
+ : null;
552
+ if (notification !== this.validationNotification) {
553
+ this.validationNotification = notification;
554
+ if (notification && typeof options.onValidationError === 'function') {
555
+ try {
556
+ options.onValidationError(checked.issues);
557
+ } catch (error) {
558
+ reportCallbackError(error);
559
+ }
560
+ }
561
+ }
562
+ });
563
+ }
564
+
565
+ start(itemId: string, baseRevision?: number): string | null {
566
+ return this.batch(() => {
567
+ if (
568
+ this.disposed ||
569
+ this.session ||
570
+ this.snapshot.disabled ||
571
+ !this.lastValidValue ||
572
+ (baseRevision !== undefined &&
573
+ baseRevision !== this.lastValidValue.revision)
574
+ )
575
+ return null;
576
+ const checked = checkOptions(this.options);
577
+ if (checked.issues.length) {
578
+ this.commit(this.options);
579
+ return null;
580
+ }
581
+ const source = this.lastValidValue.zones.find(zone =>
582
+ zone.kind === 'list'
583
+ ? zone.itemIds.includes(itemId)
584
+ : zone.placements.some(item => item.itemId === itemId),
585
+ );
586
+ if (!source) return null;
587
+ const base = this.lastValidValue;
588
+ const gridSource =
589
+ source.kind === 'grid'
590
+ ? source.placements.find(item => item.itemId === itemId)!
591
+ : null;
592
+ const from: LayoutLocation =
593
+ source.kind === 'list'
594
+ ? {
595
+ kind: 'list',
596
+ zoneId: source.id,
597
+ index: source.itemIds.indexOf(itemId),
598
+ }
599
+ : {
600
+ kind: 'grid',
601
+ zoneId: source.id,
602
+ position: { ...gridSource!.position },
603
+ };
604
+ const session: ActiveSession<T> = {
605
+ sessionId: `layout-dnd:${this.instanceId}:${++this.sessionCounter}`,
606
+ itemId,
607
+ sourceZoneId: source.id,
608
+ baseRevision: this.lastValidValue.revision,
609
+ seq: -1,
610
+ target: null,
611
+ path: new PreviewPath(sameLayoutLocation, rebaseLayoutState),
612
+ gridItemLayouts: new Map(),
613
+ gridItemLayoutErrors: new Map(),
614
+ movementPolicy: checked.movementPolicy,
615
+ searchBudget: this.options.searchBudget ?? DEFAULT_SEARCH_BUDGET,
616
+ responseTimeoutMs: this.options.responseTimeoutMs ?? 2000,
617
+ settingsKey: settingsKey(this.options),
618
+ getGridItemLayout: this.options.getGridItemLayout,
619
+ resolving: true,
620
+ previewSeq: null,
621
+ previewInputVersion: this.previewInputVersion,
622
+ };
623
+ this.session = session;
624
+ this.update({
625
+ phase: 'dragging',
626
+ sessionId: session.sessionId,
627
+ itemId,
628
+ sourceZoneId: source.id,
629
+ targetZoneId: null,
630
+ target: null,
631
+ candidate: null,
632
+ seq: -1,
633
+ displayValue: this.lastValidValue,
634
+ validity: 'pending',
635
+ failureReason: null,
636
+ gridItemLayouts: new Map(),
637
+ movementPolicy: session.movementPolicy,
638
+ });
639
+ try {
640
+ const item = base.items.find(candidate => candidate.id === itemId)!;
641
+ for (const target of base.zones) {
642
+ if (target.kind !== 'grid') continue;
643
+ const descriptor =
644
+ target.id === source.id
645
+ ? gridSource
646
+ : session.getGridItemLayout
647
+ ? session.getGridItemLayout({ item, from, target, value: base })
648
+ : gridSource;
649
+ if (this.session !== session) return null;
650
+ const copied = copyGridItemLayout(descriptor);
651
+ if (copied) session.gridItemLayouts.set(target.id, copied);
652
+ else
653
+ session.gridItemLayoutErrors.set(
654
+ target.id,
655
+ descriptor === null || descriptor === undefined
656
+ ? 'missing-grid-item-layout'
657
+ : 'invalid-grid-item-layout',
658
+ );
659
+ }
660
+ } catch (error) {
661
+ if (this.session === session) this.cancelWith('policy-error', error);
662
+ else reportCallbackError(error);
663
+ return null;
664
+ }
665
+ session.resolving = false;
666
+ this.update({ gridItemLayouts: new Map(session.gridItemLayouts) });
667
+ try {
668
+ this.options.onDragStart?.(this.event(session));
669
+ } catch (error) {
670
+ if (this.session === session)
671
+ this.cancelWith('start-handler-error', error);
672
+ else reportCallbackError(error);
673
+ }
674
+ return this.session === session ? session.sessionId : null;
675
+ });
676
+ }
677
+
678
+ requestTarget(
679
+ sessionId: string,
680
+ seq: number,
681
+ to: LayoutLocation | null,
682
+ options?: { refresh?: boolean },
683
+ ): void {
684
+ this.batch(() => {
685
+ const session = this.acceptsRequest(sessionId, seq, false);
686
+ if (!session || session.resolving) return;
687
+ // The adapter can certify that measurements and policy inputs have not
688
+ // changed. Pointer movement within one target then needs only the private
689
+ // sequence guard; publishing it would rerender every sortable child.
690
+ if (
691
+ options?.refresh === false &&
692
+ session.previewSeq === session.seq &&
693
+ session.previewInputVersion === this.previewInputVersion &&
694
+ sameTarget(session.target, to)
695
+ ) {
696
+ session.seq = seq;
697
+ session.previewSeq = seq;
698
+ // Pointer movement inside one logical cell. Reported so an observer can
699
+ // tell a coalesced packet from one that never arrived.
700
+ this.lifecycle('request-coalesced', session);
701
+ return;
702
+ }
703
+ const inputVersion = this.previewInputVersion;
704
+ const previous = this.snapshot;
705
+ const previousChanged = this.changed;
706
+ const operationVersion = this.operationVersion;
707
+ this.lifecycle('request', session);
708
+ this.calculate(session, seq, to, false);
709
+ // Reentrant callbacks may already have completed a newer request. Never
710
+ // mark that request as reusable using this older request's inputs.
711
+ if (
712
+ this.session === session &&
713
+ session.seq === seq &&
714
+ this.snapshot.phase === 'dragging'
715
+ ) {
716
+ session.previewSeq = seq;
717
+ session.previewInputVersion = inputVersion;
718
+ if (
719
+ options?.refresh === true &&
720
+ this.batchDepth === 1 &&
721
+ this.operationVersion === operationVersion &&
722
+ this.previewInputVersion === inputVersion &&
723
+ sameTarget(previous.target, to) &&
724
+ sameVisibleSnapshot(previous, this.snapshot)
725
+ ) {
726
+ // Scrolling can require a fresh visibility/canDrop check without
727
+ // changing anything React displays. Keep sequence ownership private,
728
+ // just as for refresh:false, after every validation has completed.
729
+ this.restore(previous, previousChanged);
730
+ }
731
+ }
732
+ });
733
+ }
734
+
735
+ release(sessionId: string, seq: number, to: LayoutLocation | null): void {
736
+ this.batch(() => {
737
+ const session = this.acceptsRequest(sessionId, seq, true);
738
+ if (!session || session.resolving) return;
739
+ // This is receipt on JS, not the physical touch-release timestamp.
740
+ this.lifecycle('release', session);
741
+ this.calculate(session, seq, to, true);
742
+ });
743
+ }
744
+
745
+ private calculate(
746
+ session: ActiveSession<T>,
747
+ seq: number,
748
+ target: LayoutLocation | null,
749
+ final: boolean,
750
+ ): void {
751
+ // The path accumulates inside one grid zone only; any other target builds
752
+ // on the committed value again, as the display does.
753
+ if (
754
+ !target ||
755
+ target.kind !== 'grid' ||
756
+ target.zoneId !== session.target?.zoneId
757
+ )
758
+ session.path.reset();
759
+ session.seq = seq;
760
+ session.target =
761
+ target &&
762
+ (target.kind === 'grid'
763
+ ? { ...target, position: { ...target.position } }
764
+ : { ...target });
765
+ this.update({
766
+ seq,
767
+ target: session.target,
768
+ targetZoneId: target?.zoneId ?? null,
769
+ validity: 'pending',
770
+ failureReason: null,
771
+ });
772
+ this.calculateCandidate(session, final);
773
+ }
774
+
775
+ protected location(value: DndValue<T>, itemId: string) {
776
+ return layoutLocation(value, itemId);
777
+ }
778
+
779
+ protected isSpatial(target: LayoutLocation) {
780
+ return target.kind === 'grid';
781
+ }
782
+
783
+ /** Grid destinations need the span captured at activation. */
784
+ protected precheck(session: ActiveSession<T>, target: LayoutLocation) {
785
+ if (target.kind !== 'grid' || session.gridItemLayouts.has(target.zoneId))
786
+ return null;
787
+ return session.gridItemLayoutErrors.get(target.zoneId) ?? 'invalid-target';
788
+ }
789
+
790
+ protected compute(
791
+ session: ActiveSession<T>,
792
+ target: LayoutLocation,
793
+ baseline: { base: DndValue<T>; cumulative: boolean },
794
+ committed: DndValue<T>,
795
+ from: LayoutLocation | null,
796
+ ): CandidateResult<DndValue<T>, LayoutLocation> {
797
+ // lastValidValue passed validateLayoutState when it was committed, the
798
+ // API requires immutable values, and an earlier preview was validated as
799
+ // a complete candidate. Pointer updates therefore validate only the new
800
+ // candidate instead of the whole baseline again.
801
+ const result = this.timed(
802
+ session,
803
+ 'compute-move',
804
+ () =>
805
+ computeValidatedLayoutMove({
806
+ value: baseline.cumulative
807
+ ? { ...baseline.base, revision: committed.revision }
808
+ : committed,
809
+ itemId: session.itemId,
810
+ to: target,
811
+ gridItem:
812
+ target.kind === 'grid'
813
+ ? session.gridItemLayouts.get(target.zoneId)
814
+ : undefined,
815
+ movementPolicy: session.movementPolicy,
816
+ searchBudget: session.searchBudget,
817
+ }),
818
+ computed => computed.status,
819
+ computed => ('attempts' in computed ? computed.attempts : undefined),
820
+ );
821
+ if (result.status !== 'ok') return { status: result.status };
822
+ const relative =
823
+ baseline.cumulative && from
824
+ ? relativeToCommitted(committed, from, result)
825
+ : result;
826
+ return relative.status === 'ok' ? relative : { status: relative.status };
827
+ }
828
+
829
+ // A transient gap between legal positions inside the source grid must not
830
+ // bounce neighbors back to the committed layout (the same rule as the grid
831
+ // controller). This affects display only; release still computes its own
832
+ // candidate and reports the failure reason.
833
+ protected keepsPreview(
834
+ session: ActiveSession<T>,
835
+ target: LayoutLocation,
836
+ status: 'impossible' | 'unresolved' | 'invalid',
837
+ ) {
838
+ return (
839
+ target.kind === 'grid' &&
840
+ target.zoneId === session.sourceZoneId &&
841
+ (status === 'impossible' || status === 'unresolved')
842
+ );
843
+ }
844
+
845
+ protected prepareProposal(
846
+ session: ActiveSession<T>,
847
+ result: { value: DndValue<T>; from: LayoutLocation; to: LayoutLocation },
848
+ ): DndProposal<T> {
849
+ return {
850
+ sessionId: session.sessionId,
851
+ baseRevision: session.baseRevision,
852
+ itemId: session.itemId,
853
+ from: result.from,
854
+ to: result.to,
855
+ value: result.value,
856
+ };
857
+ }
858
+
859
+ protected askPolicy(
860
+ _session: ActiveSession<T>,
861
+ _committed: DndValue<T>,
862
+ proposal: DndProposal<T>,
863
+ ): DropDecision {
864
+ return validatedDecision(
865
+ this.options.canDrop ? this.options.canDrop(proposal) : { allowed: true },
866
+ );
867
+ }
868
+
869
+ protected policyFailed(
870
+ session: ActiveSession<T>,
871
+ seq: number,
872
+ error: unknown,
873
+ ) {
874
+ if (
875
+ this.session === session &&
876
+ session.seq === seq &&
877
+ this.snapshot.phase === 'dragging'
878
+ )
879
+ this.cancelWith('policy-error', error);
880
+ else reportCallbackError(error);
881
+ }
882
+
883
+ // Policy callbacks receive the complete candidate. Validate it again if a
884
+ // callback was allowed to run before issuing an atomic state proposal.
885
+ protected revalidate(
886
+ _session: ActiveSession<T>,
887
+ committed: DndValue<T>,
888
+ proposal: DndProposal<T>,
889
+ ) {
890
+ return (
891
+ !this.options.canDrop ||
892
+ respondToValidatedDndProposal(
893
+ this.lastValidValue ?? committed,
894
+ proposal,
895
+ true,
896
+ ).status === 'accepted'
897
+ );
898
+ }
899
+
900
+ protected showCandidate(result: { value: DndValue<T> }) {
901
+ this.update({
902
+ validity: 'valid',
903
+ candidate: result.value,
904
+ displayValue: result.value,
905
+ });
906
+ }
907
+
908
+ protected showFailure(
909
+ committed: DndValue<T> | null,
910
+ failure: string,
911
+ keepPreview: boolean,
912
+ ) {
913
+ this.update({
914
+ validity: 'invalid',
915
+ failureReason: failure as DndDropFailureReason,
916
+ ...(keepPreview ? {} : { candidate: null, displayValue: committed }),
917
+ });
918
+ }
919
+
920
+ protected expectedPhase() {
921
+ return 'dragging';
922
+ }
923
+
924
+ protected sessionEnded() {
925
+ return false;
926
+ }
927
+
928
+ protected finishUnchanged(session: ActiveSession<T>) {
929
+ this.finish(session, { ...this.event(session), outcome: 'unchanged' });
930
+ }
931
+
932
+ protected registerProposal(
933
+ session: ActiveSession<T>,
934
+ proposal: DndProposal<T>,
935
+ result: { value: DndValue<T> },
936
+ ) {
937
+ session.proposal = proposal;
938
+ session.proposalKey = layoutKey(result.value);
939
+ session.deadline = now() + session.responseTimeoutMs;
940
+ this.update({ phase: 'awaiting-response' });
941
+ this.armTimer(session);
942
+ this.lifecycle('proposal', session);
943
+ }
944
+
945
+ protected emitChange(
946
+ proposal: DndProposal<T>,
947
+ result: { value: DndValue<T> },
948
+ ) {
949
+ this.options.onChange(result.value, proposal);
950
+ }
951
+
952
+ protected changeFailed(session: ActiveSession<T>, error: unknown) {
953
+ if (this.session === session)
954
+ this.interrupt(session, 'change-handler-error', error);
955
+ else reportCallbackError(error);
956
+ }
957
+
958
+ private armTimer(session: ActiveSession<T>): void {
959
+ this.armDeadline(
960
+ session.deadline,
961
+ () => this.session === session,
962
+ () => this.settle(session, 'expired'),
963
+ );
964
+ }
965
+
966
+ private settle(
967
+ session: ActiveSession<T>,
968
+ response: 'accepted' | 'rejected' | 'expired' | 'mismatch',
969
+ ): void {
970
+ if (!session.proposal) return;
971
+ this.finish(session, {
972
+ ...this.event(session),
973
+ outcome: 'proposed',
974
+ proposal: session.proposal,
975
+ targetZoneId: session.proposal.to.zoneId,
976
+ response,
977
+ });
978
+ }
979
+
980
+ private interrupt(
981
+ session: ActiveSession<T>,
982
+ reason: DndCancelReason | 'response-missing' | 'change-handler-error',
983
+ error?: unknown,
984
+ ): void {
985
+ if (!session.proposal) return;
986
+ this.finish(session, {
987
+ ...this.event(session),
988
+ outcome: 'proposed',
989
+ proposal: session.proposal,
990
+ targetZoneId: session.proposal.to.zoneId,
991
+ response: 'interrupted',
992
+ reason,
993
+ ...(error === undefined ? {} : { error }),
994
+ });
995
+ }
996
+
997
+ protected cancelWith(
998
+ reason: DndCancelReason,
999
+ error?: unknown,
1000
+ policyReason?: string,
1001
+ ): void {
1002
+ const session = this.session;
1003
+ if (!session) return;
1004
+ if (session.proposal) {
1005
+ this.interrupt(session, reason, error);
1006
+ return;
1007
+ }
1008
+ this.finish(session, {
1009
+ ...this.event(session),
1010
+ outcome: 'cancelled',
1011
+ reason,
1012
+ ...(session.target ? { targetZoneId: session.target.zoneId } : {}),
1013
+ ...(error === undefined ? {} : { error }),
1014
+ ...(policyReason === undefined ? {} : { policyReason }),
1015
+ });
1016
+ }
1017
+
1018
+ cancel(reason: DndCancelReason): void {
1019
+ this.batch(() => this.cancelWith(reason));
1020
+ }
1021
+
1022
+ private finish(session: ActiveSession<T>, event: DndDragEndEvent<T>): void {
1023
+ if (this.session !== session) return;
1024
+ if (event.outcome === 'proposed') this.lifecycle('response', session);
1025
+ this.lifecycle('end', session);
1026
+ this.clearDeadline();
1027
+ // Clear before callbacks so reentrant end handlers can safely start a new drag.
1028
+ this.session = null;
1029
+ this.update({
1030
+ phase: 'idle',
1031
+ sessionId: null,
1032
+ itemId: null,
1033
+ sourceZoneId: null,
1034
+ targetZoneId: null,
1035
+ target: null,
1036
+ candidate: null,
1037
+ seq: 0,
1038
+ displayValue: this.lastValidValue,
1039
+ validity: this.snapshot.disabled ? 'invalid' : 'valid',
1040
+ gridItemLayouts: new Map(),
1041
+ failureReason: null,
1042
+ });
1043
+ try {
1044
+ this.options.onDragEnd?.(event);
1045
+ } catch (error) {
1046
+ reportCallbackError(error);
1047
+ }
1048
+ }
1049
+
1050
+ dispose(): void {
1051
+ this.batch(() => {
1052
+ this.disposed = true;
1053
+ this.clearDeadline();
1054
+ this.update({ disabled: true });
1055
+ this.cancelWith('unmounted');
1056
+ });
1057
+ }
1058
+ }