Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -252,6 +252,14 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 좁은 폭(730-2) — 오른쪽 목록 22rem 이 시트 칸을 폭 0 으로 짓누름 → 위(시트) · 아래(목록) 쌓기 */
|
||||
@media (max-width: 900px) {
|
||||
.m02-hopyo-sheet {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: minmax(0, 3fr) minmax(0, 2fr);
|
||||
}
|
||||
}
|
||||
|
||||
.m02-hopyo-sheet__grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄)
|
||||
* · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기
|
||||
* (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음)
|
||||
* · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 = 위로 · 아래로 · 별칭 · 삭제
|
||||
* · 머리 단추 = [로직 추가] [자재 추가] · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제
|
||||
* · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) ·
|
||||
* 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸)
|
||||
* · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈)
|
||||
@@ -155,6 +155,13 @@ export function mountLaborTable(
|
||||
const n = at(i);
|
||||
if (n !== null) openAt(n);
|
||||
},
|
||||
// 줄 끝 ⋯(731-1) — 우클릭 · 길게 누름이 없어도 같은 메뉴(위로 · 아래로 · 별칭 · 삭제)
|
||||
rowMenu: edit
|
||||
? {
|
||||
show: (i) => at(i) !== null,
|
||||
open: (i, ev) => openTreeMenu(ev, menuAt(at(i) ?? 0)),
|
||||
}
|
||||
: undefined,
|
||||
editable: (i, key) => {
|
||||
const n = at(i);
|
||||
if (key !== "qty" || n === null) return false;
|
||||
|
||||
@@ -96,6 +96,24 @@ console.log(
|
||||
total: { x: 3 },
|
||||
rowClass: (i) => (i === 0 ? "is-title" : null),
|
||||
}),
|
||||
rowMenu: m.renderDataTableHtml({
|
||||
columns: [
|
||||
{ key: "a", label: "A" },
|
||||
{ key: "b", label: "B" },
|
||||
],
|
||||
rows: [
|
||||
{ a: "제목", b: 1 },
|
||||
{ a: "줄", b: 2 },
|
||||
{ a: "줄2", b: 3 },
|
||||
],
|
||||
onRowClick: () => undefined,
|
||||
rowMenu: { show: (i) => i > 0, open: () => undefined },
|
||||
}),
|
||||
noRowMenu: m.renderDataTableHtml({
|
||||
columns: [{ key: "a", label: "A" }],
|
||||
rows: [{ a: "줄" }],
|
||||
onRowClick: () => undefined,
|
||||
}),
|
||||
scale: m.SCALE,
|
||||
widths: m.renderDataTableHtml({
|
||||
columns: [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"""730-2 지킴 — 엑셀 보기 창은 좁은 폭에서 시트(위) · 목록(아래)로 쌓임(오른쪽 22rem 이 시트를 폭 0 으로 짓누르지 않음)."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
CSS = (Path(__file__).parents[2] / "M02_MasterTemplete/M02_MasterTemplete_Hopyo.css").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_sheet_stacks_on_narrow_width():
|
||||
m = re.search(r"@media \(max-width: (\d+)px\) \{\s*\.m02-hopyo-sheet \{(.*?)\}", CSS, re.S)
|
||||
assert m and int(m.group(1)) >= 700
|
||||
assert "grid-template-columns: minmax(0, 1fr)" in m.group(2)
|
||||
assert "grid-template-rows" in m.group(2)
|
||||
@@ -122,3 +122,15 @@ def test_155_2_줄_꼴_곁값(r):
|
||||
assert rc.count("is-title") == 1 # 합계 줄엔 안 붙음
|
||||
assert "white-space-collapse: preserve-spaces;" in r["css"]
|
||||
assert "tr.is-title td" in r["css"]
|
||||
|
||||
|
||||
def test_줄_끝_메뉴_단추_731(r):
|
||||
"""rowMenu — show 참인 줄의 첫 칸에만 ⋯ 단추 · 없으면 단추 없음 · 줄 번호가 data-more 로."""
|
||||
rows = r["rowMenu"].split("<tbody>")[1].split("</tbody>")[0].split("</tr>")[:3]
|
||||
assert "data-more" not in rows[0]
|
||||
for i in (1, 2):
|
||||
assert rows[i].count("data-more") == 1
|
||||
assert f'data-more="{i}"' in rows[i]
|
||||
assert rows[i].split("</td>")[0].count("data-more") == 1 # 첫 칸 안
|
||||
assert 'class="ui-icon-btn ui-icon-btn--ghost ui-data-table__more"' in rows[1]
|
||||
assert "data-more" not in r["noRowMenu"]
|
||||
|
||||
@@ -330,3 +330,17 @@
|
||||
:is(.ui-data-table__icon, .ui-data-table__lamp)[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 줄 끝 ⋯(731-1) — 첫 칸 오른쪽 가운데에 얹음(줄 높이를 안 밀음) · 글은 단추 자리만큼 비워 줄임표 */
|
||||
.ui-data-table__table td:has(> .ui-data-table__more) {
|
||||
padding-right: calc(var(--ui-size-h, 28px) + var(--spacing-4));
|
||||
}
|
||||
.ui-data-table__table td:has(> .ui-data-table__more):not(.is-frozen) {
|
||||
position: relative; /* 고정 칸은 이미 sticky(자리 기준) */
|
||||
}
|
||||
.ui-data-table__table td > .ui-data-table__more {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: var(--spacing-4);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(PLAN 243-1 · keyEdit false = 표 밖이 맡음)
|
||||
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
|
||||
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
|
||||
* 줄 끝 메뉴(731-1) = rowMenu { show(줄), open(줄, 누름) } → show 참인 줄의 첫 칸 오른쪽에 ⋯ 단추(우클릭 · 길게 누름이 없는 손가락 · 마우스도 보이는 길) ·
|
||||
* 누르면 줄 누름 없이 open 만 부름(메뉴는 부르는 쪽이 엶) · 단추 꼴 = 아이콘 단추(28 · ghost) 그대로(DOM 없는 시험도 같은 글)
|
||||
* 칸 램프(557-2) = cellClass 가 `ui-data-table__lamp` → 칸 글 앞 점 8 · 노랑 = 없음 · `is-linked` = 초록 ✓(색만으로 안 가름) ·
|
||||
* `ui-data-table__lamp--only` = 글 숨기고 점만 가운데 · 대신하는 옛 변종 = B08 `b08-summary__lamp` · `b08-milestone__lamp` · B09 `b09-bill__lamp`
|
||||
* ========================================================================== */
|
||||
@@ -65,6 +67,8 @@ export interface DataTableOptions {
|
||||
onRowClick?: (index: number) => void;
|
||||
/** 칸 누름(본문 줄만) — 주면 줄에 `data-i`·`is-clickable` 이 붙음 · 줄 누름보다 먼저 부름 */
|
||||
onCellClick?: (index: number, key: string) => void;
|
||||
/** 줄 끝 ⋯ 메뉴(731-1) — show 참인 줄의 첫 칸에 ⋯ 단추 · 누르면 줄 누름 없이 open(줄 · 누름 이벤트) */
|
||||
rowMenu?: { show: (index: number) => boolean; open: (index: number, event: MouseEvent) => void };
|
||||
/** 강조할 줄(`focusRow` 가 정함) */
|
||||
focus?: number | null;
|
||||
/** 고칠 칸인지(본문 줄만 · 합계 제외) — 참이면 칸이 글 상자 */
|
||||
@@ -113,6 +117,9 @@ export const TREE_ICON = {
|
||||
};
|
||||
|
||||
const EMPTY = "-";
|
||||
/** ⋯ 단추(731-1) — 아이콘 단추(`createIconButton` icon "dots" · 28 · ghost)와 같은 글 · 표 글은 DOM 없이 만들어서 글로 둠 */
|
||||
const MORE_BUTTON = (i: number): string =>
|
||||
`<button type="button" class="ui-icon-btn ui-icon-btn--ghost ui-data-table__more" data-size="28" data-more="${i}" title="더 보기" aria-label="더 보기"><span class="ui-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/><path d="M19 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/></svg></span></button>`;
|
||||
const ESC: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """ };
|
||||
const esc = (s: string): string => s.replace(/[&<>"]/g, (c) => ESC[c]);
|
||||
|
||||
@@ -228,7 +235,8 @@ function rowHtml(
|
||||
lv?.key === key && lv.tree !== undefined
|
||||
? `<span class="ui-data-table__icon">${lv.node ? TREE_ICON.folder : TREE_ICON.leaf}</span>`
|
||||
: "";
|
||||
tr += `<td${attrs}>${guide}${icon}${esc(txt)}</td>`;
|
||||
const menu = j === 0 && more.menu && index !== null ? MORE_BUTTON(index) : "";
|
||||
tr += `<td${attrs}>${menu}${guide}${icon}${esc(txt)}</td>`;
|
||||
}
|
||||
const cls = [alt && "is-alt", index !== null && "is-clickable", focus && "is-focus", extra]
|
||||
.filter(Boolean)
|
||||
@@ -247,6 +255,8 @@ interface RowMore {
|
||||
spans?: number[];
|
||||
/** 고칠 수 있는 칸인지(editOnDouble · 글로 그린 칸에 `is-can-edit` 표시 · 258-2) */
|
||||
can?: (key: string) => boolean;
|
||||
/** 줄 끝 ⋯ 단추(731-1) — 첫 칸 앞에(오른쪽 맞춤 · 글 앞에 놓아야 한 줄에 앉음) */
|
||||
menu?: boolean;
|
||||
}
|
||||
|
||||
/** 칸 → 열 번호 — 병합 표는 칸이 빠져 cellIndex 가 밀리므로 data-c */
|
||||
@@ -363,6 +373,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
|
||||
...(o.level ? { lv: { key: o.level.key, n: lvN, node, tree: tree?.[i] } } : {}),
|
||||
...(spans ? { spans: spans[i - from] } : {}),
|
||||
...(o.editOnDouble && o.editable ? { can: (key: string) => o.editable!(i, key) } : {}),
|
||||
...(o.rowMenu?.show(i) ? { menu: true } : {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -492,6 +503,11 @@ export function createDataTable(opts: DataTableOptions): DataTable {
|
||||
let lastKey = "";
|
||||
let lastRow: number | null = null; // 마지막 누른 줄 — Enter · F2 입력 시작 자리
|
||||
root.addEventListener("click", (event) => {
|
||||
const more = (event.target as Element).closest<HTMLElement>("[data-more]");
|
||||
if (more && root.contains(more)) {
|
||||
state.rowMenu?.open(Number(more.dataset.more), event); // 줄 누름 없이 메뉴만(731-1)
|
||||
return;
|
||||
}
|
||||
const at = cellAt(event.target);
|
||||
if (!at) return;
|
||||
lastKey = at[1];
|
||||
|
||||
Reference in New Issue
Block a user