createSortableScope · Core
공유 드래그 영역의 등록·옵션 갱신·취소·해제와 소비자 상태 반영 계약.
import { createSortableScope, type SortableScope } from "comins-sortable/core";export declare function createSortableScope(options?: SortableScopeOptions): SortableScope;options는 생략할 수 있으며 기본값은 빈 객체입니다. 생성만으로 DOM 영역이 생기지 않습니다. browser element를 registerArea에 등록할 때 환경에 연결됩니다. 이 페이지의 진입점은 /core입니다. Svelte의 같은 이름 API는 Svelte 가이드에서 별도로 설명합니다.
Core는 드래그 결과를 알리는 낮은 수준의 계약입니다. 프레임워크의 controlled 상태 반영은 각 어댑터를 사용하고, DOM 순서를 자동 반영하는 일반 JS 사용은 createSortable을 사용합니다. 아래 Core 예제는 소비자 책임을 보여주기 위해 제안된 단일 목록의 순서를 직접 DOM에 반영합니다.
SortableScopeOptions
섹션 제목: “SortableScopeOptions”export interface SortableScopeOptions { onBeforeDragStart?: (context: DragContext) => boolean | void; onDragStart?: (context: DragContext) => void; onDrag?: (context: DragContext) => void; onInsertDragArea?: (event: InsertDragAreaEvent) => void; onChange?: (change: SortableChange) => void; onAfterDrag?: (result: AfterDragResult) => void; onError?: (error: unknown) => void;}| 콜백 | 시점과 반환 계약 |
|---|---|
onBeforeDragStart |
활성화 전 호출합니다. false를 반환하면 시작을 거부합니다. 이 경로는 활성 드래그의 afterDrag 주기에 들어가지 않습니다. |
onDragStart |
활성 드래그가 시작되면 DragContext를 전달합니다. 반환값은 상태 변경 지시가 아닙니다. |
onDrag |
드래그 중 포인터·목적지 정보가 포함된 context를 전달합니다. |
onInsertDragArea |
드래그 대상의 삽입 영역 변화 이벤트를 전달합니다. |
onChange |
제안된 재정렬·이동·복사·swap 결과를 전달합니다. Core 소비자는 DOM 또는 제어 상태에 반영해야 합니다. |
onAfterDrag |
정리 후 status, reason, 필요 시 change를 전달합니다. dropped인지 확인하여 완료를 판정합니다. |
onError |
callback·세션 처리 중 보고되는 오류를 받습니다. 동기 등록/옵션 오류까지 모두 이 콜백으로 오는 것은 아닙니다. |
모든 콜백은 선택 사항입니다. onBeforeDragStart를 제외한 콜백 반환형은 void입니다. onChange 호출만으로 성공을 확정하지 마십시오. 제어 상태나 DOM이 제안을 반영하지 않으면 state-not-committed로 거부될 수 있습니다.
SortableScope
섹션 제목: “SortableScope”registerArea
섹션 제목: “registerArea”registerArea(element: Element, options: SortableAreaOptions): () => void;DOM Element와 영역 옵션을 등록하고 그 영역만 해제하는 함수를 반환합니다. Core는 CSS selector 문자열을 받지 않습니다. areaId는 scope 내에서 유일해야 하고 각 항목의 ID는 안정적이어야 합니다. 반환한 해제 함수는 중복 호출할 수 있으며, 진행 중인 드래그의 출발지·목적지를 해제하면 드래그가 종료됩니다.
updateArea
섹션 제목: “updateArea”updateArea(areaId: string, patch: SortableAreaPatch): void;등록된 영역의 옵션을 부분 변경합니다. areaId는 변경할 수 없습니다. 값 생략은 기존 설정 유지이며 undefined를 명시한 옵션은 정규화 규칙을 따릅니다. 현재 드래그의 출발지·목적지를 disabled로 바꾸면 해당 드래그를 종료합니다. 반환값 void.
refreshArea
섹션 제목: “refreshArea”refreshArea?(areaId: string): void;영역의 DOM 변경 후 geometry 캐시를 무효화하고 설정된 레이아웃 애니메이션을 실행합니다. 데이터 배열을 정렬하거나 새 영역을 등록하지 않습니다. 공개 SortableScope 타입에서는 optional이므로 scope.refreshArea?.(id)로 호출합니다. 반환값 void.
cancel
섹션 제목: “cancel”cancel(): void;현재 드래그 또는 활성화 대기를 취소합니다. 활성 드래그의 종료 사유는 escape입니다. 진행 중인 작업이 없으면 동작하지 않으며 등록 영역은 유지합니다. 반환값 void.
destroy
섹션 제목: “destroy”destroy(): void;소유한 scope 전체를 해제합니다. 드래그, 영역 listener, frame, 애니메이션과 임시 피드백을 정리합니다. 중복 호출은 허용합니다. 해제된 scope에 다시 등록하지 말고 새 scope를 생성하십시오. 반환값 void.
등록 옵션과 오류
섹션 제목: “등록 옵션과 오류”registerArea의 필수 옵션은 areaId와 item selector입니다. item과 일치하는 영역의 직접 자식이 정렬 대상입니다. 기본 getItemId는 data-sortable-id를 읽습니다. direction 기본값은 auto, activationDistance는 4, emptyInsertThreshold는 8입니다. 추가 정책은 Core 개념과 고급 정렬을 참고하십시오.
| 상황 | 결과 |
|---|---|
최초 등록 전 updateArea / refreshArea |
SortableError, INVALID_ELEMENT |
| 존재하지 않거나 해제한 area ID 갱신 | INVALID_ELEMENT |
| 해제한 scope에 새 영역 등록 | INVALID_OPTION |
| 잘못된 옵션 / selector | 옵션 검증 오류. 동기 호출 지점에서 처리합니다. |
컴포넌트·화면 해제 시 자신이 생성한 scope를 destroy()하고, 공유 scope에서 영역만 소유하면 registerArea가 반환한 함수로 해당 영역만 해제합니다. 자세한 종료 상태는 생명주기와 오류를 참고하십시오.
실행 예제
섹션 제목: “실행 예제”import { useEffect, useRef, useState } from 'react';import { createSortable, type Sortable } from 'comins-sortable';import { createSortableScope, type SortableScope, type SortableScopeOptions } from 'comins-sortable/core';import 'comins-sortable/styles.css';
export default function SortableLifecycleDemo({ lang = 'ko', mode = 'vanilla' }: { lang?: string; mode?: 'vanilla' | 'core' }) { const ko = lang === 'ko'; const host = useRef<HTMLDivElement>(null); const instance = useRef<Sortable | SortableScope | null>(null); const [generation, setGeneration] = useState(0); const [disabled, setDisabled] = useState(false); const [status, setStatus] = useState('');
useEffect(() => { const element = host.current; if (!element) return; // React owns the host. Its children are owned by this imperative example. const list = document.createElement('div'); list.className = 'task-list'; list.setAttribute('role', 'list'); for (const id of ['alpha', 'beta', 'gamma']) { const item = document.createElement('div'); item.dataset.sortableId = id; item.className = 'task transfer-item'; item.setAttribute('role', 'listitem'); item.textContent = id; list.append(item); } element.replaceChildren(list); const options: SortableScopeOptions = { onChange: change => { if (mode === 'core') { // Core reports the proposed order; the consumer must commit it. const order = change.orders.find(order => order.areaId === 'tasks'); const items = new Map(Array.from(list.children, item => [item.getAttribute('data-sortable-id'), item])); for (const id of order?.itemIds ?? []) { const item = items.get(String(id)); if (item) list.append(item); } } }, onAfterDrag: result => setStatus(`${result.status}: ${result.reason}`), onError: error => setStatus(error instanceof Error ? error.message : String(error)), }; const area = { areaId: 'tasks', item: '.transfer-item', direction: 'vertical' as const }; let unregister: (() => void) | undefined; const sortable = mode === 'core' ? createSortableScope(options) : createSortable(list, { ...area, ...options }); if (mode === 'core') unregister = sortable.registerArea(list, area); instance.current = sortable; setDisabled(false); setStatus(ko ? '준비됨. 항목을 드래그하십시오.' : 'Ready. Drag an item.'); return () => { unregister?.(); sortable.destroy(); instance.current = null; element.replaceChildren(); }; }, [generation, ko, mode]);
return <div className="demo" data-demo={`sortable-lifecycle-${mode}`}> <div className="demo-toolbar"> <button disabled={!instance.current} onClick={() => { instance.current?.updateArea('tasks', { disabled: !disabled }); setDisabled(!disabled); setStatus(!disabled ? 'disabled' : 'enabled'); }}>{disabled ? (ko ? '정렬 허용' : 'Enable sorting') : (ko ? '정렬 비활성화' : 'Disable sorting')}</button> <button disabled={!instance.current} onClick={() => { instance.current?.refreshArea?.('tasks'); setStatus('refreshArea'); }}>refreshArea</button> <button disabled={!instance.current} onClick={() => { instance.current?.cancel(); setStatus('cancel'); }}>cancel</button> <button disabled={!instance.current} onClick={() => { instance.current?.destroy(); instance.current = null; setStatus('destroyed'); }}>destroy</button> <button onClick={() => setGeneration(value => value + 1)}>{ko ? '새 인스턴스' : 'New instance'}</button> </div> <div ref={host} /> <p role="status">{status}</p> </div>;}아래 npm 선언과 고정 소스 snapshot을 대조했습니다. snapshot을 npm 게시 커밋으로 단정하지 않습니다.
comins-sortable/dist/core/scope.d.tscomins-sortable/dist/core/model.d.ts- npm 0.1.6 · Source snapshot