ngx-kit 7.1.0 → 7.3.0

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 (73) 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} +908 -277
  16. package/fesm2022/ngx-kit-core.mjs.map +1 -0
  17. package/fesm2022/ngx-kit-data-table.mjs +1 -1
  18. package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
  19. package/fesm2022/ngx-kit-date-picker.mjs +34 -22
  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 +193 -140
  24. package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -1
  25. package/fesm2022/ngx-kit-drawer-menu.mjs +3 -3
  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 +9 -3
  29. package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
  30. package/fesm2022/ngx-kit-gallery.mjs +3 -3
  31. package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
  32. package/fesm2022/ngx-kit-gradient-picker.mjs +20 -38
  33. package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
  34. package/fesm2022/ngx-kit-grid-layout.mjs +32 -30
  35. package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -1
  36. package/fesm2022/ngx-kit-image-editor.mjs +3 -3
  37. package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
  38. package/fesm2022/ngx-kit-menu.mjs +1 -1
  39. package/fesm2022/ngx-kit-menu.mjs.map +1 -1
  40. package/fesm2022/ngx-kit-message.mjs +2 -2
  41. package/fesm2022/ngx-kit-message.mjs.map +1 -1
  42. package/fesm2022/ngx-kit-notify.mjs +1 -1
  43. package/fesm2022/ngx-kit-notify.mjs.map +1 -1
  44. package/fesm2022/ngx-kit-time-picker.mjs +1 -1
  45. package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
  46. package/fesm2022/ngx-kit-virtual-scroll.mjs +2 -2
  47. package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -1
  48. package/gallery/README.md +71 -71
  49. package/gradient-picker/README.md +41 -41
  50. package/grid-layout/README.md +64 -64
  51. package/image-editor/README.md +44 -44
  52. package/menu/README.md +85 -85
  53. package/message/README.md +70 -70
  54. package/notify/README.md +56 -56
  55. package/package.json +5 -9
  56. package/time-picker/README.md +89 -89
  57. package/types/ngx-kit-angle-selector.d.ts +13 -16
  58. package/types/ngx-kit-box-shadow.d.ts +37 -95
  59. package/types/ngx-kit-color-picker.d.ts +61 -58
  60. package/types/{ngx-kit-shared.d.ts → ngx-kit-core.d.ts} +194 -34
  61. package/types/ngx-kit-data-table.d.ts +2 -2
  62. package/types/ngx-kit-date-picker.d.ts +15 -12
  63. package/types/ngx-kit-dialog.d.ts +208 -107
  64. package/types/ngx-kit-drag-resize.d.ts +74 -47
  65. package/types/ngx-kit-form-field.d.ts +26 -23
  66. package/types/ngx-kit-gradient-picker.d.ts +4 -7
  67. package/types/ngx-kit-menu.d.ts +1 -1
  68. package/types/ngx-kit-time-picker.d.ts +1 -1
  69. package/virtual-scroll/README.md +199 -199
  70. package/fesm2022/ngx-kit-shared.mjs.map +0 -1
  71. package/fesm2022/ngx-kit-webflow.mjs +0 -709
  72. package/fesm2022/ngx-kit-webflow.mjs.map +0 -1
  73. 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
  });
@@ -802,6 +768,331 @@ function mergeDeep(...objects) {
802
768
  }, {});
803
769
  }
804
770
 
771
+ /**
772
+ * Filters a hierarchical tree while preserving the branches
773
+ * that contain matching nodes.
774
+ *
775
+ * A node is included when:
776
+ * - the filter function matches the node itself, or
777
+ * - one or more of its descendants match.
778
+ *
779
+ * When only a descendant matches, a shallow copy of the parent
780
+ * is returned with only the matching descendants preserved.
781
+ *
782
+ * @param data The tree nodes to filter.
783
+ * @param filterFn Function used to determine whether a node matches.
784
+ * @param options Optional filtering configuration.
785
+ *
786
+ * @example
787
+ * ```ts
788
+ * const result = filterTreeList(
789
+ * dataTree,
790
+ * item =>
791
+ * item.name.toLowerCase().includes(filter) ||
792
+ * item.type.toLowerCase().includes(filter),
793
+ * );
794
+ * ```
795
+ *
796
+ * @example
797
+ * ```ts
798
+ * const result = filterTreeList(
799
+ * data,
800
+ * item => item.title.includes('angular'),
801
+ * { childrenKeyName: 'items' },
802
+ * );
803
+ * ```
804
+ */
805
+ function filterTreeList(data, filterFn, options) {
806
+ if (!data?.length) {
807
+ return [];
808
+ }
809
+ const childrenKeyName = options?.childrenKeyName ?? 'children';
810
+ return filterNodes(data, filterFn, childrenKeyName);
811
+ }
812
+ function filterNodes(nodes, filterFn, childrenKeyName) {
813
+ const result = [];
814
+ for (const node of nodes) {
815
+ if (filterFn(node)) {
816
+ result.push(node);
817
+ continue;
818
+ }
819
+ const children = node[childrenKeyName];
820
+ if (!Array.isArray(children) || children.length === 0) {
821
+ continue;
822
+ }
823
+ const filteredChildren = filterNodes(children, filterFn, childrenKeyName);
824
+ if (filteredChildren.length > 0) {
825
+ result.push({
826
+ ...node,
827
+ [childrenKeyName]: filteredChildren,
828
+ });
829
+ }
830
+ }
831
+ return result;
832
+ }
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
+
805
1096
  class BrowserService {
806
1097
  platformId;
807
1098
  constructor(platformId) {
@@ -885,341 +1176,681 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
885
1176
  }], ctorParameters: () => [] });
886
1177
 
887
1178
  class OverlayRef {
888
- dialogEl;
889
- teardownFn;
1179
+ element;
1180
+ closeFn;
890
1181
  componentRef;
891
- templateRef;
892
- constructor(dialogEl, teardownFn, componentRef, templateRef) {
893
- this.dialogEl = dialogEl;
894
- this.teardownFn = teardownFn;
1182
+ template;
1183
+ constructor(element, closeFn, componentRef, template) {
1184
+ this.element = element;
1185
+ this.closeFn = closeFn;
895
1186
  this.componentRef = componentRef;
896
- this.templateRef = templateRef;
1187
+ this.template = template;
897
1188
  }
898
1189
  close() {
899
- this.teardownFn();
1190
+ this.closeFn();
900
1191
  }
901
1192
  get nativeElement() {
902
- return this.dialogEl;
1193
+ return this.element;
1194
+ }
1195
+ static noop() {
1196
+ return new OverlayRef(null, () => { });
903
1197
  }
904
1198
  }
905
1199
 
906
- 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(',');
907
1208
  const DIALOG_OVERLAY_CLASSNAME = 'ngx-ui-overlay';
1209
+ const OVERLAY_HOST_CLASSNAME = 'ngx-ui-overlay-host';
1210
+ const BASE_Z_INDEX = 1000;
908
1211
  class OverlayService {
909
- overlayStacks = [];
910
- listenersAttached = false;
911
- resizeRafPending = false;
912
- doc = inject(DOCUMENT);
913
- abortController = new AbortController();
1212
+ document = inject(DOCUMENT);
914
1213
  directionService = inject(DirectionService);
915
- ngOnDestroy() {
916
- this.closeAll();
917
- 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;
918
1229
  }
919
1230
  open(options) {
920
- const { anchor, point, component, viewContainerRef, configure, onClosed, placement, alignment, margin, } = options;
921
- if (!viewContainerRef) {
922
- 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();
923
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);
924
1252
  this.attachGlobalListeners();
925
- const element = this.createDialogElement();
926
- const componentRef = viewContainerRef.createComponent(component);
927
- if (componentRef.location.nativeElement) {
928
- 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);
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;
929
1293
  }
1294
+ element.appendChild(componentRef.location.nativeElement);
930
1295
  const instance = {
1296
+ host,
1297
+ backdrop,
931
1298
  element,
932
- anchor,
933
- point,
934
- 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,
935
1309
  componentRef,
936
- onClosed,
1310
+ attachedToAppRef,
1311
+ previousActiveElement: this.getActiveElement(),
937
1312
  rafId: null,
938
1313
  focusTimeoutId: null,
939
1314
  cleanup: () => { },
940
1315
  };
941
1316
  instance.cleanup = () => this.destroyInstance(instance);
942
- this.overlayStacks.push(instance);
943
- element.showModal();
944
- const ref = new OverlayRef(element, instance.cleanup, componentRef);
945
- configure?.(componentRef.instance, ref);
946
- this.scheduleInitialLayout(instance);
947
- return ref;
948
- }
949
- openTemplate(options) {
950
- const { anchor, point, template, appRef, configure, onClosed, placement, alignment, margin } = options;
951
- this.attachGlobalListeners();
952
- 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);
953
1325
  const view = template.createEmbeddedView({});
954
1326
  appRef.attachView(view);
955
1327
  element.append(...view.rootNodes);
956
1328
  const instance = {
1329
+ host,
1330
+ backdrop,
957
1331
  element,
958
- anchor,
959
- point,
960
- 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,
961
1342
  embeddedView: view,
962
1343
  appRef,
963
- onClosed,
1344
+ previousActiveElement: this.getActiveElement(),
964
1345
  rafId: null,
965
1346
  focusTimeoutId: null,
966
1347
  cleanup: () => { },
967
1348
  };
968
1349
  instance.cleanup = () => this.destroyInstance(instance);
969
- this.overlayStacks.push(instance);
970
- element.showModal();
971
- const ref = new OverlayRef(element, instance.cleanup, undefined, template);
972
- configure?.(instance, ref);
973
- this.scheduleInitialLayout(instance);
974
- return ref;
975
- }
976
- closeAll() {
977
- // اسنپ‌شات می‌گیریم چون cleanup حین اجرا آرایه‌ی اصلی رو تغییر می‌ده
978
- [...this.overlayStacks].forEach((instance) => instance.cleanup());
979
- }
980
- // ---------------------------------------------------------------------
981
- // lifecycle
982
- // ---------------------------------------------------------------------
983
- resolvePlacementConfig(placement, alignment, margin) {
984
- return {
985
- placement: placement ?? 'auto',
986
- alignment: alignment ?? 'start',
987
- margin: margin ?? 8,
988
- };
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
+ }
989
1360
  }
990
- createDialogElement() {
991
- 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');
992
1411
  element.className = DIALOG_OVERLAY_CLASSNAME;
993
- const style = element.style;
994
- style.position = 'absolute';
995
- style.outline = 'none';
996
- style.padding = '0';
997
- style.margin = '0';
998
- style.border = 'none';
999
- style.background = 'transparent';
1000
- style.maxWidth = '100vw';
1001
- style.maxHeight = '100vh';
1002
- style.overflow = 'visible';
1003
- style.visibility = 'hidden';
1004
- 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);
1005
1441
  return element;
1006
1442
  }
1007
- scheduleInitialLayout(instance) {
1008
- instance.rafId = requestAnimationFrame(() => {
1009
- instance.rafId = null;
1010
- this.positionDialog(instance);
1011
- });
1012
- instance.focusTimeoutId = setTimeout(() => {
1013
- instance.focusTimeoutId = null;
1014
- const focusable = instance.element.querySelector(FOCUSABLE_SELECTOR);
1015
- focusable?.focus();
1016
- }, 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
+ }
1017
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
+ // ---------------------------------------------------------------------------
1018
1469
  destroyInstance(instance) {
1019
- const index = this.overlayStacks.indexOf(instance);
1020
- if (index === -1)
1021
- return; // جلوگیری از پاک‌سازی دوباره
1022
- this.overlayStacks.splice(index, 1);
1023
- // لغو کارهای زمان‌بندی‌شده‌ای که هنوز اجرا نشدن (نشتی حافظه در نسخه‌ی قبلی)
1024
- if (instance.rafId !== null)
1025
- cancelAnimationFrame(instance.rafId);
1026
- 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) {
1027
1480
  clearTimeout(instance.focusTimeoutId);
1481
+ instance.focusTimeoutId = null;
1482
+ }
1028
1483
  if (instance.componentRef) {
1029
- instance.componentRef.location.nativeElement?.remove();
1484
+ if (instance.attachedToAppRef) {
1485
+ this.applicationRef.detachView(instance.componentRef.hostView);
1486
+ }
1030
1487
  instance.componentRef.destroy();
1488
+ instance.componentRef = undefined;
1031
1489
  }
1032
1490
  if (instance.embeddedView && instance.appRef) {
1033
1491
  instance.appRef.detachView(instance.embeddedView);
1034
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;
1035
1521
  }
1036
- // قبل از remove()، دیالوگ رو با متد استاندارد close() می‌بندیم؛ این کار دو
1037
- // فایده داره که با remove() ساده به دست نمی‌اومدن:
1038
- // ۱) فوکوس به‌صورت خودکار به همون المنتی که قبل از باز شدن دیالوگ فوکوس
1039
- // داشت برمی‌گرده (رفتار استاندارد close() روی <dialog>) — با remove()
1040
- // ساده این اتفاق نمی‌افته و فوکوس معمولاً روی body گم می‌شه.
1041
- // ۲) رویداد 'close' استاندارد شلیک می‌شه، برای کدی که ممکنه به اون گوش بده.
1042
- // در try/catch گذاشتیمش چون در محیط تست (jsdom) این متدها mock هستن و ممکنه
1043
- // رفتار متفاوتی داشته باشن.
1044
1522
  try {
1045
- if (instance.element.open)
1046
- instance.element.close();
1523
+ instance.host.showPopover();
1047
1524
  }
1048
1525
  catch {
1049
- /* noop */
1526
+ // Already open, or environment doesn't fully support it - ignore.
1527
+ }
1528
+ }
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.
1050
1540
  }
1051
- instance.element.remove();
1052
- instance.onClosed?.();
1053
1541
  }
1054
- // ---------------------------------------------------------------------
1055
- // global listeners (فقط یک‌بار در کل عمر سرویس attach می‌شن)
1056
- // ---------------------------------------------------------------------
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
+ // ---------------------------------------------------------------------------
1057
1583
  attachGlobalListeners() {
1058
- if (this.listenersAttached)
1584
+ if (!this.isBrowser || this.globalListenersAttached) {
1585
+ return;
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) {
1059
1601
  return;
1060
- this.listenersAttached = true;
1061
- const { signal } = this.abortController;
1062
- this.doc.addEventListener('keydown', this.onGlobalKeyDown, { signal });
1063
- window.addEventListener('resize', this.onGlobalResize, { signal, passive: true });
1064
- this.doc.addEventListener('click', this.onGlobalPointerEvent, { signal });
1065
- this.doc.addEventListener('contextmenu', this.onGlobalPointerEvent, { signal });
1066
- }
1067
- onGlobalKeyDown = (e) => {
1068
- if (e.key !== 'Escape')
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) {
1069
1613
  return;
1070
- this.getLastDialog()?.cleanup();
1071
- };
1072
- onGlobalResize = () => {
1073
- // چند رویداد resize پشت‌سرهم فقط یک reposition واقعی اجرا می‌کنن
1074
- if (this.resizeRafPending)
1614
+ }
1615
+ if (event.key === 'Escape') {
1616
+ if (!instance.closeOnEscape || (instance.canClose && !instance.canClose())) {
1617
+ return;
1618
+ }
1619
+ event.preventDefault();
1620
+ instance.cleanup();
1075
1621
  return;
1076
- this.resizeRafPending = true;
1077
- requestAnimationFrame(() => {
1078
- this.resizeRafPending = false;
1079
- this.repositionAll();
1080
- });
1622
+ }
1623
+ if (event.key === 'Tab') {
1624
+ this.trapFocus(event, instance);
1625
+ }
1081
1626
  };
1082
- onGlobalPointerEvent = (e) => {
1083
- 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();
1084
1649
  };
1085
- handleClickOnBackdrop(event) {
1086
- const lastDialog = this.getLastDialog();
1087
- if (!lastDialog || !lastDialog.element.open)
1650
+ onDocumentContextMenu = (event) => {
1651
+ const instance = this.getLastInstance();
1652
+ if (!instance) {
1653
+ return;
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();
1088
1662
  return;
1089
- // کلیک باید دقیقاً روی خودِ دیالوگ (بک‌دراپ) باشه، نه محتوای داخلش
1090
- if (event.target !== lastDialog.element)
1663
+ }
1664
+ if (instance.element.contains(event.target)) {
1091
1665
  return;
1092
- // اگه فرم داخل دیالوگه، با کلیک بیرون بسته نشه
1093
- if (lastDialog.element.querySelector('form'))
1666
+ }
1667
+ if (!instance.closeOnOutsideClick || (instance.canClose && !instance.canClose())) {
1094
1668
  return;
1095
- const rect = lastDialog.element.getBoundingClientRect();
1096
- const clickWasInsideDialog = rect.top <= event.clientY &&
1097
- event.clientY <= rect.bottom &&
1098
- rect.left <= event.clientX &&
1099
- event.clientX <= rect.right;
1100
- const anchRect = lastDialog.anchor.getBoundingClientRect();
1101
- const clickWasInsideAnchor = anchRect.top <= event.clientY &&
1102
- event.clientY <= anchRect.bottom &&
1103
- anchRect.left <= event.clientX &&
1104
- event.clientX <= anchRect.right;
1105
- if (!clickWasInsideDialog) {
1106
- if (event.type == 'contextmenu' && lastDialog.point && clickWasInsideAnchor) {
1107
- event.preventDefault();
1108
- lastDialog.point = {
1109
- x: event.clientX,
1110
- y: event.clientY,
1111
- };
1112
- this.positionDialog(lastDialog);
1113
- }
1114
- else {
1115
- lastDialog.cleanup();
1116
- }
1117
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
+ });
1118
1692
  }
1119
- getLastDialog() {
1120
- return this.overlayStacks[this.overlayStacks.length - 1];
1121
- }
1122
- // ---------------------------------------------------------------------
1123
- // positioning
1124
- // ---------------------------------------------------------------------
1125
- repositionAll() {
1126
- this.overlayStacks.forEach((instance) => {
1127
- if (instance.element.isConnected) {
1128
- 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;
1129
1702
  }
1703
+ instance.focusTimeoutId = setTimeout(() => {
1704
+ instance.focusTimeoutId = null;
1705
+ this.focusInitialElement(instance);
1706
+ }, 0);
1130
1707
  });
1131
1708
  }
1132
- positionDialog(instance) {
1133
- const { anchor, element: dialog, placementConfig, point } = instance;
1134
- const { placement, alignment, margin } = placementConfig;
1135
- 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();
1136
1718
  return;
1137
- let anchorRect;
1138
- if (point) {
1139
- anchorRect = new DOMRect(point.x, point.y, 0, 0);
1140
1719
  }
1141
- else {
1142
- 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;
1143
1727
  }
1144
- const dialogRect = dialog.getBoundingClientRect();
1145
- const vw = Math.min(document.body.clientWidth, window.innerWidth);
1146
- const vh = Math.min(document.body.clientHeight, window.innerHeight);
1147
- const isRTL = this.directionService.isRtl();
1148
- let top = 0;
1149
- let left = 'auto';
1150
- let right = 'auto';
1151
- const spaceBelow = vh - anchorRect.bottom;
1152
- const spaceAbove = anchorRect.top;
1153
- let verticalPos = 'above';
1154
- if (placement === 'bottom' ||
1155
- (placement === 'auto' && spaceBelow >= dialogRect.height + margin)) {
1156
- top = anchorRect.bottom + margin;
1157
- verticalPos = 'above';
1158
- if (top + dialogRect.height > vh - margin) {
1159
- top = vh - dialogRect.height - margin;
1160
- verticalPos = 'bottom';
1161
- }
1728
+ if (event.shiftKey && active === first) {
1729
+ event.preventDefault();
1730
+ last.focus();
1162
1731
  }
1163
- else {
1164
- top = anchorRect.top - dialogRect.height - margin;
1165
- verticalPos = 'bottom';
1166
- if (top > spaceAbove) {
1167
- top = anchorRect.top + anchorRect.height + margin;
1168
- verticalPos = 'above';
1169
- }
1732
+ }
1733
+ restoreFocus(element) {
1734
+ if (element && element.isConnected) {
1735
+ setTimeout(() => {
1736
+ element.focus();
1737
+ }, 0);
1738
+ }
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;
1170
1750
  }
1171
- if (alignment === 'center') {
1172
- left = anchorRect.left + anchorRect.width / 2 - dialogRect.width / 2;
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;
1173
1761
  }
1174
- else if (alignment === 'start') {
1175
- if (isRTL) {
1176
- right = vw - anchorRect.right;
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;
1177
1768
  }
1178
1769
  else {
1179
- left = anchorRect.left;
1770
+ top = anchorRect.top - overlayRect.height - margin;
1180
1771
  }
1772
+ overlay.classList.toggle('tips-below', placeBelow);
1773
+ overlay.classList.toggle('tips-above', !placeBelow);
1181
1774
  }
1182
- else if (alignment === 'end') {
1183
- // انتهای دیالوگ باید با انتهای anchor یکی باشه؛ با استفاده از پراپرتی
1184
- // CSS «right» (به‌جای کم‌کردن دستیِ عرض از «left») این کار درست و مستقل
1185
- // از این‌که عرض دیالوگ چقدره انجام می‌شه (قبلاً این خط باگ داشت و دیالوگ
1186
- // رو بعد از anchor می‌ذاشت، نه هم‌تراز با لبه‌ی انتهاییِ آن).
1187
- if (isRTL) {
1188
- left = anchorRect.left;
1189
- }
1190
- else {
1191
- right = vw - anchorRect.right;
1192
- }
1775
+ if (top + overlayRect.height > viewportHeight - margin) {
1776
+ top = viewportHeight - overlayRect.height - margin;
1777
+ }
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;
1193
1793
  }
1194
- if (left !== 'auto') {
1195
- left = Math.min(Math.max(left, margin), vw - dialogRect.width - margin);
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);
1196
1830
  }
1197
- if (right !== 'auto') {
1198
- 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);
1199
1836
  }
1200
- if (top < 0) {
1201
- top = 0;
1837
+ else {
1838
+ clearTimeout(id);
1202
1839
  }
1203
- dialog.className = `${DIALOG_OVERLAY_CLASSNAME} tips-${verticalPos}`;
1204
- dialog.style.top = `${top + window.scrollY}px`;
1205
- dialog.style.left = left !== 'auto' ? `${left + window.scrollX}px` : 'auto';
1206
- dialog.style.right = right !== 'auto' ? `${right + window.scrollX}px` : 'auto';
1207
- dialog.style.transformOrigin = verticalPos == 'above' ? 'left top' : 'left bottom';
1208
- // dialog.style.transform = 'none';
1209
- dialog.style.visibility = 'visible';
1210
- dialog.style.animation = 'menu-enter 120ms cubic-bezier(0, 0, 0.2, 1)';
1211
1840
  }
1212
1841
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1213
1842
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, providedIn: 'root' });
1214
1843
  }
1215
1844
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: OverlayService, decorators: [{
1216
1845
  type: Injectable,
1217
- args: [{ providedIn: 'root' }]
1846
+ args: [{
1847
+ providedIn: 'root',
1848
+ }]
1218
1849
  }] });
1219
1850
 
1220
1851
  /**
1221
1852
  * Generated bundle index. Do not edit.
1222
1853
  */
1223
1854
 
1224
- export { BrowserService, DIALOG_OVERLAY_CLASSNAME, DirectionService, FOCUSABLE_SELECTOR, OverlayRef, OverlayService, RangeSliderComponent, SaturationComponent, SliderComponent, ValueModel, WINDOW, generateSequentialGuid, getOffsetPosition, makeConfetti, mergeConfig, mergeDeep, randomStrnig, startDragSession };
1225
- //# 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