# Floating Windows

A component that provides a windowing interface for browser content.
We provide the windows, you provide the content.

# Screenshots

## In Browser

<img src="./readme/image1.png" width="400" >

## Popped out

<img src="./readme/image2.png" width="400" >

## Interaction

<img src="./readme/floating-window.gif" width="400" >

## Features

- Can open multiple windows at a time.
- Can drag windows around inside the browser window by dragging on title bar.
- Re-size windows inside the browser window by dragging window edges.
- Pop out window, so the content is inside a separate browser window.
- Pop in window, so the popped out window returns to the original browser window.
- Click on a window to bring to front.
- Has a title bar, with title and action buttons.
- Has a close button and pop out button in title bar.
- Windows can start "popped out".
- Event listener, so you can hook your own code into UI events.
- Example code.
- Storybook examples.
- Not tied to LUI for styles/components (you can provide styles and/or components)

## Planned Features

- Provision for other controls inside the header.
- Some way of allowing this control to work in a mobile setting (either disabled, or mobile aware?)
- Style agnostic - style how you want.
- Additional content in title area.
- Ability to set the base Z value.
- Able to get/set entire windows state, so application can, say, store to local storage.
- Can define initial defaults to the context.
- Beefing up the event system (more events, multiple listeners.)
- Listeners can cancel events.
- Ability to open the window as a draggable window, a panel attached to the side, or a panel attached to the bottom.
- Keyboard controls.

<div style="page-break-after: always;"></div>

# Storybook

The link to the latest storybook is [here](https://master--6297c9d4111d26004a93eb6b.chromatic.com). This is updated automatically when changes are pushed to master.

# Installing

```bash
npm install --save @linzjs/floating-windows
```

# Styles

You'll also need to include the styles somewhere (like `main.ts` or similar) to ensure that the components are rendered as they should.

```javascript
import "node_modules/@linzjs/floating-windows/dist/style.css";
```

# Quick Example

### Surround your app content with the window context provider.

```typescript
import { FloatingWindowContextProvider } from "@linzjs/floating-windows";
import "@linzjs/floating-windows/dist/style.css";

export const App = () => {
  return (
    <FloatingWindowContextProvider>
      <AppPage />
    </FloatingWindowContextProvider>
  );
};

// Name of window (we use this below)
const windowAName = "Win A";
```

<div style="page-break-after: always;"></div>

### Create a floating window component with your content as children

```typescript
import { FloatingWindow } from "@linzjs/floating-windows";

export const FloatingWindowExample = () => {
  return (
    <FloatingWindow
      name={windowAName}
      title={"This is window A"}
      initialSize={{ width: 400, height: 400 }}
      initialPosition={{ x: 100, y: 100 }}
    >
      <div>Lorem ipsum dolor sit amet, consectetur...</div>
    </FloatingWindow>
  );
};
```

In this example the content is initially not displayed. To display it, you'll need to show the window,
which can be done by specifying the prop `startDisplayed` or programmatically.

E.g. Here's a button that does just that. It uses the context to show and hide the window,
and to find out if the window is currently open or not.

## A button to show/hide the window

```typescript
import { useFloatingWindow } from "@linzjs/floating-windows";

const TestButton = (props: { windowName: string }): JSX.Element => {
  const { showWindow, hideWindow, isWindowOpen } = useFloatingWindow();

  const isOpen = isWindowOpen(props.windowName);
  return (
    <button
      style={{ color: isOpen ? "red" : "green" }}
      onClick={() => {
        if (isOpen) {
          hideWindow(props.windowName);
        } else {
          showWindow(props.windowName);
        }
      }}
    >
      {props.windowName}
    </button>
  );
};
```

<div style="page-break-after: always;"></div>

# API

Notes:

- Windows are identified by a unique internal name, which can be different to the displayed title.

# Window Context

```typescript
export type FloatingWindowContextType = {
  setWindowDefaults: (windowName: string, windowProperties: FloatingWindowProperties) => void;
  getOpenWindowNames: () => string[];
  isWindowOpen: (windowName: string) => boolean;
  isWindowPoppedOut: (windowName: string) => boolean;
  showWindow: (windowName: string, showPoppedOut?: boolean) => void;
  hideWindow: (windowName: string | string[]) => void;
  bringWindowToTop: (windowName: string) => void;
  getTopWindowName: () => string | null;
  getWindowPosition: (windowName: string) => FloatingWindowPosition;
  setWindowPosition: (windowName: string, position: FloatingWindowPosition) => void;
  getPositionalStyle: (windowName: string) => CSS.Properties;
  getWindowSize: (windowName: string) => FloatingWindowSize;
  setWindowSize: (windowName: string, dimensions: FloatingWindowSize) => void;
  externalWindowsDOM: Record<string, Document>;
  setExternalWindowsDOM: React.Dispatch<React.SetStateAction<Record<string, Document>>>;
  popWindowOut: (windowName: string) => void;
  popWindowIn: (windowName: string) => void;
  getZ: (windowName: string) => number;
  addEventListener: (
    listenerId: string,
    eventType: FloatingWindowEventType,
    windowName: string,
    eventCallback: FloatingWindowCallback,
  ) => void;
  removeEventListener: (
    listenerId: string | null,
    eventType: FloatingWindowEventType | null,
    windowName: string | null,
  ) => void;
};
```

| Method                                                                                                                                           | Description                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `setWindowDefaults: (windowName: string, windowProperties: FloatingWindowProperties) => void`                                                    | Set initial defaults for windows.                                                                                          |
| `getOpenWindowNames: () => string[]`                                                                                                             | Return a list of the open window names.                                                                                    |
| `isWindowOpen: (windowName: string) => boolean`                                                                                                  | Returns true if the window named `windowName` is open. False otherwise                                                     |
| `isWindowPoppedOut: (windowName: string) => boolean`                                                                                             | Returns true if the window named `windowName` is popped out. False otherwise.                                              |
| `showWindow: (windowName: string, showPoppedOut?: boolean) => void`                                                                              | Show window named `windowName`, bringing it to the front. If `showPoppedOut` is true then the window is started popped out |
| `hideWindow: (windowName: string) => void`                                                                                                       | Hide window named `windowName`, closing it.                                                                                |
| `bringWindowToTop: (windowName: string) => void`                                                                                                 | Bring the window named `windowName` to the front of the display                                                            |
| `getTopWindowName: () => string \| null`                                                                                                         | Return the name of the top-most window, or null if there is none.                                                          |
| `getWindowPosition: (windowName: string) => FloatingWindowPosition`                                                                              | Return the position of the window named `windowName`                                                                       |
| `setWindowPosition: (windowName: string, position: FloatingWindowPosition) => void`                                                              | Set the position of the window `windowName` to `position`                                                                  |
| `getPositionalStyle: (windowName: string) => CSS.Properties`                                                                                     | Return a CSS style that includes the window position for window `windowName`                                               |
| `getWindowSize: (windowName: string) => FloatingWindowSize`                                                                                      | Return the size of the window named `windowName`                                                                           |
| `setWindowSize: (windowName: string, dimensions: FloatingWindowSize) => void`                                                                    | Set the size of the window called `windowName` to `dimensions`                                                             |
| `externalWindowsDOM: Record<string, Document>;`                                                                                                  |                                                                                                                            |
| `setExternalWindowsDOM: React.Dispatch<React.SetStateAction<Record<string, Document>>>`                                                          |                                                                                                                            |
| `popWindowOut: (windowName: string) => void`                                                                                                     | Pop the window named `windowName` out into it's own window.                                                                |
| `popWindowIn: (windowName: string) => void`                                                                                                      | Pop the window named `windowName` back into the browser.                                                                   |
| `getZ: (windowName: string) => number`                                                                                                           | Return the Z index of the window named `windowName`                                                                        |
| `addEventListener: ( listenerId: string, eventType: FloatingWindowEventType, windowName: string, eventCallback: FloatingWindowCallback) => void` | See [below](#events)                                                                                                       |
| `removeEventListener: ( listenerId: string \| null, eventType: FloatingWindowEventType \| null, windowName: string \| null) => void`             | See [below](#events)                                                                                                       |

# Components

## FloatingWindow

### Props

```typescript
interface FloatingWindowProps {
  name: string;
  title?: string;
  children?: JSX.Element;
  extraHeaderButtons?: ReactNode;
  initialPosition: FloatingWindowPosition;
  initialSize: FloatingWindowSize;
  startPoppedOut?: boolean;
  startDisplayed?: boolean;
}
```

| Prop                                      | Description                                                                                                                         |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `name: string`                            | Unique (internal) name of this window                                                                                               |
| `title?: string`                          | The displayed title in the window. No title bar is displayed if the title is not given.                                             |
| `children?: JSX.Element`                  | The window content.                                                                                                                 |
| `extraHeaderButtons?: ReactNode`          | Any extra buttons in the header.                                                                                                    |
| `initialPosition: FloatingWindowPosition` | Initial position (X,Y coordinates.)                                                                                                 |
| `initialSize: FloatingWindowSize`         | Initial size (width and height.)                                                                                                    |
| `startPoppedOut?: boolean`                | Set to true to start the window popped out. False to keep in in the browser, and undefined to use the last state the window was in. |
| `startDisplayed? boolean`                 | Set to true to start the window as displayed (shown.) False to start the window as hidden.                                          |

# Events

The window context has the ability to call user-supplied callbacks when internal events happen,
such as windows being popped out. A listener listens for an event type for a particular window, you specify both when adding the listener.

The `addEventListener` method allows you to add a listener to an event, and `removeEventListener` removes it. You can add multiple listeners to a single event/window combination.
Each listener you add is identified by a `listenerId:string`, allowing you to selectively remove listeners from a window/event.

```typescript
// Add a listener to showWindow event for window named "windowA"
addEventListener("myShow", FloatingWindowEventType.showWindow, "windowA", (event) => {
  console.log(`${event.windowName} shown`);
});
```

```typescript
// Remove just the listener added above
removeEventListener("myShow", FloatingWindowEventType.showWindow, "windowA");

// Remove all showWindow listeners for "windowA"
removeEventListener(null, FloatingWindowEventType.showWindow, "windowA");
```

The `removeEventListener` allows you to pass `null` for any/all of the window/event/listenerId to act as a don't care value. E.g.

```typescript
// Remove all listeners from the show window event, for all windows
windowContext.removeEventListener(null, FloatingWindowEventType.showWindow, null);
// Remove all listeners for :windowA"
windowContext.removeEventListener(null, null, "windowA");
// Remove all listeners (all windows)
windowContext.removeEventListener(null, null, null);
```

## Event Types

```typescript
enum FloatingWindowEventType {
  showWindow,
  hideWindow,
  popoutWindow,
}
```

| Event          | Description                 |
| -------------- | --------------------------- |
| `showWindow`   | When a window is shown      |
| `hideWindow`   | When a window is hidden     |
| `popoutWindow` | When a window is popped out |

# Development

## Build Locally

To build a package locally (creates `linzjs-floating-windows-0.0.0.tgz`)

```bash
npm run pack
```

it uses vite to build the javascript and css files, and typescript compiler to create the type definitions.
You can include this directly into your `package.json` file like so,

```json
  "dependencies": {
    "@linzjs/floating-windows": "file:linzjs-floating-windows-0.0.0.tgz"
  },
```

The code in the example folder does just that.

## Example Code

Inside `examples/basic` (included in the package itself) is a working example of using the package. Note it needs the package already created (as above.)

```bash
cd examples/basic
npm i
npm run dev
```

This gives you the basic vite/react application in your browser.

- Navigate to http://localhost:3000/ to display the example app with a locally generated package.
- Navigate to http://localhost:3000/local/index.html to see a version of the app which uses the package source included directly. This allows you to edit the package source and see results immediately (Without having to build a package.)

By displaying the second page, any changes to the package code will immediately be seen in the dev environment. The first page will remain unchanged as it uses the generated package until you regenerate it with `npm run pack-update-example`. This re-builds the package locally, and updates the example code without having to publish a new version to the world. (You don't have to worry about changing the version number each time.)

## Publishing

Publishing is taken care of by the github action (`.github/workflows/push.yml`) This runs the tests, builds a package, and releases it if there's any changes to the master branch that fit the semantic versioning (See [here](https://semantic-release.gitbook.io/semantic-release/)) - it also handles updating the version numbers.

## Storybook

Storybook is included as a development dependency. To build the storybook site locally

```bash
npm run build-storybook
```

And to display it

```
npm run storybook
```

There's a github action (`.github/workflows/publish-to-chromatic.yml`) that automates the process of publishing our storybook to chromatic, and running the UI tests.
