diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f83066..de7b26e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# v0.14.0
+* Added the ability to work without creating a WebSocket. Added NoSocket property to AppParams.
+* Added SocketAutoClose property to AppParams.
+* Added the ability to run a timer on the client side. Added StartTimer and StopTimer methods to Session interface.
+* Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties
+* Bug fixing
+
+# v0.13.x
+* Added NewHandler function
+* Bug fixing
+
# v0.13.0
* Added SetHotKey function to Session interface
diff --git a/README.md b/README.md
index b46d5d8..7b34d7a 100644
--- a/README.md
+++ b/README.md
@@ -2465,9 +2465,10 @@ The SizeUnit value of type SizeInFraction can be either integer or fractional.
The "grid-row-gap" and "grid-column-gap" SizeUnit properties (GridRowGap and GridColumnGap constants)
allow you to set the distance between the rows and columns of the container, respectively. The default is 0px.
-### "cell-vertical-align" property
+### "cell-vertical-align" and "cell-vertical-self-align" properties
-The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets the vertical alignment of children within the cell they are occupying. Valid values:
+The "cell-vertical-align" int property (constant CellVerticalAlign) sets the default vertical alignment of children
+within the cell they are occupying. Valid values:
| Value | Constant | Name | Alignment |
|:-----:|--------------|-----------|---------------------|
@@ -2478,9 +2479,13 @@ The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets
The default value is StretchAlign (3)
-### "cell-horizontal-align" property
+The "cell-vertical-self-align" int property (constant CellVerticalAlign) sets the vertical alignment of children
+within the cell they are occupying. This property should be set not for the grid, but for the children.
-The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int sets the horizontal alignment of children within the occupied cell. Valid values:
+### "cell-horizontal-align" and "cell-horizontal-self-align" properties
+
+The "cell-horizontal-align" int property (constant CellHorizontalSelfAlign) sets the horizontal alignment
+of children within the occupied cell. Valid values:
| Value | Constant | Name | Alignment |
|:-----:|--------------|-----------|--------------------|
@@ -2491,6 +2496,9 @@ The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int
The default value is StretchAlign (3)
+The "cell-horizontal-self-align" int property (constant CellVerticalSelfAlign) sets the horizontal alignment of children
+within the cell they are occupying. This property should be set not for the grid, but for the children.
+
## ColumnLayout
ColumnLayout is a container that implements the ViewsContainer interface.
diff --git a/appServer.go b/appServer.go
index 0d20e0e..59f9277 100644
--- a/appServer.go
+++ b/appServer.go
@@ -20,6 +20,9 @@ import (
//go:embed app_socket.js
var socketScripts string
+//go:embed app_post.js
+var httpPostScripts string
+
func debugLog(text string) {
log.Println("\033[34m" + text)
}
@@ -28,11 +31,16 @@ func errorLog(text string) {
log.Println("\033[31m" + text)
}
+type sessionInfo struct {
+ session Session
+ response chan string
+}
+
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
- sessions map[int]Session
+ sessions map[int]sessionInfo
}
func (app *application) getStartPage() string {
@@ -40,14 +48,26 @@ func (app *application) getStartPage() string {
defer freeStringBuilder(buffer)
buffer.WriteString("\n\n")
- getStartPage(buffer, app.params, socketScripts)
+ getStartPage(buffer, app.params)
buffer.WriteString("\n")
return buffer.String()
}
+func (app *application) Params() AppParams {
+ params := app.params
+ if params.NoSocket {
+ params.SocketAutoClose = 0
+ }
+ return params
+}
+
func (app *application) Finish() {
for _, session := range app.sessions {
- session.close()
+ session.session.close()
+ if session.response != nil {
+ close(session.response)
+ session.response = nil
+ }
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -69,7 +89,12 @@ func (app *application) nextSessionID() int {
}
func (app *application) removeSession(id int) {
- delete(app.sessions, id)
+ if info, ok := app.sessions[id]; ok {
+ if info.response != nil {
+ close(info.response)
+ }
+ delete(app.sessions, id)
+ }
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -79,6 +104,11 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
switch req.Method {
+ case "POST":
+ if req.URL.Path == "/" {
+ app.postHandler(w, req)
+ }
+
case "GET":
switch req.URL.Path {
case "/":
@@ -86,10 +116,20 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, app.getStartPage())
case "/ws":
- if bridge := CreateSocketBridge(w, req); bridge != nil {
+ if bridge := createSocketBridge(w, req); bridge != nil {
go app.socketReader(bridge)
}
+ case "/script.js":
+ w.WriteHeader(http.StatusOK)
+ if app.params.NoSocket {
+ io.WriteString(w, httpPostScripts)
+ } else {
+ io.WriteString(w, socketScripts)
+ }
+ io.WriteString(w, "\n")
+ io.WriteString(w, defaultScripts)
+
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
@@ -104,7 +144,87 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
-func (app *application) socketReader(bridge webBridge) {
+func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
+ cookie := http.Cookie{
+ Name: "session",
+ Value: strconv.Itoa(sessionID),
+ HttpOnly: true,
+ }
+ http.SetCookie(w, &cookie)
+}
+
+func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
+
+ if reqBody, err := io.ReadAll(req.Body); err == nil {
+ message := string(reqBody)
+
+ if ProtocolInDebugLog {
+ DebugLog(message)
+ }
+
+ if obj := ParseDataText(message); obj != nil {
+ var session Session = nil
+ var response chan string = nil
+
+ if cookie, err := req.Cookie("session"); err == nil {
+ sessionID, err := strconv.Atoi(cookie.Value)
+ if err != nil {
+ ErrorLog(err.Error())
+ } else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
+ response = info.response
+ session = info.session
+ }
+ }
+
+ command := obj.Tag()
+ startSession := false
+
+ if session == nil || command == "startSession" {
+ events := make(chan DataObject, 1024)
+ bridge := createHttpBridge(req)
+ response = bridge.response
+ answer := ""
+ session, answer = app.startSession(obj, events, bridge, response)
+
+ bridge.writeMessage(answer)
+ session.onStart()
+ if command == "session-resume" {
+ session.onResume()
+ }
+ bridge.sendResponse()
+
+ setSessionIDCookie(w, session.ID())
+ startSession = true
+
+ go sessionEventHandler(session, events, bridge)
+ }
+
+ if !startSession {
+ switch command {
+ case "nop":
+ session.sendResponse()
+
+ case "session-close":
+ session.onFinish()
+ session.App().removeSession(session.ID())
+ return
+
+ default:
+ if !session.handleAnswer(command, obj) {
+ session.addToEventsQueue(obj)
+ }
+ }
+ }
+
+ io.WriteString(w, <-response)
+ for len(response) > 0 {
+ io.WriteString(w, <-response)
+ }
+ }
+ }
+}
+
+func (app *application) socketReader(bridge *wsBridge) {
var session Session
events := make(chan DataObject, 1024)
@@ -116,7 +236,7 @@ func (app *application) socketReader(bridge webBridge) {
}
if ProtocolInDebugLog {
- DebugLog(message)
+ DebugLog("🖥️ -> " + message)
}
if obj := ParseDataText(message); obj != nil {
@@ -124,7 +244,7 @@ func (app *application) socketReader(bridge webBridge) {
switch command {
case "startSession":
answer := ""
- if session, answer = app.startSession(obj, events, bridge); session != nil {
+ if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
@@ -133,22 +253,18 @@ func (app *application) socketReader(bridge webBridge) {
}
case "reconnect":
+ session = nil
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
- if session = app.sessions[sessionID]; session != nil {
+ if info, ok := app.sessions[sessionID]; ok {
+ session = info.session
session.setBridge(events, bridge)
- answer := allocStringBuilder()
- defer freeStringBuilder(answer)
- session.writeInitScript(answer)
- if !bridge.writeMessage(answer.String()) {
- return
- }
- session.onReconnect()
go sessionEventHandler(session, events, bridge)
- return
+ session.onReconnect()
+ } else {
+ DebugLogF("Session #%d not exists", sessionID)
}
- DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
@@ -156,25 +272,31 @@ func (app *application) socketReader(bridge webBridge) {
ErrorLog(`"session" key not found`)
}
- bridge.writeMessage("restartSession();")
-
- case "answer":
- session.handleAnswer(obj)
-
- case "imageLoaded":
- session.imageManager().imageLoaded(obj, session)
-
- case "imageError":
- session.imageManager().imageLoadError(obj, session)
+ if session == nil {
+ /* answer := ""
+ if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
+ if !bridge.writeMessage(answer) {
+ return
+ }
+ session.onStart()
+ go sessionEventHandler(session, events, bridge)
+ bridge.writeMessage("restartSession();")
+ }
+ */
+ bridge.writeMessage("reloadPage();")
+ return
+ }
default:
- events <- obj
+ if !session.handleAnswer(command, obj) {
+ events <- obj
+ }
}
}
}
}
-func sessionEventHandler(session Session, events chan DataObject, bridge webBridge) {
+func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
for {
data := <-events
@@ -194,7 +316,9 @@ func sessionEventHandler(session Session, events chan DataObject, bridge webBrid
}
}
-func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) {
+func (app *application) startSession(params DataObject, events chan DataObject,
+ bridge bridge, response chan string) (Session, string) {
+
if app.createContentFunc == nil {
return nil, ""
}
@@ -205,7 +329,10 @@ func (app *application) startSession(params DataObject, events chan DataObject,
return nil, ""
}
- app.sessions[session.ID()] = session
+ app.sessions[session.ID()] = sessionInfo{
+ session: session,
+ response: response,
+ }
answer := allocStringBuilder()
defer freeStringBuilder(answer)
@@ -229,7 +356,7 @@ var apps = []*application{}
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
- app.sessions = map[int]Session{}
+ app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
diff --git a/appWasm.go b/appWasm.go
index 0b66622..1a07c36 100644
--- a/appWasm.go
+++ b/appWasm.go
@@ -17,7 +17,7 @@ type wasmApp struct {
params AppParams
createContentFunc func(Session) SessionContent
session Session
- bridge webBridge
+ bridge bridge
close chan DataObject
}
@@ -25,6 +25,12 @@ func (app *wasmApp) Finish() {
app.session.close()
}
+func (app *wasmApp) Params() AppParams {
+ params := app.params
+ params.SocketAutoClose = 0
+ return params
+}
+
func debugLog(text string) {
js.Global().Get("console").Call("log", text)
}
@@ -44,17 +50,10 @@ func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
case "session-close":
app.close <- obj
- case "answer":
- app.session.handleAnswer(obj)
-
- case "imageLoaded":
- app.session.imageManager().imageLoaded(obj, app.session)
-
- case "imageError":
- app.session.imageManager().imageLoadError(obj, app.session)
-
default:
- app.session.handleEvent(command, obj)
+ if !app.session.handleAnswer(command, obj) {
+ app.session.handleEvent(command, obj)
+ }
}
}
}
diff --git a/app_post.js b/app_post.js
new file mode 100644
index 0000000..e17e625
--- /dev/null
+++ b/app_post.js
@@ -0,0 +1,25 @@
+
+async function sendMessage(message) {
+ const response = await fetch('/', {
+ method : 'POST',
+ body : message,
+ "Content-Type" : "text/plain",
+ });
+
+ const text = await response.text();
+ if (text != "") {
+ window.eval(text)
+ }
+}
+
+window.onload = function() {
+ sendMessage( sessionInfo() );
+}
+
+window.onfocus = function() {
+ windowFocus = true
+ sendMessage( "session-resume{}" );
+}
+
+function closeSocket() {
+}
diff --git a/app_scripts.js b/app_scripts.js
index b6d6246..0b31b0a 100644
--- a/app_scripts.js
+++ b/app_scripts.js
@@ -1,27 +1,31 @@
-var sessionID = "0"
-var windowFocus = true
+let sessionID = "0"
+let windowFocus = true
window.onresize = function() {
scanElementsSize();
}
-window.onbeforeunload = function(event) {
+window.onbeforeunload = function() {
sendMessage( "session-close{session=" + sessionID +"}" );
}
-window.onblur = function(event) {
+window.onblur = function() {
windowFocus = false
sendMessage( "session-pause{session=" + sessionID +"}" );
}
+function reloadPage() {
+ location.reload();
+}
+
function sessionInfo() {
const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0";
- var message = "startSession{touch=" + touch_screen
+ let message = "startSession{touch=" + touch_screen
const style = window.getComputedStyle(document.body);
if (style) {
- var direction = style.getPropertyValue('direction');
+ const direction = style.getPropertyValue('direction');
if (direction) {
message += ",direction=" + direction
}
@@ -55,9 +59,9 @@ function sessionInfo() {
if (localStorage.length > 0) {
message += ",storage="
lead = "_{"
- for (var i = 0; i < localStorage.length; i++) {
- var key = localStorage.key(i)
- var value = localStorage.getItem(key)
+ for (let i = 0; i < localStorage.length; i++) {
+ let key = localStorage.key(i)
+ let value = localStorage.getItem(key)
key = key.replaceAll(/\\/g, "\\\\")
key = key.replaceAll(/\"/g, "\\\"")
key = key.replaceAll(/\'/g, "\\\'")
@@ -86,42 +90,41 @@ function getIntAttribute(element, tag) {
}
function scanElementsSize() {
- var element = document.getElementById("ruiRootView");
- if (element) {
- let rect = element.getBoundingClientRect();
- let width = getIntAttribute(element, "data-width");
- let height = getIntAttribute(element, "data-height");
+ const rootView = document.getElementById("ruiRootView");
+ if (rootView) {
+ let rect = rootView.getBoundingClientRect();
+ let width = getIntAttribute(rootView, "data-width");
+ let height = getIntAttribute(rootView, "data-height");
if (rect.width > 0 && rect.height > 0 && (width != rect.width || height != rect.height)) {
- element.setAttribute("data-width", rect.width);
- element.setAttribute("data-height", rect.height);
+ rootView.setAttribute("data-width", rect.width);
+ rootView.setAttribute("data-height", rect.height);
sendMessage("root-size{session=" + sessionID + ",width=" + rect.width + ",height=" + rect.height +"}");
}
}
- var views = document.getElementsByClassName("ruiView");
+ const views = document.getElementsByClassName("ruiView");
if (views) {
- var message = "resize{session=" + sessionID + ",views=["
- var count = 0
- for (var i = 0; i < views.length; i++) {
- let element = views[i];
- let noresize = element.getAttribute("data-noresize");
+ let message = "resize{session=" + sessionID + ",views=["
+ let count = 0
+ for (const view of views) {
+ let noresize = view.getAttribute("data-noresize");
if (!noresize) {
- let rect = element.getBoundingClientRect();
- let top = getIntAttribute(element, "data-top");
- let left = getIntAttribute(element, "data-left");
- let width = getIntAttribute(element, "data-width");
- let height = getIntAttribute(element, "data-height");
+ let rect = view.getBoundingClientRect();
+ let top = getIntAttribute(view, "data-top");
+ let left = getIntAttribute(view, "data-left");
+ let width = getIntAttribute(view, "data-width");
+ let height = getIntAttribute(view, "data-height");
if (rect.width > 0 && rect.height > 0 &&
(width != rect.width || height != rect.height || left != rect.left || top != rect.top)) {
- element.setAttribute("data-top", rect.top);
- element.setAttribute("data-left", rect.left);
- element.setAttribute("data-width", rect.width);
- element.setAttribute("data-height", rect.height);
+ view.setAttribute("data-top", rect.top);
+ view.setAttribute("data-left", rect.left);
+ view.setAttribute("data-width", rect.width);
+ view.setAttribute("data-height", rect.height);
if (count > 0) {
message += ",";
}
- message += "view{id=" + element.id + ",x=" + rect.left + ",y=" + rect.top + ",width=" + rect.width + ",height=" + rect.height +
- ",scroll-x=" + element.scrollLeft + ",scroll-y=" + element.scrollTop + ",scroll-width=" + element.scrollWidth + ",scroll-height=" + element.scrollHeight + "}";
+ message += "view{id=" + view.id + ",x=" + rect.left + ",y=" + rect.top + ",width=" + rect.width + ",height=" + rect.height +
+ ",scroll-x=" + view.scrollLeft + ",scroll-y=" + view.scrollTop + ",scroll-width=" + view.scrollWidth + ",scroll-height=" + view.scrollHeight + "}";
count += 1;
}
}
@@ -139,11 +142,11 @@ function scrollEvent(element, event) {
}
function updateCSSRule(selector, ruleText) {
- var styleSheet = document.styleSheets[0];
- var rules = styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules
+ const styleSheet = document.styleSheets[0];
+ const rules = styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules
selector = "." + selector
- for (var i = 0; i < rules.length; i++) {
- var rule = rules[i]
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i]
if (!rule.selectorText) {
continue;
}
@@ -165,7 +168,7 @@ function updateCSSRule(selector, ruleText) {
}
function updateCSSStyle(elementId, style) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.style = style;
scanElementsSize();
@@ -173,7 +176,7 @@ function updateCSSStyle(elementId, style) {
}
function updateCSSProperty(elementId, property, value) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.style[property] = value;
scanElementsSize();
@@ -181,7 +184,7 @@ function updateCSSProperty(elementId, property, value) {
}
function updateProperty(elementId, property, value) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.setAttribute(property, value);
scanElementsSize();
@@ -189,7 +192,7 @@ function updateProperty(elementId, property, value) {
}
function removeProperty(elementId, property) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element && element.hasAttribute(property)) {
element.removeAttribute(property);
scanElementsSize();
@@ -197,7 +200,7 @@ function removeProperty(elementId, property) {
}
function updateInnerHTML(elementId, content) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.innerHTML = content;
scanElementsSize();
@@ -205,7 +208,7 @@ function updateInnerHTML(elementId, content) {
}
function appendToInnerHTML(elementId, content) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.innerHTML += content;
scanElementsSize();
@@ -213,7 +216,7 @@ function appendToInnerHTML(elementId, content) {
}
function setDisabled(elementId, disabled) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
if ('disabled' in element) {
element.disabled = disabled
@@ -244,21 +247,21 @@ function enterOrSpaceKeyClickEvent(event) {
}
function activateTab(layoutId, tabNumber) {
- var element = document.getElementById(layoutId);
+ const element = document.getElementById(layoutId);
if (element) {
- var currentNumber = element.getAttribute("data-current");
+ const currentNumber = element.getAttribute("data-current");
if (currentNumber != tabNumber) {
function setTab(number, styleProperty, display) {
- var tab = document.getElementById(layoutId + '-' + number);
+ const tab = document.getElementById(layoutId + '-' + number);
if (tab) {
tab.className = element.getAttribute(styleProperty);
- var page = document.getElementById(tab.getAttribute("data-view"));
+ const page = document.getElementById(tab.getAttribute("data-view"));
if (page) {
page.style.display = display;
}
return
}
- var page = document.getElementById(layoutId + "-page" + number);
+ const page = document.getElementById(layoutId + "-page" + number);
if (page) {
page.style.display = display;
}
@@ -302,11 +305,10 @@ function tabCloseKeyClickEvent(layoutId, tabNumber, event) {
}
}
-
function keyEvent(element, event, tag) {
event.stopPropagation();
- var message = tag + "{session=" + sessionID + ",id=" + element.id;
+ let message = tag + "{session=" + sessionID + ",id=" + element.id;
if (event.timeStamp) {
message += ",timeStamp=" + event.timeStamp;
}
@@ -356,7 +358,7 @@ function keyUpEvent(element, event) {
}
function mouseEventData(element, event) {
- var message = ""
+ let message = ""
if (event.timeStamp) {
message += ",timeStamp=" + event.timeStamp;
@@ -368,8 +370,8 @@ function mouseEventData(element, event) {
message += ",buttons=" + event.buttons;
}
if (event.clientX) {
- var x = event.clientX;
- var el = element;
+ let x = event.clientX;
+ let el = element;
if (el.parentElement) {
x += el.parentElement.scrollLeft;
}
@@ -381,8 +383,8 @@ function mouseEventData(element, event) {
message += ",x=" + x + ",clientX=" + event.clientX;
}
if (event.clientY) {
- var y = event.clientY;
- var el = element;
+ let y = event.clientY;
+ let el = element;
if (el.parentElement) {
y += el.parentElement.scrollTop;
}
@@ -418,7 +420,7 @@ function mouseEvent(element, event, tag) {
event.stopPropagation();
//event.preventDefault()
- var message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event) + "}";
+ const message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event) + "}";
sendMessage(message);
}
@@ -461,7 +463,7 @@ function contextMenuEvent(element, event) {
function pointerEvent(element, event, tag) {
event.stopPropagation();
- var message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event);
+ let message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event);
if (event.pointerId) {
message += ",pointerId=" + event.pointerId;
@@ -525,23 +527,23 @@ function pointerOutEvent(element, event) {
function touchEvent(element, event, tag) {
event.stopPropagation();
- var message = tag + "{session=" + sessionID + ",id=" + element.id;
+ let message = tag + "{session=" + sessionID + ",id=" + element.id;
if (event.timeStamp) {
message += ",timeStamp=" + event.timeStamp;
}
if (event.touches && event.touches.length > 0) {
message += ",touches=["
- for (var i = 0; i < event.touches.length; i++) {
- var touch = event.touches.item(i)
+ for (let i = 0; i < event.touches.length; i++) {
+ const touch = event.touches.item(i)
if (touch) {
if (i > 0) {
message += ","
}
message += "touch{identifier=" + touch.identifier;
- var x = touch.clientX;
- var y = touch.clientY;
- var el = element;
+ let x = touch.clientX;
+ let y = touch.clientY;
+ let el = element;
if (el.parentElement) {
x += el.parentElement.scrollLeft;
y += el.parentElement.scrollTop;
@@ -594,12 +596,12 @@ function touchCancelEvent(element, event) {
function dropDownListEvent(element, event) {
event.stopPropagation();
- var message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + element.selectedIndex.toString() + "}"
+ const message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + element.selectedIndex.toString() + "}"
sendMessage(message);
}
function selectDropDownListItem(elementId, number) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.selectedIndex = number;
scanElementsSize();
@@ -613,33 +615,33 @@ function listItemClickEvent(element, event) {
return
}
- var selected = false;
+ let selected = false;
if (element.classList) {
const focusStyle = getListFocusedItemStyle(element);
const blurStyle = getListSelectedItemStyle(element);
selected = (element.classList.contains(focusStyle) || element.classList.contains(blurStyle));
}
- var list = element.parentNode.parentNode
+ const list = element.parentNode.parentNode
if (list) {
if (!selected) {
selectListItem(list, element, true)
}
- var message = "itemClick{session=" + sessionID + ",id=" + list.id + "}"
+ const message = "itemClick{session=" + sessionID + ",id=" + list.id + "}"
sendMessage(message);
}
}
function getListItemNumber(itemId) {
- var pos = itemId.indexOf("-")
+ const pos = itemId.indexOf("-")
if (pos >= 0) {
return parseInt(itemId.substring(pos+1))
}
}
function getStyleAttribute(element, attr, defValue) {
- var result = element.getAttribute(attr);
+ const result = element.getAttribute(attr);
if (result) {
return result;
}
@@ -655,13 +657,13 @@ function getListSelectedItemStyle(element) {
}
function selectListItem(element, item, needSendMessage) {
- var currentId = element.getAttribute("data-current");
- var message;
+ const currentId = element.getAttribute("data-current");
+ let message;
const focusStyle = getListFocusedItemStyle(element);
const blurStyle = getListSelectedItemStyle(element);
if (currentId) {
- var current = document.getElementById(currentId);
+ const current = document.getElementById(currentId);
if (current) {
if (current.classList) {
current.classList.remove(focusStyle, blurStyle);
@@ -685,7 +687,7 @@ function selectListItem(element, item, needSendMessage) {
element.setAttribute("data-current", item.id);
if (sendMessage) {
- var number = getListItemNumber(item.id)
+ const number = getListItemNumber(item.id)
if (number != undefined) {
message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + number + "}";
}
@@ -697,22 +699,22 @@ function selectListItem(element, item, needSendMessage) {
item.scrollIntoView({block: "nearest", inline: "nearest"});
}
/*
- var left = item.offsetLeft - element.offsetLeft;
+ let left = item.offsetLeft - element.offsetLeft;
if (left < element.scrollLeft) {
element.scrollLeft = left;
}
- var top = item.offsetTop - element.offsetTop;
+ let top = item.offsetTop - element.offsetTop;
if (top < element.scrollTop) {
element.scrollTop = top;
}
- var right = left + item.offsetWidth;
+ let right = left + item.offsetWidth;
if (right > element.scrollLeft + element.clientWidth) {
element.scrollLeft = right - element.clientWidth;
}
- var bottom = top + item.offsetHeight
+ let bottom = top + item.offsetHeight
if (bottom > element.scrollTop + element.clientHeight) {
element.scrollTop = bottom - element.clientHeight;
}*/
@@ -726,17 +728,15 @@ function selectListItem(element, item, needSendMessage) {
function findRightListItem(list, x, y) {
list = list.childNodes[0];
- var result;
- var count = list.childNodes.length;
- for (var i = 0; i < count; i++) {
- var item = list.childNodes[i];
+ let result;
+ for (const item of list.childNodes) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetLeft >= x) {
if (result) {
- var result_dy = Math.abs(result.offsetTop - y);
- var item_dy = Math.abs(item.offsetTop - y);
+ const result_dy = Math.abs(result.offsetTop - y);
+ const item_dy = Math.abs(item.offsetTop - y);
if (item_dy < result_dy || (item_dy == result_dy && (item.offsetLeft - x) < (result.offsetLeft - x))) {
result = item;
}
@@ -750,17 +750,15 @@ function findRightListItem(list, x, y) {
function findLeftListItem(list, x, y) {
list = list.childNodes[0];
- var result;
- var count = list.childNodes.length;
- for (var i = 0; i < count; i++) {
- var item = list.childNodes[i];
+ let result;
+ for (const item of list.childNodes) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetLeft < x) {
if (result) {
- var result_dy = Math.abs(result.offsetTop - y);
- var item_dy = Math.abs(item.offsetTop - y);
+ const result_dy = Math.abs(result.offsetTop - y);
+ const item_dy = Math.abs(item.offsetTop - y);
if (item_dy < result_dy || (item_dy == result_dy && (x - item.offsetLeft) < (x - result.offsetLeft))) {
result = item;
}
@@ -774,17 +772,15 @@ function findLeftListItem(list, x, y) {
function findTopListItem(list, x, y) {
list = list.childNodes[0];
- var result;
- var count = list.childNodes.length;
- for (var i = 0; i < count; i++) {
- var item = list.childNodes[i];
+ let result;
+ for (const item of list.childNodes) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetTop < y) {
if (result) {
- var result_dx = Math.abs(result.offsetLeft - x);
- var item_dx = Math.abs(item.offsetLeft - x);
+ const result_dx = Math.abs(result.offsetLeft - x);
+ const item_dx = Math.abs(item.offsetLeft - x);
if (item_dx < result_dx || (item_dx == result_dx && (y - item.offsetTop) < (y - result.offsetTop))) {
result = item;
}
@@ -798,17 +794,15 @@ function findTopListItem(list, x, y) {
function findBottomListItem(list, x, y) {
list = list.childNodes[0];
- var result;
- var count = list.childNodes.length;
- for (var i = 0; i < count; i++) {
- var item = list.childNodes[i];
+ let result;
+ for (const item of list.childNodes) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetTop >= y) {
if (result) {
- var result_dx = Math.abs(result.offsetLeft - x);
- var item_dx = Math.abs(item.offsetLeft - x);
+ const result_dx = Math.abs(result.offsetLeft - x);
+ const item_dx = Math.abs(item.offsetLeft - x);
if (item_dx < result_dx || (item_dx == result_dx && (item.offsetTop - y) < (result.offsetTop - y))) {
result = item;
}
@@ -844,18 +838,18 @@ function getKey(event) {
function listViewKeyDownEvent(element, event) {
const key = getKey(event);
if (key) {
- var currentId = element.getAttribute("data-current");
- var current
+ const currentId = element.getAttribute("data-current");
+ let current
if (currentId) {
current = document.getElementById(currentId);
//number = getListItemNumber(currentId);
}
if (current) {
- var item
+ let item
switch (key) {
case " ":
case "Enter":
- var message = "itemClick{session=" + sessionID + ",id=" + element.id + "}";
+ const message = "itemClick{session=" + sessionID + ",id=" + element.id + "}";
sendMessage(message);
break;
@@ -909,10 +903,8 @@ function listViewKeyDownEvent(element, event) {
case "End":
case "PageUp":
case "PageDown":
- var list = element.childNodes[0];
- var count = list.childNodes.length;
- for (var i = 0; i < count; i++) {
- var item = list.childNodes[i];
+ const list = element.childNodes[0];
+ for (const item of list.childNodes) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
@@ -932,9 +924,9 @@ function listViewKeyDownEvent(element, event) {
}
function listViewFocusEvent(element, event) {
- var currentId = element.getAttribute("data-current");
+ const currentId = element.getAttribute("data-current");
if (currentId) {
- var current = document.getElementById(currentId);
+ const current = document.getElementById(currentId);
if (current) {
if (current.classList) {
current.classList.remove(getListSelectedItemStyle(element));
@@ -945,9 +937,9 @@ function listViewFocusEvent(element, event) {
}
function listViewBlurEvent(element, event) {
- var currentId = element.getAttribute("data-current");
+ const currentId = element.getAttribute("data-current");
if (currentId) {
- var current = document.getElementById(currentId);
+ const current = document.getElementById(currentId);
if (current) {
if (current.classList) {
current.classList.remove(getListFocusedItemStyle(element));
@@ -958,30 +950,30 @@ function listViewBlurEvent(element, event) {
}
function selectRadioButton(radioButtonId) {
- var element = document.getElementById(radioButtonId);
+ const element = document.getElementById(radioButtonId);
if (element) {
- var list = element.parentNode
+ const list = element.parentNode
if (list) {
- var current = list.getAttribute("data-current");
+ const current = list.getAttribute("data-current");
if (current) {
if (current === radioButtonId) {
return
}
- var mark = document.getElementById(current + "mark");
+ const mark = document.getElementById(current + "mark");
if (mark) {
//mark.hidden = true
mark.style.visibility = "hidden"
}
}
- var mark = document.getElementById(radioButtonId + "mark");
+ const mark = document.getElementById(radioButtonId + "mark");
if (mark) {
//mark.hidden = false
mark.style.visibility = "visible"
}
list.setAttribute("data-current", radioButtonId);
- var message = "radioButtonSelected{session=" + sessionID + ",id=" + list.id + ",radioButton=" + radioButtonId + "}"
+ const message = "radioButtonSelected{session=" + sessionID + ",id=" + list.id + ",radioButton=" + radioButtonId + "}"
sendMessage(message);
scanElementsSize();
}
@@ -989,11 +981,11 @@ function selectRadioButton(radioButtonId) {
}
function unselectRadioButtons(radioButtonsId) {
- var list = document.getElementById(radioButtonsId);
+ const list = document.getElementById(radioButtonsId);
if (list) {
- var current = list.getAttribute("data-current");
+ const current = list.getAttribute("data-current");
if (current) {
- var mark = document.getElementById(current + "mark");
+ const mark = document.getElementById(current + "mark");
if (mark) {
mark.style.visibility = "hidden"
}
@@ -1001,7 +993,7 @@ function unselectRadioButtons(radioButtonsId) {
list.removeAttribute("data-current");
}
- var message = "radioButtonUnselected{session=" + sessionID + ",id=" + list.id + "}"
+ const message = "radioButtonUnselected{session=" + sessionID + ",id=" + list.id + "}"
sendMessage(message);
scanElementsSize();
}
@@ -1020,15 +1012,15 @@ function radioButtonKeyClickEvent(element, event) {
}
function editViewInputEvent(element) {
- var text = element.value
+ let text = element.value
text = text.replaceAll(/\\/g, "\\\\")
text = text.replaceAll(/\"/g, "\\\"")
- var message = "textChanged{session=" + sessionID + ",id=" + element.id + ",text=\"" + text + "\"}"
+ const message = "textChanged{session=" + sessionID + ",id=" + element.id + ",text=\"" + text + "\"}"
sendMessage(message);
}
function setInputValue(elementId, text) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.value = text;
scanElementsSize();
@@ -1036,10 +1028,10 @@ function setInputValue(elementId, text) {
}
function fileSelectedEvent(element) {
- var files = element.files;
+ const files = element.files;
if (files) {
- var message = "fileSelected{session=" + sessionID + ",id=" + element.id + ",files=[";
- for(var i = 0; i < files.length; i++) {
+ let message = "fileSelected{session=" + sessionID + ",id=" + element.id + ",files=[";
+ for(let i = 0; i < files.length; i++) {
if (i > 0) {
message += ",";
}
@@ -1053,9 +1045,9 @@ function fileSelectedEvent(element) {
}
function loadSelectedFile(elementId, index) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
- var files = element.files;
+ const files = element.files;
if (files && index >= 0 && index < files.length) {
const reader = new FileReader();
reader.onload = function() {
@@ -1080,15 +1072,15 @@ function loadSelectedFile(elementId, index) {
}
function startResize(element, mx, my, event) {
- var view = element.parentNode;
+ const view = element.parentNode;
if (!view) {
return;
}
- var startX = event.clientX;
- var startY = event.clientY;
- var startWidth = view.offsetWidth
- var startHeight = view.offsetHeight
+ let startX = event.clientX;
+ let startY = event.clientY;
+ let startWidth = view.offsetWidth
+ let startHeight = view.offsetHeight
document.addEventListener("mousemove", moveHandler, true);
document.addEventListener("mouseup", upHandler, true);
@@ -1098,7 +1090,7 @@ function startResize(element, mx, my, event) {
function moveHandler(e) {
if (mx != 0) {
- var width = startWidth + (e.clientX - startX) * mx;
+ let width = startWidth + (e.clientX - startX) * mx;
if (width <= 0) {
width = 1;
}
@@ -1107,7 +1099,7 @@ function startResize(element, mx, my, event) {
}
if (my != 0) {
- var height = startHeight + (e.clientY - startY) * my;
+ let height = startHeight + (e.clientY - startY) * my;
if (height <= 0) {
height = 1;
}
@@ -1128,7 +1120,7 @@ function startResize(element, mx, my, event) {
}
function transitionStartEvent(element, event) {
- var message = "transition-start-event{session=" + sessionID + ",id=" + element.id;
+ let message = "transition-start-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
@@ -1137,7 +1129,7 @@ function transitionStartEvent(element, event) {
}
function transitionRunEvent(element, event) {
- var message = "transition-run-event{session=" + sessionID + ",id=" + element.id;
+ let message = "transition-run-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
@@ -1146,7 +1138,7 @@ function transitionRunEvent(element, event) {
}
function transitionEndEvent(element, event) {
- var message = "transition-end-event{session=" + sessionID + ",id=" + element.id;
+ let message = "transition-end-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
@@ -1155,7 +1147,7 @@ function transitionEndEvent(element, event) {
}
function transitionCancelEvent(element, event) {
- var message = "transition-cancel-event{session=" + sessionID + ",id=" + element.id;
+ let message = "transition-cancel-event{session=" + sessionID + ",id=" + element.id;
if (event.propertyName) {
message += ",property=" + event.propertyName
}
@@ -1164,7 +1156,7 @@ function transitionCancelEvent(element, event) {
}
function animationStartEvent(element, event) {
- var message = "animation-start-event{session=" + sessionID + ",id=" + element.id;
+ let message = "animation-start-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
@@ -1173,7 +1165,7 @@ function animationStartEvent(element, event) {
}
function animationEndEvent(element, event) {
- var message = "animation-end-event{session=" + sessionID + ",id=" + element.id;
+ let message = "animation-end-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
@@ -1182,7 +1174,7 @@ function animationEndEvent(element, event) {
}
function animationCancelEvent(element, event) {
- var message = "animation-cancel-event{session=" + sessionID + ",id=" + element.id;
+ let message = "animation-cancel-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
@@ -1191,7 +1183,7 @@ function animationCancelEvent(element, event) {
}
function animationIterationEvent(element, event) {
- var message = "animation-iteration-event{session=" + sessionID + ",id=" + element.id;
+ let message = "animation-iteration-event{session=" + sessionID + ",id=" + element.id;
if (event.animationName) {
message += ",name=" + event.animationName
}
@@ -1204,10 +1196,10 @@ function stackTransitionEndEvent(stackId, propertyName, event) {
event.stopPropagation();
}
-var images = new Map();
+const images = new Map();
function loadImage(url) {
- var img = images.get(url);
+ let img = images.get(url);
if (img != undefined) {
return
}
@@ -1215,7 +1207,7 @@ function loadImage(url) {
img = new Image();
img.addEventListener("load", function() {
images.set(url, img)
- var message = "imageLoaded{session=" + sessionID + ",url=\"" + url + "\"";
+ let message = "imageLoaded{session=" + sessionID + ",url=\"" + url + "\"";
if (img.naturalWidth) {
message += ",width=" + img.naturalWidth
}
@@ -1226,9 +1218,9 @@ function loadImage(url) {
}, false);
img.addEventListener("error", function(event) {
- var message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
+ let message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
if (event && event.message) {
- var text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
+ const text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
message += ",message=\"" + text + "\"";
}
sendMessage(message + "}")
@@ -1238,7 +1230,7 @@ function loadImage(url) {
}
function loadInlineImage(url, content) {
- var img = images.get(url);
+ let img = images.get(url);
if (img != undefined) {
return
}
@@ -1246,7 +1238,7 @@ function loadInlineImage(url, content) {
img = new Image();
img.addEventListener("load", function() {
images.set(url, img)
- var message = "imageLoaded{session=" + sessionID + ",url=\"" + url + "\"";
+ let message = "imageLoaded{session=" + sessionID + ",url=\"" + url + "\"";
if (img.naturalWidth) {
message += ",width=" + img.naturalWidth
}
@@ -1257,9 +1249,9 @@ function loadInlineImage(url, content) {
}, false);
img.addEventListener("error", function(event) {
- var message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
+ let message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
if (event && event.message) {
- var text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
+ const text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
message += ",message=\"" + text + "\"";
}
sendMessage(message + "}")
@@ -1269,41 +1261,41 @@ function loadInlineImage(url, content) {
}
function clickClosePopup(element, e) {
- var popupId = element.getAttribute("data-popupId");
+ const popupId = element.getAttribute("data-popupId");
sendMessage("clickClosePopup{session=" + sessionID + ",id=" + popupId + "}")
e.stopPropagation();
}
function scrollTo(elementId, x, y) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.scrollTo(x, y);
}
}
function scrollToStart(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.scrollTo(0, 0);
}
}
function scrollToEnd(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.scrollTo(0, element.scrollHeight - element.offsetHeight);
}
}
function focus(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.focus();
}
}
function blur(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.blur();
}
@@ -1321,7 +1313,7 @@ function playerEvent(element, tag) {
}
function playerTimeUpdatedEvent(element) {
- var message = "time-update-event{session=" + sessionID + ",id=" + element.id + ",value=";
+ let message = "time-update-event{session=" + sessionID + ",id=" + element.id + ",value=";
if (element.currentTime) {
message += element.currentTime;
} else {
@@ -1331,7 +1323,7 @@ function playerTimeUpdatedEvent(element) {
}
function playerDurationChangedEvent(element) {
- var message = "duration-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
+ let message = "duration-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
if (element.duration) {
message += element.duration;
} else {
@@ -1341,7 +1333,7 @@ function playerDurationChangedEvent(element) {
}
function playerVolumeChangedEvent(element) {
- var message = "volume-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
+ let message = "volume-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
if (element.volume && !element.muted) {
message += element.volume;
} else {
@@ -1351,7 +1343,7 @@ function playerVolumeChangedEvent(element) {
}
function playerRateChangedEvent(element) {
- var message = "rate-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
+ let message = "rate-changed-event{session=" + sessionID + ",id=" + element.id + ",value=";
if (element.playbackRate) {
message += element.playbackRate;
} else {
@@ -1361,7 +1353,7 @@ function playerRateChangedEvent(element) {
}
function playerErrorEvent(element) {
- var message = "player-error-event{session=" + sessionID + ",id=" + element.id;
+ let message = "player-error-event{session=" + sessionID + ",id=" + element.id;
if (element.error) {
if (element.error.code) {
message += ",code=" + element.error.code;
@@ -1374,49 +1366,49 @@ function playerErrorEvent(element) {
}
function setMediaMuted(elementId, value) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.muted = value
}
}
function mediaPlay(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element && element.play) {
element.play()
}
}
function mediaPause(elementId) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element && element.pause) {
element.pause()
}
}
function mediaSetSetCurrentTime(elementId, time) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.currentTime = time
}
}
function mediaSetPlaybackRate(elementId, time) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.playbackRate = time
}
}
function mediaSetVolume(elementId, volume) {
- var element = document.getElementById(elementId);
+ const element = document.getElementById(elementId);
if (element) {
element.volume = volume
}
}
function startDownload(url, filename) {
- var element = document.getElementById("ruiDownloader");
+ const element = document.getElementById("ruiDownloader");
if (element) {
element.href = url;
element.setAttribute("download", filename);
@@ -1429,16 +1421,16 @@ function setTitle(title) {
}
function setTitleColor(color) {
- var metas = document.getElementsByTagName('meta');
+ const metas = document.getElementsByTagName('meta');
if (metas) {
- var item = metas.namedItem('theme-color');
+ const item = metas.namedItem('theme-color');
if (item) {
item.setAttribute('content', color)
return
}
}
- var meta = document.createElement('meta');
+ const meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
meta.setAttribute('content', color);
document.getElementsByTagName('head')[0].appendChild(meta);
@@ -1461,9 +1453,9 @@ function getTableSelectedItemStyle(element) {
}
function tableViewFocusEvent(element, event) {
- var currentId = element.getAttribute("data-current");
+ const currentId = element.getAttribute("data-current");
if (currentId) {
- var current = document.getElementById(currentId);
+ const current = document.getElementById(currentId);
if (current) {
if (current.classList) {
current.classList.remove(getTableSelectedItemStyle(element));
@@ -1474,9 +1466,9 @@ function tableViewFocusEvent(element, event) {
}
function tableViewBlurEvent(element, event) {
- var currentId = element.getAttribute("data-current");
+ const currentId = element.getAttribute("data-current");
if (currentId) {
- var current = document.getElementById(currentId);
+ const current = document.getElementById(currentId);
if (current && current.classList) {
current.classList.remove(getTableFocusedItemStyle(element));
current.classList.add(getTableSelectedItemStyle(element));
@@ -1485,7 +1477,7 @@ function tableViewBlurEvent(element, event) {
}
function setTableCellCursorByID(tableID, row, column) {
- var table = document.getElementById(tableID);
+ const table = document.getElementById(tableID);
if (table) {
if (!setTableCellCursor(table, row, column)) {
const focusStyle = getTableFocusedItemStyle(table);
@@ -1504,7 +1496,7 @@ function setTableCellCursorByID(tableID, row, column) {
function setTableCellCursor(element, row, column) {
const cellID = element.id + "-" + row + "-" + column;
- var cell = document.getElementById(cellID);
+ const cell = document.getElementById(cellID);
if (!cell || cell.getAttribute("data-disabled")) {
return false;
}
@@ -1551,7 +1543,7 @@ function moveTableCellCursor(element, row, column, dr, dc) {
if (setTableCellCursor(element, row, column)) {
return;
} else if (dr == 0) {
- var r2 = row - 1;
+ let r2 = row - 1;
while (r2 >= 0) {
if (setTableCellCursor(element, r2, column)) {
return;
@@ -1559,7 +1551,7 @@ function moveTableCellCursor(element, row, column, dr, dc) {
r2--;
}
} else if (dc == 0) {
- var c2 = column - 1;
+ let c2 = column - 1;
while (c2 >= 0) {
if (setTableCellCursor(element, row, c2)) {
return;
@@ -1596,9 +1588,9 @@ function tableViewCellKeyDownEvent(element, event) {
if (rows && columns) {
const rowCount = parseInt(rows);
const columnCount = parseInt(rows);
- row = 0;
+ let row = 0;
while (row < rowCount) {
- column = 0;
+ let column = 0;
while (columns < columnCount) {
if (setTableCellCursor(element, row, column)) {
return;
@@ -1646,13 +1638,13 @@ function tableViewCellKeyDownEvent(element, event) {
break;
case "End":
- /*var newRow = rowCount-1;
- while (newRow > row) {
+ /*
+ for (let newRow = rowCount-1; newRow > row; newRow--) {
if (setTableRowCursor(element, newRow)) {
break;
}
- newRow--;
- }*/
+ }
+ */
// TODO
break;
@@ -1676,7 +1668,7 @@ function tableViewCellKeyDownEvent(element, event) {
}
function setTableRowCursorByID(tableID, row) {
- var table = document.getElementById(tableID);
+ const table = document.getElementById(tableID);
if (table) {
if (!setTableRowCursor(table, row)) {
const focusStyle = getTableFocusedItemStyle(table);
@@ -1695,7 +1687,7 @@ function setTableRowCursorByID(tableID, row) {
function setTableRowCursor(element, row) {
const tableRowID = element.id + "-" + row;
- var tableRow = document.getElementById(tableRowID);
+ const tableRow = document.getElementById(tableRowID);
if (!tableRow || tableRow.getAttribute("data-disabled")) {
return false;
}
@@ -1760,26 +1752,22 @@ function tableViewRowKeyDownEvent(element, event) {
moveTableRowCursor(element, row, -1)
break;
- case "Home":
- var newRow = 0;
- while (newRow < row) {
+ case "Home":
+ for (let newRow = 0; newRow < row; newRow++) {
if (setTableRowCursor(element, newRow)) {
break;
}
- newRow++;
}
break;
-
- case "End":
- var newRow = rowCount-1;
- while (newRow > row) {
+
+ case "End":
+ for (let newRow = rowCount-1; newRow > row; newRow--) {
if (setTableRowCursor(element, newRow)) {
break;
}
- newRow--;
}
break;
-
+
case "PageUp":
// TODO
break;
@@ -1809,7 +1797,7 @@ function tableViewRowKeyDownEvent(element, event) {
const rows = element.getAttribute("data-rows");
if (rows) {
const rowCount = parseInt(rows);
- row = 0;
+ let row = 0;
while (row < rowCount) {
if (setTableRowCursor(element, row)) {
break;
@@ -1879,7 +1867,7 @@ function tableRowClickEvent(element, event) {
}
function imageLoaded(element, event) {
- var message = "imageViewLoaded{session=" + sessionID + ",id=" + element.id +
+ const message = "imageViewLoaded{session=" + sessionID + ",id=" + element.id +
",natural-width=" + element.naturalWidth +
",natural-height=" + element.naturalHeight +
",current-src=\"" + element.currentSrc + "\"}";
@@ -1888,16 +1876,38 @@ function imageLoaded(element, event) {
}
function imageError(element, event) {
- var message = "imageViewError{session=" + sessionID + ",id=" + element.id + "}";
+ const message = "imageViewError{session=" + sessionID + ",id=" + element.id + "}";
sendMessage(message);
}
+let timers = new Map();
+
+function startTimer(ms, timerID) {
+ let data = {
+ id: setInterval(timerFunc, ms, timerID),
+ ms: ms,
+ };
+ timers.set(timerID, data);
+}
+
+function timerFunc(timerID) {
+ sendMessage("timer{session=" + sessionID + ",timerID=" + timerID + "}");
+}
+
+function stopTimer(timerID) {
+ let timer = timers.get(timerID);
+ if (timer) {
+ clearInterval(timer.id);
+ timers.delete(timerID);
+ }
+}
+
function canvasTextMetrics(answerID, elementId, font, text) {
- var w = 0;
- var ascent = 0;
- var descent = 0;
- var left = 0;
- var right = 0;
+ let w = 0;
+ let ascent = 0;
+ let descent = 0;
+ let left = 0;
+ let right = 0;
const canvas = document.getElementById(elementId);
if (canvas) {
@@ -1909,7 +1919,7 @@ function canvasTextMetrics(answerID, elementId, font, text) {
ctx.font = font;
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'start';
- var metrics = ctx.measureText(text)
+ const metrics = ctx.measureText(text)
w = metrics.width;
ascent = metrics.actualBoundingBoxAscent;
descent = metrics.actualBoundingBoxDescent;
@@ -1933,10 +1943,28 @@ function getPropertyValue(answerID, elementId, name) {
sendMessage('answer{answerID=' + answerID + ', value=""}')
}
+function setStyles(styles) {
+ document.querySelector('style').textContent = styles
+}
+
function appendStyles(styles) {
document.querySelector('style').textContent += styles
}
+function appendAnimationCSS(css) {
+ let styles = document.getElementById('ruiAnimations');
+ if (styles) {
+ styles.textContent += css;
+ }
+}
+
+function setAnimationCSS(css) {
+ let styles = document.getElementById('ruiAnimations');
+ if (styles) {
+ styles.textContent = css;
+ }
+}
+
function getCanvasContext(elementId) {
const canvas = document.getElementById(elementId)
if (canvas) {
@@ -1977,16 +2005,16 @@ function showTooltip(element, tooltip) {
layer.style.left = "0px";
layer.style.right = "0px";
- var tooltipBox = document.getElementById("ruiTooltipText");
+ const tooltipBox = document.getElementById("ruiTooltipText");
if (tooltipBox) {
tooltipBox.innerHTML = tooltip;
}
- var left = element.offsetLeft;
- var top = element.offsetTop;
- var width = element.offsetWidth;
- var height = element.offsetHeight;
- var parent = element.offsetParent;
+ let left = element.offsetLeft;
+ let top = element.offsetTop;
+ let width = element.offsetWidth;
+ let height = element.offsetHeight;
+ let parent = element.offsetParent;
while (parent) {
left += parent.offsetLeft;
@@ -2022,7 +2050,7 @@ function showTooltip(element, tooltip) {
}
const bottomOff = height - (top + element.offsetHeight);
- var arrow = document.getElementById("ruiTooltipTopArrow");
+ let arrow = document.getElementById("ruiTooltipTopArrow");
if (bottomOff < arrow.offsetHeight + tooltipBox.offsetHeight) {
if (arrow) {
diff --git a/app_socket.js b/app_socket.js
index 8694a43..53aa2cf 100644
--- a/app_socket.js
+++ b/app_socket.js
@@ -1,51 +1,71 @@
-var socket
-var socketUrl
+let socket
function sendMessage(message) {
- if (socket) {
- socket.send(message)
+ if (!socket) {
+ createSocket(function() {
+ sendMessage( "reconnect{session=" + sessionID + "}" );
+ if (!windowFocus) {
+ windowFocus = true;
+ sendMessage( "session-resume{session=" + sessionID +"}" );
+ }
+ socket.send(message);
+ });
+ } else {
+ socket.send(message);
}
}
-window.onload = function() {
- socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
+function createSocket(onopen) {
+ let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
- var port = document.location.port
+ const port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
- socket.onopen = socketOpen;
- socket.onclose = socketClose;
- socket.onerror = socketError;
+ socket.onopen = onopen;
+ socket.onclose = onSocketClose;
+ socket.onerror = onSocketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
-};
+}
-function socketOpen() {
- sendMessage( sessionInfo() );
+function closeSocket() {
+ if (socket) {
+ socket.close()
+ }
}
-function socketReopen() {
+window.onload = createSocket(function() {
+ sendMessage( sessionInfo() );
+});
+
+window.onfocus = function() {
+ windowFocus = true
+ if (!socket) {
+ createSocket(function() {
+ sendMessage( "reconnect{session=" + sessionID + "}" );
+ sendMessage( "session-resume{session=" + sessionID +"}" );
+ });
+ } else {
+ sendMessage( "session-resume{session=" + sessionID +"}" );
+ }
+}
+
+function onSocketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
- socket = new WebSocket(socketUrl);
- socket.onopen = socketReopen;
- socket.onclose = socketClose;
- socket.onerror = socketError;
- socket.onmessage = function(event) {
- window.execScript ? window.execScript(event.data) : window.eval(event.data);
- };
+ createSocket(onSocketReopen);
}
}
-function socketClose(event) {
+function onSocketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
@@ -53,15 +73,6 @@ function socketClose(event) {
}
}
-function socketError(error) {
+function onSocketError(error) {
console.log(error);
}
-
-window.onfocus = function(event) {
- windowFocus = true
- if (!socket) {
- socketReconnect()
- } else {
- sendMessage( "session-resume{session=" + sessionID +"}" );
- }
-}
diff --git a/app_styles.css b/app_styles.css
index 2e090a5..1af77fa 100644
--- a/app_styles.css
+++ b/app_styles.css
@@ -57,9 +57,10 @@ button {
textarea {
margin: 2px;
- padding: 1px;
+ padding: 4px;
overflow: auto;
font-size: inherit;
+ resize: none;
}
ul:focus {
diff --git a/app_wasm.js b/app_wasm.js
index d4d2010..e5c1a90 100644
--- a/app_wasm.js
+++ b/app_wasm.js
@@ -1,5 +1,8 @@
-window.onfocus = function(event) {
+window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}
+
+function closeSocket() {
+}
diff --git a/application.go b/application.go
index 6b2afc8..db8d0ba 100644
--- a/application.go
+++ b/application.go
@@ -17,6 +17,7 @@ var defaultThemeText string
// Application - app interface
type Application interface {
Finish()
+ Params() AppParams
removeSession(id int)
}
@@ -24,23 +25,37 @@ type Application interface {
type AppParams struct {
// Title - title of the app window/tab
Title string
+
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color
+
// Icon - the icon file name
Icon string
+
// CertFile - path of a certificate for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
// If the certificate is signed by a certificate authority, the certFile should be the concatenation
// of the server's certificate, any intermediates, and the CA's certificate.
CertFile string
+
// KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string
+
// Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool
+
+ // NoSocket - if true then WebSockets will not be used and information exchange
+ // between the client and the server will be carried out only via http.
+ NoSocket bool
+
+ // SocketAutoClose - time in seconds after which the socket is automatically closed for an inactive session.
+ // The countdown begins after the OnPause event arrives.
+ // If the value of this property is less than or equal to 0 then the socket is not closed.
+ SocketAutoClose int
}
-func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) {
+func getStartPage(buffer *strings.Builder, params AppParams) {
buffer.WriteString(`
`)
@@ -67,11 +82,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
buffer.WriteString(appStyles)
buffer.WriteString(`
-
+
diff --git a/background.go b/background.go
index 692c73d..ec83df6 100644
--- a/background.go
+++ b/background.go
@@ -1,6 +1,9 @@
package rui
-import "strings"
+import (
+ "fmt"
+ "strings"
+)
const (
// NoRepeat is value of the Repeat property of an background image:
@@ -61,6 +64,8 @@ const (
// BackgroundElement describes the background element.
type BackgroundElement interface {
Properties
+ fmt.Stringer
+ stringWriter
cssStyle(session Session) string
Tag() string
Clone() BackgroundElement
@@ -239,3 +244,20 @@ func (image *backgroundImage) cssStyle(session Session) string {
return ""
}
+
+func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
+ image.writeToBuffer(buffer, indent, image.Tag(), []string{
+ Source,
+ Width,
+ Height,
+ ImageHorizontalAlign,
+ ImageVerticalAlign,
+ backgroundFit,
+ Repeat,
+ Attachment,
+ })
+}
+
+func (image *backgroundImage) String() string {
+ return runStringWriter(image)
+}
diff --git a/backgroundConicGradient.go b/backgroundConicGradient.go
index c2b646b..385e97f 100644
--- a/backgroundConicGradient.go
+++ b/backgroundConicGradient.go
@@ -336,3 +336,16 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
return buffer.String()
}
+
+func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
+ gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
+ Gradient,
+ CenterX,
+ CenterY,
+ Repeating,
+ })
+}
+
+func (gradient *backgroundConicGradient) String() string {
+ return runStringWriter(gradient)
+}
diff --git a/backgroundGradient.go b/backgroundGradient.go
index 9373841..12595d1 100644
--- a/backgroundGradient.go
+++ b/backgroundGradient.go
@@ -224,6 +224,33 @@ func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
return 0, false
}
+func (point *BackgroundGradientPoint) String() string {
+ result := "black"
+ if point.Color != nil {
+ switch color := point.Color.(type) {
+ case string:
+ result = color
+
+ case Color:
+ result = color.String()
+ }
+ }
+
+ if point.Pos != nil {
+ switch value := point.Pos.(type) {
+ case string:
+ result += " " + value
+
+ case SizeUnit:
+ if value.Type != Auto {
+ result += " " + value.String()
+ }
+ }
+ }
+
+ return result
+}
+
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
@@ -370,6 +397,18 @@ func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
return buffer.String()
}
+func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
+ gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
+ Gradient,
+ Repeating,
+ Direction,
+ })
+}
+
+func (gradient *backgroundLinearGradient) String() string {
+ return runStringWriter(gradient)
+}
+
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
@@ -610,3 +649,17 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
return buffer.String()
}
+func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
+ gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
+ Gradient,
+ CenterX,
+ CenterY,
+ Repeating,
+ RadialGradientShape,
+ RadialGradientRadius,
+ })
+}
+
+func (gradient *backgroundRadialGradient) String() string {
+ return runStringWriter(gradient)
+}
diff --git a/colorPicker.go b/colorPicker.go
index 45df60d..bde5c6e 100644
--- a/colorPicker.go
+++ b/colorPicker.go
@@ -34,6 +34,7 @@ func newColorPicker(session Session) View {
func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "ColorPicker"
+ picker.hasHtmlDisabled = true
picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.properties[Padding] = Px(0)
}
@@ -153,13 +154,6 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
}
}
-func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- picker.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
diff --git a/customView.go b/customView.go
index fa2cba2..a846916 100644
--- a/customView.go
+++ b/customView.go
@@ -188,10 +188,6 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
customView.superView.htmlProperties(customView.superView, buffer)
}
-func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- customView.superView.htmlDisabledProperties(customView.superView, buffer)
-}
-
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
customView.superView.cssStyle(customView.superView, builder)
}
diff --git a/datePicker.go b/datePicker.go
index 0325450..404144c 100644
--- a/datePicker.go
+++ b/datePicker.go
@@ -40,6 +40,7 @@ func newDatePicker(session Session) View {
func (picker *datePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "DatePicker"
+ picker.hasHtmlDisabled = true
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
}
@@ -303,13 +304,6 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
-func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- picker.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
diff --git a/dropDownList.go b/dropDownList.go
index d6f84f0..a62a80a 100644
--- a/dropDownList.go
+++ b/dropDownList.go
@@ -39,6 +39,7 @@ func newDropDownList(session Session) View {
func (list *dropDownListData) init(session Session) {
list.viewData.init(session)
list.tag = "DropDownList"
+ list.hasHtmlDisabled = true
list.items = []string{}
list.disabledItems = []any{}
list.dropDownListener = []func(DropDownList, int, int){}
@@ -370,13 +371,6 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
}
-func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- list.viewData.htmlDisabledProperties(self, buffer)
- if IsDisabled(list) {
- buffer.WriteString(`disabled`)
- }
-}
-
func (list *dropDownListData) onSelectedItemChanged(number, old int) {
for _, listener := range list.dropDownListener {
listener(list, number, old)
diff --git a/editView.go b/editView.go
index d8f9e9a..36b283f 100644
--- a/editView.go
+++ b/editView.go
@@ -58,6 +58,7 @@ func newEditView(session Session) View {
func (edit *editViewData) init(session Session) {
edit.viewData.init(session)
+ edit.hasHtmlDisabled = true
edit.textChangeListeners = []func(EditView, string, string){}
edit.tag = "EditView"
}
@@ -466,13 +467,6 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
}
}
-func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- edit.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText {
buffer.WriteString(GetText(edit))
@@ -517,19 +511,30 @@ func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
+
+ session := view.Session()
+ text := ""
if view != nil {
- if text, ok := stringProperty(view, Hint, view.Session()); ok {
- return text
- }
- if value := valueFromStyle(view, Hint); value != nil {
- if text, ok := value.(string); ok {
- if text, ok = view.Session().resolveConstants(text); ok {
- return text
+ var ok bool
+ text, ok = stringProperty(view, Hint, view.Session())
+ if !ok {
+ if value := valueFromStyle(view, Hint); value != nil {
+ if text, ok = value.(string); ok {
+ if text, ok = session.resolveConstants(text); !ok {
+ text = ""
+ }
+ } else {
+ text = ""
}
}
}
}
- return ""
+
+ if text != "" && !GetNotTranslate(view) {
+ text, _ = session.GetString(text)
+ }
+
+ return text
}
// GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned
diff --git a/filePicker.go b/filePicker.go
index 8081aab..cf2b357 100644
--- a/filePicker.go
+++ b/filePicker.go
@@ -83,6 +83,7 @@ func newFilePicker(session Session) View {
func (picker *filePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "FilePicker"
+ picker.hasHtmlDisabled = true
picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
@@ -260,13 +261,6 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
-func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- picker.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "fileSelected":
diff --git a/go.mod b/go.mod
index 4ad2028..ba67a24 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,6 @@ module github.com/anoshenko/rui
go 1.18
-require github.com/gorilla/websocket v1.5.0
+require github.com/gorilla/websocket v1.5.1
+
+require golang.org/x/net v0.17.0 // indirect
diff --git a/go.sum b/go.sum
index e5a03d4..272772f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,4 @@
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
diff --git a/gridLayout.go b/gridLayout.go
index 83bf227..20dbb10 100644
--- a/gridLayout.go
+++ b/gridLayout.go
@@ -5,6 +5,44 @@ import (
"strings"
)
+const (
+ // CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
+ // The "cell-vertical-align" int property sets the default vertical alignment
+ // of GridLayout children within the cell they are occupying. Valid values:
+ // * TopAlign (0) / "top"
+ // * BottomAlign (1) / "bottom"
+ // * CenterAlign (2) / "center", and
+ // * StretchAlign (2) / "stretch"
+ CellVerticalAlign = "cell-vertical-align"
+
+ // CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
+ // The "cell-horizontal-align" int property sets the default horizontal alignment
+ // of GridLayout children within the occupied cell. Valid values:
+ // * LeftAlign (0) / "left"
+ // * RightAlign (1) / "right"
+ // * CenterAlign (2) / "center"
+ // * StretchAlign (3) / "stretch"
+ CellHorizontalAlign = "cell-horizontal-align"
+
+ // CellVerticalSelfAlign is the constant for the "cell-vertical-self-align" property tag.
+ // The "cell-vertical-align" int property sets the vertical alignment of GridLayout children
+ // within the cell they are occupying. The property is set for the child view of GridLayout. Valid values:
+ // * TopAlign (0) / "top"
+ // * BottomAlign (1) / "bottom"
+ // * CenterAlign (2) / "center", and
+ // * StretchAlign (2) / "stretch"
+ CellVerticalSelfAlign = "cell-vertical-self-align"
+
+ // CellHorizontalSelfAlign is the constant for the "cell-horizontal-self-align" property tag.
+ // The "cell-horizontal-self align" int property sets the horizontal alignment of GridLayout children
+ // within the occupied cell. The property is set for the child view of GridLayout. Valid values:
+ // * LeftAlign (0) / "left"
+ // * RightAlign (1) / "right"
+ // * CenterAlign (2) / "center"
+ // * StretchAlign (3) / "stretch"
+ CellHorizontalSelfAlign = "cell-horizontal-self-align"
+)
+
// GridLayout - grid-container of View
type GridLayout interface {
ViewsContainer
diff --git a/image.go b/image.go
index 007afb8..3a740a4 100644
--- a/image.go
+++ b/image.go
@@ -80,10 +80,11 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
manager.images[url] = image
session.callFunc("loadImage", url)
+ session.sendResponse()
return image
}
-func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
+func (manager *imageManager) imageLoaded(obj DataObject) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return
@@ -109,7 +110,7 @@ func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
}
}
-func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
+func (manager *imageManager) imageLoadError(obj DataObject) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return
diff --git a/listAdapter.go b/listAdapter.go
index efa3d69..e554d69 100644
--- a/listAdapter.go
+++ b/listAdapter.go
@@ -2,8 +2,13 @@ package rui
// ListAdapter - the list data source
type ListAdapter interface {
+ // ListSize returns the number of elements in the list
ListSize() int
+
+ // ListItem creates a View of a list item at the given index
ListItem(index int, session Session) View
+
+ // IsListItemEnabled returns the status (enabled/disabled) of a list item at the given index
IsListItemEnabled(index int) bool
}
diff --git a/listLayout.go b/listLayout.go
index dc608de..6bdcac2 100644
--- a/listLayout.go
+++ b/listLayout.go
@@ -7,16 +7,22 @@ import (
const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0
+
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1
+
// BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2
+
// EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3
+
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0
+
// ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1
+
// ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2
)
diff --git a/listView.go b/listView.go
index 5a3b0e3..ab70396 100644
--- a/listView.go
+++ b/listView.go
@@ -11,20 +11,25 @@ const (
// The "list-item-clicked" event occurs when the user clicks on an item in the list.
// The main listener format: func(ListView, int), where the second argument is the item index.
ListItemClickedEvent = "list-item-clicked"
+
// ListItemSelectedEvent is the constant for "list-item-selected" property tag.
// The "list-item-selected" event occurs when a list item becomes selected.
// The main listener format: func(ListView, int), where the second argument is the item index.
ListItemSelectedEvent = "list-item-selected"
+
// ListItemCheckedEvent is the constant for "list-item-checked" property tag.
// The "list-item-checked" event occurs when a list item checkbox becomes checked/unchecked.
// The main listener format: func(ListView, []int), where the second argument is the array of checked item indexes.
ListItemCheckedEvent = "list-item-checked"
+
// ListItemStyle is the constant for "list-item-style" property tag.
// The "list-item-style" string property defines the style of an unselected item
ListItemStyle = "list-item-style"
+
// CurrentStyle is the constant for "current-style" property tag.
// The "current-style" string property defines the style of the selected item when the ListView is focused.
CurrentStyle = "current-style"
+
// CurrentInactiveStyle is the constant for "current-inactive-style" property tag.
// The "current-inactive-style" string property defines the style of the selected item when the ListView is unfocused.
CurrentInactiveStyle = "current-inactive-style"
@@ -589,7 +594,7 @@ func (listView *listViewData) getItemFrames() []Frame {
return listView.itemFrame
}
-func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
+func (listView *listViewData) itemAlign(buffer *strings.Builder) {
values := enumProperties[ItemHorizontalAlign].cssValues
if hAlign := GetListItemHorizontalAlign(listView); hAlign >= 0 && hAlign < len(values) {
buffer.WriteString(" justify-items: ")
@@ -605,7 +610,7 @@ func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
}
}
-func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
+func (listView *listViewData) itemSize(buffer *strings.Builder) {
if itemWidth := GetListItemWidth(listView); itemWidth.Type != Auto {
buffer.WriteString(` min-width: `)
buffer.WriteString(itemWidth.cssString("", listView.Session()))
@@ -619,14 +624,14 @@ func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
}
}
-func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
+func (listView *listViewData) getDivs(checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
session := listView.Session()
contentBuilder := allocStringBuilder()
defer freeStringBuilder(contentBuilder)
contentBuilder.WriteString(``)
diff --git a/mediaPlayer.go b/mediaPlayer.go
index acb904d..2eee66b 100644
--- a/mediaPlayer.go
+++ b/mediaPlayer.go
@@ -13,15 +13,18 @@ const (
// to control audio/video playback, including volume, seeking, and pause/resume playback.
// Its default value is false.
Controls = "controls"
+
// Loop is the constant for the "loop" property tag.
// If the "loop" bool property is "true", the audio/video player will automatically seek back
// to the start upon reaching the end of the audio/video.
// Its default value is false.
Loop = "loop"
+
// Muted is the constant for the "muted" property tag.
// The "muted" bool property indicates whether the audio/video will be initially silenced.
// Its default value is false.
Muted = "muted"
+
// Preload is the constant for the "preload" property tag.
// The "preload" int property is intended to provide a hint to the browser about what
// the author thinks will lead to the best user experience. It may have one of the following values:
@@ -32,72 +35,94 @@ const (
// AbortEvent is the constant for the "abort-event" property tag.
// The "abort-event" event fired when the resource was not fully loaded, but not as the result of an error.
AbortEvent = "abort-event"
+
// CanPlayEvent is the constant for the "can-play-event" property tag.
// The "can-play-event" event occurs when the browser can play the media, but estimates that not enough data has been
// loaded to play the media up to its end without having to stop for further buffering of content.
CanPlayEvent = "can-play-event"
+
// CanPlayThroughEvent is the constant for the "can-play-through-event" property tag.
// The "can-play-through-event" event occurs when the browser estimates it can play the media up
// to its end without stopping for content buffering.
CanPlayThroughEvent = "can-play-through-event"
+
// CompleteEvent is the constant for the "complete-event" property tag.
// The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated.
CompleteEvent = "complete-event"
+
// DurationChangedEvent is the constant for the "duration-changed-event" property tag.
// The "duration-changed-event" event occurs when the duration attribute has been updated.
DurationChangedEvent = "duration-changed-event"
+
// EmptiedEvent is the constant for the "emptied-event" property tag.
// The "emptied-event" event occurs when the media has become empty; for example, this event is sent if the media has already been loaded
// (or partially loaded), and the HTMLMediaElement.load method is called to reload it.
EmptiedEvent = "emptied-event"
+
// EndedEvent is the constant for the "ended-event" property tag.
// The "ended-event" event occurs when the playback has stopped because the end of the media was reached.
EndedEvent = "ended-event"
+
// LoadedDataEvent is the constant for the "loaded-data-event" property tag.
// The "loaded-data-event" event occurs when the first frame of the media has finished loading.
LoadedDataEvent = "loaded-data-event"
+
// LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag.
// The "loaded-metadata-event" event occurs when the metadata has been loaded.
LoadedMetadataEvent = "loaded-metadata-event"
+
// LoadStartEvent is the constant for the "load-start-event" property tag.
// The "load-start-event" event is fired when the browser has started to load a resource.
LoadStartEvent = "load-start-event"
+
// PauseEvent is the constant for the "pause-event" property tag.
// The "pause-event" event occurs when the playback has been paused.
PauseEvent = "pause-event"
+
// PlayEvent is the constant for the "play-event" property tag.
// The "play-event" event occurs when the playback has begun.
PlayEvent = "play-event"
+
// PlayingEvent is the constant for the "playing-event" property tag.
// The "playing-event" event occurs when the playback is ready to start after having been paused or delayed due to lack of data.
PlayingEvent = "playing-event"
+
// ProgressEvent is the constant for the "progress-event" property tag.
// The "progress-event" event is fired periodically as the browser loads a resource.
ProgressEvent = "progress-event"
+
// RateChangeEvent is the constant for the "rate-change-event" property tag.
// The "rate-change-event" event occurs when the playback rate has changed.
RateChangedEvent = "rate-changed-event"
+
// SeekedEvent is the constant for the "seeked-event" property tag.
// The "seeked-event" event occurs when a seek operation completed.
SeekedEvent = "seeked-event"
+
// SeekingEvent is the constant for the "seeking-event" property tag.
// The "seeking-event" event occurs when a seek operation began.
SeekingEvent = "seeking-event"
+
// StalledEvent is the constant for the "stalled-event" property tag.
// The "stalled-event" event occurs when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
StalledEvent = "stalled-event"
+
// SuspendEvent is the constant for the "suspend-event" property tag.
// The "suspend-event" event occurs when the media data loading has been suspended.
SuspendEvent = "suspend-event"
+
// TimeUpdateEvent is the constant for the "time-update-event" property tag.
// The "time-update-event" event occurs when the time indicated by the currentTime attribute has been updated.
TimeUpdateEvent = "time-update-event"
+
// VolumeChangedEvent is the constant for the "volume-change-event" property tag.
// The "volume-change-event" event occurs when the volume has changed.
VolumeChangedEvent = "volume-changed-event"
+
// WaitingEvent is the constant for the "waiting-event" property tag.
// The "waiting-event" event occurs when the playback has stopped because of a temporary lack of data
WaitingEvent = "waiting-event"
+
// PlayerErrorEvent is the constant for the "player-error-event" property tag.
// The "player-error-event" event is fired when the resource could not be loaded due to an error
// (for example, a network connectivity problem).
@@ -105,51 +130,68 @@ const (
// PreloadNone - value of the view "preload" property: indicates that the audio/video should not be preloaded.
PreloadNone = 0
+
// PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched.
PreloadMetadata = 1
+
// PreloadAuto - value of the view "preload" property: indicates that the whole audio file can be downloaded,
// even if the user is not expected to use it.
PreloadAuto = 2
// PlayerErrorUnknown - MediaPlayer error code: An unknown error.
PlayerErrorUnknown = 0
+
// PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request.
PlayerErrorAborted = 1
+
// PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media
// from being successfully fetched, despite having previously been available.
PlayerErrorNetwork = 2
+
// PlayerErrorDecode - MediaPlayer error code: Despite having previously been determined to be usable,
// an error occurred while trying to decode the media resource, resulting in an error.
PlayerErrorDecode = 3
+
// PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable.
PlayerErrorSourceNotSupported = 4
)
type MediaPlayer interface {
View
+
// Play attempts to begin playback of the media.
Play()
+
// Pause will pause playback of the media, if the media is already in a paused state this method will have no effect.
Pause()
+
// SetCurrentTime sets the current playback time in seconds.
SetCurrentTime(seconds float64)
+
// CurrentTime returns the current playback time in seconds.
CurrentTime() float64
+
// Duration returns the value indicating the total duration of the media in seconds.
// If no media data is available, the returned value is NaN.
Duration() float64
+
// SetPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls
// for fast forward, slow motion, and so forth. The normal playback rate is multiplied by this value to obtain
// the current rate, so a value of 1.0 indicates normal speed.
SetPlaybackRate(rate float64)
+
// PlaybackRate returns the rate at which the media is being played back.
PlaybackRate() float64
+
// SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest).
SetVolume(volume float64)
+
// Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest).
Volume() float64
+
// IsEnded function tells whether the media element is ended.
IsEnded() bool
+
// IsPaused function tells whether the media element is paused.
IsPaused() bool
}
diff --git a/mouseEvents.go b/mouseEvents.go
index 614ecf7..03dd36f 100644
--- a/mouseEvents.go
+++ b/mouseEvents.go
@@ -82,24 +82,32 @@ const (
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0
+
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present)
AuxiliaryMouseButton = 1
+
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2
+
// MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3
+
// MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1
+
// SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2
+
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4
+
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8
+
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16
)
diff --git a/numberPicker.go b/numberPicker.go
index d7b3745..0db087a 100644
--- a/numberPicker.go
+++ b/numberPicker.go
@@ -7,17 +7,37 @@ import (
)
const (
+ // NumberChangedEvent is the constant for the "" property tag.
+ // The "number-changed" property sets listener(s) that track the change in the entered value.
NumberChangedEvent = "number-changed"
- NumberPickerType = "number-picker-type"
- NumberPickerMin = "number-picker-min"
- NumberPickerMax = "number-picker-max"
- NumberPickerStep = "number-picker-step"
- NumberPickerValue = "number-picker-value"
+
+ // NumberPickerType is the constant for the "number-picker-type" property tag.
+ // The "number-picker-type" int property sets the mode of NumberPicker. It can take the following values:
+ // * NumberEditor (0) - NumberPicker is presented by editor. Default value;
+ // * NumberSlider (1) - NumberPicker is presented by slider. |
+ NumberPickerType = "number-picker-type"
+
+ // NumberPickerMin is the constant for the "number-picker-min" property tag.
+ // The "number-picker-min" int property sets the minimum value of NumberPicker. The default value is 0.
+ NumberPickerMin = "number-picker-min"
+
+ // NumberPickerMax is the constant for the "number-picker-max" property tag.
+ // The "number-picker-max" int property sets the maximum value of NumberPicker. The default value is 1.
+ NumberPickerMax = "number-picker-max"
+
+ // NumberPickerStep is the constant for the "number-picker-step" property tag.
+ // The "number-picker-step" int property sets the value change step of NumberPicker
+ NumberPickerStep = "number-picker-step"
+
+ // NumberPickerValue is the constant for the "number-picker-value" property tag.
+ // The "number-picker-value" int property sets the current value of NumberPicker. The default value is 0.
+ NumberPickerValue = "number-picker-value"
)
const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0
+
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1
)
@@ -47,6 +67,7 @@ func newNumberPicker(session Session) View {
func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "NumberPicker"
+ picker.hasHtmlDisabled = true
picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
}
@@ -232,13 +253,6 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
}
-func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- picker.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
diff --git a/properties.go b/properties.go
index a53ef8e..fbf18c2 100644
--- a/properties.go
+++ b/properties.go
@@ -11,15 +11,19 @@ type Properties interface {
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) any
getRaw(tag string) any
+
// Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
Set(tag string, value any) bool
setRaw(tag string, value any)
+
// Remove removes the property with name defined by the argument
Remove(tag string)
+
// Clear removes all properties
Clear()
+
// AllTags returns an array of the set properties
AllTags() []string
}
@@ -68,6 +72,28 @@ func (properties *propertyList) AllTags() []string {
return tags
}
+func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
+ indent string, objectTag string, tags []string) {
+
+ buffer.WriteString(objectTag)
+ buffer.WriteString(" {\n")
+
+ indent2 := indent + "\t"
+
+ for _, tag := range tags {
+ if value, ok := properties.properties[tag]; ok {
+ buffer.WriteString(indent2)
+ buffer.WriteString(tag)
+ buffer.WriteString(" = ")
+ writePropertyValue(buffer, tag, value, indent2)
+ buffer.WriteString(",\n")
+ }
+ }
+
+ buffer.WriteString(indent)
+ buffer.WriteString("}")
+}
+
func parseProperties(properties Properties, object DataObject) {
count := object.PropertyCount()
for i := 0; i < count; i++ {
diff --git a/propertyNames.go b/propertyNames.go
index 1469d9e..8a9c08d 100644
--- a/propertyNames.go
+++ b/propertyNames.go
@@ -666,7 +666,8 @@ const (
// Resize is the constant for the "resize" property tag.
// The "resize" int property sets whether an element is resizable, and if so, in which directions.
- // Valid values are "none" (0), "both" (1), horizontal (2), and "vertical" (3)
+ // Valid values are "none" / NoneResize (0), "both" / BothResize (1),
+ // "horizontal" / HorizontalResize (2), and "vertical" / VerticalResize (3)
Resize = "resize"
// UserSelect is the constant for the "user-select" property tag.
diff --git a/propertySet.go b/propertySet.go
index 5dc8bd9..24ae0be 100644
--- a/propertySet.go
+++ b/propertySet.go
@@ -331,6 +331,16 @@ var enumProperties = map[string]struct {
"justify-items",
[]string{"start", "end", "center", "stretch"},
},
+ CellVerticalSelfAlign: {
+ []string{"top", "bottom", "center", "stretch"},
+ "align-self",
+ []string{"start", "end", "center", "stretch"},
+ },
+ CellHorizontalSelfAlign: {
+ []string{"left", "right", "center", "stretch"},
+ "justify-self",
+ []string{"start", "end", "center", "stretch"},
+ },
GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow,
diff --git a/resizable.go b/resizable.go
index 7eb5f7b..a183ea5 100644
--- a/resizable.go
+++ b/resizable.go
@@ -11,22 +11,23 @@ const (
// The "side" int property determines which side of the container is used to resize.
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
Side = "side"
+
// ResizeBorderWidth is the constant for the "resize-border-width" property tag.
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
ResizeBorderWidth = "resize-border-width"
- // CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
- CellVerticalAlign = "cell-vertical-align"
- // CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
- CellHorizontalAlign = "cell-horizontal-align"
// TopSide is value of the "side" property: the top side is used to resize
TopSide = 1
+
// RightSide is value of the "side" property: the right side is used to resize
RightSide = 2
+
// BottomSide is value of the "side" property: the bottom side is used to resize
BottomSide = 4
+
// LeftSide is value of the "side" property: the left side is used to resize
LeftSide = 8
+
// AllSides is value of the "side" property: all sides is used to resize
AllSides = TopSide | RightSide | BottomSide | LeftSide
)
diff --git a/session.go b/session.go
index 08dd300..15c7f90 100644
--- a/session.go
+++ b/session.go
@@ -7,7 +7,7 @@ import (
"strings"
)
-type webBridge interface {
+type bridge interface {
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool
@@ -16,10 +16,9 @@ type webBridge interface {
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
- readMessage() (string, bool)
- writeMessage(text string) bool
- addAnimationCSS(css string)
- clearAnimation()
+ sendResponse()
+ setAnimationCSS(css string)
+ appendAnimationCSS(css string)
canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any)
@@ -111,6 +110,14 @@ type Session interface {
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
+ // StartTimer starts a timer on the client side.
+ // The first argument specifies the timer period in milliseconds.
+ // The second argument specifies a function that will be called on each timer event.
+ // The result is the id of the timer, which is used to stop the timer
+ StartTimer(ms int, timerFunc func(Session)) int
+ // StopTimer the timer with the given id
+ StopTimer(timerID int)
+
getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string
@@ -124,7 +131,7 @@ type Session interface {
nextViewID() string
styleProperty(styleTag, property string) any
- setBridge(events chan DataObject, bridge webBridge)
+ setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder)
callFunc(funcName string, args ...any)
updateInnerHTML(htmlID, html string)
@@ -134,6 +141,7 @@ type Session interface {
removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
+ sendResponse()
addAnimationCSS(css string)
clearAnimation()
canvasStart(htmlID string)
@@ -145,7 +153,8 @@ type Session interface {
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
- handleAnswer(data DataObject)
+ addToEventsQueue(data DataObject)
+ handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject)
handleResize(data DataObject)
handleEvent(command string, data DataObject)
@@ -189,13 +198,16 @@ type sessionData struct {
ignoreUpdates bool
popups *popupManager
images *imageManager
- bridge webBridge
+ bridge bridge
events chan DataObject
animationCounter int
animationCSS string
updateScripts map[string]*strings.Builder
clientStorage map[string]string
hotkeys map[string]func(Session)
+ timers map[int]func(Session)
+ nextTimerID int
+ pauseTime int64
}
func newSession(app Application, id int, customTheme string, params DataObject) Session {
@@ -214,6 +226,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){}
+ session.timers = map[int]func(Session){}
+ session.nextTimerID = 1
if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok {
@@ -237,7 +251,7 @@ func (session *sessionData) ID() int {
return session.sessionID
}
-func (session *sessionData) setBridge(events chan DataObject, bridge webBridge) {
+func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
session.events = events
session.bridge = bridge
}
@@ -330,23 +344,19 @@ func (session *sessionData) updateTooltipConstants() {
}
func (session *sessionData) reload() {
- buffer := allocStringBuilder()
- defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
- css = strings.ReplaceAll(css, "\n", `\n`)
- css = strings.ReplaceAll(css, "\t", `\t`)
- buffer.WriteString(`document.querySelector('style').textContent = "`)
- buffer.WriteString(css)
- buffer.WriteString("\";\n")
+ session.bridge.callFunc("setStyles", css)
if session.rootView != nil {
- buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
+ buffer := allocStringBuilder()
+ defer freeStringBuilder(buffer)
+
viewHTML(session.rootView, buffer)
- buffer.WriteString("';\nscanElementsSize();")
+ session.bridge.updateInnerHTML("ruiRootView", buffer.String())
+ session.bridge.callFunc("scanElementsSize")
}
- session.bridge.writeMessage(buffer.String())
session.updateTooltipConstants()
}
@@ -447,15 +457,21 @@ func (session *sessionData) finishUpdateScript(htmlID string) {
}
}
+func (session *sessionData) sendResponse() {
+ if session.bridge != nil {
+ session.bridge.sendResponse()
+ }
+}
+
func (session *sessionData) addAnimationCSS(css string) {
if session.bridge != nil {
- session.bridge.addAnimationCSS(css)
+ session.bridge.appendAnimationCSS(css)
}
}
func (session *sessionData) clearAnimation() {
if session.bridge != nil {
- session.bridge.clearAnimation()
+ session.bridge.setAnimationCSS("")
}
}
@@ -520,8 +536,27 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
return ""
}
-func (session *sessionData) handleAnswer(data DataObject) {
- session.bridge.answerReceived(data)
+func (session *sessionData) handleAnswer(command string, data DataObject) bool {
+ switch command {
+ case "answer":
+ if session.bridge != nil {
+ session.bridge.answerReceived(data)
+ }
+
+ case "imageLoaded":
+ session.imageManager().imageLoaded(data)
+
+ case "imageError":
+ session.imageManager().imageLoadError(data)
+
+ default:
+ return false
+ }
+
+ if session.bridge != nil {
+ session.bridge.sendResponse()
+ }
+ return true
}
func (session *sessionData) handleRootSize(data DataObject) {
@@ -642,6 +677,22 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
case "session-resume":
session.onResume()
+ case "timer":
+ if text, ok := data.PropertyValue("timerID"); ok {
+ timerID, err := strconv.Atoi(text)
+ if err == nil {
+ if fn, ok := session.timers[timerID]; ok {
+ fn(session)
+ } else {
+ ErrorLog(`Timer (id = ` + text + `) not exists`)
+ }
+ } else {
+ ErrorLog(err.Error())
+ }
+ } else {
+ ErrorLog(`"timerID" property not found`)
+ }
+
case "root-size":
session.handleRootSize(data)
@@ -672,6 +723,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
ErrorLog(`"id" property not found. Event: ` + command)
}
}
+
+ session.bridge.sendResponse()
}
func (session *sessionData) hotKey(event KeyEvent) {
@@ -769,3 +822,25 @@ func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")
}
+
+func (session *sessionData) addToEventsQueue(data DataObject) {
+ session.events <- data
+}
+
+func (session *sessionData) StartTimer(ms int, timerFunc func(Session)) int {
+ timerID := 0
+ if session.bridge != nil {
+ timerID = session.nextTimerID
+ session.nextTimerID++
+ session.timers[timerID] = timerFunc
+ session.bridge.callFunc("startTimer", ms, timerID)
+ }
+ return timerID
+}
+
+func (session *sessionData) StopTimer(timerID int) {
+ if session.bridge != nil {
+ session.bridge.callFunc("stopTimer", timerID)
+ delete(session.timers, timerID)
+ }
+}
diff --git a/sessionEvents.go b/sessionEvents.go
index 9c66e7b..99a4916 100644
--- a/sessionEvents.go
+++ b/sessionEvents.go
@@ -1,5 +1,7 @@
package rui
+import "time"
+
// SessionStartListener is the listener interface of a session start event
type SessionStartListener interface {
OnStart(session Session)
@@ -50,13 +52,25 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() {
if session.content != nil {
+ session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session)
}
+ if timeout := session.app.Params().SocketAutoClose; timeout > 0 {
+ go session.autoClose(session.pauseTime, timeout)
+ }
+ }
+}
+
+func (session *sessionData) autoClose(start int64, timeout int) {
+ time.Sleep(time.Second * time.Duration(timeout))
+ if session.pauseTime == start {
+ session.bridge.callFunc("closeSocket")
}
}
func (session *sessionData) onResume() {
+ session.pauseTime = 0
if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session)
diff --git a/stackLayout.go b/stackLayout.go
index bdf79a1..1c0a78f 100644
--- a/stackLayout.go
+++ b/stackLayout.go
@@ -22,10 +22,33 @@ const (
// StackLayout - list-container of View
type StackLayout interface {
ViewsContainer
+
+ // Peek returns the current (visible) View. If StackLayout is empty then it returns nil.
Peek() View
+
+ // RemovePeek removes the current View and returns it. If StackLayout is empty then it doesn't do anything and returns nil.
+ RemovePeek() View
+
+ // MoveToFront makes the given View current. Returns true if successful, false otherwise.
MoveToFront(view View) bool
+
+ // MoveToFrontByID makes the View current by viewID. Returns true if successful, false otherwise.
MoveToFrontByID(viewID string) bool
+
+ // Push adds a new View to the container and makes it current.
+ // It is similar to Append, but the addition is done using an animation effect.
+ // The animation type is specified by the second argument and can take the following values:
+ // * DefaultAnimation (0) - Default animation. For the Push function it is EndToStartAnimation, for Pop - StartToEndAnimation;
+ // * StartToEndAnimation (1) - Animation from beginning to end. The beginning and the end are determined by the direction of the text output;
+ // * EndToStartAnimation (2) - End-to-Beginning animation;
+ // * TopDownAnimation (3) - Top-down animation;
+ // * BottomUpAnimation (4) - Bottom up animation.
+ // The third argument `onPushFinished` is the function to be called when the animation ends. It may be nil.
Push(view View, animation int, onPushFinished func())
+
+ // Pop removes the current View from the container using animation.
+ // The second argument `onPopFinished`` is the function to be called when the animation ends. It may be nil.
+ // The function will return false if the StackLayout is empty and true if the current item has been removed.
Pop(animation int, onPopFinished func(View)) bool
}
@@ -277,6 +300,10 @@ func (layout *stackLayoutData) RemoveView(index int) View {
return layout.viewsContainerData.RemoveView(index)
}
+func (layout *stackLayoutData) RemovePeek() View {
+ return layout.RemoveView(len(layout.views) - 1)
+}
+
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
if view == nil {
ErrorLog("StackLayout.Push(nil, ....) is forbidden")
diff --git a/timePicker.go b/timePicker.go
index dca0836..feab2ad 100644
--- a/timePicker.go
+++ b/timePicker.go
@@ -40,6 +40,7 @@ func newTimePicker(session Session) View {
func (picker *timePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "TimePicker"
+ picker.hasHtmlDisabled = true
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
}
@@ -291,13 +292,6 @@ func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
-func (picker *timePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` disabled`)
- }
- picker.viewData.htmlDisabledProperties(self, buffer)
-}
-
func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
diff --git a/videoPlayer.go b/videoPlayer.go
index a67647c..ca8d2fd 100644
--- a/videoPlayer.go
+++ b/videoPlayer.go
@@ -8,9 +8,11 @@ const (
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer.
// The "video-width" float property defines the width of the video's display area in pixels.
VideoWidth = "video-width"
+
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer.
// The "video-height" float property defines the height of the video's display area in pixels.
VideoHeight = "video-height"
+
// Poster is the constant for the "poster" property tag of VideoPlayer.
// The "poster" property defines an URL for an image to be shown while the video is downloading.
// If this attribute isn't specified, nothing is displayed until the first frame is available,
diff --git a/view.go b/view.go
index 36bf18f..4d0d8ff 100644
--- a/view.go
+++ b/view.go
@@ -35,24 +35,33 @@ type View interface {
// Session returns the current Session interface
Session() Session
+
// Parent returns the parent view
Parent() View
+
// Tag returns the tag of View interface
Tag() string
+
// ID returns the id of the view
ID() string
+
// Focusable returns true if the view receives the focus
Focusable() bool
+
// Frame returns the location and size of the view in pixels
Frame() Frame
+
// Scroll returns the location size of the scrollable view in pixels
Scroll() Frame
+
// SetAnimated sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
SetAnimated(tag string, value any, animation Animation) bool
+
// SetChangeListener set the function to track the change of the View property
SetChangeListener(tag string, listener func(View, string))
+
// HasFocus returns 'true' if the view has focus
HasFocus() bool
@@ -65,7 +74,6 @@ type View interface {
setParentID(parentID string)
htmlSubviews(self View, buffer *strings.Builder)
htmlProperties(self View, buffer *strings.Builder)
- htmlDisabledProperties(self View, buffer *strings.Builder)
cssStyle(self View, builder cssBuilder)
addToCSSStyle(addCSS map[string]string)
@@ -93,6 +101,7 @@ type viewData struct {
noResizeEvent bool
created bool
hasFocus bool
+ hasHtmlDisabled bool
//animation map[string]AnimationEndListener
}
@@ -135,6 +144,7 @@ func (view *viewData) init(session Session) {
view.singleTransition = map[string]Animation{}
view.noResizeEvent = false
view.created = false
+ view.hasHtmlDisabled = false
}
func (view *viewData) Session() Session {
@@ -302,7 +312,6 @@ func (view *viewData) propertyChangedEvent(tag string) {
if listener, ok := view.changeListener[tag]; ok {
listener(view, tag)
}
-
}
func (view *viewData) Set(tag string, value any) bool {
@@ -404,7 +413,35 @@ func viewPropertyChanged(view *viewData, tag string) {
switch tag {
case Disabled:
- updateInnerHTML(view.parentHTMLID(), session)
+ tabIndex := GetTabIndex(view, htmlID)
+ enabledClass := view.htmlClass(false)
+ disabledClass := view.htmlClass(true)
+ session.startUpdateScript(htmlID)
+ if IsDisabled(view) {
+ session.updateProperty(htmlID, "data-disabled", "1")
+ if view.hasHtmlDisabled {
+ session.updateProperty(htmlID, "disabled", true)
+ }
+ if tabIndex >= 0 {
+ session.updateProperty(htmlID, "tabindex", -1)
+ }
+ if enabledClass != disabledClass {
+ session.updateProperty(htmlID, "class", disabledClass)
+ }
+ } else {
+ session.updateProperty(htmlID, "data-disabled", "0")
+ if view.hasHtmlDisabled {
+ session.removeProperty(htmlID, "disabled")
+ }
+ if tabIndex >= 0 {
+ session.updateProperty(htmlID, "tabindex", tabIndex)
+ }
+ if enabledClass != disabledClass {
+ session.updateProperty(htmlID, "class", enabledClass)
+ }
+ }
+ session.finishUpdateScript(htmlID)
+ updateInnerHTML(htmlID, session)
return
case Visibility:
@@ -613,6 +650,8 @@ func viewPropertyChanged(view *viewData, tag string) {
case ZIndex, Order, TabSize:
if i, ok := intProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, tag, strconv.Itoa(i))
+ } else {
+ session.updateCSSProperty(htmlID, tag, "")
}
return
@@ -660,8 +699,11 @@ func viewPropertyChanged(view *viewData, tag string) {
}
if cssTag, ok := sizeProperties[tag]; ok {
- size, _ := sizeProperty(view, tag, session)
- session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
+ if size, ok := sizeProperty(view, tag, session); ok {
+ session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
+ } else {
+ session.updateCSSProperty(htmlID, cssTag, "")
+ }
return
}
@@ -682,8 +724,11 @@ func viewPropertyChanged(view *viewData, tag string) {
}
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
- n, _ := enumProperty(view, tag, session, 0)
- session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
+ if n, ok := enumProperty(view, tag, session, 0); ok {
+ session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
+ } else {
+ session.updateCSSProperty(htmlID, valuesData.cssTag, "")
+ }
return
}
@@ -691,6 +736,8 @@ func viewPropertyChanged(view *viewData, tag string) {
if tag == floatTag {
if f, ok := floatTextProperty(view, floatTag, session, 0); ok {
session.updateCSSProperty(htmlID, floatTag, f)
+ } else {
+ session.updateCSSProperty(htmlID, floatTag, "")
}
return
}
@@ -759,20 +806,22 @@ func (view *viewData) cssStyle(self View, builder cssBuilder) {
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
view.created = true
+
+ if IsDisabled(self) {
+ buffer.WriteString(` data-disabled="1"`)
+ if view.hasHtmlDisabled {
+ buffer.WriteString(` disabled`)
+ }
+ } else {
+ buffer.WriteString(` data-disabled="0"`)
+ }
+
if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 {
buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`,
view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height))
}
}
-func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
- if IsDisabled(self) {
- buffer.WriteString(` data-disabled="1"`)
- } else {
- buffer.WriteString(` data-disabled="0"`)
- }
-}
-
func viewHTML(view View, buffer *strings.Builder) {
viewHTMLTag := view.htmlTag()
buffer.WriteRune('<')
@@ -800,8 +849,6 @@ func viewHTML(view View, buffer *strings.Builder) {
buffer.WriteRune(' ')
view.htmlProperties(view, buffer)
- buffer.WriteRune(' ')
- view.htmlDisabledProperties(view, buffer)
if view.isNoResizeEvent() {
buffer.WriteString(` data-noresize="1" `)
@@ -810,12 +857,10 @@ func viewHTML(view View, buffer *strings.Builder) {
}
if !disabled {
- if value, ok := intProperty(view, TabIndex, view.Session(), -1); ok {
+ if tabIndex := GetTabIndex(view); tabIndex >= 0 {
buffer.WriteString(`tabindex="`)
- buffer.WriteString(strconv.Itoa(value))
+ buffer.WriteString(strconv.Itoa(tabIndex))
buffer.WriteString(`" `)
- } else if view.Focusable() {
- buffer.WriteString(`tabindex="0" `)
}
}
diff --git a/viewStyle.go b/viewStyle.go
index 71b261f..4c6d6e6 100644
--- a/viewStyle.go
+++ b/viewStyle.go
@@ -13,8 +13,10 @@ type ViewStyle interface {
// Transition returns the transition animation of the property. Returns nil is there is no transition animation.
Transition(tag string) Animation
+
// Transitions returns the map of transition animations. The result is always non-nil.
Transitions() map[string]Animation
+
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
@@ -573,6 +575,9 @@ func supportedPropertyValue(value any) bool {
case []ViewShadow:
case []View:
case []any:
+ case []BackgroundElement:
+ case []BackgroundGradientPoint:
+ case []BackgroundGradientAngle:
case map[string]Animation:
default:
return false
@@ -692,6 +697,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
for _, shadow := range value {
buffer.WriteString(indent2)
shadow.writeString(buffer, indent)
+ buffer.WriteRune(',')
}
buffer.WriteRune('\n')
buffer.WriteString(indent)
@@ -701,7 +707,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
case []View:
switch len(value) {
case 0:
- buffer.WriteString("[]\n")
+ buffer.WriteString("[]")
case 1:
writeViewStyle(value[0].Tag(), value[0], buffer, indent)
@@ -740,6 +746,47 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
buffer.WriteString(" ]")
}
+ case []BackgroundElement:
+ switch len(value) {
+ case 0:
+ buffer.WriteString("[]\n")
+
+ case 1:
+ value[0].writeString(buffer, indent)
+
+ default:
+ buffer.WriteString("[\n")
+ indent2 := indent + "\t"
+ for _, element := range value {
+ buffer.WriteString(indent2)
+ element.writeString(buffer, indent2)
+ buffer.WriteString(",\n")
+ }
+
+ buffer.WriteString(indent)
+ buffer.WriteRune(']')
+ }
+
+ case []BackgroundGradientPoint:
+ buffer.WriteRune('"')
+ for i, point := range value {
+ if i > 0 {
+ buffer.WriteString(",")
+ }
+ buffer.WriteString(point.String())
+ }
+ buffer.WriteRune('"')
+
+ case []BackgroundGradientAngle:
+ buffer.WriteRune('"')
+ for i, point := range value {
+ if i > 0 {
+ buffer.WriteString(",")
+ }
+ buffer.WriteString(point.String())
+ }
+ buffer.WriteRune('"')
+
case map[string]Animation:
switch count := len(value); count {
case 0:
diff --git a/viewStyleSet.go b/viewStyleSet.go
index ad499a8..15f44ce 100644
--- a/viewStyleSet.go
+++ b/viewStyleSet.go
@@ -32,42 +32,52 @@ func (style *viewStyle) setRange(tag string, value any) bool {
}
func (style *viewStyle) setBackground(value any) bool {
+ background := []BackgroundElement{}
+
switch value := value.(type) {
case BackgroundElement:
- style.properties[Background] = []BackgroundElement{value}
- return true
+ background = []BackgroundElement{value}
case []BackgroundElement:
- style.properties[Background] = value
- return true
+ background = value
+
+ case []DataValue:
+ for _, el := range value {
+ if el.IsObject() {
+ if element := createBackground(el.Object()); element != nil {
+ background = append(background, element)
+ }
+ } else if obj := ParseDataText(el.Value()); obj != nil {
+ if element := createBackground(obj); element != nil {
+ background = append(background, element)
+ }
+ }
+ }
case DataObject:
if element := createBackground(value); element != nil {
- style.properties[Background] = []BackgroundElement{element}
- return true
+ background = []BackgroundElement{element}
}
case []DataObject:
for _, obj := range value {
- background := []BackgroundElement{}
if element := createBackground(obj); element != nil {
background = append(background, element)
}
- if len(background) > 0 {
- style.properties[Background] = background
- return true
- }
}
case string:
if obj := ParseDataText(value); obj != nil {
if element := createBackground(obj); element != nil {
- style.properties[Background] = []BackgroundElement{element}
- return true
+ background = []BackgroundElement{element}
}
}
}
+ if len(background) > 0 {
+ style.properties[Background] = background
+ return true
+ }
return false
}
diff --git a/viewsContainer.go b/viewsContainer.go
index 1ef429a..e1520f5 100644
--- a/viewsContainer.go
+++ b/viewsContainer.go
@@ -11,12 +11,16 @@ type ParentView interface {
type ViewsContainer interface {
View
ParentView
+
// Append appends a view to the end of the list of a view children
Append(view View)
+
// Insert inserts a view to the "index" position in the list of a view children
Insert(view View, index int)
+
// Remove removes a view from the list of a view children and return it
RemoveView(index int) View
+
// ViewIndex returns the index of view, -1 overwise
ViewIndex(view View) int
}
diff --git a/wasmBridge.go b/wasmBridge.go
index 8d14415..bc1902e 100644
--- a/wasmBridge.go
+++ b/wasmBridge.go
@@ -16,7 +16,7 @@ type wasmBridge struct {
closeEvent chan DataObject
}
-func createWasmBridge(close chan DataObject) webBridge {
+func createWasmBridge(close chan DataObject) bridge {
bridge := new(wasmBridge)
bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject)
@@ -102,10 +102,6 @@ func (bridge *wasmBridge) close() {
bridge.closeEvent <- NewDataObject("close")
}
-func (bridge *wasmBridge) readMessage() (string, bool) {
- return "", false
-}
-
func (bridge *wasmBridge) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
@@ -118,21 +114,24 @@ func (bridge *wasmBridge) writeMessage(script string) bool {
return true
}
-func (bridge *wasmBridge) addAnimationCSS(css string) {
+func (bridge *wasmBridge) prepareCSS(css string) string {
css = strings.ReplaceAll(css, `\t`, "\t")
css = strings.ReplaceAll(css, `\n`, "\n")
css = strings.ReplaceAll(css, `\'`, "'")
css = strings.ReplaceAll(css, `\"`, "\"")
css = strings.ReplaceAll(css, `\\`, "\\")
-
- styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
- content := styles.Get("textContent").String()
- styles.Set("textContent", content+"\n"+css)
+ return css
}
-func (bridge *wasmBridge) clearAnimation() {
+func (bridge *wasmBridge) appendAnimationCSS(css string) {
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
- styles.Set("textContent", "")
+ content := styles.Get("textContent").String()
+ styles.Set("textContent", content+"\n"+bridge.prepareCSS(css))
+}
+
+func (bridge *wasmBridge) setAnimationCSS(css string) {
+ styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
+ styles.Set("textContent", bridge.prepareCSS(css))
}
func (bridge *wasmBridge) canvasStart(htmlID string) {
@@ -276,3 +275,6 @@ func (bridge *wasmBridge) answerReceived(answer DataObject) {
func (bridge *wasmBridge) remoteAddr() string {
return "localhost"
}
+
+func (bridge *wasmBridge) sendResponse() {
+}
diff --git a/webBridge.go b/webBridge.go
index fee2412..004eeea 100644
--- a/webBridge.go
+++ b/webBridge.go
@@ -12,16 +12,30 @@ import (
"github.com/gorilla/websocket"
)
+type webBridge struct {
+ answer map[int]chan DataObject
+ answerID int
+ answerMutex sync.Mutex
+ writeMutex sync.Mutex
+ closed bool
+ canvasBuffer strings.Builder
+ canvasVarNumber int
+ updateScripts map[string]*strings.Builder
+ writeMessage func(string) bool
+ callFuncImmediately func(funcName string, args ...any) bool
+}
+
type wsBridge struct {
- conn *websocket.Conn
- answer map[int]chan DataObject
- answerID int
- answerMutex sync.Mutex
- closed bool
- buffer strings.Builder
- canvasBuffer strings.Builder
- canvasVarNumber int
- updateScripts map[string]*strings.Builder
+ webBridge
+ conn *websocket.Conn
+}
+
+type httpBridge struct {
+ webBridge
+ responseBuffer strings.Builder
+ response chan string
+ remoteAddress string
+ //conn *websocket.Conn
}
type canvasVar struct {
@@ -33,7 +47,7 @@ var upgrader = websocket.Upgrader{
WriteBufferSize: 8096,
}
-func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
+func createSocketBridge(w http.ResponseWriter, req *http.Request) *wsBridge {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
ErrorLog(err.Error())
@@ -41,41 +55,84 @@ func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
}
bridge := new(wsBridge)
- bridge.answerID = 1
- bridge.answer = make(map[int]chan DataObject)
+ bridge.initBridge()
bridge.conn = conn
- bridge.closed = false
- bridge.updateScripts = map[string]*strings.Builder{}
+ bridge.writeMessage = func(script string) bool {
+ if ProtocolInDebugLog {
+ DebugLog("🖥️ <- " + script)
+ }
+
+ if bridge.conn == nil {
+ ErrorLog("No connection")
+ return false
+ }
+
+ bridge.writeMutex.Lock()
+ err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
+ bridge.writeMutex.Unlock()
+
+ if err != nil {
+ ErrorLog(err.Error())
+ return false
+ }
+ return true
+ }
+ bridge.callFuncImmediately = bridge.callFunc
return bridge
}
-func (bridge *wsBridge) close() {
- bridge.closed = true
- bridge.conn.Close()
+func createHttpBridge(req *http.Request) *httpBridge {
+ bridge := new(httpBridge)
+ bridge.initBridge()
+ bridge.response = make(chan string, 10)
+ bridge.writeMessage = func(script string) bool {
+ if script != "" {
+ if ProtocolInDebugLog {
+ DebugLog(script)
+ }
+
+ if bridge.responseBuffer.Len() > 0 {
+ bridge.responseBuffer.WriteRune('\n')
+ }
+ bridge.responseBuffer.WriteString(script)
+ }
+ return true
+ }
+ bridge.callFuncImmediately = bridge.callImmediately
+ bridge.remoteAddress = req.RemoteAddr
+ return bridge
}
-func (bridge *wsBridge) startUpdateScript(htmlID string) bool {
+func (bridge *webBridge) initBridge() {
+ bridge.answerID = 1
+ bridge.answer = make(map[int]chan DataObject)
+ bridge.closed = false
+ bridge.updateScripts = map[string]*strings.Builder{}
+}
+
+func (bridge *webBridge) startUpdateScript(htmlID string) bool {
if _, ok := bridge.updateScripts[htmlID]; ok {
return false
}
buffer := allocStringBuilder()
bridge.updateScripts[htmlID] = buffer
- buffer.WriteString("var element = document.getElementById('")
+ buffer.WriteString("{\nlet element = document.getElementById('")
buffer.WriteString(htmlID)
buffer.WriteString("');\nif (element) {\n")
return true
}
-func (bridge *wsBridge) finishUpdateScript(htmlID string) {
+func (bridge *webBridge) finishUpdateScript(htmlID string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
- buffer.WriteString("scanElementsSize();\n}\n")
+ buffer.WriteString("scanElementsSize();\n}\n}\n")
bridge.writeMessage(buffer.String())
+
freeStringBuilder(buffer)
delete(bridge.updateScripts, htmlID)
}
}
-func (bridge *wsBridge) argToString(arg any) (string, bool) {
+func (bridge *webBridge) argToString(arg any) (string, bool) {
switch arg := arg.(type) {
case string:
arg = strings.ReplaceAll(arg, "\\", `\\`)
@@ -150,43 +207,44 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
return "", false
}
-func (bridge *wsBridge) callFunc(funcName string, args ...any) bool {
- bridge.buffer.Reset()
- bridge.buffer.WriteString(funcName)
- bridge.buffer.WriteRune('(')
+func (bridge *webBridge) callFuncScript(funcName string, args ...any) (string, bool) {
+ buffer := allocStringBuilder()
+ defer freeStringBuilder(buffer)
+
+ buffer.WriteString(funcName)
+ buffer.WriteRune('(')
for i, arg := range args {
argText, ok := bridge.argToString(arg)
if !ok {
- return false
+ return "", false
}
if i > 0 {
- bridge.buffer.WriteString(", ")
+ buffer.WriteString(", ")
}
- bridge.buffer.WriteString(argText)
+ buffer.WriteString(argText)
}
- bridge.buffer.WriteString(");")
+ buffer.WriteString(");")
- funcText := bridge.buffer.String()
- if ProtocolInDebugLog {
- DebugLog("Run func: " + funcText)
- }
- if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(funcText)); err != nil {
- ErrorLog(err.Error())
- return false
- }
- return true
+ return buffer.String(), true
}
-func (bridge *wsBridge) updateInnerHTML(htmlID, html string) {
+func (bridge *webBridge) callFunc(funcName string, args ...any) bool {
+ if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
+ return bridge.writeMessage(funcText)
+ }
+ return false
+}
+
+func (bridge *webBridge) updateInnerHTML(htmlID, html string) {
bridge.callFunc("updateInnerHTML", htmlID, html)
}
-func (bridge *wsBridge) appendToInnerHTML(htmlID, html string) {
+func (bridge *webBridge) appendToInnerHTML(htmlID, html string) {
bridge.callFunc("appendToInnerHTML", htmlID, html)
}
-func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
+func (bridge *webBridge) updateCSSProperty(htmlID, property, value string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`element.style['`)
buffer.WriteString(property)
@@ -198,7 +256,7 @@ func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
}
}
-func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
+func (bridge *webBridge) updateProperty(htmlID, property string, value any) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
if val, ok := bridge.argToString(value); ok {
buffer.WriteString(`element.setAttribute('`)
@@ -212,7 +270,7 @@ func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
}
}
-func (bridge *wsBridge) removeProperty(htmlID, property string) {
+func (bridge *webBridge) removeProperty(htmlID, property string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`if (element.hasAttribute('`)
buffer.WriteString(property)
@@ -224,28 +282,34 @@ func (bridge *wsBridge) removeProperty(htmlID, property string) {
}
}
-func (bridge *wsBridge) addAnimationCSS(css string) {
- bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
-if (styles) {
- styles.textContent += '` + css + `';
+func (bridge *webBridge) appendAnimationCSS(css string) {
+ //bridge.callFunc("appendAnimationCSS", css)
+ bridge.writeMessage(`{
+ let styles = document.getElementById('ruiAnimations');
+ if (styles) {
+ styles.textContent += '` + css + `';
+ }
}`)
}
-func (bridge *wsBridge) clearAnimation() {
- bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
-if (styles) {
- styles.textContent = '';
+func (bridge *webBridge) setAnimationCSS(css string) {
+ //bridge.callFunc("setAnimationCSS", css)
+ bridge.writeMessage(`{
+ let styles = document.getElementById('ruiAnimations');
+ if (styles) {
+ styles.textContent = '` + css + `';
+ }
}`)
}
-func (bridge *wsBridge) canvasStart(htmlID string) {
+func (bridge *webBridge) canvasStart(htmlID string) {
bridge.canvasBuffer.Reset()
- bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`)
+ bridge.canvasBuffer.WriteString("{\nconst ctx = getCanvasContext('")
bridge.canvasBuffer.WriteString(htmlID)
bridge.canvasBuffer.WriteString(`');`)
}
-func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
+func (bridge *webBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(funcName)
bridge.canvasBuffer.WriteRune('(')
@@ -259,7 +323,7 @@ func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");")
}
-func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
+func (bridge *webBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(property)
bridge.canvasBuffer.WriteString(" = ")
@@ -268,10 +332,10 @@ func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString(";")
}
-func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
+func (bridge *webBridge) createCanvasVar(funcName string, args ...any) any {
bridge.canvasVarNumber++
result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)}
- bridge.canvasBuffer.WriteString("\nvar ")
+ bridge.canvasBuffer.WriteString("\nlet ")
bridge.canvasBuffer.WriteString(result.name)
bridge.canvasBuffer.WriteString(" = ctx.")
bridge.canvasBuffer.WriteString(funcName)
@@ -287,7 +351,7 @@ func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
return result
}
-func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
+func (bridge *webBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
varName, ok := v.(canvasVar)
if !ok {
return
@@ -307,7 +371,7 @@ func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");")
}
-func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
+func (bridge *webBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nimg = images.get('")
bridge.canvasBuffer.WriteString(url)
@@ -328,51 +392,12 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
bridge.canvasBuffer.WriteString(");\n}")
}
-func (bridge *wsBridge) canvasFinish() {
- bridge.canvasBuffer.WriteString("\n")
- script := bridge.canvasBuffer.String()
- if ProtocolInDebugLog {
- DebugLog("Run script:")
- DebugLog(script)
- }
- if bridge.conn == nil {
- ErrorLog("No connection")
- } else if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
- ErrorLog(err.Error())
- }
+func (bridge *webBridge) canvasFinish() {
+ bridge.canvasBuffer.WriteString("\n}\n")
+ bridge.writeMessage(bridge.canvasBuffer.String())
}
-func (bridge *wsBridge) readMessage() (string, bool) {
- _, p, err := bridge.conn.ReadMessage()
- if err != nil {
- if !bridge.closed {
- ErrorLog(err.Error())
- }
- return "", false
- }
-
- return string(p), true
-}
-
-func (bridge *wsBridge) writeMessage(script string) bool {
- if ProtocolInDebugLog {
- DebugLog("Run script:")
- DebugLog(script)
- }
- if bridge.conn == nil {
- ErrorLog("No connection")
- return false
- }
- if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
- ErrorLog(err.Error())
- return false
- }
- return true
-}
-
-func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
- result := TextMetrics{}
-
+func (bridge *webBridge) remoteValue(funcName string, args ...any) (DataObject, bool) {
bridge.answerMutex.Lock()
answerID := bridge.answerID
bridge.answerID++
@@ -381,36 +406,36 @@ func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics
answer := make(chan DataObject)
bridge.answer[answerID] = answer
- if bridge.callFunc("canvasTextMetrics", answerID, htmlID, font, text) {
- data := <-answer
- result.Width = dataFloatProperty(data, "width")
+ funcArgs := append([]any{answerID}, args...)
+ var result DataObject = nil
+ ok := bridge.callFuncImmediately(funcName, funcArgs...)
+ if ok {
+ result = <-answer
}
+ close(answer)
delete(bridge.answer, answerID)
+ return result, true
+}
+
+func (bridge *webBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
+ result := TextMetrics{}
+ if data, ok := bridge.remoteValue("canvasTextMetrics", htmlID, font, text); ok {
+ result.Width = dataFloatProperty(data, "width")
+ }
return result
}
-func (bridge *wsBridge) htmlPropertyValue(htmlID, name string) string {
- bridge.answerMutex.Lock()
- answerID := bridge.answerID
- bridge.answerID++
- bridge.answerMutex.Unlock()
-
- answer := make(chan DataObject)
- bridge.answer[answerID] = answer
-
- if bridge.callFunc("getPropertyValue", answerID, htmlID, name) {
- data := <-answer
+func (bridge *webBridge) htmlPropertyValue(htmlID, name string) string {
+ if data, ok := bridge.remoteValue("getPropertyValue", htmlID, name); ok {
if value, ok := data.PropertyValue("value"); ok {
return value
}
}
-
- delete(bridge.answer, answerID)
return ""
}
-func (bridge *wsBridge) answerReceived(answer DataObject) {
+func (bridge *webBridge) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil {
if chanel, ok := bridge.answer[id]; ok {
@@ -427,6 +452,55 @@ func (bridge *wsBridge) answerReceived(answer DataObject) {
}
}
+func (bridge *wsBridge) close() {
+ bridge.closed = true
+ defer bridge.conn.Close()
+ bridge.conn = nil
+}
+
+func (bridge *wsBridge) readMessage() (string, bool) {
+ _, p, err := bridge.conn.ReadMessage()
+ if err != nil {
+ if !bridge.closed {
+ ErrorLog(err.Error())
+ }
+ return "", false
+ }
+
+ return string(p), true
+}
+
+func (bridge *wsBridge) sendResponse() {
+}
+
func (bridge *wsBridge) remoteAddr() string {
return bridge.conn.RemoteAddr().String()
}
+
+func (bridge *httpBridge) close() {
+ bridge.closed = true
+ // TODO
+}
+
+func (bridge *httpBridge) callImmediately(funcName string, args ...any) bool {
+ if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
+ if ProtocolInDebugLog {
+ DebugLog("Run func: " + funcText)
+ }
+ bridge.response <- funcText
+ return true
+ }
+ return false
+}
+
+func (bridge *httpBridge) sendResponse() {
+ bridge.writeMutex.Lock()
+ text := bridge.responseBuffer.String()
+ bridge.responseBuffer.Reset()
+ bridge.writeMutex.Unlock()
+ bridge.response <- text
+}
+
+func (bridge *httpBridge) remoteAddr() string {
+ return bridge.remoteAddress
+}