Перейти к основному содержанию
`Ctrl`+`K`

# 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-формы (например, формы обратной связи, поиска или авторизации), и данные попадали в обработчик на бэкенде.

- `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-логику внутри изолированного контейнера, полностью управляемого через бэкенд-методы. Такой фрейм предоставляет гибкость при сохранении интеграции с общей архитектурой приложения.

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

### Создание

Для создания отображения с 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-фрейма

#### Взаимодействие с бэкендом

Html-фрейм может взаимодействовать с бэкендом через методы:

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

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

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

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

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

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

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

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

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

Разберём на примере полей кейс-класса `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()
        }
      }