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, будет использоваться метод abibuildContentAsLink, это приведёт к переходу по ссылке, указанной в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()
}
}