Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

save button collapse and smaller logo on small screens #316

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions libs/ff-ui/source/Dropdown.ts
Original file line number Diff line number Diff line change
@@ -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<IMenuItem | string> = [];

@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`<ff-menu class=${classes} .items=${this.items} itemIndex=${this.itemIndex} setFocus></ff-menu>` : 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;
}
}
}
142 changes: 142 additions & 0 deletions libs/ff-ui/source/Menu.ts
Original file line number Diff line number Diff line change
@@ -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<IMenuItem | string> = 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`<div class="ff-divider"></div>`;
}
else {
text = item.text;
icon = item.icon || (item.checked ? "check" : "empty");
}

return html`<ff-button index=${index} selectedIndex=${this.itemIndex}
icon=${icon} text=${text} @click=${this.onClick} @keydown=${this.onKeyDown}></ff-button>`;
}

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);
}
}
}
7 changes: 7 additions & 0 deletions libs/ff-ui/source/styles/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ button, input {
overflow-y: auto;
}

.ff-scroll-x{
overflow-x: auto;
}

.ff-position-above {
position: absolute;
bottom: 0;
Expand Down Expand Up @@ -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;
Expand Down
51 changes: 40 additions & 11 deletions source/client/ui/story/TaskBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";


////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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`
<img class="sv-story-logo" src=${this.assetReader.getSystemAssetUrl("images/voyager-75grey.svg")} alt="Logo"/>
<div class="sv-mode ff-text">${taskModeText}</div>
<sv-logo .assetPath=${this.assetReader.getSystemAssetUrl("")}></sv-logo>
<div class="sv-mode ff-text">
<span class="sv-mode-sm">${taskModeText.slice(0, 2)}</span>
<span class="sv-mode-lg">${taskModeText}</span>
</div>
<div class="sv-spacer"></div>
<div class="sv-divider"></div>
<div class="ff-flex-row ff-group" @click=${this.onClickTask}>
<div class="ff-flex-row ff-group ff-scroll-x" @click=${this.onClickTask}>
${tasks.map((task, index) => html`<ff-button text=${language.getLocalizedString(task.text)} icon=${task.icon} index=${index} ?selected=${task === activeTask}></ff-button>`)}
</div>
<div class="sv-divider"></div>
<div class="sv-spacer"></div>
<div class="sv-divider"></div>
<div class="ff-flex-row ff-group">
<ff-button text=${saveName} icon="save" @click=${this.onClickSave}></ff-button>
${downloadButtonVisible ? html`<ff-button text="${language.getLocalizedString("Download")}" icon="download" @click=${this.onClickDownload}></ff-button>` : null}
<div class="ff-flex-row ff-group" style="min-width:100px">
${1 < saveOptions.length?
html`<ff-dropdown caret text="${saveName}" icon="save" @select=${this.onSelectSave} .items=${saveOptions}></ff-dropdown>`
: html`<ff-button text="${saveOptions[0].text}" icon="${saveOptions[0].icon}" @click=${()=>this.onSelectSave(new CustomEvent("select", {detail: {item: saveOptions[0]}}))}></ff-button>`
}
${exitButtonVisible ? html`<ff-button text="${language.getLocalizedString("Exit")}" icon="exit" @click=${this.onClickExit}></ff-button>` : null}
</div>
`;
Expand All @@ -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()
Expand Down
Loading