diff --git a/libs/ff-ui/source/Dropdown.ts b/libs/ff-ui/source/Dropdown.ts new file mode 100644 index 00000000..6e913bf9 --- /dev/null +++ b/libs/ff-ui/source/Dropdown.ts @@ -0,0 +1,102 @@ +/** + * FF Typescript Foundation Library + * Copyright 2019 Ralph Wiedemeier, Frame Factory GmbH + * + * License: MIT + */ + +import { customElement, property, html, PropertyValues } from "./CustomElement"; + +import Button from "./Button"; +import "./Menu"; +import { IMenuItem } from "./Menu"; + +//////////////////////////////////////////////////////////////////////////////// + +export type DropdownDirection = "up" | "down"; +export type DropdownAlign = "left" | "right"; + +@customElement("ff-dropdown") +export default class Dropdown extends Button +{ + /** Direction of the dropdown menu. Possible values: "down" (default), "up". */ + @property({ type: String }) + direction: DropdownDirection = "down"; + + @property({ type: String }) + align: DropdownAlign = "left"; + + /** Items to be displayed in the dropdown menu. */ + @property({ attribute: false }) + items: Array = []; + + @property({ type: Number }) + itemIndex = -1; + + + constructor() + { + super(); + this.caret = true; + + this.onKeyOrPointer = this.onKeyOrPointer.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + protected firstConnected() + { + super.firstConnected(); + this.classList.add("ff-dropdown"); + } + + protected connected() + { + super.connected(); + document.addEventListener("pointerdown", this.onKeyOrPointer, { capture: true, passive: true }); + document.addEventListener("keyup", this.onKeyOrPointer, { capture: true, passive: true }); + } + + protected disconnected() + { + super.disconnected(); + document.removeEventListener("pointerdown", this.onKeyOrPointer); + document.removeEventListener("keyup", this.onKeyOrPointer); + } + + protected render() + { + const classes = (this.direction === "up" ? "ff-position-above " : "ff-position-below ") + + (this.align === "right" ? "ff-align-right" : "ff-align-left"); + + const menu = this.selected ? html`` : null; + return html`${super.render()}${menu}`; + } + + protected onClick(event: MouseEvent) + { + this.selected = !this.selected; + if (!this.selected) { + setTimeout(() => this.focus(), 0); + } + } + + + + protected onKeyDown(event: KeyboardEvent) + { + super.onKeyDown(event); + + // on escape key close the dropdown menu + if (event.code === "Escape" && this.selected) { + this.selected = false; + } + } + + protected onKeyOrPointer(event: UIEvent) + { + // if pointer goes down outside this close the dropdown menu + if (this.selected && !(event.target instanceof Node && this.contains(event.target))) { + this.selected = false; + } + } +} \ No newline at end of file diff --git a/libs/ff-ui/source/Menu.ts b/libs/ff-ui/source/Menu.ts new file mode 100644 index 00000000..7f6d64b2 --- /dev/null +++ b/libs/ff-ui/source/Menu.ts @@ -0,0 +1,142 @@ +/** + * FF Typescript Foundation Library + * Copyright 2019 Ralph Wiedemeier, Frame Factory GmbH + * + * License: MIT + */ + +import "./Button"; +import { IButtonClickEvent, IButtonKeyboardEvent } from "./Button"; + +import CustomElement, { customElement, property, html } from "./CustomElement"; + +//////////////////////////////////////////////////////////////////////////////// + +export interface IMenuItem +{ + index?: number; + name?: string; + text?: string; + icon?: string; + checked?: boolean; + disabled?: boolean; + divider?: boolean; + selectedIndex?: number; + selected?: boolean; +} + +export interface IMenuSelectEvent extends CustomEvent +{ + type: "select"; + target: Menu; + detail: { + item: IMenuItem; + } +} + +@customElement("ff-menu") +export default class Menu extends CustomElement +{ + static readonly iconChecked = "fas fa-check"; + + /** Optional name to identify the dropdown. */ + @property({ type: String }) + name = ""; + + /** Optional index to identify the dropdown. */ + @property({ type: Number }) + index = 0; + + /** Entries to be displayed in the dropdown menu. */ + @property({ attribute: false }) + items: Array = null; + + @property({ type: Number }) + itemIndex = -1; + + @property({ type: Boolean }) + setFocus = false; + + + protected firstConnected() + { + this.setAttribute("role", "menu"); + this.classList.add("ff-menu"); + } + + protected render() + { + if (!this.items) { + return html``; + } + + return html`${this.items.map((item, index) => this.renderItem(item, index))}`; + } + + protected renderItem(item: IMenuItem | string, index: number) + { + let text, icon; + + if (typeof item === "string") { + text = item; + icon = "empty"; + } + else if (item.divider) { + return html`
`; + } + else { + text = item.text; + icon = item.icon || (item.checked ? "check" : "empty"); + } + + return html``; + } + + updated() + { + if (this.setFocus) { + const index = this.itemIndex >= 0 ? this.itemIndex : 0; + this.focusItem(index); + } + } + + protected focusItem(index: number) + { + const child = this.children.item(index); + + if (child instanceof HTMLElement) { + child.focus(); + } + } + + protected onClick(event: IButtonClickEvent) + { + const item = this.items[event.target.index]; + + if (!item) { + return; + } + + this.dispatchEvent(new CustomEvent("select", { + detail: { item }, + bubbles: true + }) as IMenuSelectEvent); + } + + protected onKeyDown(event: IButtonKeyboardEvent) + { + const items = this.items; + + if (event.code === "ArrowDown") { + let index = event.target.index; + do { index = (index + 1) % items.length } while (items[index]["divider"]); + this.focusItem(index); + } + else if (event.code === "ArrowUp") { + let index = event.target.index; + do { index = (index + items.length - 1) % items.length } while (items[index]["divider"]); + this.focusItem(index); + } + } +} \ No newline at end of file diff --git a/libs/ff-ui/source/styles/_styles.scss b/libs/ff-ui/source/styles/_styles.scss index dd879772..97f8125a 100644 --- a/libs/ff-ui/source/styles/_styles.scss +++ b/libs/ff-ui/source/styles/_styles.scss @@ -277,6 +277,10 @@ button, input { overflow-y: auto; } +.ff-scroll-x{ + overflow-x: auto; +} + .ff-position-above { position: absolute; bottom: 0; @@ -486,6 +490,9 @@ button, input { justify-content: flex-start; margin: 0; padding: 4px 4px; + &.ff-control{ + flex-wrap: nowrap; + } .ff-icon { height: 1.2em; diff --git a/source/client/ui/story/TaskBar.ts b/source/client/ui/story/TaskBar.ts index 1b6ddde6..1b1d86dc 100644 --- a/source/client/ui/story/TaskBar.ts +++ b/source/client/ui/story/TaskBar.ts @@ -18,14 +18,16 @@ import System from "@ff/graph/System"; import "@ff/ui/Button"; +import "@ff/ui/Dropdown"; import Button, { IButtonClickEvent } from "@ff/ui/Button"; - import SystemView, { customElement, html } from "@ff/scene/ui/SystemView"; import CVStoryApplication from "../../components/CVStoryApplication"; import CVTaskProvider, { ETaskMode, IActiveTaskEvent, ITaskSetEvent } from "../../components/CVTaskProvider"; import CVAssetReader from "../../components/CVAssetReader"; import CVLanguageManager from "client/components/CVLanguageManager"; +import { IMenuItem } from "@ff/ui/Menu"; +import CVSetup from "client/components/CVSetup"; //////////////////////////////////////////////////////////////////////////////// @@ -77,24 +79,39 @@ export default class TaskBar extends SystemView const activeTask = this.taskProvider.activeComponent; const taskMode = this.taskProvider.ins.mode.value; const taskModeText = this.taskProvider.ins.mode.getOptionText(); - const downloadButtonVisible = taskMode !== ETaskMode.Standalone; const exitButtonVisible = taskMode !== ETaskMode.Standalone; const language = this.language; const saveName = language.getLocalizedString(taskMode !== ETaskMode.Standalone ? "Save" : "Download"); + + const saveOptions :IMenuItem[] = [ + {name: "download", icon:"download", text:language.getLocalizedString("Download")} + ]; + if(taskMode !== ETaskMode.Standalone){ + saveOptions.unshift( + {name: "save", icon: "save", text: saveName}, + {name: "capture", icon: "save", text: language.getLocalizedString("Save Setup")}, + ); + } + return html` - -
${taskModeText}
+ +
+ ${taskModeText.slice(0, 2)} + ${taskModeText} +
-
+
${tasks.map((task, index) => html``)}
-
- - ${downloadButtonVisible ? html`` : null} +
+ ${1 < saveOptions.length? + html`` + : html`this.onSelectSave(new CustomEvent("select", {detail: {item: saveOptions[0]}}))}>` + } ${exitButtonVisible ? html`` : null}
`; @@ -108,9 +125,21 @@ export default class TaskBar extends SystemView } } - protected onClickSave() - { - this.story.ins.save.set(); + protected onSelectSave(event :{detail: {item: IMenuItem}}){ + switch(event.detail.item.name){ + case "save": + this.story.ins.save.set(); + break; + case "capture": + this.system.getComponent(CVSetup).ins.saveState.set(); + this.story.ins.save.set(); + break; + case "download": + this.story.ins.download.set(); + break; + default: + console.warn("Unhandled save method : ", event.detail.item.name); + } } protected onClickDownload() diff --git a/source/client/ui/story/styles.scss b/source/client/ui/story/styles.scss index 8612f9d4..eebd27b8 100644 --- a/source/client/ui/story/styles.scss +++ b/source/client/ui/story/styles.scss @@ -88,16 +88,24 @@ ff-tab-header, ff-dock-panel-header { background-color: $color-background-dark; border-bottom: 3px solid $color-background-darker; - overflow: auto; + overflow: clip visible; .ff-group { align-items: stretch; } - .sv-story-logo { - height: 28px !important; + .sv-logo{ + height: 28px; + min-width: 28px; + flex: 0 50 126px; margin: 8px; align-self: center; + .sv-short{ + max-width: 28px; + } + .sv-full{ + flex: 1 0 126px; + } } .sv-mode { @@ -108,6 +116,20 @@ ff-tab-header, ff-dock-panel-header { margin: 4px; background-color: darken($color-secondary, 5%); color: $color-background-dark; + .sv-mode-sm{ + display: none; + } + } + + @media screen and (max-width: 800px){ + .sv-mode { + .sv-mode-sm{ + display: inherit; + } + .sv-mode-lg{ + display: none; + } + } } .sv-spacer { @@ -138,6 +160,12 @@ ff-tab-header, ff-dock-panel-header { } } } + + @media screen and (max-width: 850px){ + .ff-button { + padding: 4px 2px; + } + } } //////////////////////////////////////////////////////////////////////////////// @@ -542,3 +570,18 @@ $color-component-meta-light: #d9d998; } +.sv-hover-dropdown{ + display: flex; + position: relative; + > ul { + display: none; + position: absolute; + top: 100%; + left: 0; + } + &.show > ul{ + display: inherit; + } + +} +