@evergis/uilib-gl 1.0.135 → 1.0.137

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.
@@ -1,6 +1,6 @@
1
1
  /// <reference types="lodash" />
2
2
  import { Component } from "react";
3
- import { Flip, PopoverPosition, PopoverProps, PopoverPropsDefault } from "./types";
3
+ import { ContentObserver, Flip, PopoverPosition, PopoverProps, PopoverPropsDefault } from "./types";
4
4
  export declare class Popover extends Component<PopoverProps> {
5
5
  static defaultProps: PopoverPropsDefault;
6
6
  anchorNode: HTMLElement | null;
@@ -8,9 +8,29 @@ export declare class Popover extends Component<PopoverProps> {
8
8
  popoverAnchorNode: HTMLElement | null;
9
9
  flip: Flip | null;
10
10
  childrenClicked: boolean;
11
+ contentObservers: ContentObserver[];
12
+ heightSyncFrame: number | null;
11
13
  saveAnchor: (ref: HTMLElement | null) => null;
12
14
  savePopoverAnchor: (ref: HTMLElement | null) => void;
13
15
  saveContainer: (ref: HTMLElement | null) => void;
16
+ /**
17
+ * Содержимое меняет размер и после открытия: форма раскрывает новые поля, список догружается.
18
+ * Обрезку по окну пересчитываем вслед за ним, а не только на открытии и ресайзе окна — иначе
19
+ * выросшее содержимое уезжало бы за край экрана.
20
+ *
21
+ * Размер контейнера ловит ResizeObserver, но под обрезкой он замирает: высота ребёнка задана
22
+ * явно, и рост или сжатие содержимого контейнер уже не меняют. Эти случаи ловит MutationObserver —
23
+ * по вставке и удалению узлов, то есть ровно когда форма меняет свой состав.
24
+ */
25
+ observeContent: (node: HTMLElement | null) => void;
26
+ disconnectContent: () => void;
27
+ /**
28
+ * Пересчёт — в следующем кадре, а не прямо в колбэке: правка высоты внутри колбэка
29
+ * ResizeObserver зациклила бы его («ResizeObserver loop completed…»), а пачка мутаций одного
30
+ * рендера схлопывается в один пересчёт.
31
+ */
32
+ scheduleHeightSync: () => void;
33
+ syncContainerHeight: () => void;
14
34
  getContainerPosition: (anchorNode: HTMLElement, containerNode: HTMLElement) => PopoverPosition;
15
35
  setPositions: import("lodash").DebouncedFunc<() => void>;
16
36
  closePopover: (e: Event | MouseEvent | TouchEvent) => void;
@@ -40,3 +40,14 @@ export declare type AnchorProps = {
40
40
  withTime?: boolean;
41
41
  width?: string | number;
42
42
  };
43
+ /** Наблюдатель за содержимым поповера — ResizeObserver либо MutationObserver. */
44
+ export interface ContentObserver {
45
+ disconnect: () => void;
46
+ }
47
+ /**
48
+ * Конструктор ResizeObserver структурно: сборка (tsdx) идёт своим TypeScript 3.9, в DOM-типах
49
+ * которого ResizeObserver ещё нет, и прямое имя класса валит сборку.
50
+ */
51
+ export declare type ResizeObserverConstructor = new (callback: () => void) => ContentObserver & {
52
+ observe: (target: Element) => void;
53
+ };
@@ -7,5 +7,13 @@ export declare const checkFit: (anchorNode: HTMLElement, containerNode: HTMLElem
7
7
  y: boolean;
8
8
  };
9
9
  export declare const syncPosition: (anchor: HTMLElement, node: HTMLElement) => void;
10
+ /**
11
+ * Урезает высоту содержимого до места в окне по направлению раскрытия.
12
+ *
13
+ * Прошлая обрезка перед замером снимается: иначе мерилась бы уже урезанная высота, и содержимое,
14
+ * которое с тех пор выросло или сжалось, так и жило бы с устаревшей — выросшее уезжало бы за край
15
+ * окна, сжавшееся оставляло бы пустоту. Своей прокрутки обрезка не даёт: её включает содержимое.
16
+ */
17
+ export declare const setPopupContainerHeight: (anchorNode: HTMLElement, node: HTMLElement, targetY: number) => void;
10
18
  export declare const getOriginPosition: (flip: Flip | null, origin: Origin) => Position;
11
19
  export declare const setPopupContainerPosition: (anchorNode: HTMLElement, node: HTMLElement, { anchorOriginPosition: [anchorX, anchorY], targetOriginPosition: [targetX, targetY], paddingSide }: PopoverPosition, animateAppearance: boolean, animateX: boolean, animateY: boolean) => void;
@@ -19,6 +19,12 @@ export declare type IPreviewProps = ControlText & {
19
19
  isOpen?: boolean;
20
20
  onClose?: VoidFunction;
21
21
  root?: HTMLElement | null;
22
+ /**
23
+ * Обработчик скачивания текущего изображения. Если задан, кнопка «Скачать» вызывает его
24
+ * вместо перехода по `src` — это нужно, когда файл требуется получить в момент клика
25
+ * (например, `src` показывает иконку типа файла, а скачать надо сам документ).
26
+ */
27
+ onDownload?: (image: IPreviewImage, index: number) => void;
22
28
  };
23
29
  export declare type ImageSlideShowType = [{
24
30
  chosenImage: IPreviewImage;
@@ -35,6 +41,7 @@ export interface IPreviewButtonProps {
35
41
  }
36
42
  export declare type IPreviewControlsProps = ControlText & {
37
43
  chosenImage?: IPreviewImage;
44
+ onDownload?: VoidFunction;
38
45
  fullscreen?: boolean;
39
46
  toggleFullscreen?: VoidFunction;
40
47
  close?: VoidFunction;
@@ -17667,11 +17667,43 @@ const syncPosition = (anchor, node) => {
17667
17667
  node.style.top = `${top}px`;
17668
17668
  node.style.left = `${newLeft}px`;
17669
17669
  };
17670
+ /**
17671
+ * Метки обрезки в data-атрибутах самого ребёнка: что высоту поставил Popover, и какой она была до
17672
+ * него. Обрезку нужно уметь снять, не затерев собственную инлайн-высоту содержимого.
17673
+ */
17674
+
17675
+ const CLAMPED_HEIGHT_KEY = "popoverClampedHeight";
17676
+ const OWN_HEIGHT_KEY = "popoverOwnHeight";
17677
+
17678
+ const releaseClampedHeight = childNode => {
17679
+ if (childNode.dataset[CLAMPED_HEIGHT_KEY] === undefined) return;
17680
+ childNode.style.height = childNode.dataset[OWN_HEIGHT_KEY] ?? "";
17681
+ delete childNode.dataset[CLAMPED_HEIGHT_KEY];
17682
+ delete childNode.dataset[OWN_HEIGHT_KEY];
17683
+ };
17684
+
17685
+ const clampHeight = (childNode, height) => {
17686
+ if (childNode.dataset[CLAMPED_HEIGHT_KEY] === undefined) {
17687
+ childNode.dataset[OWN_HEIGHT_KEY] = childNode.style.height;
17688
+ childNode.dataset[CLAMPED_HEIGHT_KEY] = "";
17689
+ }
17690
+
17691
+ childNode.style.height = `${height}px`;
17692
+ };
17693
+ /**
17694
+ * Урезает высоту содержимого до места в окне по направлению раскрытия.
17695
+ *
17696
+ * Прошлая обрезка перед замером снимается: иначе мерилась бы уже урезанная высота, и содержимое,
17697
+ * которое с тех пор выросло или сжалось, так и жило бы с устаревшей — выросшее уезжало бы за край
17698
+ * окна, сжавшееся оставляло бы пустоту. Своей прокрутки обрезка не даёт: её включает содержимое.
17699
+ */
17700
+
17670
17701
 
17671
17702
  const setPopupContainerHeight = (anchorNode, node, targetY) => {
17672
- if (!node.firstChild) return;
17703
+ if (!(node.firstChild instanceof HTMLElement)) return;
17673
17704
  const clientHeight = window.innerHeight;
17674
17705
  const childNode = node.firstChild;
17706
+ releaseClampedHeight(childNode);
17675
17707
  const childNodeHeight = childNode.offsetHeight;
17676
17708
  const anchorBounds = anchorNode.getBoundingClientRect();
17677
17709
  const anchorPosY = anchorBounds.top;
@@ -17680,16 +17712,15 @@ const setPopupContainerHeight = (anchorNode, node, targetY) => {
17680
17712
  const downDirection = 0;
17681
17713
 
17682
17714
  if (targetY === upDirection && childNodeHeight > anchorPosY) {
17683
- childNode.style.height = `${anchorPosY}px`;
17715
+ clampHeight(childNode, anchorPosY);
17684
17716
  }
17685
17717
 
17686
17718
  if (targetY === downDirection && childNodeHeight + anchorPosY > clientHeight) {
17687
17719
  const heightWithTopIndent = childNodeHeight + anchorPosY;
17688
17720
  const delta = heightWithTopIndent - clientHeight;
17689
- childNode.style.height = `${childNodeHeight - delta - anchorIndent}px`;
17721
+ clampHeight(childNode, childNodeHeight - delta - anchorIndent);
17690
17722
  }
17691
17723
  };
17692
-
17693
17724
  const getOriginPosition = (flip, origin) => flip ? flipPosition(flip, positions[origin]) : positions[origin];
17694
17725
  const setPopupContainerPosition = (anchorNode, node, _ref6, animateAppearance, animateX, animateY) => {
17695
17726
  let {
@@ -17726,6 +17757,8 @@ class Popover extends Component {
17726
17757
  this.flip = null; // Click inside react tree on case when children uses Portal
17727
17758
 
17728
17759
  this.childrenClicked = false;
17760
+ this.contentObservers = [];
17761
+ this.heightSyncFrame = null;
17729
17762
 
17730
17763
  this.saveAnchor = ref => {
17731
17764
  this.anchorNode = ref;
@@ -17738,6 +17771,73 @@ class Popover extends Component {
17738
17771
 
17739
17772
  this.saveContainer = ref => {
17740
17773
  this.containerNode = ref;
17774
+ this.observeContent(ref);
17775
+ };
17776
+ /**
17777
+ * Содержимое меняет размер и после открытия: форма раскрывает новые поля, список догружается.
17778
+ * Обрезку по окну пересчитываем вслед за ним, а не только на открытии и ресайзе окна — иначе
17779
+ * выросшее содержимое уезжало бы за край экрана.
17780
+ *
17781
+ * Размер контейнера ловит ResizeObserver, но под обрезкой он замирает: высота ребёнка задана
17782
+ * явно, и рост или сжатие содержимого контейнер уже не меняют. Эти случаи ловит MutationObserver —
17783
+ * по вставке и удалению узлов, то есть ровно когда форма меняет свой состав.
17784
+ */
17785
+
17786
+
17787
+ this.observeContent = node => {
17788
+ this.disconnectContent();
17789
+ if (!node) return;
17790
+ const {
17791
+ ResizeObserver: ResizeObserverClass
17792
+ } = window;
17793
+
17794
+ if (ResizeObserverClass) {
17795
+ const resizeObserver = new ResizeObserverClass(this.scheduleHeightSync);
17796
+ resizeObserver.observe(node);
17797
+ this.contentObservers.push(resizeObserver);
17798
+ }
17799
+
17800
+ if (typeof MutationObserver !== "undefined") {
17801
+ const mutationObserver = new MutationObserver(this.scheduleHeightSync);
17802
+ mutationObserver.observe(node, {
17803
+ childList: true,
17804
+ subtree: true
17805
+ });
17806
+ this.contentObservers.push(mutationObserver);
17807
+ }
17808
+ };
17809
+
17810
+ this.disconnectContent = () => {
17811
+ this.contentObservers.forEach(observer => observer.disconnect());
17812
+ this.contentObservers = [];
17813
+
17814
+ if (this.heightSyncFrame !== null) {
17815
+ cancelAnimationFrame(this.heightSyncFrame);
17816
+ this.heightSyncFrame = null;
17817
+ }
17818
+ };
17819
+ /**
17820
+ * Пересчёт — в следующем кадре, а не прямо в колбэке: правка высоты внутри колбэка
17821
+ * ResizeObserver зациклила бы его («ResizeObserver loop completed…»), а пачка мутаций одного
17822
+ * рендера схлопывается в один пересчёт.
17823
+ */
17824
+
17825
+
17826
+ this.scheduleHeightSync = () => {
17827
+ if (this.heightSyncFrame !== null) return;
17828
+ this.heightSyncFrame = requestAnimationFrame(() => {
17829
+ this.heightSyncFrame = null;
17830
+ this.syncContainerHeight();
17831
+ });
17832
+ }; // Только высота: полный `setPositions` заново проигрывал бы анимацию появления на каждом изменении.
17833
+
17834
+
17835
+ this.syncContainerHeight = () => {
17836
+ if (!this.anchorNode || !this.containerNode) return;
17837
+ const {
17838
+ targetOriginPosition: [, targetY]
17839
+ } = this.getContainerPosition(this.anchorNode, this.containerNode);
17840
+ setPopupContainerHeight(this.anchorNode, this.containerNode, targetY);
17741
17841
  };
17742
17842
 
17743
17843
  this.getContainerPosition = (anchorNode, containerNode) => {
@@ -17840,6 +17940,7 @@ class Popover extends Component {
17840
17940
 
17841
17941
  componentWillUnmount() {
17842
17942
  this.removeListeners();
17943
+ this.disconnectContent();
17843
17944
  }
17844
17945
 
17845
17946
  componentDidUpdate(prevProps) {
@@ -26427,11 +26528,16 @@ const PreviewControlsComponent = _ref => {
26427
26528
  fullScreenTitleText,
26428
26529
  closeTitleText,
26429
26530
  prevImgTitleText,
26430
- nextImgTitleText
26531
+ nextImgTitleText,
26532
+ onDownload
26431
26533
  } = _ref;
26432
26534
  return React.createElement(React.Fragment, null, React.createElement(ControlsContainer, null, React.createElement(ControlContainer$1, {
26433
26535
  withPadding: true
26434
- }, chosenImage && React.createElement("a", {
26536
+ }, chosenImage && (onDownload ? React.createElement(IconButton, {
26537
+ title: downloadTitleText || "Скачать",
26538
+ kind: "download",
26539
+ onClick: onDownload
26540
+ }) : React.createElement("a", {
26435
26541
  href: chosenImage.src,
26436
26542
  download: chosenImage.fileName,
26437
26543
  target: "_blank",
@@ -26439,7 +26545,7 @@ const PreviewControlsComponent = _ref => {
26439
26545
  }, React.createElement(IconButton, {
26440
26546
  title: downloadTitleText || "Скачать",
26441
26547
  kind: "download"
26442
- })), React.createElement(IconButton, {
26548
+ }))), React.createElement(IconButton, {
26443
26549
  title: fullScreenTitleText || "На весь экран",
26444
26550
  kind: fullscreen ? "minimize" : "maximize",
26445
26551
  onClick: toggleFullscreen
@@ -26663,7 +26769,8 @@ const PreviewComponent = _ref => {
26663
26769
  closeTitleText,
26664
26770
  prevImgTitleText,
26665
26771
  nextImgTitleText,
26666
- errorTitleText
26772
+ errorTitleText,
26773
+ onDownload
26667
26774
  } = _ref;
26668
26775
  const [{
26669
26776
  chosenImageIndex,
@@ -26686,6 +26793,7 @@ const PreviewComponent = _ref => {
26686
26793
  hasError: true
26687
26794
  } : image), [images, failedSrcs]);
26688
26795
  const decoratedChosenImage = useMemo(() => chosenImage ? decoratedImages[chosenImageIndex] ?? chosenImage : undefined, [chosenImage, chosenImageIndex, decoratedImages]);
26796
+ const handleDownload = useMemo(() => onDownload && decoratedChosenImage ? () => onDownload(decoratedChosenImage, chosenImageIndex) : undefined, [onDownload, decoratedChosenImage, chosenImageIndex]);
26689
26797
  const shouldOpenPreview = useMemo(() => images.length > 0 && isOpen, [images, isOpen]);
26690
26798
  return !shouldOpenPreview ? null : React.createElement(Portal, {
26691
26799
  root: root
@@ -26702,7 +26810,8 @@ const PreviewComponent = _ref => {
26702
26810
  fullScreenTitleText: fullScreenTitleText,
26703
26811
  closeTitleText: closeTitleText,
26704
26812
  prevImgTitleText: prevImgTitleText,
26705
- nextImgTitleText: nextImgTitleText
26813
+ nextImgTitleText: nextImgTitleText,
26814
+ onDownload: handleDownload
26706
26815
  }), React.createElement(PreviewSlideShow, {
26707
26816
  images: decoratedImages,
26708
26817
  chosenImageIndex: chosenImageIndex,