@simplysm/angular 14.2.1 → 14.2.3
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.
- package/dist/controls/button/sd-anchor.d.ts +1 -1
- package/dist/controls/button/sd-anchor.d.ts.map +1 -1
- package/dist/controls/button/sd-anchor.js +2 -2
- package/dist/controls/button/sd-button.d.ts +1 -1
- package/dist/controls/button/sd-button.d.ts.map +1 -1
- package/dist/controls/button/sd-button.js +2 -2
- package/dist/controls/checkbox/sd-checkbox.d.ts +1 -1
- package/dist/controls/checkbox/sd-checkbox.d.ts.map +1 -1
- package/dist/controls/checkbox/sd-checkbox.js +2 -2
- package/dist/controls/checkbox/sd-switch.d.ts +1 -1
- package/dist/controls/checkbox/sd-switch.d.ts.map +1 -1
- package/dist/controls/checkbox/sd-switch.js +2 -2
- package/dist/controls/dropdown/sd-dropdown.d.ts +3 -0
- package/dist/controls/dropdown/sd-dropdown.d.ts.map +1 -1
- package/dist/controls/dropdown/sd-dropdown.js +29 -9
- package/dist/controls/input/sd-textarea.d.ts +1 -1
- package/dist/controls/input/sd-textarea.d.ts.map +1 -1
- package/dist/controls/input/sd-textarea.js +2 -2
- package/dist/controls/input/sd-textfield-type-handlers.js +1 -1
- package/dist/controls/input/sd-textfield.d.ts +1 -1
- package/dist/controls/input/sd-textfield.d.ts.map +1 -1
- package/dist/controls/input/sd-textfield.js +9 -10
- package/dist/controls/list/sd-list.d.ts.map +1 -1
- package/dist/controls/list/sd-list.js +4 -6
- package/dist/controls/select/sd-select-button.d.ts.map +1 -1
- package/dist/controls/select/sd-select-button.js +3 -5
- package/dist/controls/select/sd-select.js +2 -2
- package/dist/core/error-handler/sd-global-error-handler.plugin.js +1 -1
- package/dist/core/modal/sd-modal.provider.js +1 -1
- package/dist/core/provideSdAngular.d.ts.map +1 -1
- package/dist/core/provideSdAngular.js +1 -13
- package/dist/core/setupBgTheme.d.ts +1 -1
- package/dist/core/setupBgTheme.d.ts.map +1 -1
- package/dist/core/toast/sd-toast.js +2 -2
- package/dist/core/toast/sd-toast.provider.d.ts +1 -1
- package/dist/core/toast/sd-toast.provider.d.ts.map +1 -1
- package/dist/core/toast/sd-toast.provider.js +1 -1
- package/dist/data/crud/sd-base-container.d.ts +1 -1
- package/dist/data/crud/sd-base-container.d.ts.map +1 -1
- package/dist/data/crud/sd-base-container.js +2 -2
- package/dist/data/crud/sd-crud-detail.d.ts +1 -1
- package/dist/data/crud/sd-crud-detail.d.ts.map +1 -1
- package/dist/data/crud/sd-crud-list.d.ts +3 -3
- package/dist/data/crud/sd-crud-list.d.ts.map +1 -1
- package/dist/data/shared-data/sd-shared-data-select.d.ts +2 -2
- package/dist/data/shared-data/sd-shared-data-select.d.ts.map +1 -1
- package/dist/data/sheet/injectSheetDomAccessor.d.ts +1 -0
- package/dist/data/sheet/injectSheetDomAccessor.d.ts.map +1 -1
- package/dist/data/sheet/injectSheetDomAccessor.js +7 -0
- package/dist/data/sheet/injectSheetDragSelection.d.ts +18 -0
- package/dist/data/sheet/injectSheetDragSelection.d.ts.map +1 -0
- package/dist/data/sheet/injectSheetDragSelection.js +193 -0
- package/dist/data/sheet/sd-sheet.d.ts +3 -0
- package/dist/data/sheet/sd-sheet.d.ts.map +1 -1
- package/dist/data/sheet/sd-sheet.js +17 -6
- package/dist/data/state-preset/sd-state-preset.d.ts +2 -2
- package/dist/data/state-preset/sd-state-preset.d.ts.map +1 -1
- package/dist/features/editor/sd-tiptap-editor.js +2 -2
- package/dist/features/theme/sd-theme-provider.d.ts +0 -2
- package/dist/features/theme/sd-theme-provider.d.ts.map +1 -1
- package/dist/features/theme/sd-theme-provider.js +0 -5
- package/dist/features/theme/sd-theme-selector.d.ts +1 -1
- package/dist/features/theme/sd-theme-selector.d.ts.map +1 -1
- package/dist/features/theme/sd-theme-selector.js +5 -22
- package/dist/features/visual/sd-label.d.ts +1 -1
- package/dist/features/visual/sd-label.d.ts.map +1 -1
- package/dist/features/visual/sd-label.js +2 -2
- package/dist/features/visual/sd-note.d.ts +1 -1
- package/dist/features/visual/sd-note.d.ts.map +1 -1
- package/dist/features/visual/sd-note.js +2 -2
- package/dist/features/visual/sd-progress.d.ts +1 -1
- package/dist/features/visual/sd-progress.d.ts.map +1 -1
- package/dist/features/visual/sd-progress.js +2 -2
- package/dist/layout/sidebar/sd-sidebar.d.ts.map +1 -1
- package/dist/layout/sidebar/sd-sidebar.js +3 -5
- package/dist/layout/topbar/sd-topbar.d.ts.map +1 -1
- package/dist/layout/topbar/sd-topbar.js +3 -3
- package/package.json +26 -26
- package/scss/commons/_mixins.scss +1 -1
- package/scss/commons/_theme-variables.scss +0 -8
- package/scss/commons/_variables.scss +114 -121
- package/scss/themes/_variables-blueprint.scss +65 -63
- package/scss/themes/_variables-ide-dark.scss +166 -144
- package/src/controls/button/sd-anchor.ts +3 -3
- package/src/controls/button/sd-button.ts +1 -3
- package/src/controls/checkbox/sd-checkbox.ts +2 -10
- package/src/controls/checkbox/sd-switch.ts +2 -4
- package/src/controls/dropdown/sd-dropdown.ts +33 -9
- package/src/controls/input/sd-textarea.ts +2 -4
- package/src/controls/input/sd-textfield-type-handlers.ts +1 -1
- package/src/controls/input/sd-textfield.ts +9 -12
- package/src/controls/list/sd-list.ts +3 -5
- package/src/controls/select/sd-select-button.ts +2 -4
- package/src/controls/select/sd-select.ts +1 -1
- package/src/core/error-handler/sd-global-error-handler.plugin.ts +1 -1
- package/src/core/modal/sd-modal.provider.ts +1 -1
- package/src/core/provideSdAngular.ts +1 -15
- package/src/core/setupBgTheme.ts +1 -2
- package/src/core/toast/sd-toast.provider.ts +5 -3
- package/src/data/crud/sd-base-container.ts +1 -1
- package/src/data/sheet/injectSheetDomAccessor.ts +7 -0
- package/src/data/sheet/injectSheetDragSelection.ts +226 -0
- package/src/data/sheet/sd-sheet.ts +16 -3
- package/src/features/editor/sd-tiptap-editor.ts +1 -1
- package/src/features/theme/sd-theme-provider.ts +0 -9
- package/src/features/theme/sd-theme-selector.ts +1 -10
- package/src/features/visual/sd-label.ts +1 -3
- package/src/features/visual/sd-note.ts +1 -3
- package/src/features/visual/sd-progress.ts +2 -2
- package/src/layout/sidebar/sd-sidebar.ts +2 -3
- package/src/layout/topbar/sd-topbar.ts +1 -0
- package/styles.css +278 -567
- package/scss/commons/_colors.scss +0 -301
- package/scss/commons/_density.scss +0 -23
- package/scss/sd-tokens.md +0 -132
|
@@ -12,16 +12,14 @@ import {
|
|
|
12
12
|
encapsulation: ViewEncapsulation.None,
|
|
13
13
|
standalone: true,
|
|
14
14
|
imports: [],
|
|
15
|
-
template: `
|
|
16
|
-
<ng-content />
|
|
17
|
-
`,
|
|
15
|
+
template: ` <ng-content /> `,
|
|
18
16
|
styles: [
|
|
19
17
|
/* language=SCSS */ `
|
|
20
18
|
sd-list {
|
|
21
19
|
display: flex;
|
|
22
20
|
flex-direction: column;
|
|
23
21
|
user-select: none;
|
|
24
|
-
background-color: var(--sd-bg-
|
|
22
|
+
background-color: var(--sd-bg-content);
|
|
25
23
|
border: 1px solid var(--sd-bd-strong);
|
|
26
24
|
border-radius: var(--sd-radius-default);
|
|
27
25
|
padding: var(--sd-gap-sm);
|
|
@@ -36,7 +34,7 @@ import {
|
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
// 아이템 내부에 중첩된 리스트는 카드 외형(
|
|
37
|
+
// 아이템 내부에 중첩된 리스트는 카드 외형(테두리, 라운드, 배경)을 제거
|
|
40
38
|
sd-list-item sd-list {
|
|
41
39
|
border-color: transparent;
|
|
42
40
|
border-radius: 0;
|
|
@@ -7,14 +7,12 @@ import { setupRipple } from "../../core/ripple/setupRipple";
|
|
|
7
7
|
encapsulation: ViewEncapsulation.None,
|
|
8
8
|
standalone: true,
|
|
9
9
|
imports: [],
|
|
10
|
-
template: `
|
|
11
|
-
<ng-content />
|
|
12
|
-
`,
|
|
10
|
+
template: ` <ng-content /> `,
|
|
13
11
|
styles: [
|
|
14
12
|
/* language=SCSS */ `
|
|
15
13
|
sd-select-button {
|
|
16
14
|
display: block;
|
|
17
|
-
background-color: var(--sd-bg-
|
|
15
|
+
background-color: var(--sd-bg-content);
|
|
18
16
|
font-weight: bold;
|
|
19
17
|
cursor: pointer;
|
|
20
18
|
color: var(--sd-tx-primary);
|
|
@@ -231,7 +231,7 @@ export type SelectModeValue<T> = {
|
|
|
231
231
|
|
|
232
232
|
// inset(시트 셀 등)의 disabled 는 일반 콘텐츠처럼 표시(현행 유지)
|
|
233
233
|
&[data-sd-disabled="true"] > sd-dropdown {
|
|
234
|
-
background-color: var(--sd-bg-
|
|
234
|
+
background-color: var(--sd-bg-content);
|
|
235
235
|
|
|
236
236
|
> ._sd-select-control {
|
|
237
237
|
color: var(--sd-tx-default);
|
|
@@ -21,7 +21,7 @@ export class SdGlobalErrorHandlerPlugin implements ErrorHandler {
|
|
|
21
21
|
private _hasDisplayedError = false;
|
|
22
22
|
|
|
23
23
|
handleError(event: unknown) {
|
|
24
|
-
// SSR(프리렌더) 가드: 서버에는 화면
|
|
24
|
+
// SSR(프리렌더) 가드: 서버에는 화면 오버레이, 브라우저 전역 이벤트 타입이 없음 — 로그만 남김
|
|
25
25
|
if (!this._isBrowser) {
|
|
26
26
|
logger.error(event);
|
|
27
27
|
return false;
|
|
@@ -226,7 +226,7 @@ export class SdModalProvider {
|
|
|
226
226
|
// transition duration 확인 후 대기 또는 즉시 destroy
|
|
227
227
|
const duration = parseFloat(getComputedStyle(modalEl).transitionDuration || "0");
|
|
228
228
|
if (duration > 0) {
|
|
229
|
-
// transitionend 가 정상 경로. 단 백그라운드
|
|
229
|
+
// transitionend 가 정상 경로. 단 백그라운드 탭, transition 인터럽트 시 미발화하므로
|
|
230
230
|
// duration(초→ms) + 여유 100ms 후 발화하는 setTimeout 폴백을 함께 걸어 destroy 누락을 방지.
|
|
231
231
|
modalEl.addEventListener("transitionend", doDestroy, { once: true });
|
|
232
232
|
setTimeout(doDestroy, duration * 1000 + 100);
|
|
@@ -49,7 +49,7 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
|
|
|
49
49
|
strokeWidth: 1.5,
|
|
50
50
|
}),
|
|
51
51
|
provideEnvironmentInitializer(() => {
|
|
52
|
-
// SSR(프리렌더) 가드: 테마
|
|
52
|
+
// SSR(프리렌더) 가드: 테마 저장, 복원은 브라우저 전용
|
|
53
53
|
if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
|
|
54
54
|
|
|
55
55
|
const sdTheme = inject(SdThemeProvider);
|
|
@@ -77,20 +77,6 @@ export function provideSdAngular(opt: { clientName: string }): EnvironmentProvid
|
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
const savedDensity = sdLocalStorage.get("sd-theme-density");
|
|
81
|
-
if (savedDensity === "compact" || savedDensity === "normal") {
|
|
82
|
-
sdTheme.density.set(savedDensity);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let prevDensity = sdTheme.density();
|
|
86
|
-
effect(() => {
|
|
87
|
-
const density = sdTheme.density();
|
|
88
|
-
if (density !== prevDensity) {
|
|
89
|
-
sdLocalStorage.set("sd-theme-density", density);
|
|
90
|
-
prevDensity = density;
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
80
|
const savedFontSize = sdLocalStorage.get("sd-theme-font-size");
|
|
95
81
|
if (savedFontSize != null) {
|
|
96
82
|
sdTheme.fontSize.set(savedFontSize);
|
package/src/core/setupBgTheme.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { effect, inject, PLATFORM_ID } from "@angular/core";
|
|
|
2
2
|
import { isPlatformBrowser } from "@angular/common";
|
|
3
3
|
|
|
4
4
|
export function setupBgTheme(options?: {
|
|
5
|
-
theme?:
|
|
6
|
-
"primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray";
|
|
5
|
+
theme?: "primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray";
|
|
7
6
|
}): void {
|
|
8
7
|
// SSR(프리렌더) 가드: document 반영은 브라우저 전용
|
|
9
8
|
if (!isPlatformBrowser(inject(PLATFORM_ID))) return;
|
|
@@ -20,7 +20,7 @@ import { SdSystemLogProvider } from "../config/sd-system-log.provider";
|
|
|
20
20
|
import "@simplysm/core-browser";
|
|
21
21
|
|
|
22
22
|
export type SdToastSeverity = "info" | "success" | "warning" | "danger";
|
|
23
|
-
export type SdToastTheme = "primary" |
|
|
23
|
+
export type SdToastTheme = "primary" | SdToastSeverity | "gray" | "blue-gray";
|
|
24
24
|
|
|
25
25
|
export interface SdToastContentDef<O> {
|
|
26
26
|
close: OutputEmitterRef<O | undefined>;
|
|
@@ -91,7 +91,9 @@ export class SdToastProvider {
|
|
|
91
91
|
return this._show("danger", message, useProgress);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
notify<T extends SdToastContentDef<any>>(
|
|
94
|
+
notify<T extends SdToastContentDef<any>>(
|
|
95
|
+
input: SdToastInput<T>,
|
|
96
|
+
): Promise<Parameters<T["close"]["emit"]>[0] | undefined> {
|
|
95
97
|
return new Promise((resolve) => {
|
|
96
98
|
// overlap 모드: 기존 토스트 모두 제거
|
|
97
99
|
if (this.overlap()) {
|
|
@@ -241,7 +243,7 @@ export class SdToastProvider {
|
|
|
241
243
|
let dismissPending = false;
|
|
242
244
|
let dismissed = false;
|
|
243
245
|
|
|
244
|
-
//
|
|
246
|
+
// 타이머, 리스너 정리. 외부 파괴 경로(_dismissToast/_destroyToast)에서도 호출되어
|
|
245
247
|
// 죽은 toastRef 에 대해 타이머가 깨어나는 race 와 리스너 누수를 막는다.
|
|
246
248
|
const cleanup = () => {
|
|
247
249
|
dismissed = true;
|
|
@@ -55,7 +55,7 @@ import { NgTemplateOutlet } from "@angular/common";
|
|
|
55
55
|
</sd-busy-container>
|
|
56
56
|
|
|
57
57
|
<ng-template #content>
|
|
58
|
-
<div class="flex-column fill bg-
|
|
58
|
+
<div class="flex-column fill bg-content">
|
|
59
59
|
@if (commandTplRef()) {
|
|
60
60
|
<div class="p-sm-default flex-row gap-default bdb bdb-soft">
|
|
61
61
|
<ng-template [ngTemplateOutlet]="commandTplRef()" />
|
|
@@ -51,6 +51,12 @@ export function injectSheetDomAccessor() {
|
|
|
51
51
|
return tbody.querySelector<HTMLTableRowElement>(`tr[data-r='${r}']`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function getRows(): HTMLTableRowElement[] {
|
|
55
|
+
const tbody = getTable().querySelector("tbody");
|
|
56
|
+
if (tbody == null) return [];
|
|
57
|
+
return Array.from(tbody.querySelectorAll<HTMLTableRowElement>("tr[data-r]"));
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
function getCell(r: number, c: number): HTMLTableCellElement | null {
|
|
55
61
|
const row = getRow(r);
|
|
56
62
|
if (row == null) return null;
|
|
@@ -69,6 +75,7 @@ export function injectSheetDomAccessor() {
|
|
|
69
75
|
getSelectRowIndicators,
|
|
70
76
|
getColumnResizeIndicator,
|
|
71
77
|
getRow,
|
|
78
|
+
getRows,
|
|
72
79
|
getCell,
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { DestroyRef, inject, type Signal } from "@angular/core";
|
|
2
|
+
|
|
3
|
+
export function injectSheetDragSelection<TItem>(options: {
|
|
4
|
+
domAccessor: {
|
|
5
|
+
getRows(): HTMLTableRowElement[];
|
|
6
|
+
getContainer(): HTMLElement;
|
|
7
|
+
getTHead(): HTMLTableSectionElement;
|
|
8
|
+
};
|
|
9
|
+
displayItems: Signal<TItem[]>;
|
|
10
|
+
selection: {
|
|
11
|
+
isSelected(item: TItem): boolean;
|
|
12
|
+
getSelectable(item: TItem): true | string | undefined;
|
|
13
|
+
select(item: TItem): void;
|
|
14
|
+
deselect(item: TItem): void;
|
|
15
|
+
};
|
|
16
|
+
}) {
|
|
17
|
+
const destroyRef = inject(DestroyRef);
|
|
18
|
+
|
|
19
|
+
let dragCleanup: (() => void) | null = null;
|
|
20
|
+
|
|
21
|
+
destroyRef.onDestroy(() => {
|
|
22
|
+
dragCleanup?.();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// 벗어난 1px 당 초당 스크롤 px, 그리고 초당 스크롤 상한
|
|
26
|
+
const AUTO_SCROLL_SPEED_PER_PX = 8;
|
|
27
|
+
const AUTO_SCROLL_MAX_SPEED = 2000;
|
|
28
|
+
|
|
29
|
+
// 고정 헤더에 가려지거나 컨테이너 밖에 있는 행은 소비자에게 안 보인다
|
|
30
|
+
function visibleBand(): { top: number; bottom: number } {
|
|
31
|
+
const containerRect = options.domAccessor.getContainer().getBoundingClientRect();
|
|
32
|
+
return {
|
|
33
|
+
top: containerRect.top + options.domAccessor.getTHead().offsetHeight,
|
|
34
|
+
bottom: containerRect.bottom,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rowIndexOf(rowEl: HTMLTableRowElement): number {
|
|
39
|
+
const attr = rowEl.getAttribute("data-r");
|
|
40
|
+
const rowIdx = attr == null ? Number.NaN : parseInt(attr, 10);
|
|
41
|
+
if (Number.isNaN(rowIdx)) {
|
|
42
|
+
throw new Error(`시트 행 인덱스를 읽을 수 없습니다: data-r=${attr}`);
|
|
43
|
+
}
|
|
44
|
+
return rowIdx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 보이는 행 중 clientY 가 속한 행. 위아래로 벗어나면 가장 가까운 끝 행으로 맞춘다
|
|
48
|
+
function resolveRowIndex(clientY: number): number | null {
|
|
49
|
+
const band = visibleBand();
|
|
50
|
+
const visibleRows = options.domAccessor.getRows().filter((rowEl) => {
|
|
51
|
+
const rect = rowEl.getBoundingClientRect();
|
|
52
|
+
return rect.bottom > band.top && rect.top < band.bottom;
|
|
53
|
+
});
|
|
54
|
+
if (visibleRows.length === 0) return null;
|
|
55
|
+
|
|
56
|
+
const firstRect = visibleRows[0].getBoundingClientRect();
|
|
57
|
+
const lastRect = visibleRows[visibleRows.length - 1].getBoundingClientRect();
|
|
58
|
+
if (clientY < Math.max(firstRect.top, band.top)) return rowIndexOf(visibleRows[0]);
|
|
59
|
+
if (clientY >= Math.min(lastRect.bottom, band.bottom)) {
|
|
60
|
+
return rowIndexOf(visibleRows[visibleRows.length - 1]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const rowEl of visibleRows) {
|
|
64
|
+
const rect = rowEl.getBoundingClientRect();
|
|
65
|
+
if (clientY < rect.top || clientY >= rect.bottom) continue;
|
|
66
|
+
return rowIndexOf(rowEl);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 가장자리를 넘어선 거리. 위로 벗어나면 음수, 아래로 벗어나면 양수
|
|
72
|
+
function overshootOf(clientY: number): number {
|
|
73
|
+
const band = visibleBand();
|
|
74
|
+
if (clientY < band.top) return clientY - band.top;
|
|
75
|
+
if (clientY >= band.bottom) return clientY - band.bottom;
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onSelectorPointerDown(event: PointerEvent, r: number): void {
|
|
80
|
+
if (event.button !== 0) return;
|
|
81
|
+
if (event.shiftKey) return;
|
|
82
|
+
if (event.pointerType === "touch") return;
|
|
83
|
+
|
|
84
|
+
dragCleanup?.();
|
|
85
|
+
|
|
86
|
+
const startItem = options.displayItems()[r];
|
|
87
|
+
if (startItem == null) return;
|
|
88
|
+
|
|
89
|
+
// 시작 행을 누른 결과 상태가 곧 페인트 목표. 체크박스 자체 토글보다 먼저 스냅샷해야 뒤집히지 않는다
|
|
90
|
+
const isSelectTarget = !options.selection.isSelected(startItem);
|
|
91
|
+
const startEl = event.currentTarget;
|
|
92
|
+
let dragged = false;
|
|
93
|
+
|
|
94
|
+
// 칠하기 전 상태. 드래그를 되돌려 범위를 벗어난 행은 이 값으로 복원한다
|
|
95
|
+
const stateBeforePaint = new Map<number, boolean>();
|
|
96
|
+
|
|
97
|
+
const setSelected = (rowIdx: number, selected: boolean): void => {
|
|
98
|
+
const item = options.displayItems()[rowIdx];
|
|
99
|
+
if (item == null) return;
|
|
100
|
+
if (options.selection.getSelectable(item) !== true) return;
|
|
101
|
+
|
|
102
|
+
if (selected) {
|
|
103
|
+
options.selection.select(item);
|
|
104
|
+
} else {
|
|
105
|
+
options.selection.deselect(item);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const paint = (rowIdx: number): void => {
|
|
110
|
+
const item = options.displayItems()[rowIdx];
|
|
111
|
+
if (item == null) return;
|
|
112
|
+
if (options.selection.getSelectable(item) !== true) return;
|
|
113
|
+
|
|
114
|
+
if (!stateBeforePaint.has(rowIdx)) {
|
|
115
|
+
stateBeforePaint.set(rowIdx, options.selection.isSelected(item));
|
|
116
|
+
}
|
|
117
|
+
setSelected(rowIdx, isSelectTarget);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 시작 행부터 현재 행까지가 칠할 범위. 범위를 벗어난 행은 원래대로 되돌린다
|
|
121
|
+
const applyRange = (currentRow: number): void => {
|
|
122
|
+
const from = Math.min(r, currentRow);
|
|
123
|
+
const to = Math.max(r, currentRow);
|
|
124
|
+
|
|
125
|
+
for (const rowIdx of [...stateBeforePaint.keys()]) {
|
|
126
|
+
if (rowIdx >= from && rowIdx <= to) continue;
|
|
127
|
+
setSelected(rowIdx, stateBeforePaint.get(rowIdx)!);
|
|
128
|
+
stateBeforePaint.delete(rowIdx);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let i = from; i <= to; i++) {
|
|
132
|
+
paint(i);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const applyRangeAt = (clientY: number): void => {
|
|
137
|
+
const rowIdx = resolveRowIndex(clientY);
|
|
138
|
+
if (rowIdx == null) return;
|
|
139
|
+
if (rowIdx === r && !dragged) return;
|
|
140
|
+
|
|
141
|
+
dragged = true;
|
|
142
|
+
applyRange(rowIdx);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// 가장자리를 넘긴 동안 벗어난 거리에 비례해 스크롤하며 계속 칠한다
|
|
146
|
+
let pointerY = event.clientY;
|
|
147
|
+
let rafId: number | null = null;
|
|
148
|
+
let lastFrameTime: number | null = null;
|
|
149
|
+
|
|
150
|
+
const autoScrollFrame = (time: number): void => {
|
|
151
|
+
const overshoot = overshootOf(pointerY);
|
|
152
|
+
if (overshoot === 0) {
|
|
153
|
+
rafId = null;
|
|
154
|
+
lastFrameTime = null;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const elapsedSec = lastFrameTime == null ? 0 : (time - lastFrameTime) / 1000;
|
|
159
|
+
lastFrameTime = time;
|
|
160
|
+
|
|
161
|
+
const speed =
|
|
162
|
+
Math.sign(overshoot) *
|
|
163
|
+
Math.min(Math.abs(overshoot) * AUTO_SCROLL_SPEED_PER_PX, AUTO_SCROLL_MAX_SPEED);
|
|
164
|
+
options.domAccessor.getContainer().scrollTop += speed * elapsedSec;
|
|
165
|
+
applyRangeAt(pointerY);
|
|
166
|
+
|
|
167
|
+
rafId = requestAnimationFrame(autoScrollFrame);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const onPointerMove = (e: PointerEvent): void => {
|
|
171
|
+
pointerY = e.clientY;
|
|
172
|
+
applyRangeAt(pointerY);
|
|
173
|
+
|
|
174
|
+
if (overshootOf(pointerY) !== 0 && rafId == null) {
|
|
175
|
+
rafId = requestAnimationFrame(autoScrollFrame);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const onPointerUp = (): void => {
|
|
180
|
+
dragCleanup?.();
|
|
181
|
+
if (!dragged) return;
|
|
182
|
+
|
|
183
|
+
// 시작 행은 이미 페인트로 확정됐으므로 SdCheckbox 자체 토글이 되돌리지 못하게 막는다.
|
|
184
|
+
// capture 로 걸어야 SdCheckbox 의 host (click) 보다 먼저 실행된다
|
|
185
|
+
if (!(startEl instanceof HTMLElement)) return;
|
|
186
|
+
|
|
187
|
+
const blockClick = (e: Event): void => {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
e.stopImmediatePropagation();
|
|
190
|
+
};
|
|
191
|
+
startEl.addEventListener("click", blockClick, { once: true, capture: true });
|
|
192
|
+
|
|
193
|
+
// 다른 행에서 뗀 드래그는 시작 체크박스에 click 이 오지 않는다.
|
|
194
|
+
// 남겨두면 이후의 정상 클릭 1회를 삼키므로 이번 턴이 지나면 걷어낸다
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
startEl.removeEventListener("click", blockClick, { capture: true });
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 드래그 중 브라우저 텍스트 선택 억제.
|
|
201
|
+
// pointerdown 을 preventDefault 하면 클릭 포커스까지 사라져 shift 범위 선택의 기준 행이 깨지므로,
|
|
202
|
+
// 선택 시작 자체만 막는다
|
|
203
|
+
const blockSelectStart = (e: Event): void => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
};
|
|
206
|
+
document.addEventListener("selectstart", blockSelectStart);
|
|
207
|
+
|
|
208
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
209
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
210
|
+
// 브라우저발 강제 종료(창 전환, 시스템 제스처)도 뗀 것과 동일하게 확정 처리
|
|
211
|
+
document.addEventListener("pointercancel", onPointerUp);
|
|
212
|
+
dragCleanup = () => {
|
|
213
|
+
if (rafId != null) {
|
|
214
|
+
cancelAnimationFrame(rafId);
|
|
215
|
+
rafId = null;
|
|
216
|
+
}
|
|
217
|
+
document.removeEventListener("selectstart", blockSelectStart);
|
|
218
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
219
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
220
|
+
document.removeEventListener("pointercancel", onPointerUp);
|
|
221
|
+
dragCleanup = null;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { onSelectorPointerDown };
|
|
226
|
+
}
|
|
@@ -46,6 +46,7 @@ import { useSheetDisplayPipeline } from "./useSheetDisplayPipeline";
|
|
|
46
46
|
import { useSheetCellStyling } from "./useSheetCellStyling";
|
|
47
47
|
import { useSheetFocusIndicator } from "./useSheetFocusIndicator";
|
|
48
48
|
import { injectSheetSelectRowIndicator } from "./injectSheetSelectRowIndicator";
|
|
49
|
+
import { injectSheetDragSelection } from "./injectSheetDragSelection";
|
|
49
50
|
import { SdModalProvider } from "../../core/modal/sd-modal.provider";
|
|
50
51
|
import { SdSheetConfigModal } from "./sd-sheet-config.modal";
|
|
51
52
|
import { SdEvents } from "../../core/events/sd-events";
|
|
@@ -68,7 +69,7 @@ import { SdEvents } from "../../core/events/sd-events";
|
|
|
68
69
|
{ directive: SdEvents, outputs: ["keydown.capture", "focus.capture", "blur.capture"] },
|
|
69
70
|
],
|
|
70
71
|
host: {
|
|
71
|
-
|
|
72
|
+
class: "flex-column fill",
|
|
72
73
|
"[attr.data-sd-inset]": "inset()",
|
|
73
74
|
"[attr.data-sd-focus-mode]": "focusMode()",
|
|
74
75
|
"(keydown.capture)": "onKeydownCapture($event)",
|
|
@@ -262,6 +263,7 @@ import { SdEvents } from "../../core/events/sd-events";
|
|
|
262
263
|
[canChangeFn]="selection.getCanChangeFn(item)"
|
|
263
264
|
(valueChange)="selection.toggle(item)"
|
|
264
265
|
(mousedown)="onSelectorMouseDown($event, rowIdx)"
|
|
266
|
+
(pointerdown)="onSelectorPointerDown($event, rowIdx)"
|
|
265
267
|
[inline]="true"
|
|
266
268
|
[theme]="'white'"
|
|
267
269
|
[disabled]="_selectable !== true"
|
|
@@ -370,7 +372,7 @@ import { SdEvents } from "../../core/events/sd-events";
|
|
|
370
372
|
border-radius: $border-radius;
|
|
371
373
|
|
|
372
374
|
> ._tool {
|
|
373
|
-
background-color: var(--sd-bg-
|
|
375
|
+
background-color: var(--sd-bg-canvas);
|
|
374
376
|
border-top-left-radius: $border-radius;
|
|
375
377
|
border-top-right-radius: $border-radius;
|
|
376
378
|
border-bottom: 1px solid $border-color-dark;
|
|
@@ -501,7 +503,7 @@ import { SdEvents } from "../../core/events/sd-events";
|
|
|
501
503
|
|
|
502
504
|
> tbody > tr > td {
|
|
503
505
|
font-family: var(--sd-font-family-field);
|
|
504
|
-
background-color: var(--sd-bg-
|
|
506
|
+
background-color: var(--sd-bg-content);
|
|
505
507
|
vertical-align: top;
|
|
506
508
|
|
|
507
509
|
&._fixed {
|
|
@@ -717,6 +719,13 @@ export class SdSheet<TItem> {
|
|
|
717
719
|
trackByFn: this.trackByFn,
|
|
718
720
|
});
|
|
719
721
|
|
|
722
|
+
// Drag(paint) selection
|
|
723
|
+
private readonly _dragSelection = injectSheetDragSelection<TItem>({
|
|
724
|
+
domAccessor: this.domAccessor,
|
|
725
|
+
displayItems: this.displayItems,
|
|
726
|
+
selection: this.selection,
|
|
727
|
+
});
|
|
728
|
+
|
|
720
729
|
// Icons
|
|
721
730
|
icons = {
|
|
722
731
|
tablerSettings,
|
|
@@ -853,6 +862,10 @@ export class SdSheet<TItem> {
|
|
|
853
862
|
});
|
|
854
863
|
}
|
|
855
864
|
|
|
865
|
+
onSelectorPointerDown(event: PointerEvent, r: number): void {
|
|
866
|
+
this._dragSelection.onSelectorPointerDown(event, r);
|
|
867
|
+
}
|
|
868
|
+
|
|
856
869
|
onSelectorMouseDown(event: MouseEvent, r: number): void {
|
|
857
870
|
if (!event.shiftKey) return;
|
|
858
871
|
event.preventDefault();
|
|
@@ -12,9 +12,6 @@ export const SD_THEMES = [
|
|
|
12
12
|
|
|
13
13
|
export type SdThemeName = (typeof SD_THEMES)[number]["value"];
|
|
14
14
|
|
|
15
|
-
// 밀도 축 — 테마와 직교(DEC-007). compact 는 치수 토큰 그룹(sd-density-compact)만 덮는다.
|
|
16
|
-
export type SdDensity = "normal" | "compact";
|
|
17
|
-
|
|
18
15
|
@Injectable({ providedIn: "root" })
|
|
19
16
|
export class SdThemeProvider {
|
|
20
17
|
readonly themes: readonly { value: SdThemeName; label: string }[] = SD_THEMES;
|
|
@@ -22,8 +19,6 @@ export class SdThemeProvider {
|
|
|
22
19
|
// 라이트(기본) / 블루프린트(도면) / IDE 다크
|
|
23
20
|
theme = signal<SdThemeName>("light");
|
|
24
21
|
|
|
25
|
-
density = signal<SdDensity>("normal");
|
|
26
|
-
|
|
27
22
|
readonly fontSizePresets: readonly number[] = [12, 14, 16, 20, 24, 28];
|
|
28
23
|
|
|
29
24
|
fontSize = signal<number>(12);
|
|
@@ -39,10 +34,6 @@ export class SdThemeProvider {
|
|
|
39
34
|
}
|
|
40
35
|
});
|
|
41
36
|
|
|
42
|
-
effect(() => {
|
|
43
|
-
document.body.classList.toggle("sd-density-compact", this.density() === "compact");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
37
|
effect(() => {
|
|
47
38
|
document.documentElement.style.fontSize = `${this.fontSize()}px`;
|
|
48
39
|
});
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
import { SdDropdown } from "../../controls/dropdown/sd-dropdown";
|
|
9
9
|
import { SdDropdownPopup } from "../../controls/dropdown/sd-dropdown-popup";
|
|
10
10
|
import { SdButton } from "../../controls/button/sd-button";
|
|
11
|
-
import { SdSwitch } from "../../controls/checkbox/sd-switch";
|
|
12
11
|
import { NgIcon } from "@ng-icons/core";
|
|
13
12
|
import { SdThemeProvider } from "./sd-theme-provider";
|
|
14
13
|
import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
|
|
@@ -18,7 +17,7 @@ import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
|
|
|
18
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
18
|
encapsulation: ViewEncapsulation.None,
|
|
20
19
|
standalone: true,
|
|
21
|
-
imports: [SdDropdown, SdDropdownPopup, SdButton,
|
|
20
|
+
imports: [SdDropdown, SdDropdownPopup, SdButton, NgIcon],
|
|
22
21
|
template: `
|
|
23
22
|
<sd-dropdown>
|
|
24
23
|
<sd-button [inline]="true" [theme]="'link-gray'">
|
|
@@ -65,14 +64,6 @@ import { tablerMinus, tablerPalette, tablerPlus } from "@ng-icons/tabler-icons";
|
|
|
65
64
|
}
|
|
66
65
|
</div>
|
|
67
66
|
</div>
|
|
68
|
-
<div class="flex-row gap-sm cross-align-center">
|
|
69
|
-
<span>compact</span>
|
|
70
|
-
<sd-switch
|
|
71
|
-
[inline]="true"
|
|
72
|
-
[value]="_sdTheme.density() === 'compact'"
|
|
73
|
-
(valueChange)="_sdTheme.density.set($event ? 'compact' : 'normal')"
|
|
74
|
-
/>
|
|
75
|
-
</div>
|
|
76
67
|
</div>
|
|
77
68
|
</sd-dropdown-popup>
|
|
78
69
|
</sd-dropdown>
|
|
@@ -57,9 +57,7 @@ import {
|
|
|
57
57
|
],
|
|
58
58
|
})
|
|
59
59
|
export class SdLabel {
|
|
60
|
-
theme = input<
|
|
61
|
-
"primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
|
|
62
|
-
>();
|
|
60
|
+
theme = input<"primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray">();
|
|
63
61
|
color = input<string>();
|
|
64
62
|
clickable = input(false, { transform: booleanAttribute });
|
|
65
63
|
}
|
|
@@ -57,9 +57,7 @@ import {
|
|
|
57
57
|
],
|
|
58
58
|
})
|
|
59
59
|
export class SdNote {
|
|
60
|
-
theme = input<
|
|
61
|
-
"primary" | "secondary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
|
|
62
|
-
>();
|
|
60
|
+
theme = input<"primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray">();
|
|
63
61
|
size = input<"sm" | "lg">();
|
|
64
62
|
inset = input(false, { transform: booleanAttribute });
|
|
65
63
|
}
|
|
@@ -70,7 +70,7 @@ import { PercentPipe } from "@angular/common";
|
|
|
70
70
|
&[data-sd-inset="true"] {
|
|
71
71
|
border-radius: 0;
|
|
72
72
|
border: none;
|
|
73
|
-
background-color: var(--sd-bg-
|
|
73
|
+
background-color: var(--sd-bg-track);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
`,
|
|
@@ -80,7 +80,7 @@ export class SdProgress {
|
|
|
80
80
|
inset = input(false, { transform: booleanAttribute });
|
|
81
81
|
size = input<"sm" | "lg">();
|
|
82
82
|
theme = input.required<
|
|
83
|
-
"primary" | "
|
|
83
|
+
"primary" | "info" | "success" | "warning" | "danger" | "gray" | "blue-gray"
|
|
84
84
|
>();
|
|
85
85
|
|
|
86
86
|
value = input.required<number>();
|
|
@@ -17,9 +17,7 @@ import { SdSidebarContainer } from "./sd-sidebar-container";
|
|
|
17
17
|
host: {
|
|
18
18
|
"[attr.data-sd-toggle]": "toggle()",
|
|
19
19
|
},
|
|
20
|
-
template: `
|
|
21
|
-
<ng-content />
|
|
22
|
-
`,
|
|
20
|
+
template: ` <ng-content /> `,
|
|
23
21
|
styles: [
|
|
24
22
|
/* language=SCSS */ `
|
|
25
23
|
@use "../../../scss/commons/mixins";
|
|
@@ -32,6 +30,7 @@ import { SdSidebarContainer } from "./sd-sidebar-container";
|
|
|
32
30
|
left: 0;
|
|
33
31
|
width: var(--sd-sidebar-width);
|
|
34
32
|
height: 100%;
|
|
33
|
+
background-color: var(--sd-bg-canvas);
|
|
35
34
|
border-right: 1px solid var(--sd-bd-soft);
|
|
36
35
|
|
|
37
36
|
@media not all and (max-width: variables.$breakpoint-mobile) {
|