@simplysm/angular 14.2.7 → 14.2.8

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.
@@ -5,6 +5,16 @@ interface DragResizeOptions {
5
5
  minHeightPx: Signal<number | undefined>;
6
6
  onEnd: () => void;
7
7
  }
8
+ /**
9
+ * `._dialog` 는 평소 CSS 로 중앙 정렬(`position: relative` + `margin: 0 auto`)되어 있어
10
+ * `left`/`top` 이 "정상 배치 위치 대비 상대 offset" 으로 해석된다.
11
+ * 드래그, 리사이즈는 절대 좌표로 계산하므로 시작 시점에 절대 좌표계로 전환한다.
12
+ * 전환만으로는 위치, 크기가 변하지 않으며, 이미 전환된 요소에 다시 적용해도 결과가 같다.
13
+ */
14
+ export declare function pinDialogAbsolute(dialogEl: HTMLElement): {
15
+ left: number;
16
+ top: number;
17
+ };
8
18
  export declare function injectDragResize(opt: DragResizeOptions): {
9
19
  startDrag: (event: MouseEvent) => void;
10
20
  startResize: (event: MouseEvent, dir: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"injectDragResize.d.ts","sourceRoot":"","sources":["../../../src/core/modal/injectDragResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAsB,MAAM,eAAe,CAAC;AAEhE,UAAU,iBAAiB;IACzB,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvC,WAAW,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD,CAiJA"}
1
+ {"version":3,"file":"injectDragResize.d.ts","sourceRoot":"","sources":["../../../src/core/modal/injectDragResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAsB,MAAM,eAAe,CAAC;AAEhE,UAAU,iBAAiB;IACzB,WAAW,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvC,WAAW,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAuBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAiCtF;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,iBAAiB,GAAG;IACxD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD,CA8IA"}
@@ -1,4 +1,53 @@
1
1
  import { inject, DestroyRef } from "@angular/core";
2
+ /** `auto`, `none` 등 길이가 아닌 값은 하한 없음(0)으로 본다. */
3
+ function parseCssMinPx(value) {
4
+ const parsed = parseFloat(value);
5
+ return Number.isNaN(parsed) ? 0 : parsed;
6
+ }
7
+ function shiftPercentSize(dialogStyle, prop, padPx) {
8
+ if (padPx === 0)
9
+ return;
10
+ const value = dialogStyle[prop];
11
+ if (!value.includes("%"))
12
+ return;
13
+ dialogStyle[prop] = `calc(${value} - ${padPx}px)`;
14
+ }
15
+ /**
16
+ * `._dialog` 는 평소 CSS 로 중앙 정렬(`position: relative` + `margin: 0 auto`)되어 있어
17
+ * `left`/`top` 이 "정상 배치 위치 대비 상대 offset" 으로 해석된다.
18
+ * 드래그, 리사이즈는 절대 좌표로 계산하므로 시작 시점에 절대 좌표계로 전환한다.
19
+ * 전환만으로는 위치, 크기가 변하지 않으며, 이미 전환된 요소에 다시 적용해도 결과가 같다.
20
+ */
21
+ export function pinDialogAbsolute(dialogEl) {
22
+ const wasRelative = getComputedStyle(dialogEl).position === "relative";
23
+ const parentEl = dialogEl.offsetParent;
24
+ const beforeRect = dialogEl.getBoundingClientRect();
25
+ const roundedLeft = dialogEl.offsetLeft;
26
+ const roundedTop = dialogEl.offsetTop;
27
+ dialogEl.style.position = "absolute";
28
+ dialogEl.style.margin = "0";
29
+ dialogEl.style.right = "auto";
30
+ dialogEl.style.bottom = "auto";
31
+ dialogEl.style.left = `${roundedLeft}px`;
32
+ dialogEl.style.top = `${roundedTop}px`;
33
+ if (wasRelative && parentEl != null) {
34
+ // 백분율 크기의 기준 박스가 부모 content box 에서 offsetParent padding box 로 바뀐다.
35
+ const parentStyle = getComputedStyle(parentEl);
36
+ const padX = parseFloat(parentStyle.paddingLeft) + parseFloat(parentStyle.paddingRight);
37
+ const padY = parseFloat(parentStyle.paddingTop) + parseFloat(parentStyle.paddingBottom);
38
+ shiftPercentSize(dialogEl.style, "width", padX);
39
+ shiftPercentSize(dialogEl.style, "maxWidth", padX);
40
+ shiftPercentSize(dialogEl.style, "height", padY);
41
+ shiftPercentSize(dialogEl.style, "maxHeight", padY);
42
+ }
43
+ // offsetLeft/offsetTop 은 정수로 반올림되므로, 전환 전후 실제 위치 차이만큼 되돌린다.
44
+ const afterRect = dialogEl.getBoundingClientRect();
45
+ const pinnedLeft = roundedLeft - (afterRect.left - beforeRect.left);
46
+ const pinnedTop = roundedTop - (afterRect.top - beforeRect.top);
47
+ dialogEl.style.left = `${pinnedLeft}px`;
48
+ dialogEl.style.top = `${pinnedTop}px`;
49
+ return { left: pinnedLeft, top: pinnedTop };
50
+ }
2
51
  export function injectDragResize(opt) {
3
52
  const destroyRef = inject(DestroyRef);
4
53
  let dragState;
@@ -22,8 +71,8 @@ export function injectDragResize(opt) {
22
71
  return;
23
72
  const dx = event.clientX - resizeState.startX;
24
73
  const dy = event.clientY - resizeState.startY;
25
- const minW = opt.minWidthPx() ?? 0;
26
- const minH = opt.minHeightPx() ?? 0;
74
+ const minW = resizeState.minWidth;
75
+ const minH = resizeState.minHeight;
27
76
  let newWidth = resizeState.startWidth;
28
77
  let newHeight = resizeState.startHeight;
29
78
  let newLeft = resizeState.startLeft;
@@ -71,23 +120,16 @@ export function injectDragResize(opt) {
71
120
  opt.onEnd();
72
121
  }
73
122
  }
74
- function getParentRect(dialogEl) {
75
- return dialogEl.offsetParent?.getBoundingClientRect() ?? {
76
- left: 0,
77
- top: 0,
78
- };
79
- }
80
123
  function startDrag(event) {
81
124
  const dialogEl = opt.getDialogEl();
82
125
  if (dialogEl == null)
83
126
  return;
84
- const dialogRect = dialogEl.getBoundingClientRect();
85
- const parentRect = getParentRect(dialogEl);
127
+ const pinned = pinDialogAbsolute(dialogEl);
86
128
  dragState = {
87
129
  startX: event.clientX,
88
130
  startY: event.clientY,
89
- startLeft: dialogRect.left - parentRect.left,
90
- startTop: dialogRect.top - parentRect.top,
131
+ startLeft: pinned.left,
132
+ startTop: pinned.top,
91
133
  };
92
134
  document.addEventListener("mousemove", onDocumentMouseMove);
93
135
  document.addEventListener("mouseup", onDocumentMouseUp);
@@ -96,16 +138,19 @@ export function injectDragResize(opt) {
96
138
  const dialogEl = opt.getDialogEl();
97
139
  if (dialogEl == null)
98
140
  return;
99
- const dialogRect = dialogEl.getBoundingClientRect();
100
- const parentRect = getParentRect(dialogEl);
141
+ const pinned = pinDialogAbsolute(dialogEl);
142
+ const dialogStyle = getComputedStyle(dialogEl);
101
143
  resizeState = {
102
144
  dir,
103
145
  startX: event.clientX,
104
146
  startY: event.clientY,
105
147
  startWidth: dialogEl.offsetWidth,
106
148
  startHeight: dialogEl.offsetHeight,
107
- startLeft: dialogRect.left - parentRect.left,
108
- startTop: dialogRect.top - parentRect.top,
149
+ startLeft: pinned.left,
150
+ startTop: pinned.top,
151
+ // CSS 하한을 넘겨 줄이면 실제 크기는 안 줄고 반대쪽 변만 밀려나므로, 실제 제약을 하한으로 삼는다.
152
+ minWidth: Math.max(opt.minWidthPx() ?? 0, parseCssMinPx(dialogStyle.minWidth)),
153
+ minHeight: Math.max(opt.minHeightPx() ?? 0, parseCssMinPx(dialogStyle.minHeight)),
109
154
  };
110
155
  document.addEventListener("mousemove", onDocumentMouseMove);
111
156
  document.addEventListener("mouseup", onDocumentMouseUp);
@@ -30,8 +30,12 @@ export declare class SdModal {
30
30
  actionTplRef: import("@angular/core").InputSignal<TemplateRef<any> | undefined>;
31
31
  closeRequest: import("@angular/core").OutputEmitterRef<void>;
32
32
  private readonly _dragResize;
33
+ /** 화면 넘침 클램프로 덮기 직전의 크기. 클램프 중이 아니면 undefined. */
34
+ private _clampedWidth;
35
+ private _clampedHeight;
33
36
  constructor();
34
37
  onResizeMouseDown(event: MouseEvent, dir: string): void;
38
+ onDialogMouseDown(): void;
35
39
  onHeaderMouseDown(event: MouseEvent): void;
36
40
  onBackdropClick(): void;
37
41
  onCloseButtonClick(): void;
@@ -40,8 +44,10 @@ export declare class SdModal {
40
44
  onHostResize(event: SdResizeEvent): void;
41
45
  onDialogResize(event: SdResizeEvent): void;
42
46
  onWindowResize(): void;
47
+ private _clampIntoHost;
43
48
  private _requestClose;
44
49
  private _bringToFront;
50
+ private _forgetClampedSize;
45
51
  private _calcHeight;
46
52
  private _calcWidth;
47
53
  private _getDialogEl;
@@ -1 +1 @@
1
- {"version":3,"file":"sd-modal.d.ts","sourceRoot":"","sources":["../../../src/core/modal/sd-modal.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AASvB,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,wBAAwB,CAAC;;;AAEhC,qBAkTa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwD;IAC1F,OAAO,CAAC,QAAQ,CAAC,eAAe,CAEc;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwB;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAEhD,SAAS,CAAC,QAAQ,CAAC,OAAO,yVAAW;IAErC,IAAI,+CAAgB;IACpB,GAAG,0DAAwC;IAC3C,KAAK,8CAAa;IAClB,UAAU,+CAAgB;IAC1B,eAAe,+CAAgB;IAC/B,WAAW,0DAAwC;IACnD,kBAAkB,+CAAe;IACjC,mBAAmB,+CAAe;IAClC,KAAK,+CAAgB;IACrB,IAAI,+CAAgB;IACpB,SAAS,+CAAgB;IACzB,OAAO,+CAAgB;IACvB,QAAQ,gFAA8D;IACtE,WAAW,0DAAwC;IACnD,UAAU,0DAAwC;IAClD,QAAQ,0DAAwC;IAChD,OAAO,0DAAwC;IAC/C,YAAY,oEAAkD;IAE9D,YAAY,iDAAkB;IAE9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAKzB;;IAoCH,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAKvD,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAO1C,eAAe,IAAI,IAAI;IAKvB,kBAAkB,IAAI,IAAI;IAI1B,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAS3C,aAAa,IAAI,IAAI;IAIrB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAKxC,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAK1C,cAAc,IAAI,IAAI;IAYtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,YAAY;YAIN,WAAW;YAgBX,cAAc;yCAjMjB,OAAO;2CAAP,OAAO;CAgNnB"}
1
+ {"version":3,"file":"sd-modal.d.ts","sourceRoot":"","sources":["../../../src/core/modal/sd-modal.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAC;AASvB,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,wBAAwB,CAAC;;;AAEhC,qBAmTa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAwD;IAC1F,OAAO,CAAC,QAAQ,CAAC,eAAe,CAEc;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwB;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAEhD,SAAS,CAAC,QAAQ,CAAC,OAAO,yVAAW;IAErC,IAAI,+CAAgB;IACpB,GAAG,0DAAwC;IAC3C,KAAK,8CAAa;IAClB,UAAU,+CAAgB;IAC1B,eAAe,+CAAgB;IAC/B,WAAW,0DAAwC;IACnD,kBAAkB,+CAAe;IACjC,mBAAmB,+CAAe;IAClC,KAAK,+CAAgB;IACrB,IAAI,+CAAgB;IACpB,SAAS,+CAAgB;IACzB,OAAO,+CAAgB;IACvB,QAAQ,gFAA8D;IACtE,WAAW,0DAAwC;IACnD,UAAU,0DAAwC;IAClD,QAAQ,0DAAwC;IAChD,OAAO,0DAAwC;IAC/C,YAAY,oEAAkD;IAE9D,YAAY,iDAAkB;IAE9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAKzB;IAEH,kDAAkD;IAClD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,cAAc,CAAqB;;IAqC3C,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAOvD,iBAAiB,IAAI,IAAI;IAIzB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAO1C,eAAe,IAAI,IAAI;IAKvB,kBAAkB,IAAI,IAAI;IAI1B,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAS3C,aAAa,IAAI,IAAI;IAIrB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAKxC,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAK1C,cAAc,IAAI,IAAI;IAQtB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,kBAAkB;IAK1B,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,YAAY;YAIN,WAAW;YAoBX,cAAc;yCAnPjB,OAAO;2CAAP,OAAO;CAyQnB"}
@@ -5,7 +5,7 @@ import { tablerX } from "@ng-icons/tabler-icons";
5
5
  import { SdActivatedModalProvider } from "./sd-activated-modal.provider";
6
6
  import { SdSystemConfigProvider } from "../config/sd-system-config.provider";
7
7
  import { injectFocusTrap } from "./injectFocusTrap";
8
- import { injectDragResize } from "./injectDragResize";
8
+ import { injectDragResize, pinDialogAbsolute } from "./injectDragResize";
9
9
  import { SdAnchor } from "../../controls/button/sd-anchor";
10
10
  import { SdResizeDirective } from "../events/sd-resize";
11
11
  import "@simplysm/core-browser";
@@ -130,6 +130,9 @@ export class SdModal {
130
130
  minHeightPx: this.minHeightPx,
131
131
  onEnd: () => void this._saveConfig().catch((err) => this._errorHandler.handleError(err)),
132
132
  });
133
+ /** 화면 넘침 클램프로 덮기 직전의 크기. 클램프 중이 아니면 undefined. */
134
+ _clampedWidth;
135
+ _clampedHeight;
133
136
  constructor() {
134
137
  // data-sd-init: 첫 렌더 후 설정하여 CSS transition 트리거 허용
135
138
  afterNextRender(() => {
@@ -154,6 +157,7 @@ export class SdModal {
154
157
  else {
155
158
  dialogEl.style.height = "";
156
159
  }
160
+ this._forgetClampedSize();
157
161
  });
158
162
  // key 기반 설정 복원
159
163
  effect(() => {
@@ -165,8 +169,13 @@ export class SdModal {
165
169
  }
166
170
  onResizeMouseDown(event, dir) {
167
171
  event.preventDefault();
172
+ // 사용자가 직접 정하는 크기가 자동 클램프 이전 크기보다 우선한다.
173
+ this._forgetClampedSize();
168
174
  this._dragResize.startResize(event, dir);
169
175
  }
176
+ onDialogMouseDown() {
177
+ this._bringToFront();
178
+ }
170
179
  onHeaderMouseDown(event) {
171
180
  if (!this.movable())
172
181
  return;
@@ -212,12 +221,20 @@ export class SdModal {
212
221
  const dialogEl = this._getDialogEl();
213
222
  if (dialogEl == null)
214
223
  return;
224
+ // 위치를 잡지 않은 모달은 CSS 중앙 정렬이 창 크기를 따라가므로 보정 대상이 아니다.
225
+ if (dialogEl.style.left === "" && dialogEl.style.top === "")
226
+ return;
227
+ this._clampIntoHost(dialogEl);
228
+ }
229
+ _clampIntoHost(dialogEl) {
215
230
  const hostEl = this._elRef.nativeElement;
216
- if (dialogEl.offsetLeft > hostEl.offsetWidth - 100) {
217
- dialogEl.style.left = hostEl.offsetWidth - 100 + "px";
231
+ const maxLeft = Math.max(0, hostEl.offsetWidth - 100);
232
+ if (dialogEl.offsetLeft > maxLeft) {
233
+ dialogEl.style.left = `${maxLeft}px`;
218
234
  }
219
- if (dialogEl.offsetTop > hostEl.offsetHeight - 100) {
220
- dialogEl.style.top = hostEl.offsetHeight - 100 + "px";
235
+ const maxTop = Math.max(0, hostEl.offsetHeight - 100);
236
+ if (dialogEl.offsetTop > maxTop) {
237
+ dialogEl.style.top = `${maxTop}px`;
221
238
  }
222
239
  }
223
240
  _requestClose() {
@@ -244,25 +261,47 @@ export class SdModal {
244
261
  return;
245
262
  hostEl.style.zIndex = String(maxZ + 1);
246
263
  }
264
+ _forgetClampedSize() {
265
+ this._clampedWidth = undefined;
266
+ this._clampedHeight = undefined;
267
+ }
247
268
  _calcHeight() {
248
269
  const dialogEl = this._getDialogEl();
249
270
  if (dialogEl == null)
250
271
  return;
251
- const style = getComputedStyle(this._elRef.nativeElement);
252
- const paddingTop = style.paddingTop === "" ? 0 : parseInt(style.paddingTop, 10) || 0;
253
- if (dialogEl.offsetHeight > this._elRef.nativeElement.offsetHeight - paddingTop) {
272
+ // 클램프 중이면 dialog 높이가 호스트에 맞춰져 있어 실제 필요 높이를 알 수 없다. 먼저 되돌려 잰다.
273
+ if (this._clampedHeight != null) {
274
+ dialogEl.style.height = this._clampedHeight;
275
+ dialogEl.style.maxHeight = "";
276
+ }
277
+ const hostEl = this._elRef.nativeElement;
278
+ const paddingTop = parseFloat(getComputedStyle(hostEl).paddingTop) || 0;
279
+ if (dialogEl.offsetHeight > hostEl.offsetHeight - paddingTop) {
280
+ this._clampedHeight ??= dialogEl.style.height;
254
281
  dialogEl.style.maxHeight = "100%";
255
282
  dialogEl.style.height = "100%";
256
283
  }
284
+ else {
285
+ this._clampedHeight = undefined;
286
+ }
257
287
  }
258
288
  _calcWidth() {
259
289
  const dialogEl = this._getDialogEl();
260
290
  if (dialogEl == null)
261
291
  return;
262
- if (dialogEl.offsetWidth > this._elRef.nativeElement.offsetWidth) {
292
+ if (this._clampedWidth != null) {
293
+ dialogEl.style.width = this._clampedWidth;
294
+ dialogEl.style.maxWidth = "";
295
+ }
296
+ const hostEl = this._elRef.nativeElement;
297
+ if (dialogEl.offsetWidth > hostEl.offsetWidth) {
298
+ this._clampedWidth ??= dialogEl.style.width;
263
299
  dialogEl.style.maxWidth = "100%";
264
300
  dialogEl.style.width = "100%";
265
301
  }
302
+ else {
303
+ this._clampedWidth = undefined;
304
+ }
266
305
  }
267
306
  _getDialogEl() {
268
307
  return this._elRef.nativeElement.querySelector("._dialog");
@@ -274,11 +313,14 @@ export class SdModal {
274
313
  const dialogEl = this._getDialogEl();
275
314
  if (dialogEl == null)
276
315
  return;
316
+ // 클램프 중이면 화면에 맞춘 값이 아니라 그 직전 크기를 저장해야 다시 열 때 원래 크기로 뜬다.
317
+ const savedWidth = this._clampedWidth ?? dialogEl.style.width;
318
+ const savedHeight = this._clampedHeight ?? dialogEl.style.height;
277
319
  const config = {};
278
- if (dialogEl.style.width !== "")
279
- config["width"] = dialogEl.style.width;
280
- if (dialogEl.style.height !== "")
281
- config["height"] = dialogEl.style.height;
320
+ if (savedWidth !== "")
321
+ config["width"] = savedWidth;
322
+ if (savedHeight !== "")
323
+ config["height"] = savedHeight;
282
324
  if (dialogEl.style.left !== "")
283
325
  config["left"] = dialogEl.style.left;
284
326
  if (dialogEl.style.top !== "")
@@ -298,23 +340,28 @@ export class SdModal {
298
340
  dialogEl.style.width = config["width"];
299
341
  if (config["height"] != null)
300
342
  dialogEl.style.height = config["height"];
301
- if (config["left"] != null)
302
- dialogEl.style.left = config["left"];
303
- if (config["top"] != null)
304
- dialogEl.style.top = config["top"];
343
+ this._forgetClampedSize();
344
+ if (config["left"] != null || config["top"] != null) {
345
+ pinDialogAbsolute(dialogEl);
346
+ if (config["left"] != null)
347
+ dialogEl.style.left = config["left"];
348
+ if (config["top"] != null)
349
+ dialogEl.style.top = config["top"];
350
+ this._clampIntoHost(dialogEl);
351
+ }
305
352
  }
306
353
  static ɵfac = function SdModal_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SdModal)(); };
307
354
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SdModal, selectors: [["sd-modal"]], hostVars: 4, hostBindings: function SdModal_HostBindings(rf, ctx) { if (rf & 1) {
308
355
  i0.ɵɵlistener("sdResize", function SdModal_sdResize_HostBindingHandler($event) { return ctx.onHostResize($event); })("resize", function SdModal_resize_HostBindingHandler() { return ctx.onWindowResize(); }, i0.ɵɵresolveWindow);
309
356
  } if (rf & 2) {
310
357
  i0.ɵɵattribute("data-sd-open", ctx.open() || undefined)("data-sd-float", ctx.float() || undefined)("data-sd-fill", ctx.fill() || undefined)("data-sd-position", ctx.position() || undefined);
311
- } }, inputs: { open: [1, "open"], key: [1, "key"], title: [1, "title"], hideHeader: [1, "hideHeader"], hideCloseButton: [1, "hideCloseButton"], headerStyle: [1, "headerStyle"], useCloseByBackdrop: [1, "useCloseByBackdrop"], useCloseByEscapeKey: [1, "useCloseByEscapeKey"], float: [1, "float"], fill: [1, "fill"], resizable: [1, "resizable"], movable: [1, "movable"], position: [1, "position"], minHeightPx: [1, "minHeightPx"], minWidthPx: [1, "minWidthPx"], heightPx: [1, "heightPx"], widthPx: [1, "widthPx"], actionTplRef: [1, "actionTplRef"] }, outputs: { open: "openChange", closeRequest: "closeRequest" }, features: [i0.ɵɵHostDirectivesFeature([{ directive: i1.SdResizeDirective, outputs: ["sdResize", "sdResize"] }])], ngContentSelectors: _c0, decls: 6, vars: 2, consts: [["tabindex", "-1", "role", "button", 1, "_backdrop", 3, "click", "keydown.enter"], ["tabindex", "-1", 1, "_dialog", 3, "keydown", "focus", "sdResize"], [1, "_header", 3, "style"], [1, "_content"], [1, "_header", 3, "mousedown"], [1, "_title"], [1, "_close-btn", 3, "theme"], [4, "ngTemplateOutlet"], [1, "_close-btn", 3, "click", "theme"], [3, "svg"], ["data-resize-dir", "top", 1, "_resize-handle", "_resize-top", 3, "mousedown"], ["data-resize-dir", "bottom", 1, "_resize-handle", "_resize-bottom", 3, "mousedown"], ["data-resize-dir", "left", 1, "_resize-handle", "_resize-left", 3, "mousedown"], ["data-resize-dir", "right", 1, "_resize-handle", "_resize-right", 3, "mousedown"], ["data-resize-dir", "top-left", 1, "_resize-handle", "_resize-top-left", 3, "mousedown"], ["data-resize-dir", "top-right", 1, "_resize-handle", "_resize-top-right", 3, "mousedown"], ["data-resize-dir", "bottom-left", 1, "_resize-handle", "_resize-bottom-left", 3, "mousedown"], ["data-resize-dir", "bottom-right", 1, "_resize-handle", "_resize-bottom-right", 3, "mousedown"]], template: function SdModal_Template(rf, ctx) { if (rf & 1) {
358
+ } }, inputs: { open: [1, "open"], key: [1, "key"], title: [1, "title"], hideHeader: [1, "hideHeader"], hideCloseButton: [1, "hideCloseButton"], headerStyle: [1, "headerStyle"], useCloseByBackdrop: [1, "useCloseByBackdrop"], useCloseByEscapeKey: [1, "useCloseByEscapeKey"], float: [1, "float"], fill: [1, "fill"], resizable: [1, "resizable"], movable: [1, "movable"], position: [1, "position"], minHeightPx: [1, "minHeightPx"], minWidthPx: [1, "minWidthPx"], heightPx: [1, "heightPx"], widthPx: [1, "widthPx"], actionTplRef: [1, "actionTplRef"] }, outputs: { open: "openChange", closeRequest: "closeRequest" }, features: [i0.ɵɵHostDirectivesFeature([{ directive: i1.SdResizeDirective, outputs: ["sdResize", "sdResize"] }])], ngContentSelectors: _c0, decls: 6, vars: 2, consts: [["tabindex", "-1", "role", "button", 1, "_backdrop", 3, "click", "keydown.enter"], ["tabindex", "-1", 1, "_dialog", 3, "mousedown", "keydown", "focus", "sdResize"], [1, "_header", 3, "style"], [1, "_content"], [1, "_header", 3, "mousedown"], [1, "_title"], [1, "_close-btn", 3, "theme"], [4, "ngTemplateOutlet"], [1, "_close-btn", 3, "click", "theme"], [3, "svg"], ["data-resize-dir", "top", 1, "_resize-handle", "_resize-top", 3, "mousedown"], ["data-resize-dir", "bottom", 1, "_resize-handle", "_resize-bottom", 3, "mousedown"], ["data-resize-dir", "left", 1, "_resize-handle", "_resize-left", 3, "mousedown"], ["data-resize-dir", "right", 1, "_resize-handle", "_resize-right", 3, "mousedown"], ["data-resize-dir", "top-left", 1, "_resize-handle", "_resize-top-left", 3, "mousedown"], ["data-resize-dir", "top-right", 1, "_resize-handle", "_resize-top-right", 3, "mousedown"], ["data-resize-dir", "bottom-left", 1, "_resize-handle", "_resize-bottom-left", 3, "mousedown"], ["data-resize-dir", "bottom-right", 1, "_resize-handle", "_resize-bottom-right", 3, "mousedown"]], template: function SdModal_Template(rf, ctx) { if (rf & 1) {
312
359
  i0.ɵɵprojectionDef();
313
360
  i0.ɵɵelementStart(0, "div", 0);
314
361
  i0.ɵɵlistener("click", function SdModal_Template_div_click_0_listener() { return ctx.onBackdropClick(); })("keydown.enter", function SdModal_Template_div_keydown_enter_0_listener() { return ctx.onBackdropClick(); });
315
362
  i0.ɵɵelementEnd();
316
363
  i0.ɵɵelementStart(1, "div", 1);
317
- i0.ɵɵlistener("keydown", function SdModal_Template_div_keydown_1_listener($event) { return ctx.onDialogKeydown($event); })("focus", function SdModal_Template_div_focus_1_listener() { return ctx.onDialogFocus(); })("sdResize", function SdModal_Template_div_sdResize_1_listener($event) { return ctx.onDialogResize($event); });
364
+ i0.ɵɵlistener("mousedown", function SdModal_Template_div_mousedown_1_listener() { return ctx.onDialogMouseDown(); })("keydown", function SdModal_Template_div_keydown_1_listener($event) { return ctx.onDialogKeydown($event); })("focus", function SdModal_Template_div_focus_1_listener() { return ctx.onDialogFocus(); })("sdResize", function SdModal_Template_div_sdResize_1_listener($event) { return ctx.onDialogResize($event); });
318
365
  i0.ɵɵconditionalCreate(2, SdModal_Conditional_2_Template, 5, 5, "div", 2);
319
366
  i0.ɵɵelementStart(3, "div", 3);
320
367
  i0.ɵɵprojection(4);
@@ -348,6 +395,7 @@ export class SdModal {
348
395
  <div
349
396
  class="_dialog"
350
397
  tabindex="-1"
398
+ (mousedown)="onDialogMouseDown()"
351
399
  (keydown)="onDialogKeydown($event)"
352
400
  (focus)="onDialogFocus()"
353
401
  (sdResize)="onDialogResize($event)"
@@ -411,4 +459,4 @@ export class SdModal {
411
459
  </div>
412
460
  `, styles: ["sd-modal {\n display: block;\n position: fixed;\n z-index: var(--sd-z-modal);\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding-top: calc(var(--sd-topbar-height) + var(--sd-gap-sm));\n opacity: 0;\n transition: opacity var(--sd-animation-duration) ease-in-out;\n pointer-events: none;\n}\nsd-modal > ._backdrop {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: var(--sd-bg-backdrop);\n}\nsd-modal > ._dialog {\n position: relative;\n display: flex;\n flex-direction: column;\n margin: 0 auto;\n width: fit-content;\n min-width: 200px;\n background-color: var(--sd-bg-elevated);\n border: 1px solid var(--sd-modal-bd);\n overflow: hidden;\n box-shadow: 0 calc(16 * var(--sd-shadow-size)) calc(16 * 4 * var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color), 0 var(--sd-shadow-size) calc(var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color);\n border-radius: var(--sd-radius-default);\n transform: translateY(-25px);\n transition: transform var(--sd-animation-duration) ease-in-out;\n}\nsd-modal > ._dialog:focus {\n outline: none;\n}\nsd-modal > ._dialog > ._header {\n display: flex;\n align-items: center;\n user-select: none;\n border-bottom: 1px solid var(--sd-bd-hairline);\n background-color: var(--sd-modal-header-bg);\n color: var(--sd-modal-header-tx);\n}\nsd-modal > ._dialog > ._header > ._title {\n flex: 1;\n padding: var(--sd-gap-sm) var(--sd-gap-default);\n}\nsd-modal > ._dialog > ._header > ._close-btn {\n padding: var(--sd-gap-sm) var(--sd-gap-default);\n color: var(--sd-modal-header-tx-muted);\n}\nsd-modal > ._dialog > ._header > ._close-btn:hover {\n color: var(--sd-modal-header-tx);\n}\nsd-modal > ._dialog > ._content {\n flex: 1;\n overflow: auto;\n}\nsd-modal > ._dialog > ._resize-handle {\n position: absolute;\n}\nsd-modal > ._dialog > ._resize-handle._resize-left {\n top: 0;\n left: 0;\n width: var(--sd-gap-sm);\n height: 100%;\n cursor: ew-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-right {\n top: 0;\n right: 0;\n width: var(--sd-gap-sm);\n height: 100%;\n cursor: ew-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-top {\n top: 0;\n left: 0;\n width: 100%;\n height: var(--sd-gap-sm);\n cursor: ns-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-top-right {\n right: 0;\n top: 0;\n width: var(--sd-gap-sm);\n height: var(--sd-gap-sm);\n z-index: 1;\n cursor: nesw-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-top-left {\n left: 0;\n top: 0;\n width: var(--sd-gap-sm);\n height: var(--sd-gap-sm);\n cursor: nwse-resize;\n z-index: 1;\n}\nsd-modal > ._dialog > ._resize-handle._resize-bottom {\n bottom: 0;\n left: 0;\n width: 100%;\n height: var(--sd-gap-sm);\n cursor: ns-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-bottom-right {\n right: 0;\n bottom: 0;\n width: var(--sd-gap-sm);\n height: var(--sd-gap-sm);\n z-index: 1;\n cursor: nwse-resize;\n}\nsd-modal > ._dialog > ._resize-handle._resize-bottom-left {\n left: 0;\n bottom: 0;\n width: var(--sd-gap-sm);\n height: var(--sd-gap-sm);\n cursor: nesw-resize;\n z-index: 1;\n}\nsd-modal[data-sd-open][data-sd-init] {\n opacity: 1;\n pointer-events: auto;\n}\nsd-modal[data-sd-open][data-sd-init] > ._dialog {\n transform: none;\n}\nsd-modal[data-sd-float] {\n pointer-events: none;\n}\nsd-modal[data-sd-float] > ._backdrop {\n display: none;\n}\nsd-modal[data-sd-float] > ._dialog {\n pointer-events: auto;\n opacity: 0;\n box-shadow: 0 calc(4 * var(--sd-shadow-size)) calc(4 * 4 * var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color), 0 var(--sd-shadow-size) calc(var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color);\n border: 1px solid var(--sd-bd-hairline);\n}\nsd-modal[data-sd-float] > ._dialog:focus {\n box-shadow: 0 calc(16 * var(--sd-shadow-size)) calc(16 * 4 * var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color), 0 var(--sd-shadow-size) calc(var(--sd-shadow-size) * var(--sd-shadow-blur-mult)) var(--sd-shadow-color);\n}\nsd-modal[data-sd-float][data-sd-open][data-sd-init] {\n pointer-events: none;\n}\nsd-modal[data-sd-float][data-sd-open][data-sd-init] > ._dialog {\n pointer-events: auto;\n opacity: 1;\n}\nsd-modal[data-sd-position=bottom-right] > ._dialog {\n position: absolute;\n right: calc(var(--sd-gap-xxl) * 2);\n bottom: var(--sd-gap-xxl);\n}\nsd-modal[data-sd-position=top-right] > ._dialog {\n position: absolute;\n right: var(--sd-gap-xxl);\n top: var(--sd-gap-xxl);\n}\nsd-modal[data-sd-fill] {\n padding-top: 0;\n}\nsd-modal[data-sd-fill] > ._dialog {\n width: 100%;\n height: 100%;\n border: none;\n border-radius: 0;\n}\nsd-modal[data-sd-fill] > ._dialog > ._header {\n background-color: transparent;\n color: var(--sd-tx-faint);\n}"] }]
413
461
  }], () => [], { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], hideHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHeader", required: false }] }], hideCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideCloseButton", required: false }] }], headerStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerStyle", required: false }] }], useCloseByBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "useCloseByBackdrop", required: false }] }], useCloseByEscapeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "useCloseByEscapeKey", required: false }] }], float: [{ type: i0.Input, args: [{ isSignal: true, alias: "float", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], resizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizable", required: false }] }], movable: [{ type: i0.Input, args: [{ isSignal: true, alias: "movable", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], minHeightPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeightPx", required: false }] }], minWidthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidthPx", required: false }] }], heightPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "heightPx", required: false }] }], widthPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "widthPx", required: false }] }], actionTplRef: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionTplRef", required: false }] }], closeRequest: [{ type: i0.Output, args: ["closeRequest"] }] }); })();
414
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SdModal, { className: "SdModal", filePath: "packages/angular/src/core/modal/sd-modal.ts", lineNumber: 332 }); })();
462
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(SdModal, { className: "SdModal", filePath: "packages/angular/src/core/modal/sd-modal.ts", lineNumber: 333 }); })();
@@ -1 +1 @@
1
- {"version":3,"file":"sd-modal.provider.d.ts","sourceRoot":"","sources":["../../../src/core/modal/sd-modal.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EACV,qBAAqB,EACrB,YAAY,EACb,MAAM,4BAA4B,CAAC;AAGpC,OAAO,wBAAwB,CAAC;;AAEhC;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7B,KAAK,EAAE,gBAAgB,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,KAAK,kBAAkB,GAAG,aAAa,GAAG,OAAO,GAAG,cAAc,GAAG,sBAAsB,CAAC;AAC5F,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,SAAS,MAAM,CAAA;CAAE,GACrF,CAAC,GACD,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,GAAG,EAAE;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,MAAM,EAAE,YAAY,CAClB,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,EACtD,mBAAmB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,CACtF,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,cAAc,GAAG,WAAW,CAAC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;GAEG;AACH,qBACa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAE9C,UAAU,iDAAa;IAEjB,SAAS,CAAC,CAAC,SAAS,iBAAiB,CAAC,GAAG,CAAC,EAC9C,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EACrB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAkKzD,OAAO,CAAC,aAAa;yCA5KV,eAAe;6CAAf,eAAe;CAuL3B"}
1
+ {"version":3,"file":"sd-modal.provider.d.ts","sourceRoot":"","sources":["../../../src/core/modal/sd-modal.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,gBAAgB,EACrB,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAGtF,OAAO,wBAAwB,CAAC;;AAEhC;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7B,KAAK,EAAE,gBAAgB,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,KAAK,kBAAkB,GAAG,aAAa,GAAG,OAAO,GAAG,cAAc,GAAG,sBAAsB,CAAC;AAC5F,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,SAAS,MAAM,CAAA;CAAE,GACrF,CAAC,GACD,KAAK,CAAC;AAEV;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,GAAG,EAAE;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,MAAM,EAAE,YAAY,CAClB,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,EACtD,mBAAmB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE,kBAAkB,GAAG,CAAC,CAAC,CACtF,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,cAAc,GAAG,WAAW,CAAC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;GAEG;AACH,qBACa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAE9C,UAAU,iDAAa;IAEjB,SAAS,CAAC,CAAC,SAAS,iBAAiB,CAAC,GAAG,CAAC,EAC9C,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EACrB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAkKzD,OAAO,CAAC,aAAa;yCA5KV,eAAe;6CAAf,eAAe;CAuL3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/angular",
3
- "version": "14.2.7",
3
+ "version": "14.2.8",
4
4
  "description": "심플리즘 패키지 - Angular",
5
5
  "license": "Apache-2.0",
6
6
  "author": "심플리즘",
@@ -58,10 +58,10 @@
58
58
  "jspdf": "^4.2.1",
59
59
  "rxjs": "^7.8.2",
60
60
  "tabbable": "^6.5.0",
61
- "@simplysm/service-client": "14.2.7",
62
- "@simplysm/core-browser": "14.2.7",
63
- "@simplysm/core-common": "14.2.7",
64
- "@simplysm/service-common": "14.2.7"
61
+ "@simplysm/core-common": "14.2.8",
62
+ "@simplysm/core-browser": "14.2.8",
63
+ "@simplysm/service-common": "14.2.8",
64
+ "@simplysm/service-client": "14.2.8"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@angular/compiler": "^22.0.8"
@@ -7,6 +7,68 @@ interface DragResizeOptions {
7
7
  onEnd: () => void;
8
8
  }
9
9
 
10
+ type SizeStyleProp = "width" | "height" | "maxWidth" | "maxHeight";
11
+
12
+ /** `auto`, `none` 등 길이가 아닌 값은 하한 없음(0)으로 본다. */
13
+ function parseCssMinPx(value: string): number {
14
+ const parsed = parseFloat(value);
15
+ return Number.isNaN(parsed) ? 0 : parsed;
16
+ }
17
+
18
+ function shiftPercentSize(
19
+ dialogStyle: CSSStyleDeclaration,
20
+ prop: SizeStyleProp,
21
+ padPx: number,
22
+ ): void {
23
+ if (padPx === 0) return;
24
+
25
+ const value = dialogStyle[prop];
26
+ if (!value.includes("%")) return;
27
+
28
+ dialogStyle[prop] = `calc(${value} - ${padPx}px)`;
29
+ }
30
+
31
+ /**
32
+ * `._dialog` 는 평소 CSS 로 중앙 정렬(`position: relative` + `margin: 0 auto`)되어 있어
33
+ * `left`/`top` 이 "정상 배치 위치 대비 상대 offset" 으로 해석된다.
34
+ * 드래그, 리사이즈는 절대 좌표로 계산하므로 시작 시점에 절대 좌표계로 전환한다.
35
+ * 전환만으로는 위치, 크기가 변하지 않으며, 이미 전환된 요소에 다시 적용해도 결과가 같다.
36
+ */
37
+ export function pinDialogAbsolute(dialogEl: HTMLElement): { left: number; top: number } {
38
+ const wasRelative = getComputedStyle(dialogEl).position === "relative";
39
+ const parentEl = dialogEl.offsetParent as HTMLElement | null;
40
+ const beforeRect = dialogEl.getBoundingClientRect();
41
+ const roundedLeft = dialogEl.offsetLeft;
42
+ const roundedTop = dialogEl.offsetTop;
43
+
44
+ dialogEl.style.position = "absolute";
45
+ dialogEl.style.margin = "0";
46
+ dialogEl.style.right = "auto";
47
+ dialogEl.style.bottom = "auto";
48
+ dialogEl.style.left = `${roundedLeft}px`;
49
+ dialogEl.style.top = `${roundedTop}px`;
50
+
51
+ if (wasRelative && parentEl != null) {
52
+ // 백분율 크기의 기준 박스가 부모 content box 에서 offsetParent padding box 로 바뀐다.
53
+ const parentStyle = getComputedStyle(parentEl);
54
+ const padX = parseFloat(parentStyle.paddingLeft) + parseFloat(parentStyle.paddingRight);
55
+ const padY = parseFloat(parentStyle.paddingTop) + parseFloat(parentStyle.paddingBottom);
56
+ shiftPercentSize(dialogEl.style, "width", padX);
57
+ shiftPercentSize(dialogEl.style, "maxWidth", padX);
58
+ shiftPercentSize(dialogEl.style, "height", padY);
59
+ shiftPercentSize(dialogEl.style, "maxHeight", padY);
60
+ }
61
+
62
+ // offsetLeft/offsetTop 은 정수로 반올림되므로, 전환 전후 실제 위치 차이만큼 되돌린다.
63
+ const afterRect = dialogEl.getBoundingClientRect();
64
+ const pinnedLeft = roundedLeft - (afterRect.left - beforeRect.left);
65
+ const pinnedTop = roundedTop - (afterRect.top - beforeRect.top);
66
+ dialogEl.style.left = `${pinnedLeft}px`;
67
+ dialogEl.style.top = `${pinnedTop}px`;
68
+
69
+ return { left: pinnedLeft, top: pinnedTop };
70
+ }
71
+
10
72
  export function injectDragResize(opt: DragResizeOptions): {
11
73
  startDrag: (event: MouseEvent) => void;
12
74
  startResize: (event: MouseEvent, dir: string) => void;
@@ -26,6 +88,8 @@ export function injectDragResize(opt: DragResizeOptions): {
26
88
  startHeight: number;
27
89
  startLeft: number;
28
90
  startTop: number;
91
+ minWidth: number;
92
+ minHeight: number;
29
93
  }
30
94
  | undefined;
31
95
 
@@ -50,8 +114,8 @@ export function injectDragResize(opt: DragResizeOptions): {
50
114
 
51
115
  const dx = event.clientX - resizeState.startX;
52
116
  const dy = event.clientY - resizeState.startY;
53
- const minW = opt.minWidthPx() ?? 0;
54
- const minH = opt.minHeightPx() ?? 0;
117
+ const minW = resizeState.minWidth;
118
+ const minH = resizeState.minHeight;
55
119
 
56
120
  let newWidth = resizeState.startWidth;
57
121
  let newHeight = resizeState.startHeight;
@@ -105,25 +169,17 @@ export function injectDragResize(opt: DragResizeOptions): {
105
169
  }
106
170
  }
107
171
 
108
- function getParentRect(dialogEl: HTMLElement): { left: number; top: number } {
109
- return (dialogEl.offsetParent as HTMLElement | null)?.getBoundingClientRect() ?? {
110
- left: 0,
111
- top: 0,
112
- };
113
- }
114
-
115
172
  function startDrag(event: MouseEvent): void {
116
173
  const dialogEl = opt.getDialogEl();
117
174
  if (dialogEl == null) return;
118
175
 
119
- const dialogRect = dialogEl.getBoundingClientRect();
120
- const parentRect = getParentRect(dialogEl);
176
+ const pinned = pinDialogAbsolute(dialogEl);
121
177
 
122
178
  dragState = {
123
179
  startX: event.clientX,
124
180
  startY: event.clientY,
125
- startLeft: dialogRect.left - parentRect.left,
126
- startTop: dialogRect.top - parentRect.top,
181
+ startLeft: pinned.left,
182
+ startTop: pinned.top,
127
183
  };
128
184
  document.addEventListener("mousemove", onDocumentMouseMove);
129
185
  document.addEventListener("mouseup", onDocumentMouseUp);
@@ -133,8 +189,8 @@ export function injectDragResize(opt: DragResizeOptions): {
133
189
  const dialogEl = opt.getDialogEl();
134
190
  if (dialogEl == null) return;
135
191
 
136
- const dialogRect = dialogEl.getBoundingClientRect();
137
- const parentRect = getParentRect(dialogEl);
192
+ const pinned = pinDialogAbsolute(dialogEl);
193
+ const dialogStyle = getComputedStyle(dialogEl);
138
194
 
139
195
  resizeState = {
140
196
  dir,
@@ -142,8 +198,11 @@ export function injectDragResize(opt: DragResizeOptions): {
142
198
  startY: event.clientY,
143
199
  startWidth: dialogEl.offsetWidth,
144
200
  startHeight: dialogEl.offsetHeight,
145
- startLeft: dialogRect.left - parentRect.left,
146
- startTop: dialogRect.top - parentRect.top,
201
+ startLeft: pinned.left,
202
+ startTop: pinned.top,
203
+ // CSS 하한을 넘겨 줄이면 실제 크기는 안 줄고 반대쪽 변만 밀려나므로, 실제 제약을 하한으로 삼는다.
204
+ minWidth: Math.max(opt.minWidthPx() ?? 0, parseCssMinPx(dialogStyle.minWidth)),
205
+ minHeight: Math.max(opt.minHeightPx() ?? 0, parseCssMinPx(dialogStyle.minHeight)),
147
206
  };
148
207
  document.addEventListener("mousemove", onDocumentMouseMove);
149
208
  document.addEventListener("mouseup", onDocumentMouseUp);
@@ -14,10 +14,7 @@ import {
14
14
  } from "@angular/core";
15
15
  import { outputToObservable } from "@angular/core/rxjs-interop";
16
16
  import { Subscription } from "rxjs";
17
- import type {
18
- DirectiveInputSignals,
19
- WithOptional,
20
- } from "../directive-input-signals";
17
+ import type { DirectiveInputSignals, WithOptional } from "../directive-input-signals";
21
18
  import { SdModal } from "./sd-modal";
22
19
  import { SdActivatedModalProvider } from "./sd-activated-modal.provider";
23
20
  import "@simplysm/core-browser";
@@ -18,7 +18,7 @@ import { tablerX } from "@ng-icons/tabler-icons";
18
18
  import { SdActivatedModalProvider } from "./sd-activated-modal.provider";
19
19
  import { SdSystemConfigProvider } from "../config/sd-system-config.provider";
20
20
  import { injectFocusTrap } from "./injectFocusTrap";
21
- import { injectDragResize } from "./injectDragResize";
21
+ import { injectDragResize, pinDialogAbsolute } from "./injectDragResize";
22
22
  import { SdAnchor } from "../../controls/button/sd-anchor";
23
23
  import { SdResizeDirective, type SdResizeEvent } from "../events/sd-resize";
24
24
  import "@simplysm/core-browser";
@@ -49,6 +49,7 @@ import "@simplysm/core-browser";
49
49
  <div
50
50
  class="_dialog"
51
51
  tabindex="-1"
52
+ (mousedown)="onDialogMouseDown()"
52
53
  (keydown)="onDialogKeydown($event)"
53
54
  (focus)="onDialogFocus()"
54
55
  (sdResize)="onDialogResize($event)"
@@ -368,6 +369,10 @@ export class SdModal {
368
369
  onEnd: () => void this._saveConfig().catch((err) => this._errorHandler.handleError(err)),
369
370
  });
370
371
 
372
+ /** 화면 넘침 클램프로 덮기 직전의 크기. 클램프 중이 아니면 undefined. */
373
+ private _clampedWidth: string | undefined;
374
+ private _clampedHeight: string | undefined;
375
+
371
376
  constructor() {
372
377
  // data-sd-init: 첫 렌더 후 설정하여 CSS transition 트리거 허용
373
378
  afterNextRender(() => {
@@ -391,6 +396,7 @@ export class SdModal {
391
396
  } else {
392
397
  dialogEl.style.height = "";
393
398
  }
399
+ this._forgetClampedSize();
394
400
  });
395
401
 
396
402
  // key 기반 설정 복원
@@ -404,9 +410,15 @@ export class SdModal {
404
410
 
405
411
  onResizeMouseDown(event: MouseEvent, dir: string): void {
406
412
  event.preventDefault();
413
+ // 사용자가 직접 정하는 크기가 자동 클램프 이전 크기보다 우선한다.
414
+ this._forgetClampedSize();
407
415
  this._dragResize.startResize(event, dir);
408
416
  }
409
417
 
418
+ onDialogMouseDown(): void {
419
+ this._bringToFront();
420
+ }
421
+
410
422
  onHeaderMouseDown(event: MouseEvent): void {
411
423
  if (!this.movable()) return;
412
424
  if ((event.target as HTMLElement).closest("button, sd-anchor") != null) return;
@@ -449,12 +461,20 @@ export class SdModal {
449
461
  onWindowResize(): void {
450
462
  const dialogEl = this._getDialogEl();
451
463
  if (dialogEl == null) return;
464
+ // 위치를 잡지 않은 모달은 CSS 중앙 정렬이 창 크기를 따라가므로 보정 대상이 아니다.
465
+ if (dialogEl.style.left === "" && dialogEl.style.top === "") return;
466
+ this._clampIntoHost(dialogEl);
467
+ }
468
+
469
+ private _clampIntoHost(dialogEl: HTMLElement): void {
452
470
  const hostEl = this._elRef.nativeElement;
453
- if (dialogEl.offsetLeft > hostEl.offsetWidth - 100) {
454
- dialogEl.style.left = hostEl.offsetWidth - 100 + "px";
471
+ const maxLeft = Math.max(0, hostEl.offsetWidth - 100);
472
+ if (dialogEl.offsetLeft > maxLeft) {
473
+ dialogEl.style.left = `${maxLeft}px`;
455
474
  }
456
- if (dialogEl.offsetTop > hostEl.offsetHeight - 100) {
457
- dialogEl.style.top = hostEl.offsetHeight - 100 + "px";
475
+ const maxTop = Math.max(0, hostEl.offsetHeight - 100);
476
+ if (dialogEl.offsetTop > maxTop) {
477
+ dialogEl.style.top = `${maxTop}px`;
458
478
  }
459
479
  }
460
480
 
@@ -482,23 +502,50 @@ export class SdModal {
482
502
  hostEl.style.zIndex = String(maxZ + 1);
483
503
  }
484
504
 
505
+ private _forgetClampedSize(): void {
506
+ this._clampedWidth = undefined;
507
+ this._clampedHeight = undefined;
508
+ }
509
+
485
510
  private _calcHeight(): void {
486
511
  const dialogEl = this._getDialogEl();
487
512
  if (dialogEl == null) return;
488
- const style = getComputedStyle(this._elRef.nativeElement);
489
- const paddingTop = style.paddingTop === "" ? 0 : parseInt(style.paddingTop, 10) || 0;
490
- if (dialogEl.offsetHeight > this._elRef.nativeElement.offsetHeight - paddingTop) {
513
+
514
+ // 클램프 중이면 dialog 높이가 호스트에 맞춰져 있어 실제 필요 높이를 수 없다. 먼저 되돌려 잰다.
515
+ if (this._clampedHeight != null) {
516
+ dialogEl.style.height = this._clampedHeight;
517
+ dialogEl.style.maxHeight = "";
518
+ }
519
+
520
+ const hostEl = this._elRef.nativeElement;
521
+ const paddingTop = parseFloat(getComputedStyle(hostEl).paddingTop) || 0;
522
+
523
+ if (dialogEl.offsetHeight > hostEl.offsetHeight - paddingTop) {
524
+ this._clampedHeight ??= dialogEl.style.height;
491
525
  dialogEl.style.maxHeight = "100%";
492
526
  dialogEl.style.height = "100%";
527
+ } else {
528
+ this._clampedHeight = undefined;
493
529
  }
494
530
  }
495
531
 
496
532
  private _calcWidth(): void {
497
533
  const dialogEl = this._getDialogEl();
498
534
  if (dialogEl == null) return;
499
- if (dialogEl.offsetWidth > this._elRef.nativeElement.offsetWidth) {
535
+
536
+ if (this._clampedWidth != null) {
537
+ dialogEl.style.width = this._clampedWidth;
538
+ dialogEl.style.maxWidth = "";
539
+ }
540
+
541
+ const hostEl = this._elRef.nativeElement;
542
+
543
+ if (dialogEl.offsetWidth > hostEl.offsetWidth) {
544
+ this._clampedWidth ??= dialogEl.style.width;
500
545
  dialogEl.style.maxWidth = "100%";
501
546
  dialogEl.style.width = "100%";
547
+ } else {
548
+ this._clampedWidth = undefined;
502
549
  }
503
550
  }
504
551
 
@@ -513,9 +560,13 @@ export class SdModal {
513
560
  const dialogEl = this._getDialogEl();
514
561
  if (dialogEl == null) return;
515
562
 
563
+ // 클램프 중이면 화면에 맞춘 값이 아니라 그 직전 크기를 저장해야 다시 열 때 원래 크기로 뜬다.
564
+ const savedWidth = this._clampedWidth ?? dialogEl.style.width;
565
+ const savedHeight = this._clampedHeight ?? dialogEl.style.height;
566
+
516
567
  const config: Record<string, string> = {};
517
- if (dialogEl.style.width !== "") config["width"] = dialogEl.style.width;
518
- if (dialogEl.style.height !== "") config["height"] = dialogEl.style.height;
568
+ if (savedWidth !== "") config["width"] = savedWidth;
569
+ if (savedHeight !== "") config["height"] = savedHeight;
519
570
  if (dialogEl.style.left !== "") config["left"] = dialogEl.style.left;
520
571
  if (dialogEl.style.top !== "") config["top"] = dialogEl.style.top;
521
572
 
@@ -526,7 +577,8 @@ export class SdModal {
526
577
  if (this._sdSystemConfig == null) return;
527
578
 
528
579
  const config = (await this._sdSystemConfig.getAsync(`sd-modal.${k}`)) as
529
- Record<string, string | undefined> | undefined;
580
+ | Record<string, string | undefined>
581
+ | undefined;
530
582
  if (config == null) return;
531
583
 
532
584
  const dialogEl = this._getDialogEl();
@@ -534,7 +586,13 @@ export class SdModal {
534
586
 
535
587
  if (config["width"] != null) dialogEl.style.width = config["width"];
536
588
  if (config["height"] != null) dialogEl.style.height = config["height"];
537
- if (config["left"] != null) dialogEl.style.left = config["left"];
538
- if (config["top"] != null) dialogEl.style.top = config["top"];
589
+ this._forgetClampedSize();
590
+
591
+ if (config["left"] != null || config["top"] != null) {
592
+ pinDialogAbsolute(dialogEl);
593
+ if (config["left"] != null) dialogEl.style.left = config["left"];
594
+ if (config["top"] != null) dialogEl.style.top = config["top"];
595
+ this._clampIntoHost(dialogEl);
596
+ }
539
597
  }
540
598
  }