Финансовая поддержка VPS, бесплатные дополнения и docs Донаты оплачивают сервер и двигают бесплатные дополнения MODX и документацию. Поддержать

Всего 126 114 комментариев

Алексей
Алексей
Добрый день, может кто поможет на MODX 2.8.8 ставлю ExtraFields все работает все классно, но если делаю тип поля «выпадающей список мультивыбор» из ресурсов, такое условие:

@SELECT `pagetitle`,`id` FROM `modx_site_content` WHERE `parent` = 23 ORDER BY `pagetitle` ASC
то фронтент всего сайта становится белый, в логах ошибок нет, в админке мультивыбор работает. Выявил что если отключаю системное событие OnMODXInit то сайт оживает. Подскажите в чем может быть проблема?
Konstantin
Konstantin
Спасибо Артур за ответ, удалил. Удалил также из сниппета всю логику вообще для теста, оставил только
return $SendIt->success('ок', ['html' => 'TEST']);
Ошибки ушли только после того как в названии сниппета убрал нижнее подчеркивание. Но данные пришедшие из сниппета не подставляются в указанный в пресете 'resultBlockSelector' => '#filter-results'. Что-то нужно еще прописать в js?
Евгений
Евгений
Вот мой вариант. Тоже тут решил заняться вопросом избавления себя от jquery… :)
Сделал отдельный файл pdopage-no-jquery.js. Запихнул его в /assets/components/custom/
Комментарии решил оставить.
Получилось, вроде, неплохо, но кажется, что есть что ещё можно доработать

Вызов PdoPage
<div id="pdopage">
	<div class="section__pagelist">
		[[!pdoPage?
			&parents=`[[*id]]`
			&depth=`0`
			&tpl=`tpl_example`
			&sortby=`{"publishedon": "DESC"}`
			&limit=`6`
			&tplPageActive=`@INLINE <li class="active"><span class="page-link">[[+pageNo]]</span></li>`
			&ajax=`1`
			&ajaxElemRows=`#pdopage .section__pagelist`
			&ajaxElemMore=`#pdopage .btn-loadmore`
    		        &ajaxTplMore=`@INLINE <button class="btn btn-primary btn-sm btn-loadmore">Загрузить ещё</button>`
			&ajaxMode=`button`
			&frontend_js=`[[++assets_url]]/components/custom/pdopage-no-jquery.js`
		]]
	</div>
	[[-!+page.nav]]
</div>
Проверил во всех модах: дефолт (там, собственно, скрипт и не нужен), button, scroll

Сам js-код
const pdoPage = (function () {
  /*
  Параметры, которые прокидывает PdoPage в конфиг
  const basePdoToolsConfig = {
    wrapper: '#pdopage',
    rows: '#pdopage .rows',
    pagination: '#pdopage .pagination',
    link: '#pdopage .pagination a',
    more: '#pdopage .btn-more',
    moreTpl: '<button class='btn btn-primary btn-more'>Загрузить ещё</button>',
    mode: 'button',
    history: 0,
    pageVarKey: 'page',
    pageLimit: '6',
    assetsUrl: '/assets/components/pdotools/',
    connectorUrl: '/assets/components/pdotools/connector.php',
    pageId: 1,
    hash: 'string',
    scrollTop: true,
  };*/

  const additionalConfig = {
    scrollOffset: 150,
    moreBtnLoadingHTML: '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Загружаем...',
    rowsContainerLoadingClass: 'state--progress',
  };

  let keys = {};
  let configs = {};
  let isLoading = false;
  let isEndReached = false;

  function getHashParams() {
    const hash = window.location.hash.substring(1);
    return Object.fromEntries(new URLSearchParams(hash));
  }
  function setHashParams(params) {
    const hash = new URLSearchParams(params).toString();
    window.location.hash = hash;
  }
  function removeHashKey(key) {
    const params = getHashParams();
    delete params[key];
    setHashParams(params);
  }
  function addHashKey(key, value) {
    const params = getHashParams();
    params[key] = value;
    setHashParams(params);
  }

  function insertMoreButton(config) {
    const containerWrapper = document.querySelector(config.wrapper);
    const moreBtn = document.querySelector(config.more);

    if (!containerWrapper || moreBtn !== null) return;

    const moreTpl = config.moreTpl;
    containerWrapper.insertAdjacentHTML('beforeend', moreTpl);
  }

  function removeMoreButton(config) {
    const moreBtn = document.querySelector(config.more);

    if (moreBtn) moreBtn.remove();
  }

  function setLoadingState(config) {
    const moreBtn = document.querySelector(config.more);
    const containerRows = document.querySelector(config.rows);

    containerRows.classList.add(additionalConfig.rowsContainerLoadingClass);

    if (moreBtn) {
      if (!config.moreBtnText) {
        config.moreBtnText = moreBtn.textContent;
      }
      moreBtn.disabled = 'disabled';
      moreBtn.innerHTML = additionalConfig.moreBtnLoadingHTML;
    }
  }

  function removeLoadingState(config) {
    const moreBtn = document.querySelector(config.more);
    const containerRows = document.querySelector(config.rows);

    containerRows.classList.remove(additionalConfig.rowsContainerLoadingClass);

    if (moreBtn) {
      moreBtn.disabled = '';
      moreBtn.textContent = config.moreBtnText;
    }
  }

  function loadPage(url, config, append = false) {
    if (isLoading || isEndReached) return;

    isLoading = true;

    setLoadingState(config);

    fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
      .then((res) => {
        if (!res.ok) throw new Error(`Ошибка сети: ${res.status}`);

        return res.json();
      })
      .then((data) => {
        // data.output — HTML-контент
        // data.pagination — HTML пагинации
        // data.pages — всего страниц
        // data.page — текущая страница

        const containerRows = document.querySelector(config.rows);
        const containerPagination = document.querySelector(config.pagination);

        if (!containerRows) {
          throw new Error('Контейнер ajaxElemRows не найден в DOM');
        }

        if (append) {
          containerRows.insertAdjacentHTML('beforeend', data.output);
        } else {
          containerRows.innerHTML = data.output;
        }

        if (containerPagination) {
          containerPagination.innerHTML = data.pagination;
        }

        const currentPage = Number(data.page) || 1;
        const totalPages = Number(data.pages) || 1;

        // Проверка на конец данных
        if (currentPage >= totalPages || !data.output.trim()) {
          isEndReached = true;
          removeMoreButton(config);
          if (config.onEnd) config.onEnd();
        } else {
          isEndReached = false;
          if (config.mode === 'button') {
            removeLoadingState(config);
            insertMoreButton(config);
          }
        }

        if (config.callback && typeof config.callback === 'function') {
          config.callback();
        }
      })
      .catch((err) => {
        console.error(err);
      })
      .finally(() => {
        isLoading = false;
      });
  }

  function init(config) {
    if (!config) {
      console.error('pdoPage: конфигурация не передана');
      return;
    }

    configs[config.pageVarKey] = config;

    const pageKey = config.pageVarKey;
    const wrapperSelector = config.wrapper;
    const moreBtnSelector = config.more;
    const linkSelector = config.link;

    const params = new URLSearchParams(window.location.search);
    const pageParam = params.get(pageKey) || getHashParams()[pageKey] || '1';
    keys[pageKey] = Number(pageParam);

    switch (config.mode) {
      case 'default':
        document.addEventListener('click', (e) => {
          const target = e.target.closest(linkSelector);
          if (!target) return;
          e.preventDefault();

          const href = target.href;
          const regex = new RegExp(pageKey + '=(\\d+)');
          const match = href.match(regex);
          const page = match ? Number(match[1]) : 1;

          if (keys[pageKey] === page) return;

          keys[pageKey] = page;
          isEndReached = false;

          if (config.ajaxHistory) {
            if (page === 1) {
              removeHashKey(pageKey);
              history.pushState({}, '', href.split('?')[0]);
            } else {
              addHashKey(pageKey, page);
              history.pushState({}, '', href);
            }
          }

          loadPage(href, config, false);
        });
        break;

      case 'button':
        const container = document.querySelector(wrapperSelector);
        if (!container) {
          console.error(`pdoPage: не найден контейнер ${wrapperSelector}`);
          return;
        }

        insertMoreButton(config);

        container.addEventListener('click', (e) => {
          e.preventDefault();

          const target = e.target.closest(moreBtnSelector);
          if (!target) return;

          if (isLoading || isEndReached) return;

          const nextPage = keys[pageKey] + 1;
          const baseUrl =
            target.dataset.baseUrl || window.location.href.split('?')[0];
          const newUrl = new URL(baseUrl);
          newUrl.searchParams.set(pageKey, nextPage);

          keys[pageKey] = nextPage;

          if (config.ajaxHistory) {
            addHashKey(pageKey, nextPage);
            history.pushState({}, '', newUrl);
          }

          loadPage(newUrl.toString(), config, true);
        });
        break;

      case 'scroll':
        if (!config.scrollOffset) {
          config.scrollOffset = additionalConfig.scrollOffset;
        }

        window.addEventListener('scroll', () => {
          if (isLoading || isEndReached) return;

          const containerRows = document.querySelector(config.rows);
          if (!containerRows) return;

          // Получаем позицию bottom элемента со списком относительно документа
          const rect = containerRows.getBoundingClientRect();
          const scrollY =
            window.pageYOffset || document.documentElement.scrollTop;
          const elementBottom = rect.bottom + scrollY;

          // Текущая позиция прокрутки + высота окна
          const scrollPos = window.pageYOffset + window.innerHeight;

          // Сравниваем: если прокрутка достигла или прошла элемент + offset
          if (scrollPos >= elementBottom + config.scrollOffset) {
            const nextPage = keys[pageKey] + 1;
            const baseUrl = window.location.href.split('?')[0];
            const newUrl = new URL(baseUrl);
            newUrl.searchParams.set(pageKey, nextPage);

            keys[pageKey] = nextPage;

            if (config.ajaxHistory) {
              addHashKey(pageKey, nextPage);
              history.pushState({}, '', newUrl);
            }

            loadPage(newUrl.toString(), config, true);
          }
        });
        break;
    }

    if (config.history) {
      window.addEventListener('popstate', () => {
        const params = new URLSearchParams(window.location.search);
        const page = params.get(pageKey) || getHashParams()[pageKey] || '1';

        if (keys[pageKey] !== Number(page)) {
          keys[pageKey] = Number(page);
          const baseUrl = window.location.origin + window.location.pathname;
          const newUrl = baseUrl + '?' + pageKey + '=' + page;

          loadPage(newUrl, config, false);
        }
      });
    }
  }

  return {
    initialize: init
  };
})();
Main HP
Main HP
У меня была проблема с тем, что сделки не попадали в воронку. Хотя в списке контакт создавался. В итоге надо было просто поставить галочку в сист. настройках. Этот компонент работает на старом апи. Сейчас у амо по-другому устроено.
Наумов Алексей
Наумов Алексей
Я слабо понял суть вопроса. Подозреваю что этот комментарий дублирует суть вопроса modx.pro/help/25398

Еще раз в этом случае — сниппет Login не работает с pdoTools и Fenom синтаксисом.
Евгений Лазарев
Евгений Лазарев
Нашел проблему еще одну. Я так понял нельзя вызывать сниппет с помощью fenom {'!YaSmartCaptcha' | snippet}. Поскольку при авторизации через Login если не проходил проверку то перезагрузится страница и весь fenom выйдет на странице необработанный в виде {'site_name' | option}
Сергей Водолагин
Сергей Водолагин
Отличная новость, спасибо за ваш труд.
Очень ждём!
Есть один комментарий, смотрю я на скрин
Ну и самое интересное. Вот так выглядит обновленная вкладка свойства товара
и понимаю, что опять придётся для всех цветов и размеров делать отдельные товары так как поле Остаток на складе у них общее (что не может быть — 1 цвет + 1 размер и поле остаток).
Идеальный вариант отдельная вкладка в товаре с таблицей в которой поля: Артикул (может быть одинаковый по цветам), Цвет, Размер, Остаток на складе.