ngx-kit 7.2.0 → 7.3.1

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.
Files changed (72) hide show
  1. package/angle-selector/README.md +34 -34
  2. package/box-shadow/README.md +33 -33
  3. package/color-picker/README.md +52 -52
  4. package/{shared → core}/README.md +117 -117
  5. package/data-table/README.md +120 -120
  6. package/date-picker/README.md +74 -74
  7. package/dialog/README.md +122 -92
  8. package/drag-resize/README.md +53 -55
  9. package/fesm2022/ngx-kit-angle-selector.mjs +9 -26
  10. package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
  11. package/fesm2022/ngx-kit-box-shadow.mjs +330 -356
  12. package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
  13. package/fesm2022/ngx-kit-color-picker.mjs +132 -272
  14. package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
  15. package/fesm2022/{ngx-kit-shared.mjs → ngx-kit-core.mjs} +845 -277
  16. package/fesm2022/ngx-kit-core.mjs.map +1 -0
  17. package/fesm2022/ngx-kit-data-table.mjs +3 -3
  18. package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
  19. package/fesm2022/ngx-kit-date-picker.mjs +153 -147
  20. package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
  21. package/fesm2022/ngx-kit-dialog.mjs +430 -331
  22. package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
  23. package/fesm2022/ngx-kit-drag-resize.mjs +126 -45
  24. package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -1
  25. package/fesm2022/ngx-kit-drawer-menu.mjs +2 -2
  26. package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
  27. package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
  28. package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
  29. package/fesm2022/ngx-kit-gallery.mjs +3 -3
  30. package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
  31. package/fesm2022/ngx-kit-gradient-picker.mjs +20 -38
  32. package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
  33. package/fesm2022/ngx-kit-grid-layout.mjs +19 -18
  34. package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -1
  35. package/fesm2022/ngx-kit-image-editor.mjs +3 -3
  36. package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
  37. package/fesm2022/ngx-kit-menu.mjs +1 -1
  38. package/fesm2022/ngx-kit-menu.mjs.map +1 -1
  39. package/fesm2022/ngx-kit-message.mjs +1 -1
  40. package/fesm2022/ngx-kit-message.mjs.map +1 -1
  41. package/fesm2022/ngx-kit-notify.mjs +1 -1
  42. package/fesm2022/ngx-kit-notify.mjs.map +1 -1
  43. package/fesm2022/ngx-kit-time-picker.mjs +1 -1
  44. package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
  45. package/fesm2022/ngx-kit-virtual-scroll.mjs +2 -2
  46. package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -1
  47. package/gallery/README.md +71 -71
  48. package/gradient-picker/README.md +41 -41
  49. package/grid-layout/README.md +64 -64
  50. package/image-editor/README.md +44 -44
  51. package/menu/README.md +85 -85
  52. package/message/README.md +70 -70
  53. package/notify/README.md +56 -56
  54. package/package.json +5 -9
  55. package/time-picker/README.md +89 -89
  56. package/types/ngx-kit-angle-selector.d.ts +13 -16
  57. package/types/ngx-kit-box-shadow.d.ts +37 -95
  58. package/types/ngx-kit-color-picker.d.ts +61 -58
  59. package/types/{ngx-kit-shared.d.ts → ngx-kit-core.d.ts} +156 -34
  60. package/types/ngx-kit-data-table.d.ts +2 -2
  61. package/types/ngx-kit-date-picker.d.ts +30 -29
  62. package/types/ngx-kit-dialog.d.ts +208 -107
  63. package/types/ngx-kit-drag-resize.d.ts +33 -6
  64. package/types/ngx-kit-gradient-picker.d.ts +4 -7
  65. package/types/ngx-kit-grid-layout.d.ts +0 -1
  66. package/types/ngx-kit-menu.d.ts +1 -1
  67. package/types/ngx-kit-time-picker.d.ts +1 -1
  68. package/virtual-scroll/README.md +199 -199
  69. package/fesm2022/ngx-kit-shared.mjs.map +0 -1
  70. package/fesm2022/ngx-kit-webflow.mjs +0 -709
  71. package/fesm2022/ngx-kit-webflow.mjs.map +0 -1
  72. package/types/ngx-kit-webflow.d.ts +0 -286
@@ -1,7 +1,7 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule, isPlatformBrowser, DOCUMENT } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { EventEmitter, forwardRef, HostListener, ViewChild, Output, Input, ChangeDetectionStrategy, Component, inject, ChangeDetectorRef, InjectionToken, PLATFORM_ID, Inject, Injectable, DestroyRef, signal, computed } from '@angular/core';
4
+ import { EventEmitter, forwardRef, HostListener, ViewChild, Output, Input, ChangeDetectionStrategy, Component, inject, ChangeDetectorRef, InjectionToken, PLATFORM_ID, Inject, Injectable, DestroyRef, signal, computed, ApplicationRef, EnvironmentInjector, createComponent } from '@angular/core';
5
5
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, Validators } from '@angular/forms';
6
6
 
7
7
  function getOffsetPosition(evt, parent) {
@@ -672,40 +672,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
672
672
  args: ['window:resize']
673
673
  }] } });
674
674
 
675
- function isPlainObject(value) {
676
- return (value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype);
677
- }
678
- function mergeConfig(base, override) {
679
- if (override == null) {
680
- return structuredClone(base);
681
- }
682
- return merge(base, override);
683
- }
684
- function merge(base, override) {
685
- if (override === undefined) {
686
- return base;
687
- }
688
- if (base === undefined) {
689
- return override;
690
- }
691
- if (Array.isArray(base) && Array.isArray(override)) {
692
- return [...override];
693
- }
694
- if (isPlainObject(base) && isPlainObject(override)) {
695
- const result = {};
696
- const keys = new Set([...Object.keys(base), ...Object.keys(override)]);
697
- for (const key of keys) {
698
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
699
- continue;
700
- }
701
- result[key] = merge(base[key], override[key]);
702
- }
703
- return result;
704
- }
705
- // Primitive / Function / Class / Date / RegExp ...
706
- return override;
707
- }
708
-
709
675
  const WINDOW = new InjectionToken('WINDOW', {
710
676
  factory: () => (typeof window !== 'undefined' ? window : null),
711
677
  });
@@ -865,6 +831,268 @@ function filterNodes(nodes, filterFn, childrenKeyName) {
865
831
  return result;
866
832
  }
867
833
 
834
+ const COLOR_FUNCTION_REGEX = /\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(\s*[^)]*\)/gi;
835
+ const HEX_COLOR_REGEX = /#[a-f\d]{3,4}(?:[a-f\d]{2}){0,2}\b/gi;
836
+ /**
837
+ * CSS named colors.
838
+ *
839
+ * Includes all standard CSS color keywords plus:
840
+ * - transparent
841
+ * - currentcolor
842
+ */
843
+ const CSS_COLOR_NAMES = {
844
+ aliceblue: '#f0f8ff',
845
+ antiquewhite: '#faebd7',
846
+ aqua: '#00ffff',
847
+ aquamarine: '#7fffd4',
848
+ azure: '#f0ffff',
849
+ beige: '#f5f5dc',
850
+ bisque: '#ffe4c4',
851
+ black: '#000000',
852
+ blanchedalmond: '#ffebcd',
853
+ blue: '#0000ff',
854
+ blueviolet: '#8a2be2',
855
+ brown: '#a52a2a',
856
+ burlywood: '#deb887',
857
+ cadetblue: '#5f9ea0',
858
+ chartreuse: '#7fff00',
859
+ chocolate: '#d2691e',
860
+ coral: '#ff7f50',
861
+ cornflowerblue: '#6495ed',
862
+ cornsilk: '#fff8dc',
863
+ crimson: '#dc143c',
864
+ cyan: '#00ffff',
865
+ darkblue: '#00008b',
866
+ darkcyan: '#008b8b',
867
+ darkgoldenrod: '#b8860b',
868
+ darkgray: '#a9a9a9',
869
+ darkgreen: '#006400',
870
+ darkgrey: '#a9a9a9',
871
+ darkkhaki: '#bdb76b',
872
+ darkmagenta: '#8b008b',
873
+ darkolivegreen: '#556b2f',
874
+ darkorange: '#ff8c00',
875
+ darkorchid: '#9932cc',
876
+ darkred: '#8b0000',
877
+ darksalmon: '#e9967a',
878
+ darkseagreen: '#8fbc8f',
879
+ darkslateblue: '#483d8b',
880
+ darkslategray: '#2f4f4f',
881
+ darkslategrey: '#2f4f4f',
882
+ darkturquoise: '#00ced1',
883
+ darkviolet: '#9400d3',
884
+ deeppink: '#ff1493',
885
+ deepskyblue: '#00bfff',
886
+ dimgray: '#696969',
887
+ dimgrey: '#696969',
888
+ dodgerblue: '#1e90ff',
889
+ firebrick: '#b22222',
890
+ floralwhite: '#fffaf0',
891
+ forestgreen: '#228b22',
892
+ fuchsia: '#ff00ff',
893
+ gainsboro: '#dcdcdc',
894
+ ghostwhite: '#f8f8ff',
895
+ gold: '#ffd700',
896
+ goldenrod: '#daa520',
897
+ gray: '#808080',
898
+ green: '#008000',
899
+ greenyellow: '#adff2f',
900
+ grey: '#808080',
901
+ honeydew: '#f0fff0',
902
+ hotpink: '#ff69b4',
903
+ indianred: '#cd5c5c',
904
+ indigo: '#4b0082',
905
+ ivory: '#fffff0',
906
+ khaki: '#f0e68c',
907
+ lavender: '#e6e6fa',
908
+ lavenderblush: '#fff0f5',
909
+ lawngreen: '#7cfc00',
910
+ lemonchiffon: '#fffacd',
911
+ lightblue: '#add8e6',
912
+ lightcoral: '#f08080',
913
+ lightcyan: '#e0ffff',
914
+ lightgoldenrodyellow: '#fafad2',
915
+ lightgray: '#d3d3d3',
916
+ lightgreen: '#90ee90',
917
+ lightgrey: '#d3d3d3',
918
+ lightpink: '#ffb6c1',
919
+ lightsalmon: '#ffa07a',
920
+ lightseagreen: '#20b2aa',
921
+ lightskyblue: '#87cefa',
922
+ lightslategray: '#778899',
923
+ lightslategrey: '#778899',
924
+ lightsteelblue: '#b0c4de',
925
+ lightyellow: '#ffffe0',
926
+ lime: '#00ff00',
927
+ limegreen: '#32cd32',
928
+ linen: '#faf0e6',
929
+ magenta: '#ff00ff',
930
+ maroon: '#800000',
931
+ mediumaquamarine: '#66cdaa',
932
+ mediumblue: '#0000cd',
933
+ mediumorchid: '#ba55d3',
934
+ mediumpurple: '#9370db',
935
+ mediumseagreen: '#3cb371',
936
+ mediumslateblue: '#7b68ee',
937
+ mediumspringgreen: '#00fa9a',
938
+ mediumturquoise: '#48d1cc',
939
+ mediumvioletred: '#c71585',
940
+ midnightblue: '#191970',
941
+ mintcream: '#f5fffa',
942
+ mistyrose: '#ffe4e1',
943
+ moccasin: '#ffe4b5',
944
+ navajowhite: '#ffdead',
945
+ navy: '#000080',
946
+ oldlace: '#fdf5e6',
947
+ olive: '#808000',
948
+ olivedrab: '#6b8e23',
949
+ orange: '#ffa500',
950
+ orangered: '#ff4500',
951
+ orchid: '#da70d6',
952
+ palegoldenrod: '#eee8aa',
953
+ palegreen: '#98fb98',
954
+ paleturquoise: '#afeeee',
955
+ palevioletred: '#db7093',
956
+ papayawhip: '#ffefd5',
957
+ peachpuff: '#ffdab9',
958
+ peru: '#cd853f',
959
+ pink: '#ffc0cb',
960
+ plum: '#dda0dd',
961
+ powderblue: '#b0e0e6',
962
+ purple: '#800080',
963
+ rebeccapurple: '#663399',
964
+ red: '#ff0000',
965
+ rosybrown: '#bc8f8f',
966
+ royalblue: '#4169e1',
967
+ saddlebrown: '#8b4513',
968
+ salmon: '#fa8072',
969
+ sandybrown: '#f4a460',
970
+ seagreen: '#2e8b57',
971
+ seashell: '#fff5ee',
972
+ sienna: '#a0522d',
973
+ silver: '#c0c0c0',
974
+ skyblue: '#87ceeb',
975
+ slateblue: '#6a5acd',
976
+ slategray: '#708090',
977
+ slategrey: '#708090',
978
+ snow: '#fffafa',
979
+ springgreen: '#00ff7f',
980
+ steelblue: '#4682b4',
981
+ tan: '#d2b48c',
982
+ teal: '#008080',
983
+ thistle: '#d8bfd8',
984
+ tomato: '#ff6347',
985
+ turquoise: '#40e0d0',
986
+ violet: '#ee82ee',
987
+ wheat: '#f5deb3',
988
+ white: '#ffffff',
989
+ whitesmoke: '#f5f5f5',
990
+ yellow: '#ffff00',
991
+ yellowgreen: '#9acd32',
992
+ };
993
+ /**
994
+ * Extract all CSS colors from an arbitrary CSS string.
995
+ *
996
+ * Supports:
997
+ * - Hex: #fff, #ffff, #ffffff, #ffffffff
998
+ * - rgb()
999
+ * - rgba()
1000
+ * - hsl()
1001
+ * - hsla()
1002
+ * - hwb()
1003
+ * - lab()
1004
+ * - lch()
1005
+ * - oklab()
1006
+ * - oklch()
1007
+ * - color()
1008
+ * - CSS named colors
1009
+ * - transparent
1010
+ * - currentcolor
1011
+ *
1012
+ * Works with values such as:
1013
+ *
1014
+ * box-shadow: 0 0 10px red, 0 0 20px #00ff00;
1015
+ * linear-gradient(red, rgba(0, 0, 0, .5));
1016
+ * radial-gradient(circle, blue 0%, transparent 100%);
1017
+ * conic-gradient(from 90deg, yellow, #ff0000);
1018
+ */
1019
+ function extractCssColors(value) {
1020
+ if (!value) {
1021
+ return [];
1022
+ }
1023
+ const colors = [];
1024
+ const seen = new Set();
1025
+ const addColor = (color) => {
1026
+ const normalized = color.trim();
1027
+ if (!normalized) {
1028
+ return;
1029
+ }
1030
+ const key = normalized.toLowerCase();
1031
+ if (seen.has(key)) {
1032
+ return;
1033
+ }
1034
+ seen.add(key);
1035
+ colors.push(normalized);
1036
+ };
1037
+ // ---------------------------------------------------------
1038
+ // 1. Hex colors
1039
+ // ---------------------------------------------------------
1040
+ for (const match of value.matchAll(HEX_COLOR_REGEX)) {
1041
+ addColor(match[0]);
1042
+ }
1043
+ // ---------------------------------------------------------
1044
+ // 2. CSS color functions
1045
+ // ---------------------------------------------------------
1046
+ for (const match of value.matchAll(COLOR_FUNCTION_REGEX)) {
1047
+ const candidate = match[0];
1048
+ if (isValidCssColor(candidate)) {
1049
+ addColor(candidate);
1050
+ }
1051
+ }
1052
+ // ---------------------------------------------------------
1053
+ // 3. Named colors
1054
+ // ---------------------------------------------------------
1055
+ /**
1056
+ * We intentionally extract words instead of putting all named
1057
+ * colors into one huge regex.
1058
+ */
1059
+ const words = value.match(/\b[a-z][a-z0-9-]*\b/gi) ?? [];
1060
+ for (const word of words) {
1061
+ const normalized = word.toLowerCase();
1062
+ if (!CSS_COLOR_NAMES[normalized]) {
1063
+ continue;
1064
+ }
1065
+ if (isValidCssColor(word)) {
1066
+ addColor(word);
1067
+ }
1068
+ }
1069
+ return colors;
1070
+ }
1071
+ /**
1072
+ * Uses the browser's CSS parser to validate a color.
1073
+ *
1074
+ * This is much safer than trying to validate every possible
1075
+ * CSS color syntax manually.
1076
+ */
1077
+ function isValidCssColor(value) {
1078
+ if (typeof CSS !== 'undefined' && typeof CSS.supports === 'function') {
1079
+ return CSS.supports('color', value);
1080
+ }
1081
+ // SSR / Node fallback.
1082
+ return isFallbackCssColor(value);
1083
+ }
1084
+ /**
1085
+ * Minimal validation for environments without CSS.supports(),
1086
+ * such as SSR or Node tests.
1087
+ */
1088
+ function isFallbackCssColor(value) {
1089
+ const normalized = value.trim().toLowerCase();
1090
+ if (CSS_COLOR_NAMES[normalized]) {
1091
+ return true;
1092
+ }
1093
+ return /^#[a-f\d]{3,4}(?:[a-f\d]{2}){0,2}$/i.test(normalized);
1094
+ }
1095
+
868
1096
  class BrowserService {
869
1097
  platformId;
870
1098
  constructor(platformId) {
@@ -948,341 +1176,681 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
948
1176
  }], ctorParameters: () => [] });
949
1177
 
950
1178
  class OverlayRef {
951
- dialogEl;
952
- teardownFn;
1179
+ element;
1180
+ closeFn;
953
1181
  componentRef;
954
- templateRef;
955
- constructor(dialogEl, teardownFn, componentRef, templateRef) {
956
- this.dialogEl = dialogEl;
957
- this.teardownFn = teardownFn;
1182
+ template;
1183
+ constructor(element, closeFn, componentRef, template) {
1184
+ this.element = element;
1185
+ this.closeFn = closeFn;
958
1186
  this.componentRef = componentRef;
959
- this.templateRef = templateRef;
1187
+ this.template = template;
960
1188
  }
961
1189
  close() {
962
- this.teardownFn();
1190
+ this.closeFn();
963
1191
  }
964
1192
  get nativeElement() {
965
- return this.dialogEl;
1193
+ return this.element;
1194
+ }
1195
+ static noop() {
1196
+ return new OverlayRef(null, () => { });
966
1197
  }
967
1198
  }
968
1199
 
969
- const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
1200
+ const FOCUSABLE_SELECTOR = [
1201
+ 'button:not([disabled])',
1202
+ '[href]',
1203
+ 'input:not([disabled])',
1204
+ 'select:not([disabled])',
1205
+ 'textarea:not([disabled])',
1206
+ '[tabindex]:not([tabindex="-1"])',
1207
+ ].join(',');
970
1208
  const DIALOG_OVERLAY_CLASSNAME = 'ngx-ui-overlay';
1209
+ const OVERLAY_HOST_CLASSNAME = 'ngx-ui-overlay-host';
1210
+ const BASE_Z_INDEX = 1000;
971
1211
  class OverlayService {
972
- overlayStacks = [];
973
- listenersAttached = false;
974
- resizeRafPending = false;
975
- doc = inject(DOCUMENT);
976
- abortController = new AbortController();
1212
+ document = inject(DOCUMENT);
977
1213
  directionService = inject(DirectionService);
978
- ngOnDestroy() {
979
- this.closeAll();
980
- this.abortController.abort();
1214
+ platformId = inject(PLATFORM_ID);
1215
+ applicationRef = inject(ApplicationRef);
1216
+ environmentInjector = inject(EnvironmentInjector);
1217
+ overlayStack = [];
1218
+ globalListenersAttached = false;
1219
+ layoutRaf = null;
1220
+ bodyScrollLockCount = 0;
1221
+ previousBodyOverflow = null;
1222
+ previousBodyPaddingRight = null;
1223
+ get isBrowser() {
1224
+ return isPlatformBrowser(this.platformId);
1225
+ }
1226
+ /** Number of overlays currently open. */
1227
+ get openCount() {
1228
+ return this.overlayStack.length;
981
1229
  }
982
1230
  open(options) {
983
- const { anchor, point, component, viewContainerRef, configure, onClosed, placement, alignment, margin, } = options;
984
- if (!viewContainerRef) {
985
- throw new Error('ViewContainerRef is required to render overlay content.');
1231
+ if (!this.isBrowser) {
1232
+ return OverlayRef.noop();
1233
+ }
1234
+ const { component, viewContainerRef, configure, onClosed } = options;
1235
+ const instance = this.createComponentInstance(options, component, viewContainerRef);
1236
+ this.pushInstance(instance);
1237
+ this.renderHost(instance);
1238
+ this.attachGlobalListeners();
1239
+ instance.onClosed = onClosed;
1240
+ configure?.(instance.componentRef.instance, new OverlayRef(instance.element, instance.cleanup, instance.componentRef));
1241
+ this.scheduleInitialLayout(instance);
1242
+ return new OverlayRef(instance.element, instance.cleanup, instance.componentRef);
1243
+ }
1244
+ openTemplate(options) {
1245
+ if (!this.isBrowser) {
1246
+ return OverlayRef.noop();
986
1247
  }
1248
+ const { template, appRef, configure, onClosed } = options;
1249
+ const instance = this.createTemplateInstance(options, template, appRef ?? this.applicationRef);
1250
+ this.pushInstance(instance);
1251
+ this.renderHost(instance);
987
1252
  this.attachGlobalListeners();
988
- const element = this.createDialogElement();
989
- const componentRef = viewContainerRef.createComponent(component);
990
- if (componentRef.location.nativeElement) {
991
- element.appendChild(componentRef.location.nativeElement);
1253
+ instance.onClosed = onClosed;
1254
+ const ref = new OverlayRef(instance.element, instance.cleanup, undefined, template);
1255
+ configure?.(instance, ref);
1256
+ this.scheduleInitialLayout(instance);
1257
+ return ref;
1258
+ }
1259
+ closeAll() {
1260
+ if (!this.isBrowser) {
1261
+ return;
1262
+ }
1263
+ [...this.overlayStack].forEach((instance) => {
1264
+ instance.cleanup();
1265
+ });
1266
+ }
1267
+ // ---------------------------------------------------------------------------
1268
+ // instance creation
1269
+ // ---------------------------------------------------------------------------
1270
+ createComponentInstance(options, component, viewContainerRef) {
1271
+ const host = this.createHostElement(options);
1272
+ const backdrop = this.createBackdropElement(options);
1273
+ const element = this.createOverlayElement(options);
1274
+ host.appendChild(backdrop);
1275
+ host.appendChild(element);
1276
+ let componentRef;
1277
+ let attachedToAppRef = false;
1278
+ if (viewContainerRef) {
1279
+ componentRef = options.injector
1280
+ ? viewContainerRef.createComponent(component, { injector: options.injector })
1281
+ : viewContainerRef.createComponent(component);
992
1282
  }
1283
+ else {
1284
+ // No host ViewContainerRef available (e.g. a global service opening an
1285
+ // overlay outside of any component template) - create and attach the
1286
+ // component directly against the application, like a root component.
1287
+ componentRef = createComponent(component, {
1288
+ environmentInjector: this.environmentInjector,
1289
+ elementInjector: options.injector,
1290
+ });
1291
+ this.applicationRef.attachView(componentRef.hostView);
1292
+ attachedToAppRef = true;
1293
+ }
1294
+ element.appendChild(componentRef.location.nativeElement);
993
1295
  const instance = {
1296
+ host,
1297
+ backdrop,
994
1298
  element,
995
- anchor,
996
- point,
997
- placementConfig: this.resolvePlacementConfig(placement, alignment, margin),
1299
+ anchor: options.anchor,
1300
+ point: options.point,
1301
+ placementConfig: this.resolvePlacementConfig(options.placement, options.alignment, options.margin),
1302
+ usePopover: this.resolveUsePopover(options),
1303
+ closeOnEscape: options.closeOnEscape ?? true,
1304
+ closeOnOutsideClick: options.closeOnOutsideClick ?? true,
1305
+ canClose: options.canClose,
1306
+ autoFocus: options.autoFocus ?? true,
1307
+ restoreFocus: options.restoreFocus ?? true,
1308
+ lockBodyScroll: options.lockBodyScroll ?? false,
998
1309
  componentRef,
999
- onClosed,
1310
+ attachedToAppRef,
1311
+ previousActiveElement: this.getActiveElement(),
1000
1312
  rafId: null,
1001
1313
  focusTimeoutId: null,
1002
1314
  cleanup: () => { },
1003
1315
  };
1004
1316
  instance.cleanup = () => this.destroyInstance(instance);
1005
- this.overlayStacks.push(instance);
1006
- element.showModal();
1007
- const ref = new OverlayRef(element, instance.cleanup, componentRef);
1008
- configure?.(componentRef.instance, ref);
1009
- this.scheduleInitialLayout(instance);
1010
- return ref;
1011
- }
1012
- openTemplate(options) {
1013
- const { anchor, point, template, appRef, configure, onClosed, placement, alignment, margin } = options;
1014
- this.attachGlobalListeners();
1015
- const element = this.createDialogElement();
1317
+ return instance;
1318
+ }
1319
+ createTemplateInstance(options, template, appRef) {
1320
+ const host = this.createHostElement(options);
1321
+ const backdrop = this.createBackdropElement(options);
1322
+ const element = this.createOverlayElement(options);
1323
+ host.appendChild(backdrop);
1324
+ host.appendChild(element);
1016
1325
  const view = template.createEmbeddedView({});
1017
1326
  appRef.attachView(view);
1018
1327
  element.append(...view.rootNodes);
1019
1328
  const instance = {
1329
+ host,
1330
+ backdrop,
1020
1331
  element,
1021
- anchor,
1022
- point,
1023
- placementConfig: this.resolvePlacementConfig(placement, alignment, margin),
1332
+ anchor: options.anchor,
1333
+ point: options.point,
1334
+ placementConfig: this.resolvePlacementConfig(options.placement, options.alignment, options.margin),
1335
+ usePopover: this.resolveUsePopover(options),
1336
+ closeOnEscape: options.closeOnEscape ?? true,
1337
+ closeOnOutsideClick: options.closeOnOutsideClick ?? true,
1338
+ canClose: options.canClose,
1339
+ autoFocus: options.autoFocus ?? true,
1340
+ restoreFocus: options.restoreFocus ?? true,
1341
+ lockBodyScroll: options.lockBodyScroll ?? false,
1024
1342
  embeddedView: view,
1025
1343
  appRef,
1026
- onClosed,
1344
+ previousActiveElement: this.getActiveElement(),
1027
1345
  rafId: null,
1028
1346
  focusTimeoutId: null,
1029
1347
  cleanup: () => { },
1030
1348
  };
1031
1349
  instance.cleanup = () => this.destroyInstance(instance);
1032
- this.overlayStacks.push(instance);
1033
- element.showModal();
1034
- const ref = new OverlayRef(element, instance.cleanup, undefined, template);
1035
- configure?.(instance, ref);
1036
- this.scheduleInitialLayout(instance);
1037
- return ref;
1038
- }
1039
- closeAll() {
1040
- // اسنپ‌شات می‌گیریم چون cleanup حین اجرا آرایه‌ی اصلی رو تغییر می‌ده
1041
- [...this.overlayStacks].forEach((instance) => instance.cleanup());
1042
- }
1043
- // ---------------------------------------------------------------------
1044
- // lifecycle
1045
- // ---------------------------------------------------------------------
1046
- resolvePlacementConfig(placement, alignment, margin) {
1047
- return {
1048
- placement: placement ?? 'auto',
1049
- alignment: alignment ?? 'start',
1050
- margin: margin ?? 8,
1051
- };
1350
+ return instance;
1351
+ }
1352
+ pushInstance(instance) {
1353
+ this.overlayStack.push(instance);
1354
+ // Only matters as a fallback for browsers without the Popover API -
1355
+ // real popovers stack by top-layer show order regardless of z-index.
1356
+ instance.host.style.zIndex = String(BASE_Z_INDEX + this.overlayStack.length);
1357
+ if (instance.lockBodyScroll) {
1358
+ this.lockBodyScroll();
1359
+ }
1052
1360
  }
1053
- createDialogElement() {
1054
- const element = this.doc.createElement('dialog');
1361
+ // ---------------------------------------------------------------------------
1362
+ // DOM
1363
+ // ---------------------------------------------------------------------------
1364
+ createHostElement(options) {
1365
+ const usePopover = this.resolveUsePopover(options);
1366
+ const host = this.document.createElement('div');
1367
+ host.className = OVERLAY_HOST_CLASSNAME;
1368
+ host.setAttribute('data-ngx-overlay', '');
1369
+ host.style.position = 'fixed';
1370
+ host.style.inset = '0';
1371
+ // Neutralize the browser's default `[popover]` UA stylesheet
1372
+ // (margin: auto; width/height: fit-content; border: solid; padding: .25em;
1373
+ // background: Canvas; overflow: auto) so the host reliably fills the
1374
+ // viewport whether or not it ends up being a real popover.
1375
+ host.style.margin = '0';
1376
+ host.style.padding = '0';
1377
+ host.style.border = 'none';
1378
+ host.style.width = '100%';
1379
+ host.style.height = '100%';
1380
+ host.style.maxWidth = 'none';
1381
+ host.style.maxHeight = 'none';
1382
+ host.style.overflow = 'visible';
1383
+ host.style.background = 'transparent';
1384
+ host.style.color = 'inherit';
1385
+ host.style.zIndex = String(BASE_Z_INDEX);
1386
+ host.style.pointerEvents = 'auto';
1387
+ // Popover goes on the ROOT element, not the inner panel. This puts the
1388
+ // whole overlay (backdrop + panel) in the top layer as a single unit,
1389
+ // so stacking "just works" above everything on the page - including
1390
+ // ancestors with `overflow: hidden`, `transform`, or their own z-index
1391
+ // stacking contexts, which regular `position: fixed` cannot escape.
1392
+ // The inner `element` stays a normal `position: fixed` div and keeps
1393
+ // computing against the viewport, so all the placement math is unchanged.
1394
+ if (usePopover) {
1395
+ host.setAttribute('popover', 'manual');
1396
+ }
1397
+ return host;
1398
+ }
1399
+ createBackdropElement(options) {
1400
+ const backdrop = this.document.createElement('div');
1401
+ backdrop.className = 'ngx-ui-overlay-backdrop';
1402
+ backdrop.style.position = 'absolute';
1403
+ backdrop.style.inset = '0';
1404
+ backdrop.style.pointerEvents = 'auto';
1405
+ backdrop.style.background = 'transparent';
1406
+ this.addClasses(backdrop, options.backdropClass);
1407
+ return backdrop;
1408
+ }
1409
+ createOverlayElement(options) {
1410
+ const element = this.document.createElement('div');
1055
1411
  element.className = DIALOG_OVERLAY_CLASSNAME;
1056
- const style = element.style;
1057
- style.position = 'absolute';
1058
- style.outline = 'none';
1059
- style.padding = '0';
1060
- style.margin = '0';
1061
- style.border = 'none';
1062
- style.background = 'transparent';
1063
- style.maxWidth = '100vw';
1064
- style.maxHeight = '100vh';
1065
- style.overflow = 'visible';
1066
- style.visibility = 'hidden';
1067
- this.doc.body.appendChild(element);
1412
+ element.setAttribute('role', options.role ?? 'dialog');
1413
+ if (options.ariaModal ?? true) {
1414
+ element.setAttribute('aria-modal', 'true');
1415
+ }
1416
+ if (options.ariaLabel) {
1417
+ element.setAttribute('aria-label', options.ariaLabel);
1418
+ }
1419
+ if (options.ariaLabelledby) {
1420
+ element.setAttribute('aria-labelledby', options.ariaLabelledby);
1421
+ }
1422
+ if (options.ariaDescribedby) {
1423
+ element.setAttribute('aria-describedby', options.ariaDescribedby);
1424
+ }
1425
+ element.setAttribute('tabindex', '-1');
1426
+ element.style.position = 'fixed';
1427
+ element.style.pointerEvents = 'auto';
1428
+ element.style.boxSizing = 'border-box';
1429
+ element.style.maxWidth = '100dvw'; // 'calc(100vw - 16px)';
1430
+ element.style.maxHeight = '100dvh'; // 'calc(100vh - 16px)';
1431
+ element.style.outline = 'none';
1432
+ element.style.border = 'none';
1433
+ element.style.padding = '0';
1434
+ element.style.margin = '0';
1435
+ element.style.overflow = 'auto';
1436
+ element.style.transformOrigin = 'left top';
1437
+ element.style.background = 'none';
1438
+ element.style.boxShadow = '0 0 20px light-dark(#79797955, #020202bd)';
1439
+ element.style.borderRadius = 'var(--ngx-dialog-corner-radius,1rem)';
1440
+ this.addClasses(element, options.panelClass);
1068
1441
  return element;
1069
1442
  }
1070
- scheduleInitialLayout(instance) {
1071
- instance.rafId = requestAnimationFrame(() => {
1072
- instance.rafId = null;
1073
- this.positionDialog(instance);
1074
- });
1075
- instance.focusTimeoutId = setTimeout(() => {
1076
- instance.focusTimeoutId = null;
1077
- const focusable = instance.element.querySelector(FOCUSABLE_SELECTOR);
1078
- focusable?.focus();
1079
- }, 0);
1443
+ addClasses(element, classes) {
1444
+ if (!classes) {
1445
+ return;
1446
+ }
1447
+ const list = Array.isArray(classes) ? classes : classes.split(' ');
1448
+ for (const c of list) {
1449
+ if (c.trim()) {
1450
+ element.classList.add(c.trim());
1451
+ }
1452
+ }
1080
1453
  }
1454
+ renderHost(instance) {
1455
+ this.document.body.appendChild(instance.host);
1456
+ // Must show the popover (enter the top layer) BEFORE we ever measure
1457
+ // `instance.element`'s rect - while a popover host is closed it's
1458
+ // `display: none`, and everything inside it (including the panel)
1459
+ // would measure as a 0x0 box.
1460
+ this.showHostPopover(instance);
1461
+ instance.element.style.visibility = 'hidden';
1462
+ instance.element.style.position = 'fixed';
1463
+ instance.element.style.top = '0';
1464
+ instance.element.style.left = '0';
1465
+ }
1466
+ // ---------------------------------------------------------------------------
1467
+ // lifecycle
1468
+ // ---------------------------------------------------------------------------
1081
1469
  destroyInstance(instance) {
1082
- const index = this.overlayStacks.indexOf(instance);
1083
- if (index === -1)
1084
- return; // جلوگیری از پاک‌سازی دوباره
1085
- this.overlayStacks.splice(index, 1);
1086
- // لغو کارهای زمان‌بندی‌شده‌ای که هنوز اجرا نشدن (نشتی حافظه در نسخه‌ی قبلی)
1087
- if (instance.rafId !== null)
1088
- cancelAnimationFrame(instance.rafId);
1089
- if (instance.focusTimeoutId !== null)
1470
+ const index = this.overlayStack.indexOf(instance);
1471
+ if (index === -1) {
1472
+ return;
1473
+ }
1474
+ this.overlayStack.splice(index, 1);
1475
+ if (instance.rafId !== null) {
1476
+ this.cancelAnimationFrame(instance.rafId);
1477
+ instance.rafId = null;
1478
+ }
1479
+ if (instance.focusTimeoutId !== null) {
1090
1480
  clearTimeout(instance.focusTimeoutId);
1481
+ instance.focusTimeoutId = null;
1482
+ }
1091
1483
  if (instance.componentRef) {
1092
- instance.componentRef.location.nativeElement?.remove();
1484
+ if (instance.attachedToAppRef) {
1485
+ this.applicationRef.detachView(instance.componentRef.hostView);
1486
+ }
1093
1487
  instance.componentRef.destroy();
1488
+ instance.componentRef = undefined;
1094
1489
  }
1095
1490
  if (instance.embeddedView && instance.appRef) {
1096
1491
  instance.appRef.detachView(instance.embeddedView);
1097
1492
  instance.embeddedView.destroy();
1493
+ instance.embeddedView = undefined;
1494
+ }
1495
+ this.hideHostPopover(instance);
1496
+ instance.host.remove();
1497
+ if (instance.lockBodyScroll) {
1498
+ this.unlockBodyScroll();
1499
+ }
1500
+ instance.onClosed?.();
1501
+ // Only tear down the shared/global listeners once *every* overlay is
1502
+ // closed - previously this ran unconditionally on every close, which
1503
+ // silently broke Escape/outside-click/reposition for any overlays that
1504
+ // were still open underneath.
1505
+ if (this.overlayStack.length === 0) {
1506
+ this.detachGlobalListeners();
1507
+ }
1508
+ if (instance.restoreFocus) {
1509
+ this.restoreFocus(instance.previousActiveElement);
1510
+ }
1511
+ }
1512
+ // ---------------------------------------------------------------------------
1513
+ // popover helpers
1514
+ // ---------------------------------------------------------------------------
1515
+ resolveUsePopover(options) {
1516
+ return (options.usePopover ?? true) && this.supportsPopover();
1517
+ }
1518
+ showHostPopover(instance) {
1519
+ if (!instance.usePopover) {
1520
+ return;
1098
1521
  }
1099
- // قبل از remove()، دیالوگ رو با متد استاندارد close() می‌بندیم؛ این کار دو
1100
- // فایده داره که با remove() ساده به دست نمی‌اومدن:
1101
- // ۱) فوکوس به‌صورت خودکار به همون المنتی که قبل از باز شدن دیالوگ فوکوس
1102
- // داشت برمی‌گرده (رفتار استاندارد close() روی <dialog>) — با remove()
1103
- // ساده این اتفاق نمی‌افته و فوکوس معمولاً روی body گم می‌شه.
1104
- // ۲) رویداد 'close' استاندارد شلیک می‌شه، برای کدی که ممکنه به اون گوش بده.
1105
- // در try/catch گذاشتیمش چون در محیط تست (jsdom) این متدها mock هستن و ممکنه
1106
- // رفتار متفاوتی داشته باشن.
1107
1522
  try {
1108
- if (instance.element.open)
1109
- instance.element.close();
1523
+ instance.host.showPopover();
1110
1524
  }
1111
1525
  catch {
1112
- /* noop */
1526
+ // Already open, or environment doesn't fully support it - ignore.
1113
1527
  }
1114
- instance.element.remove();
1115
- instance.onClosed?.();
1116
1528
  }
1117
- // ---------------------------------------------------------------------
1118
- // global listeners (فقط یک‌بار در کل عمر سرویس attach می‌شن)
1119
- // ---------------------------------------------------------------------
1529
+ hideHostPopover(instance) {
1530
+ if (!instance.usePopover) {
1531
+ return;
1532
+ }
1533
+ try {
1534
+ if (instance.host.matches(':popover-open')) {
1535
+ instance.host.hidePopover();
1536
+ }
1537
+ }
1538
+ catch {
1539
+ // Not open, or already removed from the DOM - ignore.
1540
+ }
1541
+ }
1542
+ supportsPopover() {
1543
+ return (typeof HTMLElement !== 'undefined' &&
1544
+ 'popover' in HTMLElement.prototype &&
1545
+ typeof HTMLElement.prototype.showPopover === 'function' &&
1546
+ typeof HTMLElement.prototype.hidePopover === 'function');
1547
+ }
1548
+ // ---------------------------------------------------------------------------
1549
+ // body scroll lock
1550
+ // ---------------------------------------------------------------------------
1551
+ lockBodyScroll() {
1552
+ this.bodyScrollLockCount++;
1553
+ if (this.bodyScrollLockCount > 1) {
1554
+ return;
1555
+ }
1556
+ const body = this.document.body;
1557
+ this.previousBodyOverflow = body.style.overflow;
1558
+ this.previousBodyPaddingRight = body.style.paddingRight;
1559
+ const scrollbarWidth = window.innerWidth - this.document.documentElement.clientWidth;
1560
+ body.style.overflow = 'hidden';
1561
+ if (scrollbarWidth > 0) {
1562
+ const currentPaddingRight = parseFloat(window.getComputedStyle(body).paddingRight) || 0;
1563
+ body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
1564
+ }
1565
+ }
1566
+ unlockBodyScroll() {
1567
+ if (this.bodyScrollLockCount === 0) {
1568
+ return;
1569
+ }
1570
+ this.bodyScrollLockCount--;
1571
+ if (this.bodyScrollLockCount > 0) {
1572
+ return;
1573
+ }
1574
+ const body = this.document.body;
1575
+ body.style.overflow = this.previousBodyOverflow ?? '';
1576
+ body.style.paddingRight = this.previousBodyPaddingRight ?? '';
1577
+ this.previousBodyOverflow = null;
1578
+ this.previousBodyPaddingRight = null;
1579
+ }
1580
+ // ---------------------------------------------------------------------------
1581
+ // global listeners
1582
+ // ---------------------------------------------------------------------------
1120
1583
  attachGlobalListeners() {
1121
- if (this.listenersAttached)
1584
+ if (!this.isBrowser || this.globalListenersAttached) {
1122
1585
  return;
1123
- this.listenersAttached = true;
1124
- const { signal } = this.abortController;
1125
- this.doc.addEventListener('keydown', this.onGlobalKeyDown, { signal });
1126
- window.addEventListener('resize', this.onGlobalResize, { signal, passive: true });
1127
- this.doc.addEventListener('click', this.onGlobalPointerEvent, { signal });
1128
- this.doc.addEventListener('contextmenu', this.onGlobalPointerEvent, { signal });
1129
- }
1130
- onGlobalKeyDown = (e) => {
1131
- if (e.key !== 'Escape')
1586
+ }
1587
+ // Flip the flag synchronously to avoid a race: if two overlays open in
1588
+ // the same tick, the second call must see the flag already set instead
1589
+ // of scheduling a second (duplicate) round of addEventListener calls.
1590
+ this.globalListenersAttached = true;
1591
+ setTimeout(() => {
1592
+ this.document.addEventListener('keydown', this.onDocumentKeydown);
1593
+ this.document.addEventListener('click', this.onDocumentClick);
1594
+ this.document.addEventListener('contextmenu', this.onDocumentContextMenu);
1595
+ this.document.addEventListener('scroll', this.onDocumentScroll, true);
1596
+ window.addEventListener('resize', this.onWindowResize);
1597
+ }, 0);
1598
+ }
1599
+ detachGlobalListeners() {
1600
+ if (!this.isBrowser || !this.globalListenersAttached) {
1132
1601
  return;
1133
- this.getLastDialog()?.cleanup();
1134
- };
1135
- onGlobalResize = () => {
1136
- // چند رویداد resize پشت‌سرهم فقط یک reposition واقعی اجرا می‌کنن
1137
- if (this.resizeRafPending)
1602
+ }
1603
+ this.globalListenersAttached = false;
1604
+ this.document.removeEventListener('keydown', this.onDocumentKeydown);
1605
+ this.document.removeEventListener('click', this.onDocumentClick);
1606
+ this.document.removeEventListener('contextmenu', this.onDocumentContextMenu);
1607
+ this.document.removeEventListener('scroll', this.onDocumentScroll, true);
1608
+ window.removeEventListener('resize', this.onWindowResize);
1609
+ }
1610
+ onDocumentKeydown = (event) => {
1611
+ const instance = this.getLastInstance();
1612
+ if (!instance) {
1138
1613
  return;
1139
- this.resizeRafPending = true;
1140
- requestAnimationFrame(() => {
1141
- this.resizeRafPending = false;
1142
- this.repositionAll();
1143
- });
1614
+ }
1615
+ if (event.key === 'Escape') {
1616
+ if (!instance.closeOnEscape || (instance.canClose && !instance.canClose())) {
1617
+ return;
1618
+ }
1619
+ event.preventDefault();
1620
+ instance.cleanup();
1621
+ return;
1622
+ }
1623
+ if (event.key === 'Tab') {
1624
+ this.trapFocus(event, instance);
1625
+ }
1144
1626
  };
1145
- onGlobalPointerEvent = (e) => {
1146
- this.handleClickOnBackdrop(e);
1627
+ onDocumentClick = (event) => {
1628
+ const instance = this.getLastInstance();
1629
+ if (!instance) {
1630
+ return;
1631
+ }
1632
+ const target = event.target;
1633
+ if (!target) {
1634
+ return;
1635
+ }
1636
+ if (instance.element.contains(target)) {
1637
+ return;
1638
+ }
1639
+ if (instance.anchor && instance.anchor.contains(target)) {
1640
+ return;
1641
+ }
1642
+ if (!instance.closeOnOutsideClick || (instance.canClose && !instance.canClose())) {
1643
+ return;
1644
+ }
1645
+ if (event.target !== instance.backdrop) {
1646
+ return;
1647
+ }
1648
+ instance.cleanup();
1147
1649
  };
1148
- handleClickOnBackdrop(event) {
1149
- const lastDialog = this.getLastDialog();
1150
- if (!lastDialog || !lastDialog.element.open)
1650
+ onDocumentContextMenu = (event) => {
1651
+ const instance = this.getLastInstance();
1652
+ if (!instance) {
1151
1653
  return;
1152
- // کلیک باید دقیقاً روی خودِ دیالوگ (بک‌دراپ) باشه، نه محتوای داخلش
1153
- if (event.target !== lastDialog.element)
1654
+ }
1655
+ if (instance.point && instance.anchor && instance.anchor.contains(event.target)) {
1656
+ event.preventDefault();
1657
+ instance.point = {
1658
+ x: event.clientX,
1659
+ y: event.clientY,
1660
+ };
1661
+ this.scheduleReposition();
1154
1662
  return;
1155
- // اگه فرم داخل دیالوگه، با کلیک بیرون بسته نشه
1156
- if (lastDialog.element.querySelector('form'))
1663
+ }
1664
+ if (instance.element.contains(event.target)) {
1157
1665
  return;
1158
- const rect = lastDialog.element.getBoundingClientRect();
1159
- const clickWasInsideDialog = rect.top <= event.clientY &&
1160
- event.clientY <= rect.bottom &&
1161
- rect.left <= event.clientX &&
1162
- event.clientX <= rect.right;
1163
- const anchRect = lastDialog.anchor.getBoundingClientRect();
1164
- const clickWasInsideAnchor = anchRect.top <= event.clientY &&
1165
- event.clientY <= anchRect.bottom &&
1166
- anchRect.left <= event.clientX &&
1167
- event.clientX <= anchRect.right;
1168
- if (!clickWasInsideDialog) {
1169
- if (event.type == 'contextmenu' && lastDialog.point && clickWasInsideAnchor) {
1170
- event.preventDefault();
1171
- lastDialog.point = {
1172
- x: event.clientX,
1173
- y: event.clientY,
1174
- };
1175
- this.positionDialog(lastDialog);
1176
- }
1177
- else {
1178
- lastDialog.cleanup();
1179
- }
1180
1666
  }
1667
+ if (!instance.closeOnOutsideClick || (instance.canClose && !instance.canClose())) {
1668
+ return;
1669
+ }
1670
+ if (event.target !== instance.backdrop) {
1671
+ return;
1672
+ }
1673
+ instance.cleanup();
1674
+ };
1675
+ onDocumentScroll = () => {
1676
+ this.scheduleReposition();
1677
+ };
1678
+ onWindowResize = () => {
1679
+ this.scheduleReposition();
1680
+ };
1681
+ scheduleReposition() {
1682
+ if (this.layoutRaf !== null) {
1683
+ return;
1684
+ }
1685
+ this.layoutRaf = this.requestAnimationFrame(() => {
1686
+ this.layoutRaf = null;
1687
+ const instances = [...this.overlayStack];
1688
+ for (const instance of instances) {
1689
+ this.positionOverlay(instance);
1690
+ }
1691
+ });
1181
1692
  }
1182
- getLastDialog() {
1183
- return this.overlayStacks[this.overlayStacks.length - 1];
1184
- }
1185
- // ---------------------------------------------------------------------
1186
- // positioning
1187
- // ---------------------------------------------------------------------
1188
- repositionAll() {
1189
- this.overlayStacks.forEach((instance) => {
1190
- if (instance.element.isConnected) {
1191
- this.positionDialog(instance);
1693
+ // ---------------------------------------------------------------------------
1694
+ // focus
1695
+ // ---------------------------------------------------------------------------
1696
+ scheduleInitialLayout(instance) {
1697
+ instance.rafId = this.requestAnimationFrame(() => {
1698
+ instance.rafId = null;
1699
+ this.positionOverlay(instance);
1700
+ if (!instance.autoFocus) {
1701
+ return;
1192
1702
  }
1703
+ instance.focusTimeoutId = setTimeout(() => {
1704
+ instance.focusTimeoutId = null;
1705
+ this.focusInitialElement(instance);
1706
+ }, 0);
1193
1707
  });
1194
1708
  }
1195
- positionDialog(instance) {
1196
- const { anchor, element: dialog, placementConfig, point } = instance;
1197
- const { placement, alignment, margin } = placementConfig;
1198
- if (!dialog.isConnected)
1709
+ focusInitialElement(instance) {
1710
+ const focusable = instance.element.querySelector(FOCUSABLE_SELECTOR);
1711
+ focusable?.focus() ?? instance.element.focus();
1712
+ }
1713
+ trapFocus(event, instance) {
1714
+ const elements = Array.from(instance.element.querySelectorAll(FOCUSABLE_SELECTOR));
1715
+ if (!elements.length) {
1716
+ event.preventDefault();
1717
+ instance.element.focus();
1199
1718
  return;
1200
- let anchorRect;
1201
- if (point) {
1202
- anchorRect = new DOMRect(point.x, point.y, 0, 0);
1203
1719
  }
1204
- else {
1205
- anchorRect = anchor.getBoundingClientRect();
1720
+ const first = elements[0];
1721
+ const last = elements[elements.length - 1];
1722
+ const active = this.getActiveElement();
1723
+ if (!event.shiftKey && active === last) {
1724
+ event.preventDefault();
1725
+ first.focus();
1726
+ return;
1206
1727
  }
1207
- const dialogRect = dialog.getBoundingClientRect();
1208
- const vw = Math.min(document.body.clientWidth, window.innerWidth);
1209
- const vh = Math.min(document.body.clientHeight, window.innerHeight);
1210
- const isRTL = this.directionService.isRtl();
1211
- let top = 0;
1212
- let left = 'auto';
1213
- let right = 'auto';
1214
- const spaceBelow = vh - anchorRect.bottom;
1215
- const spaceAbove = anchorRect.top;
1216
- let verticalPos = 'above';
1217
- if (placement === 'bottom' ||
1218
- (placement === 'auto' && spaceBelow >= dialogRect.height + margin)) {
1219
- top = anchorRect.bottom + margin;
1220
- verticalPos = 'above';
1221
- if (top + dialogRect.height > vh - margin) {
1222
- top = vh - dialogRect.height - margin;
1223
- verticalPos = 'bottom';
1224
- }
1728
+ if (event.shiftKey && active === first) {
1729
+ event.preventDefault();
1730
+ last.focus();
1225
1731
  }
1226
- else {
1227
- top = anchorRect.top - dialogRect.height - margin;
1228
- verticalPos = 'bottom';
1229
- if (top > spaceAbove) {
1230
- top = anchorRect.top + anchorRect.height + margin;
1231
- verticalPos = 'above';
1232
- }
1732
+ }
1733
+ restoreFocus(element) {
1734
+ if (element && element.isConnected) {
1735
+ setTimeout(() => {
1736
+ element.focus();
1737
+ }, 0);
1233
1738
  }
1234
- if (alignment === 'center') {
1235
- left = anchorRect.left + anchorRect.width / 2 - dialogRect.width / 2;
1739
+ }
1740
+ getActiveElement() {
1741
+ const active = this.document.activeElement;
1742
+ return active instanceof HTMLElement ? active : null;
1743
+ }
1744
+ // ---------------------------------------------------------------------------
1745
+ // positioning
1746
+ // ---------------------------------------------------------------------------
1747
+ positionOverlay(instance) {
1748
+ if (!instance.host.isConnected || !instance.element.isConnected) {
1749
+ return;
1236
1750
  }
1237
- else if (alignment === 'start') {
1238
- if (isRTL) {
1239
- right = vw - anchorRect.right;
1240
- }
1241
- else {
1242
- left = anchorRect.left;
1243
- }
1751
+ const viewportWidth = window.innerWidth;
1752
+ const viewportHeight = window.innerHeight;
1753
+ const anchorRect = this.getAnchorRect(instance);
1754
+ const overlay = instance.element;
1755
+ const overlayRect = overlay.getBoundingClientRect();
1756
+ const { placement, alignment, margin } = instance.placementConfig;
1757
+ const isRTL = this.directionService.isRtl();
1758
+ let top;
1759
+ if (placement === 'center') {
1760
+ top = anchorRect.top + anchorRect.height / 2 - overlayRect.height / 2;
1244
1761
  }
1245
- else if (alignment === 'end') {
1246
- // انتهای دیالوگ باید با انتهای anchor یکی باشه؛ با استفاده از پراپرتی
1247
- // CSS «right» (به‌جای کم‌کردن دستیِ عرض از «left») این کار درست و مستقل
1248
- // از این‌که عرض دیالوگ چقدره انجام می‌شه (قبلاً این خط باگ داشت و دیالوگ
1249
- // رو بعد از anchor می‌ذاشت، نه هم‌تراز با لبه‌ی انتهاییِ آن).
1250
- if (isRTL) {
1251
- left = anchorRect.left;
1762
+ else {
1763
+ const spaceBelow = viewportHeight - anchorRect.bottom;
1764
+ const placeBelow = placement === 'bottom' ||
1765
+ (placement === 'auto' && spaceBelow >= overlayRect.height + margin);
1766
+ if (placeBelow) {
1767
+ top = anchorRect.bottom + margin;
1252
1768
  }
1253
1769
  else {
1254
- right = vw - anchorRect.right;
1770
+ top = anchorRect.top - overlayRect.height - margin;
1255
1771
  }
1772
+ overlay.classList.toggle('tips-below', placeBelow);
1773
+ overlay.classList.toggle('tips-above', !placeBelow);
1774
+ }
1775
+ if (top + overlayRect.height > viewportHeight - margin) {
1776
+ top = viewportHeight - overlayRect.height - margin;
1256
1777
  }
1257
- if (left !== 'auto') {
1258
- left = Math.min(Math.max(left, margin), vw - dialogRect.width - margin);
1778
+ if (top < margin) {
1779
+ top = margin;
1780
+ }
1781
+ let left;
1782
+ switch (alignment) {
1783
+ case 'center':
1784
+ left = anchorRect.left + anchorRect.width / 2 - overlayRect.width / 2;
1785
+ break;
1786
+ case 'end':
1787
+ left = isRTL ? anchorRect.left : anchorRect.right - overlayRect.width;
1788
+ break;
1789
+ case 'start':
1790
+ default:
1791
+ left = isRTL ? anchorRect.right - overlayRect.width : anchorRect.left;
1792
+ break;
1793
+ }
1794
+ left = Math.min(Math.max(left, margin), Math.max(margin, viewportWidth - overlayRect.width - margin));
1795
+ overlay.style.top = `${Math.round(top)}px`;
1796
+ overlay.style.left = `${Math.round(left)}px`;
1797
+ overlay.style.right = 'auto';
1798
+ overlay.style.bottom = 'auto';
1799
+ overlay.style.visibility = 'visible';
1800
+ // NOTE: showPopover() is intentionally NOT called here. It already
1801
+ // happened once in renderHost(); calling it again on every reposition
1802
+ // (scroll/resize) throws InvalidStateError since the popover is
1803
+ // already open.
1804
+ }
1805
+ getAnchorRect(instance) {
1806
+ if (instance.point) {
1807
+ return new DOMRect(instance.point.x, instance.point.y, 0, 0);
1808
+ }
1809
+ if (instance.anchor && instance.anchor.isConnected) {
1810
+ return instance.anchor.getBoundingClientRect();
1811
+ }
1812
+ return new DOMRect(window.innerWidth / 2, window.innerHeight / 2, 0, 0);
1813
+ }
1814
+ resolvePlacementConfig(placement, alignment, margin) {
1815
+ return {
1816
+ placement: placement ?? 'auto',
1817
+ alignment: alignment ?? 'start',
1818
+ margin: margin ?? 8,
1819
+ };
1820
+ }
1821
+ // ---------------------------------------------------------------------------
1822
+ // utilities
1823
+ // ---------------------------------------------------------------------------
1824
+ getLastInstance() {
1825
+ return this.overlayStack[this.overlayStack.length - 1];
1826
+ }
1827
+ requestAnimationFrame(callback) {
1828
+ if (typeof window.requestAnimationFrame === 'function') {
1829
+ return window.requestAnimationFrame(callback);
1259
1830
  }
1260
- if (right !== 'auto') {
1261
- right = Math.min(Math.max(right, margin), vw - dialogRect.width - margin);
1831
+ return window.setTimeout(() => callback(Date.now()), 16);
1832
+ }
1833
+ cancelAnimationFrame(id) {
1834
+ if (typeof window.cancelAnimationFrame === 'function') {
1835
+ window.cancelAnimationFrame(id);
1262
1836
  }
1263
- if (top < 0) {
1264
- top = 0;
1837
+ else {
1838
+ clearTimeout(id);
1265
1839
  }
1266
- dialog.className = `${DIALOG_OVERLAY_CLASSNAME} tips-${verticalPos}`;
1267
- dialog.style.top = `${top + window.scrollY}px`;
1268
- dialog.style.left = left !== 'auto' ? `${left + window.scrollX}px` : 'auto';
1269
- dialog.style.right = right !== 'auto' ? `${right + window.scrollX}px` : 'auto';
1270
- dialog.style.transformOrigin = verticalPos == 'above' ? 'left top' : 'left bottom';
1271
- // dialog.style.transform = 'none';
1272
- dialog.style.visibility = 'visible';
1273
- dialog.style.animation = 'menu-enter 120ms cubic-bezier(0, 0, 0.2, 1)';
1274
1840
  }
1275
1841
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1276
1842
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, providedIn: 'root' });
1277
1843
  }
1278
1844
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, decorators: [{
1279
1845
  type: Injectable,
1280
- args: [{ providedIn: 'root' }]
1846
+ args: [{
1847
+ providedIn: 'root',
1848
+ }]
1281
1849
  }] });
1282
1850
 
1283
1851
  /**
1284
1852
  * Generated bundle index. Do not edit.
1285
1853
  */
1286
1854
 
1287
- export { BrowserService, DIALOG_OVERLAY_CLASSNAME, DirectionService, FOCUSABLE_SELECTOR, OverlayRef, OverlayService, RangeSliderComponent, SaturationComponent, SliderComponent, ValueModel, WINDOW, filterTreeList, generateSequentialGuid, getOffsetPosition, makeConfetti, mergeConfig, mergeDeep, randomStrnig, startDragSession };
1288
- //# sourceMappingURL=ngx-kit-shared.mjs.map
1855
+ export { BrowserService, CSS_COLOR_NAMES, DIALOG_OVERLAY_CLASSNAME, DirectionService, FOCUSABLE_SELECTOR, OVERLAY_HOST_CLASSNAME, OverlayRef, OverlayService, RangeSliderComponent, SaturationComponent, SliderComponent, ValueModel, WINDOW, extractCssColors, filterTreeList, generateSequentialGuid, getOffsetPosition, makeConfetti, mergeDeep, randomStrnig, startDragSession };
1856
+ //# sourceMappingURL=ngx-kit-core.mjs.map