Greasy Fork is available in English.

Google Data Studio | Auto Refresh

Auto update report for 𝗚𝗼𝗼𝗴𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝘂𝗱𝗶𝗼.

// ==UserScript==
// @name                 Google Data Studio | Auto Refresh
// @description          Auto update report for 𝗚𝗼𝗼𝗴𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝘂𝗱𝗶𝗼.

// @name:en              Google Data Studio | Auto Refresh
// @description:en       Auto update report for 𝗚𝗼𝗼𝗴𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝘂𝗱𝗶𝗼.

// @name:ru              Google Data Studio | Автообновление
// @description:ru       Автообновление для 𝗚𝗼𝗼𝗴𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝘂𝗱𝗶𝗼.

// @name:uk              Google Data Studio | Автопоновлення
// @description:uk       Автопоновлення для 𝗚𝗼𝗼𝗴𝗹𝗲 𝗗𝗮𝘁𝗮 𝗦𝘁𝘂𝗱𝗶𝗼.

// @name:bg              Google Data Studio | Автоматична актуализация
// @description:bg       Автоматична актуализация за Google Data Studio.

// @iconURL              http://www.gstatic.com/analytics-suite/header/suite/v2/ic_data_studio.svg
// @version              1.3
// @match                https://datastudio.google.com/reporting/*
// @require              https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @noframes
// @namespace            https://stomaks.me
// @supportURL           https://stomaks.me?feedback
// @contributionURL      https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=stomaks@gmail.com&item_name=Greasy+Fork+donation
// @author               Maxim Stoyanov (stomaks)
// @developer            Maxim Stoyanov (stomaks)
// @license              MIT
// @compatible           chrome
// @compatible           firefox
// @compatible           opera
// @compatible           safari
// ==/UserScript==



(function () {
  'use strict';

  const module = GM.info.script;



  // en: # Component: "Text".
  // ru: # Компонент: "Текст".
  module.locate = {
    "en": {
      "auto-refresh-button-enable": 'Enable automatic data refresh',
      "auto-refresh-button-disable": 'Disable automatic data refresh',
      "auto-refresh-button-more": 'Additionally',
      "minutes": 'minutes'
    },
    "ru": {
      "auto-refresh-button-enable": 'Включить автоматическое обновление данных',
      "auto-refresh-button-disable": 'Отключить автоматическое обновление данных',
      "auto-refresh-button-more": 'Дополнительно',
      "minutes": 'минут'
    },
    "uk": {
      "auto-refresh-button-enable": 'Увімкнути автоматичне оновлення даних',
      "auto-refresh-button-disable": 'Відключити автоматичне оновлення даних',
      "auto-refresh-button-more": 'Додатково',
      "minutes": 'хвилин'
    },
    "bg": {
      "auto-refresh-button-enable": 'Активиране на автоматично опресняване на данните',
      "auto-refresh-button-disable": 'Деактивирайте автоматичното опресняване на данните',
      "auto-refresh-button-more": 'В допълнение',
      "minutes": 'минути'
    }
  };



  // en: # Component: "language".
  // ru: # Компонент: "Язык".
  module.lang = (() => {
    let lang = "en";
    let temp = (navigator ? (navigator.language || navigator.systemLanguage || navigator.userLanguage) : lang).substr(0, 2).toLowerCase();

    return module.locate[temp] ? temp : lang;
  })();



  // en: # Component: "CSS".
  // ru: # Компонент: "Стили".
  module.css = () => {

    // Интеграция иконок
    $(`head`).append(`
<link rel="preload" as="font" href="//stomaks.app/fonts/MaterialIcons/MaterialIcons1.woff2" type="font/woff2" crossorigin="anonymous">
<link rel="preload" as="font" href="//stomaks.app/fonts/MaterialIcons/MaterialIcons2.woff2" type="font/woff2" crossorigin="anonymous">
<link href="//stomaks.app/styles/icons.min.css" rel="stylesheet">`);



    // Интеграция стилей
    $(`head`).append(`
<style name="alt" class="module">
  /* # alt */
  body > [name="alt"] {
    position: absolute;
    top: 50%;
    left: 50%;
    border-radius: 2px;
    padding: 5px 10px;
    max-width: 500px;
    background: rgba(97, 97, 97, 0.9);
    -webkit-transition: opacity 250ms 250ms cubic-bezier(.4, 0, .2, 1),
      transform 250ms 250ms cubic-bezier(.4, 0, .2, 1),
      top 250ms 0ms cubic-bezier(.4, 0, .2, 1),
      left 250ms 0ms cubic-bezier(.4, 0, .2, 1);
    transition: opacity 250ms 250ms cubic-bezier(.4, 0, .2, 1),
      transform 250ms 250ms cubic-bezier(.4, 0, .2, 1),
      top 250ms 0ms cubic-bezier(.4, 0, .2, 1),
      left 250ms 0ms cubic-bezier(.4, 0, .2, 1);
    -webkit-transform: scale3d(0, 0, 0);
    transform: scale3d(0, 0, 0);
    -webkit-transform-origin: top left;
    transform-origin: top left;
    opacity: 0;
    pointer-events: none;
    z-index: 970;
  }

  body > [name="alt"],
  body > [name="alt"] > * {
    color: #fff;
    font-family: Roboto, Helvetica, Arial, sans-serif;
    font-size: 10px;
    line-height: initial;
    letter-spacing: .5px;
  }

  body > [name="alt"][state="show"] {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
    opacity: 1;
  }
</style>`);



    // Интеграция стилей
    $(`head`).append(`
<style name="${ module.name.replace(/"/g, "&quot;") }" class="module">
  auto-refresh-button {
    vertical-align: middle;
  }

  auto-refresh-button > button:not([name="more"]) {
    min-width: 48px;
    max-width: 48px;
    width: 48px;
    min-height: 35px;
    max-height: 35px;
    height: 35px;
    padding: 0 !important;
    border-top-right-radius: 0 !important;
    border-bottom-right-radius: 0 !important;
  }

  auto-refresh-button > button[name="more"] {
    min-width: 20px;
    max-width: 20px;
    width: 20px;
    min-height: 35px;
    max-height: 35px;
    height: 35px;
    padding: 0 !important;
    border-top-left-radius: 0 !important;
    border-bottom-left-radius: 0 !important;
    border-left: 0;
  }

  auto-refresh-button > button:not([name="more"]) [icon="autorenew"].spin-bg {
    --color: 26, 115, 232;
  }

  body > div#auto-refresh-button-menu {
    position: absolute;
    top: 50px;
    left: 50%;
    width: 240px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12);
    z-index: 999;
  }

  body > div#auto-refresh-button-menu > ul {
    list-style: none;
    padding: 0;
  }

  body > div#auto-refresh-button-menu > ul > li {
    font-size: 14px;
    height: 40px;
    line-height: 40px;
  }

  body > div#auto-refresh-button-menu > ul > li > [icon] {
    margin-right: 16px;
    vertical-align: middle;
    color: transparent;
  }

  body > div#auto-refresh-button-menu > ul > li[done] > [icon] {
    color: rgba(0,0,0,.54);
  }

  body > div#auto-refresh-button-menu > ul > li input {
    margin-right: 10px;
    border: 0;
    border-bottom: 2px solid #e0e0e0;
    padding: 5px;
    background: transparent;
    width: 50px;
  }

</style>`);
  };



  // en: # Component: "HTML".
  // ru: # Компонент: "HTML-разметка".
  module.html = () => {
    let locate = module.locate[module.lang];



    // Интеграция панели "Всплывающая подсказка"
    $(`body`).append(`<div name="alt"></div>`);

    // Интеграция кнопки "Автообновление"
    $(`<auto-refresh-button class="header-button ng-star-inserted">
<button class="gmat-button mat-focus-indicator mat-stroked-button mat-button-base" alt="${locate["auto-refresh-button-enable"]}">
  <span class="mat-button-wrapper"><i icon="autorenew"></i></span>
  <div class="mat-button-ripple mat-ripple" matripple=""></div>
  <div class="mat-button-focus-overlay"></div>
</button>
<button name="more" aria-label="${locate["auto-refresh-button-more"]}" class="split-button-menu-button gmat-button mat-focus-indicator mat-menu-trigger mat-stroked-button mat-button-base">
  <span class="mat-button-wrapper">
  <ace-icon class="ace-icon ace-icon-arrow-drop-down ace-icon-size-small" icon="arrow-drop-down" size="small">
  <mat-icon class="mat-icon notranslate mat-icon-no-color ng-star-inserted" role="img" aria-hidden="true">
  <svg width="100%" height="100%" viewBox="0 0 18 18" fit="" preserveAspectRatio="xMidYMid meet" focusable="false">
  <path d="M5 7h8l-4 4-4-4z" fill-rule="evenodd"></path>
  </svg>
  </mat-icon>
  </ace-icon>
  </span>
  <div class="mat-button-ripple mat-ripple"></div>
  <div class="mat-button-focus-overlay"></div>
</button>
</auto-refresh-button>`).insertAfter( $(`product-tools-header refresh-button`) );
  };



  // en: # Component: "Events".
  // ru: # Компонент: "События".
  module.events = () => {
    let locate = module.locate[module.lang];



    $(`html > body`)

    // Показать подсказку
    .on("mouseover focus", "*", function ( event = window.event ) {
      // Контейнер
      let el_container = $(this).find(event.target).closest(`[alt]`);

      module.actions.showAlt( el_container );
    })

    // Показать меню для выбора интервала автообновлений
    .on("mouseup", `product-tools-header auto-refresh-button > button[name="more"]`, function ( event = window.event ) {
      const el_body = $(`body`);

      const coord = module.actions.getCoordinates( $(this) );

      if ( $(`#auto-refresh-button-menu`).length ) {
        return;
      }

      let _el = $(`
<div id="auto-refresh-button-menu" style="top: ${coord.Y + $(this).height() + 2}px; left: ${coord.X + $(this).width() - 240}px;">
<ul>
<li minutes="5" class="mat-menu-item"><i icon="done"></i><span>5 ${locate["minutes"]}</span></li>
<li minutes="10" class="mat-menu-item"><i icon="done"></i><span>10 ${locate["minutes"]}</span></li>
<li minutes="30" class="mat-menu-item other"><i icon="done"></i><input type="number" value="30" placeholder="30" min="1" max="1440" step="1"><span>${locate["minutes"]}</span></li>
</ul>
</div>`);

      // Выбрать нужное
      let delay = localStorage.getItem(`${module.report_id}-delay`) || 5;

      if ( !_el.find(`li[minutes="${delay}"]`).length ) {
        _el.find(`li[minutes="30"] input`).val(delay).attr("value", delay);
        _el.find(`li[minutes="30"]`).attr("minutes", delay);
      }

      _el.find(`li[minutes="${delay}"]`).attr("done", "done");

      el_body.append(_el);
    })

    // Скрыть меню для выбора интервала для автообновлений
    .on("mouseup", function( event = window.event ) {
      if ( $(event.target).closest('auto-refresh-button > button[name="more"], #auto-refresh-button-menu').length ) {
        // клик внутри элемента
        return;
      }

      // клик снаружи элемента
      $('#auto-refresh-button-menu').remove();
    })

    // Показать меню для выбора интервала автообновлений
    .on("mouseup", `#auto-refresh-button-menu > ul > li`, function ( event = window.event ) {
      const el = $(this);

      el.attr("done", "done");

      el.siblings(`li`).removeAttr("done");

      localStorage.setItem(`${module.report_id}-delay`, el.attr("minutes"));
    })

    // Показать меню для выбора интервала автообновлений
    .on("input", `#auto-refresh-button-menu > ul > li input`, function ( event = window.event ) {
      const el = $(this);

      let val = el.val() || 30;
      el.attr("value", val);
      el.closest(`li`).attr("minutes", val);

      localStorage.setItem(`${module.report_id}-delay`, val);
    })

    // [Включить|Выключить] автообновление.
    .on("mouseup", `product-tools-header auto-refresh-button > button:not([name="more"])`, function ( event = window.event ) {
      const el = $(this);

      let state = el.attr("state");

      if ( state == "on" ) {
        state = "off";
        el.find(`[icon="autorenew"]`).removeClass("spin-bg");
        el.attr("alt", locate["auto-refresh-button-enable"]);

        // Отмена обновления.
        if ( module.actions.updateReport.timer_id ) {
          clearInterval(module.actions.updateReport.timer_id);
          module.actions.updateReport.timer_id = null;
        }
      } else {
        state = "on";
        el.find(`[icon="autorenew"]`).addClass("spin-bg");
        el.attr("alt", locate["auto-refresh-button-disable"]);

        // Запустить обновление.
        if ( !module.actions.updateReport.timer_id ) {
          module.actions.updateReport();
        }
      }

      console.log(new Date(), module.name, "state", state);
      el.attr("state", state);
      localStorage.setItem(`${module.report_id}-state`, state);
    })
    ;
  };



  // en: # Component: "Actions".
  // ru: # Компонент: "Действия".
  module.actions = () => {
    let locate = module.locate[module.lang];

    // ID отчета
    module.report_id = new RegExp(/^\/reporting\/(.+?)\//).exec(location.pathname)[1];



    //+----------------------------------------------------------------------------------------------+
    /** Метод-утилита "getCoordinates" - Получает координаты курсора или элемента.
     *
     * @param {string|jQuery} Путь к элементу, ссылка на элемент или объект с настройками.
     *
     * @param {object} callback Данные для подписанных функций или функция обратного вызова.
     *
     * @return {object|null|function} Объект, или выполняет функцию обратного вызова.
     */
    module.actions.getCoordinates = function ( data, callback = null, event = window.event ) {
      let result = {};

      try {
        result.data = {};

        if ( data == null ) {
          result.data = {
            X: event.clientX || null,
            Y: event.clientY || null,
            x: event.pageX || null,
            y: event.pageY || null
          };
        } else {
          switch ( typeof data ) {
            case "string":
              data = $(data);

            case "object":
              if ( data instanceof jQuery && data.is(":visible") ) {
                result.data = {
                  X: data.position().left || null,
                  Y: data.position().top || null,
                  x: data.offset().left || null,
                  y: data.offset().top || null
                };
              }
              break;

            default: break;
          }
        }



        // Без обратного вызова
        if ( !callback ) {
          return result.data;
        }

        // Функция обратного вызова
        if ( typeof callback === "function" ) {
          return callback( result );
        }
      } catch ( error ) {
        result.error = error;
        result.data = null;
      }

      return result.data;
    };
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Метод-действие "showAlt" - Отображает подсказку.
     *
     * @param {string|jQuery} Путь к элементу, или ссылка на элемент, или объект с настройками.
     *
     * @param {object} callback Данные для подписанных функций или функция обратного вызова.
     *
     * @return {object|null|function} Объект, или выполняет функцию обратного вызова.
     */
    module.actions.showAlt = function ( data, callback = null, event = window.event ) {
      let result = {};

      result.data = {};

      try {
        function _ ( x, y ) {
          let float = [];
          let temp = x / $(`body`).width() * 100;

          if ( temp <= 10 ) {
            float.push("left");
          } else if ( temp > 10 && temp < 90 ) {
            float.push("center");
          } else {
            float.push("right");
          }

          temp = y / $(`body`).height() * 100;

          if ( temp <= 10 ) {
            float.push("top");
          } else if ( temp > 10 && temp < 90 ) {
            float.push("center");
          } else {
            float.push("bottom");
          }

          return float;
        }

        // Контейнер
        switch ( typeof data ) {
          case "string":
            if ( data.length > 0 ) {
              result.data.container = $(data);
              break;
            }

          case "object":
            if ( data instanceof jQuery ) {
              result.data.container = data;
              break;
            }

          default:
            throw new TypeError(`Входящие данные не определены или имеют неверный тип данных.`);
        }

        // Подсказка
        result.data.alt = result.data.container.attr("alt");

        // Направление подсказки
        result.data.float = [];
        {
          let temp = result.data.container.attr("alt-float");

          if ( typeof temp === "string" ) {
            temp = temp.split(" ");

            result.data.float = [temp[0], temp[1]];
          }
        }

        // Определение соответствия текста в alt и в элементе
        function isAlt ( el, alt ) {
          if ( el.children().length > 0 ) {
            // Видимые в элементы
            el = el.children(`:visible:not(.content)`).filter(function() {
              return !($(this).css(`opacity`) === "0" || $(this).css(`visibility`) === "hidden");
            });
          }

          // Текст
          result.data.text = el.text().replace(/^\s+|\s+$/g, ``);

          return (typeof alt === "string" && alt.length ); // && alt !== result.data.text
        }

        if ( isAlt(result.data.container, result.data.alt) ) {
          $(`body > div[name="alt"]`)
            .attr("state", "show")
            .html( result.data.alt );



          // Получить координаты контейнера
          result.data.coordinates = module.actions.getCoordinates( data );

          // Валидация направления подсказки
          {
            let isX = false;
            if ( result.data.float[0] === "left" || result.data.float[0] === "center" || result.data.float[0] === "right" ) {
              isX = true;
            }

            let isY = false;
            if ( result.data.float[1] === "top" || result.data.float[1] === "center" || result.data.float[1] === "bottom" ) {
              isY = true;
            }

            if ( !isX || !isY || result.data.float.length !== 2 ) {
              // Получить положение контейнера
              let temp = _(result.data.coordinates.x, result.data.coordinates.y);

              if ( !isX ) {
                result.data.float = [temp[0], result.data.float[1]];
              }

              if ( !isY ) {
                result.data.float = [result.data.float[0], temp[1]];
              }
            }
          }



          let app_width = $(`body`).outerWidth() || $(`body`).width();
          let app_height = $(`body`).outerHeight() || $(`body`).height();

          let alt_width = $(`body > div[name="alt"]`).outerWidth() || $(`body > div[name="alt"]`).width();
          let alt_height = $(`body > div[name="alt"]`).outerHeight() || $(`body > div[name="alt"]`).height();

          let container_width = result.data.container.outerWidth() || result.data.container.width();
          let container_height = result.data.container.outerHeight() || result.data.container.height();

          switch ( result.data.float.join(" ")  ) {
            case "left top": // ↘
              result.data.coordinates.y += container_height + 10;
              break;

            case "left center": // →
              result.data.coordinates.x += container_width + 20;
              result.data.coordinates.y += (container_height - alt_height ) / 2;
              break;

            case "left bottom": // ↗
              result.data.coordinates.x += container_width + 20;
              result.data.coordinates.y += (container_height - alt_height ) / 2;
              break;

            case "center top": // ↓
              result.data.coordinates.x += ((container_width - alt_width) / 2);
              result.data.coordinates.y += container_height + 10;
              break;

            case "center center": // •
              result.data.coordinates.x += ((container_width - alt_width) / 2);
              result.data.coordinates.y += container_height + 10;
              break;

            case "center bottom": // ↑
              result.data.coordinates.x += ((container_width - alt_width) / 2);
              result.data.coordinates.y -= alt_height + 10;
              break;

            case "right top": // ↙
              result.data.coordinates.x += container_width - alt_width;
              result.data.coordinates.y += container_height + 10;
              break;

            case "right center": // ←
              result.data.coordinates.x -= alt_width + 25;
              result.data.coordinates.y += (container_height - alt_height ) / 2;
              break;

            case "right bottom": // ↖
              result.data.coordinates.x -= alt_width + 25;
              result.data.coordinates.y += (container_height - alt_height ) / 2;
              break;
          }

          if ( result.data.coordinates.x < 20 ) {
            result.data.coordinates.x = 20;
          }
          if ( result.data.coordinates.x + 40 >= app_width ) {
            result.data.coordinates.x = app_width - (alt_width + 20);
          }

          if ( result.data.coordinates.y < 20 ) {
            result.data.coordinates.y = 20;
          }
          if ( result.data.coordinates.y + 40 >= app_height ) {
            result.data.coordinates.y = app_height - (alt_height + 20);
          }



          $(`body > div[name="alt"]`)
            .css({
            "left": result.data.coordinates.x,
            "top": result.data.coordinates.y,
            "-webkit-transform-origin": result.data.float.join(" "),
            "transform-origin": result.data.float.join(" ")
          });
        } else {
          $(`body > div[name="alt"]`)
            .attr("state", "hide");
        }



        // Без обратного вызова
        if ( !callback ) {
          return result.data;
        }

        // Функция обратного вызова
        if ( typeof callback === "function" ) {
          return callback( result );
        }
      } catch ( error ) {
        result.error = error;
        result.data = null;
      }

      return result.data;
    };
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Проверить, мы на странице редактирования?
     */
    module.actions.isEdit = function () {
      return /^\/reporting\/.+?\/page\/.+?\/edit/.test(location.pathname);
    };
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Проверить, мы на странице просмотра?
     */
    module.actions.isView = function () {
      return /^\/reporting\/.+?\/page\/.+?/.test(location.pathname) && !/^\/reporting\/.+?\/page\/.+?\/edit/.test(location.pathname);
    };
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Метод-действие "updateReport" - Обновляет отчет.
     */
    module.actions.updateReport = function () {
      // Кнопка ручного обновления
      const el_button = $(`product-tools-header refresh-button`);

      if ( el_button.length ) {
        el_button.find(`> button`).click();

        console.log(new Date(), module.name, "updateReport");
      }

      let delay = localStorage.getItem(`${module.report_id}-delay`);

      if ( delay == "" || delay == null || delay < 1 ) {
        delay = 5;
      }

      // Задержка перед запуском
      delay = Number(delay) * 60 * 1000;

      let state = localStorage.getItem(`${module.report_id}-state`);
      console.log(new Date(), module.name, "delay", delay, "state", state);

      // Запустить автообновление отчета через указаное время
      if ( state == "on" ) {
        if ( !module.actions.updateReport.timer_id ) {
          module.actions.updateReport.timer_id = setInterval(module.actions.updateReport, delay);
        }
      } else {
        if ( module.actions.updateReport.timer_id ) {
          clearInterval(module.actions.updateReport.timer_id);
        }
        module.actions.updateReport.timer_id = null;
      }
    };
    module.actions.updateReport.timer_id = null;
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Проверить состояние.
     */
    (() => {
      // Если мы на странице в режиме просмотра отчета (не редактирования)
      let state = localStorage.getItem(`${module.report_id}-state`);
      let el = $(`product-tools-header auto-refresh-button > button:not([name="more"])`);

      if ( state == "on" ) {
        state = "on";
        el.find(`[icon="autorenew"]`).addClass("spin-bg");
        el.attr("alt", locate["auto-refresh-button-disable"]);

        if ( module.actions.isView() ) {
          // Запустить обновление.
          if ( !module.actions.updateReport.timer_id ) {
            module.actions.updateReport();
          }
        }
      } else {
        state = "off";
        el.find(`[icon="autorenew"]`).removeClass("spin-bg");
        el.attr("alt", locate["auto-refresh-button-enable"]);

        // Отмена обновления.
        if ( module.actions.updateReport.timer_id ) {
          clearInterval(module.actions.updateReport.timer_id);
        }
        module.actions.updateReport.timer_id = null;
      }

      el.attr("state", state);
    })();
    //+----------------------------------------------------------------------------------------------+



    //+----------------------------------------------------------------------------------------------+
    /** Отслеживание изменения URL.
     */
    module.actions.checkURLchange = function () {
      const oldURL = module.actions.checkURLchange.oldURL;
      const currentURL = window.location.href;

      if ( currentURL != oldURL ) {
        module.actions.checkURLchange.oldURL = currentURL;

        // URL изменился!
        console.log(new Date(), module.name, "URL change");

        let el = $(`product-tools-header auto-refresh-button > button:not([name="more"])`);
        let state;

        // Если мы на странице в режиме просмотра отчета (не редактирования)
        if ( module.actions.isView() ) {
          state = localStorage.getItem(`${module.report_id}-state`);

          if ( state == "on" ) {
            // Запустить обновление.
            if ( !module.actions.updateReport.timer_id ) {
              module.actions.updateReport();
            }

            console.log(new Date(), module.name, "start updateReport");
          } else {
            // Отмена обновления.
            if ( module.actions.updateReport.timer_id ) {
              clearInterval(module.actions.updateReport.timer_id);
            }
            module.actions.updateReport.timer_id = null;

            console.log(new Date(), module.name, "stop updateReport");
          }
        } else {
          // Отмена обновления.
          if ( module.actions.updateReport.timer_id ) {
            clearInterval(module.actions.updateReport.timer_id);
          }
          module.actions.updateReport.timer_id = null;

          console.log(new Date(), module.name, "stop updateReport");
        }
      }

      module.actions.checkURLchange.oldURL = window.location.href;

      setInterval(module.actions.checkURLchange, 1000);
    };

    module.actions.checkURLchange.oldURL = window.location.href;
    module.actions.checkURLchange.currentURL = window.location.href;

    module.actions.checkURLchange();
    //+----------------------------------------------------------------------------------------------+
  };



  // en: # Component: "INIT".
  // ru: # Компонент: "Инициализация".
  (() => {
    // Отслеживаем появление элемента на странице
    (function tick () {
      const el = $(`product-tools-header refresh-button`);

      if ( el.length ) {
        console.log(new Date(), module.name, "init");

        module.css();
        module.html();
        module.events();
        module.actions();
      } else {
        setTimeout(tick, 1000);
      }
    })();
  })();
})();