Перекрытие макета элемента только при первом отображении этого содержимого

Вопрос или проблема

Когда я использовал кастомный пакет мейсонри на Angular с помощью кастомной директивы, в первый раз контент наложился, так как я не правильно рассчитал позицию. Почему это произошло?

Я проверил отладку, и индекс целевого элемента был неправильным, поэтому они накладывались на предыдущий контент. Что мне сделать?

Я думаю, что проблема возникла только в директиве.

вставьте описание изображения здесь

import {  Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, Renderer2 } from '@angular/core';
import $ from 'jquery';

@Directive({
  selector: '[appPinGrid]',
  // standalone: true
})
export class PinGridDirective implements OnChanges, OnDestroy  {

  @Input()
  gutter: number = 0;

  @Input()
  columns: number = 0;

  @Input()
  minColWidth: number = 2;

  @Input()
  maxColWidth: number = 10;

  @Input()
  doubleColumnWidth: number = 10;

  @Input()
  itemClass: string = '';

  @Input() public data: any;

  @Output() onLayoutComplete: EventEmitter<any> = new EventEmitter();

  container: any;
  containerWidth: number = 0;
  containeHeight: number = 0;
  columnWidth: any;
  items: any;

  // columns of grid
  columnHeightArray: any;

  // timer for detect changes
  controlSkipper: any;
  resizeControlSkipper: any;

  // host listener for resize window
  // resizeListener: any;

  isSearchFirstTime: any = true;
  constructor(public el: ElementRef, public renderer: Renderer2) { }
  ngOnDestroy() {
    // unsubscribe event listener
    this.resizeListener();
  }

  resizeListener(){
 this.renderer.listen('window', 'resize', () => {
    if (this.resizeControlSkipper)
      clearTimeout(this.controlSkipper)
    this.resizeControlSkipper = setTimeout(() => {
      // console.log('relayout call');
      this.reLayout();
    }, 400);
  });
} 

  ngOnChanges(changes: { data: any; }) {
    if (this.isSearchFirstTime) {
      this.isSearchFirstTime = false;
      // this is for skip first changes detect for initialieze all directive (scroll unnecessary api of second page from here so this solution is apply)
    } else {
      if (changes.data) {
        if (this.controlSkipper)
          clearTimeout(this.controlSkipper)

        this.controlSkipper = setTimeout(async () => {
          this.reLayout();
        }, 500);
      }
    }
  }

  reLayout() {
    // console.log('relayout');
    this.container = this.el.nativeElement;
    this.items = this.el.nativeElement.childNodes;
    this.items.forEach((element: any) => {
      $(element).css('opacity', '0');
    });
    this.layoutItem();
  }

  async layoutItem() {
    // console.log('layout');
    this.getColumnWidth();
    this.initColumnHeightarray();

    for (var i = 0; i < this.items.length; i++) {
      this.placeItem(this.items[i]);
    }
    this.items.forEach((element: any) => {
      console.log('element: ', element);
      $(element).css('opacity', '1');
    });
    console.log('layout complete in function');
    await $(this.container).height(Math.max.apply({}, this.columnHeightArray));
    // console.log('set wrapper height', Math.max.apply({}, this.columnHeightArray));
    await this.onLayoutComplete.emit('LayoutComplete');
  }

  initColumnHeightarray() {
    this.resetColumnHeight();
    for (var i = 0; i < this.columns; i++) {
      this.columnHeightArray.push(0);
    }
  }

  resetColumnHeight() {
    this.columnHeightArray = [];
  }

  getColumnWidth() {
    this.containerWidth = this.el.nativeElement.offsetWidth;
    this.columnWidth = Math.floor((this.containerWidth / this.columns) - this.gutter);
    if (this.columnWidth <= Number(this.minColWidth) && this.columns >= 2) {
      this.columns--;
      this.getColumnWidth();
    }
    // in below condition I check double column width
    // because column minus if card width < 200 and suppose column = 1 then it's width is increase 
    // now maximum card width is 300 and container is about 465(200 column width + 2 number of column + 2 * gutter size) something, so that, below condition is become true and add one column i.e. 2.
    // now two column card width is again < 200 thus it will gose to infonite.
    // so I check that if container have enough space to display two column then I add column othersize it will display only one column
    if (this.columnWidth >= Number(this.maxColWidth) && this.containerWidth > this.doubleColumnWidth) {
      this.columns++;
      this.getColumnWidth();
    }
  }

  calculateImageSize() {
  }

  async placeItem(item: { nodeType: number; getElementsByClassName: (arg0: string) => { (): any; new(): any; length: number; }; }) {
    if (item.nodeType != Node.ELEMENT_NODE) {
        return; // Skip non-element nodes
    }

    // Find the column with the minimum height
    const minimumColHeight = Math.min.apply({}, this.columnHeightArray);
    const minColIndex = $.inArray(minimumColHeight, this.columnHeightArray);

    // Calculate the position
    const targetIndex = minColIndex;
    const fixMarginLeft = (this.containerWidth - this.columnWidth * this.columns - this.gutter * (this.columns - 1)) / 2;

    // If the item has the specified class, adjust its size
    if (item.getElementsByClassName(this.itemClass).length > 0) {
        const img = item.getElementsByClassName(this.itemClass)[0];
        $(img).css('position', 'initial');
        // Ensure width and height are correctly set for proper placement
        const aspectRatio = $(img).width() / ($(img).height() || 1);
        $(img).css('width', this.columnWidth);
        $(img).css('height', Math.floor(this.columnWidth / aspectRatio));
    }

    // Calculate the position for the item
    console.log('this.columnHeightArray[targetIndex]: ', this.columnHeightArray[targetIndex]);
    const position = {
      left: (this.columnWidth + this.gutter) * targetIndex + fixMarginLeft,
      top: this.columnHeightArray[targetIndex] + this.gutter // Adding gutter to the top position
    };

    // Position the item
    $(item).css('position', 'absolute');
    $(item).css('top', position.top);
    $(item).css('left', position.left);

    // Update the column height to account for the new item
    this.columnHeightArray[targetIndex] += $(item).outerHeight() + this.gutter; // Update column height including gutter
    console.log('this.columnHeightArray[targetIndex]: ', this.columnHeightArray[targetIndex]);
  }

}

Я хочу правильно настроить позицию. Как я могу это сделать?

Ответ или решение

Проблема, с которой вы столкнулись, связана с неправильным вычислением позиций элементов при первом рендере в вашем приказе "PinGridDirective". Судя по вашему описанию и предоставленному коду, ошибка возникает из-за того, что вы не учитываете, что размеры или высоты элементов могут быть не установлены до выполнения раскладки. Это может привести к наложению элементов.

Решение проблемы наложения элементов:

  1. Проверка размера изображений: Убедитесь, что размеры изображений загружены перед выполнением раскладки. Вам может понадобиться использовать событие загрузки изображений (например, load), чтобы дождаться, пока изображения будут полностью загружены, прежде чем производить вычисления.

  2. Отложенный рендеринг: Вместо того чтобы запускать reLayout сразу после изменений, дождитесь полной загрузки всех данных и изображений, прежде чем вызывать его. Это можно сделать с помощью Promise или async/await.

  3. Использование метода расчета размеров: Хотя у вас есть функция для расчета ширины колонки, убедитесь, что вы также проверяете фактические размеры элементов внутри вашего контейнера после их загрузки. Для этого может потребоваться дополнительная логика.

  4. Рендеринг после определенного таймаута: Попробуйте увеличить задержку перед вызовом reLayout, чтобы были учтены задержки загрузки элементов. Однако будьте осторожны, чтобы не слишком увеличивать задержку, так как это может привести к плохому пользовательскому опыту.

  5. Обновление резизера и очистка: Код для обновления при изменении размера окна также должен учитывать, что элементы могут быть снова наложены. Убедитесь, что ваш resizeListener корректно срабатывает и вызывает reLayout с учетом текущих размеров элементов контейнера.

Пример кода

Измените ваш метод layoutItem, добавив условие для ожидания завершения загрузки изображений:

async layoutItem() {
    this.getColumnWidth();
    this.initColumnHeightarray();

    const promises = [];

    // Собираем промисы для всех изображений
    for (let i = 0; i < this.items.length; i++) {
        const item = this.items[i];
        const itemImages = item.getElementsByClassName(this.itemClass);

        for (let img of itemImages) {
            if (img.complete) {
                // Если изображение уже загружено
                continue;
            } else {
                // Добавляем промис для загрузки изображения
                promises.push(new Promise((resolve) => {
                    img.onload = () => { resolve(); };
                    img.onerror = () => { resolve(); }; // Обрабатываем ошибки
                }));
            }
        }        
    }

    await Promise.all(promises); // Ждем загрузки всех изображений

    // Теперь размещаем элементы
    for (var i = 0; i < this.items.length; i++) {
        this.placeItem(this.items[i]);
    }

    this.items.forEach((element: any) => {
        $(element).css('opacity', '1');
    });

    await $(this.container).height(Math.max.apply({}, this.columnHeightArray));
    this.onLayoutComplete.emit('LayoutComplete');
}

Заключение

С помощью этих изменений вы сможете избежать наложения элементов при первом рендере. Очень важно, чтобы все размеры и положения элементов были правильно рассчитаны, прежде чем вы их раскладываете. Надеюсь, это решение поможет вам улучшить производительность вашего компонента и устранить проблемы с наложением контента.

Оцените материал
Добавить комментарий

Капча загружается...