2021-11-04 14:59:25 +03:00
|
|
|
package rui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2024-09-12 14:05:11 +03:00
|
|
|
// Constants for [FilePicker] specific properties and events
|
2021-11-04 14:59:25 +03:00
|
|
|
const (
|
|
|
|
// FileSelectedEvent is the constant for "file-selected-event" property tag.
|
2024-09-18 13:50:06 +03:00
|
|
|
//
|
|
|
|
// 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()`.
|
2024-11-13 12:56:39 +03:00
|
|
|
FileSelectedEvent PropertyName = "file-selected-event"
|
2024-09-18 13:50:06 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
// Accept is the constant for "accept" property tag.
|
2024-09-18 13:50:06 +03:00
|
|
|
//
|
|
|
|
// 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.
|
2024-11-13 12:56:39 +03:00
|
|
|
Accept PropertyName = "accept"
|
2024-09-18 13:50:06 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
// Multiple is the constant for "multiple" property tag.
|
2024-09-18 13:50:06 +03:00
|
|
|
//
|
|
|
|
// 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.
|
2024-11-13 12:56:39 +03:00
|
|
|
Multiple PropertyName = "multiple"
|
2021-11-04 14:59:25 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// FileInfo describes a file which selected in the FilePicker view
|
|
|
|
type FileInfo struct {
|
|
|
|
// Name - the file's name.
|
|
|
|
Name string
|
2024-09-18 13:50:06 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
// LastModified specifying the date and time at which the file was last modified
|
|
|
|
LastModified time.Time
|
2024-09-18 13:50:06 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
// Size - the size of the file in bytes.
|
|
|
|
Size int64
|
2024-09-18 13:50:06 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
// MimeType - the file's MIME type.
|
|
|
|
MimeType string
|
|
|
|
}
|
|
|
|
|
2024-09-12 14:05:11 +03:00
|
|
|
// FilePicker represents the FilePicker view
|
2021-11-04 14:59:25 +03:00
|
|
|
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
|
2024-11-13 12:56:39 +03:00
|
|
|
files []FileInfo
|
|
|
|
loader map[int]func(FileInfo, []byte)
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2022-09-01 11:04:50 +03:00
|
|
|
view.init(session)
|
2021-11-04 14:59:25 +03:00
|
|
|
setInitParams(view, params)
|
|
|
|
return view
|
|
|
|
}
|
|
|
|
|
|
|
|
func newFilePicker(session Session) View {
|
2024-11-13 12:56:39 +03:00
|
|
|
return new(filePickerData) // NewFilePicker(session, nil)
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
|
2022-09-01 11:04:50 +03:00
|
|
|
func (picker *filePickerData) init(session Session) {
|
|
|
|
picker.viewData.init(session)
|
2021-11-04 14:59:25 +03:00
|
|
|
picker.tag = "FilePicker"
|
2024-04-23 18:24:51 +03:00
|
|
|
picker.hasHtmlDisabled = true
|
2021-11-04 14:59:25 +03:00
|
|
|
picker.files = []FileInfo{}
|
|
|
|
picker.loader = map[int]func(FileInfo, []byte){}
|
2024-11-18 17:20:25 +03:00
|
|
|
picker.set = picker.setFunc
|
|
|
|
picker.changed = picker.propertyChanged
|
2021-11-04 14:59:25 +03:00
|
|
|
|
2022-05-22 12:54:02 +03:00
|
|
|
}
|
|
|
|
|
2022-01-15 01:20:04 +03:00
|
|
|
func (picker *filePickerData) Focusable() bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
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
|
2022-11-02 20:10:19 +03:00
|
|
|
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
|
2021-11-04 14:59:25 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-18 17:20:25 +03:00
|
|
|
func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyName {
|
2021-11-04 14:59:25 +03:00
|
|
|
|
|
|
|
switch tag {
|
|
|
|
case FileSelectedEvent:
|
2024-11-18 17:20:25 +03:00
|
|
|
return setOneArgEventListener[FilePicker, []FileInfo](picker, tag, value)
|
2021-11-04 14:59:25 +03:00
|
|
|
|
|
|
|
case Accept:
|
|
|
|
switch value := value.(type) {
|
|
|
|
case string:
|
2024-11-18 17:20:25 +03:00
|
|
|
return setStringPropertyValue(picker, Accept, strings.Trim(value, " \t\n"))
|
2021-11-04 14:59:25 +03:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2024-11-18 17:20:25 +03:00
|
|
|
return setStringPropertyValue(picker, Accept, buffer.String())
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
2024-11-13 12:56:39 +03:00
|
|
|
notCompatibleType(tag, value)
|
|
|
|
return nil
|
|
|
|
}
|
2021-11-20 11:15:28 +03:00
|
|
|
|
2024-11-18 17:20:25 +03:00
|
|
|
return picker.viewData.setFunc(tag, value)
|
2024-11-13 12:56:39 +03:00
|
|
|
}
|
|
|
|
|
2024-11-18 17:20:25 +03:00
|
|
|
func (picker *filePickerData) propertyChanged(tag PropertyName) {
|
2024-11-13 12:56:39 +03:00
|
|
|
switch tag {
|
|
|
|
case Accept:
|
2024-11-18 17:20:25 +03:00
|
|
|
session := picker.Session()
|
|
|
|
if css := acceptPropertyCSS(picker); css != "" {
|
|
|
|
session.updateProperty(picker.htmlID(), "accept", css)
|
2024-11-13 12:56:39 +03:00
|
|
|
} else {
|
2024-11-18 17:20:25 +03:00
|
|
|
session.removeProperty(picker.htmlID(), "accept")
|
2021-11-20 11:15:28 +03:00
|
|
|
}
|
2021-11-04 14:59:25 +03:00
|
|
|
|
|
|
|
default:
|
2024-11-18 17:20:25 +03:00
|
|
|
picker.viewData.propertyChanged(tag)
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (picker *filePickerData) htmlTag() string {
|
|
|
|
return "input"
|
|
|
|
}
|
|
|
|
|
2024-11-13 12:56:39 +03:00
|
|
|
func acceptPropertyCSS(view View) string {
|
|
|
|
accept, ok := stringProperty(view, Accept, view.Session())
|
2021-11-04 14:59:25 +03:00
|
|
|
if !ok {
|
2024-11-13 12:56:39 +03:00
|
|
|
if value := valueFromStyle(view, Accept); value != nil {
|
2022-05-23 15:22:14 +03:00
|
|
|
accept, ok = value.(string)
|
|
|
|
}
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
2022-05-23 15:22:14 +03:00
|
|
|
|
2021-11-04 14:59:25 +03:00
|
|
|
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)
|
|
|
|
|
2024-11-13 12:56:39 +03:00
|
|
|
if accept := acceptPropertyCSS(picker); accept != "" {
|
2021-11-04 14:59:25 +03:00
|
|
|
buffer.WriteString(` accept="`)
|
|
|
|
buffer.WriteString(accept)
|
|
|
|
buffer.WriteRune('"')
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer.WriteString(` type="file"`)
|
2022-08-31 22:17:46 +03:00
|
|
|
if IsMultipleFilePicker(picker) {
|
2021-11-04 14:59:25 +03:00
|
|
|
buffer.WriteString(` multiple`)
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer.WriteString(` oninput="fileSelectedEvent(this)"`)
|
2022-04-15 15:41:44 +03:00
|
|
|
if picker.getRaw(ClickEvent) == nil {
|
|
|
|
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
|
|
|
|
}
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
|
2024-11-13 12:56:39 +03:00
|
|
|
func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
|
2021-11-04 14:59:25 +03:00
|
|
|
switch command {
|
|
|
|
case "fileSelected":
|
2022-12-18 18:22:58 +03:00
|
|
|
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
|
2021-11-04 14:59:25 +03:00
|
|
|
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
|
|
|
|
|
2024-11-13 12:56:39 +03:00
|
|
|
for _, listener := range GetFileSelectedListeners(picker) {
|
2021-11-04 14:59:25 +03:00
|
|
|
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)
|
2022-08-31 22:17:46 +03:00
|
|
|
// 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 {
|
2021-11-04 14:59:25 +03:00
|
|
|
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.
|
2022-08-31 22:17:46 +03:00
|
|
|
// 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 {
|
2022-07-28 12:11:27 +03:00
|
|
|
return boolStyledProperty(view, subviewID, Multiple, false)
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
|
2022-08-31 22:17:46 +03:00
|
|
|
// 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 {
|
2024-11-24 16:43:31 +03:00
|
|
|
if view = getSubview(view, subviewID); view != nil {
|
2021-11-04 14:59:25 +03:00
|
|
|
accept, ok := stringProperty(view, Accept, view.Session())
|
|
|
|
if !ok {
|
2022-05-23 15:22:14 +03:00
|
|
|
if value := valueFromStyle(view, Accept); value != nil {
|
|
|
|
accept, ok = value.(string)
|
|
|
|
}
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|
|
|
|
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.
|
2022-08-31 22:17:46 +03:00
|
|
|
// 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) {
|
2024-11-18 17:20:25 +03:00
|
|
|
return getOneArgEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
|
2021-11-04 14:59:25 +03:00
|
|
|
}
|