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()
    }
  }