- 인력 · 기계 · 재료 · 단가산출 줄 자원 이름 = 단추(이름 · 규격) → 찾아 넣기 창 → 바꿈 · [찾아 바꾸기] 걷음 - 항목 머리 = 띠(바탕 · 왼쪽 굵은 띠 · 번호) · 줄 = 들여쓰기 + 왼쪽 세로 선 · 가나다 한 단 더 · 접기 - 순서 = 손잡이 끌기(줄 = 항목 안 · 사이 · 항목 끌기 · 자리 선 표시) · [↑][↓] 걷음 - 지움 = X 아이콘 · 확인 없이 지우고 알림 눌러 되돌리기(그 뒤 고침 있으면 못 함) - 산출근거 율 줄 「번지.복사N.노무비」 → 그 조각 이름 · 값 식 = 번지 값 · 합계 덧셈 항 넷 넘으면 항목 소계끼리 - 알림 부품: 누름 동작 알림은 같은 문구 버림 안 함(두 번째 지움 되돌리기 안 뜨던 흠) - 글 · 근거 카드 높이 늘던 흠 고침 · 시험 test_804_4 끌어옮김 · 번지 이름 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.5 KiB
TypeScript
81 lines
3.5 KiB
TypeScript
/* =============================================================================
|
|
* line_stack_drag.ts
|
|
* 줄 쌓기 왼쪽 끌어 옮기기(PLAN 804-5 · 요구 682-4) — 손잡이(⋮⋮)를 누르고 끌면 놓을 자리에 선 표시 · 놓으면 `drop`
|
|
* 줄 = 항목 안 · 항목 사이 어디든(빈 항목 = 그 안) → `k.p`(항목 k 의 p 자리) · 항목 = 항목 사이 → `p`
|
|
* 포인터 끌기(마우스 · 터치 같음) · Esc = 그만 · 자리 표시 = 클래스(`data-drop`)만 — 끄는 동안 배치 안 바뀜
|
|
* ========================================================================== */
|
|
|
|
export interface DragOpts {
|
|
/** 끄는 것 — 줄 카드 · 항목 상자 */
|
|
what: "line" | "item";
|
|
/** 끌기 영역(`.lstk-edit`) */
|
|
root: HTMLElement;
|
|
/** 끄는 요소(반투명) */
|
|
self: HTMLElement;
|
|
/** 놓음 — line `k.p` · item `p` */
|
|
drop: (to: string) => void;
|
|
}
|
|
|
|
const mid = (r: DOMRect): number => r.top + r.height / 2;
|
|
|
|
/** 포인터 y 의 놓을 자리 + 표시할 요소 · 꼴 */
|
|
function spotOf(o: DragOpts, y: number): { to: string; at: HTMLElement; side: string } | null {
|
|
const boxes = [...o.root.querySelectorAll<HTMLElement>(":scope > .lstk-item")];
|
|
if (!boxes.length) return null;
|
|
if (o.what === "item") {
|
|
const k = boxes.findIndex((b) => y < mid(b.getBoundingClientRect()));
|
|
return k < 0
|
|
? { to: String(boxes.length), at: boxes[boxes.length - 1], side: "after" }
|
|
: { to: String(k), at: boxes[k], side: "before" };
|
|
}
|
|
// 줄 — 포인터가 든 항목(위 · 아래 밖 = 처음 · 끝 항목)
|
|
let k = boxes.findIndex((b) => y < b.getBoundingClientRect().bottom);
|
|
if (k < 0) k = boxes.length - 1;
|
|
const cards = [...boxes[k].querySelectorAll<HTMLElement>(":scope .lstk-lines > .lstk-line")];
|
|
if (!cards.length) return { to: `${k}.0`, at: boxes[k], side: "in" };
|
|
const p = cards.findIndex((c) => y < mid(c.getBoundingClientRect()));
|
|
return p < 0
|
|
? { to: `${k}.${cards.length}`, at: cards[cards.length - 1], side: "after" }
|
|
: { to: `${k}.${p}`, at: cards[p], side: "before" };
|
|
}
|
|
|
|
/** 손잡이에 끌기를 붙임 */
|
|
export function attachDrag(grip: HTMLElement, o: DragOpts): void {
|
|
grip.addEventListener("pointerdown", (e) => {
|
|
if (e.button !== 0) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
grip.setPointerCapture(e.pointerId);
|
|
let spot: ReturnType<typeof spotOf> = null;
|
|
const clear = (): void => {
|
|
for (const x of o.root.querySelectorAll("[data-drop]")) x.removeAttribute("data-drop");
|
|
};
|
|
o.self.classList.add("is-dragging");
|
|
o.root.dataset.dragging = o.what;
|
|
const move = (ev: PointerEvent): void => {
|
|
clear();
|
|
spot = spotOf(o, ev.clientY);
|
|
if (spot) spot.at.dataset.drop = spot.side;
|
|
};
|
|
const end = (ok: boolean): void => {
|
|
grip.removeEventListener("pointermove", move);
|
|
grip.removeEventListener("pointerup", up);
|
|
grip.removeEventListener("pointercancel", cancel);
|
|
window.removeEventListener("keydown", esc);
|
|
clear();
|
|
o.self.classList.remove("is-dragging");
|
|
delete o.root.dataset.dragging;
|
|
if (ok && spot) o.drop(spot.to);
|
|
};
|
|
const up = (): void => end(true);
|
|
const cancel = (): void => end(false);
|
|
const esc = (ev: KeyboardEvent): void => {
|
|
if (ev.key === "Escape") end(false);
|
|
};
|
|
grip.addEventListener("pointermove", move);
|
|
grip.addEventListener("pointerup", up);
|
|
grip.addEventListener("pointercancel", cancel);
|
|
window.addEventListener("keydown", esc);
|
|
});
|
|
}
|