package rui import ( "fmt" "strconv" "strings" ) const ( // CurrentTabChangedEvent is the constant for "current-tab-changed" property tag. // The "current-tab-changed" event occurs when the new tab becomes active. // The main listener format: func(TabsLayout, int, int), where // the second argument is the index of the new active tab, // the third argument is the index of the old active tab. CurrentTabChangedEvent = "current-tab-changed" // Icon is the constant for "icon" property tag. // The string "icon" property defines the icon name that is displayed in the tab. Icon = "icon" // TabCloseButton is the constant for "tab-close-button" property tag. // The "tab-close-button" is the bool property. If it is "true" then a close button is displayed within the tab. TabCloseButton = "tab-close-button" // TabCloseEvent is the constant for "tab-close-event" property tag. // The "tab-close-event" occurs when when the user clicks on the tab close button. // The main listener format: func(TabsLayout, int), where the second argument is the index of the tab. TabCloseEvent = "tab-close-event" // TopTabs - tabs of TabsLayout are on the top TopTabs = 0 // BottomTabs - tabs of TabsLayout are on the bottom BottomTabs = 1 // LeftTabs - tabs of TabsLayout are on the left. Bookmarks are rotated counterclockwise 90 degrees. LeftTabs = 2 // RightTabs - tabs of TabsLayout are on the right. Bookmarks are rotated clockwise 90 degrees. RightTabs = 3 // LeftListTabs - tabs of TabsLayout are on the left LeftListTabs = 4 // RightListTabs - tabs of TabsLayout are on the right RightListTabs = 5 // HiddenTabs - tabs of TabsLayout are hidden HiddenTabs = 6 inactiveTabStyle = "data-inactiveTabStyle" activeTabStyle = "data-activeTabStyle" ) // TabsLayout - multi-tab container of View type TabsLayout interface { ViewsContainer ListAdapter /* // Current return the index of active tab currentItem() int // SetCurrent set the index of active tab SetCurrent(current int) // TabsLocation return the location of tabs. It returns one of the following values: HiddenTabs (0), // TopTabs (1), BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6) tabsLocation() int // TabsLocation set the location of tabs. Valid values: HiddenTabs (0), TopTabs (1), // BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6) SetTabsLocation(location int) // TabStyle() return styles of tab in the passive and the active state TabStyle() (string, string) SetTabStyle(tabStyle string, activeTabStyle string) */ } type tabsLayoutData struct { viewsContainerData //currentTab, tabsLocation int //tabStyle, activeTabStyle string tabListener []func(TabsLayout, int, int) tabCloseListener []func(TabsLayout, int) } // NewTabsLayout create new TabsLayout object and return it func NewTabsLayout(session Session) TabsLayout { view := new(tabsLayoutData) view.Init(session) return view } func newTabsLayout(session Session) View { return NewTabsLayout(session) } // Init initialize fields of ViewsContainer by default values func (tabsLayout *tabsLayoutData) Init(session Session) { tabsLayout.viewsContainerData.Init(session) tabsLayout.tag = "TabsLayout" tabsLayout.systemClass = "ruiTabsLayout" tabsLayout.tabListener = []func(TabsLayout, int, int){} tabsLayout.tabCloseListener = []func(TabsLayout, int){} } func (tabsLayout *tabsLayoutData) currentItem() int { result, _ := intProperty(tabsLayout, Current, tabsLayout.session, 0) return result } func (tabsLayout *tabsLayoutData) Get(tag string) interface{} { return tabsLayout.get(strings.ToLower(tag)) } func (tabsLayout *tabsLayoutData) get(tag string) interface{} { switch tag { case CurrentTabChangedEvent: return tabsLayout.tabListener case TabCloseEvent: return tabsLayout.tabCloseListener } return tabsLayout.viewsContainerData.get(tag) } func (tabsLayout *tabsLayoutData) Remove(tag string) { tabsLayout.remove(strings.ToLower(tag)) } func (tabsLayout *tabsLayoutData) remove(tag string) { switch tag { case CurrentTabChangedEvent: if len(tabsLayout.tabListener) > 0 { tabsLayout.tabListener = []func(TabsLayout, int, int){} tabsLayout.propertyChangedEvent(tag) } return case TabCloseEvent: if len(tabsLayout.tabCloseListener) > 0 { tabsLayout.tabCloseListener = []func(TabsLayout, int){} tabsLayout.propertyChangedEvent(tag) } return case Current: oldCurrent := tabsLayout.currentItem() delete(tabsLayout.properties, Current) if oldCurrent == 0 { return } if tabsLayout.created { tabsLayout.session.runScript(fmt.Sprintf("activateTab(%v, %d);", tabsLayout.htmlID(), 0)) for _, listener := range tabsLayout.tabListener { listener(tabsLayout, 0, oldCurrent) } } case Tabs: delete(tabsLayout.properties, Tabs) if tabsLayout.created { htmlID := tabsLayout.htmlID() updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle(), tabsLayout.session) updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle(), tabsLayout.session) updateCSSStyle(htmlID, tabsLayout.session) updateInnerHTML(htmlID, tabsLayout.session) } case TabStyle, CurrentTabStyle: delete(tabsLayout.properties, tag) if tabsLayout.created { htmlID := tabsLayout.htmlID() updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle(), tabsLayout.session) updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle(), tabsLayout.session) updateInnerHTML(htmlID, tabsLayout.session) } case TabCloseButton: delete(tabsLayout.properties, tag) if tabsLayout.created { updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session) } default: tabsLayout.viewsContainerData.remove(tag) return } tabsLayout.propertyChangedEvent(tag) } func (tabsLayout *tabsLayoutData) Set(tag string, value interface{}) bool { return tabsLayout.set(strings.ToLower(tag), value) } func (tabsLayout *tabsLayoutData) set(tag string, value interface{}) bool { if value == nil { tabsLayout.remove(tag) return true } switch tag { case CurrentTabChangedEvent: listeners := tabsLayout.valueToTabListeners(value) if listeners == nil { notCompatibleType(tag, value) return false } tabsLayout.tabListener = listeners case TabCloseEvent: listeners := tabsLayout.valueToCloseListeners(value) if listeners == nil { notCompatibleType(tag, value) return false } tabsLayout.tabCloseListener = listeners case Current: oldCurrent := tabsLayout.currentItem() if !tabsLayout.setIntProperty(Current, value) { return false } current := tabsLayout.currentItem() if oldCurrent == current { return true } if tabsLayout.created { tabsLayout.session.runScript(fmt.Sprintf("activateTab(%v, %d);", tabsLayout.htmlID(), current)) for _, listener := range tabsLayout.tabListener { listener(tabsLayout, current, oldCurrent) } } case Tabs: if !tabsLayout.setEnumProperty(Tabs, value, enumProperties[Tabs].values) { return false } if tabsLayout.created { htmlID := tabsLayout.htmlID() updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle(), tabsLayout.session) updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle(), tabsLayout.session) updateCSSStyle(htmlID, tabsLayout.session) updateInnerHTML(htmlID, tabsLayout.session) } case TabStyle, CurrentTabStyle: if text, ok := value.(string); ok { if text == "" { delete(tabsLayout.properties, tag) } else { tabsLayout.properties[tag] = text } } else { notCompatibleType(tag, value) return false } if tabsLayout.created { htmlID := tabsLayout.htmlID() updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle(), tabsLayout.session) updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle(), tabsLayout.session) updateInnerHTML(htmlID, tabsLayout.session) } case TabCloseButton: if !tabsLayout.setBoolProperty(tag, value) { return false } if tabsLayout.created { updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session) } default: return tabsLayout.viewsContainerData.set(tag, value) } tabsLayout.propertyChangedEvent(tag) return true } func (tabsLayout *tabsLayoutData) valueToTabListeners(value interface{}) []func(TabsLayout, int, int) { if value == nil { return []func(TabsLayout, int, int){} } switch value := value.(type) { case func(TabsLayout, int, int): return []func(TabsLayout, int, int){value} case func(TabsLayout, int): fn := func(view TabsLayout, current, old int) { value(view, current) } return []func(TabsLayout, int, int){fn} case func(TabsLayout): fn := func(view TabsLayout, current, old int) { value(view) } return []func(TabsLayout, int, int){fn} case func(int, int): fn := func(view TabsLayout, current, old int) { value(current, old) } return []func(TabsLayout, int, int){fn} case func(int): fn := func(view TabsLayout, current, old int) { value(current) } return []func(TabsLayout, int, int){fn} case func(): fn := func(view TabsLayout, current, old int) { value() } return []func(TabsLayout, int, int){fn} case []func(TabsLayout, int, int): return value case []func(TabsLayout, int): listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, current, old int) { val(view, current) } } return listeners case []func(TabsLayout): listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, current, old int) { val(view) } } return listeners case []func(int, int): listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, current, old int) { val(current, old) } } return listeners case []func(int): listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, current, old int) { val(current) } } return listeners case []func(): listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, current, old int) { val() } } return listeners case []interface{}: listeners := make([]func(TabsLayout, int, int), len(value)) for i, val := range value { if val == nil { return nil } switch val := val.(type) { case func(TabsLayout, int, int): listeners[i] = val case func(TabsLayout, int): listeners[i] = func(view TabsLayout, current, old int) { val(view, current) } case func(TabsLayout): listeners[i] = func(view TabsLayout, current, old int) { val(view) } case func(int, int): listeners[i] = func(view TabsLayout, current, old int) { val(current, old) } case func(int): listeners[i] = func(view TabsLayout, current, old int) { val(current) } case func(): listeners[i] = func(view TabsLayout, current, old int) { val() } default: return nil } } return listeners } return nil } func (tabsLayout *tabsLayoutData) valueToCloseListeners(value interface{}) []func(TabsLayout, int) { if value == nil { return []func(TabsLayout, int){} } switch value := value.(type) { case func(TabsLayout, int): return []func(TabsLayout, int){value} case func(int): fn := func(view TabsLayout, index int) { value(index) } return []func(TabsLayout, int){fn} case []func(TabsLayout, int): return value case []func(int): listeners := make([]func(TabsLayout, int), len(value)) for i, val := range value { if val == nil { return nil } listeners[i] = func(view TabsLayout, index int) { val(index) } } return listeners case []interface{}: listeners := make([]func(TabsLayout, int), len(value)) for i, val := range value { if val == nil { return nil } switch val := val.(type) { case func(TabsLayout, int): listeners[i] = val case func(int): listeners[i] = func(view TabsLayout, index int) { val(index) } default: return nil } } return listeners } return nil } func (tabsLayout *tabsLayoutData) tabsLocation() int { tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0) return tabs } func (tabsLayout *tabsLayoutData) inactiveTabStyle() string { if style, ok := stringProperty(tabsLayout, TabStyle, tabsLayout.session); ok { return style } switch tabsLayout.tabsLocation() { case LeftTabs, RightTabs: return "ruiInactiveVerticalTab" } return "ruiInactiveTab" } func (tabsLayout *tabsLayoutData) activeTabStyle() string { if style, ok := stringProperty(tabsLayout, CurrentTabStyle, tabsLayout.session); ok { return style } switch tabsLayout.tabsLocation() { case LeftTabs, RightTabs: return "ruiActiveVerticalTab" } return "ruiActiveTab" } func (tabsLayout *tabsLayoutData) ListSize() int { if tabsLayout.views == nil { tabsLayout.views = []View{} } return len(tabsLayout.views) } func (tabsLayout *tabsLayoutData) ListItem(index int, session Session) View { if tabsLayout.views == nil { tabsLayout.views = []View{} } if index < 0 || index >= len(tabsLayout.views) { return NewTextView(session, Params{ Text: "Invalid index", }) } views := []View{} page := tabsLayout.views[index] if icon, ok := stringProperty(page, Icon, session); ok && icon != "" { views = append(views, NewImageView(session, Params{ Source: icon, Row: 0, Column: 0, })) } title, ok := stringProperty(page, Title, session) if !ok || title == "" { title = "No title" } if !GetNotTranslate(tabsLayout, "") { title, _ = tabsLayout.Session().GetString(title) } views = append(views, NewTextView(session, Params{ Text: title, Row: 0, Column: 1, })) closeButton, _ := boolProperty(tabsLayout, TabCloseButton, tabsLayout.session) if close, ok := boolProperty(page, TabCloseButton, tabsLayout.session); ok { closeButton = close } if closeButton { views = append(views, NewGridLayout(session, Params{ Style: "ruiTabCloseButton", Row: 0, Column: 2, Content: "✕", ClickEvent: func() { // TODO close menu for _, listener := range tabsLayout.tabCloseListener { listener(tabsLayout, index) } }, })) } tabsHeight, _ := sizeConstant(session, "ruiTabHeight") return NewGridLayout(session, Params{ Height: tabsHeight, CellWidth: []SizeUnit{AutoSize(), Fr(1), AutoSize()}, CellVerticalAlign: CenterAlign, GridRowGap: Px(8), Content: views, }) } func (tabsLayout *tabsLayoutData) IsListItemEnabled(index int) bool { return true } // Append appends view to the end of view list func (tabsLayout *tabsLayoutData) Append(view View) { if tabsLayout.views == nil { tabsLayout.views = []View{} } if view != nil { tabsLayout.viewsContainerData.Append(view) if len(tabsLayout.views) == 1 { tabsLayout.properties[Current] = 0 for _, listener := range tabsLayout.tabListener { listener(tabsLayout, 0, -1) } defer tabsLayout.propertyChangedEvent(Current) } updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session) } } // Insert inserts view to the "index" position in view list func (tabsLayout *tabsLayoutData) Insert(view View, index int) { if tabsLayout.views == nil { tabsLayout.views = []View{} } if view != nil { if current := tabsLayout.currentItem(); current >= int(index) { tabsLayout.properties[Current] = current + 1 defer tabsLayout.propertyChangedEvent(Current) } tabsLayout.viewsContainerData.Insert(view, index) } } // Remove removes view from list and return it func (tabsLayout *tabsLayoutData) RemoveView(index int) View { if tabsLayout.views == nil { tabsLayout.views = []View{} return nil } i := int(index) count := len(tabsLayout.views) if i >= count { return nil } if count == 1 { view := tabsLayout.views[0] tabsLayout.views = []View{} for _, listener := range tabsLayout.tabListener { listener(tabsLayout, 0, 0) } tabsLayout.propertyChangedEvent(Current) return view } current := tabsLayout.currentItem() removeCurrent := (i == current) if i < current || (removeCurrent && i == count-1) { tabsLayout.properties[Current] = current - 1 for _, listener := range tabsLayout.tabListener { listener(tabsLayout, current-1, current) } defer tabsLayout.propertyChangedEvent(Current) } return tabsLayout.viewsContainerData.RemoveView(index) } func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) { tabsLayout.viewsContainerData.htmlProperties(self, buffer) buffer.WriteString(` data-inactiveTabStyle="`) buffer.WriteString(tabsLayout.inactiveTabStyle()) buffer.WriteString(`" data-activeTabStyle="`) buffer.WriteString(tabsLayout.activeTabStyle()) buffer.WriteString(`" data-current="`) buffer.WriteString(tabsLayout.htmlID()) buffer.WriteRune('-') buffer.WriteString(strconv.Itoa(tabsLayout.currentItem())) buffer.WriteRune('"') } func (tabsLayout *tabsLayoutData) cssStyle(self View, builder cssBuilder) { tabsLayout.viewsContainerData.cssStyle(self, builder) switch tabsLayout.tabsLocation() { case TopTabs: builder.add(`grid-template-rows`, `auto 1fr`) case BottomTabs: builder.add(`grid-template-rows`, `1fr auto`) case LeftTabs, LeftListTabs: builder.add(`grid-template-columns`, `auto 1fr`) case RightTabs, RightListTabs: builder.add(`grid-template-columns`, `1fr auto`) } } func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builder) { if tabsLayout.views == nil { return } //viewCount := len(tabsLayout.views) current := tabsLayout.currentItem() location := tabsLayout.tabsLocation() tabsLayoutID := tabsLayout.htmlID() if location != HiddenTabs { tabsHeight, _ := sizeConstant(tabsLayout.session, "ruiTabHeight") tabsSpace, _ := sizeConstant(tabsLayout.session, "ruiTabSpace") rowLayout := false buffer.WriteString(`
`) inactiveStyle := tabsLayout.inactiveTabStyle() activeStyle := tabsLayout.activeTabStyle() notTranslate := GetNotTranslate(tabsLayout, "") closeButton, _ := boolProperty(tabsLayout, TabCloseButton, tabsLayout.session) tabMargin := "" if tabsSpace.Type != Auto && tabsSpace.Value > 0 { if rowLayout { tabMargin = ` margin-right: ` + tabsSpace.cssString("") + ";" } else { tabMargin = ` margin-bottom: ` + tabsSpace.cssString("") + ";" } } var tabStyle, titleDiv string switch location { case LeftTabs, RightTabs: tabStyle = `display: grid; grid-template-rows: auto 1fr auto; align-items: center; justify-items: center; grid-row-gap: 8px;` case LeftListTabs, RightListTabs: tabStyle = `display: grid; grid-template-columns: auto 1fr auto; align-items: start; justify-items: center; grid-column-gap: 8px;` default: tabStyle = `display: grid; grid-template-columns: auto 1fr auto; align-items: center; justify-items: center; grid-column-gap: 8px;` } switch location { case LeftListTabs, RightListTabs: if tabsHeight.Type != Auto { tabStyle += ` height: ` + tabsHeight.cssString("") + ";" } } switch location { case LeftTabs: titleDiv = `
` case RightTabs: titleDiv = `
` default: titleDiv = `
` } last := len(tabsLayout.views) - 1 for n, view := range tabsLayout.views { icon, _ := stringProperty(view, Icon, tabsLayout.session) title, _ := stringProperty(view, Title, tabsLayout.session) if !notTranslate { title, _ = tabsLayout.Session().GetString(title) } buffer.WriteString(`
`) if icon != "" { switch location { case LeftTabs: buffer.WriteString(``) } buffer.WriteString(titleDiv) buffer.WriteString(title) buffer.WriteString(`
`) close, ok := boolProperty(view, TabCloseButton, tabsLayout.session) if !ok { close = closeButton } if close { buffer.WriteString(`
`) case RightTabs: buffer.WriteString(`grid-row-start: 3; grid-row-end: 4; grid-column-start: 1; grid-column-end: 2;">`) default: buffer.WriteString(`grid-row-start: 1; grid-row-end: 2; grid-column-start: 3; grid-column-end: 4;">`) } buffer.WriteString(`✕
`) } buffer.WriteString(`
`) } buffer.WriteString(`
`) } for n, view := range tabsLayout.views { buffer.WriteString(`
`) view.addToCSSStyle(map[string]string{`position`: `absolute`, `left`: `0`, `right`: `0`, `top`: `0`, `bottom`: `0`}) viewHTML(tabsLayout.views[n], buffer) buffer.WriteString(`
`) } } func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, data DataObject) bool { switch command { case "tabClick": if numberText, ok := data.PropertyValue("number"); ok { if number, err := strconv.Atoi(numberText); err == nil { current := tabsLayout.currentItem() if current != number { tabsLayout.properties[Current] = number for _, listener := range tabsLayout.tabListener { listener(tabsLayout, number, current) } tabsLayout.propertyChangedEvent(Current) } } } return true case "tabCloseClick": if numberText, ok := data.PropertyValue("number"); ok { if number, err := strconv.Atoi(numberText); err == nil { for _, listener := range tabsLayout.tabCloseListener { listener(tabsLayout, number) } } } return true } return tabsLayout.viewsContainerData.handleCommand(self, command, data) }