Html-фрейм#

HTML-фрейм — это элемент интерфейса, предназначенный для редактирования и отображения HTML-кода. В системе Global реализовано два вида фрейма: обычный и произвольный.

Обычный Html-фрейм#

Обычный HTML-фрейм предназначен исключительно для отображения статического HTML-контента, данный тип фрейма не интерактивен.

Используется, чтобы:

  • визуализировать отчёты, графики, диаграммы (SVG, Canvas и т.п.);

  • показать сформированный HTML-документ (например, письмо, шаблон, справку);

  • отобразить содержимое без возможности редактирования или взаимодействия.

Создание#

В Avi инициализируем анонимный класс:

def reportView(): ReportView = {
  new ReportView {
    override def meta: TypeTag = this
  }
}

После этого создаём trait ReportView.

Создаём case class для хранения html атрибута:

case class Row(var sReportHtml: NString)

В trait переопределяем onRefresh и в нём собираем case class для того, чтобы вывести html. sReportHtml хранит сгенерированный html.

override protected def onRefresh: Recs = {
   val sReportHtml =
      """
        |<html>
        |<body>
        |    Привет, мир!
        |</body>
        |</html>
        |""".stripMargin
     Row(sReportHtml = sReportHtml)
}

После этого создаём в Avm representation:

<representation name="ReportView" caption="Отчёт" editMode="notEdit">
       <layout>
           <simpleComposer>
               <frame toolBar.isVisible="false">
                   <html htmlAttr="sReportHtml"/>
               </frame>
           </simpleComposer>
      </layout>
</representation>

Свойства#

Основные свойства HTML-фрейма:

  • isEditable — разрешить редактирование страницы;

  • isInsertImageBtnEnabled — вставить изображение;

  • htmlAttr — атрибут, содержащий текст HTML (без него не будет HTML, который мы сформируем);

  • htmlSaveMode — режим сохранения страницы;

  • scrollDownAfterRefresh — переместиться в конец документа после обновления;

  • Sandbox — управляет sandbox-разрешениями фрейма;

  • useMasterData — указывает, что редактор получает и изменяет данные из выборки мастер-фрейма.

Разрешение выполнения JavaScript в HTML-фреймах#

В целях повышения безопасности система жёстко регулирует выполнение JavaScript (JS) внутри HTML-фреймов. Возможность запуска JS зависит от режима работы выборки:

  • Режим редактирования — выполнение JS запрещено всегда, независимо от настроек sandbox в avm;

  • Режим просмотра (readOnly) — выполнение JS разрешено, только если оно явно разрешено через свойство sandbox в avm.

Настройка свойства sandbox в avm#

Свойство sandbox представляет собой список разрешений, разделённых пробелами. По умолчанию, если свойство не указано, устанавливается allow-same-origin.

Внимание

Чтобы разрешить выполнение JS в режиме просмотра, вы обязательно должны включить флаг allow-scripts.

Пример

<!--Важно, чтобы у representation editMode был notEdit (режим просмотра), тогда атрибут sandbox будет работать-->
<representation name="CardDet_Description" caption="Описание" editMode="notEdit">
        <layout>
            <simpleComposer>
                <frame toolBar.isVisible="false">
                    <html htmlAttr="sDescription" isEditable="true" sandbox="allow-scripts allow-popups allow-modals"/>
                </frame>
            </simpleComposer>
        </layout>
</representation>

Список основных разрешений

  • allow-scripts — ключевое разрешение. Позволяет выполнение JavaScript. Без этого флага любой JS в HTML-фрейме будет заблокирован, даже в режиме просмотра.
    Используется, чтобы создавать интерактивные элементы: обрабатывать клики, показывать всплывающие подсказки, анимировать диаграммы или обновлять содержимое без перезагрузки страницы.

  • allow-same-origin — позволяет контенту фрейма получать доступ к данным (например, cookies, localStorage) как если бы он был загружен с того же домена (origin). Включается по умолчанию, если не указано иное. Если вы его укажете, но не укажете allow-scripts, то JS всё равно не будет работать.
    Используется, чтобы сохранять состояние между перезагрузками (например, выбранные фильтры) и делать запросы к API с аутентификацией через cookies.

  • allow-forms — позволяет отправлять формы изнутри фрейма.
    Используется, чтобы пользователь мог заполнять и отправлять HTML-формы (например, формы обратной связи, поиска или авторизации), и данные попадали в обработчик на backend.

  • allow-popups — позволяет открывать новые окна браузера (например, через window.open).
    Используется, чтобы по клику в фрейме открывалась новая вкладка — например, ссылка на внешний сайт, документ для печати или страница авторизации в соцсетях.

  • allow-modals — позволяет показывать модальные окна (alert, confirm, prompt).
    Используется, чтобы отображать системные диалоги для подтверждения действий, показа ошибок или запроса информации у пользователя без создания кастомных модальных окон.

  • allow-popups-to-escape-sandbox — позволяет новым всплывающим окнам не наследовать песочницу, то есть работать как обычные вкладки.
    Используется, чтобы страницы, открытые из фрейма (например, платежные системы, внешние сервисы), работали без ограничений — могли отправлять формы, запускать скрипты и использовать все веб-API.

  • allow-pointer-lock — позволяет использовать Pointer Lock API.
    Используется, чтобы приложения (например, 3D-редакторы, игры или картографические сервисы) могли захватывать курсор мыши для точного управления без ограничений границ экрана.

Подробнее о всех доступных разрешениях можно узнать в официальной документации: набор sandbox-разрешений.

Произвольный Html-фрейм#

В случае, когда стандартного инструментария Global не хватает для реализации пользовательского интерфейса, есть возможность сделать отображение, содержащее произвольный Html-фрейм.

Произвольный Html-фрейм — это механизм встраивания пользовательского HTML-контента в интерфейс приложения. Он позволяет использовать собственную HTML-разметку, стили и (при разрешении) JavaScript-логику внутри изолированного контейнера, полностью управляемого через backend-методы. Такой фрейм предоставляет гибкость при сохранении интеграции с общей архитектурой приложения.

Ключевая особенность произвольного фрейма — он отлавливает действия пользователя (например, отправку формы, клик по ссылке, ввод текста) и передаёт их в backend для дальнейшей обработки. Это позволяет реализовывать интерактивные интерфейсы — например, чаты, кастомные формы или динамические панели управления — с сохранением интеграции в общую архитектуру приложения.

Создание#

Для создания отображения с html-фреймом нужно унаследовать выборку от Btk_HtmlWebAbsAvi и создать в ней отображение, унаследованное от WebHtml из Btk_HtmlWebAbsAvi.

В отображении нужно имплементировать методы onRefresh, getHtmlText, onSubmitForm и onBeforeNavigate.
В avm отображения нужно в теге <frame> создать тег <extControl> и указать в нём параметр name="btk/HtmlFrame".

Пример создания

Avi

class RplTst_TestHtmlAvi extends Btk_HtmlWebAbsAvi {

  def testHtml(): TestHtml = {
    new TestHtml {
      override def meta: TypeTag = this
    }
  }

  trait TestHtml extends WebHtml {
    override def onRefresh: Recs = {
      null
    }

    override def getHtmlText: NString = {
      """
        |<!docType html>
        |<html>
        |<head></head>
        |<body>
        |      <div>Текст</div>
        |<body>
        |""".stripMargin
    }

    override def onSubmitForm(data: JObject): WebExtResponse = {
      WebExtResponse.none()
    }

    override def onBeforeNavigate(url: NString): WebExtResponse = {
      WebExtResponse.none()
    }
  }
}

Avm

<representation name="TestHtml">
      <layout>
           <simpleComposer>
               <frame>
                   <extControl name="btk/HtmlFrame"/>
               </frame>
           </simpleComposer>
      </layout>
</representation>

Возможности произвольного Html-фрейма#

Взаимодействие с backend#

Html-фрейм может взаимодействовать с backend через стандартные обработчики событий и через gsf-запросы из JavaScript-кода.

Доступные обработчики:

  • onSubmitForm — исполняется на событие submit в элементе <form>. На вход подаётся JObject с полями:

    • formData — пары ключ-значение, представляющие поля формы и их значения, в формате JObject;

    • formName — название формы, строка;

    • idButton — идентификатор источника отправки формы, строка.

  • onBeforeNavigate — исполняется при клике по элементу <a> или его дочернему элементу. На вход подаётся значение поля href, строка.

  • onSetElementValue — исполняется при вводе в элементы <textarea> и <input>, если у них установлено значение поля gsf:postData="true". На вход подаётся кейс-класс с идентификатором элемента и введённым значением.

  • onBuild — исполняется при построении узла компонента. На вход ничего не подаётся.

  • onPostRequest — исполняется при выполнении gsf-запроса из JavaScript-кода внутри Html-фрейма.

Gsf-запросы и метод onPostRequest

Gsf-запросы используются, когда JavaScript-код внутри Html-фрейма должен передать данные в серверную логику выборки. Такой вызов не привязан к конкретному событию Html-фрейма: его можно выполнить из любого JavaScript-обработчика, например при клике, изменении значения, получении сообщения через postMessage или другом пользовательском действии.

Дальнейшие шаги относятся только к gsf-запросам и методу onPostRequest.

  1. Подготовить произвольный Html-фрейм.

    Для использования gsf-запросов должны быть выполнены требования для произвольного Html-фрейма:

    • в avm-файле в теге <frame> должен быть указан <extControl name="btk/HtmlFrame"/>;

    • отображение должно наследоваться от WebHtml из Btk_HtmlWebAbsAvi.

  2. Добавить обработчик события в JavaScript-код.

    В JavaScript-коде HTML-фрейма нужно определить событие, по которому данные будут отправляться на backend.

    Например, gsf-запрос можно вызвать из обработчика клика:

    document.getElementById("buttonId").addEventListener("click", () => {
      document.gsf
        .newRequest("testRequest", {field: "value"})
        .post()
        .then(result => {
          console.log(result);
        });
    });
    
  3. Передать данные через document.gsf.newRequest(...).

    Метод newRequest принимает два аргумента:

    • имя запроса — строка, по которой в выборке идентифицируется вызов;

    • объект данных — произвольный JavaScript-объект, который преобразуется в JSON и передаётся в метод onPostRequest.

    Для работы с запросом используются методы объекта Request:

    • .post() — отправляет запрос на backend и вызывает метод onPostRequest;

    • .useLock() — блокирует пользовательский интерфейс до получения ответа от backend;

    • .useSync и .useASync — пока не используются.

    Пример вызова с блокировкой интерфейса:

    document.gsf
      .newRequest("testRequest", {field: "value"})
      .useLock();
    
  4. Переопределить onPostRequest в выборке.

    На стороне backend данные обрабатываются в методе onPostRequest. Метод необходимо переопределить в классе выборки с Html-фреймом.

    override def onPostRequest(name: NString, data: JObject): JObject = {
      super.onPostRequest(name, data)
    }
    

    В параметр name передаётся имя запроса, указанное при вызове newRequest. В параметр data передаётся JSON-объект с данными из JavaScript-кода.

  5. Обработать запрос по имени.

    Внутри onPostRequest можно определить обработку для разных gsf-запросов по значению параметра name.

    override def onPostRequest(name: NString, data: JObject): JObject = {
      name match {
        case ns"testRequest" =>
          val value = data.getNString("field")
          JObject()
    
        case _ =>
          super.onPostRequest(name, data)
      }
    }
    
  6. Использовать postMessage, если данные приходят из iframe.

    Gsf-запрос можно использовать вместе с postMessage, если iframe передаёт данные через событие postMessage и эти данные нужно обработать на backend. В этом сценарии postMessage используется для передачи данных на клиентской стороне, а document.gsf.newRequest(...) — для дальнейшей передачи полученных данных в серверную логику выборки.

    В HTML-странице, которая принимает сообщение от iframe, добавляется обработчик события message. Событие возникает, когда iframe отправляет сообщение через window.parent.postMessage(...).

    window.addEventListener("message", function(event) {
      // обработка сообщения
    });
    

    Перед обработкой данных необходимо проверить источник сообщения. Это позволяет убедиться, что сообщение пришло от ожидаемого iframe.

    Для проверки используется:

    • event.source — ссылка на окно, отправившее сообщение;

    • document.getElementById("iframeId").contentWindow — ссылка на окно целевого iframe.

    Пример обработки сообщения и передачи данных на backend:

    window.addEventListener("message", (event) => {
      if (event.source === document.getElementById("iframeId").contentWindow) {
        document.gsf
          .newRequest("testRequest", event.data)
          .post()
          .then(result => {
            console.log(result);
          });
      }
    });
    

Взаимодействие с фронтендом#

Backend может взаимодействовать с Html-фреймом только через модель данных.
Изменять модель данных можно через возвращаемый WebExtResponse или напрямую, обращаясь к методам buildContentAsHtml и buildContentAsLink в abi (интерфейс backend-приложения).

Разберём на примере полей кейс-класса WebExtResponse:

  • needRefreshHtml — если true, модель данных обновляется через метод buildContentAsHtml (перекрывается параметром needNavigateToUrl). Если false — нет.

  • tryRestoreHtmlPosition — если true, будет попытка вернуть полосу прокрутки и зум в состояние, в котором они были до обновления или сворачивания. Если false — нет. Аналогичный параметр есть в buildContentAsHtml.

  • needNavigateToUrl — если true, будет использоваться метод abi buildContentAsLink, это приведёт к переходу по ссылке, указанной в urlToNavigate, и игнорированию остальных параметров. Если false — нет.

  • urlToNavigate — адрес ссылки для перехода, параметр используется только в случае needNavigateToUrl = true. Аналогичный параметр есть в buildContentAsLink.

  • useZoom — если true, в области html-фрейма перестаёт действовать встроенное масштабирование браузера и начинает действовать собственное. Аналогичный параметр есть в buildContentAsHtml.

  • initZoomType — настраивает тип начального масштабирования, работает только в случае useZoom = true:

    • если "targetElementClientWidthRatio" — задаётся элемент фрейма через его id в параметре zoomTargetElementId и желаемое соотношение ширины этого элемента к ширине окна браузера в параметре zoomValue (числом);

    • если "multiplier" — увеличивает фрейм в zoomValue раз;

    • если "none" — без начального масштабирования.
      Аналогичный параметр есть в buildContentAsHtml.

  • zoomValue — значение начального масштабирования, используется только при initZoomType="targetElementClientWidthRatio" или initZoomType="multiplier". Аналогичный параметр есть в buildContentAsHtml.

  • zoomTargetElementId — идентификатор элемента, относительно которого рассчитывается начальное масштабирование, используется только в случае initZoomType="targetElementClientWidthRatio". Аналогичный параметр есть в buildContentAsHtml.

Пример взаимодействия

  trait TestWeb extends Default with WebHtml{
    override def onRefresh: Recs = WebHtmlRow(generateHtml())

    var clicked = false
    var formData = None.ns

    /**
      * Получить Html для отображения в web-компоненте
      * @return
      */
    override def getHtmlText: NString = {
      if (selection.isActive) {
        selection.refresh()
        selection.getSelfVar("cData").asNString
      } else {
        generateHtml()
      }
    }

    def generateHtml(): NString = {
      val text =
        s"""
          |<html>
          |  <head>
          |    <link th:href="@{static/bts/kiosk/static/css/bootstrap.min.css}" rel="stylesheet">
          |  </head>
          |  <body>
          |    <ul>
          |      <li><a href="button://1">${if (clicked) "reloadHtml" else "Reloaded"}</a></li>
          |      <li><a href="button://2">GO to link</a></li>
          |      <li><a href="button://3">No action</a></li>
          |      <li><a href="https://ru.wikipedia.org" class="external">ExternalLink</a></li>
          |    </ul>
          |    <form id="form">
          |      <label>Number: </label><input type="number" required pattern=""\\d{5}"" name="Number" gsf:postData="true" id="Number"></input>
          |      <label>Text: </label><input type="text" name="Text" gsf:postData="true" id="Text"></input>
          |      <label>Check: </label><input type="checkbox" name="check" gsf:postData="true" id="Check"></input>
          |      <label>Date: </label><input type="date" name="date" gsf:postData="true" id="Date"></input>
          |      <label>Memo: </label><textarea rows="5" cols="33" name="textarea" gsf:postData="true" id="Textarea"></textarea>
          |     <br><br>
          |     <button type="submit">Submit form</button>
          |     ${if (formData.isNotNullOrEmpty) formData else ""}
          |   </form>
          |  </body>
          |</html>
          |""".stripMargin

      Bts_MtStateApi().processTemplates(text, Map(), {link => Bts_MtStateApi().buildStaticLinkAsRelativePath(relativeResourceRoot, link)})
    }

    override def onBeforeNavigate(url: NString): WebExtResponse = {
      if (url === "button://1") {
        clicked = !clicked
        WebExtResponse.refreshHtml()
      } else if (url === "button://2") {
        WebExtResponse.gotToUrl("https://ru.wikipedia.org")
      } else {
        WebExtResponse.none()
      }
    }

    override def onSubmitForm(data: JObject): WebExtResponse = {
      formData = data.toPrettyNString
      WebExtResponse.refreshHtml()
    }

    override def onSetElementValue(event: SetElementValueEvent): WebExtResponse = {
      event.id match {
        case ns"Number" =>
          println(s"Установлено Number, значение: ${event.value.asNNumber}")
        case ns"Text" =>
          println(s"Установлено Text, значение: ${event.value.asNString}")
        case ns"Check" =>
          println(s"Установлено Check, значение: ${event.value.asNNumber.toBoolean}")
        case ns"Textarea" =>
          println(s"Установлено Memo, значение: ${event.value.asNString}")
        case ns"Date" =>
          println(s"Установлено Date, значение: ${event.value.asNDate}")
      }


      WebExtResponse.none()
    }
  }