feat(device): 단축키로만 되던 일에 터치 단추 — 시트 입력줄 단추 줄(찾기 · 모두 같은 값 · 줄바꿈 · 고침 취소 · 오늘 · $ 돌리기) · B05 자유지정 [점 빼기] [끝] · B06 상세 · M02 나무 [여럿] · 공용 .ui-touch-only (기기대응 4-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -76,3 +76,8 @@ html:is([data-size="compact"], [data-size="medium"])
|
||||
> :first-child {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* 터치 모드에서만 보이는 단추(자판 없는 기기용 · 4-3) — 마우스 화면엔 없음 */
|
||||
html:not([data-touch="on"]) .ui-touch-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { parseA1, rangeToA1, toA1 } from "./spreadsheet_address";
|
||||
import { attachClipboard, pasteTableAt, type PasteCheck } from "./spreadsheet_clipboard";
|
||||
import { applyCommand, CommandError } from "./spreadsheet_commands";
|
||||
import { createEditor, slotOf } from "./spreadsheet_editor";
|
||||
import { mountTouchKeys } from "./spreadsheet_touch_keys";
|
||||
import { toCalcValue } from "./spreadsheet_eval";
|
||||
import { attachExtras } from "./spreadsheet_extras";
|
||||
import { createCalcEngine } from "./spreadsheet_graph";
|
||||
@@ -161,6 +162,7 @@ export function createSpreadsheet(
|
||||
const extras = attachExtras(ctx, ed, toolbar?.root ?? null);
|
||||
others.push(...extras.parts);
|
||||
const detachKeys = attachKeys(ctx, ed, extras.keys);
|
||||
mountTouchKeys(ctx, ed); // 자판 없는 기기 단추 줄(기기대응 4-3 · 터치 모드에서만 보임)
|
||||
const detachMouse = attachMouse(ctx, ed, extras.mouse);
|
||||
/** `addSheet` 가 나중에 붙인 탭 줄(탭 없이 띄운 시트) */
|
||||
let lateTabs: PartHandle | null = null;
|
||||
|
||||
@@ -300,3 +300,14 @@
|
||||
html[data-touch="on"] .ss-toolbar .ss-toolbar__more {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* 자판 없는 기기 단추 줄(기기대응 4-3 · spreadsheet_touch_keys.ts) — 터치 모드에서만 */
|
||||
.ss-touch-keys {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-touch="on"] .ss-touch-keys {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_touch_keys.ts
|
||||
* 자판 없는 기기용 단추 줄(기기대응 4-3) — 단축키로만 되던 일을 같은 키 길(`attachKeys`)로 보냄.
|
||||
* 입력줄(`ed.bar`) 끝에 붙고 터치 모드에서만 보임(spreadsheet_toolbar.css). 마우스 화면은 그대로.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import type { Editor } from "./spreadsheet_editor";
|
||||
import type { SpreadsheetContext } from "./spreadsheet_view_types";
|
||||
|
||||
const KEYS: [label: string, title: string, init: KeyboardEventInit, edit: boolean][] = [
|
||||
["🔍", "찾기 · 바꾸기(Ctrl+F)", { key: "f", code: "KeyF", ctrlKey: true }, false],
|
||||
["⇉", "고른 칸 모두 같은 값(Ctrl+Enter)", { key: "Enter", ctrlKey: true }, true],
|
||||
["↵", "칸 안 줄바꿈(Alt+Enter)", { key: "Enter", altKey: true }, true],
|
||||
["✕", "고침 취소(Esc)", { key: "Escape" }, true],
|
||||
["📅", "오늘 날짜(Ctrl+;)", { key: ";", code: "Semicolon", ctrlKey: true }, true],
|
||||
["$", "참조 $ 돌리기(F4)", { key: "F4" }, true],
|
||||
];
|
||||
|
||||
export function mountTouchKeys(ctx: SpreadsheetContext, ed: Editor): void {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ss-touch-keys";
|
||||
for (const [label, title, init, edit] of KEYS) {
|
||||
if (edit && ctx.readOnly) continue;
|
||||
const b = createButton({
|
||||
label,
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
// 고치는 중이면 그 칸(입력줄 · 칸 편집기) — 아니면 칸 편집기로
|
||||
const target = document.activeElement === ed.formula ? ed.formula : ed.ta;
|
||||
target.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { ...init, bubbles: true, cancelable: true }),
|
||||
);
|
||||
},
|
||||
});
|
||||
b.title = title;
|
||||
b.setAttribute("aria-label", title);
|
||||
// 누를 때 초점을 뺏지 않음 — 고치던 칸이 그대로(손가락은 호환 mousedown 이 옴)
|
||||
b.addEventListener("mousedown", (e) => e.preventDefault());
|
||||
row.append(b);
|
||||
}
|
||||
ed.bar.append(row);
|
||||
}
|
||||
@@ -205,7 +205,14 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
const hint = document.createElement("em");
|
||||
hint.className = "b05-route-profile__tool-hint";
|
||||
hint.textContent = callbacks.freeHint();
|
||||
wrap.append(hint);
|
||||
// 자판 없는 기기 — Backspace · Enter 대신(기기대응 4-3 · 같은 키 길 · 터치 모드에서만 보임)
|
||||
const press = (key: string) => () =>
|
||||
document.body.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
|
||||
const back = toolButton("점 빼기", "마지막 점 빼기 (Backspace)", press("Backspace"));
|
||||
const end = toolButton("끝", "자유지정 끝 (Enter)", press("Enter"));
|
||||
back.classList.add("ui-touch-only");
|
||||
end.classList.add("ui-touch-only");
|
||||
wrap.append(back, end, hint);
|
||||
} else if (mode === "straighten" && pending !== null) {
|
||||
const hint = document.createElement("em");
|
||||
hint.className = "b05-route-profile__tool-hint";
|
||||
|
||||
@@ -352,6 +352,21 @@ export function attachDetailSelect(input: {
|
||||
input.commit();
|
||||
});
|
||||
|
||||
// 자판 없는 기기 — Shift · Ctrl 누르고 고르기 대신 「여럿」 켜고 톡(기기대응 4-3 · 터치 모드에서만 보임)
|
||||
let multi = false;
|
||||
const multiBtn = document.createElement("button");
|
||||
multiBtn.type = "button";
|
||||
multiBtn.className = "b06-cross-card__zoom-btn ui-touch-only";
|
||||
multiBtn.textContent = "여럿";
|
||||
multiBtn.title = "선 여럿 고르기 (Shift 누르고 누르기와 같음)";
|
||||
multiBtn.setAttribute("aria-pressed", "false");
|
||||
multiBtn.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
multi = !multi;
|
||||
multiBtn.setAttribute("aria-pressed", String(multi));
|
||||
multiBtn.classList.toggle("is-on", multi);
|
||||
});
|
||||
|
||||
const show = (): void => {
|
||||
const chosen = selected.map((i) => pieces[i]);
|
||||
hits.forEach((hit, i) => hit.classList.toggle("is-selected", selected.includes(i)));
|
||||
@@ -373,7 +388,8 @@ export function attachDetailSelect(input: {
|
||||
put.hidden = !mode || !selectionSource(mode, chosen);
|
||||
// [+ − ⤢] 왼쪽(카드 꼴) — 그림을 다시 그리면 막대가 새로 생김
|
||||
const bar = cad.querySelector(".b06-cross-card__zoom");
|
||||
if (bar && label.parentElement !== bar) bar.prepend(label, put);
|
||||
if (bar && label.parentElement !== bar) bar.prepend(label, put, multiBtn);
|
||||
multiBtn.hidden = !mode;
|
||||
};
|
||||
|
||||
/** 누른 자리(그림 안만) → 선 · 영역 — 판정 선 굵기 대신 거리로(261-3). */
|
||||
@@ -388,7 +404,7 @@ export function attachDetailSelect(input: {
|
||||
|
||||
const onClick = (event: MouseEvent): void => {
|
||||
const pick = pickOf(event);
|
||||
const shift = event.shiftKey || event.ctrlKey || event.metaKey;
|
||||
const shift = event.shiftKey || event.ctrlKey || event.metaKey || multi;
|
||||
const additive = shift || mode === "area";
|
||||
noteText = null;
|
||||
if (!pick || !("piece" in pick)) {
|
||||
|
||||
@@ -148,7 +148,21 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
add.hidden = !opt.canEdit;
|
||||
// 「N개 고름」 · 「+」 — 층 단추 줄 오른쪽 끝(30-2 · 부르는 쪽이 붙임)
|
||||
const status = el("span", { className: "m02-tree__picked", attrs: { hidden: "" } });
|
||||
const tools = el("span", { className: "m02-tree__tools", children: [status, add] });
|
||||
// 자판 없는 기기 — Ctrl 누르고 고르기 대신 「여럿」 켜고 톡(기기대응 4-3 · 터치 모드에서만 보임)
|
||||
let multi = false;
|
||||
const multiBtn = createButton({
|
||||
label: "여럿",
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
multi = !multi;
|
||||
multiBtn.classList.toggle("is-on", multi);
|
||||
multiBtn.setAttribute("aria-pressed", String(multi));
|
||||
},
|
||||
});
|
||||
multiBtn.classList.add("ui-touch-only");
|
||||
multiBtn.title = "여럿 고르기 (Ctrl 누르고 누르기와 같음)";
|
||||
multiBtn.setAttribute("aria-pressed", "false");
|
||||
const tools = el("span", { className: "m02-tree__tools", children: [status, multiBtn, add] });
|
||||
|
||||
/* --- 그리기 --- */
|
||||
const canCopy = !!api.paste;
|
||||
@@ -275,7 +289,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
picked,
|
||||
anchor,
|
||||
one.id,
|
||||
{ ctrl: event.ctrlKey || event.metaKey, shift: event.shiftKey },
|
||||
{ ctrl: event.ctrlKey || event.metaKey || multi, shift: event.shiftKey },
|
||||
visibleIds(),
|
||||
);
|
||||
if (got.handled) event.stopImmediatePropagation();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
sub1 화면 서버(5174)에 견본 통합문서를 붙여 CDP 터치를 넣음:
|
||||
① 톡 = 칸 고름 · 화면 자판 안 뜸(inputMode none) ② 고른 칸에서 끌기 = 범위
|
||||
③ 안 고른 칸에서 끌기 = 고름 그대로(화면 밀기) ④ 두 번 톡 = 고침 · 자판 뜸(inputMode text)
|
||||
⑤ ⋯ 단추(터치 모드) = 칸 메뉴
|
||||
⑤ ⋯ 단추(터치 모드) = 칸 메뉴 ⑥ 자판 없는 단추 줄 · 🔍 = 찾기
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
@@ -75,6 +75,23 @@ def test_spreadsheet_touch_select_range_edit():
|
||||
_touch(cdp, "touchEnd", [])
|
||||
page.wait_for_timeout(450)
|
||||
|
||||
# ⑥ 자판 없는 단추 줄(4-3) — 6개 보임 · 🔍 = 찾기 칸 열림
|
||||
n = page.evaluate(
|
||||
"[...document.querySelectorAll('.ss-touch-keys > button')].filter(b => b.getClientRects().length).length"
|
||||
)
|
||||
assert n == 6, n
|
||||
page.evaluate("document.querySelector('.ss-touch-keys > button').click()")
|
||||
page.wait_for_function( # 찾기 칸은 늦게 불러옴(크기 예산)
|
||||
"() => [...document.querySelectorAll('.ss-find__input')].some(e => e.getClientRects().length)",
|
||||
timeout=10000,
|
||||
)
|
||||
page.keyboard.press("Escape")
|
||||
page.evaluate("document.activeElement.blur()")
|
||||
gx, gy, gw, gh = page.evaluate( # 찾기 칸이 자리를 먹어 격자가 옮겨졌을 수 있음
|
||||
"(() => { const g = document.querySelector('.aislo-grid').getBoundingClientRect();"
|
||||
" return [g.left, g.top, g.width, g.height]; })()"
|
||||
)
|
||||
|
||||
a = (gx + 90, gy + 60)
|
||||
tap(*a) # ① 고름
|
||||
first = sel()
|
||||
@@ -105,4 +122,5 @@ def test_spreadsheet_touch_select_range_edit():
|
||||
assert page.evaluate(
|
||||
"[...document.querySelectorAll('.ui-ctx-menu')].some(m => !m.hidden && m.getClientRects().length)"
|
||||
)
|
||||
|
||||
browser.close()
|
||||
|
||||
Reference in New Issue
Block a user