Added drag-and-drop of files

This commit is contained in:
Alexei Anoshenko 2025-06-11 14:20:41 +03:00
parent b76e3e56d8
commit 4f07435637
23 changed files with 206 additions and 124 deletions

View File

@ -243,7 +243,7 @@ func parseAnimation(obj DataObject) AnimationProperty {
animation := new(animationData) animation := new(animationData)
animation.init() animation.init()
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if node := obj.Property(i); node != nil { if node := obj.Property(i); node != nil {
tag := PropertyName(node.Tag()) tag := PropertyName(node.Tag())
if node.Type() == TextNode { if node.Type() == TextNode {
@ -491,7 +491,7 @@ func animationSet(properties Properties, tag PropertyName, value any) []Property
case DataNode: case DataNode:
parseObject := func(obj DataObject) (AnimatedProperty, bool) { parseObject := func(obj DataObject) (AnimatedProperty, bool) {
result := AnimatedProperty{} result := AnimatedProperty{}
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if node := obj.Property(i); node.Type() == TextNode { if node := obj.Property(i); node.Type() == TextNode {
propTag := strings.ToLower(node.Tag()) propTag := strings.ToLower(node.Tag())
switch propTag { switch propTag {
@ -1050,7 +1050,7 @@ func setAnimationProperty(properties Properties, tag PropertyName, value any) bo
case DataNode: case DataNode:
animations := []AnimationProperty{} animations := []AnimationProperty{}
result := true result := true
for i := 0; i < value.ArraySize(); i++ { for i := range value.ArraySize() {
if obj := value.ArrayElement(i).Object(); obj != nil { if obj := value.ArrayElement(i).Object(); obj != nil {
if anim := parseAnimation(obj); anim.hasAnimatedProperty() { if anim := parseAnimation(obj); anim.hasAnimatedProperty() {
animations = append(animations, anim) animations = append(animations, anim)

View File

@ -1008,20 +1008,26 @@ function setInputValue(elementId, text) {
} }
} }
function filesTextForMessage(files) {
let message = "files=[";
for(let i = 0; i < files.length; i++) {
if (i > 0) {
message += ",";
}
message += "_{name=\"" + files[i].name +
"\",last-modified=" + files[i].lastModified +
",size=" + files[i].size +
",mime-type=\"" + files[i].type + "\"}";
}
message += "]";
return message
}
function fileSelectedEvent(element) { function fileSelectedEvent(element) {
const files = element.files; const files = element.files;
if (files) { if (files) {
let message = "fileSelected{session=" + sessionID + ",id=" + element.id + ",files=["; let message = "fileSelected{session=" + sessionID + ",id=" + element.id + "," + filesTextForMessage(files) + "}";
for(let i = 0; i < files.length; i++) { sendMessage(message);
if (i > 0) {
message += ",";
}
message += "_{name=\"" + files[i].name +
"\",last-modified=" + files[i].lastModified +
",size=" + files[i].size +
",mime-type=\"" + files[i].type + "\"}";
}
sendMessage(message + "]}");
} }
} }
@ -2162,19 +2168,18 @@ function dragAndDropEvent(element, event, tag) {
message += ',data="' + dataText + '"'; message += ',data="' + dataText + '"';
} }
dataText = "" const files = event.dataTransfer.files
for (const file of event.dataTransfer.files) { if (files && files.length > 0) {
if (dataText != "") { message += "," + filesTextForMessage(files)
dataText += ";"; element["dragFiles"] = files;
} } else {
dataText += file.name; element["dragFiles"] = null;
}
if (dataText != "") {
message += ',files="' + dataText + '"';
} }
if (event.dataTransfer.effectAllowed && event.dataTransfer.effectAllowed != "uninitialized") { if (event.dataTransfer.effectAllowed && event.dataTransfer.effectAllowed != "uninitialized") {
message += ',effect-allowed="' + event.dataTransfer.effectAllowed + '"'; message += ',effect-allowed="' + event.dataTransfer.effectAllowed + '"';
} }
if (event.dataTransfer.dropEffect) { if (event.dataTransfer.dropEffect) {
message += ',drop-effect="' + event.dataTransfer.dropEffect + '"'; message += ',drop-effect="' + event.dataTransfer.dropEffect + '"';
} }
@ -2251,3 +2256,34 @@ function dropEvent(element, event) {
event.preventDefault(); event.preventDefault();
dragAndDropEvent(element, event, "drop-event") dragAndDropEvent(element, event, "drop-event")
} }
function loadDropFile(elementId, name, size) {
const element = document.getElementById(elementId);
if (element) {
const files = element["dragFiles"];
if (files) {
for(let i = 0; i < files.length; i++) {
const file = files[i]
if (file.name == name && file.size == size) {
const reader = new FileReader();
reader.onload = function() {
sendMessage("fileLoaded{session=" + sessionID + ",id=" + element.id +
",name=`" + name +
"`,size=" + size +
",last-modified=" + file.lastModified +
",mime-type=\"" + file.type +
"\",data=`" + reader.result + "`}");
}
reader.onerror = function(error) {
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=\"" + name + "\",size=" + size + ",error=`" + error + "`}");
}
reader.readAsDataURL(file);
return
}
}
}
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=`" + name + "`,size=" + size + ",error=`File not found`}");
} else {
sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=`" + name + "`,size=" + size + ",error=`Invalid View id`}");
}
}

View File

@ -67,7 +67,7 @@ func createBackground(obj DataObject) BackgroundElement {
} }
count := obj.PropertyCount() count := obj.PropertyCount()
for i := 0; i < count; i++ { for i := range count {
if node := obj.Property(i); node.Type() == TextNode { if node := obj.Property(i); node.Type() == TextNode {
if value := node.Text(); value != "" { if value := node.Text(); value != "" {
result.Set(PropertyName(node.Tag()), value) result.Set(PropertyName(node.Tag()), value)

View File

@ -279,7 +279,7 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
} }
buffer.WriteString("ellipse ") buffer.WriteString("ellipse ")
shapeText = "" shapeText = ""
for i := 0; i < count; i++ { for i := range count {
buffer.WriteString(value[i].cssString("50%", session)) buffer.WriteString(value[i].cssString("50%", session))
buffer.WriteString(" ") buffer.WriteString(" ")
} }
@ -291,7 +291,7 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
} }
buffer.WriteString("ellipse ") buffer.WriteString("ellipse ")
shapeText = "" shapeText = ""
for i := 0; i < count; i++ { for i := range count {
if value[i] != nil { if value[i] != nil {
switch value := value[i].(type) { switch value := value[i].(type) {
case SizeUnit: case SizeUnit:

View File

@ -433,7 +433,7 @@ func (border *borderProperty) String() string {
func (border *borderProperty) setBorderObject(obj DataObject) bool { func (border *borderProperty) setBorderObject(obj DataObject) bool {
result := true result := true
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if node := obj.Property(i); node != nil { if node := obj.Property(i); node != nil {
tag := PropertyName(node.Tag()) tag := PropertyName(node.Tag())
switch node.Type() { switch node.Type() {

View File

@ -346,3 +346,9 @@ func (customView *CustomViewData) SetTransition(tag PropertyName, animation Anim
customView.superView.SetTransition(tag, animation) customView.superView.SetTransition(tag, animation)
} }
} }
func (customView *CustomViewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if customView.superView != nil {
customView.superView.LoadFile(file, result)
}
}

View File

@ -222,7 +222,7 @@ func (object *dataObject) ToParams() Params {
case ArrayNode: case ArrayNode:
array := []any{} array := []any{}
for i := 0; i < node.ArraySize(); i++ { for i := range node.ArraySize() {
if data := node.ArrayElement(i); data != nil { if data := node.ArrayElement(i); data != nil {
if data.IsObject() { if data.IsObject() {
if obj := data.Object(); obj != nil { if obj := data.Object(); obj != nil {
@ -465,7 +465,7 @@ func ParseDataText(text string) DataObject {
return invalidEscape() return invalidEscape()
} }
x := 0 x := 0
for i := 0; i < 2; i++ { for range 2 {
ch := data[n2] ch := data[n2]
if ch >= '0' && ch <= '9' { if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0') x = x*16 + int(ch-'0')
@ -485,7 +485,7 @@ func ParseDataText(text string) DataObject {
return invalidEscape() return invalidEscape()
} }
x := 0 x := 0
for i := 0; i < 4; i++ { for range 4 {
ch := data[n2] ch := data[n2]
if ch >= '0' && ch <= '9' { if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0') x = x*16 + int(ch-'0')

View File

@ -164,7 +164,7 @@ const (
type DragAndDropEvent struct { type DragAndDropEvent struct {
MouseEvent MouseEvent
Data map[string]string Data map[string]string
Files string Files []FileInfo
Target View Target View
EffectAllowed int EffectAllowed int
DropEffect int DropEffect int
@ -216,7 +216,7 @@ func (event *DragAndDropEvent) init(session Session, data DataObject) {
} }
} }
// TODO files event.Files = parseFilesTag(data)
} }
func stringToDropEffect(text string) (int, bool) { func stringToDropEffect(text string) (int, bool) {
@ -436,6 +436,13 @@ func dragAndDropHtml(view View, buffer *strings.Builder) {
} }
} }
func (view *viewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result != nil {
view.fileLoader[file.key()] = result
view.Session().callFunc("loadDropFile", view.htmlID(), file.Name, file.Size)
}
}
// GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned. // GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) {

View File

@ -1,7 +1,7 @@
package rui package rui
import ( import (
"encoding/base64" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -67,6 +67,8 @@ type FileInfo struct {
// MimeType - the file's MIME type. // MimeType - the file's MIME type.
MimeType string MimeType string
data []byte
} }
// FilePicker represents the FilePicker view // FilePicker represents the FilePicker view
@ -82,11 +84,12 @@ type FilePicker interface {
type filePickerData struct { type filePickerData struct {
viewData viewData
files []FileInfo files []FileInfo
loader map[int]func(FileInfo, []byte) //loader map[int]func(FileInfo, []byte)
} }
func (file *FileInfo) initBy(node DataValue) { func dataToFileInfo(node DataValue) FileInfo {
var file FileInfo
if obj := node.Object(); obj != nil { if obj := node.Object(); obj != nil {
file.Name, _ = obj.PropertyValue("name") file.Name, _ = obj.PropertyValue("name")
file.MimeType, _ = obj.PropertyValue("mime-type") file.MimeType, _ = obj.PropertyValue("mime-type")
@ -103,6 +106,11 @@ func (file *FileInfo) initBy(node DataValue) {
} }
} }
} }
return file
}
func (file FileInfo) key() string {
return fmt.Sprintf("%s:%d", file.Name, int(file.Size))
} }
// NewFilePicker create new FilePicker object and return it // NewFilePicker create new FilePicker object and return it
@ -122,7 +130,7 @@ func (picker *filePickerData) init(session Session) {
picker.tag = "FilePicker" picker.tag = "FilePicker"
picker.hasHtmlDisabled = true picker.hasHtmlDisabled = true
picker.files = []FileInfo{} picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){} //picker.loader = map[int]func(FileInfo, []byte){}
picker.set = picker.setFunc picker.set = picker.setFunc
picker.changed = picker.propertyChanged picker.changed = picker.propertyChanged
@ -137,16 +145,23 @@ func (picker *filePickerData) Files() []FileInfo {
} }
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result == nil { if result != nil {
return for i, info := range picker.files {
} if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified {
if info.data != nil {
for i, info := range picker.files { result(info, info.data)
if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified { } else {
picker.loader[i] = result picker.fileLoader[info.key()] = func(file FileInfo, data []byte) {
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i) picker.files[i].data = data
return result(file, data)
}
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
}
return
}
} }
picker.viewData.LoadFile(file, result)
} }
} }
@ -248,65 +263,30 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
} }
} }
func parseFilesTag(data DataObject) []FileInfo {
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := range count {
if value := node.ArrayElement(i); value != nil {
files[i] = dataToFileInfo(value)
}
}
return files
}
return nil
}
func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool { func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command { switch command {
case "fileSelected": case "fileSelected":
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode { if files := parseFilesTag(data); files != nil {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := 0; i < count; i++ {
if value := node.ArrayElement(i); value != nil {
files[i].initBy(value)
}
}
picker.files = files picker.files = files
for _, listener := range GetFileSelectedListeners(picker) { for _, listener := range GetFileSelectedListeners(picker) {
listener(picker, files) listener(picker, files)
} }
} }
return true return true
case "fileLoaded":
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
var file FileInfo
file.initBy(data)
var fileData []byte = nil
if base64Data, ok := data.PropertyValue("data"); ok {
if index := strings.LastIndex(base64Data, ","); index >= 0 {
base64Data = base64Data[index+1:]
}
decode, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
fileData = decode
} else {
ErrorLog(err.Error())
}
}
result(file, fileData)
delete(picker.loader, index)
}
}
return true
case "fileLoadingError":
if error, ok := data.PropertyValue("error"); ok {
ErrorLog(error)
}
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
if index >= 0 && index < len(picker.files) {
result(picker.files[index], nil)
} else {
result(FileInfo{}, nil)
}
delete(picker.loader, index)
}
}
return true
} }
return picker.viewData.handleCommand(self, command, data) return picker.viewData.handleCommand(self, command, data)
@ -354,7 +334,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
} }
if ok { if ok {
result := strings.Split(accept, ",") result := strings.Split(accept, ",")
for i := 0; i < len(result); i++ { for i := range len(result) {
result[i] = strings.Trim(result[i], " \t\n") result[i] = strings.Trim(result[i], " \t\n")
} }
return result return result

View File

@ -159,7 +159,7 @@ func NewFilterProperty(params Params) FilterProperty {
func newFilterProperty(obj DataObject) FilterProperty { func newFilterProperty(obj DataObject) FilterProperty {
filter := new(filterData) filter := new(filterData)
filter.init() filter.init()
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if node := obj.Property(i); node != nil { if node := obj.Property(i); node != nil {
tag := node.Tag() tag := node.Tag()
switch node.Type() { switch node.Type() {

View File

@ -176,8 +176,7 @@ func (listLayout *listLayoutData) createContent() bool {
htmlID := listLayout.htmlID() htmlID := listLayout.htmlID()
isDisabled := IsDisabled(listLayout) isDisabled := IsDisabled(listLayout)
count := adapter.ListSize() for i := range adapter.ListSize() {
for i := 0; i < count; i++ {
if view := adapter.ListItem(i, session); view != nil { if view := adapter.ListItem(i, session); view != nil {
view.setParentID(htmlID) view.setParentID(htmlID)
if isDisabled { if isDisabled {

View File

@ -428,7 +428,7 @@ func (listView *listViewData) ReloadListViewData() {
listView.itemFrame = make([]Frame, itemCount) listView.itemFrame = make([]Frame, itemCount)
} }
for i := 0; i < itemCount; i++ { for i := range itemCount {
listView.items[i] = adapter.ListItem(i, listView.Session()) listView.items[i] = adapter.ListItem(i, listView.Session())
} }
} else if len(listView.items) > 0 { } else if len(listView.items) > 0 {
@ -632,7 +632,7 @@ func (listView *listViewData) checkboxSubviews(adapter ListAdapter, buffer *stri
current := GetCurrent(listView) current := GetCurrent(listView)
checkedItems := GetListViewCheckedItems(listView) checkedItems := GetListViewCheckedItems(listView)
for i := 0; i < count; i++ { for i := range count {
buffer.WriteString(`<div id="`) buffer.WriteString(`<div id="`)
buffer.WriteString(listViewID) buffer.WriteString(listViewID)
buffer.WriteRune('-') buffer.WriteRune('-')
@ -693,7 +693,7 @@ func (listView *listViewData) noneCheckboxSubviews(adapter ListAdapter, buffer *
itemStyle := itemStyleBuilder.String() itemStyle := itemStyleBuilder.String()
current := GetCurrent(listView) current := GetCurrent(listView)
for i := 0; i < count; i++ { for i := range count {
buffer.WriteString(`<div id="`) buffer.WriteString(`<div id="`)
buffer.WriteString(listViewID) buffer.WriteString(listViewID)
buffer.WriteRune('-') buffer.WriteRune('-')

View File

@ -740,7 +740,7 @@ func (popup *popupData) init(view View, popupParams Params) {
popupCellHeight = append(popupCellHeight, AutoSize()) popupCellHeight = append(popupCellHeight, AutoSize())
gap, _ := sizeConstant(session, "ruiPopupButtonGap") gap, _ := sizeConstant(session, "ruiPopupButtonGap")
cellWidth := []SizeUnit{} cellWidth := []SizeUnit{}
for i := 0; i < buttonCount; i++ { for range buttonCount {
cellWidth = append(cellWidth, Fr(1)) cellWidth = append(cellWidth, Fr(1))
} }

View File

@ -121,7 +121,7 @@ func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
func parseProperties(properties Properties, object DataObject) { func parseProperties(properties Properties, object DataObject) {
count := object.PropertyCount() count := object.PropertyCount()
for i := 0; i < count; i++ { for i := range count {
if node := object.Property(i); node != nil { if node := object.Property(i); node != nil {
switch node.Type() { switch node.Type() {
case TextNode: case TextNode:

View File

@ -734,7 +734,7 @@ func (session *sessionData) handleSessionInfo(params DataObject) {
if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode { if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode {
if obj := node.Object(); obj != nil { if obj := node.Object(); obj != nil {
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if element := obj.Property(i); element.Type() == TextNode { if element := obj.Property(i); element.Type() == TextNode {
session.clientStorage[element.Tag()] = element.Text() session.clientStorage[element.Tag()] = element.Text()
} }

View File

@ -61,7 +61,7 @@ func loadStringResources(text string) {
table = map[string]string{} table = map[string]string{}
} }
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if prop := obj.Property(i); prop != nil && prop.Type() == TextNode { if prop := obj.Property(i); prop != nil && prop.Type() == TextNode {
table[prop.Tag()] = prop.Text() table[prop.Tag()] = prop.Text()
} }
@ -72,7 +72,7 @@ func loadStringResources(text string) {
tag := data.Tag() tag := data.Tag()
if tag == "strings" { if tag == "strings" {
for i := 0; i < data.PropertyCount(); i++ { for i := range data.PropertyCount() {
if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode { if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode {
parseStrings(prop.Object(), prop.Tag()) parseStrings(prop.Object(), prop.Tag())
} }

View File

@ -739,7 +739,7 @@ func (table *tableViewData) setFunc(tag PropertyName, value any) []PropertyName
case DataObject: case DataObject:
params := Params{} params := Params{}
for k := 0; k < value.PropertyCount(); k++ { for k := range value.PropertyCount() {
if prop := value.Property(k); prop != nil && prop.Type() == TextNode { if prop := value.Property(k); prop != nil && prop.Type() == TextNode {
params[PropertyName(prop.Tag())] = prop.Text() params[PropertyName(prop.Tag())] = prop.Text()
} }
@ -797,7 +797,7 @@ func (table *tableViewData) setFunc(tag PropertyName, value any) []PropertyName
if strings.Contains(value, ",") { if strings.Contains(value, ",") {
if values := strings.Split(value, ","); len(values) == 2 { if values := strings.Split(value, ","); len(values) == 2 {
var n = []int{0, 0} var n = []int{0, 0}
for i := 0; i < 2; i++ { for i := range 2 {
var err error var err error
if n[i], err = strconv.Atoi(values[i]); err != nil { if n[i], err = strconv.Atoi(values[i]); err != nil {
ErrorLog(err.Error()) ErrorLog(err.Error())

View File

@ -695,7 +695,7 @@ func (theme *theme) addText(themeText string) bool {
objToStyle := func(obj DataObject) ViewStyle { objToStyle := func(obj DataObject) ViewStyle {
params := Params{} params := Params{}
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if node := obj.Property(i); node != nil { if node := obj.Property(i); node != nil {
switch node.Type() { switch node.Type() {
case ArrayNode: case ArrayNode:
@ -712,14 +712,14 @@ func (theme *theme) addText(themeText string) bool {
return NewViewStyle(params) return NewViewStyle(params)
} }
for i := 0; i < count; i++ { for i := range count {
if d := data.Property(i); d != nil { if d := data.Property(i); d != nil {
switch tag := d.Tag(); tag { switch tag := d.Tag(); tag {
case "constants": case "constants":
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.constants[prop.Tag()] = prop.Text() theme.constants[prop.Tag()] = prop.Text()
} }
@ -731,7 +731,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.touchConstants[prop.Tag()] = prop.Text() theme.touchConstants[prop.Tag()] = prop.Text()
} }
@ -743,7 +743,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.colors[prop.Tag()] = prop.Text() theme.colors[prop.Tag()] = prop.Text()
} }
@ -755,7 +755,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.darkColors[prop.Tag()] = prop.Text() theme.darkColors[prop.Tag()] = prop.Text()
} }
@ -767,7 +767,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.images[prop.Tag()] = prop.Text() theme.images[prop.Tag()] = prop.Text()
} }
@ -779,7 +779,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ObjectNode { if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil { if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount() objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ { for k := range objCount {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.darkImages[prop.Tag()] = prop.Text() theme.darkImages[prop.Tag()] = prop.Text()
} }
@ -790,7 +790,7 @@ func (theme *theme) addText(themeText string) bool {
case "styles": case "styles":
if d.Type() == ArrayNode { if d.Type() == ArrayNode {
arraySize := d.ArraySize() arraySize := d.ArraySize()
for k := 0; k < arraySize; k++ { for k := range arraySize {
if element := d.ArrayElement(k); element != nil && element.IsObject() { if element := d.ArrayElement(k); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil { if obj := element.Object(); obj != nil {
theme.styles[obj.Tag()] = objToStyle(obj) theme.styles[obj.Tag()] = objToStyle(obj)
@ -803,7 +803,7 @@ func (theme *theme) addText(themeText string) bool {
if d.Type() == ArrayNode && strings.HasPrefix(tag, "styles:") { if d.Type() == ArrayNode && strings.HasPrefix(tag, "styles:") {
if rule, ok := parseMediaRule(tag); ok { if rule, ok := parseMediaRule(tag); ok {
arraySize := d.ArraySize() arraySize := d.ArraySize()
for k := 0; k < arraySize; k++ { for k := range arraySize {
if element := d.ArrayElement(k); element != nil && element.IsObject() { if element := d.ArrayElement(k); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil { if obj := element.Object(); obj != nil {
rule.styles[obj.Tag()] = objToStyle(obj) rule.styles[obj.Tag()] = objToStyle(obj)

View File

@ -155,7 +155,7 @@ func (event *TouchEvent) init(data DataObject) {
event.Touches = []Touch{} event.Touches = []Touch{}
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
if node := data.PropertyByTag("touches"); node != nil && node.Type() == ArrayNode { if node := data.PropertyByTag("touches"); node != nil && node.Type() == ArrayNode {
for i := 0; i < node.ArraySize(); i++ { for i := range node.ArraySize() {
if element := node.ArrayElement(i); element != nil && element.IsObject() { if element := node.ArrayElement(i); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil { if obj := element.Object(); obj != nil {
var touch Touch var touch Touch

View File

@ -369,7 +369,7 @@ func valueToTransformProperty(value any) TransformProperty {
parseObject := func(obj DataObject) TransformProperty { parseObject := func(obj DataObject) TransformProperty {
transform := NewTransformProperty(nil) transform := NewTransformProperty(nil)
ok := true ok := true
for i := 0; i < obj.PropertyCount(); i++ { for i := range obj.PropertyCount() {
if prop := obj.Property(i); prop.Type() == TextNode { if prop := obj.Property(i); prop.Type() == TextNode {
if !transform.Set(PropertyName(prop.Tag()), prop.Text()) { if !transform.Set(PropertyName(prop.Tag()), prop.Text()) {
ok = false ok = false

View File

@ -105,3 +105,12 @@ func InlineImageFromResource(filename string) (string, bool) {
return "", false return "", false
} }
// InlineFileFromData creates inline image from the image data and mimetype.
func InlineFileFromData(data []byte, mimeType string) string {
if data == nil {
return ""
}
return "data:" + mimeType + ";base64," + base64.StdEncoding.EncodeToString(data)
}

45
view.go
View File

@ -1,6 +1,7 @@
package rui package rui
import ( import (
"encoding/base64"
"fmt" "fmt"
"maps" "maps"
"strconv" "strconv"
@ -71,6 +72,12 @@ type View interface {
// HasFocus returns 'true' if the view has focus // HasFocus returns 'true' if the view has focus
HasFocus() bool HasFocus() bool
// LoadFile loads the content of the dropped file by drag-and-drop mechanism for all views except FilePicker.
// The selected file is loaded for FilePicker view.
//
// This function is asynchronous. The "result" function will be called after loading the data.
LoadFile(file FileInfo, result func(FileInfo, []byte))
init(session Session) init(session Session)
handleCommand(self View, command PropertyName, data DataObject) bool handleCommand(self View, command PropertyName, data DataObject) bool
htmlClass(disabled bool) string htmlClass(disabled bool) string
@ -115,6 +122,7 @@ type viewData struct {
set func(tag PropertyName, value any) []PropertyName set func(tag PropertyName, value any) []PropertyName
remove func(tag PropertyName) []PropertyName remove func(tag PropertyName) []PropertyName
changed func(tag PropertyName) changed func(tag PropertyName)
fileLoader map[string]func(FileInfo, []byte)
} }
func newView(session Session) View { func newView(session Session) View {
@ -160,6 +168,7 @@ func (view *viewData) init(session Session) {
view.noResizeEvent = false view.noResizeEvent = false
view.created = false view.created = false
view.hasHtmlDisabled = false view.hasHtmlDisabled = false
view.fileLoader = map[string]func(FileInfo, []byte){}
} }
func (view *viewData) Session() Session { func (view *viewData) Session() Session {
@ -1179,6 +1188,42 @@ func (view *viewData) handleCommand(self View, command PropertyName, data DataOb
self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height")) self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height"))
return true return true
*/ */
case "fileLoaded":
file := dataToFileInfo(data)
key := file.key()
if listener := view.fileLoader[key]; listener != nil {
delete(view.fileLoader, key)
if base64Data, ok := data.PropertyValue("data"); ok {
if index := strings.LastIndex(base64Data, ","); index >= 0 {
base64Data = base64Data[index+1:]
}
decode, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
listener(file, decode)
} else {
ErrorLog(err.Error())
}
}
}
return true
case "fileLoadingError":
file := dataToFileInfo(data)
key := file.key()
if error, ok := data.PropertyValue("error"); ok {
ErrorLogF(`Load "%s" file error: %s`, file.Name, error)
}
if listener := view.fileLoader[key]; listener != nil {
delete(view.fileLoader, key)
listener(file, nil)
}
return true
default: default:
return false return false
} }

View File

@ -64,7 +64,7 @@ func setTransitionProperty(properties Properties, value any) bool {
return false return false
case ArrayNode: case ArrayNode:
for i := 0; i < value.ArraySize(); i++ { for i := range value.ArraySize() {
if obj := value.ArrayElement(i).Object(); obj != nil { if obj := value.ArrayElement(i).Object(); obj != nil {
if !setObject(obj) { if !setObject(obj) {
return false return false
@ -133,7 +133,7 @@ func setTransitionProperty(properties Properties, value any) bool {
case ArrayNode: case ArrayNode:
result := true result := true
for i := 0; i < value.ArraySize(); i++ { for i := range value.ArraySize() {
if obj := value.ArrayElement(i).Object(); obj != nil { if obj := value.ArrayElement(i).Object(); obj != nil {
result = setObject(obj) && result result = setObject(obj) && result
} else { } else {