mirror of https://github.com/onivim/oni.git
359 lines
9.0 KiB
TypeScript
359 lines
9.0 KiB
TypeScript
/**
|
|
* SidebarStore.ts
|
|
*
|
|
* State management for the sidebar split
|
|
*/
|
|
|
|
import { Reducer, Store } from "redux"
|
|
import { createStore as createReduxStore } from "./../../Redux"
|
|
|
|
import { Configuration } from "../Configuration"
|
|
import { DefaultConfiguration } from "../Configuration/DefaultConfiguration"
|
|
import { WindowManager, WindowSplitHandle } from "./../WindowManager"
|
|
import { SidebarContentSplit } from "./SidebarContentSplit"
|
|
import { SidebarSplit } from "./SidebarSplit"
|
|
|
|
import * as Oni from "oni-api"
|
|
|
|
export interface ISidebarState {
|
|
entries: ISidebarEntry[]
|
|
|
|
// Active means that the tab is currently selected
|
|
activeEntryId: string
|
|
|
|
isActive: boolean
|
|
|
|
width: string
|
|
}
|
|
|
|
export type SidebarIcon = string
|
|
|
|
export interface ISidebarEntry {
|
|
// TODO: Remove this, duplicated between here and `SidebarPane`
|
|
id: string
|
|
icon: SidebarIcon
|
|
pane: SidebarPane
|
|
hasNotification?: boolean
|
|
}
|
|
|
|
export interface SidebarPane extends Oni.IWindowSplit {
|
|
id: string
|
|
title: string
|
|
|
|
enter(): void
|
|
leave(): void
|
|
}
|
|
|
|
export class SidebarManager {
|
|
private _store: Store<ISidebarState>
|
|
|
|
private _iconSplit: WindowSplitHandle
|
|
private _contentSplit: WindowSplitHandle
|
|
|
|
public get activeEntryId(): string {
|
|
return this._store.getState().activeEntryId
|
|
}
|
|
|
|
public get entries(): ISidebarEntry[] {
|
|
return this._store.getState().entries
|
|
}
|
|
|
|
get isFocused(): boolean {
|
|
return this._contentSplit.isFocused
|
|
}
|
|
|
|
public get store(): Store<ISidebarState> {
|
|
return this._store
|
|
}
|
|
|
|
constructor(
|
|
private _windowManager: WindowManager = null,
|
|
private _configuration: Configuration,
|
|
) {
|
|
this._store = createStore()
|
|
|
|
this._configuration.onConfigurationChanged.subscribe(val => {
|
|
if (typeof val["sidebar.width"] === "string") {
|
|
this.setWidth(val["sidebar.width"])
|
|
}
|
|
})
|
|
|
|
this.setWidth(this._configuration.getValue("sidebar.width"))
|
|
|
|
this._iconSplit = this._windowManager.createSplit("left", new SidebarSplit(this))
|
|
this._contentSplit = this._windowManager.createSplit("left", new SidebarContentSplit(this))
|
|
}
|
|
|
|
public increaseWidth(): void {
|
|
if (this._contentSplit.isVisible) {
|
|
this.store.dispatch({ type: "INCREASE_WIDTH" })
|
|
}
|
|
}
|
|
|
|
public decreaseWidth(): void {
|
|
if (this._contentSplit.isVisible) {
|
|
this.store.dispatch({ type: "DECREASE_WIDTH" })
|
|
}
|
|
}
|
|
|
|
public setWidth(width: string): void {
|
|
if (width) {
|
|
this._store.dispatch({
|
|
type: "SET_WIDTH",
|
|
width,
|
|
})
|
|
}
|
|
}
|
|
|
|
public setNotification(id: string): void {
|
|
if (id) {
|
|
this._store.dispatch({
|
|
type: "SET_NOTIFICATION",
|
|
id,
|
|
})
|
|
}
|
|
}
|
|
|
|
public setActiveEntry(id: string): void {
|
|
if (id) {
|
|
const oldId = this._store.getState().activeEntryId
|
|
|
|
this._store.dispatch({
|
|
type: "SET_ACTIVE_ID",
|
|
activeEntryId: id,
|
|
})
|
|
|
|
if (oldId !== id) {
|
|
this._contentSplit.show()
|
|
} else if (!this._contentSplit.isVisible) {
|
|
this._contentSplit.show()
|
|
} else if (this._contentSplit.isVisible) {
|
|
this._contentSplit.hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
public focusContents(): void {
|
|
if (this._contentSplit.isVisible) {
|
|
this._contentSplit.focus()
|
|
}
|
|
}
|
|
|
|
public toggleSidebarVisibility(): void {
|
|
if (this._contentSplit.isVisible) {
|
|
this._contentSplit.hide()
|
|
|
|
if (this._contentSplit.isFocused) {
|
|
this._iconSplit.focus()
|
|
}
|
|
} else {
|
|
this._contentSplit.show()
|
|
}
|
|
}
|
|
|
|
public toggleVisibilityById(id: string): void {
|
|
if (id) {
|
|
if (id !== this.activeEntryId) {
|
|
this._store.dispatch({
|
|
type: "SET_ACTIVE_ID",
|
|
activeEntryId: id,
|
|
})
|
|
|
|
this._contentSplit.show()
|
|
this._contentSplit.focus()
|
|
} else {
|
|
if (this._contentSplit.isVisible) {
|
|
this._contentSplit.hide()
|
|
} else {
|
|
// In some cases you can have an ACTIVE entry that is hidden
|
|
this._contentSplit.show()
|
|
this._contentSplit.focus()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public enter(): void {
|
|
this._store.dispatch({ type: "ENTER" })
|
|
}
|
|
|
|
public leave(): void {
|
|
this._store.dispatch({ type: "LEAVE" })
|
|
}
|
|
|
|
public add(icon: SidebarIcon, pane: SidebarPane): void {
|
|
const entry = {
|
|
id: pane.id,
|
|
icon,
|
|
pane,
|
|
}
|
|
this._store.dispatch({
|
|
type: "ADD_ENTRY",
|
|
entry,
|
|
})
|
|
}
|
|
|
|
public hide(): void {
|
|
this._contentSplit.hide()
|
|
this._iconSplit.hide()
|
|
}
|
|
}
|
|
|
|
const DefaultSidebarState: ISidebarState = {
|
|
entries: [],
|
|
activeEntryId: null,
|
|
isActive: false,
|
|
width: null,
|
|
}
|
|
|
|
export type SidebarActions =
|
|
| {
|
|
type: "SET_ACTIVE_ID"
|
|
activeEntryId: string
|
|
}
|
|
| {
|
|
type: "ADD_ENTRY"
|
|
entry: ISidebarEntry
|
|
}
|
|
| {
|
|
type: "SET_NOTIFICATION"
|
|
id: string
|
|
}
|
|
| {
|
|
type: "SET_WIDTH"
|
|
width: string
|
|
}
|
|
| {
|
|
type: "ENTER"
|
|
}
|
|
| {
|
|
type: "LEAVE"
|
|
}
|
|
| {
|
|
type: "INCREASE_WIDTH"
|
|
}
|
|
| {
|
|
type: "DECREASE_WIDTH"
|
|
}
|
|
|
|
export const changeSize = (change: "increase" | "decrease") => (
|
|
size: string,
|
|
defaultValue = DefaultConfiguration["sidebar.width"],
|
|
): string => {
|
|
const [numberString, letters = "em"] = size.match(/[a-zA-Z]+|[0-9]+/g)
|
|
const isAllowedUnit = ["em", "px", "vw"].includes(letters)
|
|
const unitsToUse = isAllowedUnit ? letters : "em"
|
|
const convertedNumber = Number(numberString)
|
|
if (isNaN(convertedNumber)) {
|
|
return defaultValue
|
|
}
|
|
|
|
// If too small don't allow a decrease and vice versa
|
|
const tooSmall = convertedNumber - 1 < 1 && change === "decrease"
|
|
const tooBig = convertedNumber + 1 > 50 && change === "increase"
|
|
|
|
const changed =
|
|
tooBig || tooSmall
|
|
? convertedNumber
|
|
: change === "increase"
|
|
? convertedNumber + 1
|
|
: convertedNumber - 1
|
|
|
|
return `${changed}${unitsToUse}`
|
|
}
|
|
|
|
export const increaseWidth = changeSize("increase")
|
|
export const decreaseWidth = changeSize("decrease")
|
|
|
|
export const sidebarReducer: Reducer<ISidebarState> = (
|
|
state: ISidebarState = DefaultSidebarState,
|
|
action: SidebarActions,
|
|
) => {
|
|
const newState = {
|
|
...state,
|
|
entries: entriesReducer(state.entries, action),
|
|
}
|
|
|
|
switch (action.type) {
|
|
case "ENTER":
|
|
return {
|
|
...newState,
|
|
isActive: true,
|
|
}
|
|
case "LEAVE":
|
|
return {
|
|
...newState,
|
|
isActive: false,
|
|
}
|
|
case "SET_WIDTH":
|
|
return {
|
|
...newState,
|
|
width: action.width,
|
|
}
|
|
case "SET_ACTIVE_ID":
|
|
return {
|
|
...newState,
|
|
activeEntryId: action.activeEntryId,
|
|
}
|
|
case "ADD_ENTRY":
|
|
if (!state.activeEntryId) {
|
|
return {
|
|
...newState,
|
|
activeEntryId: action.entry.pane.id,
|
|
}
|
|
} else {
|
|
return newState
|
|
}
|
|
case "DECREASE_WIDTH":
|
|
return {
|
|
...newState,
|
|
width: decreaseWidth(newState.width),
|
|
}
|
|
case "INCREASE_WIDTH":
|
|
return {
|
|
...newState,
|
|
width: increaseWidth(newState.width),
|
|
}
|
|
default:
|
|
return newState
|
|
}
|
|
}
|
|
|
|
export const entriesReducer: Reducer<ISidebarEntry[]> = (
|
|
state: ISidebarEntry[] = [],
|
|
action: SidebarActions,
|
|
) => {
|
|
switch (action.type) {
|
|
case "ADD_ENTRY":
|
|
return [...state, action.entry]
|
|
case "SET_ACTIVE_ID":
|
|
return state.map(e => {
|
|
if (e.id === action.activeEntryId) {
|
|
return {
|
|
...e,
|
|
hasNotification: false,
|
|
}
|
|
} else {
|
|
return e
|
|
}
|
|
})
|
|
case "SET_NOTIFICATION":
|
|
return state.map(e => {
|
|
if (e.id !== action.id) {
|
|
return e
|
|
} else {
|
|
return {
|
|
...e,
|
|
hasNotification: true,
|
|
}
|
|
}
|
|
})
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
export const createStore = (): Store<ISidebarState> => {
|
|
return createReduxStore("Sidebar", sidebarReducer, DefaultSidebarState)
|
|
}
|