rui_orig/filePicker.go

426 lines
12 KiB
Go

package rui
import (
"encoding/base64"
"strconv"
"strings"
"time"
)
// Constants for [FilePicker] specific properties and events
const (
// FileSelectedEvent is the constant for "file-selected-event" property tag.
//
// Used by `FilePicker`.
// Fired when user selects file(s).
//
// General listener format:
// `func(picker rui.FilePicker, files []rui.FileInfo)`.
//
// where:
// picker - Interface of a file picker which generated this event,
// files - Array of description of selected files.
//
// Allowed listener formats:
// `func(picker rui.FilePicker)`,
// `func(files []rui.FileInfo)`,
// `func()`.
FileSelectedEvent = "file-selected-event"
// Accept is the constant for "accept" property tag.
//
// Used by `FilePicker`.
// Set the list of allowed file extensions or MIME types.
//
// Supported types: `string`, `[]string`.
//
// Internal type is `string`, other types converted to it during assignment.
//
// Conversion rules:
// `string` - may contain single value of multiple separated by comma(`,`).
// `[]string` - an array of acceptable file extensions or MIME types.
Accept = "accept"
// Multiple is the constant for "multiple" property tag.
//
// Used by `FilePicker`.
// Controls whether multiple files can be selected.
//
// Supported types: `bool`, `int`, `string`.
//
// Values:
// `true` or `1` or "true", "yes", "on", "1" - Several files can be selected.
// `false` or `0` or "false", "no", "off", "0" - Only one file can be selected.
Multiple = "multiple"
)
// FileInfo describes a file which selected in the FilePicker view
type FileInfo struct {
// Name - the file's name.
Name string
// LastModified specifying the date and time at which the file was last modified
LastModified time.Time
// Size - the size of the file in bytes.
Size int64
// MimeType - the file's MIME type.
MimeType string
}
// FilePicker represents the FilePicker view
type FilePicker interface {
View
// Files returns the list of selected files.
// If there are no files selected then an empty slice is returned (the result is always not nil)
Files() []FileInfo
// LoadFile loads the content of the selected file. This function is asynchronous.
// The "result" function will be called after loading the data.
LoadFile(file FileInfo, result func(FileInfo, []byte))
}
type filePickerData struct {
viewData
files []FileInfo
fileSelectedListeners []func(FilePicker, []FileInfo)
loader map[int]func(FileInfo, []byte)
}
func (file *FileInfo) initBy(node DataValue) {
if obj := node.Object(); obj != nil {
file.Name, _ = obj.PropertyValue("name")
file.MimeType, _ = obj.PropertyValue("mime-type")
if size, ok := obj.PropertyValue("size"); ok {
if n, err := strconv.ParseInt(size, 10, 64); err == nil {
file.Size = n
}
}
if value, ok := obj.PropertyValue("last-modified"); ok {
if n, err := strconv.ParseInt(value, 10, 64); err == nil {
file.LastModified = time.UnixMilli(n)
}
}
}
}
// NewFilePicker create new FilePicker object and return it
func NewFilePicker(session Session, params Params) FilePicker {
view := new(filePickerData)
view.init(session)
setInitParams(view, params)
return view
}
func newFilePicker(session Session) View {
return NewFilePicker(session, nil)
}
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){}
}
func (picker *filePickerData) String() string {
return getViewString(picker, nil)
}
func (picker *filePickerData) Focusable() bool {
return true
}
func (picker *filePickerData) Files() []FileInfo {
return picker.files
}
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result == nil {
return
}
for i, info := range picker.files {
if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified {
picker.loader[i] = result
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
return
}
}
}
func (picker *filePickerData) Remove(tag string) {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
switch tag {
case FileSelectedEvent:
if len(picker.fileSelectedListeners) > 0 {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
picker.propertyChangedEvent(tag)
}
case Accept:
delete(picker.properties, tag)
if picker.created {
picker.session.removeProperty(picker.htmlID(), "accept")
}
picker.propertyChangedEvent(tag)
default:
picker.viewData.remove(tag)
}
}
func (picker *filePickerData) Set(tag string, value any) bool {
return picker.set(strings.ToLower(tag), value)
}
func (picker *filePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case FileSelectedEvent:
listeners, ok := valueToEventListeners[FilePicker, []FileInfo](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(FilePicker, []FileInfo){}
}
picker.fileSelectedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case Accept:
switch value := value.(type) {
case string:
value = strings.Trim(value, " \t\n")
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
case []string:
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, val := range value {
val = strings.Trim(val, " \t\n")
if val != "" {
if buffer.Len() > 0 {
buffer.WriteRune(',')
}
buffer.WriteString(val)
}
}
if buffer.Len() == 0 {
picker.remove(Accept)
} else {
picker.properties[Accept] = buffer.String()
}
default:
notCompatibleType(tag, value)
return false
}
if picker.created {
if css := picker.acceptCSS(); css != "" {
picker.session.updateProperty(picker.htmlID(), "accept", css)
} else {
picker.session.removeProperty(picker.htmlID(), "accept")
}
}
picker.propertyChangedEvent(tag)
return true
default:
return picker.viewData.set(tag, value)
}
}
func (picker *filePickerData) htmlTag() string {
return "input"
}
func (picker *filePickerData) acceptCSS() string {
accept, ok := stringProperty(picker, Accept, picker.Session())
if !ok {
if value := valueFromStyle(picker, Accept); value != nil {
accept, ok = value.(string)
}
}
if ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, value := range strings.Split(accept, ",") {
if value = strings.Trim(value, " \t\n"); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
if value[0] != '.' && !strings.Contains(value, "/") {
buffer.WriteRune('.')
}
buffer.WriteString(value)
}
}
return buffer.String()
}
return ""
}
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
if accept := picker.acceptCSS(); accept != "" {
buffer.WriteString(` accept="`)
buffer.WriteString(accept)
buffer.WriteRune('"')
}
buffer.WriteString(` type="file"`)
if IsMultipleFilePicker(picker) {
buffer.WriteString(` multiple`)
}
buffer.WriteString(` oninput="fileSelectedEvent(this)"`)
if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
}
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "fileSelected":
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
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
for _, listener := range picker.fileSelectedListeners {
listener(picker, files)
}
}
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)
}
// GetFilePickerFiles returns the list of FilePicker selected files
// If there are no files selected then an empty slice is returned (the result is always not nil)
// If the second argument (subviewID) is not specified or it is "" then selected files of the first argument (view) is returned
func GetFilePickerFiles(view View, subviewID ...string) []FileInfo {
subview := ""
if len(subviewID) > 0 {
subview = subviewID[0]
}
if picker := FilePickerByID(view, subview); picker != nil {
return picker.Files()
}
return []FileInfo{}
}
// LoadFilePickerFile loads the content of the selected file. This function is asynchronous.
// The "result" function will be called after loading the data.
// If the second argument (subviewID) is "" then the file from the first argument (view) is loaded
func LoadFilePickerFile(view View, subviewID string, file FileInfo, result func(FileInfo, []byte)) {
if picker := FilePickerByID(view, subviewID); picker != nil {
picker.LoadFile(file, result)
}
}
// IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func IsMultipleFilePicker(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Multiple, false)
}
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetFilePickerAccept(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
if value := valueFromStyle(view, Accept); value != nil {
accept, ok = value.(string)
}
}
if ok {
result := strings.Split(accept, ",")
for i := 0; i < len(result); i++ {
result[i] = strings.Trim(result[i], " \t\n")
}
return result
}
}
return []string{}
}
// GetFileSelectedListeners returns the "file-selected-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.
func GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) {
return getEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
}