diff --git a/.github/ISSUE_TEMPLATE/newfeature.md b/.github/ISSUE_TEMPLATE/newfeature.md new file mode 100644 index 00000000..e7eeeefa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/newfeature.md @@ -0,0 +1,81 @@ +--- +name: ๐Ÿš€ Feature Request +about: Suggest a new feature or enhancement for Flexycakes +title: "[FEATURE] " +labels: "enhancement, needs-triage" +assignees: "" +--- + +## ๐ŸŽฏ Feature Summary + + + +## ๐Ÿค” Problem Statement + + + +**Is your feature request related to a problem?** + +**Example scenario:** + + + +## ๐Ÿ’ก Proposed Solution + + + +**What would you like to happen?** + +**How would it work?** + + + +## ๐Ÿ”„ Alternatives Considered + + + +## ๐Ÿ“‹ Implementation Details + + + +**Affected Components:** + +- [ ] Layout engine +- [ ] Tab/Tabset functionality +- [ ] Drag & Drop +- [ ] Theming/Styling +- [ ] API/Actions +- [ ] Documentation +- [ ] Other: ****\_\_\_**** + +**Breaking Changes:** + +- [ ] This would be a breaking change +- [ ] This is backward compatible +- [ ] Not sure + +## ๐ŸŽจ Mockups/Examples + + + +## ๐ŸŒŸ Benefits + + + +- **For Users:** +- **For Developers:** +- **For the Project:** + +## ๐Ÿ“š Additional Context + + + +--- + +### ๐Ÿ“ Contributor Notes + + + +- Check if this feature aligns with [project roadmap](https://github.com/users/powerdragonfire/projects/10/views/1) +- Consider backward compatibility with original FlexLayout +- Review impact on bundle size and performance diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dadcff4..e5e95a9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,447 +1,30 @@ -## 0.8.17 - 2025-05-03 +# Change Log -* **Fixed:** Issues with tab redraw and scroll when page is scrolled down (corrects fix for [#488]) +## 1.2.1 -## 0.8.16 - 2025-04-30 +- **Fixed:** v0.9 Hidden Tabset Fix, courtesy of @Lukas Gรถtz. See PR Conversation [here](https://github.com/caplin/FlexLayout/pull/485). -* **Fixed:** [#488](https://github.com/caplin/FlexLayout/issues/488) Wrong tab position on scroll +## 1.2.0 -## 0.8.15 - 2025-04-25 +- **Enhanced:** README enhancements - new Kibana, Discord, LOGO! +- **Enhanced:** Watch script to support Symbolic Linking with HRM for local development +- **Fixed:** BorderTabSet 'not all code path returns something' fix. -* **Added:** Option to tabset customization to allow the creation of a control to the left of the tabs (see the 'New Features' layout in the demo for an example). -* **Changed:** Only re-render tabs when they are visible. +## 1.1.0 - NPM Publish Error -## 0.8.14 - 2025-04-18 +## 1.0.0 -* **Removed:** UMD builds. -* **Changed:** Package type is now 'module'. -* **Changed:** Demo build now uses Vite. -* **Added:** Icons are now exported. -* **Added:** Error boundary now has a retry button. +- **Added:** New global boolean variable `tabSetEnableHideWhenEmpty` to control hiding of empty tabsets. +- **Added:** New optional boolean attribute `enableHideWhenEmpty` on `ITabSetAttribute` that inherits from the global `tabSetEnableHideWhenEmpty` setting. +- **Added:** New "ecmind" layout demonstrating row node with three tabsets (left and right empty, middle filled) with `tabSetEnableHideWhenEmpty` enabled. +- **Enhanced:** Row.tsx component now supports hiding empty tabsets when they have no children and `enableHideWhenEmpty` is set to true. +- **Added:** Demo functionality with "Add to left empty Tabset" and "Add to right empty Tabset" buttons to demonstrate dynamic tabset visibility. +- **Feature:** Empty tabsets can now serve as placeholders for future tab insertion, appearing only when populated with content. +- **Use Case:** Enables programmatic opening of tabs in placeholder tabsets for complex layouts like PDF editors with side panels. -## 0.8.13 - 2025-04-15 (deprecated) +## 0.9.0 -Published with missing types in module exports +- **Added:** Pin/unpin feature for side (left/right) and bottom panels. Users can now keep panels permanently visible ("pinned") or allow them to collapse automatically ("unpinned"), similar to behavior in modern IDEs and dashboard layouts. +- **Enhanced:** Improved layout flexibility by allowing users to control panel visibility behavior, helping to streamline workflows and reduce visual clutter in complex layouts. -## 0.8.12 - 2025-04-15 - -* **Updated:** Dependencies. -* **Fixed:** Initial tab flash. -* **Disabled:** Popout of MUI tabs in demo (because Emotion generated styles in production cannot be copied to the popout window). -* **Converted:** Cypress tests to Playwright. -* **Updated:** Demo app to use React hooks. - -## 0.8.11 - 2025-04-09 (deprecated) - -Published with additional files by mistake. - -## 0.8.10 - 2025-04-09 - -* **Fixed:** [#481](https://github.com/caplin/FlexLayout/issues/481) Numpad Enter doesn't confirm rename. -* **Workaround:** Addressed a `` issue in React 19 ([https://github.com/facebook/react/issues/29585](https://github.com/facebook/react/issues/29585)) causing tabs to re-mount when moved. - -## 0.8.9 - 2025-04-04 - -* **Fixed:** [#480](https://github.com/caplin/FlexLayout/issues/480) `Actions.selectTab` is called when closing a Tab. -* **Added:** `isVisible()` method to `TabNode`. - -## 0.8.8 - 2025-03-22 - -* **Enabled:** Escape key to close the overflow menu. -* **Prevented:** Initial reposition flash when there are hidden tabs. -* **Removed:** Roboto font from the demo. - -## 0.8.7 - 2025-03-17 - -* **Improved:** Tab scrolling into the visible area. -* **Added:** Sections about tab and tabset customization to the README. - -## 0.8.6 - 2025-03-15 - -* **Restructured:** SCSS files to remove the use of the deprecated `@import` rule. -* **Added:** `combined.css` containing all themes. -* **Updated:** Demo to use `combined.css` for simple theme switching using class names. -* **Added:** Option in the demo to show the layout structure. - -## 0.8.5 - 2025-03-08 - -* **Changed:** The mini scrollbar now only shows when tabs are hovered over. - -## 0.8.4 - 2025-03-03 - -* **Added:** Attribute `'enableTabScrollbar'` to `TabSet` and `Border` nodes. Enabling this attribute will show a mini 'scrollbar' for the tabs to indicate the scroll position. See the Demo app's default layout for an example. - -## 0.8.3 - 2025-02-21 - -* **Prevented:** Sticky buttons from scrolling when there are no tabs. -* **Fixed:** Border `'show'` attribute. -* **Removed:** Code to adjust popout positions when loading. - -## 0.8.2 - 2025-02-15 - -* **Updated:** Dependencies. -* **Enabled:** Use with React 19. -* **Removed:** Strict mode from the demo due to a bug in React 19 ([https://github.com/facebook/react/issues/29585](https://github.com/facebook/react/issues/29585)) causing tabs to re-mount when moved. -* **Used:** CodeSandbox in `README.md` since React 19 doesn't create UMD versions needed by JSFiddle. - -## 0.8.1 - 2024-09-24 - -* **Fixed:** `enableDrag` on tab and tabset nodes. -* **Fixed:** Calculation for min/max tabset height from min/max tab height. -* **Modified:** Stylesheet code in the demo to reduce flash. - -## 0.8.0 - 2024-09-12 - -**New Features:** - -* Wrap tabs option. -* Improved popouts, no longer keep a placeholder tab. -* Drag from the overflow menu. -* Improved splitter resizing. -* Now uses HTML drag and drop to allow cross-window dragging. -* Rendering now uses flexbox rather than absolute positions, which should make styling easier. -* Rounded theme. -* Updated dependencies. - -**Breaking Changes:** - -* The `addTabWithDragAndDrop` signature has changed and must now be called from a drag start handler. -* The `moveTabWithDragAndDrop` signature has changed and must now be called from a drag start handler. -* Removed `addTabWithDragAndDropIndirect`. -* Removed `onTabDrag` (custom internal drag). -* Removed the `font` prop; use CSS variables `--font-size` and `--font-family` instead. -* Removed the `titleFactory` and `iconFactory` props; use `onRenderTab` instead. -* Removed the tabset header option. -* Removed attributes: `for insets`, `tabset header`, `row/tabset width and height`, `legacymenu`, etc. -* Several CSS changes reflect the use of flexbox. - -## 0.7.15 - 2023-11-14 - -* **Added:** Arrow icon to edge indicators. - -## 0.7.14 - 2023-11-10 - -* **Added:** Attribute `tabsetClassName` to tab nodes. This will add the class name to the parent tabset when there is a single stretched tab. Updated the mosaic layout in the demo to use this to color headers. - -## 0.7.13 - 2023-10-22 - -* **New attribute on tabset:** `enableSingleTabStretch` will stretch a single tab to take up all the remaining space and change the style to look like a header. Combined with `enableDrop`, this can be used to create a Mosaic-style layout (headed panels without tabs). See the new Mosaic Style layout in the Demo. -* The layout methods `addTabToTabSet` and `addTabToActiveTabSet` now return the added `TabNode`. -* **Fixed:** [#352](https://github.com/caplin/FlexLayout/issues/352) - `Layout.getDomRect` returning null. - -## 0.7.12 - -* `Action.setActiveTabset` can now take `undefined` to unset the active tabset. -* **Added:** Tab attribute `contentClassName` to add a class to the tab content. - -## 0.7.11 - -* **Added:** `ITabSetRenderValues.overflowPosition` to allow the overflow button position to be specified. If left undefined, the position will be after sticky buttons as before. -* **New model attribute:** `enableRotateBorderIcons`. This allows the tab icons in the left and right borders to rotate with the text or not; the default is `true`. -* **Added:** Additional class names to edge indicators. - -## 0.7.10 - -* **Fixed:** [#399](https://github.com/caplin/FlexLayout/issues/399) - The overflow button in a tabset is now placed after any sticky buttons (additional buttons that stick to the last tab of a tabset) but before any other buttons. -* **Enabled:** Sticky buttons in border tabsets. - -## 0.7.9 - -* **Fixed:** Drag issue found when used in a devtool extension. -* **Fixed:** Double render in popout when in strict mode. - -## 0.7.8 - -* **Fixed:** Popout size of tab with individual border size. -* **Hid:** Edge handles when disabled. -* **Updated:** Version of Cypress. - -## 0.7.7 - -* **Fixed:** [#379](https://github.com/caplin/FlexLayout/issues/379) - `uuid` could only be generated in secure contexts. - -## 0.7.6 - -* **Removed:** Dependency on the `uuid` package. -* **Added:** `action` argument to the `onModelChange` callback. - -## 0.7.5 - -* **Fixed:** [#340](https://github.com/caplin/FlexLayout/issues/340) - Error dragging a tabset into an empty tabset. - -## 0.7.4 - -* **Fixed:** Popout windows when using ``. -* **Output now targets:** ES6. - -## 0.7.3 - -* **Fixed:** Right edge marker location when border `enableAutoHide`. -* Dragging out a selected border tab will now leave the border unselected. - -## 0.7.2 - -* **New Layout JSON tabs:** Added to the demo. -* **Added:** `--color-icon` CSS rootOrientationVertical. - -## 0.7.1 - -* **Fixed:** [#310](https://github.com/caplin/FlexLayout/issues/310) - Added new layout method: `moveTabWithDragAndDrop(node)` to allow tab dragging to be started from custom code. - -## 0.7.0 - -* **Updated:** Dependencies, in particular, changed React peer dependency to React 18. -* **Made changes for:** React 18. - -## 0.6.10 - -* **Fixed:** [#312](https://github.com/caplin/FlexLayout/issues/312), Chrome warning for wheel event listener. - -## 0.6.9 - -* **Fixed:** [#308](https://github.com/caplin/FlexLayout/issues/308), Allow dragging within a maximized tabset. - -## 0.6.8 - -* **Added:** `onTabSetPlaceHolder` prop to render the tabset area when there are no tabs. - -## 0.6.7 - -* **Added:** More CSS variables, Underline theme, and updated dependencies. - -## 0.6.6 - -* **Fixed:** [#296](https://github.com/caplin/FlexLayout/issues/296). - -## 0.6.5 - -* **Fixed:** [#289](https://github.com/caplin/FlexLayout/issues/289), Allow setting attributes to undefined value. - -## 0.6.4 - -* Code tidy. -* Updated dependencies. - -## 0.6.3 - -* **Changed:** To using named rather than default import/exports. This will require changing top-level imports: - ```javascript - // from: - import FlexLayout from 'flexlayout-react'; - // to: - import * as FlexLayout from 'flexlayout-react'; - ``` -* **Added:** Typedoc link to README. - -## 0.6.2 - -* **Extended:** `icons` prop to allow the use of functions to set icons. -* **Added:** `onShowOverflowMenu` callback for handling the display of the tab overflow menu. - -## 0.6.1 - -* **Used:** Portal for the drag rectangle to preserve React context in `onRenderTab`. - -## 0.6.0 - -* **Changed:** Icons to use SVG images, which will now scale with the font size. -* **Improved:** Element spacing, removed most margin/padding spacers. -* The overflow menu and drag rectangle will now show the tab icon and content as rendered in the tab. -* **Added:** `altName` attribute to `TabNode`. This will be used as the name in the overflow menu if there is no `name` attribute (e.g., the tab has just an icon). -* **Changed:** The drag outline colors from red/green to light blue/green. -* **Removed:** `closeIcon` prop from `Layout`; use the `icons` property instead. -* **Changed:** `onRenderDragRect` callback to take a `ReactElement` rather than a string. The content now contains the tab button as rendered. - -## 0.5.21 - -* **Fixed:** Copying stylesheet links for popout windows when `cssRules` throw an exception. -* **Added:** Option `enableUseVisibility` to allow the use of `visibility: hidden` rather than `display: none` for hiding elements. - -## 0.5.20 - -* **Added:** Cypress Tests. -* **Fixed:** Bug with tab icon not showing. - -## 0.5.19 - -* **Added:** `onRenderFloatingTabPlaceholder` callback prop for rendering the floating tab placeholder. -* **Changed:** Style sheets to use CSS custom properties (variables) for several values. -* **Fixed:** Selected index in a single empty tabset. -* **Added:** `onContextMenu` callback prop for handling context menus on tabs and tabsets. -* **Added:** `onAuxMouseClick` callback prop for handling mouse clicks on tabs and tabsets with alt, meta, shift keys, and also handles center mouse clicks. - -## 0.5.18 - -* **Added:** `onRenderDragRect` callback prop for rendering the drag rectangles. -* **New border attribute:** `enableAutoHide`, to hide the border if it has zero tabs. - -## 0.5.17 - -* **New global option:** `splitterExtra`, to allow splitters to have extended hit test areas. This makes it easier to use narrow splitters. -* **Added new TabNode attributes:** `borderWidth` and `borderHeight`. These allow for individual border sizes for certain tabs. -* **Fixed:** [#263](https://github.com/caplin/FlexLayout/issues/263) - Border splitters not taking the minimum size of the center into account. -* **Improved:** Algorithm for finding the drop location. -* **Additional parameter:** `cursor`, for `onTabDrag`. - -## 0.5.16 - -* **Added:** 'New Features' layout to the demo. -* **New tab attribute:** `helpText`, to show a tooltip over tabs. -* **New model action:** `deleteTabset`, to delete a tabset and all its child tabs. -* **New tabset attribute:** `enableClose`, to close the tabset. - -## 0.5.15 - -* **Added new Layout prop:** `onTabDrag` that allows tab dragging to be intercepted. -* **Added example of `onTabDrag`:** In the demo app, the example shows a list where tabs can be dragged into, moved in the list, and dragged back out into the layout. -* Node IDs that are not assigned a value are now auto-generated using a UUID rather than a rolling number (e.g., previous ID: #3, new ID: #0c459064-8dee-444e-8636-eb9ab910fb27). -* **Made:** The `toJson` method of the node public. - -## 0.5.14 - -* **Fixed:** An issue with copying styles for a floating window when using a CSS-in-JS solution. -* **Fixed:** [#227](https://github.com/caplin/FlexLayout/issues/227) - Edge rects are not moved if the window is resized while dragging. - -## 0.5.13 - -* **Added prop:** `realtimeResize` to make tabs resize as their splitters are dragged. - **Warning:** This can cause resizing to become choppy when tabs are slow to draw. - -## 0.5.12 - -* **New callback on Model:** To allow `TabSet` attributes to be set when a tab is moved in such a way that it creates a new `TabSet`. -* **Added:** Config attributes to `TabSet` and `Border`. -* **Added:** `headerButtons` to `ITabSetRenderValues` to allow a different set of buttons to be applied to headed `TabSets`. - -## 0.5.11 - -* **Added:** `StickyButtons` to `onRenderTabSet` render values to allow for the implementation of a Chrome-style + button. -* **Added:** Example of the + button to the default layout in the demo app. - -## 0.5.10 - -* Adjusted the selected tab when tabs are popped out to an external window. - -## 0.5.9 - -* `TitleFactory` can now return an object with `titleContent` and `name` (name is used for the tab overflow menu). -* Corrected the position of `rootOrientationVertical` in the TypeScript JSON model. - -## 0.5.8 - -* **Fixed:** [#172](https://github.com/caplin/FlexLayout/issues/172) - Added global `rootOrientationVertical` attribute to allow vertical layout for the root 'row'. -* **Added:** Missing exports for the TypeScript JSON model. -* **Moved:** CRA example to a separate repo. - -## 0.5.7 - -* **Added:** TypeScript typings for the model JSON. -* **Fixed:** Drag rectangle showing as a dot before the first position was found (when dragging into the layout). -* **Fixed:** [#191](https://github.com/caplin/FlexLayout/issues/191) - Global Attributes for class names not working. -* **Fixed:** [#212](https://github.com/caplin/FlexLayout/issues/212) - TypeScript issue with `ILayoutState`. - -## 0.5.6 - -* **Added:** External drag and drop into the layout; see the new `onExternalDrag` prop. -* **Updated:** Demo to accept dragged links, HTML, and text. -* Tab scrolling direction changed to match VSCode. -* Improved positioning of a single tab when the overflow menu is shown. -* Some small changes to theme colors. - -## 0.5.5 - -* **Fixed:** [#170](https://github.com/caplin/FlexLayout/issues/170) - Closing the last tab of a maximized tabset crashes the layout. - -## 0.5.4 - -* **Fixed:** Issue running with React 17.0.1. -* Window title now updates when a tab is renamed. - -## 0.5.3 - -* **Changed:** Class name strings to enum values. -* **Replaced:** TSLint with ESLint. -* **Added:** Create-React-App (CRA) example. -* **New theme:** 'light' (lighter and without box shadows, gradients). -* **Renamed:** Existing 'light' theme to 'gray'. - -## 0.5.2 - -* **Fixed:** Issues caused by double touch/mouse events in iOS. -* **Prevented:** iOS scroll during drag in the demo app. -* **Added:** Extra option to `onRenderTab` to allow the name of the item in the overflow menu to be set. -* **New option:** `closeType` for tabs. -* The maximized tabset now sets others to `display: none` rather than using `z-index`. -* **Disabled:** Maximize if only one tabset. -* Splitters will now default to 8px on desktop and 12px on mobile (so they can be tapped more easily). -* Close element is enlarged on mobile. - -## 0.5.1 - -* Various small fixes. - -## 0.5.0 - -* Overflowing tabs now scroll to keep the selected tab in view. They can also be manually scrolled using the mouse wheel. -* Now works on scrolling pages. -* **NOTE:** Several CSS classes with names starting with `flexlayout__tabset_header...` have been renamed to `flexlayout__tabset_tabbar...`. - -## 0.4.9 - -* Keep the selected tab in the tabset/border when another tab is moved out. - -## 0.4.8 - -* **Added:** Minimum size attributes on tabset and border. -* **Added:** Extra CSS classes on elements for border and splitter styling. - -## 0.4.7 - -* **Added:** `font` property. -* Font now defaults to `medium`. -* Tabs now auto-adjust to the current font. -* **Added:** `fontSize` dropdown to the demo. -* **Modified:** CSS for the above font size changes and to remove some fixed sizes. -* **Added:** New attributes to control the auto-selection of tabs. - -## 0.4.6 - -* **Added:** `icons` prop to allow default icons to be replaced. -* **Added:** `tabLocation` attribute to tabsets to allow top and bottom tab placement. -* **Modified:** CSS; the default font is now 14px. - -## 0.4.5 - -* **Fixed:** Use of global objects for use when server-side rendering. -* **Added:** Error boundary around tab contents to prevent tab rendering exceptions from crashing the app. - -## 0.4.4 - -* **Changed:** All components except `Layout` to use React Hooks. -* Popouts now wait for stylesheets to load. -* **Fixed:** Problem rendering popouts in Safari. - -## 0.4.3 - -* **Fixed:** `addTabWithDragAndDrop` not working since 0.4.0. - -## 0.4.2 - -* Use Sass to generate light and dark themes. - -## 0.4.1 - -* Copy styles into popout tabs. - -## 0.4.0 - -* **Added:** Ability to pop out tabs into new browser windows. Press the 'reload from file' button in the demo app to load new layouts with the `popout` attribute. - -## 0.3.11 - -* **Added:** Overflow menu to border tabs. -* **Fixed:** Issues running on IE11. - -## 0.3.10 - -* **Removed:** Deprecated React lifecycle methods. Will now work in React strict mode without warnings (for example, in apps created with Create React App). \ No newline at end of file +### For older changes please see [Flexlayout ChangeLog](https://github.com/caplin/FlexLayout/blob/master/CHANGELOG.md) diff --git a/README.md b/README.md index defe6e0b..ee94bad3 100755 --- a/README.md +++ b/README.md @@ -1,100 +1,176 @@ -# FlexLayout +
+ + React Hook Form Logo - React hook custom hook for form validation + +
-[![GitHub](https://img.shields.io/github/license/Caplin/FlexLayout)](https://github.com/caplin/FlexLayout/blob/master/LICENSE) -![npm](https://img.shields.io/npm/dw/flexlayout-react) -[![npm](https://img.shields.io/npm/v/flexlayout-react)](https://www.npmjs.com/package/flexlayout-react) +
-FlexLayout is a layout manager that arranges React components in multiple tabsets, tabs can be resized and moved. +[![npm downloads](https://img.shields.io/npm/dm/react-hook-form.svg?style=for-the-badge)](https://www.npmjs.com/package/flexycakes) +[![npm](https://img.shields.io/npm/l/react-hook-form?style=for-the-badge)](https://github.com/powerdragonfire/flexycakes/blob/master/LICENSE) +[![Discord](https://img.shields.io/discord/754891658327359538.svg?style=for-the-badge&label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/7bERmQjH) -![FlexLayout Demo Screenshot](screenshots/Screenshot_light.png?raw=true "FlexLayout Demo Screenshot") +
-[Run the Demo](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/demo/index.html) +

+ Examples | + API | + Quick Start | + Feature Tracker | + Sandbox | + Flexlayout +

-Try it now using [CodeSandbox](https://codesandbox.io/p/sandbox/yvjzqf) +This is **Flexycakes**, a fork of FlexLayout with enhanced features including pin/unpin functionality for panels. Flexycakes extends the original FlexLayout capabilities with additional user interface improvements and workflow optimizations. -[API Doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/index.html) +## ๐Ÿ“ Pin Board -[Screenshot of Caplin Liberator Explorer using FlexLayout](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.20/images/LiberatorExplorerV3_3.PNG) +
-FlexLayout's only dependency is React. +### ๐Ÿ’ฐ Paid Bounties Program -Features: -* splitters -* tabs (scrolling or wrapped) -* tab dragging and ordering -* tabset dragging (move all the tabs in a tabset in one operation) -* dock to tabset or edge of frame -* maximize tabset (double click tabset header or use icon) -* tab overflow (show menu when tabs overflow, scroll tabs using mouse wheel) -* border tabsets -* popout tabs into new browser windows -* submodels, allow layouts inside layouts -* tab renaming (double click tab text to rename) -* theming - light, dark, underline, gray, rounded and combined -* works on mobile devices (iPad, Android) -* add tabs using drag, add to active tabset, add to tabset by id -* tab and tabset attributes: enableTabStrip, enableDock, enableDrop... -* customizable tabs and tabset rendering -* component state is preserved when tabs are moved -* Playwright tests -* typescript type declarations +
-## Installation +> **๐Ÿš€ Exciting News!** We're launching a paid bounty system to support young developers in the open source community. -FlexLayout is in the npm repository. install using: +
-``` -npm install flexlayout-react -``` +[![Join Discord](https://img.shields.io/badge/Join%20Discord-Get%20Updates-7289da?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/7bERmQjH) -Import FlexLayout in your modules: +
-``` -import {Layout, Model} from 'flexlayout-react'; -``` +**What we're working on:** -Include the light, dark, underline, gray, rounded or combined theme by either: +- ๐Ÿ” Researching the best bounty platforms +- ๐Ÿ“‹ Setting up contract logistics +- ๐ŸŽฏ Defining contribution opportunities +- ๐Ÿ’ก Creating developer-friendly workflows -Adding an import in your js code: +**Stay tuned:** Join our Discord community for real-time updates and early access to bounty opportunities! -``` -import 'flexlayout-react/style/light.css'; +## ๐Ÿš€ Quick Start + +### Installation + +Install Flexycakes from npm: + +```bash +npm install flexycakes ``` -or by copying the relevant css from the node_modules/flexlayout-react/style directory to your - public assets folder (e.g. public/style) and linking the css in your html: +or using yarn: +```bash +yarn add flexycakes ``` - + +or using pnpm: + +```bash +pnpm add flexycakes ``` -[How to change the theme dynamically in code](#dynamically-changing-the-theme) +### Basic Setup + +1. **Import the components:** + + ```javascript + import { Layout, Model } from "flexycakes"; + ``` + +2. **Include a theme:** + + **Option A:** Import in your JavaScript code: + + ```javascript + import "flexycakes/style/light.css"; + ``` + + **Option B:** Link in your HTML: + + ```html + + ``` + +3. **Optional:** For dynamic theming, see [Theme Switching](#theme-switching) + +### Development Setup with Symbolic Linking + +If you're contributing to Flexycakes or want to develop locally: + +1. **Clone the repository:** + + ```bash + git clone https://github.com/powerdragonfire/flexycakes.git + cd flexycakes + ``` + +2. **Install dependencies:** + + ```bash + pnpm install + ``` +3. **Create a symbolic link:** -## Usage + ```bash + # Build the library first + pnpm build -The `` component renders the tabsets and splitters, it takes the following props: + # Create global link + npm link + # or with pnpm + pnpm link --global + ``` +4. **Link in your project:** -#### Required props: + ```bash + cd /path/to/your/project + npm link flexycakes + # or with pnpm + pnpm link --global flexycakes + ``` +5. **Start development mode:** -| Prop | Description | -| --------------- | ----------------- | -| model | the layout model | -| factory | a factory function for creating React components | + ```bash + # In flexycakes directory + pnpm dev + ``` -Additional [optional props](#optional-layout-props) + This will watch for changes and rebuild automatically. -The model is tree of Node objects that define the structure of the layout. +--- -The factory is a function that takes a Node object and returns a React component that should be hosted by a tab in the layout. +## ๐Ÿ“– Usage Guide -The model can be created using the Model.fromJson(jsonObject) static method, and can be saved using the model.toJson() method. +### Basic Layout Setup -## Example Configuration: +The `` component renders the tabsets and splitters. Here's what you need: + +#### Required Props + +| Prop | Type | Description | +| --------- | -------- | ----------------------------------------- | +| `model` | Model | The layout model containing the structure | +| `factory` | Function | Creates React components for each tab | + +> ๐Ÿ’ก **Tip:** See [Optional Layout Props](#layout-properties) for additional configuration options. + +### Core Concepts + +The **model** is a tree of Node objects that define your layout structure: + +- Created using `Model.fromJson(jsonObject)` +- Saved using `model.toJson()` + +The **factory** function takes a Node object and returns the React component to render in that tab. + +### Simple Example ```javascript +// Define your layout structure const json = { global: {}, borders: [], @@ -108,10 +184,10 @@ const json = { children: [ { type: "tab", - name: "One", - component: "placeholder", - } - ] + name: "Dashboard", + component: "dashboard", + }, + ], }, { type: "tabset", @@ -119,379 +195,434 @@ const json = { children: [ { type: "tab", - name: "Two", - component: "placeholder", - } - ] - } - ] - } + name: "Settings", + component: "settings", + }, + ], + }, + ], + }, }; -``` - -## Example Code -```javascript +// Create your app const model = Model.fromJson(json); function App() { - - const factory = (node) => { - const component = node.getComponent(); - - if (component === "placeholder") { - return
{node.getName()}
; - } - } - - return ( - - ); + const factory = (node) => { + const component = node.getComponent(); + + switch (component) { + case "dashboard": + return ; + case "settings": + return ; + default: + return
{node.getName()}
; + } + }; + + return ; } -``` - -The above code would render two tabsets horizontally each containing a single tab that hosts a div component (returned from the factory). The tabs could be moved and resized by dragging and dropping. Additional tabs could be added to the layout by sending actions to the model. +``` Simple layout +> ๐ŸŽฎ **Try it live:** [CodeSandbox Demo](https://codesandbox.io/p/devbox/flexlayout-example-master-forked-g3tdzn?workspaceId=ws_Uv3sirbkAsy6tf81TaC9XG) | [TypeScript Example](https://github.com/nealus/flexlayout-vite-example) -Try it now using [CodeSandbox](https://codesandbox.io/p/sandbox/yvjzqf) +--- -A simple Typescript example can be found here: +## ๐Ÿ—๏ธ Layout Structure -https://github.com/nealus/flexlayout-vite-example +### JSON Model Components -The model json contains 4 top level elements: +The model JSON has 4 main sections: -* global - (optional) where global options are defined -* layout - where the main row/tabset/tabs layout hierarchy is defined -* borders - (optional) where up to 4 borders are defined ("top", "bottom", "left", "right"). -* popouts - (optional) where the popout windows are defined +| Section | Required | Description | +| --------- | -------- | ---------------------------------------------- | +| `global` | โŒ | Global layout options | +| `layout` | โœ… | Main layout hierarchy | +| `borders` | โŒ | Edge panels ("top", "bottom", "left", "right") | +| `popouts` | โŒ | External window definitions | -The layout element is built up using 3 types of 'node': +### Node Types -* row - rows contains a list of tabsets and child rows, the top level 'row' will render horizontally (unless the global attribute rootOrientationVertical is set) -, child 'rows' will render in the opposite orientation to their parent row. +#### ๐Ÿ”น Row Nodes -* tabset - tabsets contain a list of tabs and the index of the selected tab +- **Purpose:** Container for tabsets and child rows +- **Orientation:** Top-level rows are horizontal (unless `rootOrientationVertical` is set) +- **Children:** Tabsets and other rows -* tab - tabs specify the name of the component that they should host (that will be loaded via the factory) and the text of the actual tab. +#### ๐Ÿ”น TabSet Nodes -The layout structure is defined with rows within rows that contain tabsets that themselves contain tabs. +- **Purpose:** Container for tabs +- **Properties:** List of tabs + selected tab index +- **Behavior:** Auto-created when tabs move, deleted when empty (unless `enableDeleteWhenEmpty: false`) -Within the demo app you can show the layout structure by ticking the 'Show layout' checkbox, rows are shown in blue, tabsets in orange. +#### ๐Ÿ”น Tab Nodes -![FlexLayout Demo Showing Layout](screenshots/Screenshot_layout.png?raw=true "Demo showing layout") +- **Purpose:** Individual panel content +- **Properties:** Component name (for factory) + display text +- **Content:** Loaded via the factory function -The optional borders element is made up of border nodes +#### ๐Ÿ”น Border Nodes -* border - borders contain a list of tabs and the index of the selected tab, they can only be used in the borders -top level element. +- **Purpose:** Edge-docked panels +- **Location:** Can only exist in the `borders` section +- **Behavior:** Similar to tabsets but docked to screen edges -The tree structure for the JSON model is well defined as Typescript interfaces, see [JSON Model](#json-model-definition) +![FlexLayout Demo Showing Layout](screenshots/Screenshot_layout.png?raw=true "Demo showing layout structure") -Each type of node has a defined set of requires/optional attributes. +> ๐Ÿ’ก **Pro Tip:** Use the [demo app](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/demo/index.html) to visually create layouts, then export the JSON via 'Show Layout JSON in console'. -Weights on rows and tabsets specify the relative weight of these nodes within the parent row, the actual values do not matter just their relative values (ie two tabsets of weights 30,70 would render the same if they had weights of 3,7). +### Weight System -NOTE: the easiest way to create your initial layout JSON is to use the [demo](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/demo/index.html) app, modify one of the -existing layouts by dragging/dropping and adding nodes then press the 'Show Layout JSON in console' button to print the JSON to the browser developer console. +Node weights determine relative sizing: -By changing global or node attributes you can change the layout appearance and functionality, for example: +- **Example:** Weights of 30,70 = same as 3,7 +- **Usage:** Only relative values matter +- **Application:** Applies to rows and tabsets within their parent -Setting tabSetEnableTabStrip:false in the global options would change the layout into a multi-splitter (without -tabs or drag and drop). +--- -``` - global: {tabSetEnableTabStrip:false}, -``` +## ๐ŸŽจ Customization -## Dynamically Changing the Theme +### Theme Switching -The 'combined.css' theme contains all the other themes and can be used for theme switching. +#### Available Themes -When using combined.css, add a className (of the form "flexlayout__theme_[theme name]") to the div containing the `` to select the applied theme. +- `light.css` - Clean light theme +- `dark.css` - Dark mode theme +- `gray.css` - Neutral gray theme +- `underline.css` - Minimal underline style +- `rounded.css` - Rounded corners theme +- `combined.css` - All themes in one file -For example: -``` -
- -
-``` +#### Dynamic Theme Switching -Change the theme in code by changing the className on the containing div. +Use the `combined.css` theme for runtime switching: -For example: -``` - containerRef.current!.className = "flexlayout__theme_dark" +```javascript +// Setup with theme container +
+ +
; + +// Change theme programmatically +containerRef.current.className = "flexlayout__theme_dark"; ``` -## Customizing Tabs +### Custom Tab Rendering -You can use the `` prop onRenderTab to customize the tab rendering: +Customize individual tabs with `onRenderTab`: +Tab customization areas -FlexLayout Tab structure +```javascript +const onRenderTab = (node, renderValues) => { + // renderValues.leading = ; (red area) + // renderValues.content += " *"; (green area) + renderValues.buttons.push(); // yellow area +}; -Update the renderValues parameter as needed: +; +``` -renderValues.leading : the red block +### Custom TabSet Rendering -renderValues.content : the green block +Customize tabset headers with `onRenderTabSet`: -renderValues.buttons : the yellow block +TabSet customization areas -For example: +```javascript +const onRenderTabSet = (node, renderValues) => { + // Add persistent buttons (red area) + renderValues.stickyButtons.push( + , + ); + // Add contextual buttons (green area) + renderValues.buttons.push(); +}; ``` -onRenderTab = (node: TabNode, renderValues: ITabRenderValues) => { - // renderValues.leading = ; - // renderValues.content += " *"; - renderValues.buttons.push(); -} -``` - -## Customizing Tabsets -You can use the `` prop onRenderTabSet to customize the tabset rendering: +--- +## โšก Actions & Events -FlexLayout Tab structure +### Model Actions -Update the renderValues parameter as needed: +All layout changes happen through actions via `Model.doAction()`: -renderValues.leading : the blue block +```javascript +// Add a new tab +model.doAction( + Actions.addNode( + { type: "tab", component: "grid", name: "New Grid" }, + "targetTabsetId", + DockLocation.CENTER, + 0, // position (use -1 for end) + ), +); -renderValues.stickyButtons : the red block +// Update global settings +model.doAction( + Actions.updateModelAttributes({ + splitterSize: 40, + }), +); +``` -renderValues.buttons : the green block +> ๐Ÿ“š **Reference:** [Full Actions API](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/classes/Actions.html) +### Tab Events -For example: +Handle tab lifecycle events: -``` -onRenderTabSet = (node: (TabSetNode | BorderNode), renderValues: ITabSetRenderValues) => { - renderValues.stickyButtons.push( - ); - - renderValues.buttons.push(); +```javascript +function MyComponent({ node }) { + useEffect(() => { + // Listen for resize events + node.setEventListener("resize", ({ rect }) => { + console.log("Tab resized:", rect); + }); + + // Save data before serialization + node.setEventListener("save", () => { + node.getConfig().myData = getCurrentData(); + }); + + // Handle visibility changes + node.setEventListener("visibility", ({ visible }) => { + if (visible) startUpdates(); + else stopUpdates(); + }); + }, [node]); } ``` -## Model Actions +#### Available Events -Once the model json has been loaded all changes to the model are applied through actions. +| Event | Parameters | Description | +| ------------ | ----------- | ---------------------------- | +| `resize` | `{rect}` | Tab resized during layout | +| `close` | `none` | Tab is being closed | +| `visibility` | `{visible}` | Tab visibility changed | +| `save` | `none` | Before serialization to JSON | -You apply actions using the `Model.doAction()` method. +--- -This method takes a single argument, created by one of the action -generators (accessed as `FlexLayout.Actions.`): +## ๐ŸชŸ Popout Windows -[Actions doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/classes/Actions.html) +### Overview -### Examples +Render tabs in external browser windows for multi-monitor setups. -```js -model.doAction(FlexLayout.Actions.addNode( - {type:"tab", component:"grid", name:"a grid", id:"5"}, - "1", FlexLayout.DockLocation.CENTER, 0)); -``` - -This example adds a new grid component to the center of tabset with id "1" and at the 0'th tab position (use value -1 to add to the end of the tabs). +### Setup Requirements +1. **HTML Host Page:** Copy `popout.html` to your public directory +2. **Tab Configuration:** Add `enablePopout: true` to tab definitions +3. **Styling:** Styles are automatically copied from main window -```js -model.doAction(FlexLayout.Actions.updateModelAttributes({ - splitterSize:40 -})); -``` +### Code Considerations -The above example would increase the size of the splitters, this could be used to make -adjusting the layout easier on a small device. +Popout windows use a different `document` and `window`. Access them via: -Note: you can get the id of a node (e.g., the node returned by the `addNode` -action) using the method `node.getId()`. -If an id wasn't assigned when the node was created, then one will be created for you of the form `#` (e.g. `#0c459064-8dee-444e-8636-eb9ab910fb27`). +```javascript +function PopoutComponent() { + const selfRef = useRef(); -Note: You can intercept actions resulting from GUI changes before they are applied by -implementing the `onAction` callback property of the `Layout`. + const handleEvent = () => { + // โŒ Wrong - uses main window + document.addEventListener("click", handler); -## Optional Layout Props + // โœ… Correct - uses popout window + const popoutDoc = selfRef.current.ownerDocument; + const popoutWindow = popoutDoc.defaultView; + popoutDoc.addEventListener("click", handler); + }; -There are many optional properties that can be applied to the layout: + return
Content
; +} +``` -[Layout Properties doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/ILayoutProps.html) +### Limitations +- โš ๏ธ **Context:** Code runs in main window JS context +- โš ๏ธ **Events:** Must use popout window/document for listeners +- โš ๏ธ **Throttling:** Timers throttle when main window is hidden +- โš ๏ธ **Libraries:** Third-party controls may not work without modification +- โš ๏ธ **Zoom:** Incorrect sizing when browser is zoomed +- โš ๏ธ **State:** Windows can't reload in maximized/minimized states -## JSON Model Definition +> ๐Ÿ“– **Deep Dive:** [React Portals in Popups](https://dev.to/noriste/the-challenges-of-rendering-an-openlayers-map-in-a-popup-through-react-2elh) -The JSON model is well defined as a set of TypeScript interfaces, see the doc for details of all the attributes allowed: +--- -## Model Config Attributes +## ๐Ÿ› ๏ธ Development -[Model Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonModel.html) +### Running Locally -## Global Config Attributes +```bash +# Install dependencies +pnpm install -[Global Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IGlobalAttributes.html) +# Start development server +pnpm dev -## Row Config Attributes +# Run tests (in separate terminal) +pnpm playwright +``` -[Row Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonRowNode.html) +Playwright UI -## TabSet Config Attributes +### Building -[Tabset Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonTabSetNode.html) +```bash +# Build for distribution +pnpm build +``` -Note: tabsets will be dynamically created as tabs are moved, and deleted when all their tabs are removed (unless enableDeleteWhenEmpty is false). +The built files will be in the `dist/` directory. -## Tab Config attributes +--- -[Tab Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonTabNode.html) +## ๐Ÿ“š Appendix -## Border Config attributes +
+๐Ÿ”ง Configuration Reference -[Border Attributes doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonBorderNode.html) +### Layout Properties +Complete list of optional props for the `` component: +- [Layout Properties Documentation](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/ILayoutProps.html) +### JSON Model Schemas +TypeScript interfaces for all configuration objects: -## Layout Component Methods to Create New Tabs +#### Core Model -There are methods on the Layout Component for adding tabs: +- [Model Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonModel.html) +- [Global Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IGlobalAttributes.html) -[Layout Methods doc](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/classes/Layout.html) +#### Node Types -Example: +- [Row Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonRowNode.html) +- [TabSet Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonTabSetNode.html) +- [Tab Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonTabNode.html) +- [Border Attributes](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/interfaces/IJsonBorderNode.html) -``` -layoutRef.current.addTabToTabSet("NAVIGATION", {type:"tab", component:"grid", name:"a grid"}); -``` -This would add a new grid component to the tabset with id "NAVIGATION" (where layoutRef is a ref to the Layout element, see https://reactjs.org/docs/refs-and-the-dom.html ). +
+
+๐ŸŽฏ Advanced Usage +### Layout Component Methods -## Tab Node Events +Programmatically add tabs using Layout component methods: -You can handle events on nodes by adding a listener, this would typically be done -when the component is mounted in a useEffect method: +```javascript +// Get reference to Layout component +const layoutRef = useRef(); -Example: +// Add tab to specific tabset +layoutRef.current.addTabToTabSet("NAVIGATION", { + type: "tab", + component: "grid", + name: "New Grid", +}); ``` - function MyComponent({node}) { - useEffect(() => { - // save subject in flexlayout node tree - node.setEventListener("save", () => { - node.getConfig().subject = subject; - }; - }, []); - } +[Complete Layout Methods API](https://rawgit.com/caplin/FlexLayout/demos/demos/v0.8/typedoc/classes/Layout.html) -``` +### Multi-Splitter Mode -| Event | parameters | Description | -| ------------- |:-------------:| -----| -| resize | {rect} | called when tab is resized during layout, called before it is rendered with the new size| -| close | none | called when a tab is closed | -| visibility | {visible} | called when the visibility of a tab changes | -| save | none | called before a tabnode is serialized to json, use to save node config by adding data to the object returned by node.getConfig()| +Remove tabs entirely for a pure splitter interface: -## Popout Windows +```javascript +const json = { + global: { + tabSetEnableTabStrip: false, + }, + // ... rest of layout +}; +``` -Tabs can be rendered into external browser windows (for use in multi-monitor setups) -by configuring them with the enablePopout attribute. When this attribute is present -an additional icon is shown in the tab header bar allowing the tab to be popped out -into an external window. +### Node ID Management -For popouts to work there needs to be an additional html page 'popout.html' hosted -at the same location as the main page (copy the one from the demo app). The popout.html is the host page for the -popped out tab, the styles from the main page will be copied into it at runtime. +```javascript +// Auto-generated IDs are UUIDs +const autoId = "#0c459064-8dee-444e-8636-eb9ab910fb27"; -Because popouts are rendering into a different document to the main layout any code in the popped out -tab that uses the global document or window objects for event listeners will not work correctly (for example custom popup menus where the code uses document.addEventListener(...)), -they need to instead use the document/window of the popout. To get the document/window of the popout use the -following method on one of the elements rendered in the popout (for example a ref or target in an event handler): +// Get node ID +const nodeId = node.getId(); -``` - const currentDocument = selfRef.current.ownerDocument; - const currentWindow = currentDocument.defaultView!; +// Use in actions +model.doAction(Actions.selectTab(nodeId)); ``` -In the above code selfRef is a React ref to the toplevel element in the tab being rendered. +
-Note: libraries may support popout windows by allowing you to specify the document to use, -for example see the getDocument() callback in agGrid at https://www.ag-grid.com/javascript-grid-callbacks/ +
+๐Ÿ”— Alternative Solutions -### Limitations of Popouts -* FlexLayout uses React Portals to draw the popout window content, - this means all the code runs in the main Window's JS context, so effectively the popout windows are just extensions of the area on which the main window can render panels. +Comparing Flexycakes with other React layout managers: -* Your code must use the popout window/document in popout windows when adding event listeners (e.g popoutDocument.addEventListener(...)). +| Library | Repository | Key Features | +| -------------- | ----------------------------------------------------------------------------- | ---------------------------------- | +| **Flexycakes** | [powerdragonfire/flexycakes](https://github.com/powerdragonfire/flexycakes) | Enhanced FlexLayout with pin/unpin | +| rc-dock | [ticlo/rc-dock](https://github.com/ticlo/rc-dock) | Dock-style layouts | +| Dockview | [dockview.dev](https://dockview.dev/) | Modern docking solution | +| Lumino | [jupyterlab/lumino](https://github.com/jupyterlab/lumino) | JupyterLab's layout system | +| Golden Layout | [golden-layout/golden-layout](https://github.com/golden-layout/golden-layout) | Multi-window layouts | +| React Mosaic | [nomcopter/react-mosaic](https://github.com/nomcopter/react-mosaic) | Tiling window manager | -* Timers throttle when main window is in the background - you could implement a webworker timer replacement if needed (which will not throttle) -* Many third party controls will use the global document for some event listeners, - these will not work correctly without modification -* Some third party controls will suspend when the global document is hidden - you can use the tab overlay attribute to 'gray out' these tabs when the main window is hidden -* Resize observers may be throttled (or stay attached to the main window), so you may need to use some other way to resize the component when in a popout. -* Popouts will not size and position correctly when the browser is zoomed (ie set to 50% zoom) -* Popouts cannot reload in maximized or minimized states -* by default flexlayout will maintain react state when moving tabs between windows, but you can use the -enableWindowReMount tab attribute to force the component to re-mount. +
-See this article about using React portals in this way: https://dev.to/noriste/the-challenges-of-rendering-an-openlayers-map-in-a-popup-through-react-2elh +
+๐Ÿ“‹ Migration Guide -## Running the Demo and Building the Project +### From FlexLayout to Flexycakes -First install dependencies: +Flexycakes is a drop-in replacement for FlexLayout with additional features: -``` -pnpm install -``` +1. **Update package:** -Run the Demo app: + ```bash + npm uninstall flexlayout-react + npm install flexycakes + ``` -``` -pnpm dev -``` +2. **Update imports:** -The 'pnpm dev' command will watch for changes to FlexLayout and the Demo, so you can make changes to the FlexLayout code and see the changes in your browser. + ```javascript + // Before + import FlexLayout from "flexlayout-react"; -Once the demo is running you can run the Playwright tests by running (in another terminal window) + // After + import { Layout, Model } from "flexycakes"; + ``` -``` -pnpm playwright -``` +3. **Update CSS imports:** + + ```javascript + // Before + import "flexlayout-react/style/light.css"; + + // After + import "flexycakes/style/light.css"; + ``` -PlaywrightUI +### New Features in Flexycakes -To build the npm distribution run 'pnpm build'. +- ๐Ÿ“Œ Pin/Unpin functionality for panels +- ๐ŸŽจ Enhanced UI components +- โšก Improved workflow optimizations +- ๐Ÿ”ง Additional customization options -## Alternative Layout Managers +
-| Name | Repository | -| ------------- |:-------------| -| rc-dock | https://github.com/ticlo/rc-dock | -| Dockview | https://dockview.dev/ | -| lumino | https://github.com/jupyterlab/lumino | -| golden-layout | https://github.com/golden-layout/golden-layout | -| react-mosaic | https://github.com/nomcopter/react-mosaic | +--- diff --git a/demo/App.tsx b/demo/App.tsx index d7641b34..d840c0b5 100644 --- a/demo/App.tsx +++ b/demo/App.tsx @@ -138,6 +138,22 @@ function App() { } } + const onAddToLeftEmptyTabset = (event: React.MouseEvent) => { + (layoutRef!.current!).addTabToTabSet("mwLeftTabSet", { + component: "grid", + icon: "images/article.svg", + name: "Grid " + nextGridIndex.current++ + }); + } + + const onAddToRightEmptyTabset = (event: React.MouseEvent) => { + (layoutRef!.current!).addTabToTabSet("mwRightTabSet", { + component: "grid", + icon: "images/article.svg", + name: "Grid " + nextGridIndex.current++ + }); + } + const onAddFromTabSetButton = (node: TabSetNode | BorderNode) => { const addedTab = (layoutRef!.current!).addTabToTabSet(node.getId(), { component: "grid", @@ -546,6 +562,7 @@ function App() { +
@@ -595,6 +612,12 @@ function App() { Add Drag + {layoutFile === "ecmind" && +
+ + +
+ }
{contents} diff --git a/demo/public/layouts/ecmind.layout b/demo/public/layouts/ecmind.layout new file mode 100644 index 00000000..ee6a12b0 --- /dev/null +++ b/demo/public/layouts/ecmind.layout @@ -0,0 +1,106 @@ +{ + "global": { + "splitterEnableHandle": true, + "tabEnablePopout": true, + "tabSetEnableActiveIcon": true, + "tabSetMinWidth": 130, + "tabSetMinHeight": 100, + "tabSetEnableTabScrollbar": true, + "borderMinSize": 100, + "borderEnableTabScrollbar": true, + "tabSetEnableDeleteWhenEmpty": false, + "tabSetEnableHideWhenEmpty": true, + "borderEnableAutoHide": false + }, + "borders": [ + { + "type": "border", + "location": "bottom", + "children": [ + { + "type": "tab", + "id": "#0ae8e0fb-dba2-4b14-9d75-08781231479a", + "name": "Output", + "component": "grid", + "enableClose": false, + "icon": "images/bar_chart.svg" + }, + { + "type": "tab", + "id": "#803a2efe-e507-4735-9c2a-46ce6042c1a2", + "name": "Terminal", + "component": "grid", + "enableClose": false, + "icon": "images/terminal.svg" + }, + { + "type": "tab", + "id": "#7bac972e-fd5f-4582-a511-4feede448394", + "name": "Layout JSON", + "component": "json" + } + ] + }, + { + "type": "border", + "location": "left", + "children": [ + { + "type": "tab", + "id": "#21c49854-be85-4e32-96c3-61962f71bc15", + "name": "Navigation", + "altName": "The Navigation Tab", + "component": "grid", + "enableClose": false, + "icon": "images/folder.svg" + } + ] + }, + { + "type": "border", + "location": "right", + "children": [ + { + "type": "tab", + "id": "#ec253996-0724-416b-a097-23f85a89afbe", + "name": "Options", + "component": "grid", + "enableClose": false, + "icon": "images/settings.svg" + } + ] + } + ], + "layout": { + "type": "row", + "id": "#11b6dde6-2808-4a87-b378-dd6ed2a92547", + "children": [ + { + "id": "mwLeftTabSet", + "type": "tabset", + "weight": 33, + "children": [] + }, + { + "id": "mwMiddleTabSet", + "type": "tabset", + "weight": 33, + "children": [ + { + "type": "tab", + "name": "OpenLayers Map", + "component": "map", + "enablePopoutOverlay": true + } + ] + }, + { + "id": "mwRightTabSet", + "type": "tabset", + "weight": 33, + "children": [] + } + ] + }, + "popouts": {} +} \ No newline at end of file diff --git a/flexycakes-logo.png b/flexycakes-logo.png new file mode 100644 index 00000000..51377da3 Binary files /dev/null and b/flexycakes-logo.png differ diff --git a/package.json b/package.json index f0def5c2..f3aa3120 100755 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { - "name": "flexlayout-react", - "version": "0.8.17", - "description": "A multi-tab docking layout manager", - "author": "Caplin Systems Ltd", + "name": "flexycakes", + "version": "1.2.1", + "description": "Community fork of caplin/FlexLayout (ISC). Not affiliated.", "repository": { "type": "git", - "url": "git+https://github.com/caplin/FlexLayout.git" + "url": "git+https://github.com/powerdragonfire/flexycakes.git" }, "license": "ISC", "type": "module", @@ -46,6 +45,7 @@ "scripts": { "dev": "vite", "preview": "vite preview", + "watch": "vite build --watch", "build": "npm run build:clean && npm run build:demo && npm run css && npm run build:lib && npm run build:types && npm run doc", "build:clean": "rimraf demo/dist dist/ types/ typedoc/", "build:demo": "vite build", @@ -88,7 +88,7 @@ "react": "^19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", - "react-scripts": "5.0.1", + "react-scripts": "^5.0.1", "rimraf": "^6.0.1", "sass": "^1.86.3", "styled-components": "^6.1.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41f06c7b..310f5849 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,7 +78,7 @@ importers: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) react-scripts: - specifier: 5.0.1 + specifier: ^5.0.1 version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@types/babel__core@7.20.5)(eslint@9.24.0(jiti@1.21.7))(react@19.1.0)(sass@1.86.3)(type-fest@0.21.3)(typescript@5.8.3) rimraf: specifier: ^6.0.1 @@ -5772,6 +5772,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..dee48d65 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - core-js + - core-js-pure + - esbuild diff --git a/src/I18nLabel.ts b/src/I18nLabel.ts index c79e22b0..94fc1d22 100644 --- a/src/I18nLabel.ts +++ b/src/I18nLabel.ts @@ -7,6 +7,8 @@ export enum I18nLabel { Maximize = "Maximize tab set", Restore = "Restore tab set", Popout_Tab = "Popout selected tab", + Pin_Tab = "Pin tab", + Unpin_Tab = "Unpin tab", Overflow_Menu_Tooltip = "Hidden tabs", Error_rendering_component = "Error rendering component", Error_rendering_component_retry = "Retry", diff --git a/src/model/IJsonModel.ts b/src/model/IJsonModel.ts index 9545c4b6..6e61ab94 100755 --- a/src/model/IJsonModel.ts +++ b/src/model/IJsonModel.ts @@ -6,19 +6,19 @@ export interface IJsonModel { global?: IGlobalAttributes; borders?: IJsonBorderNode[]; layout: IJsonRowNode; // top level 'row' is horizontal, rows inside rows take opposite orientation to parent row (ie can act as columns) - popouts?: Record; + popouts?: Record; } export interface IJsonRect { - x: number; - y: number; - width: number; - height: number; + x: number; + y: number; + width: number; + height: number; } export interface IJsonPopout { layout: IJsonRowNode; - rect: IJsonRect ; + rect: IJsonRect; } export interface IJsonBorderNode extends IBorderAttributes { @@ -31,234 +31,234 @@ export interface IJsonRowNode extends IRowAttributes { } export interface IJsonTabSetNode extends ITabSetAttributes { - /** Marks this as the active tab set, read from initial json but - * must subseqently be set on the model (only one tab set can be active)*/ - active?: boolean; - /** Marks this tab set as being maximized, read from initial json but - * must subseqently be set on the model (only one tab set can be maximized) */ - maximized?: boolean; + /** Marks this as the active tab set, read from initial json but + * must subseqently be set on the model (only one tab set can be active)*/ + active?: boolean; + /** Marks this tab set as being maximized, read from initial json but + * must subseqently be set on the model (only one tab set can be maximized) */ + maximized?: boolean; children: IJsonTabNode[]; } -export interface IJsonTabNode extends ITabAttributes { -} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IJsonTabNode extends ITabAttributes {} //---------------------------------------------------------------------------------------------------------- // below this line is autogenerated from attributes in code via Model static method toTypescriptInterfaces() //---------------------------------------------------------------------------------------------------------- export interface IGlobalAttributes { - /** + /** Value for BorderNode attribute autoSelectTabWhenClosed if not overridden whether to select new/moved tabs in border when the border is currently closed Default: false */ - borderAutoSelectTabWhenClosed?: boolean; + borderAutoSelectTabWhenClosed?: boolean; - /** + /** Value for BorderNode attribute autoSelectTabWhenOpen if not overridden whether to select new/moved tabs in border when the border is already open Default: true */ - borderAutoSelectTabWhenOpen?: boolean; + borderAutoSelectTabWhenOpen?: boolean; - /** + /** Value for BorderNode attribute className if not overridden class applied to tab button Default: undefined */ - borderClassName?: string; + borderClassName?: string; - /** + /** Value for BorderNode attribute enableAutoHide if not overridden hide border if it has zero tabs Default: false */ - borderEnableAutoHide?: boolean; + borderEnableAutoHide?: boolean; - /** + /** Value for BorderNode attribute enableDrop if not overridden whether tabs can be dropped into this border Default: true */ - borderEnableDrop?: boolean; + borderEnableDrop?: boolean; - /** + /** Value for BorderNode attribute enableTabScrollbar if not overridden whether to show a mini scrollbar for the tabs Default: false */ - borderEnableTabScrollbar?: boolean; + borderEnableTabScrollbar?: boolean; - /** + /** Value for BorderNode attribute maxSize if not overridden the maximum size of the tab area Default: 99999 */ - borderMaxSize?: number; + borderMaxSize?: number; - /** + /** Value for BorderNode attribute minSize if not overridden the minimum size of the tab area Default: 0 */ - borderMinSize?: number; + borderMinSize?: number; - /** + /** Value for BorderNode attribute size if not overridden size of the tab area when selected Default: 200 */ - borderSize?: number; + borderSize?: number; - /** + /** enable docking to the edges of the layout, this will show the edge indicators Default: true */ - enableEdgeDock?: boolean; + enableEdgeDock?: boolean; - /** + /** boolean indicating if tab icons should rotate with the text in the left and right borders Default: true */ - enableRotateBorderIcons?: boolean; + enableRotateBorderIcons?: boolean; - /** + /** the top level 'row' will layout horizontally by default, set this option true to make it layout vertically Default: false */ - rootOrientationVertical?: boolean; + rootOrientationVertical?: boolean; - /** + /** enable a small centralized handle on all splitters Default: false */ - splitterEnableHandle?: boolean; + splitterEnableHandle?: boolean; - /** + /** additional width in pixels of the splitter hit test area Default: 0 */ - splitterExtra?: number; + splitterExtra?: number; - /** + /** width in pixels of all splitters between tabsets/borders Default: 8 */ - splitterSize?: number; + splitterSize?: number; - /** + /** Value for TabNode attribute borderHeight if not overridden height when added to border, -1 will use border size Default: -1 */ - tabBorderHeight?: number; + tabBorderHeight?: number; - /** + /** Value for TabNode attribute borderWidth if not overridden width when added to border, -1 will use border size Default: -1 */ - tabBorderWidth?: number; + tabBorderWidth?: number; - /** + /** Value for TabNode attribute className if not overridden class applied to tab button Default: undefined */ - tabClassName?: string; + tabClassName?: string; - /** + /** Value for TabNode attribute closeType if not overridden see values in ICloseType Default: 1 */ - tabCloseType?: ICloseType; + tabCloseType?: ICloseType; - /** + /** Value for TabNode attribute contentClassName if not overridden class applied to tab content Default: undefined */ - tabContentClassName?: string; + tabContentClassName?: string; - /** + /** Default: 0.3 */ - tabDragSpeed?: number; + tabDragSpeed?: number; - /** + /** Value for TabNode attribute enableClose if not overridden allow user to close tab via close button Default: true */ - tabEnableClose?: boolean; + tabEnableClose?: boolean; - /** + /** Value for TabNode attribute enableDrag if not overridden allow user to drag tab to new location Default: true */ - tabEnableDrag?: boolean; + tabEnableDrag?: boolean; - /** + /** Value for TabNode attribute enablePopout if not overridden enable popout (in popout capable browser) Default: false */ - tabEnablePopout?: boolean; + tabEnablePopout?: boolean; - /** + /** Value for TabNode attribute enablePopoutIcon if not overridden whether to show the popout icon in the tabset header if this tab enables popouts Default: true */ - tabEnablePopoutIcon?: boolean; + tabEnablePopoutIcon?: boolean; - /** + /** Value for TabNode attribute enablePopoutOverlay if not overridden if this tab will not work correctly in a popout window when the main window is backgrounded (inactive) @@ -266,702 +266,720 @@ export interface IGlobalAttributes { Default: false */ - tabEnablePopoutOverlay?: boolean; + tabEnablePopoutOverlay?: boolean; - /** + /** Value for TabNode attribute enableRename if not overridden allow user to rename tabs by double clicking Default: true */ - tabEnableRename?: boolean; + tabEnableRename?: boolean; - /** + /** Value for TabNode attribute enableRenderOnDemand if not overridden whether to avoid rendering component until tab is visible Default: true */ - tabEnableRenderOnDemand?: boolean; + tabEnableRenderOnDemand?: boolean; - /** + /** Value for TabNode attribute icon if not overridden the tab icon Default: undefined */ - tabIcon?: string; + tabIcon?: string; - /** + /** Value for TabNode attribute maxHeight if not overridden the max height of this tab Default: 99999 */ - tabMaxHeight?: number; + tabMaxHeight?: number; - /** + /** Value for TabNode attribute maxWidth if not overridden the max width of this tab Default: 99999 */ - tabMaxWidth?: number; + tabMaxWidth?: number; - /** + /** Value for TabNode attribute minHeight if not overridden the min height of this tab Default: 0 */ - tabMinHeight?: number; + tabMinHeight?: number; - /** + /** Value for TabNode attribute minWidth if not overridden the min width of this tab Default: 0 */ - tabMinWidth?: number; + tabMinWidth?: number; - /** + /** Value for TabSetNode attribute autoSelectTab if not overridden whether to select new/moved tabs in tabset Default: true */ - tabSetAutoSelectTab?: boolean; + tabSetAutoSelectTab?: boolean; - /** + /** Value for TabSetNode attribute classNameTabStrip if not overridden a class name to apply to the tab strip Default: undefined */ - tabSetClassNameTabStrip?: string; + tabSetClassNameTabStrip?: string; - /** + /** Value for TabSetNode attribute enableActiveIcon if not overridden whether the active icon (*) should be displayed when the tabset is active Default: false */ - tabSetEnableActiveIcon?: boolean; + tabSetEnableActiveIcon?: boolean; - /** + /** Value for TabSetNode attribute enableClose if not overridden allow user to close tabset via a close button Default: false */ - tabSetEnableClose?: boolean; + tabSetEnableClose?: boolean; - /** + /** Value for TabSetNode attribute enableDeleteWhenEmpty if not overridden whether to delete this tabset when is has no tabs Default: true */ - tabSetEnableDeleteWhenEmpty?: boolean; + tabSetEnableDeleteWhenEmpty?: boolean; /** + Value for TabSetNode attribute enableHideWhenEmpty if not overridden + + whether to hide this tabset when it has no tabs + + Default: inherited from Global attribute tabSetEnableHidWhenEmpty (default false) + */ + tabSetEnableHideWhenEmpty?: boolean; + + /** Value for TabSetNode attribute enableDivide if not overridden allow user to drag tabs to region of this tabset, splitting into new tabset Default: true */ - tabSetEnableDivide?: boolean; + tabSetEnableDivide?: boolean; - /** + /** Value for TabSetNode attribute enableDrag if not overridden allow user to drag tabs out this tabset Default: true */ - tabSetEnableDrag?: boolean; + tabSetEnableDrag?: boolean; - /** + /** Value for TabSetNode attribute enableDrop if not overridden allow user to drag tabs into this tabset Default: true */ - tabSetEnableDrop?: boolean; + tabSetEnableDrop?: boolean; - /** + /** Value for TabSetNode attribute enableMaximize if not overridden allow user to maximize tabset to fill view via maximize button Default: true */ - tabSetEnableMaximize?: boolean; + tabSetEnableMaximize?: boolean; - /** + /** Value for TabSetNode attribute enableSingleTabStretch if not overridden if the tabset has only a single tab then stretch the single tab to fill area and display in a header style Default: false */ - tabSetEnableSingleTabStretch?: boolean; + tabSetEnableSingleTabStretch?: boolean; - /** + /** Value for TabSetNode attribute enableTabScrollbar if not overridden whether to show a mini scrollbar for the tabs Default: false */ - tabSetEnableTabScrollbar?: boolean; + tabSetEnableTabScrollbar?: boolean; - /** + /** Value for TabSetNode attribute enableTabStrip if not overridden enable tab strip and allow multiple tabs in this tabset Default: true */ - tabSetEnableTabStrip?: boolean; + tabSetEnableTabStrip?: boolean; - /** + /** Value for TabSetNode attribute enableTabWrap if not overridden wrap tabs onto multiple lines Default: false */ - tabSetEnableTabWrap?: boolean; + tabSetEnableTabWrap?: boolean; - /** + /** Value for TabSetNode attribute maxHeight if not overridden maximum height (in px) for this tabset Default: 99999 */ - tabSetMaxHeight?: number; + tabSetMaxHeight?: number; - /** + /** Value for TabSetNode attribute maxWidth if not overridden maximum width (in px) for this tabset Default: 99999 */ - tabSetMaxWidth?: number; + tabSetMaxWidth?: number; - /** + /** Value for TabSetNode attribute minHeight if not overridden minimum height (in px) for this tabset Default: 0 */ - tabSetMinHeight?: number; + tabSetMinHeight?: number; - /** + /** Value for TabSetNode attribute minWidth if not overridden minimum width (in px) for this tabset Default: 0 */ - tabSetMinWidth?: number; + tabSetMinWidth?: number; - /** + /** Value for TabSetNode attribute tabLocation if not overridden the location of the tabs either top or bottom Default: "top" */ - tabSetTabLocation?: ITabLocation; - + tabSetTabLocation?: ITabLocation; } export interface IRowAttributes { - /** + /** the unique id of the row, if left undefined a uuid will be assigned Default: undefined */ - id?: string; + id?: string; - /** + /** Fixed value: "row" */ - type?: string; + type?: string; - /** + /** relative weight for sizing of this row in parent row Default: 100 */ - weight?: number; - + weight?: number; } export interface ITabSetAttributes { - /** + /** whether to select new/moved tabs in tabset Default: inherited from Global attribute tabSetAutoSelectTab (default true) */ - autoSelectTab?: boolean; + autoSelectTab?: boolean; - /** + /** a class name to apply to the tab strip Default: inherited from Global attribute tabSetClassNameTabStrip (default undefined) */ - classNameTabStrip?: string; + classNameTabStrip?: string; - /** + /** a place to hold json config used in your own code Default: undefined */ - config?: any; + config?: any; - /** + /** whether the active icon (*) should be displayed when the tabset is active Default: inherited from Global attribute tabSetEnableActiveIcon (default false) */ - enableActiveIcon?: boolean; + enableActiveIcon?: boolean; - /** + /** allow user to close tabset via a close button Default: inherited from Global attribute tabSetEnableClose (default false) */ - enableClose?: boolean; + enableClose?: boolean; - /** + /** whether to delete this tabset when is has no tabs Default: inherited from Global attribute tabSetEnableDeleteWhenEmpty (default true) */ - enableDeleteWhenEmpty?: boolean; + enableDeleteWhenEmpty?: boolean; - /** + /** + whether to hide this tabset when it has no tabs + + Default: inherited from Global attribute tabSetEnableHideWhenEmpty (default false) + */ + enableHideWhenEmpty?: boolean; + + /** allow user to drag tabs to region of this tabset, splitting into new tabset Default: inherited from Global attribute tabSetEnableDivide (default true) */ - enableDivide?: boolean; + enableDivide?: boolean; - /** + /** allow user to drag tabs out this tabset Default: inherited from Global attribute tabSetEnableDrag (default true) */ - enableDrag?: boolean; + enableDrag?: boolean; - /** + /** allow user to drag tabs into this tabset Default: inherited from Global attribute tabSetEnableDrop (default true) */ - enableDrop?: boolean; + enableDrop?: boolean; - /** + /** allow user to maximize tabset to fill view via maximize button Default: inherited from Global attribute tabSetEnableMaximize (default true) */ - enableMaximize?: boolean; + enableMaximize?: boolean; - /** + /** if the tabset has only a single tab then stretch the single tab to fill area and display in a header style Default: inherited from Global attribute tabSetEnableSingleTabStretch (default false) */ - enableSingleTabStretch?: boolean; + enableSingleTabStretch?: boolean; - /** + /** whether to show a mini scrollbar for the tabs Default: inherited from Global attribute tabSetEnableTabScrollbar (default false) */ - enableTabScrollbar?: boolean; + enableTabScrollbar?: boolean; - /** + /** enable tab strip and allow multiple tabs in this tabset Default: inherited from Global attribute tabSetEnableTabStrip (default true) */ - enableTabStrip?: boolean; + enableTabStrip?: boolean; - /** + /** wrap tabs onto multiple lines Default: inherited from Global attribute tabSetEnableTabWrap (default false) */ - enableTabWrap?: boolean; + enableTabWrap?: boolean; - /** + /** the unique id of the tab set, if left undefined a uuid will be assigned Default: undefined */ - id?: string; + id?: string; - /** + /** maximum height (in px) for this tabset Default: inherited from Global attribute tabSetMaxHeight (default 99999) */ - maxHeight?: number; + maxHeight?: number; - /** + /** maximum width (in px) for this tabset Default: inherited from Global attribute tabSetMaxWidth (default 99999) */ - maxWidth?: number; + maxWidth?: number; - /** + /** minimum height (in px) for this tabset Default: inherited from Global attribute tabSetMinHeight (default 0) */ - minHeight?: number; + minHeight?: number; - /** + /** minimum width (in px) for this tabset Default: inherited from Global attribute tabSetMinWidth (default 0) */ - minWidth?: number; + minWidth?: number; - /** + /** Default: undefined */ - name?: string; + name?: string; - /** + /** index of selected/visible tab in tabset Default: 0 */ - selected?: number; + selected?: number; - /** + /** the location of the tabs either top or bottom Default: inherited from Global attribute tabSetTabLocation (default "top") */ - tabLocation?: ITabLocation; + tabLocation?: ITabLocation; - /** + /** Fixed value: "tabset" */ - type?: string; + type?: string; - /** + /** relative weight for sizing of this tabset in parent row Default: 100 */ - weight?: number; - + weight?: number; } export interface ITabAttributes { - /** + /** if there is no name specifed then this value will be used in the overflow menu Default: undefined */ - altName?: string; + altName?: string; - /** + /** height when added to border, -1 will use border size Default: inherited from Global attribute tabBorderHeight (default -1) */ - borderHeight?: number; + borderHeight?: number; - /** + /** width when added to border, -1 will use border size Default: inherited from Global attribute tabBorderWidth (default -1) */ - borderWidth?: number; + borderWidth?: number; - /** + /** class applied to tab button Default: inherited from Global attribute tabClassName (default undefined) */ - className?: string; + className?: string; - /** + /** see values in ICloseType Default: inherited from Global attribute tabCloseType (default 1) */ - closeType?: ICloseType; + closeType?: ICloseType; - /** + /** string identifying which component to run (for factory) Default: undefined */ - component?: string; + component?: string; - /** + /** a place to hold json config for the hosted component Default: undefined */ - config?: any; + config?: any; - /** + /** class applied to tab content Default: inherited from Global attribute tabContentClassName (default undefined) */ - contentClassName?: string; + contentClassName?: string; - /** + /** allow user to close tab via close button Default: inherited from Global attribute tabEnableClose (default true) */ - enableClose?: boolean; + enableClose?: boolean; - /** + /** allow user to drag tab to new location Default: inherited from Global attribute tabEnableDrag (default true) */ - enableDrag?: boolean; + enableDrag?: boolean; - /** + /** enable popout (in popout capable browser) Default: inherited from Global attribute tabEnablePopout (default false) */ - enablePopout?: boolean; + enablePopout?: boolean; - /** + /** whether to show the popout icon in the tabset header if this tab enables popouts Default: inherited from Global attribute tabEnablePopoutIcon (default true) */ - enablePopoutIcon?: boolean; + enablePopoutIcon?: boolean; - /** + /** if this tab will not work correctly in a popout window when the main window is backgrounded (inactive) then enabling this option will gray out this tab Default: inherited from Global attribute tabEnablePopoutOverlay (default false) */ - enablePopoutOverlay?: boolean; + enablePopoutOverlay?: boolean; - /** + /** allow user to rename tabs by double clicking Default: inherited from Global attribute tabEnableRename (default true) */ - enableRename?: boolean; + enableRename?: boolean; - /** + /** whether to avoid rendering component until tab is visible Default: inherited from Global attribute tabEnableRenderOnDemand (default true) */ - enableRenderOnDemand?: boolean; + enableRenderOnDemand?: boolean; - /** + /** if enabled the tab will re-mount when popped out/in Default: false */ - enableWindowReMount?: boolean; + enableWindowReMount?: boolean; - /** + /** + whether the tab remains open when clicking elsewhere + + Default: true + */ + pinned?: boolean; + + /** An optional help text for the tab to be displayed upon tab hover. Default: undefined */ - helpText?: string; + helpText?: string; - /** + /** the tab icon Default: inherited from Global attribute tabIcon (default undefined) */ - icon?: string; + icon?: string; - /** + /** the unique id of the tab, if left undefined a uuid will be assigned Default: undefined */ - id?: string; + id?: string; - /** + /** the max height of this tab Default: inherited from Global attribute tabMaxHeight (default 99999) */ - maxHeight?: number; + maxHeight?: number; - /** + /** the max width of this tab Default: inherited from Global attribute tabMaxWidth (default 99999) */ - maxWidth?: number; + maxWidth?: number; - /** + /** the min height of this tab Default: inherited from Global attribute tabMinHeight (default 0) */ - minHeight?: number; + minHeight?: number; - /** + /** the min width of this tab Default: inherited from Global attribute tabMinWidth (default 0) */ - minWidth?: number; + minWidth?: number; - /** + /** name of tab to be displayed in the tab button Default: "[Unnamed Tab]" */ - name?: string; + name?: string; - /** + /** class applied to parent tabset when this is the only tab and it is stretched to fill the tabset Default: undefined */ - tabsetClassName?: string; + tabsetClassName?: string; - /** + /** Fixed value: "tab" */ - type?: string; - + type?: string; } export interface IBorderAttributes { - /** + /** whether to select new/moved tabs in border when the border is currently closed Default: inherited from Global attribute borderAutoSelectTabWhenClosed (default false) */ - autoSelectTabWhenClosed?: boolean; + autoSelectTabWhenClosed?: boolean; - /** + /** whether to select new/moved tabs in border when the border is already open Default: inherited from Global attribute borderAutoSelectTabWhenOpen (default true) */ - autoSelectTabWhenOpen?: boolean; + autoSelectTabWhenOpen?: boolean; - /** + /** class applied to tab button Default: inherited from Global attribute borderClassName (default undefined) */ - className?: string; + className?: string; - /** + /** a place to hold json config used in your own code Default: undefined */ - config?: any; + config?: any; - /** + /** hide border if it has zero tabs Default: inherited from Global attribute borderEnableAutoHide (default false) */ - enableAutoHide?: boolean; + enableAutoHide?: boolean; - /** + /** whether tabs can be dropped into this border Default: inherited from Global attribute borderEnableDrop (default true) */ - enableDrop?: boolean; + enableDrop?: boolean; - /** + /** whether to show a mini scrollbar for the tabs Default: inherited from Global attribute borderEnableTabScrollbar (default false) */ - enableTabScrollbar?: boolean; + enableTabScrollbar?: boolean; - /** + /** the maximum size of the tab area Default: inherited from Global attribute borderMaxSize (default 99999) */ - maxSize?: number; + maxSize?: number; - /** + /** the minimum size of the tab area Default: inherited from Global attribute borderMinSize (default 0) */ - minSize?: number; + minSize?: number; - /** + /** index of selected/visible tab in border; -1 means no tab selected Default: -1 */ - selected?: number; + selected?: number; - /** + /** show/hide this border Default: true */ - show?: boolean; + show?: boolean; - /** + /** size of the tab area when selected Default: inherited from Global attribute borderSize (default 200) */ - size?: number; + size?: number; - /** + /** Fixed value: "border" */ - type?: string; - -} \ No newline at end of file + type?: string; +} diff --git a/src/model/Model.ts b/src/model/Model.ts index f730b672..17401607 100755 --- a/src/model/Model.ts +++ b/src/model/Model.ts @@ -665,6 +665,7 @@ export class Model { // tabset attributeDefinitions.add("tabSetEnableDeleteWhenEmpty", true).setType(Attribute.BOOLEAN); + attributeDefinitions.add("tabSetEnableHideWhenEmpty", false).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDrop", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDrag", true).setType(Attribute.BOOLEAN); attributeDefinitions.add("tabSetEnableDivide", true).setType(Attribute.BOOLEAN); diff --git a/src/model/RowNode.ts b/src/model/RowNode.ts index d3fdae82..c44839f3 100755 --- a/src/model/RowNode.ts +++ b/src/model/RowNode.ts @@ -95,18 +95,34 @@ export class RowNode extends Node implements IDropTarget { this.attributes.weight = weight; } + /** @internal */ + isHiddenNode(node: TabSetNode | RowNode): boolean { + return node instanceof TabSetNode && node.getChildren().length === 0 && node.isEnableHideWhenEmpty(); + } + /** @internal */ getSplitterBounds(index: number) { const h = this.getOrientation() === Orientation.HORZ; const c = this.getChildren(); const ss = this.model.getSplitterSize(); const fr = c[0].getRect(); - const lr = c[c.length - 1].getRect(); + let lr = c[c.length - 1].getRect(); + + // Special-case: Last node is hidden, in this case + // we take the most right node which is visible + for (let i = c.length - 1; i >= 0; i--) { + const n = c[i] as TabSetNode | RowNode; + if (!this.isHiddenNode(n)) { + lr = n.getRect(); + break; + } + } let p = h ? [fr.x, lr.getRight()] : [fr.y, lr.getBottom()]; const q = h ? [fr.x, lr.getRight()] : [fr.y, lr.getBottom()]; for (let i = 0; i < index; i++) { const n = c[i] as TabSetNode | RowNode; + if (this.isHiddenNode(n)) continue; p[0] += h ? n.getMinWidth() : n.getMinHeight(); q[0] += h ? n.getMaxWidth() : n.getMaxHeight(); if (i > 0) { @@ -117,6 +133,7 @@ export class RowNode extends Node implements IDropTarget { for (let i = c.length - 1; i >= index; i--) { const n = c[i] as TabSetNode | RowNode; + if (this.isHiddenNode(n)) continue; p[1] -= (h ? n.getMinWidth() : n.getMinHeight()) + ss; q[1] -= (h ? n.getMaxWidth() : n.getMaxHeight()) + ss; } @@ -143,7 +160,7 @@ export class RowNode extends Node implements IDropTarget { sum += s; } - const startRect = c[index].getRect() + const startRect = c[index].getRect(); const startPosition = (h ? startRect.x : startRect.y) - ss; return { initialSizes, sum, startPosition }; @@ -158,7 +175,8 @@ export class RowNode extends Node implements IDropTarget { const sizes = [...initialSizes]; - if (splitterPos < startPosition) { // moved left + if (splitterPos < startPosition) { + // moved left let shift = startPosition - splitterPos; let altShift = 0; if (sizes[index] + shift > smax) { @@ -180,7 +198,7 @@ export class RowNode extends Node implements IDropTarget { } } - for (let i = index+1; i < c.length; i++) { + for (let i = index + 1; i < c.length; i++) { const n = c[i] as TabSetNode | RowNode; const m = h ? n.getMaxWidth() : n.getMaxHeight(); if (sizes[i] + altShift < m) { @@ -191,16 +209,14 @@ export class RowNode extends Node implements IDropTarget { sizes[i] = m; } } - - } else { let shift = splitterPos - startPosition; let altShift = 0; - if (sizes[index-1] + shift > smax) { - altShift = sizes[index-1] + shift - smax; - sizes[index-1] = smax; + if (sizes[index - 1] + shift > smax) { + altShift = sizes[index - 1] + shift - smax; + sizes[index - 1] = smax; } else { - sizes[index-1] += shift; + sizes[index - 1] += shift; } for (let i = index; i < c.length; i++) { @@ -229,7 +245,7 @@ export class RowNode extends Node implements IDropTarget { } // 0.1 is to prevent weight ever going to zero - const weights = sizes.map(s => Math.max(0.1, s) * 100 / sum); + const weights = sizes.map((s) => (Math.max(0.1, s) * 100) / sum); // console.log(splitterPos, startPosition, "sizes", sizes); // console.log("weights",weights); @@ -364,7 +380,6 @@ export class RowNode extends Node implements IDropTarget { this.model.setActiveTabset(child, this.windowId); this.addChild(child); } - } /** @internal */ @@ -438,8 +453,7 @@ export class RowNode extends Node implements IDropTarget { if (dragNode instanceof TabSetNode || dragNode instanceof RowNode) { node = dragNode; // need to turn round if same orientation unless docking oposite direction - if (node instanceof RowNode && node.getOrientation() === this.getOrientation() && - (location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) { + if (node instanceof RowNode && node.getOrientation() === this.getOrientation() && (location.getOrientation() === this.getOrientation() || location === DockLocation.CENTER)) { node = new RowNode(this.model, this.windowId, {}); node.addChild(dragNode); } @@ -465,11 +479,11 @@ export class RowNode extends Node implements IDropTarget { } else { this.addChild(node, index); } - } else if (horz && dockLocation === DockLocation.LEFT || !horz && dockLocation === DockLocation.TOP) { + } else if ((horz && dockLocation === DockLocation.LEFT) || (!horz && dockLocation === DockLocation.TOP)) { this.addChild(node, 0); - } else if (horz && dockLocation === DockLocation.RIGHT || !horz && dockLocation === DockLocation.BOTTOM) { + } else if ((horz && dockLocation === DockLocation.RIGHT) || (!horz && dockLocation === DockLocation.BOTTOM)) { this.addChild(node); - } else if (horz && dockLocation === DockLocation.TOP || !horz && dockLocation === DockLocation.LEFT) { + } else if ((horz && dockLocation === DockLocation.TOP) || (!horz && dockLocation === DockLocation.LEFT)) { const vrow = new RowNode(this.model, this.windowId, {}); const hrow = new RowNode(this.model, this.windowId, {}); hrow.setWeight(75); @@ -481,7 +495,7 @@ export class RowNode extends Node implements IDropTarget { vrow.addChild(node); vrow.addChild(hrow); this.addChild(vrow); - } else if (horz && dockLocation === DockLocation.BOTTOM || !horz && dockLocation === DockLocation.RIGHT) { + } else if ((horz && dockLocation === DockLocation.BOTTOM) || (!horz && dockLocation === DockLocation.RIGHT)) { const vrow = new RowNode(this.model, this.windowId, {}); const hrow = new RowNode(this.model, this.windowId, {}); hrow.setWeight(75); @@ -502,8 +516,6 @@ export class RowNode extends Node implements IDropTarget { this.model.tidy(); } - - /** @internal */ isEnableDrop() { return true; @@ -524,12 +536,11 @@ export class RowNode extends Node implements IDropTarget { return RowNode.attributeDefinitions; } - - // NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize + // NOTE: flex-grow cannot have values < 1 otherwise will not fill parent, need to normalize normalizeWeights() { let sum = 0; for (const n of this.children) { - const node = (n as TabSetNode | RowNode); + const node = n as TabSetNode | RowNode; sum += node.getWeight(); } @@ -538,8 +549,8 @@ export class RowNode extends Node implements IDropTarget { } for (const n of this.children) { - const node = (n as TabSetNode | RowNode); - node.setWeight(Math.max(0.001, 100 * node.getWeight() / sum)); + const node = n as TabSetNode | RowNode; + node.setWeight(Math.max(0.001, (100 * node.getWeight()) / sum)); } } @@ -547,12 +558,8 @@ export class RowNode extends Node implements IDropTarget { private static createAttributeDefinitions(): AttributeDefinitions { const attributeDefinitions = new AttributeDefinitions(); attributeDefinitions.add("type", RowNode.TYPE, true).setType(Attribute.STRING).setFixed(); - attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription( - `the unique id of the row, if left undefined a uuid will be assigned` - ); - attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription( - `relative weight for sizing of this row in parent row` - ); + attributeDefinitions.add("id", undefined).setType(Attribute.STRING).setDescription(`the unique id of the row, if left undefined a uuid will be assigned`); + attributeDefinitions.add("weight", 100).setType(Attribute.NUMBER).setDescription(`relative weight for sizing of this row in parent row`); return attributeDefinitions; } diff --git a/src/model/TabNode.ts b/src/model/TabNode.ts index c9d38450..fe9fe48a 100755 --- a/src/model/TabNode.ts +++ b/src/model/TabNode.ts @@ -158,6 +158,10 @@ export class TabNode extends Node implements IDraggable { return this.getAttr("enableRenderOnDemand") as boolean; } + isPinned() { + return this.getAttr("pinned") as boolean; + } + getMinWidth() { return this.getAttr("minWidth") as number; } @@ -364,7 +368,9 @@ export class TabNode extends Node implements IDraggable { attributeDefinitions.add("enableWindowReMount", false).setType(Attribute.BOOLEAN).setDescription( `if enabled the tab will re-mount when popped out/in` ); - + attributeDefinitions.add("pinned", true).setType(Attribute.BOOLEAN).setDescription( + `whether the tab remains open when clicking elsewhere` + ); attributeDefinitions.addInherited("enableClose", "tabEnableClose").setType(Attribute.BOOLEAN).setDescription( `allow user to close tab via close button` ); diff --git a/src/model/TabSetNode.ts b/src/model/TabSetNode.ts index 0c11c27f..c6ba216b 100755 --- a/src/model/TabSetNode.ts +++ b/src/model/TabSetNode.ts @@ -173,6 +173,10 @@ export class TabSetNode extends Node implements IDraggable, IDropTarget { return this.getAttr("enableDeleteWhenEmpty") as boolean; } + isEnableHideWhenEmpty() { + return this.getAttr("enableHideWhenEmpty") as boolean; + } + isEnableDrop() { return this.getAttr("enableDrop") as boolean; } @@ -529,6 +533,9 @@ export class TabSetNode extends Node implements IDraggable, IDropTarget { attributeDefinitions.addInherited("enableDeleteWhenEmpty", "tabSetEnableDeleteWhenEmpty").setDescription( `whether to delete this tabset when is has no tabs` ); + attributeDefinitions.addInherited("enableHideWhenEmpty", "tabSetEnableHideWhenEmpty").setDescription( + "whether to hide this tabset when it has no tabs" + ); attributeDefinitions.addInherited("enableDrop", "tabSetEnableDrop").setDescription( `allow user to drag tabs into this tabset` ); diff --git a/src/view/BorderTab.tsx b/src/view/BorderTab.tsx index 02dceaa4..8bc31c33 100644 --- a/src/view/BorderTab.tsx +++ b/src/view/BorderTab.tsx @@ -17,6 +17,8 @@ export function BorderTab(props: IBorderTabProps) { const { layout, border, show } = props; const selfRef = React.useRef(null); const timer = React.useRef(undefined); + const selectedNode = border.getSelectedNode(); + const pinned = selectedNode?.isPinned(); React.useLayoutEffect(() => { const contentRect = layout.getBoundingClientRect(selfRef.current!); @@ -55,20 +57,47 @@ export function BorderTab(props: IBorderTabProps) { style.display = show ? "flex" : "none"; + if (show && pinned === false) { + style.position = "absolute"; + style.zIndex = 999; + style.pointerEvents = "none"; + style.backgroundColor = "transparent"; + const headerRect = border.getTabHeaderRect(); + if (border.getLocation() === DockLocation.LEFT) { + style.left = headerRect.width; + style.top = 0; + style.bottom = 0; + } else if (border.getLocation() === DockLocation.RIGHT) { + style.right = headerRect.width; + style.top = 0; + style.bottom = 0; + } else if (border.getLocation() === DockLocation.TOP) { + style.top = headerRect.height; + style.left = 0; + style.right = 0; + } else { // DockLocation.BOTTOM + style.bottom = headerRect.height; + style.left = 0; + style.right = 0; + } + } + const className = layout.getClassName(CLASSES.FLEXLAYOUT__BORDER_TAB_CONTENTS); + const splitter = show && pinned !== false ? : null; + if (border.getLocation() === DockLocation.LEFT || border.getLocation() === DockLocation.TOP) { return ( <>
- {show && } + {splitter} ); } else { return ( <> - {show && } + {splitter}
diff --git a/src/view/BorderTabSet.tsx b/src/view/BorderTabSet.tsx index ed6056b1..0873a015 100755 --- a/src/view/BorderTabSet.tsx +++ b/src/view/BorderTabSet.tsx @@ -178,9 +178,48 @@ export const BorderTabSet = (props: IBorderTabSetProps) => { } const selectedIndex = border.getSelected(); - if (selectedIndex !== -1) { - const selectedTabNode = border.getChildren()[selectedIndex] as TabNode; - if (selectedTabNode !== undefined && layout.isSupportsPopout() && selectedTabNode.isEnablePopout()) { + const selectedTabNode = selectedIndex !== -1 ? (border.getChildren()[selectedIndex] as TabNode) : undefined; + const isPinned = selectedTabNode?.isPinned(); + + React.useEffect(() => { + if (selectedTabNode && !isPinned) { + const onBodyPointerDown = (e: PointerEvent) => { + const layoutRect = layout.getDomRect(); + const x = e.clientX - layoutRect.x; + const y = e.clientY - layoutRect.y; + if (!border.getTabHeaderRect().contains(x, y) && !border.getContentRect().contains(x, y)) { + layout.doAction(Actions.selectTab(selectedTabNode.getId())); + } + }; + document.addEventListener("pointerdown", onBodyPointerDown); + return () => document.removeEventListener("pointerdown", onBodyPointerDown); + } + console.error("BorderTabSet pinned returned empty callback.") + return () => {}; + }, [selectedTabNode, isPinned, border, layout]); + + if (selectedTabNode !== undefined) { + const pinTitle = selectedTabNode.isPinned() ? layout.i18nName(I18nLabel.Unpin_Tab) : layout.i18nName(I18nLabel.Pin_Tab); + const pinIcon = selectedTabNode.isPinned() + ? ((typeof icons.unpin === "function") ? icons.unpin(selectedTabNode) : icons.unpin) + : ((typeof icons.pin === "function") ? icons.pin(selectedTabNode) : icons.pin); + const onPinClick = (event: React.MouseEvent) => { + layout.doAction(Actions.updateNodeAttributes(selectedTabNode.getId(), { pinned: !selectedTabNode.isPinned() })); + event.stopPropagation(); + }; + buttons.push( + + ); + + if (layout.isSupportsPopout() && selectedTabNode.isEnablePopout()) { const popoutTitle = layout.i18nName(I18nLabel.Popout_Tab); buttons.push(
); -}; - - +}; \ No newline at end of file diff --git a/src/view/Tab.tsx b/src/view/Tab.tsx index f294623f..486be228 100755 --- a/src/view/Tab.tsx +++ b/src/view/Tab.tsx @@ -71,6 +71,11 @@ export const Tab = (props: ITabProps) => { rect.styleWithPosition(style); + if (parentNode instanceof BorderNode && !node.isPinned()) { + style.zIndex = 1000; + style.boxShadow = "0 2px 8px rgba(0,0,0,0.2)"; + } + let overlay = null; if (selected) {