From 1a4040bd0009d3fb1e46f011e05aafe633b64e22 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko Date: Fri, 14 Jan 2022 17:20:04 -0500 Subject: [PATCH] Added TableView cell/row selection mode --- CHANGELOG.md | 4 + app_scripts.js | 485 ++++++++++++++++++++++++++++++++++++++++------ customView.go | 6 +- datePicker.go | 4 + defaultTheme.rui | 16 +- demo/tableDemo.go | 6 + dropDownList.go | 4 + editView.go | 4 + filePicker.go | 4 + focusEvents.go | 8 +- listView.go | 23 ++- mediaPlayer.go | 4 + numberPicker.go | 4 + resizeEvent.go | 2 +- session.go | 10 +- tableView.go | 344 ++++++++++++++++++++++++++++---- tableViewUtils.go | 59 ++++++ timePicker.go | 4 + view.go | 18 +- 19 files changed, 888 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecdf41b..910ea22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v0.5.0 + +* Added HasFocus function to the View interface + # v0.4.0 * Added SetTitle and SetTitleColor function to the Session interface diff --git a/app_scripts.js b/app_scripts.js index 86f1742..89f82e0 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -615,12 +615,13 @@ function selectDropDownListItem(elementId, number) { function listItemClickEvent(element, event) { event.stopPropagation(); + var selected = false; if (element.classList) { - selected = (element.classList.contains("ruiListItemFocused") || element.classList.contains("ruiListItemSelected")); - } else { - selected = element.className.indexOf("ruiListItemFocused") >= 0 || element.className.indexOf("ruiListItemSelected") >= 0; - } + const focusStyle = getListFocusedItemStyle(element); + const blurStyle = getListSelectedItemStyle(element); + selected = (element.classList.contains(focusStyle) || element.classList.contains(blurStyle)); + } var list = element.parentNode.parentNode if (list) { @@ -640,18 +641,27 @@ function getListItemNumber(itemId) { } } +function getStyleAttribute(element, attr, defValue) { + var result = element.getAttribute(attr); + if (result) { + return result; + } + return defValue; +} + +function getListFocusedItemStyle(element) { + return getStyleAttribute(element, "data-focusitemstyle", "ruiListItemFocused"); +} + +function getListSelectedItemStyle(element) { + return getStyleAttribute(element, "data-bluritemstyle", "ruiListItemSelected"); +} + function selectListItem(element, item, needSendMessage) { var currentId = element.getAttribute("data-current"); var message; - var focusStyle = element.getAttribute("data-focusitemstyle"); - var blurStyle = element.getAttribute("data-bluritemstyle"); - - if (!focusStyle) { - focusStyle = "ruiListItemFocused" - } - if (!blurStyle) { - blurStyle = "ruiListItemSelected" - } + const focusStyle = getListFocusedItemStyle(element); + const blurStyle = getListSelectedItemStyle(element); if (currentId) { var current = document.getElementById(currentId); @@ -801,24 +811,29 @@ function findBottomListItem(list, x, y) { return result } -function listViewKeyDownEvent(element, event) { - var key; +function getKey(event) { if (event.key) { - key = event.key; - } else if (event.keyCode) { + return event.key; + } + + if (event.keyCode) { switch (event.keyCode) { - case 13: key = "Enter"; break; - case 32: key = " "; break; - case 33: key = "PageUp"; break; - case 34: key = "PageDown"; break; - case 35: key = "End"; break; - case 36: key = "Home"; break; - case 37: key = "ArrowLeft"; break; - case 38: key = "ArrowUp"; break; - case 39: key = "ArrowRight"; break; - case 40: key = "ArrowDown"; break; + case 13: return "Enter"; + case 32: return " "; + case 33: return "PageUp"; + case 34: return "PageDown"; + case 35: return "End"; + case 36: return "Home"; + case 37: return "ArrowLeft"; + case 38: return "ArrowUp"; + case 39: return "ArrowRight"; + case 40: return "ArrowDown"; } } +} + +function listViewKeyDownEvent(element, event) { + const key = getKey(event); if (key) { var currentId = element.getAttribute("data-current"); var current @@ -885,21 +900,10 @@ function listViewFocusEvent(element, event) { if (currentId) { var current = document.getElementById(currentId); if (current) { - var focusStyle = element.getAttribute("data-focusitemstyle"); - var blurStyle = element.getAttribute("data-bluritemstyle"); - if (!focusStyle) { - focusStyle = "ruiListItemFocused" - } - if (!blurStyle) { - blurStyle = "ruiListItemSelected" - } - if (current.classList) { - current.classList.remove(blurStyle); - current.classList.add(focusStyle); - } else { // IE < 10 - current.className = "ruiListItem " + focusStyle; - } + current.classList.remove(getListSelectedItemStyle(element)); + current.classList.add(getListFocusedItemStyle(element)); + } } } } @@ -909,20 +913,9 @@ function listViewBlurEvent(element, event) { if (currentId) { var current = document.getElementById(currentId); if (current) { - var focusStyle = element.getAttribute("data-focusitemstyle"); - var blurStyle = element.getAttribute("data-bluritemstyle"); - if (!focusStyle) { - focusStyle = "ruiListItemFocused" - } - if (!blurStyle) { - blurStyle = "ruiListItemSelected" - } - if (current.classList) { - current.classList.remove(focusStyle); - current.classList.add(blurStyle); - } else { // IE < 10 - current.className = "ruiListItem " + blurStyle; + current.classList.remove(getListFocusedItemStyle(element)); + current.classList.add(getListSelectedItemStyle(element)); } } } @@ -1372,4 +1365,388 @@ function setTitleColor(color) { function detailsEvent(element) { sendMessage("details-open{session=" + sessionID + ",id=" + element.id + ",open=" + (element.open ? "1}" : "0}")); +} + +function getTableFocusedItemStyle(element) { + return getStyleAttribute(element, "data-focusitemstyle", "ruiCurrentTableCellFocused"); +} + +function getTableSelectedItemStyle(element) { + return getStyleAttribute(element, "data-bluritemstyle", "ruiCurrentTableCell"); +} + +function tableViewFocusEvent(element, event) { + var currentId = element.getAttribute("data-current"); + if (currentId) { + var current = document.getElementById(currentId); + if (current) { + if (current.classList) { + current.classList.remove(getTableSelectedItemStyle(element)); + current.classList.add(getTableFocusedItemStyle(element)); + } + } + } +} + +function tableViewBlurEvent(element, event) { + var currentId = element.getAttribute("data-current"); + if (currentId) { + var current = document.getElementById(currentId); + if (current && current.classList) { + current.classList.remove(getTableFocusedItemStyle(element)); + current.classList.add(getTableSelectedItemStyle(element)); + } + } +} + +function setTableCellCursor(element, row, column) { + const cellID = element.id + "-" + row + "-" + column; + var cell = document.getElementById(cellID); + if (!cell) { + return false; + } + + const focusStyle = getTableFocusedItemStyle(element); + const oldCellID = element.getAttribute("data-current"); + if (oldCellID) { + const oldCell = document.getElementById(oldCellID); + if (oldCell && oldCell.classList) { + oldCell.classList.remove(focusStyle); + oldCell.classList.remove(getTableSelectedItemStyle(element)); + } + } + + cell.classList.add(focusStyle); + element.setAttribute("data-current", cellID); + + sendMessage("currentCell{session=" + sessionID + ",id=" + element.id + + ",row=" + row + ",column=" + column + "}"); + return true; +} + +function moveTableCellCursor(element, row, column, dr, dc) { + const rows = element.getAttribute("data-rows"); + if (!rows) { + return; + } + const columns = element.getAttribute("data-columns"); + if (!columns) { + return; + } + + const rowCount = parseInt(rows); + const columnCount = parseInt(columns); + + row += dr; + column += dc; + while (row >= 0 && row < rowCount && column >= 0 && column < columnCount) { + if (setTableCellCursor(element, row, column)) { + return; + } else if (dr == 0) { + var r2 = row - 1; + while (r2 >= 0) { + if (setTableCellCursor(element, r2, column)) { + return; + } + r2--; + } + } else if (dc == 0) { + var c2 = column - 1; + while (c2 >= 0) { + if (setTableCellCursor(element, row, c2)) { + return; + } + c2--; + } + } + row += dr; + column += dc; + } +} + +function tableViewCellKeyDownEvent(element, event) { + const key = getKey(event); + if (key) { + const currentId = element.getAttribute("data-current"); + if (currentId) { + const elements = currentId.split("-"); + if (elements.length >= 3) { + const row = parseInt(elements[1], 10) + const column = parseInt(elements[2], 10) + + switch (key) { + case " ": + case "Enter": + sendMessage("cellClick{session=" + sessionID + ",id=" + element.id + + ",row=" + row + ",column=" + column + "}"); + break; + + case "ArrowLeft": + moveTableCellCursor(element, row, column, 0, -1) + break; + + case "ArrowRight": + moveTableCellCursor(element, row, column, 0, 1) + break; + + case "ArrowDown": + moveTableCellCursor(element, row, column, 1, 0) + break; + + case "ArrowUp": + moveTableCellCursor(element, row, column, -1, 0) + break; + + case "Home": + // TODO + break; + + case "End": + /*var newRow = rowCount-1; + while (newRow > row) { + if (setTableRowCursor(element, newRow)) { + break; + } + newRow--; + }*/ + // TODO + break; + + case "PageUp": + // TODO + break; + + case "PageDown": + // TODO + break; + + default: + return; + } + + event.stopPropagation(); + event.preventDefault(); + return; + } + } + + switch (key) { + case "ArrowLeft": + case "ArrowRight": + case "ArrowDown": + case "ArrowUp": + case "Home": + case "End": + case "PageUp": + case "PageDown": + const rows = element.getAttribute("data-rows"); + const columns = element.getAttribute("data-columns"); + if (rows && columns) { + const rowCount = parseInt(rows); + const columnCount = parseInt(rows); + row = 0; + while (row < rowCount) { + column = 0; + while (columns < columnCount) { + if (setTableCellCursor(element, row, column)) { + event.stopPropagation(); + event.preventDefault(); + return; + } + column++; + } + row++; + } + } + break; + + default: + return; + } + + } + + event.stopPropagation(); + event.preventDefault(); +} + +function setTableRowCursor(element, row) { + const tableRowID = element.id + "-" + row; + var tableRow = document.getElementById(tableRowID); + if (!tableRow) { + return false; + } + + const focusStyle = getTableFocusedItemStyle(element); + const oldRowID = element.getAttribute("data-current"); + if (oldRowID) { + const oldRow = document.getElementById(oldRowID); + if (oldRow && oldRow.classList) { + oldRow.classList.remove(focusStyle); + oldRow.classList.remove(getTableSelectedItemStyle(element)); + + } + } + + tableRow.classList.add(focusStyle); + element.setAttribute("data-current", tableRowID); + + sendMessage("currentRow{session=" + sessionID + ",id=" + element.id + ",row=" + row + "}"); + return true; +} + +function moveTableRowCursor(element, row, dr) { + const rows = element.getAttribute("data-rows"); + if (!rows) { + return; + } + + const rowCount = parseInt(rows); + row += dr; + while (row >= 0 && row < rowCount) { + if (setTableRowCursor(element, row)) { + return; + } + row += dr; + } +} + +function tableViewRowKeyDownEvent(element, event) { + const key = getKey(event); + if (key) { + const currentId = element.getAttribute("data-current"); + if (currentId) { + const elements = currentId.split("-"); + if (elements.length >= 2) { + const row = parseInt(elements[1], 10); + switch (key) { + case " ": + case "Enter": + sendMessage("rowClick{session=" + sessionID + ",id=" + element.id + ",row=" + row + "}"); + break; + + case "ArrowDown": + moveTableRowCursor(element, row, 1) + break; + + case "ArrowUp": + moveTableRowCursor(element, row, -1) + break; + + case "Home": + var newRow = 0; + while (newRow < row) { + if (setTableRowCursor(element, newRow)) { + break; + } + newRow++; + } + break; + + case "End": + var newRow = rowCount-1; + while (newRow > row) { + if (setTableRowCursor(element, newRow)) { + break; + } + newRow--; + } + break; + + case "PageUp": + // TODO + break; + + case "PageDown": + // TODO + break; + + default: + return; + } + event.stopPropagation(); + event.preventDefault(); + return; + } + } + + switch (key) { + case "ArrowLeft": + case "ArrowRight": + case "ArrowDown": + case "ArrowUp": + case "Home": + case "End": + case "PageUp": + case "PageDown": + const rows = element.getAttribute("data-rows"); + if (rows) { + const rowCount = parseInt(rows); + row = 0; + while (row < rowCount) { + if (setTableRowCursor(element, row)) { + break; + } + row++; + } + } + break; + + default: + return; + } + } + + event.stopPropagation(); + event.preventDefault(); +} + +function tableCellClickEvent(element, event) { + event.preventDefault(); + + const elements = element.id.split("-"); + if (elements.length < 3) { + return + } + + const tableID = elements[0]; + const row = parseInt(elements[1], 10); + const column = parseInt(elements[2], 10); + const table = document.getElementById(tableID); + if (table) { + const selection = table.getAttribute("data-selection"); + if (selection == "cell") { + const currentID = table.getAttribute("data-current"); + if (!currentID || currentID != element.ID) { + setTableCellCursor(table, row, column) + } + } + } + + sendMessage("cellClick{session=" + sessionID + ",id=" + tableID + + ",row=" + row + ",column=" + column + "}"); +} + +function tableRowClickEvent(element, event) { + event.preventDefault(); + + const elements = element.id.split("-"); + if (elements.length < 2) { + return + } + + const tableID = elements[0]; + const row = parseInt(elements[1], 10); + const table = document.getElementById(tableID); + if (table) { + const selection = table.getAttribute("data-selection"); + if (selection == "cell") { + const currentID = table.getAttribute("data-current"); + if (!currentID || currentID != element.ID) { + setTableRowCursor(table, row) + } + } + } + + sendMessage("rowClick{session=" + sessionID + ",id=" + tableID + ",row=" + row + "}"); } \ No newline at end of file diff --git a/customView.go b/customView.go index aed1499..3c31d45 100644 --- a/customView.go +++ b/customView.go @@ -148,11 +148,15 @@ func (customView *CustomViewData) Scroll() Frame { return customView.superView.Scroll() } +func (customView *CustomViewData) HasFocus() bool { + return customView.superView.HasFocus() +} + func (customView *CustomViewData) onResize(self View, x, y, width, height float64) { customView.superView.onResize(customView.superView, x, y, width, height) } -func (customView *CustomViewData) onItemResize(self View, index int, x, y, width, height float64) { +func (customView *CustomViewData) onItemResize(self View, index string, x, y, width, height float64) { customView.superView.onItemResize(customView.superView, index, x, y, width, height) } diff --git a/datePicker.go b/datePicker.go index 129b32b..f2e379f 100644 --- a/datePicker.go +++ b/datePicker.go @@ -44,6 +44,10 @@ func (picker *datePickerData) Init(session Session) { picker.dateChangedListeners = []func(DatePicker, time.Time){} } +func (picker *datePickerData) Focusable() bool { + return true +} + func (picker *datePickerData) normalizeTag(tag string) string { tag = strings.ToLower(tag) switch tag { diff --git a/defaultTheme.rui b/defaultTheme.rui index 23fd6f2..1524a89 100644 --- a/defaultTheme.rui +++ b/defaultTheme.rui @@ -213,24 +213,32 @@ theme { text-color = @ruiPopupTextColor, radius = 4px, shadow = _{spread-radius=4px, blur=16px, color=#80808080}, - } + }, ruiPopupTitle { background-color = @ruiPopupTitleColor, text-color = @ruiPopupTitleTextColor, min-height = 24px, - } + }, ruiMessageText { padding-left = 64px, padding-right = 64px, padding-top = 32px, padding-bottom = 32px, - } + }, ruiPopupMenuItem { padding-top = 4px, padding-bottom = 4px, padding-left = 8px, padding-right = 8px, - } + }, + ruiCurrentTableCell { + background-color=@ruiSelectedColor, + text-color=@ruiSelectedTextColor, + }, + ruiCurrentTableCellFocused { + background-color=@ruiHighlightColor, + text-color=@ruiHighlightTextColor, + }, ], } diff --git a/demo/tableDemo.go b/demo/tableDemo.go index 728d797..2e23c1a 100644 --- a/demo/tableDemo.go +++ b/demo/tableDemo.go @@ -32,6 +32,8 @@ GridLayout { DropDownList { row = 5, column = 1, id = tableFootStyle, current = 0, items = ["none", "tableFoot1", "rui.Params"]}, Checkbox { row = 6, column = 0:1, id = tableRowStyle, content = "Row style" }, Checkbox { row = 7, column = 0:1, id = tableColumnStyle, content = "Column style" }, + TextView { row = 8, text = "Selection mode" }, + DropDownList { row = 8, column = 1, id = tableSelectionMode, current = 0, items = ["none", "cell", "row"]}, ] } ] @@ -93,6 +95,10 @@ func createTableViewDemo(session rui.Session) rui.View { } } + rui.Set(view, "tableSelectionMode", rui.DropDownEvent, func(list rui.DropDownList, number int) { + rui.Set(view, "demoTableView1", rui.SelectionMode, number) + }) + rui.Set(view, "tableCellGap", rui.DropDownEvent, func(list rui.DropDownList, number int) { if number == 0 { rui.Set(view, "demoTableView1", rui.Gap, rui.Px(0)) diff --git a/dropDownList.go b/dropDownList.go index 8008226..8b170cf 100644 --- a/dropDownList.go +++ b/dropDownList.go @@ -39,6 +39,10 @@ func (list *dropDownListData) Init(session Session) { list.dropDownListener = []func(DropDownList, int){} } +func (list *dropDownListData) Focusable() bool { + return true +} + func (list *dropDownListData) Remove(tag string) { list.remove(strings.ToLower(tag)) } diff --git a/editView.go b/editView.go index 959a0ff..8c9bd19 100644 --- a/editView.go +++ b/editView.go @@ -63,6 +63,10 @@ func (edit *editViewData) Init(session Session) { edit.tag = "EditView" } +func (edit *editViewData) Focusable() bool { + return true +} + func (edit *editViewData) normalizeTag(tag string) string { tag = strings.ToLower(tag) switch tag { diff --git a/filePicker.go b/filePicker.go index 5faac07..8864d3b 100644 --- a/filePicker.go +++ b/filePicker.go @@ -89,6 +89,10 @@ func (picker *filePickerData) Init(session Session) { picker.fileSelectedListeners = []func(FilePicker, []FileInfo){} } +func (picker *filePickerData) Focusable() bool { + return true +} + func (picker *filePickerData) Files() []FileInfo { return picker.files } diff --git a/focusEvents.go b/focusEvents.go index 9bb041e..6a52c07 100644 --- a/focusEvents.go +++ b/focusEvents.go @@ -140,11 +140,9 @@ func getFocusListeners(view View, subviewID string, tag string) []func(View) { } func focusEventsHtml(view View, buffer *strings.Builder) { - for tag, js := range focusEvents { - if value := view.getRaw(tag); value != nil { - if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 { - buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) - } + if view.Focusable() { + for _, js := range focusEvents { + buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) } } } diff --git a/listView.go b/listView.go index 4353526..28ba149 100644 --- a/listView.go +++ b/listView.go @@ -1083,14 +1083,12 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) { func (listView *listViewData) handleCommand(self View, command string, data DataObject) bool { switch command { case "itemSelected": - if text, ok := data.PropertyValue(`number`); ok { - if number, err := strconv.Atoi(text); err == nil { - listView.properties[Current] = number - for _, listener := range listView.selectedListeners { - listener(listView, number) - } - listView.propertyChangedEvent(Current) + if number, ok := dataIntProperty(data, `number`); ok { + listView.properties[Current] = number + for _, listener := range listView.selectedListeners { + listener(listView, number) } + listView.propertyChangedEvent(Current) } case "itemUnselected": @@ -1162,9 +1160,14 @@ func (listView *listViewData) onItemClick() { } } -func (listView *listViewData) onItemResize(self View, index int, x, y, width, height float64) { - if index >= 0 && index < len(listView.itemFrame) { - listView.itemFrame[index] = Frame{Left: x, Top: y, Width: width, Height: height} +func (listView *listViewData) onItemResize(self View, index string, x, y, width, height float64) { + n, err := strconv.Atoi(index) + if err != nil { + ErrorLog(err.Error()) + } else if n >= 0 && n < len(listView.itemFrame) { + listView.itemFrame[n] = Frame{Left: x, Top: y, Width: width, Height: height} + } else { + ErrorLogF(`Invalid ListView item index: %d`, n) } } diff --git a/mediaPlayer.go b/mediaPlayer.go index 4c2534c..7493f93 100644 --- a/mediaPlayer.go +++ b/mediaPlayer.go @@ -168,6 +168,10 @@ func (player *mediaPlayerData) Init(session Session) { player.tag = "MediaPlayer" } +func (player *mediaPlayerData) Focusable() bool { + return true +} + func (player *mediaPlayerData) Remove(tag string) { player.remove(strings.ToLower(tag)) } diff --git a/numberPicker.go b/numberPicker.go index 5f5bf58..c126d99 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -50,6 +50,10 @@ func (picker *numberPickerData) Init(session Session) { picker.numberChangedListeners = []func(NumberPicker, float64){} } +func (picker *numberPickerData) Focusable() bool { + return true +} + func (picker *numberPickerData) normalizeTag(tag string) string { tag = strings.ToLower(tag) switch tag { diff --git a/resizeEvent.go b/resizeEvent.go index 070dc6a..f00ab9c 100644 --- a/resizeEvent.go +++ b/resizeEvent.go @@ -18,7 +18,7 @@ func (view *viewData) onResize(self View, x, y, width, height float64) { } } -func (view *viewData) onItemResize(self View, index int, x, y, width, height float64) { +func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) { } func (view *viewData) setFrameListener(tag string, value interface{}) bool { diff --git a/session.go b/session.go index e015baf..505a78d 100644 --- a/session.go +++ b/session.go @@ -378,14 +378,10 @@ func (session *sessionData) handleResize(data DataObject) { } if viewID, ok := obj.PropertyValue("id"); ok { if n := strings.IndexRune(viewID, '-'); n > 0 { - if index, err := strconv.Atoi(viewID[n+1:]); err == nil { - if view := session.viewByHTMLID(viewID[:n]); view != nil { - view.onItemResize(view, index, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) - } else { - ErrorLogF(`View with id == %s not found`, viewID[:n]) - } + if view := session.viewByHTMLID(viewID[:n]); view != nil { + view.onItemResize(view, viewID[n+1:], getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) } else { - ErrorLogF(`Invalid view id == %s not found`, viewID) + ErrorLogF(`View with id == %s not found`, viewID[:n]) } } else if view := session.viewByHTMLID(viewID); view != nil { view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) diff --git a/tableView.go b/tableView.go index 0c7a90f..933739b 100644 --- a/tableView.go +++ b/tableView.go @@ -214,11 +214,14 @@ type TableView interface { View ParanetView ReloadTableData() + CellFrame(row, column int) Frame + getCurrent() CellIndex } type tableViewData struct { viewData cellViews []View + cellFrame []Frame cellSelectedListener, cellClickedListener []func(TableView, int, int) rowSelectedListener, rowClickedListener []func(TableView, int) current CellIndex @@ -245,6 +248,7 @@ func (table *tableViewData) Init(session Session) { table.viewData.Init(session) table.tag = "TableView" table.cellViews = []View{} + table.cellFrame = []Frame{} table.cellSelectedListener = []func(TableView, int, int){} table.cellClickedListener = []func(TableView, int, int){} table.rowSelectedListener = []func(TableView, int){} @@ -270,6 +274,10 @@ func (table *tableViewData) normalizeTag(tag string) string { return tag } +func (table *tableViewData) Focusable() bool { + return GetSelectionMode(table, "") != NoneSelection +} + func (table *tableViewData) Get(tag string) interface{} { return table.get(table.normalizeTag(tag)) } @@ -312,6 +320,10 @@ func (table *tableViewData) remove(tag string) { table.current.Column = -1 table.propertyChanged(tag) + case SelectionMode: + table.viewData.remove(tag) + table.propertyChanged(tag) + default: table.viewData.remove(tag) } @@ -486,14 +498,51 @@ func (table *tableViewData) set(tag string, value interface{}) bool { } case Current: - switch GetSelectionMode(table, "") { - case NoneSelection: + switch value := value.(type) { + case int: + table.current.Row = value + table.current.Column = -1 - case CellSelection: - // TODO + case CellIndex: + table.current = value - case RowSelection: - // TODO + case DataObject: + if row, ok := dataIntProperty(value, "row"); ok { + table.current.Row = row + } + if column, ok := dataIntProperty(value, "column"); ok { + table.current.Column = column + } + + case string: + if strings.Contains(value, ",") { + if values := strings.Split(value, ","); len(values) == 2 { + var n = []int{0, 0} + for i := 0; i < 2; i++ { + var err error + if n[i], err = strconv.Atoi(values[i]); err != nil { + ErrorLog(err.Error()) + return false + } + } + table.current.Row = n[0] + table.current.Column = n[1] + } else { + notCompatibleType(tag, value) + } + } else { + n, err := strconv.Atoi(value) + if err != nil { + ErrorLog(err.Error()) + return false + } + table.current.Row = n + table.current.Column = -1 + } + + default: + notCompatibleType(tag, value) + return false } default: @@ -524,11 +573,75 @@ func (table *tableViewData) propertyChanged(tag string) { updateCSSProperty(htmlID, "border-spacing", gap.cssString("0"), session) updateCSSProperty(htmlID, "border-collapse", "separate", session) } + + case SelectionMode: + htmlID := table.htmlID() + session := table.Session() + + switch GetSelectionMode(table, "") { + case CellSelection: + updateProperty(htmlID, "tabindex", "0", session) + updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)", session) + updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)", session) + updateProperty(htmlID, "data-selection", "cell", session) + updateProperty(htmlID, "data-focusitemstyle", table.currentStyle(), session) + updateProperty(htmlID, "data-bluritemstyle", table.currentInactiveStyle(), session) + + if table.current.Row >= 0 && table.current.Column >= 0 { + updateProperty(htmlID, "data-current", table.cellID(table.current.Row, table.current.Column), session) + } else { + removeProperty(htmlID, "data-current", session) + } + updateProperty(htmlID, "onkeydown", "tableViewCellKeyDownEvent(this, event)", session) + + case RowSelection: + updateProperty(htmlID, "tabindex", "0", session) + updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)", session) + updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)", session) + updateProperty(htmlID, "data-selection", "cell", session) + updateProperty(htmlID, "data-focusitemstyle", table.currentStyle(), session) + updateProperty(htmlID, "data-bluritemstyle", table.currentInactiveStyle(), session) + + if table.current.Row >= 0 { + updateProperty(htmlID, "data-current", table.rowID(table.current.Row), session) + } else { + removeProperty(htmlID, "data-current", session) + } + updateProperty(htmlID, "onkeydown", "tableViewRowKeyDownEvent(this, event)", session) + + default: // NoneSelection + for _, prop := range []string{"tabindex", "data-current", "onfocus", "onblur", "onkeydown", "data-selection"} { + removeProperty(htmlID, prop, session) + } + } + updateInnerHTML(htmlID, session) } } table.propertyChangedEvent(tag) } +func (table *tableViewData) currentStyle() string { + if value := table.getRaw(CurrentStyle); value != nil { + if style, ok := value.(string); ok { + if style, ok = table.session.resolveConstants(style); ok { + return style + } + } + } + return "ruiCurrentTableCellFocused" +} + +func (table *tableViewData) currentInactiveStyle() string { + if value := table.getRaw(CurrentInactiveStyle); value != nil { + if style, ok := value.(string); ok { + if style, ok = table.session.resolveConstants(style); ok { + return style + } + } + } + return "ruiCurrentTableCell" +} + func (table *tableViewData) valueToCellListeners(value interface{}) []func(TableView, int, int) { if value == nil { return []func(TableView, int, int){} @@ -643,16 +756,69 @@ func (table *tableViewData) htmlTag() string { return "table" } -func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { - table.cellViews = []View{} +func (table *tableViewData) rowID(index int) string { + return fmt.Sprintf("%s-%d", table.htmlID(), index) +} - content := table.getRaw(Content) - if content == nil { - return +func (table *tableViewData) cellID(row, column int) string { + return fmt.Sprintf("%s-%d-%d", table.htmlID(), row, column) +} + +func (table *tableViewData) htmlProperties(self View, buffer *strings.Builder) { + + if content := table.content(); content != nil { + buffer.WriteString(` data-rows="`) + buffer.WriteString(strconv.Itoa(content.RowCount())) + buffer.WriteString(`" data-columns="`) + buffer.WriteString(strconv.Itoa(content.ColumnCount())) + buffer.WriteRune('"') } - adapter, ok := content.(TableAdapter) - if !ok { + if selectionMode := GetSelectionMode(table, ""); selectionMode != NoneSelection { + buffer.WriteString(` onfocus="tableViewFocusEvent(this, event)" onblur="tableViewBlurEvent(this, event)" data-focusitemstyle="`) + buffer.WriteString(table.currentStyle()) + buffer.WriteString(`" data-bluritemstyle="`) + buffer.WriteString(table.currentInactiveStyle()) + buffer.WriteRune('"') + + switch selectionMode { + case RowSelection: + buffer.WriteString(` data-selection="row" onkeydown="tableViewRowKeyDownEvent(this, event)"`) + if table.current.Row >= 0 { + buffer.WriteString(` data-current="`) + buffer.WriteString(table.rowID(table.current.Row)) + buffer.WriteRune('"') + } + + case CellSelection: + buffer.WriteString(` data-selection="cell" onkeydown="tableViewCellKeyDownEvent(this, event)"`) + if table.current.Row >= 0 && table.current.Column >= 0 { + buffer.WriteString(` data-current="`) + buffer.WriteString(table.cellID(table.current.Row, table.current.Column)) + buffer.WriteRune('"') + } + } + } + + table.viewData.htmlProperties(self, buffer) +} + +func (table *tableViewData) content() TableAdapter { + if content := table.getRaw(Content); content != nil { + if adapter, ok := content.(TableAdapter); ok { + return adapter + } + } + + return nil +} + +func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { + table.cellViews = []View{} + table.cellFrame = []Frame{} + + adapter := table.content() + if adapter == nil { return } @@ -662,10 +828,12 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { return } + table.cellFrame = make([]Frame, rowCount*columnCount) + rowStyle := table.getRowStyle() var cellStyle1 TableCellStyle = nil - if style, ok := content.(TableCellStyle); ok { + if style, ok := adapter.(TableCellStyle); ok { cellStyle1 = style } @@ -691,6 +859,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { view.Init(session) ignorCells := []struct{ row, column int }{} + selectionMode := GetSelectionMode(table, "") tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) { for row := startRow; row < endRow; row++ { @@ -706,14 +875,31 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { } } - if cssBuilder.buffer.Len() > 0 { - buffer.WriteString(``) - } else { - buffer.WriteString("") + buffer.WriteString(` 0 { + buffer.WriteString(` style="`) + buffer.WriteString(cssBuilder.buffer.String()) + buffer.WriteString(`"`) + } + buffer.WriteString(">") + for column := 0; column < columnCount; column++ { ignore := false for _, cell := range ignorCells { @@ -748,7 +934,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { return value case string: - if value, ok = session.resolveConstants(value); ok { + if value, ok := session.resolveConstants(value); ok { if n, err := strconv.Atoi(value); err == nil { return n } @@ -780,6 +966,23 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { buffer.WriteRune('<') buffer.WriteString(cellTag) + buffer.WriteString(` id="`) + buffer.WriteString(table.cellID(row, column)) + buffer.WriteString(`" class="ruiView`) + + if selectionMode == CellSelection && row == table.current.Row && column == table.current.Column { + buffer.WriteRune(' ') + if table.HasFocus() { + buffer.WriteString(table.currentStyle()) + } else { + buffer.WriteString(table.currentInactiveStyle()) + } + } + buffer.WriteRune('"') + + if selectionMode == CellSelection { + buffer.WriteString(` onclick="tableCellClickEvent(this, event)"`) + } if columnSpan > 1 { buffer.WriteString(` colspan="`) @@ -1087,6 +1290,10 @@ func (table *tableViewData) getCellBorder() BorderProperty { return nil } +func (table *tableViewData) getCurrent() CellIndex { + return table.current +} + func (table *tableViewData) cssStyle(self View, builder cssBuilder) { table.viewData.cssViewStyle(builder, table.Session()) @@ -1101,30 +1308,93 @@ func (table *tableViewData) cssStyle(self View, builder cssBuilder) { } func (table *tableViewData) ReloadTableData() { + if content := table.content(); content != nil { + updateProperty(table.htmlID(), "data-rows", strconv.Itoa(content.RowCount()), table.Session()) + updateProperty(table.htmlID(), "data-columns", strconv.Itoa(content.ColumnCount()), table.Session()) + } updateInnerHTML(table.htmlID(), table.Session()) } -func (cell *tableCellView) Set(tag string, value interface{}) bool { - return cell.set(strings.ToLower(tag), value) +func (table *tableViewData) onItemResize(self View, index string, x, y, width, height float64) { + if n := strings.IndexRune(index, '-'); n > 0 { + if row, err := strconv.Atoi(index[:n]); err == nil { + if column, err := strconv.Atoi(index[n+1:]); err == nil { + if content := table.content(); content != nil { + i := row*content.ColumnCount() + column + if i < len(table.cellFrame) { + table.cellFrame[i].Left = x + table.cellFrame[i].Top = y + table.cellFrame[i].Width = width + table.cellFrame[i].Height = height + } + } + } else { + ErrorLog(err.Error()) + } + } else { + ErrorLog(err.Error()) + } + } else { + ErrorLogF(`Invalid cell index: %s`, index) + } } -func (cell *tableCellView) set(tag string, value interface{}) bool { - switch tag { - case VerticalAlign: - tag = TableVerticalAlign - } - return cell.viewData.set(tag, value) -} - -func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { - session := cell.Session() - cell.viewData.cssViewStyle(builder, session) - - if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { - builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) +func (table *tableViewData) CellFrame(row, column int) Frame { + if content := table.content(); content != nil { + i := row*content.ColumnCount() + column + if i < len(table.cellFrame) { + return table.cellFrame[i] + } } + return Frame{} } func (table *tableViewData) Views() []View { return table.cellViews } + +func (table *tableViewData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "currentRow": + if row, ok := dataIntProperty(data, "row"); ok && row != table.current.Row { + table.current.Row = row + for _, listener := range table.rowSelectedListener { + listener(table, row) + } + } + + case "currentCell": + if row, ok := dataIntProperty(data, "row"); ok { + if column, ok := dataIntProperty(data, "column"); ok { + if row != table.current.Row || column != table.current.Column { + table.current.Row = row + table.current.Column = column + for _, listener := range table.cellSelectedListener { + listener(table, row, column) + } + } + } + } + + case "rowClick": + if row, ok := dataIntProperty(data, "row"); ok { + for _, listener := range table.rowClickedListener { + listener(table, row) + } + } + + case "cellClick": + if row, ok := dataIntProperty(data, "row"); ok { + if column, ok := dataIntProperty(data, "column"); ok { + for _, listener := range table.cellClickedListener { + listener(table, row, column) + } + } + } + + default: + return table.viewData.handleCommand(self, command, data) + } + + return true +} diff --git a/tableViewUtils.go b/tableViewUtils.go index 6b84aeb..3d079da 100644 --- a/tableViewUtils.go +++ b/tableViewUtils.go @@ -1,5 +1,28 @@ package rui +import "strings" + +func (cell *tableCellView) Set(tag string, value interface{}) bool { + return cell.set(strings.ToLower(tag), value) +} + +func (cell *tableCellView) set(tag string, value interface{}) bool { + switch tag { + case VerticalAlign: + tag = TableVerticalAlign + } + return cell.viewData.set(tag, value) +} + +func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { + session := cell.Session() + cell.viewData.cssViewStyle(builder, session) + + if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { + builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) + } +} + // GetSelectionMode returns the mode of the TableView elements selection. // Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2). // If the second argument (subviewID) is "" then a value from the first argument (view) is returned. @@ -15,6 +38,42 @@ func GetSelectionMode(view View, subviewID string) int { return NoneSelection } +// GetSelectionMode returns the index of the TableView selected row. +// If there is no selected row, then a value less than 0 are returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCurrentTableRow(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + + if view != nil { + if selectionMode := GetSelectionMode(view, ""); selectionMode != NoneSelection { + if tableView, ok := view.(TableView); ok { + return tableView.getCurrent().Row + } + } + } + return -1 +} + +// GetCurrentTableCell returns the row and column index of the TableView selected cell. +// If there is no selected cell, then a value of the row and column index less than 0 is returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCurrentTableCell(view View, subviewID string) CellIndex { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + + if view != nil { + if selectionMode := GetSelectionMode(view, ""); selectionMode != NoneSelection { + if tableView, ok := view.(TableView); ok { + return tableView.getCurrent() + } + } + } + return CellIndex{Row: -1, Column: -1} +} + // GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell. // If there are no listeners then the empty list is returned. // If the second argument (subviewID) is "" then a value from the first argument (view) is returned. diff --git a/timePicker.go b/timePicker.go index 42fd616..1c2fa5f 100644 --- a/timePicker.go +++ b/timePicker.go @@ -44,6 +44,10 @@ func (picker *timePickerData) Init(session Session) { picker.timeChangedListeners = []func(TimePicker, time.Time){} } +func (picker *timePickerData) Focusable() bool { + return true +} + func (picker *timePickerData) normalizeTag(tag string) string { tag = strings.ToLower(tag) switch tag { diff --git a/view.go b/view.go index 2c57464..f9ce65e 100644 --- a/view.go +++ b/view.go @@ -58,6 +58,8 @@ type View interface { SetAnimated(tag string, value interface{}, 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 handleCommand(self View, command string, data DataObject) bool htmlClass(disabled bool) string @@ -73,7 +75,7 @@ type View interface { getTransitions() Params onResize(self View, x, y, width, height float64) - onItemResize(self View, index int, x, y, width, height float64) + onItemResize(self View, index string, x, y, width, height float64) setNoResizeEvent() isNoResizeEvent() bool setScroll(x, y, width, height float64) @@ -95,6 +97,7 @@ type viewData struct { scroll Frame noResizeEvent bool created bool + hasFocus bool //animation map[string]AnimationEndListener } @@ -737,7 +740,14 @@ func (view *viewData) handleCommand(self View, command string, data DataObject) case TouchStart, TouchEnd, TouchMove, TouchCancel: handleTouchEvents(self, command, data) - case FocusEvent, LostFocusEvent: + case FocusEvent: + view.hasFocus = true + for _, listener := range getFocusListeners(view, "", command) { + listener(self) + } + + case LostFocusEvent: + view.hasFocus = false for _, listener := range getFocusListeners(view, "", command) { listener(self) } @@ -838,3 +848,7 @@ func (view *viewData) SetChangeListener(tag string, listener func(View, string)) view.changeListener[tag] = listener } } + +func (view *viewData) HasFocus() bool { + return view.hasFocus +}