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(`</style> <style id="ruiAnimations"></style> - <script> -`) - buffer.WriteString(defaultScripts) - buffer.WriteString(addScripts) - buffer.WriteString(`</script> + <script src="/script.js"></script> </head> <body id="body" onkeydown="keyDownEvent(this, event)"> <div class="ruiRoot" id="ruiRootView"></div> 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(`<div style="display: grid;`) - listView.itemAlign(self, contentBuilder) + listView.itemAlign(contentBuilder) onDivBuilder := allocStringBuilder() defer freeStringBuilder(onDivBuilder) @@ -681,7 +686,7 @@ func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vChec return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String() } -func (listView *listViewData) checkboxItemDiv(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) string { +func (listView *listViewData) checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign int) string { itemStyleBuilder := allocStringBuilder() defer freeStringBuilder(itemStyleBuilder) @@ -760,15 +765,15 @@ func (listView *listViewData) currentInactiveStyle() string { return listView.itemStyle(CurrentInactiveStyle, "ruiListItemSelected") } -func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builder, checkbox int) { +func (listView *listViewData) checkboxSubviews(buffer *strings.Builder, checkbox int) { count := listView.adapter.ListSize() listViewID := listView.htmlID() hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView) vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView) - itemDiv := listView.checkboxItemDiv(self, checkbox, hCheckboxAlign, vCheckboxAlign) - onDiv, offDiv, contentDiv := listView.getDivs(self, checkbox, hCheckboxAlign, vCheckboxAlign) + itemDiv := listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign) + onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign) current := GetCurrent(listView) checkedItems := GetListViewCheckedItems(listView) @@ -784,7 +789,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde buffer.WriteString(listView.currentInactiveStyle()) } buffer.WriteString(`" onclick="listItemClickEvent(this, event)" data-left="0" data-top="0" data-width="0" data-height="0" style="display: grid; justify-items: stretch; align-items: stretch;`) - listView.itemSize(self, buffer) + listView.itemSize(buffer) if !listView.adapter.IsListItemEnabled(i) { buffer.WriteString(`" data-disabled="1`) } @@ -815,7 +820,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde } } -func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Builder) { +func (listView *listViewData) noneCheckboxSubviews(buffer *strings.Builder) { count := listView.adapter.ListSize() listViewID := listView.htmlID() @@ -824,8 +829,8 @@ func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Bu itemStyleBuilder.WriteString(`data-left="0" data-top="0" data-width="0" data-height="0" style="max-width: 100%; max-height: 100%; display: grid;`) - listView.itemAlign(self, itemStyleBuilder) - listView.itemSize(self, itemStyleBuilder) + listView.itemAlign(itemStyleBuilder) + listView.itemSize(itemStyleBuilder) itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`) itemStyle := itemStyleBuilder.String() @@ -865,12 +870,12 @@ func (listView *listViewData) updateCheckboxItem(index int, checked bool) { checkbox := GetListViewCheckbox(listView) hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView) vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView) - onDiv, offDiv, contentDiv := listView.getDivs(listView, checkbox, hCheckboxAlign, vCheckboxAlign) + onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign) buffer := allocStringBuilder() defer freeStringBuilder(buffer) - buffer.WriteString(listView.checkboxItemDiv(listView, checkbox, hCheckboxAlign, vCheckboxAlign)) + buffer.WriteString(listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign)) if checked { buffer.WriteString(onDiv) } else { @@ -1061,9 +1066,9 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) { checkbox := GetListViewCheckbox(listView) if checkbox == NoneCheckbox { - listView.noneCheckboxSubviews(self, buffer) + listView.noneCheckboxSubviews(buffer) } else { - listView.checkboxSubviews(self, buffer, checkbox) + listView.checkboxSubviews(buffer, checkbox) } buffer.WriteString(`</div>`) 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 +}