diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 000000000..9f5ddbee4 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CxJS is a feature-rich JavaScript framework for building complex web front-ends, such as BI tools, dashboards and admin apps. This is a monorepo using yarn workspaces that contains the main cx package, documentation, gallery, testing environments, and various themes. + +## Development Commands + +### Build System +- `yarn build` or `npm run build` - Builds the main CxJS library using custom build tools +- `node packages/cx/build/index.js` - Direct build command for the cx package + +### Testing +- `yarn test` or `npm test` - Runs tests using Mocha with custom configuration +- Tests are configured in `test/mocha.config.js` + +### Development Servers +- `yarn start` or `npm start` - Runs documentation site development server +- `yarn docs` - Alternative command for documentation +- `yarn gallery` - Runs the gallery application showcasing widgets and themes +- `yarn litmus` - Runs the litmus testing environment for bug reproduction +- `yarn fiddle` - Runs the online code editor/playground + +### TypeScript Examples +- `cd ts-minimal && yarn start` - Runs TypeScript minimal example development server +- `cd ts-minimal && yarn build` - Builds TypeScript minimal example for production + +### Theme Building +- `npm run build:theme:core` - Builds core theme +- `npm run build:theme:dark` - Builds dark theme +- `npm run build:theme:frost` - Builds frost theme +- `npm run build:theme:material` - Builds material theme + +## Architecture + +### Monorepo Structure +The project uses yarn workspaces with these main areas: +- `packages/cx/` - Core framework source code +- `docs/` - Documentation site and content +- `gallery/` - Widget gallery and theme showcase +- `litmus/` - Bug reproduction and testing environment +- `fiddle/` - Online code editor +- `ts-minimal/` - TypeScript minimal example +- `themes/` - Various UI themes + +### Core Package Structure (packages/cx/) +- `src/util/` - Utility functions and helpers +- `src/data/` - Data binding, stores, and state management +- `src/ui/` - Core UI framework and widgets +- `src/widgets/` - Form controls, grids, overlays +- `src/charts/` - Charting components +- `src/svg/` - SVG drawing utilities +- `src/hooks/` - React-like hooks for functional components + +### Build System +- Custom build tools located in `cx-build-tools` package +- Uses Rollup for JavaScript bundling +- SCSS compilation for stylesheets +- Modular builds for different parts (util, data, ui, widgets, charts, svg, hooks) + +## TypeScript Configuration + +### JSX Configuration +- The project uses custom JSX configuration with `jsxImportSource: "cx"` +- For newer TypeScript projects, use `"jsx": "react-jsx"` and `"jsxImportSource": "cx"` +- For legacy projects, use `"jsxFactory": "cx"` + +### Path Mapping +Configure TypeScript paths for development: +```json +{ + "paths": { + "cx": ["../packages/cx/src"], + "cx-react": ["../packages/cx-react"] + } +} +``` + +## Key Framework Concepts + +### Data Binding +- Uses two-way data binding with store-based state management +- Accessor chains for deep property access (e.g., `{bind: "user.profile.name"}`) +- Controllers for computed values and business logic + +### Widget System +- All UI components inherit from Widget base class +- Supports both declarative configuration and functional components +- Rich set of form controls, grids, charts, and layout components + +### Theming +- SCSS-based theming system with variables and mixins +- Multiple ready-to-use themes available as separate packages +- Theme packages follow pattern: `cx-theme-{name}` + +## Testing Strategy + +### Test Environments +- `litmus/` - Manual testing environment for bug reproduction and feature development +- Organized by bugs, features, and performance tests +- Examples in `litmus/bugs/`, `litmus/features/`, `litmus/performance/` + +### Running Specific Tests +- Tests are located in various subdirectories +- Use Mocha test runner with Babel transpilation +- Configuration in `test/mocha.config.js` + +## Development Workflow + +### Adding New Features +1. Implement in appropriate `packages/cx/src/` subdirectory +2. Add TypeScript definitions (.d.ts files) +3. Create examples in `litmus/features/` +4. Add documentation in `docs/content/` +5. Update gallery examples if relevant + +### Working with Themes +- Theme source files are in individual theme packages +- Use webpack configurations for building theme assets +- Test themes using gallery application + +### Package Management +- Use yarn for consistency with workspace configuration +- Install dependencies at root level for shared packages +- Individual packages have their own package.json for specific dependencies + +## Claude Code Notes + +### File Paths +- Always use relative paths (e.g., `gallery/tsconfig.json`) instead of absolute paths (e.g., `D:/Code/CxJS/cxjs/gallery/tsconfig.json`) when reading and editing files to avoid "File has been unexpectedly modified" errors on Windows. \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..529f56d1b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,36 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn build)", + "Bash(for f in packages/cx/src/util/date/*.ts)", + "Bash(do [ ! -f \"$f%.ts.d.ts\" ])", + "Bash(echo:*)", + "Bash(done)", + "Bash(find:*)", + "Bash(head:*)", + "Bash(npx tsc:*)", + "Bash(cut:*)", + "WebFetch(domain:github.com)", + "Bash(curl -s https://raw.githubusercontent.com/codaxy/cxjs/master/packages/cx/src/data/View.d.ts)", + "Bash(yarn build2:*)", + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(cat package.json)", + "Bash(curl:*)", + "Bash(yarn check-types:*)", + "Bash(yarn test)", + "Bash(claude mcp list:*)", + "mcp__playwright__browser_navigate", + "mcp__playwright__browser_tabs", + "mcp__playwright__browser_console_messages", + "mcp__playwright__browser_network_requests", + "mcp__playwright__browser_snapshot", + "mcp__playwright__browser_wait_for", + "mcp__playwright__browser_run_code", + "mcp__playwright__browser_click", + "mcp__playwright__browser_take_screenshot", + "Bash(yarn compile:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..9d74ed1c5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: yarn install --immutable + + - name: Build cx package + run: yarn build + + - name: Run tests + run: yarn test diff --git a/.gitignore b/.gitignore index 52130c8ae..5f6e16cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,7 @@ node_modules *.hot-update.js *.log dist +build -Thumbs.db \ No newline at end of file +Thumbs.db +.playwright-mcp \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 644d4bfc6..3d16fca8f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "request": "launch", "name": "Debug Tests", "skipFiles": ["/**"], - "runtimeArgs": ["test", "--inspect"] + "runtimeArgs": ["test", "--inspect"], + "cwd": "${workspaceFolder}/packages/cx" }, { "type": "node", diff --git a/.vscode/settings.json b/.vscode/settings.json index 88f2f5255..7e2a0fe4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,39 @@ { - "cSpell.words": ["flexbox", "hscroll", "immer", "lookupfield", "resizer", "trello"] + "cSpell.words": ["flexbox", "hscroll", "immer", "lookupfield", "resizer", "trello"], + "explorer.autoRevealExclude": { + "**/node_modules": false + }, + "javascript.preferences.importModuleSpecifier": "relative", + "files.watcherExclude": { + "**/node_modules": true, + "**/dist": true, + "**/build": true, + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/build": true + }, + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#1c95fe", + "activityBar.background": "#1c95fe", + "activityBar.foreground": "#e7e7e7", + "activityBar.inactiveForeground": "#e7e7e799", + "activityBarBadge.background": "#be0166", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#e7e7e799", + "sash.hoverBorder": "#1c95fe", + "statusBar.background": "#017ce6", + "statusBar.foreground": "#e7e7e7", + "statusBarItem.hoverBackground": "#1c95fe", + "statusBarItem.remoteBackground": "#017ce6", + "statusBarItem.remoteForeground": "#e7e7e7", + "titleBar.activeBackground": "#017ce6", + "titleBar.activeForeground": "#e7e7e7", + "titleBar.inactiveBackground": "#017ce699", + "titleBar.inactiveForeground": "#e7e7e799" + }, + "peacock.color": "#017ce6" } diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 000000000..80332fbe8 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,542 @@ +# CxJS TypeScript Migration Guide + +This document captures patterns and best practices learned during the migration from JavaScript to TypeScript. + +## Table of Contents + +1. [Config Interfaces](#config-interfaces) +2. [Instance Types](#instance-types) +3. [Generic Base Classes](#generic-base-classes) +4. [Property Declarations](#property-declarations) +5. [Prototype Property Initialization](#prototype-property-initialization) +6. [Type Casting Patterns](#type-casting-patterns) +7. [Component Props and State](#component-props-and-state) +8. [Common Fixes](#common-fixes) +9. [JSX Components](#jsx-components) +10. [Class Constructors](#class-constructors) + +--- + +## Config Interfaces + +Every widget should have a Config interface that extends the appropriate parent config: + +```typescript +export interface ButtonConfig extends HtmlElementConfig { + confirm?: Prop>; + pressed?: BooleanProp; + icon?: StringProp; + disabled?: BooleanProp; + onClick?: string | ((e: MouseEvent, instance: Instance) => void); +} + +export class Button extends HtmlElement { + // ... +} +``` + +### Config Naming Convention + +- Use `{WidgetName}Config` for the interface name +- Extend the parent widget's config (e.g., `HtmlElementConfig`, `PureContainerConfig`, `ContainerConfig`) + +### Property Types in Config + +- Use `Prop` for bindable properties that accept values, bindings, or selectors +- Use `StringProp`, `BooleanProp`, `NumberProp` for common typed props +- Use `BindingInput` when the property is specifically used with `Binding.get()` +- Use literal union types for constrained string values (e.g., `"start" | "center" | "end"`) + +--- + +## Instance Types + +### Creating Custom Instance Types + +When a widget needs custom properties on its instance (like a specialized store), create a custom instance interface: + +```typescript +export interface SandboxInstance extends Instance { + store: ExposedValueView; +} + +export class Sandbox extends PureContainerBase { + initInstance(context: RenderingContext, instance: SandboxInstance): void { + instance.store = new ExposedValueView({ + /* ... */ + }); + } +} +``` + +### Standard Instance Types + +- `Instance` - Base instance type for most widgets +- `HtmlElementInstance` - For HTML-based widgets, includes `tooltips` property +- Custom interfaces extending `Instance` for specialized needs + +### TooltipParentInstance Pattern + +Widgets that support tooltips should use `HtmlElementInstance` which implements: + +```typescript +export interface TooltipParentInstance extends Instance { + tooltips: { [key: string]: TooltipInstance }; +} +``` + +--- + +## Generic Base Classes + +### Using Generic vs Non-Generic Base Classes + +CxJS provides both generic and non-generic versions of base classes: + +| Non-Generic | Generic | +| --------------- | ------------------------------------------------- | +| `Container` | `ContainerBase` | +| `PureContainer` | `PureContainerBase` | +| `HtmlElement` | `HtmlElement` (already generic) | + +**Use the generic Base version when creating typed widgets:** + +```typescript +// Correct - using generic base +export class FlexBox extends ContainerBase {} + +// Incorrect - Container is not generic +export class FlexBox extends Container {} // Error! +``` + +--- + +## Property Declarations + +### Using `declare` for Class Properties + +Use `declare` to inform TypeScript about properties that exist at runtime but aren't initialized in the class body: + +```typescript +export class FlexBox extends ContainerBase { + declare spacing?: string | boolean; + declare hspacing?: boolean | string; + declare wrap?: boolean; + declare baseClass: string; // Non-nullable when defined in prototype +} +``` + +### When to Use `declare` + +- Properties initialized via prototype assignment +- Properties set by the framework during initialization +- Properties from config that are copied to the instance + +### Non-Nullable vs Optional + +```typescript +// Optional - may or may not be set +declare spacing?: string; + +// Non-nullable - always defined (e.g., in prototype) +declare baseClass: string; +``` + +--- + +## Prototype Property Initialization + +### Using `undefined` Instead of `null` + +When TypeScript types don't include `null`, use `undefined` for prototype initialization: + +```typescript +// Type doesn't allow null +buttonMod?: string; + +// Incorrect - type error +MsgBox.prototype.buttonMod = null; + +// Correct +MsgBox.prototype.buttonMod = undefined; +``` + +### Non-Nullable Prototype Properties + +When a property is always defined in the prototype, declare it as non-nullable: + +```typescript +export class Section extends ContainerBase { + declare baseClass: string; // Not optional - defined in prototype +} + +Section.prototype.baseClass = "section"; +``` + +--- + +## Type Casting Patterns + +### Accessing Properties Not in Instance Type + +When accessing properties that exist at runtime but aren't in the type: + +```typescript +// Use type assertion +prepareData(context: RenderingContext, instance: Instance): void { + let { eventHandlers } = (instance as any); + // ... +} +``` + +### Config Object Casting for Constructors + +When a constructor uses `Object.assign` but the config type is limited: + +```typescript +// First, create a proper config interface +export interface ExposedValueViewConfig extends ViewConfig { + containerBinding?: Binding; + key?: string | null; + recordName?: string; +} + +// Then the constructor accepts the extended config +instance.store = new ExposedValueView({ + store: instance.parentStore, + containerBinding: this.storageBinding, + key: null, + recordName: this.recordName, +}); +``` + +--- + +## Component Props and State + +### React Component Props Interface + +For VDOM components, create typed props and state interfaces: + +```typescript +interface ResizerCmpProps { + instance: Instance; + data: Record; // Use Record for dynamic data properties +} + +interface ResizerCmpState { + dragged: boolean; + offset: number; + initialPosition?: { clientX: number; clientY: number }; +} + +class ResizerCmp extends VDOM.Component { + // ... +} +``` + +### Using `Record` for Data + +When component data has dynamically declared properties (via `declareData`), use `Record`: + +```typescript +interface MyComponentProps { + instance: Instance; + data: Record; // Allows accessing any property +} +``` + +--- + +## Common Fixes + +### 1. "Type 'X' is not generic" + +**Problem:** Using non-generic base class with type parameters. + +**Fix:** Use the generic `Base` version: + +```typescript +// Wrong +class MyWidget extends Container {} + +// Correct +class MyWidget extends ContainerBase {} +``` + +### 2. "Property does not exist on type 'Instance'" + +**Problem:** Accessing custom instance properties. + +**Fix:** Create custom instance interface or use type assertion: + +```typescript +// Option 1: Custom interface +interface MyInstance extends Instance { + customProp: string; +} + +// Option 2: Type assertion +(instance as any).customProp; +``` + +### 3. "Object literal may only specify known properties" + +**Problem:** Passing extra properties to a constructor. + +**Fix:** Extend the config interface: + +```typescript +export interface ExtendedConfig extends BaseConfig { + extraProp?: string; +} +``` + +### 4. "'X' is possibly 'undefined'" + +**Problem:** Accessing potentially undefined properties. + +**Fix:** Use optional chaining or non-null assertion: + +```typescript +// Optional chaining +if (components?.header) { + /* ... */ +} + +// Or declare as non-nullable if always defined +declare; +baseClass: string; +``` + +### 5. Method Signature Mismatches + +**Problem:** Overridden methods have incompatible signatures. + +**Fix:** Match parameter types with parent class or use compatible subtypes: + +```typescript +// Parent uses Instance, child can use more specific type +prepareData(context: RenderingContext, instance: HtmlElementInstance): void { + super.prepareData(context, instance); +} +``` + +--- + +## JSX Components + +### Adding the JSX Pragma + +For components that render JSX in their `render` method, add the JSX pragma at the top of the file: + +```typescript +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig } from "../svg/BoundedObject"; +// ... rest of imports +``` + +This tells TypeScript/Babel to use React's JSX factory for transforming JSX syntax. + +### When to Add the Pragma + +- Any `.tsx` file that contains JSX syntax (e.g., ``, `
`, ``) +- Files with `render()` methods that return JSX elements +- Files that use VDOM components + +--- + +## Class Constructors + +### Always Add a Constructor + +Every widget class should have an explicit constructor that accepts the config type: + +```typescript +export class MyWidget extends BoundedObject { + constructor(config?: MyWidgetConfig) { + super(config); + } + + // ... rest of the class +} +``` + +### Why Add Constructors + +- Provides proper type inference when creating widgets +- Ensures config properties are correctly typed +- Makes the class API explicit and self-documenting + +--- + +## Migration Checklist + +When migrating a widget from JS to TS: + +1. [ ] Remove `//@ts-nocheck` directive +2. [ ] Add JSX pragma `/** @jsxImportSource react */` if file contains JSX +3. [ ] Create `{WidgetName}Config` interface extending appropriate parent +4. [ ] Add generic type parameters to base class if needed +5. [ ] Add constructor accepting the config type +6. [ ] Add `declare` statements for all class properties +7. [ ] Add type annotations to all methods +8. [ ] Create custom instance interface if needed +9. [ ] Fix prototype initializations (use `undefined` not `null` where needed) +10. [ ] Declare `baseClass` as non-nullable if defined in prototype +11. [ ] Verify with `yarn check-types` in packages/cx +12. [ ] Delete the corresponding `.d.ts` file + +--- + +## File Organization + +After migration, each widget should have: + +- `Widget.tsx` - The implementation with inline types +- No separate `Widget.d.ts` - Types are in the source file + +Index files (`index.ts`) should re-export all public types: + +```typescript +export { Button, ButtonConfig } from "./Button"; +export { FlexBox, FlexBoxConfig } from "./FlexBox"; +``` + +## Typed RenderingContext Usage + +RenderingContext uses `[key: string]: any` to allow dynamic property access. Parent widgets set context properties that children consume. To add type safety, define typed context interfaces that extend RenderingContext and use them directly in method signatures. + +### Pattern: Typed Method Signatures + +Define a typed context interface next to the widget that sets the context properties, then use it in method signatures: + +```typescript +// In ValidationGroup.ts +import { RenderingContext } from "../../ui/RenderingContext"; + +/** Typed context interface for form-related context properties */ +export interface FormRenderingContext extends RenderingContext { + parentDisabled?: boolean; + parentReadOnly?: boolean; + parentViewMode?: boolean | string; + parentTabOnEnterKey?: boolean; + parentVisited?: boolean; + parentStrict?: boolean; + parentAsterisk?: boolean; + validation?: { errors: ValidationErrorData[] }; + lastFieldId?: string; +} + +export class ValidationGroup extends PureContainerBase<...> { + // Use typed context directly in method signature + explore(context: FormRenderingContext, instance: ValidationGroupInstance): void { + context.push("parentStrict", coalesce(instance.data.strict, context.parentStrict)); + context.push("parentDisabled", coalesce(instance.data.disabled, context.parentDisabled)); + super.explore(context, instance); + } +} +``` + +### Existing Typed Contexts + +**FormRenderingContext** (ValidationGroup.ts): + +- Used by: ValidationGroup, Field, Label, ValidationError, Button +- Properties: `parentDisabled`, `parentReadOnly`, `parentViewMode`, `parentTabOnEnterKey`, `parentVisited`, `parentStrict`, `parentAsterisk`, `validation`, `lastFieldId` + +**SvgRenderingContext** (BoundedObject.ts): + +- Used by: Svg, BoundedObject and all SVG components +- Properties: `parentRect`, `inSvg`, `addClipRect` + +**ChartRenderingContext** (Chart.ts) - extends SvgRenderingContext: + +- Used by: Chart, all chart components (LineGraph, ScatterGraph, Gridlines, Marker, etc.) +- Properties: `axes` (Record of axis calculators) + +### Guidelines + +1. **Define locally**: Keep context interfaces next to the widgets that define them +2. **Export for consumers**: Export the interface so child widgets can import and use it +3. **Extend appropriately**: ChartRenderingContext extends SvgRenderingContext since charts need `parentRect` +4. **Use non-null assertion**: When accessing context properties that must exist, use `context.axes!` + +--- + +## Nice to Have Improvements + +[x] Typed RenderingContext usage +[x] Better StructuredProp and typed ContentResolver +[x] dropdownOptions might typed as DropdownConfig? +[x] Properly type Component.create, Widget.create, etc. +[x] Find controller by type +[x] Use Creatable in various places +[x] Typed selection and dataAdapters +[ ] Full Creatable typing (probably impossible) +[ ] Resolve tree-shaking +[ ] Allow Netlify to prerender CxJS docs so AI can consume it or make a static site somehow + +--- + +## Documentation Comparison Testing + +After migrating widgets, compare the local documentation site against the production site to detect runtime errors. + +### Setup + +1. **Start documentation server:** + + ```bash + cd docs && yarn start + ``` + + Local server runs at http://localhost:8065 + +2. **Start TypeScript watch compilation (optional):** + + ```bash + cd packages/cx && yarn compile -w + ``` + +3. **Set up Playwright MCP for browser automation:** + ```bash + claude mcp add --transport stdio --scope local playwright -- npx -y @playwright/mcp@latest + ``` + +### Testing Process + +1. Navigate through documentation pages on both local (localhost:8065) and production (docs.cxjs.io) +2. Check browser console for errors on local site +3. Compare rendered output between sites +4. After fixing bugs, run `yarn compile` in `packages/cx` (HMR will reload) + +### Common Migration Bugs + +1. **Missing declare statements** - TypeScript class fields without `declare` overwrite values from `Object.assign(this, config)` in parent constructors, causing undefined properties + +2. **Missing Config interfaces** - Widgets need proper Config interfaces extending parent configs. Reference online `.d.ts` files for proper typing: + ``` + https://github.com/codaxy/cxjs/blob/master/packages/cx/src/ui/{WidgetName}.d.ts + ``` + +--- + +### Active Bugs + +[x] Infinite Grid repeating row values (Docs) +[x] Missing search icon in Aquamarine theme (Gallery) +[x] SWC JSX plugin uses src + +## Finalization + +[x] Check online .d.ts files for all widgets at the end +[x] Check all Config files without comments if comments are available online +[x] Migrate gallery +[x] Migrate changes independently done to master +[x] Write detailed documentation and migration paths +[ ] Publish under cx@ts tag and test in other applications +[ ] Migrate all docs examples to typescript +[ ] Figure out a fiddle replacement (with AI :) +[x] Migrate some of the libraries (Google Maps, Diagrams) diff --git a/benchmark/package.json b/benchmark/package.json index 2a8c4d065..cd0560692 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -9,46 +9,45 @@ }, "dependencies": { "casual": "^1.6.2", - "core-js": "^3.39.0", - "cx": "^25.4.1", - "cx-react": "^24.7.1", + "core-js": "^3.47.0", + "cx": "workspace:*", + "cx-react": "workspace:*", "intl": "^1.2.5", - "react": "^18.3.1", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@types/node": "^22.10.1", - "@types/react": "^18.3.14", - "babel-loader": "^9.2.1", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "babel-loader": "^10.0.0", "babel-plugin-transform-cx-imports": "^21.3.0", "babel-plugin-transform-cx-jsx": "^21.3.0", "babel-preset-cx-env": "^24.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.5", "if-loader": "^1.0.2", "inline-manifest-webpack-plugin": "^4.0.2", "json-loader": "^0.5.7", "mini-css-extract-plugin": "^2.9.2", "modify-babel-preset": "^3.2.1", "sass": "^1.77.8", - "sass-loader": "^16.0.1", + "sass-loader": "^16.0.6", "serve": "^14.2.4", "style-loader": "^4.0.0", "svg-url-loader": "^8.0.0", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cleanup-plugin": "^0.5.1", "webpack-cli": "^5.1.4", "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" } } diff --git a/docs/app/DocsNav.js b/docs/app/DocsNav.js index 768fba92d..b165994da 100644 --- a/docs/app/DocsNav.js +++ b/docs/app/DocsNav.js @@ -11,7 +11,7 @@ export const docsNavTree = [ ], }, { - text: "Pre-requsites", + text: "About", children: [ { text: "JSX", url: "~/intro/jsx" }, { text: "CLI", url: "~/intro/command-line" }, @@ -20,6 +20,10 @@ export const docsNavTree = [ text: "Breaking Changes", url: "~/intro/breaking-changes", }, + { + text: "TypeScript Migration", + url: "~/intro/type-script-migration", + }, { text: "Step by Step Tutorial", url: "~/intro/step-by-step", diff --git a/docs/app/icons.js b/docs/app/icons.js index c58ccf768..eabdeb3d5 100644 --- a/docs/app/icons.js +++ b/docs/app/icons.js @@ -2,7 +2,7 @@ import { VDOM } from 'cx/ui'; import { Icon } from 'cx/widgets'; Icon.registerFactory((name, props) => { - props = { ...props }; - props.className = `fa fa-${name} ${props.className || ''}`; - return + let { key, ...rest } = props; + rest.className = `fa fa-${name} ${rest.className || ''}`; + return }); diff --git a/docs/babel-config.js b/docs/babel-config.js index 1eadb324a..a2c66de0c 100644 --- a/docs/babel-config.js +++ b/docs/babel-config.js @@ -1,36 +1,42 @@ - +let transformImports = require("../packages/babel-plugin-transform-cx-imports"); module.exports = function (options) { - var isProduction = options.production; + var isProduction = options.production; - return { - "cacheDirectory": true, - "cacheIdentifier": "v16", - "presets": [ - ["@babel/preset-env", { - loose: true, - modules: false, - useBuiltIns: "usage", - corejs: 3, - targets: { - chrome: 55, - ie: 11, - firefox: 30, - edge: 12, - safari: 9 - } - }] - ], - "plugins": [ - ['transform-cx-jsx', { - trimWhitespace: true, - trimWhitespaceExceptions: ['Md', 'CodeSnippet', 'CodeSplit'] - }], - ["@babel/transform-react-jsx", { "runtime": 'automatic' }], - "@babel/proposal-function-bind", - isProduction && ["transform-cx-imports", { useSrc: true }], - ].filter(Boolean) - } + return { + cacheDirectory: true, + cacheIdentifier: "v16", + presets: [ + "@babel/typescript", + [ + "@babel/preset-env", + { + loose: true, + modules: false, + useBuiltIns: "usage", + corejs: 3, + targets: { + chrome: 55, + ie: 11, + firefox: 30, + edge: 12, + safari: 9, + }, + }, + ], + ], + plugins: [ + [ + "transform-cx-jsx", + { + trimWhitespace: true, + trimWhitespaceExceptions: ["Md", "CodeSnippet", "CodeSplit"], + }, + ], + ["@babel/transform-react-jsx", { runtime: "automatic" }], + "@babel/proposal-function-bind", + [transformImports, { useSrc: true }], + //isProduction && false && ["transform-cx-imports", { useSrc: true }], + ].filter(Boolean), + }; }; - - diff --git a/docs/components/Md.js b/docs/components/Md.js index 50d18e1ea..56d48cd01 100644 --- a/docs/components/Md.js +++ b/docs/components/Md.js @@ -66,6 +66,7 @@ export class Md extends HtmlElement { return super.add({ type: HtmlElement, innerHtml: md, + className: "cxe-md-block", }); } } diff --git a/docs/components/Md.scss b/docs/components/Md.scss new file mode 100644 index 000000000..5edaf82ec --- /dev/null +++ b/docs/components/Md.scss @@ -0,0 +1,26 @@ +.cxe-md-block > table { + max-width: 50em; + border-collapse: collapse; + margin: 1em 0; + + th, + td { + border: 1px solid #ddd; + padding: 8px 12px; + text-align: left; + font-size: 13px; + } + + th { + background: #f5f5f5; + font-weight: 600; + } + + tr:nth-child(even) { + background: #fafafa; + } + + code { + background: rgba(128, 128, 128, 0.15); + } +} diff --git a/docs/components/index.scss b/docs/components/index.scss index b1f09112f..6b0a78afb 100644 --- a/docs/components/index.scss +++ b/docs/components/index.scss @@ -8,6 +8,7 @@ @import "EditOnGitX"; @import "Floater"; @import "DocSearch"; +@import "Md"; .dxb-table { max-width: 50em; diff --git a/docs/content/Article.scss b/docs/content/Article.scss index 3baf17d3c..150c62dbc 100644 --- a/docs/content/Article.scss +++ b/docs/content/Article.scss @@ -38,6 +38,10 @@ color: #414340; font-size: 24px; scroll-margin-top: 200px; + + & ~ p { + margin-top:24px; + } } h2 { diff --git a/docs/content/charts/configs/axis.js b/docs/content/charts/configs/axis.js index 551af6577..4bc83c2c7 100644 --- a/docs/content/charts/configs/axis.js +++ b/docs/content/charts/configs/axis.js @@ -199,7 +199,7 @@ export default { onCreateLabelFormatter: { type: "function", description: - A function used to create a formatter function for axis labels. See See [Complex Labels](~/examples/charts/axis/complex-labels) example for more info. + A function used to create a formatter function for axis labels. See [Complex Labels](~/examples/charts/axis/complex-labels) example for more info. } }; diff --git a/docs/content/charts/configs/legendary.js b/docs/content/charts/configs/legendary.js index a57131348..9e2ef3361 100644 --- a/docs/content/charts/configs/legendary.js +++ b/docs/content/charts/configs/legendary.js @@ -1,5 +1,4 @@ import {Md} from 'docs/components/Md'; -import {ShapeRender} from "cx/src/charts"; export default { active: { diff --git a/docs/content/concepts/CreatingComponents.js b/docs/content/concepts/CreatingComponents.js index 305636ec5..ae7a6defc 100644 --- a/docs/content/concepts/CreatingComponents.js +++ b/docs/content/concepts/CreatingComponents.js @@ -1,10 +1,9 @@ import { Md } from "../../components/Md"; -import { Content, FlexCol, Slider, Tab } from "cx/widgets"; +import { Content, FlexCol, Slider, Tab, Widget } from "cx/widgets"; import { CodeSplit } from "../../components/CodeSplit"; import { CodeSnippet } from "../../components/CodeSnippet"; -import { Widget } from "../../../packages/cx/src/ui/Widget"; -class Square extends Widget { +class Square extends Widget{ declareData(...args) { super.declareData( { diff --git a/docs/content/intro/BreakingChanges.js b/docs/content/intro/BreakingChanges.js index ff9e14e12..aaaf9ac12 100644 --- a/docs/content/intro/BreakingChanges.js +++ b/docs/content/intro/BreakingChanges.js @@ -13,6 +13,61 @@ export const BreakingChanges = This page will provide information about breaking changes and how to migrate your applications to the latest versions of the framework. + ## 26.1.0 + + ### TypeScript Migration + + The CxJS framework has been fully migrated to TypeScript. This is a major change that brings + improved type safety, better IDE support, and enhanced developer experience. + + ### Separation from React JSX Types + + CxJS now provides its own JSX type definitions instead of relying on React's JSX types. + This separation was necessary because CxJS JSX has fundamental differences from React JSX. + + To use the new JSX types, update your `tsconfig.json`: + + + {` +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "cx" + } +} + `} + + + With this configuration, TypeScript will use CxJS-specific JSX types, providing proper + type checking for CxJS attributes and components. + + ### Package Upgrades Required + + The following packages have been updated and should be upgraded to version 26.x: + + - `cx` - Core framework package + - `cx-react` - React adapter (now written in TypeScript) + - `babel-plugin-transform-cx-imports` - Babel plugin for optimizing imports + - `swc-plugin-transform-cx-jsx` - SWC plugin for CxJS JSX transformation + - `swc-plugin-transform-cx-imports` - SWC plugin for optimizing imports + - `cx-scss-manifest-webpack-plugin` - Webpack plugin for SCSS manifest generation + + > **Note:** Some projects have copied `cx-scss-manifest-webpack-plugin` and used it in source form. + These projects should now transition to using the official npm package instead. These versions will not work + with the 26.x releases due to internal path changes and CSS will appear broken. Alternatively, the plugin can be patched to use the `build` + folder instead of the `src` folder to detect which components are actually being used in the project. + + ### React 18+ Required + + CxJS 26.x requires React 18 or later. The framework now uses the modern React 18 APIs including + `createRoot` from `react-dom/client`. If your application is still using React 17 + or earlier, you will need to upgrade React before upgrading to CxJS 26.x. + + ### Migration Guide + + For a comprehensive guide on migrating your applications to TypeScript and taking advantage of the new + type system, please refer to the [TypeScript Migration Guide](~/intro/type-script-migration). + ## 24.10.0 ### Legend and LegendEntry rendering diff --git a/docs/content/intro/TypeScriptMigration.js b/docs/content/intro/TypeScriptMigration.js new file mode 100644 index 000000000..5ded4fab4 --- /dev/null +++ b/docs/content/intro/TypeScriptMigration.js @@ -0,0 +1,604 @@ +import { CodeSnippet } from "../../components/CodeSnippet"; +import { CodeSplit } from "../../components/CodeSplit"; +import { Md } from "../../components/Md"; + +export const TypeScriptMigration = ( + + + # TypeScript Migration Guide + + Starting with CxJS 26.x, the core framework has been migrated to TypeScript. This guide covers the patterns + and best practices for working with TypeScript in CxJS applications, whether you're migrating an existing + project or starting fresh. + + ## Project Setup + + ### TypeScript Configuration + + Configure your `tsconfig.json` with the following settings for optimal CxJS support: + + + {` +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "cx", + "moduleResolution": "bundler", + "esModuleInterop": true + } +} + `} + + + The key setting is `"jsxImportSource": "cx"`. CxJS now provides its own JSX type definitions + instead of relying on React's JSX types. This means CxJS-specific attributes like `visible`, + `controller`, `layout`, and data-binding functions (`bind()`, `expr()`, `tpl()`) are + properly typed without conflicts with React's typings. + + ### Webpack Configuration + + With TypeScript, you technically no longer need `babel-loader` or special Babel plugins for CxJS. + You can use just `ts-loader` to handle TypeScript files: + + + {` +{ + test: /\\.(ts|tsx)$/, + loader: 'ts-loader', + exclude: /node_modules/ +} + `} + + + However, removing `babel-loader` and the `transform-cx-jsx` plugin requires significant + refactoring of your application, as described in the next section. For most existing projects, + we recommend keeping the Babel plugins during the migration process. + + ### Without `transform-cx-jsx` Plugin + + Applications can continue to work with the `transform-cx-jsx` plugin enabled, which is the + recommended approach for existing projects. However, if you want to run your application + without this plugin, the following requirements apply: + + 1. All functional components must be wrapped in `createFunctionalComponent` calls + 2. The special JSX prop syntax (`-bind`, `-expr`, `-tpl`) must be converted to function calls + (`bind()`, `tpl()`, `expr()`) or object form like `{{ bind: "prop" }}`, + `{{ tpl: "template" }}`, or `{{ expr: "1+1" }}` + 3. All components previously developed in JavaScript must be ported to TypeScript. + + ### Bundle Size Optimization (Optional) + + While not required, you can use `babel-plugin-transform-cx-imports` to minimize bundle size + by transforming CxJS imports to more specific paths: + + + {` +// Install the plugin +npm install babel-plugin-transform-cx-imports --save-dev + +// In babel.config.js +{ + plugins: [ + ["transform-cx-imports", { useSrc: true }] + ] +} + `} + + + If using this plugin, chain `babel-loader` after `ts-loader`: + + + {` +{ + test: /\\.(ts|tsx)$/, + exclude: /node_modules/, + use: ['babel-loader', 'ts-loader'] +} + `} + + + ### Vite + + Besides Webpack, CxJS now also supports Vite as a build tool. Vite offers faster development server + startup and hot module replacement. For a ready-to-use Vite template, check out the + [CxJS Vite Template](https://github.com/codaxy/cxjs-vite-template). + + ## General Improvements + + ### Typed Controller Methods + + With TypeScript, you can use `getControllerByType` to get a typed controller reference instead of + using string method names. This provides compile-time safety and IDE autocomplete. + + + {` +import { Controller, bind } from "cx/ui"; +import { Button, Section } from "cx/widgets"; + +class PageController extends Controller { + onSave() { + // save logic + } + + onDelete(id: string) { + // delete logic + } +} + +export default ( + +
+ {/* Type-safe controller method calls */} + + +
+
+); + `}
+
+ + The `getControllerByType(ControllerClass)` method searches up the widget tree and returns a + typed controller instance, enabling full autocomplete and compile-time type checking for + controller methods and their parameters. + + ### Typed RenderingContext + + CxJS uses a `RenderingContext` object to pass information down the widget tree during rendering. + Different widget families define typed context interfaces that extend `RenderingContext` for + type-safe access to context properties. + + **Available typed contexts:** + + - `FormRenderingContext` - Form validation context (`parentDisabled`, `parentReadOnly`, `validation`, etc.) + - `SvgRenderingContext` - SVG layout context (`parentRect`, `inSvg`, `addClipRect`) + - `ChartRenderingContext` - Chart context extending SVG (`axes`) + + When creating custom widgets that consume these context properties, import and use the typed + context interface in your method signatures: + + + {` +import type { FormRenderingContext } from "cx/widgets"; + +export class MyFormWidget extends Field { + explore(context: FormRenderingContext, instance: Instance) { + // Type-safe access to form context properties + if (context.parentDisabled) { + // handle disabled state + } + super.explore(context, instance); + } +} + `} + + + ### Typed ContentResolver + + The `ContentResolver` widget now supports type inference for the `onResolve` callback params. + TypeScript automatically infers the resolved types from your params definition: + + + {` +import { ContentResolver } from "cx/widgets"; +import { createAccessorModelProxy } from "cx/data"; + +interface AppModel { + user: { name: string; age: number }; +} + +const model = createAccessorModelProxy(); + + + age: model.user.age, // AccessorChain + limit: 10, // number literal + }} + onResolve={(params) => { + // TypeScript infers: + // params.name: string + // params.age: number + // params.limit: number + return
{params.name} is {params.age} years old
; + }} +/> + `}
+
+ + **Type resolution behavior:** + + | Param Type | Resolved Type | + |------------|---------------| + | Literal values (`42`, `"text"`) | Preserves type (`number`, `string`) | + | `AccessorChain<T>` | `T` | + | `Selector<T>` / `GetSet<T>` | `T` | + | `bind()` / `tpl()` / `expr()` | `any` (runtime-only) | + + The utility types `ResolveProp<P>` and `ResolveStructuredProp<S>` are exported from `cx/ui` + if you need to use them in your own generic components. + + ### Expression Helpers + + CxJS provides a set of helper functions for creating type-safe selectors from accessor chains. + These are useful for boolean props like `visible`, `disabled`, or `readOnly`: + + + {` +import { createAccessorModelProxy } from "cx/data"; +import { truthy, isEmpty, equal, greaterThan } from "cx/ui"; +import { TextField, NumberField, Button } from "cx/widgets"; + +interface FormModel { + name: string; + age: number; + items: string[]; +} + +const m = createAccessorModelProxy(); + + + + + + {/* Show warning if name is empty */} +
+ Name is required +
+ + {/* Enable button only if age >= 18 */} + + + {/* Show special message for specific age */} +
+ Welcome to adulthood! +
+
+ `}
+
+ + **Available helpers:** + + | Helper | Description | + |--------|-------------| + | `truthy(accessor)` | True if value is truthy (`!!x`) | + | `falsy(accessor)` | True if value is falsy (`!x`) | + | `isTrue(accessor)` | True if value is strictly `true` | + | `isFalse(accessor)` | True if value is strictly `false` | + | `hasValue(accessor)` | True if value is not null/undefined | + | `isEmpty(accessor)` | True if string/array is empty or null | + | `isNonEmpty(accessor)` | True if string/array has content | + | `equal(accessor, value)` | True if `x == value` (loose) | + | `notEqual(accessor, value)` | True if `x != value` (loose) | + | `strictEqual(accessor, value)` | True if `x === value` | + | `strictNotEqual(accessor, value)` | True if `x !== value` | + | `lessThan(accessor, value)` | True if `x < value` | + | `lessThanOrEqual(accessor, value)` | True if `x <= value` | + | `greaterThan(accessor, value)` | True if `x > value` | + | `greaterThanOrEqual(accessor, value)` | True if `x >= value` | + + These helpers return `Selector<boolean>` which can be used anywhere a boolean binding is expected. + + ### Typed Config Properties + + Several widget config properties now have improved type definitions that provide better + autocomplete and type checking when using the `type` or `$type` pattern. + + #### Selection + + Grid, PieChart, and BubbleGraph support typed `selection` configs: + + + {` +import { Grid } from "cx/widgets"; +import { KeySelection } from "cx/ui"; + + + `} + + + Supported selection types: `Selection`, `KeySelection`, `PropertySelection`, `SimpleSelection`. + + #### Chart Axes + + Chart axes support typed configs for different axis types: + + + {` +import { Chart } from "cx/charts"; +import { NumericAxis, CategoryAxis } from "cx/charts"; + + + {/* chart content */} + + `} + + + Supported axis types: `Axis`, `NumericAxis`, `CategoryAxis`, `TimeAxis`. + + #### Data Adapters + + Grid and List support typed `dataAdapter` configs: + + + {` +import { Grid } from "cx/widgets"; +import { GroupAdapter } from "cx/ui"; + + + `} + + + Supported adapter types: `ArrayAdapter`, `GroupAdapter`, `TreeAdapter`. + + #### Dropdown Options + + Form fields with dropdowns (ColorField, DateTimeField, MonthField, LookupField) accept + typed `dropdownOptions`: + + + {` +import { DateTimeField } from "cx/widgets"; + + + `} + + + #### Typed Controllers + + The `controller` property accepts multiple forms: a class, a config object with `type`/`$type`, + an inline config, or a factory function. Because this type is intentionally flexible ("open"), + TypeScript's generic inference may not catch extra or misspelled properties in config objects. + + Use the `validateConfig` helper to enable strict property checking: + + + {` +import { validateConfig } from "cx/util"; +import { Controller } from "cx/ui"; + +interface MyControllerConfig { + apiEndpoint: string; + maxRetries: number; +} + +class MyController extends Controller { + declare apiEndpoint: string; + declare maxRetries: number; + + constructor(config?: MyControllerConfig) { + super(config); + } +} + +// validateConfig enables strict checking +
+ `} + + + The `validateConfig` function is a compile-time helper that returns its input unchanged at runtime. + It can be used with any config object that follows the `{ type: Class, ...props }` pattern. + + ## Authoring Widgets + + Previously, CxJS widgets had to be written in JavaScript with optional + TypeScript declaration files (`.d.ts`) for typing. With CxJS 26.x, you can now author + widgets entirely in TypeScript. + + > **Important:** Widget files must use the `/** @jsxImportSource react */` pragma because + the widget's `render` method uses React JSX. + + ### Complete Widget Example + + Here's a complete example showing all the steps to create a CxJS widget in TypeScript: + + + {` +/** @jsxImportSource react */ + +import { BooleanProp, StringProp, RenderingContext, VDOM } from "cx/ui"; +import { HtmlElement, HtmlElementConfig } from "cx/widgets"; + +// 1. Define the Config interface +export interface MyButtonConfig extends HtmlElementConfig { + icon?: StringProp; + pressed?: BooleanProp; +} + +// 2. Extend the appropriate generic base class (Instance type argument is optional) +export class MyButton extends HtmlElement { + + // 3. Use declare for all properties from config/prototype + declare icon?: string; + declare pressed?: boolean; + declare baseClass: string; + + // 4. Declare bindable props in declareData + declareData(...args) { + super.declareData(...args, { + icon: undefined, + pressed: undefined, + }); + } + + // 5. Add constructor accepting the config type + constructor(config?: MyButtonConfig) { + super(config); + } + + // 6. Implement render method with React JSX + render( + context: RenderingContext, + instance: Instance, + key: string + ): React.ReactNode { + return ( + + ); + } +} + +// 7. Initialize prototype properties +MyButton.prototype.baseClass = "mybutton"; + `} + + + ### Key Steps + + 1. **Add React JSX pragma** - Use `/** @jsxImportSource react */` at the top of widget files + 2. **Define Config interface** - Name it `[WidgetName]Config` and extend the parent's config + 3. **Extend generic base class** - Use `HtmlElement<Config>`, `ContainerBase<Config>`, etc. + 4. **Use `declare` for properties** - Prevents TypeScript from overwriting config/prototype values + 5. **Declare bindable props in `declareData`** - Register props that support data binding + 6. **Add typed constructor** - Accepts the config type for proper type inference + 7. **Implement render method** - Returns React JSX elements + + ### Config Property Types + + Use these types for bindable properties in your Config interface: + + | Type | Usage | + |------|-------| + | `StringProp` | Bindable string property | + | `BooleanProp` | Bindable boolean property | + | `NumberProp` | Bindable number property | + | `Prop<T>` | Bindable property of custom type T | + | `RecordsProp` | Array data (Grid, List) | + + ### Using `declare` for Properties + + > **Important:** Widget properties must use `declare` to avoid being overwritten. Without `declare`, + TypeScript class fields will override values passed through the config (via `Object.assign` in the + constructor) or values defined on the prototype. + + + {` +// WRONG - these fields will override config values with undefined +export class MyWidget extends HtmlElement { + icon?: string; // Overwrites config.icon! + pressed?: boolean; // Overwrites config.pressed! +} + +// CORRECT - declare tells TypeScript the field exists without initializing it +export class MyWidget extends HtmlElement { + declare icon?: string; + declare pressed?: boolean; + declare baseClass: string; // Non-nullable when defined in prototype +} + `} + + + ### Base Classes + + CxJS provides generic base classes for creating typed widgets. The second type argument (Instance) is optional: + + | Base Class | Use Case | + |------------|----------| + | `HtmlElement<Config>` | Widgets rendering HTML elements | + | `ContainerBase<Config>` | Widgets containing other widgets | + | `PureContainerBase<Config>` | Containers without HTML wrapper | + | `Field<Config>` | Form input widgets | + + ### Custom Instance Types + + When a widget needs custom properties on its instance, create a custom instance interface: + + + {` +export interface MyWidgetInstance extends Instance { + customData: SomeType; +} + +export class MyWidget extends HtmlElement { + initInstance(context: RenderingContext, instance: MyWidgetInstance): void { + instance.customData = initializeSomething(); + super.initInstance(context, instance); + } +} + `} + + + ### Migration Checklist + + When migrating a widget from JavaScript to TypeScript: + + 1. Add JSX pragma `/** @jsxImportSource react */` if file contains JSX + 2. Create `[WidgetName]Config` interface extending appropriate parent + 3. Add generic type parameters to base class if needed + 4. Add constructor accepting the config type + 5. Add `declare` statements for all class properties + 6. Add type annotations to all methods + 7. Create custom instance interface if needed + 8. Fix prototype initializations (use `undefined` not `null` where needed) + 9. Declare `baseClass` as non-nullable if defined in prototype + 10. Delete the corresponding `.d.ts` file + + ### File Organization + + After migration, each widget should have: + + - `Widget.tsx` - The implementation with inline types + - No separate `Widget.d.ts` - Types are in the source file + + Index files (`index.ts`) should re-export all public types: + + + {` +export { Button, ButtonConfig } from "./Button"; +export { FlexBox, FlexBoxConfig } from "./FlexBox"; + `} + + + +); diff --git a/docs/content/intro/index.js b/docs/content/intro/index.js index aa9b0d252..11d0326e1 100644 --- a/docs/content/intro/index.js +++ b/docs/content/intro/index.js @@ -7,6 +7,7 @@ export * from './StepByStep'; export * from './Jsx'; export * from './BreakingChanges'; export * from './NpmPackages'; +export * from './TypeScriptMigration'; import { bumpVersion } from '../version'; diff --git a/docs/index.html b/docs/index.html index c53431cc4..963bf9ef9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -16,8 +16,6 @@ type="text/css" /> - - <%= htmlWebpackPlugin.options.reactScripts %> <%= htmlWebpackPlugin.options.gtmb %> diff --git a/docs/index.js b/docs/index.js index f6a46babf..fcd1115d5 100644 --- a/docs/index.js +++ b/docs/index.js @@ -1,35 +1,32 @@ -import { startHotAppLoop, Url, History, enableCultureSensitiveFormatting } from 'cx/ui'; -import { Timing, Debug } from 'cx/util'; -import { Widget } from 'cx/ui'; -import { enableTooltips } from 'cx/widgets'; -import {Main} from './app/Main'; -import {store} from './app/store'; -import './app/icons'; +import { enableCultureSensitiveFormatting, History, startHotAppLoop, Url } from "cx/ui"; +import { Debug, Timing } from "cx/util"; +import { enableTooltips } from "cx/widgets"; +import "./app/icons"; +import { Main } from "./app/Main"; +import { store } from "./app/store"; import "./index.scss"; +import "cx-theme-aquamarine"; enableTooltips(); enableCultureSensitiveFormatting(); -let stop, start = () => { +let stop, + start = () => { + Url.setBaseFromScript("~/app*.js"); + History.connect(store, "url", "hash"); + Timing.enable("app-loop"); + //Timing.enable('vdom-render'); + Debug.enable("app-data"); + //Widget.lazyInit = false; + //Widget.optimizePrepare = false; + //Debug.enable('process-data'); + //Debug.enable('should-update'); + stop = startHotAppLoop(module, document.getElementById("app"), store, Main); + }; - Url.setBaseFromScript('~/app*.js'); - History.connect(store, 'url', "hash"); - Timing.enable('app-loop'); - //Timing.enable('vdom-render'); - Debug.enable('app-data'); - //Widget.lazyInit = false; - //Widget.optimizePrepare = false; - //Debug.enable('process-data'); - //Debug.enable('should-update'); - stop = startHotAppLoop(module, document.getElementById('app'), store, Main); -}; - -if (Object.assign && window.fetch && window.WeakMap && window.Intl) - start(); +if (Object.assign && window.fetch && window.WeakMap && window.Intl) start(); else { - import(/* webpackChunkName: "polyfill" */ './polyfill') - .then(start) - .catch(error => { - console.error(error); - }); -} \ No newline at end of file + import(/* webpackChunkName: "polyfill" */ "./polyfill").then(start).catch((error) => { + console.error(error); + }); +} diff --git a/docs/package.json b/docs/package.json index a685ca3f1..8a99fe280 100644 --- a/docs/package.json +++ b/docs/package.json @@ -9,48 +9,48 @@ }, "dependencies": { "casual": "^1.6.2", - "core-js": "^3.39.0", - "cx": "^25.4.1", - "cx-react": "^24.7.1", + "core-js": "^3.47.0", + "cx": "workspace:*", + "cx-react": "workspace:*", + "cx-theme-aquamarine": "workspace:*", "illuminate-js": "^1.0.0-alpha.2", "intl": "^1.2.5", "marked": "^4.3.0", - "react": "^18.3.1", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@types/node": "^22.10.1", - "@types/react": "^18.3.14", - "babel-loader": "^9.2.1", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0", - "babel-preset-cx-env": "^24.0.0", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "babel-loader": "^10.0.0", + "babel-plugin-transform-cx-imports": "workspace:*", + "babel-plugin-transform-cx-jsx": "workspace:*", + "babel-preset-cx-env": "workspace:*", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.5", "if-loader": "^1.0.2", "inline-manifest-webpack-plugin": "^4.0.2", "json-loader": "^0.5.7", "mini-css-extract-plugin": "^2.9.2", "modify-babel-preset": "^3.2.1", "sass": "^1.77.8", - "sass-loader": "^16.0.1", + "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "svg-url-loader": "^8.0.0", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cleanup-plugin": "^0.5.1", "webpack-cli": "^5.1.4", "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" } } diff --git a/docs/webpack.config.js b/docs/webpack.config.js index ce2cd46a2..174bf0739 100644 --- a/docs/webpack.config.js +++ b/docs/webpack.config.js @@ -23,7 +23,19 @@ if (production) { rules: [ { test: /\.scss$/, - use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], + use: [ + MiniCssExtractPlugin.loader, + "css-loader", + { + loader: "sass-loader", + options: { + sassOptions: { + quietDeps: true, + silenceDeprecations: ["import", "global-builtin"], + }, + }, + }, + ], }, { test: /\.css$/, @@ -82,7 +94,19 @@ if (production) { rules: [ { test: /\.scss$/, - use: ["style-loader", "css-loader", "sass-loader"], + use: [ + "style-loader", + "css-loader", + { + loader: "sass-loader", + options: { + sassOptions: { + quietDeps: true, + silenceDeprecations: ["import", "global-builtin"], + }, + }, + }, + ], }, { test: /\.css$/, @@ -123,24 +147,13 @@ if (production) { var common = { resolve: { - alias: { - "cx/src": path.resolve(path.join(__dirname, "../packages/cx/src")), - "cx/locale": path.resolve(path.join(__dirname, "../packages/cx/locale")), - cx: path.resolve(path.join(__dirname, "../packages/cx/src")), - "cx-react": path.resolve(path.join(__dirname, "../packages/cx-react")), - //'cx-react': path.resolve(path.join(__dirname, '../packages/cx-inferno')), - //'cx-react': path.resolve(path.join(__dirname, '../packages/cx-preact')), - "cx-react-css-transition-group": path.resolve( - path.join(__dirname, "../packages/cx-react-css-transition-group"), - ), - docs: __dirname, - }, + extensions: [".js", ".ts", ".tsx"], }, module: { rules: [ { - test: /\.js$/, + test: /\.(js|ts|tsx)$/, include: /[\\\/](misc|docs|cx|cx-react)[\\\/]/, //exclude: /(babelHelpers)/, use: [ @@ -169,10 +182,10 @@ var common = { path: __dirname, filename: "[name].js", }, - externals: { - react: "React", - "react-dom": "ReactDOM", - }, + // externals: { + // react: "React", + // "react-dom": "ReactDOM", + // }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, "index.html"), @@ -198,7 +211,7 @@ var common = { buildDependencies: { // 2. Add your config as buildDependency to get cache invalidation on config change - config: [__filename], + config: [__filename, path.join(__dirname, "./babel-config.js")], // 3. If you have other things the build depends on you can add them here // Note that webpack, loaders and all modules referenced from your config are automatically added diff --git a/fiddle/package.json b/fiddle/package.json index 59bd20789..ac190988f 100644 --- a/fiddle/package.json +++ b/fiddle/package.json @@ -7,7 +7,8 @@ "scripts": { "build": "webpack", "build:root": "webpack", - "start": "webpack-dev-server --open" + "start": "webpack-dev-server --open", + "test": "mocha --require @babel/register app/**/*.spec.js" }, "repository": { "type": "git", @@ -25,57 +26,58 @@ }, "homepage": "https://gitlab.com/mstijak/cx-fiddle#README", "dependencies": { - "@babel/runtime": "^7.26.0", + "@babel/runtime": "^7.28.4", "assert": "^2.1.0", "buffer": "^6.0.3", "casual": "^1.6.2", "codemirror": "^5.65.18", - "core-js": "^3.39.0", - "cx": "^25.4.1", - "cx-react": "^24.7.1", + "core-js": "^3.47.0", + "cx": "workspace:*", + "cx-react": "workspace:*", "deep-equal": "^2.2.3", "deepmerge": "^4.3.1", "path-browserify": "^1.0.1", "prettier": "^3.3.3", "process": "^0.11.10", "query-string": "^9.1.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@babel/register": "^7.25.9", - "@babel/runtime-corejs2": "^7.26.0", - "@babel/runtime-corejs3": "^7.26.0", - "@types/node": "^22.10.1", - "@types/react": "^18.3.14", - "babel-loader": "^9.2.1", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@babel/register": "^7.28.3", + "@babel/runtime-corejs2": "^7.28.4", + "@babel/runtime-corejs3": "^7.28.4", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "babel-loader": "^10.0.0", "babel-plugin-transform-cx-imports": "^21.3.0", "babel-plugin-transform-cx-jsx": "^21.3.0", "babel-preset-cx-env": "^24.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.5", "if-loader": "^1.0.2", "inline-manifest-webpack-plugin": "^4.0.2", "json-loader": "^0.5.7", "mini-css-extract-plugin": "^2.9.2", + "mocha": "^10.8.2", "modify-babel-preset": "^3.2.1", "sass": "^1.77.8", - "sass-loader": "^16.0.1", + "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "svg-url-loader": "^8.0.0", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cleanup-plugin": "^0.5.1", "webpack-cli": "^5.1.4", "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.2", "webpack-md5-hash": "^0.0.6", "webpack-merge": "^6.0.1" } diff --git a/gallery/components/ThemeLoader.tsx b/gallery/components/ThemeLoader.tsx index a968e0851..35a95c634 100644 --- a/gallery/components/ThemeLoader.tsx +++ b/gallery/components/ThemeLoader.tsx @@ -1,7 +1,6 @@ -import { cx, ContentResolver } from "cx/widgets"; -import { loadTheme } from "../themes"; import { createFunctionalComponent } from "cx/ui"; -import { StringProp } from "cx/src/core"; +import { ContentResolver } from "cx/widgets"; +import { loadTheme } from "../themes"; function importTheme(theme) { switch (theme) { diff --git a/gallery/config/babel-config.js b/gallery/config/babel-config.js index 6527b0784..26714d70a 100644 --- a/gallery/config/babel-config.js +++ b/gallery/config/babel-config.js @@ -19,7 +19,7 @@ module.exports = { useBuiltIns: "usage", cx: { imports: { - useSrc: true, + //useSrc: true, }, }, }, diff --git a/gallery/config/webpack.config.js b/gallery/config/webpack.config.js index 4f08523de..ea52953f2 100644 --- a/gallery/config/webpack.config.js +++ b/gallery/config/webpack.config.js @@ -1,7 +1,5 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"), - merge = require("webpack-merge"), path = require("path"), - babelCfg = require("./babel-config"), p = (p) => path.join(__dirname, "../", p || ""), gtm = require("../../misc/tracking/gtm.js"), reactScripts = require("../../misc/reactScripts"), @@ -10,43 +8,43 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"), module.exports = (production) => ({ resolve: { alias: { - app: p("."), - "cx/src": p("../packages/cx/src"), - cx: p("../packages/cx"), - "cx-react": p("../packages/cx-react"), - "cx-theme-material": p("../packages/cx-theme-material"), - "cx-theme-frost": p("../packages/cx-theme-frost"), - "cx-theme-dark": p("../packages/cx-theme-dark"), - "cx-theme-aquamarine": p("../packages/cx-theme-aquamarine"), - "cx-theme-material-dark": p("../packages/cx-theme-material-dark"), - "cx-theme-space-blue": p("../packages/cx-theme-space-blue"), - "cx-theme-packed-dark": p("../packages/cx-theme-packed-dark"), + //app: p("."), + //"cx/src": p("../packages/cx/src"), + //cx: p("../packages/cx"), + //"cx-react": p("../packages/cx-react"), + // "cx-theme-material": p("../packages/cx-theme-material"), + // "cx-theme-frost": p("../packages/cx-theme-frost"), + // "cx-theme-dark": p("../packages/cx-theme-dark"), + // "cx-theme-aquamarine": p("../packages/cx-theme-aquamarine"), + // "cx-theme-material-dark": p("../packages/cx-theme-material-dark"), + // "cx-theme-space-blue": p("../packages/cx-theme-space-blue"), + // "cx-theme-packed-dark": p("../packages/cx-theme-packed-dark"), //uncomment the line below to alias cx-react to cx-preact or some other React replacement library //'cx-react': 'cx-preact', }, - extensions: [".js", ".ts", ".tsx", ".json"], + extensions: [".ts", ".js", ".tsx", ".json"], }, module: { rules: [ { - test: /\.tsx?$/, - include: /gallery/, + test: /\.(ts|tsx)?$/, + //include: /gallery/, use: [ - { - loader: "babel-loader", - options: babelCfg, - }, + // { + // loader: "babel-loader", + // options: babelCfg, + // }, "ts-loader", ], }, - { - test: /\.js$/, - //add here any ES6 based library - include: /[\\\/](cx|cx-react|gallery|misc|cx-theme-.*)[\\\/]/, - loader: "babel-loader", - options: babelCfg, - }, + // { + // test: /\.js$/, + // //add here any ES6 based library + // include: /[\\\/](cx|cx-react|gallery|misc|cx-theme-.*)[\\\/]/, + // loader: "babel-loader", + // options: babelCfg, + // }, { test: /\.(png|jpg|svg)/, loader: "file-loader", @@ -73,6 +71,10 @@ module.exports = (production) => ({ loader: "sass-loader", options: { sourceMap: !production, + sassOptions: { + quietDeps: true, + silenceDeprecations: ["import", "global-builtin", "color-functions", "slash-div"], + }, }, }, ], @@ -85,16 +87,15 @@ module.exports = (production) => ({ }, entry: { //vendor: ['cx-react', p('polyfill.js')], - app: [p("../misc/babelHelpers"), p("index")], + app: [p("index.tsx")], }, output: { - path: p("dist"), filename: "[name].js", }, - externals: { - react: "React", - "react-dom": "ReactDOM", - }, + // externals: { + // react: "React", + // "react-dom": "ReactDOM", + // }, optimization: { runtimeChunk: "single", @@ -108,18 +109,6 @@ module.exports = (production) => ({ reactScripts: production ? reactScripts : reactScriptsDev, favicon: p("assets/favicon.png"), }), - //new InlineManifestWebpackPlugin(), - // new ScriptExtHtmlWebpackPlugin({ - // async: /\.js$/, - // preload: { - // test: /(aquamarine)/, - // chunks: "async" - // }, - // prefetch: { - // test: /\.js$/, - // chunks: "async" - // } - // }) ], cache: { diff --git a/gallery/config/webpack.dev.js b/gallery/config/webpack.dev.js index ef3448d1a..e5c5ccd04 100644 --- a/gallery/config/webpack.dev.js +++ b/gallery/config/webpack.dev.js @@ -4,17 +4,18 @@ var webpack = require("webpack"), path = require("path"); var specific = { - module: { - rules: [], - }, + // module: { + // rules: [], + // }, mode: "development", - optimization: { moduleIds: "named" }, + //optimization: { moduleIds: "named" }, output: { publicPath: "/", + path: path.join(__dirname, ".."), //required for unknown reasons }, - devtool: "eval", + //devtool: "eval", devServer: { - //contentBase: path.join(__dirname, ".."), + //static: path.join(__dirname, ".."), hot: true, port: 8088, historyApiFallback: true, diff --git a/gallery/examples/baseline/index.tsx b/gallery/examples/baseline/index.tsx index bf6b278e7..77ebb0e2b 100644 --- a/gallery/examples/baseline/index.tsx +++ b/gallery/examples/baseline/index.tsx @@ -1,20 +1,24 @@ -import {cx, Section, FlexRow, Button, TextField, Menu, Submenu, Checkbox, Switch} from 'cx/widgets'; -import {bind} from 'cx/ui'; +import { bind } from "cx/ui"; +import { Button, Checkbox, Menu, Section, Submenu, Switch, TextField } from "cx/widgets"; -export default - Source Code -
-

- All widgets respect the baseline. -

+export default ( + + + Source Code + +
+

All widgets respect the baseline.

-
- +
+   - +   -   @@ -23,21 +27,22 @@ export default Span of text   - - Dropdown - - Item1 - Item2 - - + + Dropdown + + Item1 + Item2 + +   Check   - -
-
-
+ +
+ + +); -import {hmr} from '../../routes/hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../routes/hmr.js"; +hmr(module); diff --git a/gallery/global.d.ts b/gallery/global.d.ts deleted file mode 100644 index c297fa82a..000000000 --- a/gallery/global.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare class System { - static import(path: string): Promise -} - -declare let module: any; \ No newline at end of file diff --git a/gallery/index.html b/gallery/index.html index 33b2ad2e1..beb0705fe 100644 --- a/gallery/index.html +++ b/gallery/index.html @@ -8,7 +8,7 @@ href="https://fonts.googleapis.com/css?family=Roboto:400,500|Open+Sans:400,500,600,700|Material+Icons|Inter" rel="stylesheet" /> - <%= htmlWebpackPlugin.options.reactScripts %> <%= htmlWebpackPlugin.options.gtmh %> + <%= htmlWebpackPlugin.options.gtmh %> <%= htmlWebpackPlugin.options.gtmb %> diff --git a/gallery/index.ts b/gallery/index.ts deleted file mode 100644 index 9c4c96ffc..000000000 --- a/gallery/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Store } from 'cx/data'; -import { Url, History, startHotAppLoop, enableCultureSensitiveFormatting } from 'cx/ui'; -import { Timing, Debug } from 'cx/util'; -import {enableTooltips} from 'cx/widgets'; -//css -import "./style"; -import Routes from './routes'; -import {registerStore} from './routes/hmr'; - -enableTooltips(); -enableCultureSensitiveFormatting(); - - -function start() { - - //store - const store = new Store(); - - //routing - Url.setBaseFromScript('app*.js'); - History.connect(store, 'url'); - - //debug - Timing.enable('app-loop'); - Debug.enable('app-data'); - - registerStore(store); - - //app loop - let stop = startHotAppLoop(module, document.getElementById('app'), store, Routes); -} - -if (typeof window["fetch"] === "undefined" || typeof window["Intl"] === "undefined") { - import("./polyfill") - .then(start); -} else { - start(); -} - diff --git a/gallery/index.tsx b/gallery/index.tsx new file mode 100644 index 000000000..399a544a5 --- /dev/null +++ b/gallery/index.tsx @@ -0,0 +1,31 @@ +import { Store } from "cx/data"; +import { Url, History, startHotAppLoop, enableCultureSensitiveFormatting } from "cx/ui"; +import { Timing, Debug } from "cx/util"; +import { enableTooltips } from "cx/widgets"; +//css +import "./style"; +import Routes from "./routes"; +import { registerStore } from "./routes/hmr"; + +enableTooltips(); +enableCultureSensitiveFormatting(); + +function start() { + //store + const store = new Store(); + + //routing + Url.setBaseFromScript("app*.js"); + History.connect(store, "url"); + + //debug + Timing.enable("app-loop"); + Debug.enable("app-data"); + + registerStore(store); + + //app loop + let stop = startHotAppLoop(module, document.getElementById("app"), store, Routes); +} + +start(); diff --git a/gallery/package.json b/gallery/package.json index 2f8c6031e..15ed57fec 100644 --- a/gallery/package.json +++ b/gallery/package.json @@ -4,58 +4,58 @@ "version": "1.0.0", "scripts": { "start": "webpack-dev-server --config config/webpack.dev.js --open", - "build": "webpack --config config/webpack.prod.js" + "build": "webpack --config config/webpack.prod.js", + "check-types": "tsc --noEmit" }, "dependencies": { "casual": "^1.6.2", - "core-js": "^3.39.0", - "cx": "^25.4.1", - "cx-react": "^24.7.1", - "cx-theme-aquamarine": "^18.7.3", - "cx-theme-dark": "^18.7.1", - "cx-theme-frost": "^18.7.1", - "cx-theme-material": "^18.7.0", - "cx-theme-material-dark": "^20.1.0", - "cx-theme-packed-dark": "^24.4.1", - "cx-theme-space-blue": "^24.5.1", + "core-js": "^3.47.0", + "cx": "workspace:*", + "cx-react": "workspace:*", + "cx-theme-aquamarine": "workspace:*", + "cx-theme-dark": "workspace:*", + "cx-theme-frost": "workspace:*", + "cx-theme-material": "workspace:*", + "cx-theme-material-dark": "workspace:*", + "cx-theme-packed-dark": "workspace:*", + "cx-theme-space-blue": "workspace:*", "intl": "^1.2.5", "plural": "^1.1.0", - "react": "^18.3.1", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@types/node": "^22.10.1", - "@types/react": "^18.3.14", - "babel-loader": "^9.2.1", - "babel-plugin-transform-cx-imports": "^21.3.0", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "babel-loader": "^10.0.0", + "babel-plugin-transform-cx-imports": "^26.0.1", "babel-plugin-transform-cx-jsx": "^21.3.0", "babel-preset-cx-env": "^24.0.0", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.5", "if-loader": "^1.0.2", "json-loader": "^0.5.7", "mini-css-extract-plugin": "^2.9.2", "modify-babel-preset": "^3.2.1", "sass": "^1.77.8", - "sass-loader": "^16.0.1", + "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "svg-url-loader": "^8.0.0", "ts-loader": "^9.5.1", - "typescript": "^5.7.2", + "typescript": "^5.9.3", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" } } diff --git a/gallery/polyfill.js b/gallery/polyfill.js deleted file mode 100644 index eb33effc1..000000000 --- a/gallery/polyfill.js +++ /dev/null @@ -1,3 +0,0 @@ -import "core-js/stable"; -import 'whatwg-fetch'; - diff --git a/gallery/routes/charts/column/combination.tsx b/gallery/routes/charts/column/combination.tsx index bb169af3f..33696234c 100644 --- a/gallery/routes/charts/column/combination.tsx +++ b/gallery/routes/charts/column/combination.tsx @@ -1,105 +1,125 @@ -import { cx, Section, FlexRow, Repeater, Checkbox, Grid } from 'cx/widgets'; -import { bind, expr, tpl, Controller, KeySelection } from 'cx/ui'; -import { Chart, Legend, Gridlines, LineGraph, CategoryAxis, - NumericAxis, ColumnGraph, TimeAxis, Range, Marker, Column } from 'cx/charts'; -import { Svg, Line, Rectangle, Text, ClipRect } from 'cx/svg'; -import casual from '../../../util/casual'; +import { CategoryAxis, Chart, Column, Gridlines, Marker, NumericAxis } from "cx/charts"; +import { Rectangle, Svg, Text } from "cx/svg"; +import { bind, Controller, expr, KeySelection, tpl } from "cx/ui"; +import { FlexRow, Grid, Repeater, Section } from "cx/widgets"; -var categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +var categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; class PageController extends Controller { init() { super.init(); var v1 = 100; - this.store.set('$page.points2', Array.from({length: categories.length}, (_, i) => ({ - x: categories[i], - v1: v1 = (v1 + (Math.random() - 0.5) * 30), - v2: v1 + 50 + Math.random() * 100 - }))); - + this.store.set( + "$page.points2", + Array.from({ length: categories.length }, (_, i) => ({ + x: categories[i], + v1: (v1 = v1 + (Math.random() - 0.5) * 30), + v2: v1 + 50 + Math.random() * 100, + })), + ); } } var columnSelection = new KeySelection({ - keyField: 'x', - bind: '$page.selection', - record: { bind: '$point' }, - index: { bind: '$index' } + keyField: "x", + bind: "$page.selection", + record: { bind: "$point" }, + index: { bind: "$index" }, }); let mw = 768; -export default - Source Code -
- +export default ( + + + Source Code + +
+ - = mw ? CategoryAxis : {type: CategoryAxis, labelAnchor: "end", labelRotation: -45, labelDy: '0.35em' }, - y: { type: NumericAxis, vertical: true, snapToTicks: 0 } }}> - + = mw + ? CategoryAxis + : { type: CategoryAxis, labelAnchor: "end", labelRotation: -45, labelDy: "0.35em" }, + y: { type: NumericAxis, vertical: true, snapToTicks: 0 }, + }} + > + - - - - - - + + + + + + - - + + - - -
-
+
+
+
+); -import { hmr } from '../../hmr.js'; - +hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; ++hmr(module); diff --git a/gallery/routes/charts/column/normalized.tsx b/gallery/routes/charts/column/normalized.tsx index c13352fca..1b8856239 100644 --- a/gallery/routes/charts/column/normalized.tsx +++ b/gallery/routes/charts/column/normalized.tsx @@ -1,11 +1,9 @@ -import { cx, Section, FlexRow, Repeater, Checkbox, Grid } from 'cx/widgets'; -import { bind, expr, tpl, Controller, KeySelection } from 'cx/ui'; -import { Chart, Legend, Gridlines, LineGraph, CategoryAxis, - NumericAxis, ColumnGraph, TimeAxis, Range, Marker, Column } from 'cx/charts'; -import { Svg, Line, Rectangle, Text, ClipRect } from 'cx/svg'; -import casual from '../../../util/casual'; +import { CategoryAxis, Chart, Column, Gridlines, Legend, NumericAxis } from "cx/charts"; +import { Svg } from "cx/svg"; +import { bind, Controller, KeySelection, tpl } from "cx/ui"; +import { Repeater, Section } from "cx/widgets"; -var categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +var categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; class PageController extends Controller { init() { @@ -14,69 +12,91 @@ class PageController extends Controller { var v1 = 500; var v2 = 500; var v3 = 500; - this.store.set('$page.points4', Array.from({length: 10}, (_, i) => ({ - x: 2000 + i, - v1: v1 = v1 + (Math.random() - 0.5) * 100, - v2: v2 = v2 + (Math.random() - 0.5) * 100, - v3: v3 = v3 + (Math.random() - 0.5) * 100, - }))); - + this.store.set( + "$page.points4", + Array.from({ length: 10 }, (_, i) => ({ + x: 2000 + i, + v1: (v1 = v1 + (Math.random() - 0.5) * 100), + v2: (v2 = v2 + (Math.random() - 0.5) * 100), + v3: (v3 = v3 + (Math.random() - 0.5) * 100), + })), + ); } } var columnSelection = new KeySelection({ - keyField: 'x', - bind: '$page.selection', - record: { bind: '$point' }, - index: { bind: '$index' } + keyField: "x", + bind: "$page.selection", + record: { bind: "$point" }, + index: { bind: "$index" }, }); let mw = 768; -export default - Source Code -
- - = mw ? CategoryAxis : {type: CategoryAxis, labelAnchor: "end", labelRotation: -45, labelDy: '0.35em' }, - y: { type: NumericAxis, vertical: true, normalized: true, format: 'p;0' } - }}> - - - - - - - - - - - - -
-
+export default ( + + + Source Code + +
+ + = mw + ? CategoryAxis + : { type: CategoryAxis, labelAnchor: "end", labelRotation: -45, labelDy: "0.35em" }, + y: { type: NumericAxis, vertical: true, normalized: true, format: "p;0" }, + }} + > + + + + + + + + + + + +
+
+); -import { hmr } from '../../hmr.js'; - +hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; ++hmr(module); diff --git a/gallery/routes/charts/pie-chart/multilevel.tsx b/gallery/routes/charts/pie-chart/multilevel.tsx index 5ebe67b39..7f37d952e 100644 --- a/gallery/routes/charts/pie-chart/multilevel.tsx +++ b/gallery/routes/charts/pie-chart/multilevel.tsx @@ -1,77 +1,92 @@ -import {cx, Section, FlexRow, Repeater} from 'cx/widgets'; -import {bind, expr, tpl, Controller, KeySelection} from 'cx/ui'; -import { PieChart, PieSlice, Legend, ColorMap } from 'cx/charts'; -import {Svg, Line, Rectangle, Text} from 'cx/svg'; +import { cx, Section, FlexRow, Repeater } from "cx/widgets"; +import { bind, expr, tpl, Controller, KeySelection } from "cx/ui"; +import { PieChart, PieSlice, Legend, ColorMap } from "cx/charts"; +import { Svg, Line, Rectangle, Text } from "cx/svg"; class PageController extends Controller { init() { super.init(); - this.store.set('multilevel.points', Array.from({length: 7}).map((_, i) => { - var value = 20 + Math.random() * 100; - return { - x: i * 5, - v: value, - slices: Array.from({length: 5}).map(x=>({sv: value / 5})) - } - })); + this.store.set( + "multilevel.points", + Array.from({ length: 7 }).map((_, i) => { + var value = 20 + Math.random() * 100; + return { + x: i * 5, + v: value, + slices: Array.from({ length: 5 }).map((x) => ({ sv: value / 5 })), + }; + }), + ); } } -export default - Source Code -
- - - - - - - - - - - - - - -
-
+export default ( + + + Source Code + +
+ + + + + + + + + + + + +
+
+); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/charts/scatter-graph/standard.tsx b/gallery/routes/charts/scatter-graph/standard.tsx index 301ae7e37..faf2af913 100644 --- a/gallery/routes/charts/scatter-graph/standard.tsx +++ b/gallery/routes/charts/scatter-graph/standard.tsx @@ -1,59 +1,75 @@ -import {cx, Section, FlexRow} from 'cx/widgets'; -import {bind, expr, tpl, Controller} from 'cx/ui'; -import {Chart, ScatterGraph, NumericAxis, Gridlines, Legend} from 'cx/charts'; -import {Svg} from 'cx/svg'; +import { cx, Section, FlexRow } from "cx/widgets"; +import { bind, expr, tpl, Controller } from "cx/ui"; +import { Chart, ScatterGraph, NumericAxis, Gridlines, Legend } from "cx/charts"; +import { Svg } from "cx/svg"; class PageController extends Controller { - init() { - super.init(); - this.store.set('$page.reds', Array.from({length: 200}, (_, i) => ({ + init() { + super.init(); + this.store.set( + "$page.reds", + Array.from({ length: 200 }, (_, i) => ({ x: 100 + Math.random() * 300, y: Math.random() * 300, - size: Math.random() * 20 - }))); - this.store.set('$page.blues', Array.from({length: 200}, (_, i) => ({ + size: Math.random() * 20, + })), + ); + this.store.set( + "$page.blues", + Array.from({ length: 200 }, (_, i) => ({ x: Math.random() * 300, y: 100 + Math.random() * 300, - size: Math.random() * 20 - }))); - } + size: Math.random() * 20, + })), + ); + } } -export default - Source Code -
- - - - - +export default ( + + + Source Code + +
+ + + + + - - -
-
+
+ +
+
+); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/general/grids/drag-drop.tsx b/gallery/routes/general/grids/drag-drop.tsx index 98dd01709..1515193ca 100644 --- a/gallery/routes/general/grids/drag-drop.tsx +++ b/gallery/routes/general/grids/drag-drop.tsx @@ -1,123 +1,118 @@ -import {DragHandle, FlexRow, Grid, HtmlElement, Section, cx} from "cx/widgets"; -import {Controller, KeySelection, bind} from "cx/ui"; +import { Controller, KeySelection, bind } from "cx/ui"; +import { DragHandle, FlexRow, Grid, Section } from "cx/widgets"; //Filtering not implemented to keep it short function insertElement(array, index, ...args) { - return [...array.slice(0, index), ...args, ...array.slice(index)]; + return [...array.slice(0, index), ...args, ...array.slice(index)]; } function move(store, target, e) { - let selection = e.source.records.map(r => r.data); + let selection = e.source.records.map((r) => r.data); - store.update( - e.source.data.source, - array => array.filter((a, i) => selection.indexOf(a) == -1) - ); + store.update(e.source.data.source, (array) => array.filter((a, i) => selection.indexOf(a) == -1)); - if (e.source.data.source == target) e.source.records.forEach(record => { - if (record.index < e.target.insertionIndex) e.target.insertionIndex--; - }); + if (e.source.data.source == target) + e.source.records.forEach((record) => { + if (record.index < e.target.insertionIndex) e.target.insertionIndex--; + }); - store.update(target, insertElement, e.target.insertionIndex, ...selection); + store.update(target, insertElement, e.target.insertionIndex, ...selection); } class PageController extends Controller { - init() { - this.store.init( - "grid1", - Array.from({length: 10}, (_, c) => ({ - id: c + 1, - name: "Item " + (c + 1), - number: Math.random() * 100 - })) - ); + init() { + this.store.init( + "grid1", + Array.from({ length: 10 }, (_, c) => ({ + id: c + 1, + name: "Item " + (c + 1), + number: Math.random() * 100, + })), + ); - this.store.init( - "grid2", - Array.from({length: 10}, (_, c) => ({ - id: 10000 + c + 1, - name: "Item " + (c + 1), - number: Math.random() * 100 - })) - ); - } + this.store.init( + "grid2", + Array.from({ length: 10 }, (_, c) => ({ + id: 10000 + c + 1, + name: "Item " + (c + 1), + number: Math.random() * 100, + })), + ); + } } -; - export default ( - - Source Code + + + Source Code + - -
- e.source.data.type == "record"} - onDrop={(e, {store}) => move(store, "grid1", e)} - selection={{type: KeySelection, multiple: true, bind: "s1"}} - /> -
-
- - - ☰ - - - ) - }, - { - style: "width: 300px", - field: "name", - header: "Name", - sortable: true - }, - { - field: "number", - header: "Number", - format: "n;2", - sortable: true, - align: "right" - } - ]} - dragSource={ - {mode: "copy", data: {type: "record", source: "grid2"}} - } - dropZone={{mode: "insertion"}} - onDropTest={e => e.source.data.type == "record"} - onDrop={(e, {store}) => move(store, "grid2", e)} - /> -
-
-
+ +
+ e.source.data.type == "record"} + onDrop={(e, { store }) => move(store, "grid1", e)} + selection={{ type: KeySelection, multiple: true, bind: "s1" }} + /> +
+
+ + + + ), + }, + { + style: "width: 300px", + field: "name", + header: "Name", + sortable: true, + }, + { + field: "number", + header: "Number", + format: "n;2", + sortable: true, + align: "right", + }, + ]} + dragSource={{ mode: "copy", data: { type: "record", source: "grid2" } }} + dropZone={{ mode: "insertion" }} + onDropTest={(e) => e.source.data.type == "record"} + onDrop={(e, { store }) => move(store, "grid2", e)} + /> +
+
+
); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/general/grids/dynamic-grouping.tsx b/gallery/routes/general/grids/dynamic-grouping.tsx index 119405a29..f6ec0ff9e 100644 --- a/gallery/routes/general/grids/dynamic-grouping.tsx +++ b/gallery/routes/general/grids/dynamic-grouping.tsx @@ -1,119 +1,113 @@ -import {Grid, HtmlElement, LookupField, Section, cx} from "cx/widgets"; -import {Controller, bind} from "cx/ui"; -import casual from '../../../util/casual'; +import { Controller, bind } from "cx/ui"; +import { Grid, LookupField, Section } from "cx/widgets"; +import casual from "../../../util/casual"; import "../../../util/plural"; class PageController extends Controller { + onInit() { + //init grid data + this.store.set( + "$page.records", + Array.from({ length: 100 }).map((v, i) => ({ + id: i + 1, + fullName: casual.full_name, + continent: casual.continent, + browser: casual.browser, + os: casual.operating_system, + visits: casual.integer(1, 100), + })), + ); - onInit() { - //init grid data - this.store.set( - "$page.records", - Array - .from({length: 100}) - .map((v, i) => ({ - id: i + 1, - fullName: casual.full_name, - continent: casual.continent, - browser: casual.browser, - os: casual.operating_system, - visits: casual.integer(1, 100) - })) - ); + //init grouping options + this.store.set("$page.groupableFields", [ + { id: "continent", text: "Continent" }, + { id: "browser", text: "Browser" }, + { id: "os", text: "Operating System" }, + ]); - //init grouping options - this.store.set("$page.groupableFields", [ - {id: "continent", text: "Continent"}, - {id: "browser", text: "Browser"}, - {id: "os", text: "Operating System"} - ]); - - this.store.set("$page.groups", [{id: "browser", text: "Browser"}]); - - //when changed, apply grouping to the grid - this.addTrigger("grouping", ["$page.groups"], g => { - var groupings = [{key: {}, showFooter: true}]; - groupings.push(...(g || []).map(x => x.id)); - var grid = this.widget.findFirst(Grid); - grid.groupBy(groupings, {autoConfigure: true}); - }, true); - } + this.store.set("$page.groups", [{ id: "browser", text: "Browser" }]); + } } - export default ( - - Source Code -
-

- Group by: -

- 1 ? {$group.$name:s:TOTAL} + " - " : "") + {$group.fullName} + " " + {$group.fullName:plural;item}' - } - }, - { - header: "Continent", - field: "continent", - sortable: true, - aggregate: "distinct", - aggregateField: "continents", - footer: { - tpl: "{$group.continents} {$group.continents:plural;continent}" - } - }, - { - header: "Browser", - field: "browser", - sortable: true, - aggregate: "distinct", - aggregateField: "browsers", - footer: { - tpl: "{$group.browsers} {$group.browsers:plural;browser}" - } - }, - { - header: "OS", - field: "os", - sortable: true, - aggregate: "distinct", - aggregateField: "oss", - footer: {tpl: "{$group.oss} {$group.oss:plural;OS}"} - }, - { - header: "Visits", - field: "visits", - sortable: true, - aggregate: "sum", - align: "right" - } - ] - } - /> -
-
+ + + Source Code + +
+

+ Group by:{" "} + +

+ { + var groupings = [{ key: {}, showFooter: true }]; + groupings.push(...(g || []).map((x) => x.id)); + return groupings; + }} + columns={[ + { + header: "Name", + field: "fullName", + sortable: true, + aggregate: "count", + footer: { + expr: '({$group.$level} > 1 ? {$group.$name:s:TOTAL} + " - " : "") + {$group.fullName} + " " + {$group.fullName:plural;item}', + }, + }, + { + header: "Continent", + field: "continent", + sortable: true, + aggregate: "distinct", + aggregateField: "continents", + footer: { + tpl: "{$group.continents} {$group.continents:plural;continent}", + }, + }, + { + header: "Browser", + field: "browser", + sortable: true, + aggregate: "distinct", + aggregateField: "browsers", + footer: { + tpl: "{$group.browsers} {$group.browsers:plural;browser}", + }, + }, + { + header: "OS", + field: "os", + sortable: true, + aggregate: "distinct", + aggregateField: "oss", + footer: { tpl: "{$group.oss} {$group.oss:plural;OS}" }, + }, + { + header: "Visits", + field: "visits", + sortable: true, + aggregate: "sum", + align: "right", + }, + ]} + /> +
+
); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/general/grids/filtering.tsx b/gallery/routes/general/grids/filtering.tsx index 4048c9f3c..1d4131164 100644 --- a/gallery/routes/general/grids/filtering.tsx +++ b/gallery/routes/general/grids/filtering.tsx @@ -1,165 +1,139 @@ -import { Grid, HtmlElement, Pagination, TextField, Section, Select, cx } from "cx/widgets"; -import { Controller, bind } from "cx/ui"; import { getComparer } from "cx/data"; -import casual from '../../../util/casual'; +import { Controller, bind } from "cx/ui"; +import { Grid, Pagination, Section, Select, TextField } from "cx/widgets"; +import casual from "../../../util/casual"; class PageController extends Controller { - init() { - super.init(); + init() { + super.init(); - var dataSet = Array - .from({ length: 1000 }) - .map((v, i) => ({ - id: i + 1, - fullName: casual.full_name, - phone: casual.phone, - city: casual.city + var dataSet = Array.from({ length: 1000 }).map((v, i) => ({ + id: i + 1, + fullName: casual.full_name, + phone: casual.phone, + city: casual.city, })); - this.store.init("$page.pageSize", 20); - this.store.init("$page.filter", { name: null, phone: null, city: null }); - - //if context changes, go to the first page - this.addTrigger( - "page", - [ "$page.pageSize", "$page.sorters", "$page.filter" ], - () => { - this.store.set("$page.page", 1); - }, - true - ); + this.store.init("$page.pageSize", 20); + this.store.init("$page.filter", { name: null, phone: null, city: null }); - this.addTrigger( - "pagination", - ["$page.pageSize", "$page.page", "$page.sorters", "$page.filter"], - (size, page, sorters, filter) => { - //simulate server call - setTimeout( - () => { - var filtered = dataSet; - if (filter) { - if (filter.name) { - var checks = filter.name - .split(" ") - .map(w => new RegExp(w, "gi")); - filtered = filtered.filter( - x => checks.every(ex => x.fullName.match(ex)) - ); - } + //if context changes, go to the first page + this.addTrigger( + "page", + ["$page.pageSize", "$page.sorters", "$page.filter"], + () => { + this.store.set("$page.page", 1); + }, + true, + ); - if (filter.phone) - filtered = filtered.filter( - x => x.phone.indexOf(filter.phone) != -1 - ); + this.addTrigger( + "pagination", + ["$page.pageSize", "$page.page", "$page.sorters", "$page.filter"], + (size, page, sorters, filter) => { + //simulate server call + setTimeout(() => { + var filtered = dataSet; + if (filter) { + if (filter.name) { + var checks = filter.name.split(" ").map((w) => new RegExp(w, "gi")); + filtered = filtered.filter((x) => checks.every((ex) => x.fullName.match(ex))); + } - if (filter.city) - filtered = filtered.filter( - x => x.city.indexOf(filter.city) != -1 - ); - } - var compare = getComparer( - (sorters || []).map(x => ({ - value: { bind: x.field }, - direction: x.direction - })) - ); - filtered.sort(compare); - //simulate database sort - this.store.set( - "$page.records", - filtered.slice((page - 1) * size, page * size) - ); - this.store.set( - "$page.pageCount", - Math.ceil(filtered.length / size) - ); - }, - 100 - ); - }, - true - ); - } -}; + if (filter.phone) filtered = filtered.filter((x) => x.phone.indexOf(filter.phone) != -1); + if (filter.city) filtered = filtered.filter((x) => x.city.indexOf(filter.city) != -1); + } + var compare = getComparer( + (sorters || []).map((x) => ({ + value: { bind: x.field }, + direction: x.direction, + })), + ); + filtered.sort(compare); + //simulate database sort + this.store.set("$page.records", filtered.slice((page - 1) * size, page * size)); + this.store.set("$page.pageCount", Math.ceil(filtered.length / size)); + }, 100); + }, + true, + ); + } +} export default ( - - Source Code -
- - ) - } - }, - { - header1: "Phone", - header2: { - items: ( - - ) - }, - field: "phone" - }, - { - header1: "City", - header2: { - allowSorting: false, - items: ( - - ) - }, - field: "city", - sortable: true - } - ] - } - sorters={bind("$page.sorters")} - remoteSort - /> -
- - -
-
-
+ + + Source Code + +
+ + ), + }, + }, + { + header1: "Phone", + header2: { + items: ( + + ), + }, + field: "phone", + }, + { + header1: "City", + header2: { + allowSorting: false, + items: ( + + ), + }, + field: "city", + sortable: true, + }, + ]} + sorters={bind("$page.sorters")} + remoteSort + /> +
+ + +
+
+
); -import { hmr } from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/general/grids/form-editing.tsx b/gallery/routes/general/grids/form-editing.tsx index 3651c5abd..31b1b9bda 100644 --- a/gallery/routes/general/grids/form-editing.tsx +++ b/gallery/routes/general/grids/form-editing.tsx @@ -1,132 +1,128 @@ -import { - Grid, - Button, - Section, - cx, - Checkbox, - TextField, - ValidationGroup, - FlexCol, - FlexRow -} from "cx/widgets"; -import {Controller, bind, expr, KeySelection, LabelsLeftLayout} from "cx/ui"; -import casual from '../../../util/casual'; +import { Grid, Button, Section, cx, Checkbox, TextField, ValidationGroup, FlexCol, FlexRow } from "cx/widgets"; +import { Controller, bind, expr, KeySelection, LabelsLeftLayout } from "cx/ui"; +import casual from "../../../util/casual"; class PageController extends Controller { - init() { - super.init(); + init() { + super.init(); - this.store.set( - "$page.records", - Array - .from({length: 5}) - .map((v, i) => ({ - id: i + 1, - fullName: casual.full_name, - phone: casual.phone, - city: casual.city, - notified: casual.coin_flip - })) - ); + this.store.set( + "$page.records", + Array.from({ length: 5 }).map((v, i) => ({ + id: i + 1, + fullName: casual.full_name, + phone: casual.phone, + city: casual.city, + notified: casual.coin_flip, + })), + ); - this.addTrigger("$page.form", ["$page.id", "$page.records"], (id, - records) => { - this.store.set("$page.form", records.find(a => a.id == id)); - this.store.set("$page.add", false); - }); - } + this.addTrigger("$page.form", ["$page.id", "$page.records"], (id, records) => { + this.store.set( + "$page.form", + records.find((a) => a.id == id), + ); + this.store.set("$page.add", false); + }); + } - newRecord() { - let records = this.store.get("$page.records"); - this.store.set("$page.add", true); - this.store.set("$page.form", {fullName: "New Entry"}); - } + newRecord() { + let records = this.store.get("$page.records"); + this.store.set("$page.add", true); + this.store.set("$page.form", { fullName: "New Entry" }); + } - saveRecord() { - let page = this.store.get("$page"), newRecords; - if (page.add) { - let id = page.records.reduce((acc, rec) => Math.max(acc, rec.id), 0) + 1; - newRecords = [...page.records, Object.assign({id: id}, page.form)]; - this.store.set("$page.id", id); - } else newRecords = page.records.map(r => r.id == page.id ? page.form : r); + saveRecord() { + let page = this.store.get("$page"), + newRecords; + if (page.add) { + let id = page.records.reduce((acc, rec) => Math.max(acc, rec.id), 0) + 1; + newRecords = [...page.records, Object.assign({ id: id }, page.form)]; + this.store.set("$page.id", id); + } else newRecords = page.records.map((r) => (r.id == page.id ? page.form : r)); - this.store.set("$page.records", newRecords); - } + this.store.set("$page.records", newRecords); + } - removeRecord(id) { - let newRecords = this.store.get("$page.records").filter(r => r.id != id); - this.store.set("$page.records", newRecords); - } + removeRecord(id) { + let newRecords = this.store.get("$page.records").filter((r) => r.id != id); + this.store.set("$page.records", newRecords); + } } export default ( - - Source Code - -
- - - - ) - } - ]} - /> -
- -
+ + + Source Code + + +
+ + + + ), + }, + ]} + /> +
+ +
-
- - - - - - - -
-
-
+
+ + + + + + + +
+
+
); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/routes/general/grids/row-editing.js b/gallery/routes/general/grids/row-editing.js deleted file mode 100644 index 6fe21859d..000000000 --- a/gallery/routes/general/grids/row-editing.js +++ /dev/null @@ -1,169 +0,0 @@ -import {cx, Grid, Button, TextField, NumberField, Section} from "cx/widgets"; -import {Controller} from "cx/ui"; -import casual from '../../../util/casual'; - -class PageController extends Controller { - onInit() { - //init grid data - if (!this.store.get('$page.records')) - this.shuffle(); - } - - shuffle() { - this.store.set( - "$page.records", - Array - .from({length: 10}) - .map((v, i) => ({ - fullName: casual.full_name, - continent: casual.continent, - browser: casual.browser, - os: casual.operating_system, - visits: casual.integer(1, 100) - })) - ); - } - - editRow(e, {store}) { - let record = store.get('$record'); - //keep old values - store.set('$record.$editing', record); - } - - saveRow(e, {store}) { - store.delete('$record.$editing'); - } - - cancelRowEditing(e, {store}) { - let oldRecord = store.get('$record.$editing'); - if (oldRecord.add) - store.delete('$record'); - else - store.set('$record', oldRecord); - } - - addRow(e) { - this.store.update('$page.records', records => [...records, { - $editing: {add: true} - }]) - } - - deleteRow(e, {store}) { - store.delete('$record'); - } -} - -export default ( - - Source Code -
- - - - }, - { - header: "Continent", field: "continent", sortable: true, - items: - - - }, - { - header: "Browser", field: "browser", sortable: true, - items: - - - }, - { - header: "OS", field: "os", sortable: true, - items: - - - }, - { - header: "Visits", - field: "visits", - sortable: true, - align: "right", - items: - - - }, { - header: 'Actions', - style: "whitespace: nowrap", - align:"center", - items: - - - - - - } - ] - } - /> -
-

- -

-
-
-); diff --git a/gallery/routes/general/grids/row-editing.tsx b/gallery/routes/general/grids/row-editing.tsx new file mode 100644 index 000000000..58331eeb3 --- /dev/null +++ b/gallery/routes/general/grids/row-editing.tsx @@ -0,0 +1,204 @@ +import { bind, Controller, expr } from "cx/ui"; +import { Button, Grid, NumberField, Section, TextField } from "cx/widgets"; +import casual from "../../../util/casual"; + +class PageController extends Controller { + onInit() { + //init grid data + if (!this.store.get("$page.records")) this.shuffle(); + } + + shuffle() { + this.store.set( + "$page.records", + Array.from({ length: 10 }).map((v, i) => ({ + fullName: casual.full_name, + continent: casual.continent, + browser: casual.browser, + os: casual.operating_system, + visits: casual.integer(1, 100), + })), + ); + } + + editRow(e, { store }) { + let record = store.get("$record"); + //keep old values + store.set("$record.$editing", record); + } + + saveRow(e, { store }) { + store.delete("$record.$editing"); + } + + cancelRowEditing(e, { store }) { + let oldRecord = store.get("$record.$editing"); + if (oldRecord.add) store.delete("$record"); + else store.set("$record", oldRecord); + } + + addRow(e) { + this.store.update("$page.records", (records) => [ + ...records, + { + $editing: { add: true }, + }, + ]); + } + + deleteRow(e, { store }) { + store.delete("$record"); + } +} + +export default ( + + + Source Code + +
+ + + + ), + }, + { + header: "Continent", + field: "continent", + sortable: true, + items: ( + + + + ), + }, + { + header: "Browser", + field: "browser", + sortable: true, + items: ( + + + + ), + }, + { + header: "OS", + field: "os", + sortable: true, + items: ( + + + + ), + }, + { + header: "Visits", + field: "visits", + sortable: true, + align: "right", + items: ( + + + + ), + }, + { + header: "Actions", + style: "whitespace: nowrap", + align: "center", + items: ( + + + + + + + ), + }, + ]} + /> +
+

+ +

+
+
+); diff --git a/gallery/routes/general/grids/row-expanding.js b/gallery/routes/general/grids/row-expanding.tsx similarity index 100% rename from gallery/routes/general/grids/row-expanding.js rename to gallery/routes/general/grids/row-expanding.tsx diff --git a/gallery/routes/general/grids/tree-grid.tsx b/gallery/routes/general/grids/tree-grid.tsx index 4110a3ace..3b809524c 100644 --- a/gallery/routes/general/grids/tree-grid.tsx +++ b/gallery/routes/general/grids/tree-grid.tsx @@ -1,87 +1,85 @@ -import {Grid, HtmlElement, Button, Section, Select, cx, ValidationGroup, TreeAdapter, TreeNode} from "cx/widgets"; -import {Controller, bind, expr, KeySelection} from "cx/ui"; -import {Format} from "cx/util"; -import casual from '../../../util/casual'; +import { Grid, HtmlElement, Button, Section, Select, cx, ValidationGroup, TreeAdapter, TreeNode } from "cx/widgets"; +import { Controller, bind, expr, KeySelection } from "cx/ui"; +import { Format } from "cx/util"; +import casual from "../../../util/casual"; class PageController extends Controller { + idSeq: number; - idSeq: number; + init() { + super.init(); + this.idSeq = 0; + this.store.set("$page.data", this.generateRecords()); + } - init() { - super.init(); - this.idSeq = 0; - this.store.set("$page.data", this.generateRecords()); - } - - generateRecords(node?) { - if (!node || node.$level < 5) - return Array - .from({length: 5}) - .map(() => ({ - id: ++this.idSeq, - fullName: casual.full_name, - phone: casual.phone, - city: casual.city, - notified: casual.coin_flip, - $leaf: casual.coin_flip - })); - } + generateRecords(node?) { + if (!node || node.$level < 5) + return Array.from({ length: 5 }).map(() => ({ + id: ++this.idSeq, + fullName: casual.full_name, + phone: casual.phone, + city: casual.city, + notified: casual.coin_flip, + $leaf: casual.coin_flip, + })); + } } export default ( - - Source Code -
- - controller.generateRecords(node) - }} - selection={{type: KeySelection, bind: "$page.selection"}} - columns={[ - { - header: "Name", - field: "fullName", - sortable: true, - style: "white-space: nowrap", - items: ( - - - - ) - }, - {header: "Phone", field: "phone", style: "white-space: nowrap"}, - {header: "City", field: "city", sortable: true}, - { - header: "Notified", - field: "notified", - sortable: true, - value: {expr: '{$record.notified} ? "Yes" : "No"'} - } - ]} - /> -
-
+ + + Source Code + +
+ instance.getControllerByType(PageController).generateRecords(node), + }} + selection={{ type: KeySelection, bind: "$page.selection" }} + columns={[ + { + header: "Name", + field: "fullName", + sortable: true, + style: "white-space: nowrap", + items: ( + + + + ), + }, + { header: "Phone", field: "phone", style: "white-space: nowrap" }, + { header: "City", field: "city", sortable: true }, + { + header: "Notified", + field: "notified", + sortable: true, + value: { expr: '{$record.notified} ? "Yes" : "No"' }, + }, + ]} + /> +
+
); -import {hmr} from '../../hmr.js'; -hmr(module); \ No newline at end of file +import { hmr } from "../../hmr.js"; +hmr(module); diff --git a/gallery/style.js b/gallery/style.js deleted file mode 100644 index de0a412ab..000000000 --- a/gallery/style.js +++ /dev/null @@ -1,2 +0,0 @@ -import style from "./index.scss"; -style.use(); diff --git a/gallery/style.ts b/gallery/style.ts new file mode 100644 index 000000000..62563fee6 --- /dev/null +++ b/gallery/style.ts @@ -0,0 +1,3 @@ +//@ts-expect-error +import style from "./index.scss"; +style.use(); diff --git a/gallery/themes/aquamarine.js b/gallery/themes/aquamarine.js deleted file mode 100644 index 0c8cc98cf..000000000 --- a/gallery/themes/aquamarine.js +++ /dev/null @@ -1,6 +0,0 @@ -import style from "./aquamarine.useable.scss"; -import {registerTheme} from './index'; - -import {applyThemeOverrides} from "cx-theme-aquamarine/src"; - -registerTheme("aquamarine", style, applyThemeOverrides); \ No newline at end of file diff --git a/gallery/themes/aquamarine.ts b/gallery/themes/aquamarine.ts new file mode 100644 index 000000000..bdbfe9ce5 --- /dev/null +++ b/gallery/themes/aquamarine.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import style from "./aquamarine.useable.scss"; +import { registerTheme } from "./index"; + +import { applyThemeOverrides } from "cx-theme-aquamarine"; + +registerTheme("aquamarine", style, applyThemeOverrides); diff --git a/gallery/themes/core.js b/gallery/themes/core.js deleted file mode 100644 index aa520d67d..000000000 --- a/gallery/themes/core.js +++ /dev/null @@ -1,4 +0,0 @@ -import style from "./core.useable.scss"; -import {registerTheme} from './index'; - -registerTheme("core", style); \ No newline at end of file diff --git a/gallery/themes/core.ts b/gallery/themes/core.ts new file mode 100644 index 000000000..153dc7ddb --- /dev/null +++ b/gallery/themes/core.ts @@ -0,0 +1,5 @@ +// @ts-ignore +import style from "./core.useable.scss"; +import { registerTheme } from "./index"; + +registerTheme("core", style); diff --git a/gallery/themes/dark.js b/gallery/themes/dark.js deleted file mode 100644 index 981c67151..000000000 --- a/gallery/themes/dark.js +++ /dev/null @@ -1,4 +0,0 @@ -import style from "./dark.useable.scss"; -import {registerTheme} from './index'; - -registerTheme("dark", style); \ No newline at end of file diff --git a/gallery/themes/dark.ts b/gallery/themes/dark.ts new file mode 100644 index 000000000..69dff5e62 --- /dev/null +++ b/gallery/themes/dark.ts @@ -0,0 +1,5 @@ +// @ts-ignore +import style from "./dark.useable.scss"; +import { registerTheme } from "./index"; + +registerTheme("dark", style); diff --git a/gallery/themes/frost.js b/gallery/themes/frost.js deleted file mode 100644 index 6b662876d..000000000 --- a/gallery/themes/frost.js +++ /dev/null @@ -1,5 +0,0 @@ -import style from "./frost.useable.scss"; -import {registerTheme} from './index'; -import {applyThemeOverrides} from "cx-theme-frost/src"; - -registerTheme("frost", style, applyThemeOverrides); \ No newline at end of file diff --git a/gallery/themes/frost.ts b/gallery/themes/frost.ts new file mode 100644 index 000000000..5991065aa --- /dev/null +++ b/gallery/themes/frost.ts @@ -0,0 +1,6 @@ +// @ts-ignore +import style from "./frost.useable.scss"; +import { registerTheme } from "./index"; +import { applyThemeOverrides } from "cx-theme-frost"; + +registerTheme("frost", style, applyThemeOverrides); diff --git a/gallery/themes/index.ts b/gallery/themes/index.ts index a7aebac9a..6e1243410 100644 --- a/gallery/themes/index.ts +++ b/gallery/themes/index.ts @@ -1,14 +1,12 @@ -import {bump} from '../routes/hmr.js'; +import { bump } from "../routes/hmr.js"; let activeStyle = null; -import {Icon} from 'cx/widgets'; -import {Localization} from 'cx/ui'; +import { Icon } from "cx/widgets"; +import { Localization } from "cx/ui"; Localization.trackDefaults(); export function loadTheme(name) { - - if (activeStyle) - activeStyle.unuse(); + if (activeStyle) activeStyle.unuse(); let style = themes[name]; activeStyle = style; @@ -18,8 +16,7 @@ export function loadTheme(name) { Icon.restoreDefaultIcons(); Localization.restoreDefaults(); - if (callbacks[name]) - callbacks[name](); + if (callbacks[name]) callbacks[name](); bump(); } @@ -27,7 +24,7 @@ export function loadTheme(name) { const themes = {}; const callbacks = {}; -export function registerTheme(name, style, callback) { +export function registerTheme(name: string, style: any, callback?: () => void) { themes[name] = style; callbacks[name] = callback; -} \ No newline at end of file +} diff --git a/gallery/themes/material-dark.js b/gallery/themes/material-dark.js deleted file mode 100644 index 6b7aab044..000000000 --- a/gallery/themes/material-dark.js +++ /dev/null @@ -1,6 +0,0 @@ -import style from "./material-dark.useable.scss"; -import {registerTheme} from './index'; - -import {applyThemeOverrides} from "cx-theme-material-dark/src"; - -registerTheme("material-dark", style, applyThemeOverrides); \ No newline at end of file diff --git a/gallery/themes/material-dark.ts b/gallery/themes/material-dark.ts new file mode 100644 index 000000000..7e89de8a3 --- /dev/null +++ b/gallery/themes/material-dark.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import style from "./material-dark.useable.scss"; +import { registerTheme } from "./index"; + +import { applyThemeOverrides } from "cx-theme-material-dark"; + +registerTheme("material-dark", style, applyThemeOverrides); diff --git a/gallery/themes/material.js b/gallery/themes/material.js deleted file mode 100644 index 4e00c522e..000000000 --- a/gallery/themes/material.js +++ /dev/null @@ -1,6 +0,0 @@ -import style from "./material.useable.scss"; -import {registerTheme} from './index'; - -import {applyThemeOverrides} from "cx-theme-material/src"; - -registerTheme("material", style, applyThemeOverrides); \ No newline at end of file diff --git a/gallery/themes/material.ts b/gallery/themes/material.ts new file mode 100644 index 000000000..347db9ab4 --- /dev/null +++ b/gallery/themes/material.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import style from "./material.useable.scss"; +import { registerTheme } from "./index"; + +import { applyThemeOverrides } from "cx-theme-material"; + +registerTheme("material", style, applyThemeOverrides); diff --git a/gallery/themes/packed-dark.js b/gallery/themes/packed-dark.js deleted file mode 100644 index 3e2c350d8..000000000 --- a/gallery/themes/packed-dark.js +++ /dev/null @@ -1,5 +0,0 @@ -import style from "./packed-dark.useable.scss"; -import { registerTheme } from "./index"; -import { applyThemeOverrides } from "cx-theme-packed-dark/src"; - -registerTheme("packed-dark", style, applyThemeOverrides); diff --git a/gallery/themes/packed-dark.ts b/gallery/themes/packed-dark.ts new file mode 100644 index 000000000..294ae5550 --- /dev/null +++ b/gallery/themes/packed-dark.ts @@ -0,0 +1,6 @@ +// @ts-ignore +import style from "./packed-dark.useable.scss"; +import { registerTheme } from "./index"; +import { applyThemeOverrides } from "cx-theme-packed-dark"; + +registerTheme("packed-dark", style, applyThemeOverrides); diff --git a/gallery/themes/space-blue.js b/gallery/themes/space-blue.js deleted file mode 100644 index ef09be41b..000000000 --- a/gallery/themes/space-blue.js +++ /dev/null @@ -1,6 +0,0 @@ -import style from "./space-blue.useable.scss"; -import {registerTheme} from './index'; - -import {applyThemeOverrides} from "cx-theme-space-blue/src"; - -registerTheme("space-blue", style, applyThemeOverrides); \ No newline at end of file diff --git a/gallery/themes/space-blue.ts b/gallery/themes/space-blue.ts new file mode 100644 index 000000000..74620a0eb --- /dev/null +++ b/gallery/themes/space-blue.ts @@ -0,0 +1,7 @@ +// @ts-ignore +import style from "./space-blue.useable.scss"; +import { registerTheme } from "./index"; + +import { applyThemeOverrides } from "cx-theme-space-blue"; + +registerTheme("space-blue", style, applyThemeOverrides); diff --git a/gallery/tsconfig.json b/gallery/tsconfig.json index 3ae032907..4d32ec9da 100644 --- a/gallery/tsconfig.json +++ b/gallery/tsconfig.json @@ -1,20 +1,25 @@ { - "compilerOptions": { - "target": "es6", - "jsx": "react", - "allowJs": true, - "moduleResolution": "node", - "module": "esnext", - "sourceMap": false, - "jsxFactory": "cx", - "baseUrl": ".", - "paths": { - "cx": [ "../packages/cx/src" ], - "cx-react": [ "../packages/cx-react" ] - }, - "outDir": "./build/" - }, - "exclude": [ - "node_modules", "dist" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "es2024", + "jsx": "react-jsx", + //"jsxFactory": "cx", + "jsxImportSource": "cx", + "allowJs": true, + "moduleResolution": "bundler", + "module": "esnext", + "sourceMap": false, + "baseUrl": ".", + "paths": { + "cx-theme-aquamarine": ["../packages/cx-theme-aquamarine"], + "cx-theme-frost": ["../packages/cx-theme-frost"], + "cx-theme-material": ["../packages/cx-theme-material"], + "cx-theme-material-dark": ["../packages/cx-theme-material-dark"], + "cx-theme-packed-dark": ["../packages/cx-theme-packed-dark"], + "cx-theme-space-blue": ["../packages/cx-theme-space-blue"] + }, + "outDir": "./build/", + "types": ["node", "webpack-env"] + }, + "include": [".", "../misc"], + "exclude": ["node_modules", "dist"] +} diff --git a/litmus/index.js b/litmus/index.js index de351bd7f..99d03a21c 100644 --- a/litmus/index.js +++ b/litmus/index.js @@ -140,10 +140,9 @@ import "./index.scss"; // import Demo from "./features/charts/PointReducer"; // import Demo from "./features/charts/line-graph/LineGraph"; // import Demo from "./bugs/GridDefaultSortFieldClearableSortIssue"; -// import Demo from "./bugs/GridFixedColumnsFixedHeaderColumnsPosition"; +import Demo from "./bugs/GridFixedColumnsFixedHeaderColumnsPosition"; // import Demo from "./features/charts/axis/ComplexAxisLabels"; -//import Demo from "./bugs/pie-chart-active-bind"; -import Demo from "./bugs/grouping"; +// import Demo from "./bugs/pie-chart-active-bind"; let store = (window.store = new Store()); Widget.resetCounter(); diff --git a/litmus/package.json b/litmus/package.json index 4a22ef547..b8128c6e3 100644 --- a/litmus/package.json +++ b/litmus/package.json @@ -7,31 +7,30 @@ }, "dependencies": { "casual": "^1.6.2", - "core-js": "^3.39.0", - "cx": "^25.4.1", - "cx-immer": "^25.5.0", - "cx-react": "^24.7.1", + "core-js": "^3.47.0", + "cx": "workspace:*", + "cx-immer": "workspace:*", + "cx-react": "workspace:*", "immer": "^10.1.1", "intl": "^1.2.5", - "react": "^18.3.1", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@types/node": "^22.10.1", - "@types/react": "^18.3.14", - "babel-loader": "^9.2.1", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/node": "^25.0.3", + "@types/react": "^19.2.7", + "babel-loader": "^10.0.0", "babel-plugin-transform-cx-imports": "^21.3.0", "babel-plugin-transform-cx-jsx": "^21.3.0", "babel-preset-cx-env": "^24.0.0", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.6.3", + "html-webpack-plugin": "^5.6.5", "if-loader": "^1.0.2", "inline-manifest-webpack-plugin": "^4.0.2", "json-loader": "^0.5.7", @@ -39,18 +38,18 @@ "modify-babel-preset": "^3.2.1", "process": "^0.11.10", "sass": "^1.77.8", - "sass-loader": "^16.0.1", + "sass-loader": "^16.0.6", "style-loader": "^4.0.0", "svg-url-loader": "^8.0.0", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "url-loader": "^4.1.1", - "webpack": "^5.97.1", + "webpack": "^5.103.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cleanup-plugin": "^0.5.1", "webpack-cli": "^5.1.4", "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.2", "webpack-merge": "^6.0.1" } } diff --git a/misc/components/Animicon.js b/misc/components/Animicon.js deleted file mode 100644 index f805342f4..000000000 --- a/misc/components/Animicon.js +++ /dev/null @@ -1,20 +0,0 @@ -import { computable } from 'cx/ui'; -import { getSelector } from 'cx/src/data'; - -export const Animicon = ({ shape, onClick }) => ( - -
{ - return { - 'lines-button': true, - close: true, - x: shape == 'close', - 'arrow arrow-left': shape == 'arrow', - }; - })} - > - -
-
-); diff --git a/misc/components/Animicon.tsx b/misc/components/Animicon.tsx new file mode 100644 index 000000000..eddc8c493 --- /dev/null +++ b/misc/components/Animicon.tsx @@ -0,0 +1,20 @@ +import { computable, createFunctionalComponent } from "cx/ui"; +import { getSelector } from "cx/data"; + +export const Animicon = createFunctionalComponent(({ shape, onClick }: any) => ( + +
{ + return { + "lines-button": true, + close: true, + x: shape == "close", + "arrow arrow-left": shape == "arrow", + }; + })} + > + +
+
+)); diff --git a/misc/components/GitHubStarCount.js b/misc/components/GitHubStarCount.js deleted file mode 100644 index a748a5201..000000000 --- a/misc/components/GitHubStarCount.js +++ /dev/null @@ -1,31 +0,0 @@ -import { VDOM } from "cx/ui"; - -export class GitHubStarCount extends VDOM.Component { - render() { - return ( - - ); - } - - shouldComponentUpdate() { - return false; - } - - componentDidMount() { - let script = document.createElement("script"); - script.async = true; - script.defer = true; - script.src = "https://buttons.github.io/buttons.js"; - document.body.appendChild(script); - } -} diff --git a/misc/components/GitHubStarCount.tsx b/misc/components/GitHubStarCount.tsx new file mode 100644 index 000000000..4f173d8cb --- /dev/null +++ b/misc/components/GitHubStarCount.tsx @@ -0,0 +1,32 @@ +/** @jsxImportSource react */ +import { VDOM } from "cx/ui"; + +export class GitHubStarCount extends VDOM.Component { + render() { + return ( + + ); + } + + shouldComponentUpdate() { + return false; + } + + componentDidMount() { + let script = document.createElement("script"); + script.async = true; + script.defer = true; + script.src = "https://buttons.github.io/buttons.js"; + document.body.appendChild(script); + } +} diff --git a/misc/components/NavTree.js b/misc/components/NavTree.js deleted file mode 100644 index 978b0dc65..000000000 --- a/misc/components/NavTree.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Repeater, Link } from "cx/widgets"; -import { createFunctionalComponent, computable, DataProxy } from "cx/ui"; -import { ref } from "cx/hooks"; - -export const NavTree = createFunctionalComponent(({ tree, url, showCategory }) => { - let treeRef = ref(tree); - let urlRef = ref(url); - - let visibleNode = computable(treeRef, urlRef, (tree, url) => { - let node = - tree && - tree?.find( - (item) => item.url && (url.startsWith(item.url) || item.alternativeUrls?.some((alt) => url.startsWith(alt))) - ); - return node || { children: [] }; - }); - - return ( - -
showCategory && !!visibleNode(data).text}> - visibleNode(data).href} text={(data) => visibleNode(data).text} url="dummy" /> -
- visibleNode(data).children} recordAlias="$group"> -
- - - -
- - - ); -}); diff --git a/misc/components/NavTree.tsx b/misc/components/NavTree.tsx new file mode 100644 index 000000000..233a0e9aa --- /dev/null +++ b/misc/components/NavTree.tsx @@ -0,0 +1,34 @@ +import { Repeater, Link } from "cx/widgets"; +import { bind, createFunctionalComponent, computable } from "cx/ui"; +import { ref } from "cx/hooks"; + +export const NavTree = createFunctionalComponent(({ tree, url, showCategory }: any) => { + let treeRef = ref(tree); + let urlRef = ref(url); + + let visibleNode = computable(treeRef, urlRef, (tree, url) => { + let node = + tree && + url && + tree?.find( + (item) => + item.url && (url.startsWith(item.url) || item.alternativeUrls?.some((alt) => url.startsWith(alt))), + ); + return node || { children: [] }; + }); + + return ( + +
showCategory && !!visibleNode(data).text}> + visibleNode(data).href} text={(data) => visibleNode(data).text} url="dummy" /> +
+ visibleNode(data).children} recordAlias="$group"> +
+ + + +
+ + + ); +}); diff --git a/misc/components/ScrollIntoView.d.ts b/misc/components/ScrollIntoView.d.ts deleted file mode 100644 index 6ffb2bc38..000000000 --- a/misc/components/ScrollIntoView.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as Cx from '../../packages/cx/src/core'; - -export interface ScrollIntoViewProps extends Cx.StyledContainerProps { - selector?: Cx.StringProp -} - -export class ScrollIntoView extends Cx.Widget {} diff --git a/misc/components/ScrollIntoView.js b/misc/components/ScrollIntoView.js deleted file mode 100644 index 9d78b6784..000000000 --- a/misc/components/ScrollIntoView.js +++ /dev/null @@ -1,46 +0,0 @@ -import {Container, VDOM} from "cx/ui"; -import {scrollElementIntoView} from "cx/util"; - -export class ScrollIntoView extends Container { - - declareData(...args) { - super.declareData(...args, { - selector: undefined - }); - } - - render(context, instance, key) { - let {data} = instance; - return - {this.renderChildren(context, instance)} - - } -} - -ScrollIntoView.prototype.styled = true; - -class ScrollIntoViewCmp extends VDOM.Component { - render() { - let {style, className, children} = this.props; - return
this.el = el} style={style} className={className}>{children}
- } - - componentDidMount() { - this.scrollIntoView(); - } - - componentDidUpdate() { - this.scrollIntoView(); - } - - scrollIntoView() { - let child = this.el.querySelector(this.props.selector); - if (child) - scrollElementIntoView(child) - } -} \ No newline at end of file diff --git a/misc/components/ScrollIntoView.tsx b/misc/components/ScrollIntoView.tsx new file mode 100644 index 000000000..915078cf9 --- /dev/null +++ b/misc/components/ScrollIntoView.tsx @@ -0,0 +1,56 @@ +/** @jsxImportSource react */ +import { StyledContainerBase, StyledContainerConfig, VDOM, StringProp, Instance, RenderingContext } from "cx/ui"; +import { scrollElementIntoView } from "cx/util"; + +export interface ScrollIntoViewConfig extends StyledContainerConfig { + selector?: StringProp; +} + +export class ScrollIntoView extends StyledContainerBase { + declareData(...args: any[]) { + super.declareData(...args, { + selector: undefined, + }); + } + + render(context: RenderingContext, instance: Instance, key: string) { + let { data } = instance; + return ( + + {this.renderChildren(context, instance)} + + ); + } +} + +class ScrollIntoViewCmp extends VDOM.Component { + el: HTMLElement | null = null; + + render() { + let { style, className, children } = this.props; + return ( +
{ + this.el = el; + }} + style={style} + className={className} + > + {children} +
+ ); + } + + componentDidMount() { + this.scrollIntoView(); + } + + componentDidUpdate() { + this.scrollIntoView(); + } + + scrollIntoView() { + let child = this.el?.querySelector(this.props.selector); + if (child) scrollElementIntoView(child); + } +} diff --git a/misc/components/ScrollReset.d.ts b/misc/components/ScrollReset.d.ts deleted file mode 100644 index 94d0bef43..000000000 --- a/misc/components/ScrollReset.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as Cx from '../../packages/cx/src/core'; - -export interface ScrollResetProps extends Cx.HtmlElementProps { - trigger?: Cx.StructuredProp -} - -export class ScrollReset extends Cx.Widget {} diff --git a/misc/components/ScrollReset.js b/misc/components/ScrollReset.js deleted file mode 100644 index 75440914e..000000000 --- a/misc/components/ScrollReset.js +++ /dev/null @@ -1,57 +0,0 @@ -import { HtmlElement } from 'cx/widgets'; -import { closest } from 'cx/util'; -import { VDOM } from 'cx/ui'; - -export class ScrollReset extends HtmlElement { - - declareData() { - super.declareData(...arguments, { - trigger: { - structured: true - } - }) - } - - render(context, instance, key) { - return ( - - {this.renderChildren(context, instance)} - - ) - } -} - -class ScrollResetComponent extends VDOM.Component { - - shouldComponentUpdate(props) { - return props.shouldUpdate; - } - - render() { - var {data} = this.props; - return
{ - this.el = el - }} className={data.classNames} style={data.style}> - {this.props.children} -
- } - - componentDidMount() { - this.trigger = this.props.data.trigger; - } - - componentDidUpdate() { - var trigger = this.props.data.trigger; - if (this.trigger != trigger) { - this.trigger = trigger; - var parent = closest(this.el, x => x.scrollTop != 0); - if (parent) - parent.scrollTop = 0; - } - } -} diff --git a/misc/components/ScrollReset.tsx b/misc/components/ScrollReset.tsx new file mode 100644 index 000000000..3eceb8a78 --- /dev/null +++ b/misc/components/ScrollReset.tsx @@ -0,0 +1,65 @@ +/** @jsxImportSource react */ +import { HtmlElement, HtmlElementConfig } from 'cx/widgets'; +import { closest } from 'cx/util'; +import { VDOM } from 'cx/ui'; +import { StructuredProp } from 'cx/ui'; + +export interface ScrollResetConfig extends HtmlElementConfig { + trigger?: StructuredProp; +} + +export class ScrollReset extends HtmlElement { + + declareData() { + super.declareData(...arguments, { + trigger: { + structured: true + } + }) + } + + render(context, instance, key) { + return ( + + {this.renderChildren(context, instance)} + + ) + } +} + +class ScrollResetComponent extends VDOM.Component { + el: HTMLElement | null = null; + trigger: any; + + shouldComponentUpdate(props: any) { + return props.shouldUpdate; + } + + render() { + var {data} = this.props; + return
{ + this.el = el + }} className={data.classNames} style={data.style}> + {this.props.children} +
+ } + + componentDidMount() { + this.trigger = this.props.data.trigger; + } + + componentDidUpdate() { + var trigger = this.props.data.trigger; + if (this.trigger != trigger) { + this.trigger = trigger; + var parent = closest(this.el, (x: HTMLElement) => x.scrollTop != 0); + if (parent) + parent.scrollTop = 0; + } + } +} diff --git a/misc/components/SideDrawer.js b/misc/components/SideDrawer.js deleted file mode 100644 index 449853dc6..000000000 --- a/misc/components/SideDrawer.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Overlay } from 'cx/widgets'; - -export const SideDrawer = ({ out, children }) => ( - - - {children} - - -); diff --git a/misc/components/SideDrawer.tsx b/misc/components/SideDrawer.tsx new file mode 100644 index 000000000..175daec09 --- /dev/null +++ b/misc/components/SideDrawer.tsx @@ -0,0 +1,10 @@ +import { createFunctionalComponent } from "cx/ui"; +import { Overlay } from "cx/widgets"; + +export const SideDrawer = createFunctionalComponent(({ out, children }: any) => ( + + + {children} + + +)); diff --git a/misc/layout/index.js b/misc/layout/index.js deleted file mode 100644 index 33bef7c44..000000000 --- a/misc/layout/index.js +++ /dev/null @@ -1,147 +0,0 @@ -import { PureContainer, Link, ContentPlaceholder } from "cx/widgets"; -import { Animicon } from "../components/Animicon"; -import { SideDrawer } from "../components/SideDrawer"; -import { NavTree } from "../components/NavTree"; - -import Logo from "./cxjs.svg"; -import CodeSandboxIcon from "./CodeSandbox.svg"; -import { GitHubStarCount } from "../components/GitHubStarCount"; -import { computable } from "cx/ui"; -import { isArray } from "cx/util"; - -const TopLinks = ({ topLinks, mod, alternativeLinks, children }) => ( - -
- {Object.keys(topLinks || {}).map((url) => ( - - { - let links = alternativeLinks[url]; - return isArray(links) && links.some((link) => currentUrl.startsWith(link)) ? true : null; - }) - } - /> - - ))} - {children} -
-
-); - -export const MasterLayout = ({ app, children, shadow, navTree, title, topLinks, alternativeLinks }) => ( - - -
-
-
- { - store.update("master.drawer.icon", (shape) => { - switch (shape) { - case "arrow": - return "close"; - - case "close": - return null; - - default: - return "arrow"; - } - }); - }} - /> -
- - - Home - - - Docs - - - Gallery - - - Fiddle - - -
- - - version - - - - CodeSandbox - - - -
-
-
-
-

{title}

- -
-
- - - -
-
- {children} - -
{ - if (e.target.nodeName == "A") store.delete("master.drawer.icon"); - }} - > - - - -
-
- Home -
-
- Docs - - -
-
- - Gallery - - - -
-
- - Fiddle - -
-
-
-
-
-
-); diff --git a/misc/layout/index.tsx b/misc/layout/index.tsx new file mode 100644 index 000000000..6977603a9 --- /dev/null +++ b/misc/layout/index.tsx @@ -0,0 +1,151 @@ +import { PureContainer, Link, ContentPlaceholder } from "cx/widgets"; +import { Animicon } from "../components/Animicon"; +import { SideDrawer } from "../components/SideDrawer"; +import { NavTree } from "../components/NavTree"; + +// @ts-ignore +import Logo from "./cxjs.svg"; +// @ts-ignore +import CodeSandboxIcon from "./CodeSandbox.svg"; +import { GitHubStarCount } from "../components/GitHubStarCount"; +import { bind, computable, createFunctionalComponent, expr } from "cx/ui"; +import { isArray } from "cx/util"; + +const TopLinks = createFunctionalComponent(({ topLinks, mod, alternativeLinks, children }: any) => ( + +
+ {Object.keys(topLinks || {}).map((url) => ( + + { + let links = alternativeLinks[url]; + return isArray(links) && links.some((link) => currentUrl.startsWith(link)) ? true : null; + }) + } + /> + + ))} + {children} +
+
+)); + +export const MasterLayout = createFunctionalComponent( + ({ app, children, shadow, navTree, title, topLinks, alternativeLinks }: any) => ( + + +
+
+
+ { + store.update("master.drawer.icon", (shape) => { + switch (shape) { + case "arrow": + return "close"; + + case "close": + return null; + + default: + return "arrow"; + } + }); + }} + /> +
+ + + Home + + + Docs + + + Gallery + + + Fiddle + + +
+ + + version + + + + CodeSandbox + + + +
+
+
+
+

{title}

+ +
+
+ + + +
+
+ {children} + +
{ + if (e.target.nodeName == "A") store.delete("master.drawer.icon"); + }} + > + + + +
+
+ Home +
+
+ Docs + + +
+
+ + Gallery + + + +
+
+ + Fiddle + +
+
+
+
+
+
+ ), +); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 79b28bdf7..000000000 --- a/package-lock.json +++ /dev/null @@ -1,13010 +0,0 @@ -{ - "name": "cxjs", - "version": "1.1.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "cxjs", - "version": "1.1.1", - "license": "MIT", - "workspaces": [ - "packages/*", - "docs", - "gallery", - "litmus", - "benchmark", - "fiddle" - ], - "dependencies": { - "@types/react": "^18.2.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/plugin-transform-react-jsx": "^7.21.5", - "@babel/preset-env": "^7.21.5", - "@babel/register": "^7.21.0", - "babel-jest": "29.5.0", - "env-test": "^1.0.0", - "mocha": "^10.2.0", - "prettier": "^2.8.8" - } - }, - "benchmark": { - "version": "1.0.0", - "dependencies": { - "casual": "^1.6.2", - "core-js": "^3.30.1", - "cx": "^23.4.1", - "cx-react": "^17.10.0", - "intl": "^1.2.5", - "react": "^18.2.0", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.2.0", - "whatwg-fetch": "^3.6.2" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@types/node": "^18.16.3", - "@types/react": "^18.2.0", - "babel-loader": "^9.1.2", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0", - "babel-preset-cx-env": "^21.1.1", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.1", - "if-loader": "^1.0.2", - "inline-manifest-webpack-plugin": "^4.0.2", - "json-loader": "^0.5.7", - "mini-css-extract-plugin": "^2.7.5", - "modify-babel-preset": "^3.2.1", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "serve": "^14.2.0", - "style-loader": "^3.3.2", - "svg-url-loader": "^8.0.0", - "url-loader": "^4.1.1", - "webpack": "^5.81.0", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cleanup-plugin": "^0.5.1", - "webpack-cli": "^5.0.2", - "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^4.13.3", - "webpack-merge": "^5.8.0" - } - }, - "benchmark/node_modules/cx": { - "version": "23.12.3", - "resolved": "https://registry.npmjs.org/cx/-/cx-23.12.3.tgz", - "integrity": "sha512-S/7k6RVV0OqLRD/eVwppWrYa9RUeBU/2xFYE+BGLSkgaRs7hfnfozRlU7drMX6gg04AnEZum+yBTyXDLeZ//FA==", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "docs": { - "version": "1.0.0", - "dependencies": { - "casual": "^1.6.2", - "core-js": "^3.30.1", - "cx": "^23.4.1", - "cx-react": "^17.10.0", - "illuminate-js": "^1.0.0-alpha.2", - "intl": "^1.2.5", - "marked": "^4.3.0", - "react": "^18.2.0", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.2.0", - "whatwg-fetch": "^3.6.2" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@types/node": "^18.16.3", - "@types/react": "^18.2.0", - "babel-loader": "^9.1.2", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0", - "babel-preset-cx-env": "^21.1.1", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.1", - "if-loader": "^1.0.2", - "inline-manifest-webpack-plugin": "^4.0.2", - "json-loader": "^0.5.7", - "mini-css-extract-plugin": "^2.7.5", - "modify-babel-preset": "^3.2.1", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "style-loader": "^3.3.2", - "svg-url-loader": "^8.0.0", - "url-loader": "^4.1.1", - "webpack": "^5.81.0", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cleanup-plugin": "^0.5.1", - "webpack-cli": "^5.0.2", - "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^4.13.3", - "webpack-merge": "^5.8.0" - } - }, - "docs/node_modules/cx": { - "version": "23.12.3", - "resolved": "https://registry.npmjs.org/cx/-/cx-23.12.3.tgz", - "integrity": "sha512-S/7k6RVV0OqLRD/eVwppWrYa9RUeBU/2xFYE+BGLSkgaRs7hfnfozRlU7drMX6gg04AnEZum+yBTyXDLeZ//FA==", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "fiddle": { - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@babel/runtime": "^7.21.5", - "assert": "^2.0.0", - "buffer": "^6.0.3", - "casual": "^1.6.2", - "codemirror": "^5.45.0", - "core-js": "^3.30.1", - "cx": "^23.4.1", - "cx-react": "^17.4.1", - "deep-equal": "^2.2.1", - "deepmerge": "^4.3.1", - "path-browserify": "^1.0.1", - "prettier": "^2.8.8", - "process": "^0.11.10", - "query-string": "^8.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "regenerator-runtime": "0.13.11", - "whatwg-fetch": "^3.6.2" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@babel/register": "^7.21.0", - "@babel/runtime-corejs2": "^7.21.5", - "@babel/runtime-corejs3": "^7.21.5", - "@types/node": "^18.16.3", - "@types/react": "^18.2.0", - "babel-loader": "^9.1.2", - "babel-plugin-transform-cx-imports": "^21.1.0", - "babel-plugin-transform-cx-jsx": "^21.1.0", - "babel-preset-cx-env": "^21.1.1", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.1", - "if-loader": "^1.0.2", - "inline-manifest-webpack-plugin": "^4.0.2", - "json-loader": "^0.5.7", - "mini-css-extract-plugin": "^2.7.5", - "modify-babel-preset": "^3.2.1", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "style-loader": "^3.3.2", - "svg-url-loader": "^8.0.0", - "url-loader": "^4.1.1", - "webpack": "^5.81.0", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cleanup-plugin": "^0.5.1", - "webpack-cli": "^5.0.2", - "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^4.13.3", - "webpack-md5-hash": "^0.0.6", - "webpack-merge": "^5.8.0" - } - }, - "fiddle/node_modules/codemirror": { - "version": "5.65.13", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.13.tgz", - "integrity": "sha512-SVWEzKXmbHmTQQWaz03Shrh4nybG0wXx2MEu3FO4ezbPW8IbnZEd5iGHGEffSUaitKYa3i+pHpBsSvw8sPHtzg==" - }, - "fiddle/node_modules/cx": { - "version": "23.12.3", - "resolved": "https://registry.npmjs.org/cx/-/cx-23.12.3.tgz", - "integrity": "sha512-S/7k6RVV0OqLRD/eVwppWrYa9RUeBU/2xFYE+BGLSkgaRs7hfnfozRlU7drMX6gg04AnEZum+yBTyXDLeZ//FA==", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "gallery": { - "version": "1.0.0", - "dependencies": { - "casual": "^1.6.2", - "core-js": "^3.30.1", - "cx": "^23.4.1", - "cx-react": "^17.10.0", - "cx-theme-aquamarine": "^18.7.3", - "cx-theme-dark": "^18.7.1", - "cx-theme-frost": "^18.7.1", - "cx-theme-material": "^18.7.0", - "cx-theme-material-dark": "^20.1.0", - "cx-theme-space-blue": "^20.1.0", - "intl": "^1.2.5", - "plural": "^1.1.0", - "react": "^18.2.0", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.2.0", - "whatwg-fetch": "^3.6.2" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@types/node": "^18.16.3", - "@types/react": "^18.2.0", - "babel-loader": "^9.1.2", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0", - "babel-preset-cx-env": "^21.1.1", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.1", - "if-loader": "^1.0.2", - "json-loader": "^0.5.7", - "mini-css-extract-plugin": "^2.7.5", - "modify-babel-preset": "^3.2.1", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "style-loader": "^3.3.2", - "svg-url-loader": "^8.0.0", - "ts-loader": "^9.4.2", - "typescript": "^5.0.4", - "url-loader": "^4.1.1", - "webpack": "^5.81.0", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cli": "^5.0.2", - "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^4.13.3", - "webpack-merge": "^5.8.0" - } - }, - "gallery/node_modules/cx": { - "version": "23.12.3", - "resolved": "https://registry.npmjs.org/cx/-/cx-23.12.3.tgz", - "integrity": "sha512-S/7k6RVV0OqLRD/eVwppWrYa9RUeBU/2xFYE+BGLSkgaRs7hfnfozRlU7drMX6gg04AnEZum+yBTyXDLeZ//FA==", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "litmus": { - "version": "1.0.0", - "dependencies": { - "casual": "^1.6.2", - "core-js": "^3.30.1", - "cx": "^23.4.1", - "cx-immer": "^22.3.0", - "cx-react": "^17.10.0", - "immer": "^10.0.1", - "intl": "^1.2.5", - "react": "^18.2.0", - "react-dev-utils": "^12.0.1", - "react-dom": "^18.2.0", - "whatwg-fetch": "^3.6.2" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/preset-typescript": "^7.21.5", - "@types/node": "^18.16.3", - "@types/react": "^18.2.0", - "babel-loader": "^9.1.2", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0", - "babel-preset-cx-env": "^21.1.1", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.3", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.5.1", - "if-loader": "^1.0.2", - "inline-manifest-webpack-plugin": "^4.0.2", - "json-loader": "^0.5.7", - "mini-css-extract-plugin": "^2.7.5", - "modify-babel-preset": "^3.2.1", - "process": "^0.11.10", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "style-loader": "^3.3.2", - "svg-url-loader": "^8.0.0", - "ts-loader": "^9.4.2", - "typescript": "^5.0.4", - "url-loader": "^4.1.1", - "webpack": "^5.81.0", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cleanup-plugin": "^0.5.1", - "webpack-cli": "^5.0.2", - "webpack-combine-loaders": "^2.0.4", - "webpack-dev-server": "^4.13.3", - "webpack-merge": "^5.8.0" - } - }, - "litmus/node_modules/cx": { - "version": "23.12.3", - "resolved": "https://registry.npmjs.org/cx/-/cx-23.12.3.tgz", - "integrity": "sha512-S/7k6RVV0OqLRD/eVwppWrYa9RUeBU/2xFYE+BGLSkgaRs7hfnfozRlU7drMX6gg04AnEZum+yBTyXDLeZ//FA==", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.21.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.7.tgz", - "integrity": "sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.8.tgz", - "integrity": "sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-compilation-targets": "^7.21.5", - "@babel/helper-module-transforms": "^7.21.5", - "@babel/helpers": "^7.21.5", - "@babel/parser": "^7.21.8", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/@babel/generator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.5.tgz", - "integrity": "sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.21.5.tgz", - "integrity": "sha512-uNrjKztPLkUk7bpCNC0jEKDJzzkvel/W+HguzbN8krA+LPfC1CEobJEvAvGka2A/M+ViOqXdcRL0GqPUJSjx9g==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz", - "integrity": "sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.21.5", - "@babel/helper-validator-option": "^7.21.0", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.8.tgz", - "integrity": "sha512-+THiN8MqiH2AczyuZrnrKL6cAxFRRQDKW9h1YkBvbgKmAm6mwiacig1qT73DHIWMGo40GRnsEfN3LA+E6NtmSw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-member-expression-to-functions": "^7.21.5", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.21.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/helper-split-export-declaration": "^7.18.6", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.8.tgz", - "integrity": "sha512-zGuSdedkFtsFHGbexAvNuipg1hbtitDLo2XE8/uf6Y9sOQV1xsYX/2pNbtedp/X0eU1pIt+kGvaqHCowkRbS5g==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.3.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz", - "integrity": "sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.5.tgz", - "integrity": "sha512-nIcGfgwpH2u4n9GG1HpStW5Ogx7x7ekiFHbjjFRKXbn5zUvqO9ZgotCO4x1aNbKn/x/xOUaXEhyNHCwtFCpxWg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", - "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz", - "integrity": "sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==", - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-simple-access": "^7.21.5", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz", - "integrity": "sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.21.5.tgz", - "integrity": "sha512-/y7vBgsr9Idu4M6MprbOVUfH3vs7tsIfnVWv/Ml2xgwvyH6LTngdfbf5AdsKwkJy4zgy1X/kuNrEKvhhK28Yrg==", - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-member-expression-to-functions": "^7.21.5", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz", - "integrity": "sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz", - "integrity": "sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", - "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", - "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.5.tgz", - "integrity": "sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.5", - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.21.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.8.tgz", - "integrity": "sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==", - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", - "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-proposal-optional-chaining": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-function-bind": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.18.9.tgz", - "integrity": "sha512-9RfxqKkRBCCT0xoBl9AqieCMscJmSAL9HYixGMWH549jUpT9csWWK/HEYZEx9t9iW/PRSXgX95x9bDlgtAJGFA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-function-bind": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", - "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-function-bind": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.18.6.tgz", - "integrity": "sha512-wZN0Aq/AScknI9mKGcR3TpHdASMufFGaeJgc1rhPmLtZ/PniwjePSh8cfh8tXMB3U4kh/3cRKrLjDtedejg8jQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz", - "integrity": "sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz", - "integrity": "sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.21.5.tgz", - "integrity": "sha512-wb1mhwGOCaXHDTcsRYMKF9e5bbMgqwxtqa2Y1ifH96dXJPwbuLX9qHy3clhrxVqgMz7nyNXs8VkxdH8UBcjKqA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", - "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", - "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", - "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.20.7", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.21.5.tgz", - "integrity": "sha512-TR653Ki3pAwxBxUe8srfF3e4Pe3FTA46uaNHYyQwIoM4oWKSoOZiDNyHJ0oIoDIUPSRQbQG7jzgVBX3FPVne1Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/template": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", - "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", - "integrity": "sha512-nYWpjKW/7j/I/mZkGVgHJXh4bA1sfdFnJoOXwJuj4m3Q2EraO/8ZyrkCau9P5tbHQk01RMSt6KYLCsW7730SXQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", - "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.5.tgz", - "integrity": "sha512-OVryBEgKUbtqMoB7eG2rs6UFexJi6Zj6FDXx+esBLPTCxCNxAY9o+8Di7IsUGJ+AVhp5ncK0fxWUBd0/1gPhrQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.21.5", - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/helper-simple-access": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", - "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", - "license": "MIT", - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-identifier": "^7.19.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", - "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.20.5", - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", - "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.21.5.tgz", - "integrity": "sha512-ELdlq61FpoEkHO6gFRpfj0kUgSwQTGoaEU8eMRoS8Dv3v6e7BjEAj5WMtIBRdHUeAioMhKP5HyxNzNnP+heKbA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-module-imports": "^7.21.4", - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/plugin-syntax-jsx": "^7.21.4", - "@babel/types": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.21.5.tgz", - "integrity": "sha512-ZoYBKDb6LyMi5yCsByQ5jmXsHAQDDYeexT1Szvlmui+lADvfSecr5Dxd/PkrTC3pAD182Fcju1VQkB4oCp9M+w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5", - "regenerator-transform": "^0.15.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", - "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", - "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-typescript": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.21.5.tgz", - "integrity": "sha512-LYm/gTOwZqsYohlvFUe/8Tujz75LqqVC2w+2qPHLR+WyWHGCZPN1KBpJCJn+4Bk4gOkQy/IXKIge6az5MqwlOg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.5.tgz", - "integrity": "sha512-wH00QnTTldTbf/IefEVyChtRdw5RJvODT/Vb4Vcxq1AZvtXj6T0YeX0cAcXhI6/BdGuiP3GcNIL4OQbI2DVNxg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.21.5", - "@babel/helper-compilation-targets": "^7.21.5", - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", - "@babel/plugin-proposal-async-generator-functions": "^7.20.7", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.21.0", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.21.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.21.5", - "@babel/plugin-transform-async-to-generator": "^7.20.7", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.21.0", - "@babel/plugin-transform-classes": "^7.21.0", - "@babel/plugin-transform-computed-properties": "^7.21.5", - "@babel/plugin-transform-destructuring": "^7.21.3", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.21.5", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.20.11", - "@babel/plugin-transform-modules-commonjs": "^7.21.5", - "@babel/plugin-transform-modules-systemjs": "^7.20.11", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.21.3", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.21.5", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.20.7", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.21.5", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.21.5", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.5.tgz", - "integrity": "sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.21.5", - "@babel/helper-validator-option": "^7.21.0", - "@babel/plugin-syntax-jsx": "^7.21.4", - "@babel/plugin-transform-modules-commonjs": "^7.21.5", - "@babel/plugin-transform-typescript": "^7.21.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/register": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.21.0.tgz", - "integrity": "sha512-9nKsPmYDi5DidAqJaQooxIhsLJiNMkGr8ypQ8Uic7cIox7UCDsM7HuUGxdGT7mSDTYbqzIdsOWzfBton/YJrMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "find-cache-dir": "^2.0.0", - "make-dir": "^2.1.0", - "pirates": "^4.0.5", - "source-map-support": "^0.5.16" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", - "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs2": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.21.5.tgz", - "integrity": "sha512-S2tysuG+DdRT77RwSQjlQWGdPW+91F3JmVLUxo5Lggu3LmTe57NVpagpxu0UW/mzP++UQY0r38I7wnhav8X4fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-js": "^2.6.12", - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs2/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.21.5.tgz", - "integrity": "sha512-FRqFlFKNazWYykft5zvzuEl1YyTDGsIRrjV9rvxvYkUC7W/ueBng1X68Xd6uRMzAaJ0xMKn08/wem5YS1lpX8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.25.1", - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.5.tgz", - "integrity": "sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.5", - "@babel/helper-environment-visitor": "^7.21.5", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.5", - "@babel/types": "^7.21.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/@babel/types": { - "version": "7.21.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.5.tgz", - "integrity": "sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.21.5", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.25.16" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.4.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "license": "MIT" - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", - "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-multi-entry": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-multi-entry/-/plugin-multi-entry-6.0.0.tgz", - "integrity": "sha512-msBgVncGQwh/ahxeP/rc8MXVZNBOjoVCsBuDk6uqyFzDv/SZN7jksfAsu6DJ2w4r5PaBX3/OXOjVPeCxya2waA==", - "license": "MIT", - "dependencies": { - "@rollup/plugin-virtual": "^3.0.0", - "matched": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-multi-entry/node_modules/matched": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/matched/-/matched-5.0.1.tgz", - "integrity": "sha512-E1fhSTPRyhAlNaNvGXAgZQlq1hL0bgYMTk/6bktVlIhzUnX/SZs7296ACdVeNJE8xFNGSuvd9IpI7vSnmcqLvw==", - "license": "MIT", - "dependencies": { - "glob": "^7.1.6", - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.1.tgz", - "integrity": "sha512-fK8O0IL5+q+GrsMLuACVNk2x21g3yaw+sG2qn16SnUd3IlBsQyvWxLMGHmCmXRMecPjGRSZ/1LmZB4rjQm68og==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz", - "integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.18.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.5.tgz", - "integrity": "sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.3.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.34", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", - "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.11", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", - "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", - "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", - "license": "MIT" - }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "license": "MIT" - }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.2.0", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static/node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", - "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", - "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", - "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", - "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", - "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.5", - "@webassemblyjs/helper-api-error": "1.11.5", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", - "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", - "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", - "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", - "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", - "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", - "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/helper-wasm-section": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5", - "@webassemblyjs/wasm-opt": "1.11.5", - "@webassemblyjs/wasm-parser": "1.11.5", - "@webassemblyjs/wast-printer": "1.11.5" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", - "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/ieee754": "1.11.5", - "@webassemblyjs/leb128": "1.11.5", - "@webassemblyjs/utf8": "1.11.5" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", - "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-buffer": "1.11.5", - "@webassemblyjs/wasm-gen": "1.11.5", - "@webassemblyjs/wasm-parser": "1.11.5" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", - "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@webassemblyjs/helper-api-error": "1.11.5", - "@webassemblyjs/helper-wasm-bytecode": "1.11.5", - "@webassemblyjs/ieee754": "1.11.5", - "@webassemblyjs/leb128": "1.11.5", - "@webassemblyjs/utf8": "1.11.5" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", - "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.11.5", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.1.tgz", - "integrity": "sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.1.tgz", - "integrity": "sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.2.tgz", - "integrity": "sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/@zeit/schemas": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.29.0.tgz", - "integrity": "sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-dynamic-import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", - "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adm-zip": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", - "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", - "license": "MIT", - "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async-array-reduce": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/async-array-reduce/-/async-array-reduce-0.2.1.tgz", - "integrity": "sha512-/ywTADOcaEnwiAnOEi0UB/rAcIq5bTFfCV9euv3jLYFUMmy6KvKccTQUnLlp8Ensmfj43wHSmbGiPqjsZ6RhNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", - "license": "MIT" - }, - "node_modules/babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.5.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", - "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-cache-dir": "^3.3.2", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-loader/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/babel-loader/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", - "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-transform-cx-imports": { - "resolved": "packages/babel-plugin-transform-cx-imports", - "link": true - }, - "node_modules/babel-plugin-transform-cx-jsx": { - "resolved": "packages/babel-plugin-transform-cx-jsx", - "link": true - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-cx-env": { - "resolved": "packages/babel-preset-cx-env", - "link": true - }, - "node_modules/babel-preset-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", - "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/benchmark": { - "resolved": "benchmark", - "link": true - }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/boxen": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", - "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, - "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buble": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/buble/-/buble-0.19.8.tgz", - "integrity": "sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA==", - "license": "MIT", - "dependencies": { - "acorn": "^6.1.1", - "acorn-dynamic-import": "^4.0.0", - "acorn-jsx": "^5.0.1", - "chalk": "^2.4.2", - "magic-string": "^0.25.3", - "minimist": "^1.2.0", - "os-homedir": "^2.0.0", - "regexpu-core": "^4.5.4" - }, - "bin": { - "buble": "bin/buble" - } - }, - "node_modules/buble/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/buble/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/buble/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/buble/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/buble/node_modules/regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/buble/node_modules/regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/buble/node_modules/regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/buble/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001482", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz", - "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "license": "Apache-2.0" - }, - "node_modules/casual": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/casual/-/casual-1.6.2.tgz", - "integrity": "sha512-NQObL800rg32KZ9bBajHbyDjxLXxxuShChQg7A4tbSeG3n1t7VYGOSkzFSI9gkSgOHp+xilEJ7G0L5l6M30KYA==", - "license": "MIT", - "dependencies": { - "mersenne-twister": "^1.0.1", - "moment": "^2.15.2" - } - }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chalk-template/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cli-select/-/cli-select-1.1.2.tgz", - "integrity": "sha512-PSvWb8G0PPmBNDcz/uM2LkZN3Nn5JmhUl465tTfynQAXjKzFpmHbxStM6X/+awKp5DJuAaHMzzMPefT0suGm1w==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^3.2.0" - } - }, - "node_modules/clipboardy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", - "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arch": "^2.2.0", - "execa": "^5.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz", - "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw==", - "license": "MIT" - }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/core-js": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", - "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", - "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.30.1.tgz", - "integrity": "sha512-nXBEVpmUnNRhz83cHd9JRQC52cTMcuXAmR56+9dSMpRdpeA4I1PX6yjmhd71Eyc/wXNsdBdUDIj1QTIeZpU5Tg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/create-cx-app": { - "resolved": "packages/create-cx-app", - "link": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/css-loader": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", - "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.19", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/css-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "license": "MIT" - }, - "node_modules/cx": { - "resolved": "packages/cx", - "link": true - }, - "node_modules/cx-build-tools": { - "resolved": "packages/cx-build-tools", - "link": true - }, - "node_modules/cx-cli": { - "resolved": "packages/cx-cli", - "link": true - }, - "node_modules/cx-immer": { - "resolved": "packages/cx-immer", - "link": true - }, - "node_modules/cx-react": { - "resolved": "packages/cx-react", - "link": true - }, - "node_modules/cx-scss-manifest-webpack-plugin": { - "resolved": "packages/cx-scss-manifest-webpack-plugin", - "link": true - }, - "node_modules/cx-theme-aquamarine": { - "resolved": "packages/cx-theme-aquamarine", - "link": true - }, - "node_modules/cx-theme-dark": { - "resolved": "packages/cx-theme-dark", - "link": true - }, - "node_modules/cx-theme-frost": { - "resolved": "packages/cx-theme-frost", - "link": true - }, - "node_modules/cx-theme-material": { - "resolved": "packages/cx-theme-material", - "link": true - }, - "node_modules/cx-theme-material-dark": { - "resolved": "packages/cx-theme-material-dark", - "link": true - }, - "node_modules/cx-theme-space-blue": { - "resolved": "packages/cx-theme-space-blue", - "link": true - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decode-uri-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", - "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/deep-equal": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", - "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.0", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true, - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", - "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/docs": { - "resolved": "docs", - "link": true - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "license": "MIT" - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.380", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", - "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/env-test/-/env-test-1.0.0.tgz", - "integrity": "sha512-OBA+y3XKFzO363wYmA3BbA3nigmEm5acuGCK7wJ3jsHd8IrJVpNMcCaKpqsLM1N3cgISUSp3K6tNkibGHrx4wQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", - "license": "MIT" - }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fast-url-parser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fiddle": { - "resolved": "fiddle", - "link": true - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", - "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-cache-dir/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-cache-dir/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-cache-dir/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-cache-dir/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "license": "MIT" - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gallery": { - "resolved": "gallery", - "link": true - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", - "license": "MIT", - "dependencies": { - "is-glob": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-glob/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", - "integrity": "sha512-cTUzZ1+NqjGEKjmVgZKLMdiFg3m9MdRXkZW2OEe69WYVi5ONLMmlnSZdXzGGMOq0C8jGDrL6EWyEDDUioHO/pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "webpack": "^5.20.0" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/if-loader": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/if-loader/-/if-loader-1.0.2.tgz", - "integrity": "sha512-ZZqRvkcIbi0COigsjOPaBsuTAXXOxULLCMwERqhv3BCFDb/Fzh1FAu8u7SB9DwdXuTlWeKWdfdLrXkCtzXTFSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/illuminate-js": { - "version": "1.0.0-alpha.2", - "resolved": "https://registry.npmjs.org/illuminate-js/-/illuminate-js-1.0.0-alpha.2.tgz", - "integrity": "sha512-Qjwvjwsu4RXfkpM0mSqOMZlom3KevYC2/JalIzFlslb4gtRcURIN6Ydq9QHlP74l8j/SxhkP4v8VSscQxcULrQ==", - "license": "MIT" - }, - "node_modules/immer": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.1.tgz", - "integrity": "sha512-zg++jJLsKKTwXGeSYIw0HgChSYQGtu0UDTnbKx5aGLYgte4CwTmH9eJDYyQ6FheyUtBe+lQW9FrGxya1G+Dtmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/inline-manifest-webpack-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/inline-manifest-webpack-plugin/-/inline-manifest-webpack-plugin-4.0.2.tgz", - "integrity": "sha512-j1Q0Y7m2GVsTxnOzQ7YzIlfn5Th2Ga6Ivoqme1G0iGZc8m7R3aQY8HfzLW7ew3CwmqdZb/O26mf9Ak2JA7zzKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map-url": "0.4.0" - } - }, - "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/intl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/intl/-/intl-1.2.5.tgz", - "integrity": "sha512-rK0KcPHeBFBcqsErKSpvZnrOmWOj+EmDkyJ57e90YWaQNqbcivcqmKDlHEeNprDWOsKzPsh1BfSpPQdDvclHVw==", - "license": "MIT" - }, - "node_modules/intl-io": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/intl-io/-/intl-io-0.3.0.tgz", - "integrity": "sha512-fsiAEyhgrGLI1B7wB0/hMVxBDlc4IprTmrTibxOuUkHtY6nf/Mg6O0BzRXq2T9B7v2PqMtyrcdGqu5kEUgksMA==", - "license": "MIT" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.12.0", - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-port-reachable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", - "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "license": "MIT" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.5.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" - }, - "node_modules/litmus": { - "resolved": "litmus", - "link": true - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.hasin": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.hasin/-/lodash.hasin-4.5.2.tgz", - "integrity": "sha512-AFAitwTSq1Ka/1J9uBaVxpLBP5OI3INQvkl4wKcgIYxoA0S3aqO1QWXHR9aCcOrWtPFqP7GzlFncZfe0Jz0kNw==", - "license": "MIT" - }, - "node_modules/lodash.isempty": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", - "license": "MIT" - }, - "node_modules/lodash.isnil": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", - "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", - "license": "MIT" - }, - "node_modules/lodash.omitby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", - "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==", - "license": "MIT" - }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/matched": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/matched/-/matched-1.0.2.tgz", - "integrity": "sha512-7ivM1jFZVTOOS77QsR+TtYHH0ecdLclMkqbf5qiJdX2RorqfhsL65QHySPZgDE0ZjHoh+mQUNHTanNXIlzXd0Q==", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "async-array-reduce": "^0.2.1", - "glob": "^7.1.2", - "has-glob": "^1.0.0", - "is-valid-glob": "^1.0.0", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz", - "integrity": "sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==", - "license": "Unlicense", - "dependencies": { - "fs-monkey": "^1.0.3" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/mersenne-twister": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", - "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==", - "license": "MIT" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.5.tgz", - "integrity": "sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdir-p": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mkdir-p/-/mkdir-p-0.0.7.tgz", - "integrity": "sha512-VkWaZNxDgZle/aJAemUAWdyYX7geyuleKYFfRejf/pFKjxBDbWrMAy41ijg5EiI1U00WR9JcvynuDedE/fTxLA==" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/modify-babel-preset": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/modify-babel-preset/-/modify-babel-preset-3.2.1.tgz", - "integrity": "sha512-AjBTxH9FrXpagLv4++2wj6eIPwJq5As+7ZQY0muXv5GAMOCUxO69Kuc4M570ZMuBJgaNMBOEV0d/e544k/Gqtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-relative": "^0.8.7" - } - }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true, - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/os-homedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-2.0.0.tgz", - "integrity": "sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", - "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/plural": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/plural/-/plural-1.1.0.tgz", - "integrity": "sha512-kX459gcOOllXDCPolRRBItCjvWU4bULIg4R3OYRMAp/4mZuBfF/H7b/SBsTJZkyc+GFkJV4ENitITOz45kyzqQ==", - "license": "MIT" - }, - "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz", - "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", - "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.4.1", - "filter-obj": "^5.1.0", - "split-on-first": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-shallow-renderer": { - "version": "16.15.0", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", - "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-test-renderer": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", - "integrity": "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-is": "^18.2.0", - "react-shallow-renderer": "^16.15.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/recursive-readdir-sync": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz", - "integrity": "sha512-QhkBh/V7T3L2m8FrwZEZ/VnSZU35bv7DSy/VlKVfcq10zvwwuxeuDLH7DZYFGHFyXefHchZmsHFLELR7poGjog==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "license": "MIT", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-relative": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", - "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", - "dev": true, - "license": "MIT" - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.3", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.12.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "license": "MIT", - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-dir/node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rollup": { - "version": "3.21.3", - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-babel": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz", - "integrity": "sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "rollup-pluginutils": "^2.8.1" - }, - "peerDependencies": { - "@babel/core": "7 || ^7.0.0-rc.2", - "rollup": ">=0.60.0 <3" - } - }, - "node_modules/rollup-plugin-buble": { - "version": "0.19.8", - "resolved": "https://registry.npmjs.org/rollup-plugin-buble/-/rollup-plugin-buble-0.19.8.tgz", - "integrity": "sha512-8J4zPk2DQdk3rxeZvxgzhHh/rm5nJkjwgcsUYisCQg1QbT5yagW+hehYEW7ZNns/NVbDCTv4JQ7h4fC8qKGOKw==", - "license": "MIT", - "dependencies": { - "buble": "^0.19.8", - "rollup-pluginutils": "^2.3.3" - } - }, - "node_modules/rollup-plugin-multi-entry": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-multi-entry/-/rollup-plugin-multi-entry-2.1.0.tgz", - "integrity": "sha512-YVVsI15uvbxMKdeYS5NXQa5zbVr/DYdDBBwseC80+KAc7mqDUjM6Qe4wl+jFucVw1yvBDZFk0PPSBZqoLq8xUA==", - "license": "MIT", - "dependencies": { - "matched": "^1.0.2" - } - }, - "node_modules/rollup-plugin-prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-prettier/-/rollup-plugin-prettier-3.0.0.tgz", - "integrity": "sha512-E0UqeVX1F+ATrHsXKXIywddjK+iFKOeOGI/drZY/wVq/xfHPjghviIhsFz7I0Wfuzp8jeN+4L7kVwQ/X84mOBw==", - "license": "MIT", - "dependencies": { - "@types/prettier": "^1.0.0 || ^2.0.0", - "diff": "5.1.0", - "lodash.hasin": "4.5.2", - "lodash.isempty": "4.4.0", - "lodash.isnil": "4.0.0", - "lodash.omitby": "4.6.0", - "magic-string": "0.26.7" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "prettier": "^1.0.0 || ^2.0.0", - "rollup": "^1.0.0 || ^2.0.0 || ^3.0.0" - } - }, - "node_modules/rollup-plugin-prettier/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/rollup-plugin-prettier/node_modules/magic-string": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.7.tgz", - "integrity": "sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==", - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "license": "MIT", - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/route-parser": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/route-parser/-/route-parser-0.0.5.tgz", - "integrity": "sha512-nsii+MXoNb7NyF05LP9kaktx6AoBVT/7zUgDnzIb5IoYAvYkbZOAuoLJjVdsyEVxWv0swCxWkKDK4cMva+WDBA==", - "license": "MIT", - "engines": { - "node": ">= 0.9" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sass": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", - "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", - "license": "MIT", - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/sass-loader": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.2.tgz", - "integrity": "sha512-nrIdVAAte3B9icfBiGWvmMhT/D+eCDwnk+yA7VE/76dp/WkHX+i44Q/pfo71NYbwj0Ap+PGsn0ekOuU1WFJ2AA==", - "dev": true, - "license": "MIT", - "dependencies": { - "klona": "^2.0.6", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.0.tgz", - "integrity": "sha512-+HOw/XK1bW8tw5iBilBz/mJLWRzM8XM6MPxL4J/dKzdxq1vfdEWSwhaR7/yS8EJp5wzvP92p1qirysJvnEtjXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zeit/schemas": "2.29.0", - "ajv": "8.11.0", - "arg": "5.0.2", - "boxen": "7.0.0", - "chalk": "5.0.1", - "chalk-template": "0.4.0", - "clipboardy": "3.0.0", - "compression": "1.7.4", - "is-port-reachable": "4.0.0", - "serve-handler": "6.1.5", - "update-check": "1.5.4" - }, - "bin": { - "serve": "build/main.js" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/serve-handler": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz", - "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "fast-url-parser": "1.1.3", - "mime-types": "2.1.18", - "minimatch": "3.1.2", - "path-is-inside": "1.0.2", - "path-to-regexp": "2.2.1", - "range-parser": "1.2.0" - } - }, - "node_modules/serve-handler/node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-handler/node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "~1.33.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-handler/node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/serve/node_modules/chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sirv": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", - "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha512-liJwHPI9x9d9w5WSIjM58MqGmmb7XzNqwdUA3kSBQ4lmDngexlKwawGzK3J1mKXi6+sysoMDlpVyZh9sv5vRfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "license": "MIT" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy-transport/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/spdy-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/spdy/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/spdy/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/split-on-first": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", - "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "license": "MIT", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-loader": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.2.tgz", - "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-url-loader": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-8.0.0.tgz", - "integrity": "sha512-5doSXvl18hY1fGsRLdhWAU5jgzgxJ06/gc/26cpuDnN0xOz1HmmfhkpL29SSrdIvhtxQ1UwGzmk7wTT/l48mKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-loader": "~6.2.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", - "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, - "node_modules/ts-loader": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", - "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-check": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", - "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "file-loader": "*", - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "file-loader": { - "optional": true - } - } - }, - "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/url-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/url-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/url-loader/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/verror/node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/webpack": { - "version": "5.81.0", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.13.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz", - "integrity": "sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^7.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-cleanup-plugin": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/webpack-cleanup-plugin/-/webpack-cleanup-plugin-0.5.1.tgz", - "integrity": "sha512-K+noogbbNOgve6gB+LVqXda6NJhaZozmhoiNGqq+ia70VY8KFtvVkDgbKTS/JpEbdGST5VbdR90xZonMiPldZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.union": "4.6.0", - "minimatch": "3.0.3", - "recursive-readdir-sync": "1.0.6" - } - }, - "node_modules/webpack-cleanup-plugin/node_modules/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/webpack-cli": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.2.tgz", - "integrity": "sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.0.1", - "@webpack-cli/info": "^2.0.1", - "@webpack-cli/serve": "^2.0.2", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-combine-loaders": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/webpack-combine-loaders/-/webpack-combine-loaders-2.0.4.tgz", - "integrity": "sha512-5O5PYVE5tZ3I3uUm3QB7niLEJzLketl8hvAcJwa4YmwNWS/vixfVsqhtUaBciP8J4u/GwIHV52d7kkgZJFvDnw==", - "dev": true, - "license": "ISC", - "dependencies": { - "qs": "^6.5.2" - } - }, - "node_modules/webpack-combine-loaders/node_modules/qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz", - "integrity": "sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-md5-hash": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/webpack-md5-hash/-/webpack-md5-hash-0.0.6.tgz", - "integrity": "sha512-HrQ0AJpeXHRa3IjsgyyEfTx8EqYs5y/4x/WklSYsNDcqBixHzCkrmJV5U+4ks+sx7ycKoIdqWLdyuk913FCS+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "md5": "^2.0.0" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", - "license": "MIT" - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "license": "MIT", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/babel-plugin-transform-cx-imports": { - "version": "21.3.0", - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.21.4" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "mocha": "^10.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "packages/babel-plugin-transform-cx-jsx": { - "version": "21.3.0", - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.21.4" - }, - "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "mocha": "^10.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "packages/babel-preset-cx-env": { - "version": "21.3.0", - "license": "SEE LICENSE.md", - "dependencies": { - "@babel/plugin-proposal-function-bind": "^7.18.9", - "@babel/plugin-transform-react-jsx": "^7.21.5", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0", - "@babel/preset-env": "^7.0.0-0" - } - }, - "packages/create-cx-app": { - "version": "21.7.0", - "license": "MIT", - "dependencies": { - "cx-cli": "21.6.2" - }, - "bin": { - "create-cx-app": "index.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "packages/create-cx-app/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "packages/create-cx-app/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "packages/create-cx-app/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "packages/create-cx-app/node_modules/copy-dir": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-0.3.0.tgz", - "integrity": "sha512-jKk3Uk5MFmeMOZU+m7TFOxCZZnjezWgb6zXIQtO9c/LKDkmyGA0RO5zl3o2ysxilPGHXY0pqurbUNuZXiUPRCg==", - "dependencies": { - "mkdir-p": "~0.0.4" - } - }, - "packages/create-cx-app/node_modules/cx-cli": { - "version": "21.6.2", - "resolved": "https://registry.npmjs.org/cx-cli/-/cx-cli-21.6.2.tgz", - "integrity": "sha512-e4FVMc/po8+G+qWnIpJscKWPaLIrR7jZtakrdN27fvrwGbhbmTTQgF1WLu2LWde41oahopHcGXuibLJbjZm3Jw==", - "dependencies": { - "chalk": "^2.1.0", - "cli-select": "^1.1.2", - "commander": "^2.9.0", - "copy-dir": "^0.3.0", - "request": "^2.88.2", - "unzipper": "^0.10.11" - }, - "bin": { - "cx": "index.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "packages/create-cx-app/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "packages/create-cx-app/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "packages/create-cx-app/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "packages/cx": { - "version": "24.0.1", - "license": "MIT", - "dependencies": { - "intl-io": "^0.3.0", - "route-parser": "^0.0.5" - }, - "devDependencies": { - "react-test-renderer": "^18.2.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" - } - }, - "packages/cx-build-tools": { - "version": "21.1.0", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@rollup/plugin-multi-entry": "^6.0.0", - "babel-preset-cx-env": "^21.1.1", - "prettier": "^2.8.8", - "rollup": "^3.21.3", - "rollup-plugin-babel": "^4.4.0", - "rollup-plugin-buble": "^0.19.8", - "rollup-plugin-multi-entry": "^2.1.0", - "rollup-plugin-prettier": "^3.0.0", - "sass": "^1.62.1" - } - }, - "packages/cx-cli": { - "version": "23.12.1", - "license": "SEE LICENSE.md", - "dependencies": { - "adm-zip": "^0.5.10", - "chalk": "^4.1.2", - "cli-select": "^1.1.2", - "commander": "^10.0.1", - "copy-dir": "^1.3.0", - "request": "^2.88.2" - }, - "bin": { - "cx": "index.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "packages/cx-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/cx-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/cx-immer": { - "version": "22.3.0", - "license": "MIT", - "peerDependencies": { - "immer": "*" - } - }, - "packages/cx-react": { - "version": "17.10.1", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "packages/cx-scss-manifest-webpack-plugin": { - "version": "18.6.0", - "license": "MIT", - "peerDependencies": { - "cx": "*" - } - }, - "packages/cx-theme-aquamarine": { - "version": "18.7.3", - "license": "SEE LICENSE.md" - }, - "packages/cx-theme-dark": { - "version": "18.7.1", - "license": "SEE LICENSE.md" - }, - "packages/cx-theme-frost": { - "version": "18.7.1", - "license": "SEE LICENSE.md" - }, - "packages/cx-theme-material": { - "version": "18.7.0", - "license": "SEE LICENSE.md" - }, - "packages/cx-theme-material-dark": { - "version": "20.1.0", - "license": "SEE LICENSE.md" - }, - "packages/cx-theme-space-blue": { - "version": "20.11.0", - "license": "SEE LICENSE.md" - } - } -} diff --git a/package.json b/package.json index 6a763b42e..b6a3c67f5 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "Framework for enterprise JavaScript applications", "scripts": { "start": "cd docs && webpack-dev-server --config webpack.config.js --open", - "test": "mocha --config ./test/mocha.config.js", + "test": "yarn workspaces foreach -A --include cx --include babel-plugin-transform-cx-jsx --include babel-plugin-transform-cx-imports run test", "docs": "webpack-dev-server --config docs/webpack.config.js --open", - "build": "node packages/cx/build/index.js", + "build": "yarn workspace cx-react run build && yarn workspace cx run compile && yarn workspace cx run build", "build:docs": "webpack --config docs/webpack.config.js", "build:docs:root": "webpack --config docs/webpack.config.js", "measure:docs": "webpack-dev-server --config docs/webpack.config.js --open", @@ -21,6 +21,8 @@ "build:theme:material": "webpack --config themes/material/webpack.config.js", "build:theme:marine": "webpack --config themes/marine/webpack.config.js", "build:theme:playground": "webpack --config themes/playground/webpack.config.js", + "compile:themes": "yarn workspaces foreach -A --include 'cx-theme-*' run compile", + "build:themes": "yarn workspaces foreach -A --include 'cx-theme-*' run build", "gallery": "webpack-dev-server --config gallery/config/webpack.dev.js --open", "build:gallery": "webpack --config gallery/config/webpack.prod.js", "build:gallery:root": "webpack --config gallery/config/webpack.prod.js", @@ -47,7 +49,8 @@ "gallery", "litmus", "benchmark", - "fiddle" + "fiddle", + "ts-minimal" ], "jest": { "transform": { @@ -58,23 +61,23 @@ ] }, "dependencies": { - "@types/react": "^18.3.14", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@rollup/plugin-node-resolve": "^16.0.3", + "@types/react": "^19.2.7", + "react": "^19.2.3", + "react-dom": "^19.2.3" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "@babel/preset-env": "^7.26.0", - "@babel/register": "^7.25.9", - "babel-jest": "29.7.0", + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/preset-env": "^7.28.5", + "@babel/register": "^7.28.3", "env-test": "^1.0.0", - "mocha": "^10.8.2", "prettier": "^3.3.3", - "webpack-dev-server": "^5.1.0" + "webpack-dev-server": "^5.2.2" }, "resolutions": { - "@babel/helper-define-polyfill-provider": "^0.1.5" + "@babel/helper-define-polyfill-provider": "^0.1.5", + "chalk": "^4.1.2" }, "packageManager": "yarn@4.4.1+sha512.f825273d0689cc9ead3259c14998037662f1dcd06912637b21a450e8da7cfeb4b1965bbee73d16927baa1201054126bc385c6f43ff4aa705c8631d26e12460f1" } diff --git a/packages/babel-plugin-transform-cx-imports/index.js b/packages/babel-plugin-transform-cx-imports/index.js index 00709b0c9..6354d703e 100644 --- a/packages/babel-plugin-transform-cx-imports/index.js +++ b/packages/babel-plugin-transform-cx-imports/index.js @@ -1,6 +1,6 @@ "use strict"; -var manifest = require('cx/manifest.js'); +var manifest = require("cx/manifest.js"); // let manifest = { // 'widgets/TextField': { @@ -9,42 +9,44 @@ var manifest = require('cx/manifest.js'); // } // }; -module.exports = function(options, o1) { +module.exports = function (options, o1) { var t = options.types; return { visitor: { ImportDeclaration(path, scope) { - var opts = scope.opts; var src = path.node.source.value; var importScss = opts.sass || opts.scss; - if (src.indexOf("cx/") == 0 && src.indexOf('cx/src/') != 0) { + if (src.startsWith("cx/") && !src.endsWith(".js")) { var remainder = src.substring(3); if (opts.useSrc) { var imports = []; path.node.specifiers.forEach(function (s) { - var expanded = remainder + '/' + s.imported.name; + var expanded = remainder + "/" + s.imported.name; + //console.log("FULL:", expanded); + //console.log("SRC:", src); var srcFile = manifest[expanded]; if (srcFile) { - if (srcFile.js) - imports.push(t.importDeclaration([s], t.stringLiteral('cx/' + srcFile.js))); + if (srcFile.js) { + imports.push(t.importDeclaration([s], t.stringLiteral("cx/" + srcFile.js))); + //console.log("RESOLVED:", srcFile.js); + } if (srcFile.scss && importScss) { - imports.push(t.importDeclaration([], t.stringLiteral('cx/' + srcFile.scss))); + imports.push(t.importDeclaration([], t.stringLiteral("cx/" + srcFile.scss))); } - } - else { - throw new Error(`Cx import ${s.imported.name} not found in package ${remainder}.`) + } else { + throw new Error(`Cx import ${s.imported.name} not found in folder ${remainder}.`); } }); path.replaceWithMultiple(imports); } } - } - } - } + }, + }, + }; }; diff --git a/packages/babel-plugin-transform-cx-imports/package.json b/packages/babel-plugin-transform-cx-imports/package.json index d130d63d6..e8cbcd196 100644 --- a/packages/babel-plugin-transform-cx-imports/package.json +++ b/packages/babel-plugin-transform-cx-imports/package.json @@ -1,17 +1,17 @@ { "name": "babel-plugin-transform-cx-imports", - "version": "24.0.0", + "version": "26.1.0", "description": "Rewrite Cx imports for simplicity and optimal output size.", "repository": "https://github.com/codaxy/cxjs", "author": "Codaxy", "license": "MIT", "main": "index.js", "dependencies": { - "@babel/plugin-syntax-jsx": "^7.25.9" + "@babel/plugin-syntax-jsx": "^7.27.1" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", "mocha": "^10.8.2" }, "peerDependencies": { diff --git a/packages/babel-plugin-transform-cx-imports/test.js b/packages/babel-plugin-transform-cx-imports/test.js index 9d1654f45..947a59669 100644 --- a/packages/babel-plugin-transform-cx-imports/test.js +++ b/packages/babel-plugin-transform-cx-imports/test.js @@ -1,8 +1,8 @@ "use strict"; -var babel = require("@babel/core"), +var babel = require("@babel/core"), plugin = require("./index"), - assert = require('assert'); + assert = require("assert"); function unwrap(code) { return code.substring(0, code.length - 1); @@ -10,69 +10,63 @@ function unwrap(code) { } function lines(code) { - return unwrap(code).split('\n'); + return unwrap(code).split("\n"); } -describe('babel-plugin-transform-cx-imports', function() { - +describe("babel-plugin-transform-cx-imports", function () { it("skips non-cx import", function () { - let code = `import _ from "lodash"`; let output = babel.transform(code, { - plugins: [plugin] + plugins: [plugin], }).code; assert.equal(unwrap(output), 'import _ from "lodash"'); }); it("supports multiple imports", function () { - let code = `import { Widget, VDOM } from "cx/ui"`; let output = babel.transform(code, { - plugins: [plugin] + plugins: [plugin], }).code; assert.equal(unwrap(output), 'import { Widget, VDOM } from "cx/ui"'); }); it("imports Cx widgets from source", function () { - let code = `import {TextField} from "cx/widgets"`; let output = babel.transform(code, { - plugins: [[plugin, { useSrc: true }]] + plugins: [[plugin, { useSrc: true }]], }).code; - assert.equal(unwrap(output), 'import { TextField } from "cx/src/widgets/form/TextField.js"'); + assert.equal(unwrap(output), 'import { TextField } from "cx/widgets/form/TextField.js"'); }); - it("imports scss file if available and option is set", function () { - + it.skip("imports scss file if available and option is set", function () { let code = `import {TextField} from "cx/widgets"`; let output = babel.transform(code, { - plugins: [[plugin, { useSrc: true, scss: true }]] + plugins: [[plugin, { useSrc: true, scss: true }]], }).code; assert.deepEqual(lines(output), [ - 'import { TextField } from "cx/src/widgets/form/TextField.js";', - 'import "cx/src/widgets/form/TextField.scss"' + 'import { TextField } from "cx/widgets/form/TextField.js";', + 'import "cx/widgets/form/TextField.scss"', ]); }); it("imports multiple things from source", function () { - let code = `import { Text, TextField } from "cx/widgets"`; let output = babel.transform(code, { - plugins: [[plugin, { useSrc: true }]] + plugins: [[plugin, { useSrc: true }]], }).code; assert.deepEqual(lines(output), [ - 'import { Text } from "cx/src/ui/Text.js";', - 'import { TextField } from "cx/src/widgets/form/TextField.js"' + 'import { Text } from "cx/ui/Text.js";', + 'import { TextField } from "cx/widgets/form/TextField.js"', ]); }); }); diff --git a/packages/babel-plugin-transform-cx-jsx/package.json b/packages/babel-plugin-transform-cx-jsx/package.json index 9ebcbaa58..b763cb8a0 100644 --- a/packages/babel-plugin-transform-cx-jsx/package.json +++ b/packages/babel-plugin-transform-cx-jsx/package.json @@ -1,17 +1,17 @@ { "name": "babel-plugin-transform-cx-jsx", - "version": "24.0.0", + "version": "26.1.0", "description": "Transpile JSX into Cx config objects", "repository": "https://github.com/codaxy/cxjs", "author": "Codaxy", "license": "MIT", "main": "index.js", "dependencies": { - "@babel/plugin-syntax-jsx": "^7.25.9" + "@babel/plugin-syntax-jsx": "^7.27.1" }, "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", "mocha": "^10.8.2" }, "peerDependencies": { diff --git a/packages/babel-preset-cx-env/package.json b/packages/babel-preset-cx-env/package.json index a26ac2497..aa6714486 100644 --- a/packages/babel-preset-cx-env/package.json +++ b/packages/babel-preset-cx-env/package.json @@ -1,15 +1,15 @@ { "name": "babel-preset-cx-env", - "version": "24.0.0", + "version": "26.1.0", "description": "Babel preset for CxJS apps", "main": "./index.js", "author": "Codaxy", "license": "SEE LICENSE.md", "dependencies": { - "@babel/plugin-proposal-function-bind": "^7.25.9", - "@babel/plugin-transform-react-jsx": "^7.25.9", - "babel-plugin-transform-cx-imports": "^21.3.0", - "babel-plugin-transform-cx-jsx": "^21.3.0" + "@babel/plugin-proposal-function-bind": "^7.27.1", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "babel-plugin-transform-cx-imports": "^26.1.0", + "babel-plugin-transform-cx-jsx": "^26.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0", diff --git a/packages/cx-build-tools/README.md b/packages/cx-build-tools/README.md new file mode 100644 index 000000000..e41fbed03 --- /dev/null +++ b/packages/cx-build-tools/README.md @@ -0,0 +1,197 @@ +# cx-build-tools + +Build tools for creating CxJS themes and component packages. + +## Installation + +```bash +npm install cx-build-tools --save-dev +# or +yarn add cx-build-tools -D +``` + +## Available Tools + +### getPathResolver + +Creates a path resolver function for consistent path references. + +```javascript +const getPathResolver = require("cx-build-tools/getPathResolver"); + +const resolvePath = getPathResolver(__dirname); +const srcPath = resolvePath("src"); +const distPath = resolvePath("dist"); +``` + +### buildJS + +Bundles JavaScript/TypeScript output using Rollup. + +```javascript +const buildJS = require("cx-build-tools/buildJS"); + +await buildJS( + srcPath, // Source directory + distPath, // Output directory + entries, // Entry configurations + paths, // Import path mappings (optional) + externals // External modules (optional) +); +``` + +### buildSCSS + +Compiles SCSS files into CSS. + +```javascript +const buildSCSS = require("cx-build-tools/buildSCSS"); + +await buildSCSS( + ["src/variables.scss", "src/styles.scss"], // Input files + "dist/output.css" // Output file +); +``` + +Supports `~cx/` prefix for importing CxJS styles: +```scss +@import "~cx/widgets/Button"; +``` + +## Building a Theme + +### Project Structure + +``` +cx-theme-mytheme/ +├── src/ +│ ├── index.ts # Theme overrides +│ └── variables.scss # SCSS variables +├── build/ # TypeScript output +├── dist/ # Final output +├── build.js +├── package.json +└── tsconfig.json +``` + +### package.json + +```json +{ + "name": "cx-theme-mytheme", + "version": "1.0.0", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./dist/": "./dist/" + }, + "sideEffects": true, + "scripts": { + "compile": "tsc", + "build": "node build" + }, + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "cx-build-tools": "*", + "typescript": "^5.0.0" + } +} +``` + +### Recommended Scripts + +- **`compile`** - Compiles TypeScript source to the `build/` folder +- **`build`** - Bundles JS and compiles SCSS to the `dist/` folder + +Run in sequence: +```bash +yarn compile && yarn build +``` + +### build.js + +```javascript +const getPathResolver = require("cx-build-tools/getPathResolver"); +const buildJS = require("cx-build-tools/buildJS"); +const buildSCSS = require("cx-build-tools/buildSCSS"); +const fs = require("fs"); + +const theme = getPathResolver(__dirname); +const themeSrc = getPathResolver(theme("src")); +const themeBuild = getPathResolver(theme("build")); + +// Ensure dist folder exists +if (!fs.existsSync(theme("dist"))) { + fs.mkdirSync(theme("dist")); +} + +async function build() { + await Promise.all([ + buildJS( + theme("src"), + theme("dist"), + [{ + name: "index", + options: { input: [themeBuild("index.js")] }, + output: {} + }], + null, + ["cx/ui", "cx/widgets"] + ), + buildSCSS( + [themeSrc("variables.scss")], + theme("dist/theme.css") + ) + ]); +} + +build().catch(console.error); +``` + +### src/index.ts + +```typescript +import { Localization } from "cx/ui"; + +export function applyThemeOverrides() { + Localization.override("cx/widgets/Dropdown", { + arrow: false + }); +} + +applyThemeOverrides(); +``` + +## Building a Component Package + +For packages with custom CxJS components: + +```javascript +const getPathResolver = require("cx-build-tools/getPathResolver"); +const buildJS = require("cx-build-tools/buildJS"); + +const pkg = getPathResolver(__dirname); +const pkgBuild = getPathResolver(pkg("build")); + +async function build() { + await buildJS( + pkg("src"), + pkg("dist"), + [{ + name: "index", + options: { input: [pkgBuild("index.js")] }, + output: {} + }], + null, + ["cx/ui", "cx/widgets", "cx/util"] // Mark cx imports as external + ); +} + +build().catch(console.error); +``` diff --git a/packages/cx-build-tools/babel.config.js b/packages/cx-build-tools/babel.config.js index ffb2a0a99..cc6543894 100644 --- a/packages/cx-build-tools/babel.config.js +++ b/packages/cx-build-tools/babel.config.js @@ -5,10 +5,13 @@ module.exports = { [ "@babel/preset-env", { - loose: true, modules: false, + targets: { + chrome: "79" + } }, ], + "@babel/preset-typescript", ], plugins: [ //"@babel/external-helpers", diff --git a/packages/cx-build-tools/buildJS.js b/packages/cx-build-tools/buildJS.js index f89facfea..81ece4f8e 100644 --- a/packages/cx-build-tools/buildJS.js +++ b/packages/cx-build-tools/buildJS.js @@ -1,7 +1,7 @@ let rollup = require("rollup"), - path = require("path"), fs = require("fs"), babel = require("@rollup/plugin-babel"), + { nodeResolve } = require("@rollup/plugin-node-resolve"), babelConfig = require("./babel.config"), importAlias = require("./importAlias"), manifestRecorder = require("./manifestRecorder"), @@ -10,6 +10,8 @@ let rollup = require("rollup"), module.exports = function build(srcPath, distPath, entries, paths, externals) { let src = getPathResolver(srcPath); + let build = getPathResolver(src("../build")); + if (!fs.existsSync(build("."))) build = null; let dist = getPathResolver(distPath); let manifest = {}; @@ -37,25 +39,35 @@ module.exports = function build(srcPath, distPath, entries, paths, externals) { }, plugins: [], }, - e.options + e.options, ); options.plugins.push( + nodeResolve({ + extensions: [".js", ".jsx", ".ts", ".tsx"], + }), babel({ babelHelpers: "bundled", - presets: babelConfig.presets, - plugins: [...babelConfig.plugins, manifestRecorder(manifest, paths, src("."))], + //presets: babelConfig.presets, + plugins: [ + //...babelConfig.plugins, + manifestRecorder(manifest, paths, (build ?? src)(".")), + ], + extensions: [ + ".js", + //".jsx", ".ts", ".tsx" + ], }), importAlias({ paths: paths, - path: srcPath, //src('./' + e.name + '/') + path: build("."), //src('./' + e.name + '/') }), prettier({ tabWidth: 2, printWidth: 120, useTabs: true, parser: "babel", - }) + }), //buble(), ); diff --git a/packages/cx-build-tools/buildSCSS.js b/packages/cx-build-tools/buildSCSS.js index 082d04298..6c5dc2a81 100644 --- a/packages/cx-build-tools/buildSCSS.js +++ b/packages/cx-build-tools/buildSCSS.js @@ -1,9 +1,12 @@ let fs = require("fs"), + path = require("path"), renderSCSS = require("./renderSCSS"); module.exports = async function buildSCSS(paths, output) { try { let result = await renderSCSS(paths); + let dir = path.dirname(output); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(output, result.css); console.log("CSS", output, `${(result.css.length / 1024).toFixed(1)} kB`); } catch (err) { diff --git a/packages/cx-build-tools/copyFiles.js b/packages/cx-build-tools/copyFiles.js new file mode 100644 index 000000000..0602e113e --- /dev/null +++ b/packages/cx-build-tools/copyFiles.js @@ -0,0 +1,67 @@ +const fs = require("fs"); +const path = require("path"); + +/** + * Syncs files matching a pattern from source to destination directory. + * Copies files from src to dest and removes files in dest that don't exist in src. + * @param {string} srcDir - Source directory path + * @param {string} destDir - Destination directory path + * @param {string|RegExp} pattern - File extension (e.g., '.scss') or RegExp to match + */ +function copyFiles(srcDir, destDir, pattern) { + const matcher = + typeof pattern === "string" ? (name) => name.endsWith(pattern) : (name) => pattern.test(name); + + // Collect all matching files in src + const srcFiles = new Set(); + + function collectSrcFiles(currentSrc, relativePath) { + if (!fs.existsSync(currentSrc)) return; + const entries = fs.readdirSync(currentSrc, { withFileTypes: true }); + for (const entry of entries) { + const entryRelativePath = path.join(relativePath, entry.name); + if (entry.isDirectory()) { + collectSrcFiles(path.join(currentSrc, entry.name), entryRelativePath); + } else if (matcher(entry.name)) { + srcFiles.add(entryRelativePath); + } + } + } + + // Copy files from src to dest + function copyRecursive(currentSrc, currentDest, relativePath) { + if (!fs.existsSync(currentSrc)) return; + const entries = fs.readdirSync(currentSrc, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(currentSrc, entry.name); + const destPath = path.join(currentDest, entry.name); + if (entry.isDirectory()) { + copyRecursive(srcPath, destPath, path.join(relativePath, entry.name)); + } else if (matcher(entry.name)) { + fs.mkdirSync(currentDest, { recursive: true }); + fs.copyFileSync(srcPath, destPath); + } + } + } + + // Remove files in dest that don't exist in src + function removeOrphans(currentDest, relativePath) { + if (!fs.existsSync(currentDest)) return; + const entries = fs.readdirSync(currentDest, { withFileTypes: true }); + for (const entry of entries) { + const destPath = path.join(currentDest, entry.name); + const entryRelativePath = path.join(relativePath, entry.name); + if (entry.isDirectory()) { + removeOrphans(destPath, entryRelativePath); + } else if (matcher(entry.name) && !srcFiles.has(entryRelativePath)) { + fs.unlinkSync(destPath); + } + } + } + + collectSrcFiles(srcDir, ""); + copyRecursive(srcDir, destDir, ""); + removeOrphans(destDir, ""); +} + +module.exports = copyFiles; diff --git a/packages/cx-build-tools/manifestRecorder.js b/packages/cx-build-tools/manifestRecorder.js index 73e253a7b..7023f5864 100644 --- a/packages/cx-build-tools/manifestRecorder.js +++ b/packages/cx-build-tools/manifestRecorder.js @@ -1,49 +1,39 @@ -var fs = require('fs'), - pathResolve = require('./pathResolve'), - fixPathSeparators = require(('./fixPathSeparators')), - p = require('path') - +var fs = require("fs"), + pathResolve = require("./pathResolve"), + fixPathSeparators = require("./fixPathSeparators"), + p = require("path"); module.exports = function (manifest, paths, pkgSrc) { - var imports = {}; return function () { return { visitor: { - ImportDeclaration: function (path, scope) { - path.node.specifiers.forEach(spec=>{ + path.node.specifiers.forEach((spec) => { var fileName = fixPathSeparators(scope.file.opts.filename); var localImports = imports[fileName]; - if (!localImports) - localImports = imports[fileName] = {}; - var resolvedPath = pathResolve( - p.dirname(scope.file.opts.filename), - path.node.source.value - ); - if (!/\.js^/.test(resolvedPath)) - resolvedPath += '.js'; + if (!localImports) localImports = imports[fileName] = {}; + var resolvedPath = pathResolve(p.dirname(scope.file.opts.filename), path.node.source.value); + if (!/\.(js|jsx)^/.test(resolvedPath)) resolvedPath += ".js"; localImports[spec.local.name] = resolvedPath; }); }, ExportNamedDeclaration: function (path, scope) { - let names = []; if (path.node.specifiers) { - path.node.specifiers.forEach(s => { + path.node.specifiers.forEach((s) => { names.push(s.exported.name); }); } if (path.node.declaration) { - if (path.node.declaration.id) - names.push(path.node.declaration.id.name); + if (path.node.declaration.id) names.push(path.node.declaration.id.name); if (path.node.declaration.declarations) { - path.node.declaration.declarations.forEach(decl => { + path.node.declaration.declarations.forEach((decl) => { if (decl.id) { names.push(decl.id.name); } @@ -51,7 +41,7 @@ module.exports = function (manifest, paths, pkgSrc) { } } - names.forEach(name=> { + names.forEach((name) => { let path = fixPathSeparators(scope.file.opts.filename), srcPath = path; @@ -61,26 +51,25 @@ module.exports = function (manifest, paths, pkgSrc) { for (var key in paths) { if (path.indexOf(key) == 0) { - var jsPath = 'src/' + srcPath.substring(pkgSrc.length + 1); + var jsPath = srcPath.substring(pkgSrc.length + 1); - var expName = paths[key].substring(3) + '/' + name; + var expName = paths[key].substring(3) + "/" + name; - if (!manifest[expName]) - manifest[expName] = {}; + if (!manifest[expName]) manifest[expName] = {}; if (!manifest[expName].js) { manifest[expName].js = jsPath; - if (fs.existsSync(srcPath.replace(/\.js$/, '.scss'))) - manifest[expName].scss = jsPath.replace(/\.js$/, '.scss'); + if (fs.existsSync(srcPath.replace(/\.js(x)?$/, ".scss"))) + manifest[expName].scss = jsPath.replace(/\.js(x)?$/, ".scss"); } break; } } }); - } - } - } - } + }, + }, + }; + }; }; diff --git a/packages/cx-build-tools/package.json b/packages/cx-build-tools/package.json index 2e127d886..79b60bd62 100644 --- a/packages/cx-build-tools/package.json +++ b/packages/cx-build-tools/package.json @@ -11,9 +11,11 @@ }, "homepage": "https://github.com/codaxy/cxjs", "dependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-env": "^7.26.0", - "@rollup/plugin-babel": "^6.0.4", + "@babel/core": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", "babel-preset-cx-env": "^24.0.0", "prettier": "^3.3.3", "rollup": "^4.28.1", diff --git a/packages/cx-build-tools/renderSCSS.js b/packages/cx-build-tools/renderSCSS.js index f15e90210..6806e33fd 100644 --- a/packages/cx-build-tools/renderSCSS.js +++ b/packages/cx-build-tools/renderSCSS.js @@ -1,30 +1,39 @@ -var sass = require("sass"), - path = require("path"); +const sass = require("sass"); +const path = require("path"); -function getImport(path) { - return `@import "${path}";`; +function getImport(importPath) { + return `@import "${importPath}";`; } -module.exports = function renderSCSS(paths) { - return new Promise((resolve, reject) => { - let data = paths.map(getImport).join("\n"); - sass.render( - { - data, - importer: function (name, prev, done) { - if (name.indexOf("~cx/") == 0) { - let resolvedFile = path.resolve(__dirname, "../cx/" + name.substring(4) + ".scss"); - return { - file: resolvedFile, - }; - } - return { file: name }; +module.exports = async function renderSCSS(paths) { + try { + const data = paths.map(getImport).join("\n"); + + const result = await sass.compileStringAsync(data, { + importers: [ + { + findFileUrl(url) { + if (url.startsWith("~cx/")) { + const resolvedFile = path.resolve(__dirname, "../cx/" + url.substring(4) + ".scss"); + return new URL(`file://${resolvedFile.replace(/\\/g, "/")}`); + } + // Handle absolute paths (Unix-style starting with /) + if (url.startsWith("/")) { + return new URL(`file://${url}`); + } + // Handle Windows absolute paths (e.g., C:/...) + if (/^[a-zA-Z]:/.test(url)) { + return new URL(`file:///${url}`); + } + return null; + }, }, - }, - function (err, result) { - if (err) reject(err); - else resolve(result); - } - ); - }); + ], + silenceDeprecations: ["import", "global-builtin"], + }); + + return result; + } catch (err) { + throw err; + } }; diff --git a/packages/cx-react/index.js b/packages/cx-react/index.js deleted file mode 100644 index 13142c462..000000000 --- a/packages/cx-react/index.js +++ /dev/null @@ -1,26 +0,0 @@ -var React = require("react"), - { - unstable_batchedUpdates, - render, - findDOMNode, - createPortal, - unstable_renderSubtreeIntoContainer, - hydrate, - } = require("react-dom"), - { createRoot, hydrateRoot } = require("react-dom/client"); - -var vdom = React; -vdom.DOM = { - unstable_batchedUpdates, - render, - findDOMNode, - createPortal, - createRoot, - hydrateRoot, - hydrate, - unstable_renderSubtreeIntoContainer, -}; - -module.exports = { - VDOM: vdom, -}; diff --git a/packages/cx-react/package.json b/packages/cx-react/package.json index 2f25c427b..72e2a9f01 100644 --- a/packages/cx-react/package.json +++ b/packages/cx-react/package.json @@ -1,13 +1,30 @@ { "name": "cx-react", - "version": "24.7.1", + "version": "26.1.0", "description": "React based rendering for CxJS applications.", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "check-types": "tsc --noEmit" + }, "author": "Codaxy", "license": "MIT", "homepage": "https://github.com/codaxy/cxjs", "peerDependencies": { + "@types/react": ">=18", "react": ">=18", "react-dom": ">=18" + }, + "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3" } } diff --git a/packages/cx-react/src/index.ts b/packages/cx-react/src/index.ts new file mode 100644 index 000000000..85cd591ae --- /dev/null +++ b/packages/cx-react/src/index.ts @@ -0,0 +1,25 @@ +import React from "react"; +import { unstable_batchedUpdates, createPortal } from "react-dom"; +import { createRoot, hydrateRoot, type Root } from "react-dom/client"; + +export type { Root }; + +export interface CxVDOM extends Omit { + allowRenderOutputCaching?: boolean; + DOM: { + unstable_batchedUpdates: typeof unstable_batchedUpdates; + createPortal: typeof createPortal; + createRoot: typeof createRoot; + hydrateRoot: typeof hydrateRoot; + }; +} + +const vdom = React as CxVDOM; +vdom.DOM = { + unstable_batchedUpdates, + createPortal, + createRoot, + hydrateRoot, +}; + +export const VDOM = vdom; diff --git a/packages/cx-react/tsconfig.json b/packages/cx-react/tsconfig.json new file mode 100644 index 000000000..0b6f4663d --- /dev/null +++ b/packages/cx-react/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2024", + "jsx": "react-jsx", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist"], + "include": ["src"] +} diff --git a/packages/cx-scss-manifest-webpack-plugin/package.json b/packages/cx-scss-manifest-webpack-plugin/package.json index 9aadbac89..1dac3fe94 100644 --- a/packages/cx-scss-manifest-webpack-plugin/package.json +++ b/packages/cx-scss-manifest-webpack-plugin/package.json @@ -1,19 +1,20 @@ { - "name": "cx-scss-manifest-webpack-plugin", - "version": "18.6.0", - "description": "Webpack plugin that inspects app's source code and generates a manifest file for importing only required elements of CxJS SCSS", - "main": "src/index.js", - "author": "Codaxy", - "license": "MIT", - "bugs": { - "url": "https://github.com/codaxy/cxjs" - }, - "homepage": "https://github.com/codaxy/cxjs", - "peerDependencies": { - "cx": "*" - }, - "repository": { - "type": "git", - "url": "git@github.com:codaxy/cx.git" - } + "name": "cx-scss-manifest-webpack-plugin", + "version": "26.1.0", + "description": "Webpack plugin that inspects app's source code and generates a manifest file for importing only required elements of CxJS SCSS", + "main": "src/index.js", + "author": "Codaxy", + "license": "MIT", + "bugs": { + "url": "https://github.com/codaxy/cxjs" + }, + "homepage": "https://github.com/codaxy/cxjs", + "peerDependencies": { + "cx": "*", + "webpack": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "git@github.com:codaxy/cx.git" + } } diff --git a/packages/cx-scss-manifest-webpack-plugin/src/index.js b/packages/cx-scss-manifest-webpack-plugin/src/index.js index 1160d0fc0..67d0bd61e 100644 --- a/packages/cx-scss-manifest-webpack-plugin/src/index.js +++ b/packages/cx-scss-manifest-webpack-plugin/src/index.js @@ -19,77 +19,72 @@ function CxScssManifestPlugin(options) { } } -let ns1 = /cx[\\\/]src[\\\/](\w*)[\\\/]/; -let ns2 = /cx[\\\/](\w*)[\\\/]/; +let ns1 = /cx[\\\/]build[\\\/](\w*)[\\\/]/; +let ns2 = /cx[\\\/]src[\\\/](\w*)[\\\/]/; +let ns3 = /cx[\\\/](\w*)[\\\/]/; -CxScssManifestPlugin.prototype.apply = function(compiler) { +CxScssManifestPlugin.prototype.apply = function (compiler) { let manifest = this.manifest; let dirty = false; - compiler.hooks.compilation.tap(pluginName, compilation => { - compilation.hooks.additionalAssets.tap(pluginName, () => { - compilation.chunks.forEach(chunk => { - for (const module of chunk.modulesIterable) { - if ( - !module.resource || - module.resource.indexOf("node_modules") !== -1 - ) - return; - - module.dependencies.forEach(dep => { - /* - It would be better to use usedExports but they are missing in webpack 4 - */ - - if ( - !dep.module || - !dep.module.buildMeta || - !dep.module.buildMeta.providedExports || - !dep.module.resource - ) - return; - - let ns = - dep.module.resource.match(ns1) || - dep.module.resource.match(ns2); - if (!ns) return; - - dep.module.buildMeta.providedExports.forEach(exp => { - let cxModule = ns[1] + "/" + exp; - if (!manifest[cxModule] && cxManifest[cxModule]) { - dirty = true; - manifest[cxModule] = true; - } - }); - }); + const write = () => { + let content = "//THIS FILE IS AUTO-GENERATED USING cx-scss-manifest-webpack-plugin\n\n"; + content += "$cx-include-all: false;\n\n"; + + let keys = Object.keys(manifest); + keys.sort(); + + content += "@include cx-widgets(\n"; + content += keys.map((k) => '\t"cx/' + k + '"').join(",\n"); + content += "\n);\n"; + + let previousContent = fs.readFileSync(this.opts.outputPath, "utf8"); + if (content != previousContent) { + console.log("CxJS SCSS manifest update."); + fs.writeFileSync(this.opts.outputPath, content); + return true; + } + return false; + }; + + compiler.hooks.compilation.tap(pluginName, (compilation) => { + compilation.hooks.finishModules.tap(pluginName, (modules) => { + for (const module of modules) { + let moduleResource = getResource(module); + if (!moduleResource || moduleResource.indexOf("node_modules") !== -1) continue; + //console.log('M', moduleResource); + for (let dependency of module.dependencies) { + if (!dependency.name) continue; + let depModule = compilation.moduleGraph.getModule(dependency); + let resource = getResource(depModule); + //console.log(' D', dependency.name, resource); + if (!resource) continue; + let ns = resource.match(ns1) || resource.match(ns2) || resource.match(ns3); + if (!ns) continue; + let cxModule = ns[1] + "/" + dependency.name; + //console.log(' I', cxModule); + if (!manifest[cxModule] && cxManifest[cxModule]) { + dirty = true; + manifest[cxModule] = true; + } } - }); + } + //console.log('MODULES'); }); compilation.hooks.needAdditionalPass.tap(pluginName, () => { - let content = - "//THIS FILE IS AUTO-GENERATED USING cx-scss-manifest-webpack-plugin\n\n"; - content += "$cx-include-all: false;\n\n"; - - let keys = Object.keys(manifest); - keys.sort(); - - content += "@include cx-widgets(\n"; - content += keys.map(k => '\t"cx/' + k + '"').join(",\n"); - content += "\n);\n"; - - if (dirty) { - let previousContent = fs.readFileSync(this.opts.outputPath, "utf8"); - if (content != previousContent) { - console.log("CxJS SCSS manifest update."); - fs.writeFileSync(this.opts.outputPath, content); - } - dirty = false; + //console.log('NEED-EXTRA PASS', dirty); + if (write()) { + //TODO: Figure out how to trigger a new SCSS round } - - return dirty; + return false; }); }); }; +function getResource(module) { + if (!module) return null; + return module.rootModule ? module.rootModule.resource : module.resource; +} + module.exports = CxScssManifestPlugin; diff --git a/packages/cx-theme-aquamarine/build.js b/packages/cx-theme-aquamarine/build.js index 291981356..28e86b7b4 100644 --- a/packages/cx-theme-aquamarine/build.js +++ b/packages/cx-theme-aquamarine/build.js @@ -6,6 +6,7 @@ const getPathResolver = require("cx-build-tools/getPathResolver"), let theme = getPathResolver(resolvePath(__dirname)); let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); async function build() { try { @@ -18,7 +19,7 @@ async function build() { { name: "index", options: { - input: [themeSrc("index.js")] + input: [themeBuild("index.js")] }, output: {} } diff --git a/packages/cx-theme-aquamarine/package.json b/packages/cx-theme-aquamarine/package.json index 3b06256d6..c7a020b5a 100644 --- a/packages/cx-theme-aquamarine/package.json +++ b/packages/cx-theme-aquamarine/package.json @@ -1,17 +1,37 @@ { - "name": "cx-theme-aquamarine", - "version": "18.7.3", - "description": "Aquamarine theme CSS styles for Cx applications", - "main": "./dist/index.js", - "author": "Codaxy", - "license": "SEE LICENSE.md", - "scripts": { - "build": "node build" - }, - "files": [ - "README.md", - "LICENSE.md", - "src", - "dist" - ] + "name": "cx-theme-aquamarine", + "version": "26.1.0", + "description": "Aquamarine theme CSS styles for Cx applications", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, + "author": "Codaxy", + "license": "SEE LICENSE.md", + "scripts": { + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" + }, + "files": [ + "README.md", + "LICENSE.md", + "src", + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-aquamarine/src/index.scss b/packages/cx-theme-aquamarine/src/index.scss index 1f1dc31ea..03f069b9b 100644 --- a/packages/cx-theme-aquamarine/src/index.scss +++ b/packages/cx-theme-aquamarine/src/index.scss @@ -1,9 +1,10 @@ -@import "~cx/src/index"; +@use "sass:map"; +@import "~cx/sass/index"; -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); @if (cx-included("cx/widgets/Section")) { //SECTION @@ -197,13 +198,17 @@ $mod: map-get($cx-besm, mod); &.#{$state}animate { opacity: 0; transform: scale(0.3); - transition: opacity 0.2s, transform 0.2s; + transition: + opacity 0.2s, + transform 0.2s; } &.#{$state}animated { opacity: 1; transform: none; - transition: opacity 0.3s, transform 0.3s; + transition: + opacity 0.3s, + transform 0.3s; } } diff --git a/packages/cx-theme-aquamarine/src/index.js b/packages/cx-theme-aquamarine/src/index.ts similarity index 100% rename from packages/cx-theme-aquamarine/src/index.js rename to packages/cx-theme-aquamarine/src/index.ts diff --git a/packages/cx-theme-aquamarine/src/variables.scss b/packages/cx-theme-aquamarine/src/variables.scss index 0ea487c33..4cbd425a8 100644 --- a/packages/cx-theme-aquamarine/src/variables.scss +++ b/packages/cx-theme-aquamarine/src/variables.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + //THEME SPECIFIC VARIABLES // pick theme color scheme $primary-color-blue: #47ccde !default; //#00BFFF !default;//lighten(#0587DD, 12) !default;// @@ -282,7 +284,7 @@ $cx-default-progressbar-indicator-background-color: rgba($cx-theme-primary-color //list $cx-default-list-item-padding: 8px 12px !default; -@import "~cx/src/variables"; +@import "~cx/sass/variables"; //VARIABLE MAPS //LIST @@ -360,17 +362,16 @@ $cx-button-mods: cx-deep-map-merge( hover: ( // background-color: darken($cx-theme-primary-color, 5), //box-shadow: 0 2px 12px rgba($cx-theme-primary-color, 0.7), - //background: linear-gradient(to top right, darken($cx-theme-primary-color-dark, 4), darken($cx-theme-primary-color, 4)),,,, + //background: linear-gradient(to top right, darken($cx-theme-primary-color-dark, 4), darken($cx-theme-primary-color, 4)),,,, ), focus: ( //background-color: darken($cx-theme-primary-color, 10), - //box-shadow: 0 2px 6px rgba(darken($cx-theme-primary-color, 30), 0.5) ,,,,, + //box-shadow: 0 2px 6px rgba(darken($cx-theme-primary-color, 30), 0.5) ,,,,, ), active: ( background-color: darken($cx-theme-primary-color, 9), box-shadow: none, - background: - linear-gradient( + background: linear-gradient( to top right, darken($cx-theme-primary-color-dark, 8), darken($cx-theme-primary-color, 8) @@ -398,15 +399,14 @@ $cx-button-mods: cx-deep-map-merge( hover: ( //background-color: darken($cx-theme-danger-color, 5), //background: linear-gradient(to top right, darken($cx-theme-danger-color-dark, 6), darken($cx-theme-danger-color, 6)), - //box-shadow: 0 1px 2px rgba($cx-theme-danger-color, 0.4),,,, + //box-shadow: 0 1px 2px rgba($cx-theme-danger-color, 0.4),,,, ), focus: ( background-color: darken($cx-theme-danger-color, 10), ), active: ( background-color: darken($cx-theme-danger-color, 9), - background: - linear-gradient( + background: linear-gradient( to top right, darken($cx-theme-danger-color-dark, 10), darken($cx-theme-danger-color, 10) @@ -427,9 +427,8 @@ $cx-button-mods: cx-deep-map-merge( border-color: transparent, box-shadow: none, ), - hover: - map-merge( - map-get($cx-list-item, hover), + hover: map.merge( + map.get($cx-list-item, hover), ( box-shadow: none, border-color: transparent, @@ -454,9 +453,8 @@ $cx-button-mods: cx-deep-map-merge( border-color: transparent, box-shadow: none, ), - hover: - map-merge( - map-get($cx-list-item, hover), + hover: map.merge( + map.get($cx-list-item, hover), ( box-shadow: none, border-color: transparent, @@ -631,8 +629,7 @@ $cx-tab-mods: cx-deep-map-merge( ), ), // If a tab is inside a dark-colored container, use mod="line-accent". - line-accent: - ( + line-accent: ( default: ( color: rgba(255, 255, 255, 0.7), border-left-color: transparent !important, @@ -716,7 +713,7 @@ $cx-section-mods: cx-deep-map-merge( ); //MENU -$cx-dropdown-styles: map-merge( +$cx-dropdown-styles: map.merge( $cx-dropdown-styles, ( font-size: $cx-default-box-font-size - 1px, @@ -727,7 +724,7 @@ $cx-dropdown-styles: map-merge( ) ); -$cx-menu-state-style-map: map-merge( +$cx-menu-state-style-map: map.merge( $cx-menu-state-style-map, ( default: ( @@ -826,8 +823,8 @@ $cx-grid-header-state-style-map: cx-deep-map-merge( ), hover: ( //color: rgba($cx-default-grid-header-color, 0.45), - background-color: darken($cx-default-grid-header-background-color, 3) - //border-color: $cx-default-grid-header-border-color,,,,, + background-color: darken($cx-default-grid-header-background-color, 3), + //border-color: $cx-default-grid-header-border-color,,,,, ), sorted: ( color: $cx-theme-primary-text-color, @@ -946,8 +943,8 @@ $cx-textarea-state-style-map: cx-deep-map-merge( border-color: $cx-default-input-border-color, ), focus: ( - box-shadow: 0px 1px 2px rgba($cx-theme-primary-color, 0.5) - // background-color: rgba($cx-default-color, 0.015),,,,, + box-shadow: 0px 1px 2px rgba($cx-theme-primary-color, 0.5), + // background-color: rgba($cx-default-color, 0.015),,,,, ), hover: ( // background-color: rgba($cx-default-color, 0.009) diff --git a/packages/cx-theme-aquamarine/tsconfig.json b/packages/cx-theme-aquamarine/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-aquamarine/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-dark/build.js b/packages/cx-theme-dark/build.js index 83afb782c..6c00b5599 100644 --- a/packages/cx-theme-dark/build.js +++ b/packages/cx-theme-dark/build.js @@ -6,6 +6,7 @@ const getPathResolver = require("cx-build-tools/getPathResolver"), let theme = getPathResolver(resolvePath(__dirname)); let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); async function build() { try { @@ -18,7 +19,7 @@ async function build() { { name: "index", options: { - input: [themeSrc("index.js")] + input: [themeBuild("index.js")] }, output: {} } diff --git a/packages/cx-theme-dark/package.json b/packages/cx-theme-dark/package.json index ed1694788..4c8f24208 100644 --- a/packages/cx-theme-dark/package.json +++ b/packages/cx-theme-dark/package.json @@ -1,17 +1,37 @@ { - "name": "cx-theme-dark", - "version": "18.7.1", - "description": "Dark theme CSS styles for Cx applications", - "main": "./dist/index.js", - "author": "Codaxy", - "license": "SEE LICENSE.md", - "scripts": { - "build": "node build" - }, - "files": [ - "README.md", - "LICENSE.md", - "src", - "dist" - ] + "name": "cx-theme-dark", + "version": "26.1.0", + "description": "Dark theme CSS styles for Cx applications", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, + "author": "Codaxy", + "license": "SEE LICENSE.md", + "scripts": { + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" + }, + "files": [ + "README.md", + "LICENSE.md", + "src", + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-dark/src/index.js b/packages/cx-theme-dark/src/index.ts similarity index 100% rename from packages/cx-theme-dark/src/index.js rename to packages/cx-theme-dark/src/index.ts diff --git a/packages/cx-theme-dark/src/widgets.scss b/packages/cx-theme-dark/src/widgets.scss index fd2377e76..3970c282c 100644 --- a/packages/cx-theme-dark/src/widgets.scss +++ b/packages/cx-theme-dark/src/widgets.scss @@ -1,8 +1,9 @@ +@use "sass:map"; -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); @if (cx-included('cx/widgets/Section')) { diff --git a/packages/cx-theme-dark/tsconfig.json b/packages/cx-theme-dark/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-dark/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-frost/build.js b/packages/cx-theme-frost/build.js new file mode 100644 index 000000000..0db66d50f --- /dev/null +++ b/packages/cx-theme-frost/build.js @@ -0,0 +1,68 @@ +const getPathResolver = require("cx-build-tools/getPathResolver"), + resolvePath = getPathResolver(__dirname), + cxSrc = getPathResolver(resolvePath("../cx/src")), + buildJS = require("cx-build-tools/buildJS"), + buildSCSS = require("cx-build-tools/buildSCSS"); + +let theme = getPathResolver(resolvePath(__dirname)); +let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); + +async function build() { + try { + console.log("Building theme..."); + return Promise.all([ + buildJS( + theme("src"), + theme("dist"), + [ + { + name: "index", + options: { + input: [themeBuild("index.js")] + }, + output: {} + } + ], + null, + ["cx/ui", "cx/widgets"] + ), + buildSCSS( + [ + themeSrc("variables.scss"), + resolvePath("../cx-build-tools/reset.scss") + ], + theme("dist/reset.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("widgets/index.scss"), + cxSrc("ui/index.scss") + ], + theme("dist/widgets.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("charts/index.scss") + ], + theme("dist/charts.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("svg/index.scss") + ], + theme("dist/svg.css") + ) + ]); + } catch (err) { + console.error(err); + } +} + +build(); diff --git a/packages/cx-theme-frost/package.json b/packages/cx-theme-frost/package.json index 14bb278bd..d58cccf37 100644 --- a/packages/cx-theme-frost/package.json +++ b/packages/cx-theme-frost/package.json @@ -1,14 +1,37 @@ { - "name": "cx-theme-frost", - "version": "18.7.1", - "description": "Frost theme CSS styles for Cx applications", - "main": "./dist/index.js", - "author": "Codaxy", - "license": "SEE LICENSE.md", - "files": [ - "README.md", - "LICENSE.md", - "src", - "dist" - ] + "name": "cx-theme-frost", + "version": "26.1.0", + "description": "Frost theme CSS styles for Cx applications", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, + "author": "Codaxy", + "license": "SEE LICENSE.md", + "scripts": { + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" + }, + "files": [ + "README.md", + "LICENSE.md", + "src", + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-frost/src/index.scss b/packages/cx-theme-frost/src/index.scss index 50f6608f8..dc5d45b44 100644 --- a/packages/cx-theme-frost/src/index.scss +++ b/packages/cx-theme-frost/src/index.scss @@ -1,13 +1,14 @@ +@use "sass:map"; @import "~cx/src/index"; -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); -@if (cx-included('cx/widgets/Section')) { +@if (cx-included("cx/widgets/Section")) { $section-mods: primary success warning error; - + @each $i in $section-mods { .#{$block}section.#{$mod}#{$i} { .#{$element}section-header { @@ -15,7 +16,7 @@ $mod: map-get($cx-besm, mod); } } } - + .#{$block}section.#{$mod}card.#{$mod}card { box-shadow: $cx-default-section-box-shadow; } @@ -43,7 +44,7 @@ $mod: map-get($cx-besm, mod); } } -@if (cx-included('cx/widgets/DateTimeField')) { +@if (cx-included("cx/widgets/DateTimeField")) { .#{$block}datetimefield .#{$block}calendar { border-color: transparent !important; } @@ -53,26 +54,28 @@ $mod: map-get($cx-besm, mod); } } -@if (cx-included('cx/widgets/MonthField')) { +@if (cx-included("cx/widgets/MonthField")) { .#{$block}monthfield .#{$block}monthpicker { border-color: transparent !important; } } -@if (cx-included('cx/widgets/ColorPicker')) { +@if (cx-included("cx/widgets/ColorPicker")) { .#{$block}colorfield .#{$block}colorpicker { border-color: transparent !important; } } //override for chrome mobile default press effect -.#{$block}button, .#{$block}tab, .#{$block}menu { +.#{$block}button, +.#{$block}tab, +.#{$block}menu { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } //progressbar -@if (cx-included('cx/widgets/ProgressBar')) { +@if (cx-included("cx/widgets/ProgressBar")) { .#{$block}progressbar { overflow: hidden; } -} \ No newline at end of file +} diff --git a/packages/cx-theme-frost/src/index.js b/packages/cx-theme-frost/src/index.ts similarity index 100% rename from packages/cx-theme-frost/src/index.js rename to packages/cx-theme-frost/src/index.ts diff --git a/packages/cx-theme-frost/src/variables.scss b/packages/cx-theme-frost/src/variables.scss index d10f60bec..668e3d0d5 100644 --- a/packages/cx-theme-frost/src/variables.scss +++ b/packages/cx-theme-frost/src/variables.scss @@ -1,3 +1,4 @@ +@use "sass:map"; // GLOBAL VARIABLES $cx-default-color: #373a3c !default; @@ -202,12 +203,12 @@ $cx-window-footer-state-style-map: cx-deep-map-merge($cx-window-footer-state-sty ) )); -$cx-dropdown-styles: map-merge($cx-dropdown-styles, ( +$cx-dropdown-styles: map.merge($cx-dropdown-styles, ( border-width: 2px, border-style: solid )); -$cx-menu-state-style-map: map-merge($cx-menu-state-style-map, ( +$cx-menu-state-style-map: map.merge($cx-menu-state-style-map, ( default: ( ) diff --git a/packages/cx-theme-frost/tsconfig.json b/packages/cx-theme-frost/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-frost/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-material-dark/build.js b/packages/cx-theme-material-dark/build.js new file mode 100644 index 000000000..5b4838fe7 --- /dev/null +++ b/packages/cx-theme-material-dark/build.js @@ -0,0 +1,70 @@ +const getPathResolver = require("cx-build-tools/getPathResolver"), + resolvePath = getPathResolver(__dirname), + cxSrc = getPathResolver(resolvePath("../cx/src")), + buildJS = require("cx-build-tools/buildJS"), + buildSCSS = require("cx-build-tools/buildSCSS"); + +let theme = getPathResolver(resolvePath(__dirname)); +let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); + +async function build() { + try { + console.log("Building theme..."); + return Promise.all([ + buildJS( + theme("src"), + theme("dist"), + [ + { + name: "index", + options: { + input: [themeBuild("index.js")] + }, + output: {} + } + ], + null, + ["cx/ui", "cx/widgets"] + ), + buildSCSS( + [ + themeSrc("variables.scss"), + resolvePath("../cx-build-tools/reset.scss"), + themeSrc("reset.scss") + ], + theme("dist/reset.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("widgets/index.scss"), + cxSrc("ui/index.scss"), + themeSrc("widgets.scss") + ], + theme("dist/widgets.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("charts/index.scss") + ], + theme("dist/charts.css") + ), + buildSCSS( + [ + themeSrc("variables.scss"), + cxSrc("variables.scss"), + cxSrc("svg/index.scss") + ], + theme("dist/svg.css") + ) + ]); + } catch (err) { + console.error(err); + } +} + +build(); diff --git a/packages/cx-theme-material-dark/package.json b/packages/cx-theme-material-dark/package.json index 9a1347f1f..ea363a52d 100644 --- a/packages/cx-theme-material-dark/package.json +++ b/packages/cx-theme-material-dark/package.json @@ -1,17 +1,37 @@ { - "name": "cx-theme-material-dark", - "version": "20.1.0", - "description": "Material Dark theme CSS styles for Cx applications", - "main": "./dist/index.js", - "author": "Codaxy", - "license": "SEE LICENSE.md", - "scripts": { - "build": "node build" - }, - "files": [ - "README.md", - "LICENSE.md", - "src", - "dist" - ] + "name": "cx-theme-material-dark", + "version": "26.1.0", + "description": "Material Dark theme CSS styles for Cx applications", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, + "author": "Codaxy", + "license": "SEE LICENSE.md", + "scripts": { + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" + }, + "files": [ + "README.md", + "LICENSE.md", + "src", + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-material-dark/src/index.js b/packages/cx-theme-material-dark/src/index.js deleted file mode 100644 index 971598029..000000000 --- a/packages/cx-theme-material-dark/src/index.js +++ /dev/null @@ -1,92 +0,0 @@ -import {Localization} from 'cx/ui'; -import {Icon} from 'cx/widgets'; -import {VDOM} from 'cx/ui'; - -export function applyThemeOverrides() { - - Localization.override('cx/widgets/Dropdown', { - arrow: false, - offset: 0, - elementExplode: 0 - }); - - Localization.override('cx/widgets/Window', { - animate: true, - destroyDelay: 200 - }); - - Localization.override('cx/widgets/MenuItem', { - dropdownOptions: { - pad: false - } - }); - - // enable wrapper focus tracking so appropriate css class can be applied to it - Localization.override('cx/widgets/Field', { - trackFocus: true - }); - - // set all MsgBox buttons to flat-primary - Localization.override('cx/widgets/MsgBox', { - buttonMod: "flat-primary", - footerDirection: "row-reverse", - footerJustify: "start" - }); - - // show all borders on all grids - Localization.override('cx/widgets/Grid', { - showBorder: true - }); - - - // material icons added - Icon.registerFactory((name, props) => { - return VDOM.createElement('i', { - ...props, - className: 'material-icons ' + (props.className || '') - }, name) - }); - - Icon.unregister('close'); - Icon.unregister('folder'); - - Icon.register('calendar', props => Icon.render('date_range', props)); - Icon.register('drop-down', props => Icon.render('keyboard_arrow_down', props)); - Icon.register('sort-asc', props => Icon.render('arrow_upward', props)); - Icon.register('folder-open', props => Icon.render('folder_open', props)); - Icon.register('file', props => Icon.render('insert_drive_file', props)); - - // Icon.register('forward', props => { - // return - // - - // - // - // }); -} - -applyThemeOverrides(); - -export function enableMaterialLabelPlacement() { - Localization.override('cx/widgets/Field', { - labelPlacement: "material" - }); - Localization.override('cx/widgets/LabeledContainer', { - labelPlacement: "material" - }); -} - -export function enableMaterialHelpPlacement() { - Localization.override('cx/widgets/Field', { - helpPlacement: "material", - validationMode: "help" - }); -} \ No newline at end of file diff --git a/packages/cx-theme-material-dark/src/index.ts b/packages/cx-theme-material-dark/src/index.ts new file mode 100644 index 000000000..00803996a --- /dev/null +++ b/packages/cx-theme-material-dark/src/index.ts @@ -0,0 +1,92 @@ +import {Localization} from 'cx/ui'; +import {Icon} from 'cx/widgets'; +import {VDOM} from 'cx/ui'; + +export function applyThemeOverrides() { + + Localization.override('cx/widgets/Dropdown', { + arrow: false, + offset: 0, + elementExplode: 0 + }); + + Localization.override('cx/widgets/Window', { + animate: true, + destroyDelay: 200 + }); + + Localization.override('cx/widgets/MenuItem', { + dropdownOptions: { + pad: false + } + }); + + // enable wrapper focus tracking so appropriate css class can be applied to it + Localization.override('cx/widgets/Field', { + trackFocus: true + }); + + // set all MsgBox buttons to flat-primary + Localization.override('cx/widgets/MsgBox', { + buttonMod: "flat-primary", + footerDirection: "row-reverse", + footerJustify: "start" + }); + + // show all borders on all grids + Localization.override('cx/widgets/Grid', { + showBorder: true + }); + + + // material icons added + Icon.registerFactory((name, props) => { + return VDOM.createElement('i', { + ...props, + className: 'material-icons ' + (props.className || '') + }, name) + }); + + Icon.unregister('close'); + Icon.unregister('folder'); + + Icon.register('calendar', (props: Record) => Icon.render('date_range', props)); + Icon.register('drop-down', (props: Record) => Icon.render('keyboard_arrow_down', props)); + Icon.register('sort-asc', (props: Record) => Icon.render('arrow_upward', props)); + Icon.register('folder-open', (props: Record) => Icon.render('folder_open', props)); + Icon.register('file', (props: Record) => Icon.render('insert_drive_file', props)); + + // Icon.register('forward', props => { + // return + // + + // + // + // }); +} + +applyThemeOverrides(); + +export function enableMaterialLabelPlacement() { + Localization.override('cx/widgets/Field', { + labelPlacement: "material" + }); + Localization.override('cx/widgets/LabeledContainer', { + labelPlacement: "material" + }); +} + +export function enableMaterialHelpPlacement() { + Localization.override('cx/widgets/Field', { + helpPlacement: "material", + validationMode: "help" + }); +} \ No newline at end of file diff --git a/packages/cx-theme-material-dark/src/variables.scss b/packages/cx-theme-material-dark/src/variables.scss index 13d33e000..7a4983bc1 100644 --- a/packages/cx-theme-material-dark/src/variables.scss +++ b/packages/cx-theme-material-dark/src/variables.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + //THEME SPECIFIC VARIABLES // pick theme color scheme $primary-color-blue: #8cc8fe !default; @@ -625,7 +627,7 @@ $cx-section-mods: cx-deep-map-merge( ); //MENU -$cx-dropdown-styles: map-merge( +$cx-dropdown-styles: map.merge( $cx-dropdown-styles, ( color: $cx-medium-emphasis, @@ -635,7 +637,7 @@ $cx-dropdown-styles: map-merge( ) ); -$cx-menu-state-style-map: map-merge( +$cx-menu-state-style-map: map.merge( $cx-menu-state-style-map, ( default: ( diff --git a/packages/cx-theme-material-dark/src/widgets.scss b/packages/cx-theme-material-dark/src/widgets.scss index 2d570c666..066bf9673 100644 --- a/packages/cx-theme-material-dark/src/widgets.scss +++ b/packages/cx-theme-material-dark/src/widgets.scss @@ -1,7 +1,9 @@ -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +@use "sass:map"; + +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); //SECTION @if (cx-included("cx/widgets/Section")) { diff --git a/packages/cx-theme-material-dark/tsconfig.json b/packages/cx-theme-material-dark/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-material-dark/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-material/build.js b/packages/cx-theme-material/build.js index 83afb782c..6c00b5599 100644 --- a/packages/cx-theme-material/build.js +++ b/packages/cx-theme-material/build.js @@ -6,6 +6,7 @@ const getPathResolver = require("cx-build-tools/getPathResolver"), let theme = getPathResolver(resolvePath(__dirname)); let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); async function build() { try { @@ -18,7 +19,7 @@ async function build() { { name: "index", options: { - input: [themeSrc("index.js")] + input: [themeBuild("index.js")] }, output: {} } diff --git a/packages/cx-theme-material/package.json b/packages/cx-theme-material/package.json index cab0de20c..4e5ecc41f 100644 --- a/packages/cx-theme-material/package.json +++ b/packages/cx-theme-material/package.json @@ -1,17 +1,37 @@ { - "name": "cx-theme-material", - "version": "18.7.0", - "description": "Material theme CSS styles for Cx applications", - "main": "./dist/index.js", - "author": "Codaxy", - "license": "SEE LICENSE.md", - "scripts": { - "build": "node build" - }, - "files": [ - "README.md", - "LICENSE.md", - "src", - "dist" - ] + "name": "cx-theme-material", + "version": "26.1.0", + "description": "Material theme CSS styles for Cx applications", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, + "author": "Codaxy", + "license": "SEE LICENSE.md", + "scripts": { + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" + }, + "files": [ + "README.md", + "LICENSE.md", + "src", + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-material/src/index.js b/packages/cx-theme-material/src/index.js deleted file mode 100644 index 15a6fa160..000000000 --- a/packages/cx-theme-material/src/index.js +++ /dev/null @@ -1,92 +0,0 @@ -import {Localization} from 'cx/ui'; -import {Icon} from 'cx/widgets'; -import {VDOM} from 'cx/ui'; - -export function applyThemeOverrides() { - - Localization.override('cx/widgets/Dropdown', { - arrow: false, - offset: 0, - elementExplode: 0 - }); - - Localization.override('cx/widgets/Window', { - animate: true, - destroyDelay: 200 - }); - - Localization.override('cx/widgets/MenuItem', { - dropdownOptions: { - pad: false - } - }); - - // enable wrapper focus tracking so appropriate css class can be applied to it - Localization.override('cx/widgets/Field', { - trackFocus: true - }); - - // set all MsgBox buttons to flat-primary - Localization.override('cx/widgets/MsgBox', { - buttonMod: "flat-primary", - footerDirection: "row-reverse", - footerJustify: "start" - }); - - // show all borders on all grids - Localization.override('cx/widgets/Grid', { - showBorder: true - }); - - - // material icons added - Icon.registerFactory((name, props) => { - return VDOM.createElement('i', { - ...props, - className: 'material-icons ' + (props.className || '') - }, name) - }); - - Icon.unregister('close'); - Icon.unregister('folder'); - - Icon.register('calendar', props => Icon.render('date_range', props)); - Icon.register('drop-down', props => Icon.render('keyboard_arrow_down', props)); - Icon.register('sort-asc', props => Icon.render('arrow_upward', props)); - Icon.register('folder-open', props => Icon.render('folder_open', props)); - Icon.register('file', props => Icon.render('insert_drive_file', props)); - - Icon.register('forward', props => { - return - - - - - }); -} - -applyThemeOverrides(); - -export function enableMaterialLabelPlacement() { - Localization.override('cx/widgets/Field', { - labelPlacement: "material" - }); - Localization.override('cx/widgets/LabeledContainer', { - labelPlacement: "material" - }); -} - -export function enableMaterialHelpPlacement() { - Localization.override('cx/widgets/Field', { - helpPlacement: "material", - validationMode: "help" - }); -} \ No newline at end of file diff --git a/packages/cx-theme-material/src/index.tsx b/packages/cx-theme-material/src/index.tsx new file mode 100644 index 000000000..7f8b1d09f --- /dev/null +++ b/packages/cx-theme-material/src/index.tsx @@ -0,0 +1,92 @@ +import {Localization} from 'cx/ui'; +import {Icon} from 'cx/widgets'; +import {VDOM} from 'cx/ui'; + +export function applyThemeOverrides() { + + Localization.override('cx/widgets/Dropdown', { + arrow: false, + offset: 0, + elementExplode: 0 + }); + + Localization.override('cx/widgets/Window', { + animate: true, + destroyDelay: 200 + }); + + Localization.override('cx/widgets/MenuItem', { + dropdownOptions: { + pad: false + } + }); + + // enable wrapper focus tracking so appropriate css class can be applied to it + Localization.override('cx/widgets/Field', { + trackFocus: true + }); + + // set all MsgBox buttons to flat-primary + Localization.override('cx/widgets/MsgBox', { + buttonMod: "flat-primary", + footerDirection: "row-reverse", + footerJustify: "start" + }); + + // show all borders on all grids + Localization.override('cx/widgets/Grid', { + showBorder: true + }); + + + // material icons added + Icon.registerFactory((name, props) => { + return VDOM.createElement('i', { + ...props, + className: 'material-icons ' + (props.className || '') + }, name) + }); + + Icon.unregister('close'); + Icon.unregister('folder'); + + Icon.register('calendar', (props: Record) => Icon.render('date_range', props)); + Icon.register('drop-down', (props: Record) => Icon.render('keyboard_arrow_down', props)); + Icon.register('sort-asc', (props: Record) => Icon.render('arrow_upward', props)); + Icon.register('folder-open', (props: Record) => Icon.render('folder_open', props)); + Icon.register('file', (props: Record) => Icon.render('insert_drive_file', props)); + + Icon.register('forward', (props: Record) => { + return + + + + + }); +} + +applyThemeOverrides(); + +export function enableMaterialLabelPlacement() { + Localization.override('cx/widgets/Field', { + labelPlacement: "material" + }); + Localization.override('cx/widgets/LabeledContainer', { + labelPlacement: "material" + }); +} + +export function enableMaterialHelpPlacement() { + Localization.override('cx/widgets/Field', { + helpPlacement: "material", + validationMode: "help" + }); +} \ No newline at end of file diff --git a/packages/cx-theme-material/src/variables.scss b/packages/cx-theme-material/src/variables.scss index c4659cc61..5629a436d 100644 --- a/packages/cx-theme-material/src/variables.scss +++ b/packages/cx-theme-material/src/variables.scss @@ -1,4 +1,6 @@ /* prettier-ignore */ +@use "sass:map"; + //THEME SPECIFIC VARIABLES // pick theme color scheme $primary-color-blue: #2196f3 !default; @@ -359,8 +361,8 @@ $cx-button-mods: cx-deep-map-merge( box-shadow: none, ), hover: - map-merge( - map-get($cx-list-item, hover), + map.merge( + map.get($cx-list-item, hover), ( box-shadow: none, border-color: transparent, @@ -386,8 +388,8 @@ $cx-button-mods: cx-deep-map-merge( box-shadow: none, ), hover: - map-merge( - map-get($cx-list-item, hover), + map.merge( + map.get($cx-list-item, hover), ( box-shadow: none, border-color: transparent, @@ -659,7 +661,7 @@ $cx-section-mods: cx-deep-map-merge( ); //MENU -$cx-dropdown-styles: map-merge( +$cx-dropdown-styles: map.merge( $cx-dropdown-styles, ( font-size: $cx-default-box-font-size + 1px, @@ -670,7 +672,7 @@ $cx-dropdown-styles: map-merge( ) ); -$cx-menu-state-style-map: map-merge( +$cx-menu-state-style-map: map.merge( $cx-menu-state-style-map, ( default: ( diff --git a/packages/cx-theme-material/src/widgets.scss b/packages/cx-theme-material/src/widgets.scss index d6cce61b6..f658587b6 100644 --- a/packages/cx-theme-material/src/widgets.scss +++ b/packages/cx-theme-material/src/widgets.scss @@ -1,7 +1,9 @@ -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +@use "sass:map"; + +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); //SECTION @if (cx-included("cx/widgets/Section")) { diff --git a/packages/cx-theme-material/tsconfig.json b/packages/cx-theme-material/tsconfig.json new file mode 100644 index 000000000..4cc684f62 --- /dev/null +++ b/packages/cx-theme-material/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "es2024", + "jsx": "react-jsx", + "jsxImportSource": "cx", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"], + "cx/jsx-runtime": ["../cx/jsx-runtime"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-packed-dark/build.js b/packages/cx-theme-packed-dark/build.js index 83afb782c..6c00b5599 100644 --- a/packages/cx-theme-packed-dark/build.js +++ b/packages/cx-theme-packed-dark/build.js @@ -6,6 +6,7 @@ const getPathResolver = require("cx-build-tools/getPathResolver"), let theme = getPathResolver(resolvePath(__dirname)); let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); async function build() { try { @@ -18,7 +19,7 @@ async function build() { { name: "index", options: { - input: [themeSrc("index.js")] + input: [themeBuild("index.js")] }, output: {} } diff --git a/packages/cx-theme-packed-dark/package.json b/packages/cx-theme-packed-dark/package.json index ac6f8afe7..8868b23e4 100644 --- a/packages/cx-theme-packed-dark/package.json +++ b/packages/cx-theme-packed-dark/package.json @@ -1,17 +1,37 @@ { "name": "cx-theme-packed-dark", - "version": "24.4.1", + "version": "26.1.0", "description": "Packed dark theme CSS styles for Cx applications", - "main": "./dist/index.js", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, "author": "Codaxy", "license": "SEE LICENSE.md", "scripts": { - "build": "node build" + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" }, "files": [ "README.md", "LICENSE.md", "src", - "dist" - ] + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-packed-dark/src/index.js b/packages/cx-theme-packed-dark/src/index.ts similarity index 100% rename from packages/cx-theme-packed-dark/src/index.js rename to packages/cx-theme-packed-dark/src/index.ts diff --git a/packages/cx-theme-packed-dark/src/widgets.scss b/packages/cx-theme-packed-dark/src/widgets.scss index bc1437ea0..beff233b8 100644 --- a/packages/cx-theme-packed-dark/src/widgets.scss +++ b/packages/cx-theme-packed-dark/src/widgets.scss @@ -1,7 +1,9 @@ -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +@use "sass:map"; + +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); @if (cx-included("cx/widgets/Section")) { $section-mods: primary success warning error; diff --git a/packages/cx-theme-packed-dark/tsconfig.json b/packages/cx-theme-packed-dark/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-packed-dark/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx-theme-space-blue/build.js b/packages/cx-theme-space-blue/build.js index 291981356..28e86b7b4 100644 --- a/packages/cx-theme-space-blue/build.js +++ b/packages/cx-theme-space-blue/build.js @@ -6,6 +6,7 @@ const getPathResolver = require("cx-build-tools/getPathResolver"), let theme = getPathResolver(resolvePath(__dirname)); let themeSrc = getPathResolver(theme("src")); +let themeBuild = getPathResolver(theme("build")); async function build() { try { @@ -18,7 +19,7 @@ async function build() { { name: "index", options: { - input: [themeSrc("index.js")] + input: [themeBuild("index.js")] }, output: {} } diff --git a/packages/cx-theme-space-blue/package.json b/packages/cx-theme-space-blue/package.json index 5e795e2e8..51f914de6 100644 --- a/packages/cx-theme-space-blue/package.json +++ b/packages/cx-theme-space-blue/package.json @@ -1,17 +1,37 @@ { "name": "cx-theme-space-blue", - "version": "24.5.1", + "version": "26.1.0", "description": "Space Blue theme CSS styles for Cx applications", - "main": "./dist/index.js", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./src/": "./src/", + "./build/": "./build/", + "./dist/": "./dist/" + }, + "sideEffects": true, "author": "Codaxy", "license": "SEE LICENSE.md", "scripts": { - "build": "node build" + "build": "yarn compile && node build", + "compile": "tsc", + "check-types": "tsc --noEmit" }, "files": [ "README.md", "LICENSE.md", "src", - "dist" - ] + "dist", + "build" + ], + "peerDependencies": { + "cx": "*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } } diff --git a/packages/cx-theme-space-blue/src/index.js b/packages/cx-theme-space-blue/src/index.ts similarity index 100% rename from packages/cx-theme-space-blue/src/index.js rename to packages/cx-theme-space-blue/src/index.ts diff --git a/packages/cx-theme-space-blue/src/variables.scss b/packages/cx-theme-space-blue/src/variables.scss index 3efe4ddba..0e54c5566 100644 --- a/packages/cx-theme-space-blue/src/variables.scss +++ b/packages/cx-theme-space-blue/src/variables.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + //THEME SPECIFIC VARIABLES $cx-theme-primary-color: #4dcbac !default; $cx-theme-primary-color-light: lighten($cx-theme-primary-color, 5); @@ -479,7 +481,7 @@ $cx-section-mods: cx-deep-map-merge( ); //MENU -$cx-dropdown-styles: map-merge( +$cx-dropdown-styles: map.merge( $cx-dropdown-styles, ( background-color: $cx-default-background-color-lighter, diff --git a/packages/cx-theme-space-blue/src/widgets.scss b/packages/cx-theme-space-blue/src/widgets.scss index f49e29f7f..3bf172e58 100644 --- a/packages/cx-theme-space-blue/src/widgets.scss +++ b/packages/cx-theme-space-blue/src/widgets.scss @@ -1,14 +1,15 @@ +@use "sass:map"; @import "~cx/src/index"; -$block: map-get($cx-besm, block); -$element: map-get($cx-besm, element); -$state: map-get($cx-besm, state); -$mod: map-get($cx-besm, mod); +$block: map.get($cx-besm, block); +$element: map.get($cx-besm, element); +$state: map.get($cx-besm, state); +$mod: map.get($cx-besm, mod); // SECTION -@if (cx-included('cx/widgets/Section')) { +@if (cx-included("cx/widgets/Section")) { $section-mods: primary success warning error; - + .#{$block}section.#{$mod}card.#{$mod}card { box-shadow: $cx-default-section-box-shadow; } @@ -37,7 +38,7 @@ $mod: map-get($cx-besm, mod); } } -@if (cx-included('cx/widgets/Calendar')) { +@if (cx-included("cx/widgets/Calendar")) { // calendar .#{$block}calendar { .#{$element}calendar-day-names { @@ -45,14 +46,14 @@ $mod: map-get($cx-besm, mod); } &.#{$state}disabled { - background-color: $cx-default-background-color-dark; + background-color: $cx-default-background-color-dark; pointer-events: none; - opacity: .6; + opacity: 0.6; } } } -@if (cx-included('cx/widgets/DateTimeField')) { +@if (cx-included("cx/widgets/DateTimeField")) { .#{$block}datetimefield .#{$block}calendar { border-color: transparent !important; } @@ -62,35 +63,35 @@ $mod: map-get($cx-besm, mod); } } -@if (cx-should-include('cx/widgets/DateTimePicker')) { +@if (cx-should-include("cx/widgets/DateTimePicker")) { .#{$element}wheel-option { border-color: transparent; } } -@if (cx-included('cx/widgets/MonthField')) { +@if (cx-included("cx/widgets/MonthField")) { .#{$block}monthfield .#{$block}monthpicker { border-color: transparent; box-shadow: none; } } - -// Monthpicker -@if (cx-included('cx/widgets/MonthPicker')) { +// Monthpicker +@if (cx-included("cx/widgets/MonthPicker")) { .#{$block}monthpicker { background-color: $cx-default-background-color-dark; - border-color: rgba(255,255,255, .4); - text-transform: uppercase; + border-color: rgba(255, 255, 255, 0.4); + text-transform: uppercase; td { - border-top: 1px solid rgba(255,255,255, .2); + border-top: 1px solid rgba(255, 255, 255, 0.2); } tbody:not(:first-child) { tr:first-child { - th, td { - border-top: 1px solid rgba(255,255,255, .2); + th, + td { + border-top: 1px solid rgba(255, 255, 255, 0.2); } } } @@ -103,25 +104,25 @@ $mod: map-get($cx-besm, mod); background-color: lighten($cx-default-background-color-dark, 5); border-color: #636363; color: $cx-disabled-text; - } - } + } + } } -@if (cx-included('cx/widgets/ColorPicker')) { +@if (cx-included("cx/widgets/ColorPicker")) { .#{$block}colorfield .#{$block}colorpicker { border-color: transparent !important; } } // lookupfield -@if (cx-included('cx/widgets/LookupField')) { +@if (cx-included("cx/widgets/LookupField")) { .#{$element}lookupfield-tag-clear { color: #d0da7a; } } // select -@if (cx-included('cx/widgets/Select')) { +@if (cx-included("cx/widgets/Select")) { .#{$block}select option { color: $cx-default-color; background-color: $cx-default-background-color-lighter; @@ -129,7 +130,9 @@ $mod: map-get($cx-besm, mod); } //override for chrome mobile default press effect -.#{$block}button, .#{$block}tab, .#{$block}menu { +.#{$block}button, +.#{$block}tab, +.#{$block}menu { -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } @@ -139,11 +142,11 @@ $mod: map-get($cx-besm, mod); } //progressbar -@if (cx-included('cx/widgets/ProgressBar')) { +@if (cx-included("cx/widgets/ProgressBar")) { .#{$block}progressbar { margin: 10px 0 0; border-radius: 25px; - box-shadow: inset 0 -1px 1px rgba(255,255,255, .2); + box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.2); .#{$element}progressbar-label { top: -16px; @@ -153,43 +156,41 @@ $mod: map-get($cx-besm, mod); .#{$element}progressbar-indicator { // background-image: linear-gradient(to bottom, #77b6e0, #1e5f8a); border-radius: 25px; - box-shadow: inset 0 -1px 2px rgba(255,255,255, .3); - } + box-shadow: inset 0 -1px 2px rgba(255, 255, 255, 0.3); + } } } //LIST .#{$block}list.#{$state}selectable { &.#{$mod}bordered { - border-color: rgba(#d9dfe3, .3); + border-color: rgba(#d9dfe3, 0.3); - & > .#{$element}list-item:not(:first-child) { - border-top-color: rgba(#d9dfe3, .3); - } + & > .#{$element}list-item:not(:first-child) { + border-top-color: rgba(#d9dfe3, 0.3); + } } } //GRID .#{$element}grid-data { .#{$state}selectable & { - &.#{$state}selected { - background-color: rgba($cx-theme-primary-color, .2); + background-color: rgba($cx-theme-primary-color, 0.2); &.#{$state}cursor { - background-color: rgba($cx-theme-primary-color, .2); + background-color: rgba($cx-theme-primary-color, 0.2); color: #d0da7a; } - } + } } } -.#{$element}grid-group-footer, .#{$element}grid-group-caption { - +.#{$element}grid-group-footer, +.#{$element}grid-group-caption { td { color: #b2dfdb; line-height: $cx-default-grid-data-line-height; - } &:not(:first-child) { @@ -199,7 +200,6 @@ $mod: map-get($cx-besm, mod); } } - .#{$element}grid-group-caption { td { color: $cx-theme-primary-color-light; @@ -247,5 +247,5 @@ $mod: map-get($cx-besm, mod); // switch .cxe-switch-handle:active { - background-color: lighten($cx-default-switch-handle-background-color, 5); -} \ No newline at end of file + background-color: lighten($cx-default-switch-handle-background-color, 5); +} diff --git a/packages/cx-theme-space-blue/tsconfig.json b/packages/cx-theme-space-blue/tsconfig.json new file mode 100644 index 000000000..f883e4b02 --- /dev/null +++ b/packages/cx-theme-space-blue/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2024", + "allowJs": false, + "moduleResolution": "bundler", + "module": "esnext", + "lib": ["dom", "dom.iterable", "ESNext"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "cx/*": ["../cx/build/*"] + }, + "outDir": "./build", + "rootDir": "./src", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": false, + "noImplicitThis": true, + "alwaysStrict": true, + "removeComments": false + }, + "exclude": ["dist", "build"], + "include": ["src"] +} diff --git a/packages/cx/.mocharc.json b/packages/cx/.mocharc.json new file mode 100644 index 000000000..488d9507f --- /dev/null +++ b/packages/cx/.mocharc.json @@ -0,0 +1,5 @@ +{ + "loader": "ts-node/esm", + "extension": ["ts", "tsx"], + "spec": ["src/**/*.spec.*"] +} diff --git a/packages/cx/.npmignore b/packages/cx/.npmignore index 41d35b142..23886d7c1 100644 --- a/packages/cx/.npmignore +++ b/packages/cx/.npmignore @@ -1,2 +1,2 @@ -build +!build !dist \ No newline at end of file diff --git a/packages/cx/build.js b/packages/cx/build.js new file mode 100644 index 000000000..97b125c6f --- /dev/null +++ b/packages/cx/build.js @@ -0,0 +1,133 @@ +const buildJS = require("cx-build-tools/buildJS"), + buildSCSS = require("cx-build-tools/buildSCSS"), + copyFiles = require("cx-build-tools/copyFiles"), + getPathResolver = require("cx-build-tools/getPathResolver"), + fs = require("fs"), + resolvePath = getPathResolver(__dirname), + cxSrc = getPathResolver(resolvePath("./src")), + cxBuild = getPathResolver(resolvePath("./build")); + +const entries = [ + { + name: "util", + options: { + input: cxBuild("util/index.js"), + }, + output: {}, + }, + { + name: "data", + + options: { + input: cxBuild("data/index.js"), + }, + output: {}, + }, + { + name: "ui", + options: { + input: cxBuild("ui/index.js"), + }, + output: {}, + }, + { + name: "widgets", + options: { + input: cxBuild("widgets/index.js"), + }, + output: {}, + }, + { + name: "svg", + options: { + input: cxBuild("svg/index.js"), + }, + output: {}, + }, + { + name: "charts", + options: { + input: cxBuild("charts/index.js"), + }, + output: {}, + }, + { + name: "hooks", + options: { + input: cxBuild("hooks/index.js"), + }, + output: {}, + }, + { + name: "jsx-runtime", + options: { + input: cxBuild("jsx-runtime.js"), + }, + output: {}, + }, +]; + +const externalPaths = { + [cxBuild("./util/")]: "cx/util", + [cxBuild("./data/")]: "cx/data", + [cxBuild("./ui/")]: "cx/ui", + [cxBuild("./widgets")]: "cx/widgets", + [cxBuild("./charts")]: "cx/charts", + [cxBuild("./svg/")]: "cx/svg", + [cxBuild("./hooks/")]: "cx/hooks", + [cxBuild("./jsx-runtime")]: "cx/jsx-runtime", +}; + +(async function buildAll() { + console.log("Building cx..."); + try { + let distPath = resolvePath("./dist"); + if (!fs.existsSync(distPath)) { + fs.mkdirSync(distPath); + } + + // // Copy SCSS files from src to build + // copyFiles(cxSrc("."), cxBuild("."), ".scss"); + + await Promise.all([ + buildJS(resolvePath("./src"), resolvePath("./dist"), entries, externalPaths), + buildSCSS([resolvePath("../cx-build-tools/reset.scss")], resolvePath("./dist/reset.css")), + buildSCSS( + [ + cxSrc("variables.scss"), + resolvePath("../cx-build-tools/divide.scss"), + cxSrc("widgets/index.scss"), + cxSrc("ui/index.scss"), + ], + resolvePath("./dist/widgets.css"), + ), + buildSCSS( + [cxSrc("variables.scss"), resolvePath("../cx-build-tools/divide.scss"), cxSrc("charts/index.scss")], + resolvePath("./dist/charts.css"), + ), + buildSCSS( + [cxSrc("variables.scss"), resolvePath("../cx-build-tools/divide.scss"), cxSrc("svg/index.scss")], + resolvePath("./dist/svg.css"), + ), + ]); + } catch (err) { + console.log("Build error.", err); + } + + // console.log("Building cx-redux..."); + // await build( + // resolvePath("../../cx-redux/src"), + // resolvePath("../../cx-redux/dist"), + // [ + // { + // name: "index", + // options: { + // input: [resolvePath("../../cx-redux/src/index.js")] + // }, + // output: {} + // } + // ], + // null, + // ["redux", "cx/data"] + // ); +})(); diff --git a/packages/cx/build/index.js b/packages/cx/build/index.js deleted file mode 100644 index 4757773d4..000000000 --- a/packages/cx/build/index.js +++ /dev/null @@ -1,120 +0,0 @@ -const buildJS = require("cx-build-tools/buildJS"), - buildSCSS = require("cx-build-tools/buildSCSS"), - getPathResolver = require("cx-build-tools/getPathResolver"), - fs = require("fs"), - resolvePath = getPathResolver(__dirname), - cxSrc = getPathResolver(resolvePath("../src")); - -const entries = [ - { - name: "util", - options: { - input: cxSrc("util/index.js"), - }, - output: {}, - }, - { - name: "data", - - options: { - input: cxSrc("data/index.js"), - }, - output: {}, - }, - { - name: "ui", - options: { - input: cxSrc("ui/index.js"), - }, - output: {}, - }, - { - name: "widgets", - options: { - input: cxSrc("widgets/index.js"), - }, - output: {}, - }, - { - name: "svg", - options: { - input: cxSrc("svg/index.js"), - }, - output: {}, - }, - { - name: "charts", - options: { - input: cxSrc("charts/index.js"), - }, - output: {}, - }, - { - name: "hooks", - options: { - input: cxSrc("hooks/index.js"), - }, - output: {}, - }, -]; - -const externalPaths = { - [cxSrc("./util/")]: "cx/util", - [cxSrc("./data/")]: "cx/data", - [cxSrc("./ui/")]: "cx/ui", - [cxSrc("./widgets")]: "cx/widgets", - [cxSrc("./charts")]: "cx/charts", - [cxSrc("./svg/")]: "cx/svg", - [cxSrc("./hooks/")]: "cx/hooks", -}; - -(async function buildAll() { - console.log("Building cx..."); - try { - let distPath = resolvePath("../dist"); - if (!fs.existsSync(distPath)) { - fs.mkdirSync(distPath); - } - - await Promise.all([ - buildJS(resolvePath("../src"), resolvePath("../dist"), entries, externalPaths), - buildSCSS([resolvePath("../../cx-build-tools/reset.scss")], resolvePath("../dist/reset.css")), - buildSCSS( - [ - cxSrc("variables.scss"), - resolvePath("../../cx-build-tools/divide.scss"), - cxSrc("widgets/index.scss"), - cxSrc("ui/index.scss"), - ], - resolvePath("../dist/widgets.css") - ), - buildSCSS( - [cxSrc("variables.scss"), resolvePath("../../cx-build-tools/divide.scss"), cxSrc("charts/index.scss")], - resolvePath("../dist/charts.css") - ), - buildSCSS( - [cxSrc("variables.scss"), resolvePath("../../cx-build-tools/divide.scss"), cxSrc("svg/index.scss")], - resolvePath("../dist/svg.css") - ), - ]); - } catch (err) { - console.log("Build error.", err); - } - - // console.log("Building cx-redux..."); - // await build( - // resolvePath("../../cx-redux/src"), - // resolvePath("../../cx-redux/dist"), - // [ - // { - // name: "index", - // options: { - // input: [resolvePath("../../cx-redux/src/index.js")] - // }, - // output: {} - // } - // ], - // null, - // ["redux", "cx/data"] - // ); -})(); diff --git a/packages/cx/charts.d.ts b/packages/cx/charts.d.ts deleted file mode 100644 index 0418506a9..000000000 --- a/packages/cx/charts.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src/charts'; \ No newline at end of file diff --git a/packages/cx/charts.js b/packages/cx/charts.js deleted file mode 100644 index 3082fc889..000000000 --- a/packages/cx/charts.js +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/charts'; \ No newline at end of file diff --git a/packages/cx/data.d.ts b/packages/cx/data.d.ts deleted file mode 100644 index f6ef4678a..000000000 --- a/packages/cx/data.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src/data'; \ No newline at end of file diff --git a/packages/cx/data.js b/packages/cx/data.js deleted file mode 100644 index abd622bf6..000000000 --- a/packages/cx/data.js +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/data'; \ No newline at end of file diff --git a/packages/cx/hooks.d.ts b/packages/cx/hooks.d.ts deleted file mode 100644 index 82420a6a1..000000000 --- a/packages/cx/hooks.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './src/hooks'; \ No newline at end of file diff --git a/packages/cx/hooks.js b/packages/cx/hooks.js deleted file mode 100644 index 4b63212a1..000000000 --- a/packages/cx/hooks.js +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/hooks'; \ No newline at end of file diff --git a/packages/cx/index.js b/packages/cx/index.js deleted file mode 100644 index 9dc969551..000000000 --- a/packages/cx/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as util from './util'; -import * as data from './data'; -import * as ui from './ui'; -import * as widgets from './widgets'; -import * as svg from './svg'; -import * as charts from './charts'; -import * as hooks from './hooks'; - -export { - util, - data, - ui, - widgets, - svg, - charts, - hooks -}; diff --git a/packages/cx/locale/de-de.js b/packages/cx/locale/de-de.js deleted file mode 100644 index f80bdcf34..000000000 --- a/packages/cx/locale/de-de.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'de-de'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Dieses Feld wird benötigt.', - validatingText: 'Wird validiert...', - validationExceptionText: 'Bei der Eingabevalidierung ist ein Fehler aufgetreten. Überprüfen Sie das Log für weitere Details.' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Wird geladen...', - queryErrorText: 'Bei der Abfrage der gesuchten Daten ist ein Felhler aufgetreten.', - noResultsText: 'Keine Ergebnisse gefunden.', - minQueryLengthMessageText: 'Geben Sie mindestens {0} Zeichen ein.' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Wählen Sie {0:d} oder vorher.', - maxExclusiveErrorText: 'Wählen Sie ein Datum vor dem {0:d}.', - minValueErrorText: 'Wählen Sie {0:d} oder später.', - minExclusiveErrorText: 'Wählen Sie ein Datum nach dem {0:d}.', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Heute', - startWithMonday: true -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Ungültiges Datum eingegeben.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Geben Sie {0} oder weniger ein.', - maxExclusiveErrorText: 'Geben Sie eine Zahl kleiner als {0} ein.', - minValueErrorText: 'Geben Sie {0} oder mehr ein.', - minExclusiveErrorText: 'Geben Sie eine Zahl größer als {0} ein.', - inputErrorText: 'Ungültige Zahl eingegeben.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'Ungültige Eingabe.', - minLengthValidationErrorText: 'Bitte tragen Sie noch {[{0}-{1}]} Zeichen ein.', - maxLengthValidationErrorText: 'Benutzen Sie {0} oder weniger Zeichen.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'Wird hochgeladen...' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Ja", - noText: "Nein" -}); \ No newline at end of file diff --git a/packages/cx/locale/en-us.js b/packages/cx/locale/en-us.js deleted file mode 100644 index 8e056c09c..000000000 --- a/packages/cx/locale/en-us.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'en-us'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'This field is required.', - validatingText: 'Validation is in progress...', - validationExceptionText: 'Something went wrong during input validation. Check log for more details.' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Loading...', - queryErrorText: 'Error occurred while querying for lookup data.', - noResultsText: 'No results found.', - minQueryLengthMessageText: 'Type in at least {0} character(s).' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Select {0:d} or before.', - maxExclusiveErrorText: 'Select a date before {0:d}.', - minValueErrorText: 'Select {0:d} or later.', - minExclusiveErrorText: 'Select a date after {0:d}.', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Today', - startWithMonday: false -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Invalid date entered.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Enter {0} or less.', - maxExclusiveErrorText: 'Enter a number less than {0}.', - minValueErrorText: 'Enter {0} or more.', - minExclusiveErrorText: 'Enter a number greater than {0}.', - inputErrorText: 'Invalid number entered.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'The entered value is not valid.', - minLengthValidationErrorText: 'Enter {[{0}-{1}]} more character(s).', - maxLengthValidationErrorText: 'Use {0} characters or fewer.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'Upload is in progress.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Yes", - noText: "No" -}); \ No newline at end of file diff --git a/packages/cx/locale/es-es.js b/packages/cx/locale/es-es.js deleted file mode 100644 index d62a8f662..000000000 --- a/packages/cx/locale/es-es.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'es-es'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Este campo es requerido.', - validatingText: 'La validación está en progreso...', - validationExceptionText: 'Algo salió mal durante la validación de entrada. Revise el registro para más detalles.' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Cargando...', - queryErrorText: 'Se produjo un error al consultar los datos de búsqueda.', - noResultsText: 'No se han encontrado resultados.', - minQueryLengthMessageText: 'Escriba al menos {0} caracteres.' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Seleccione {0: d} o antes.', - maxExclusiveErrorText: 'Seleccione una fecha antes {0:d}.', - minValueErrorText: 'Seleccione {0: d} o posterior', - minExclusiveErrorText: 'Seleccione una fecha después de {0: d}.', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Hoy', - startWithMonday: true -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Fecha introducida no es válida.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Ingrese {0} o menos.', - maxExclusiveErrorText: 'Ingrese un número menor que {0}.', - minValueErrorText: 'Ingrese {0} o más.', - minExclusiveErrorText: 'Ingrese un número mayor que {0}.', - inputErrorText: 'Número inválido ingresado.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'El valor ingresado no es válido.', - minLengthValidationErrorText: 'Ingrese {[{0} - {1}]} más caracteres.', - maxLengthValidationErrorText: 'Use {0} caracteres o menos.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'La carga está en progreso.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Sí", - noText: "No" -}); \ No newline at end of file diff --git a/packages/cx/locale/fr-fr.js b/packages/cx/locale/fr-fr.js deleted file mode 100644 index e50611af2..000000000 --- a/packages/cx/locale/fr-fr.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'fr-fr'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Ce champ est requis.', - validatingText: 'La validation est en cours ...', - validationExceptionText: "Une erreur s'est produite lors de la validation des entrées. Consultez le journal pour plus de détails." -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Chargement...', - queryErrorText: "Une erreur s'est produite lors de l'interrogation des données de recherche.", - noResultsText: 'Aucun résultat trouvé.', - minQueryLengthMessageText: 'Tapez au moins {0} caractère (s).' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Sélectionnez {0:d} ou avant.', - maxExclusiveErrorText: 'Sélectionnez une date avant {0:d}.', - minValueErrorText: 'Sélectionnez {0:d} ou plus tard.', - minExclusiveErrorText: 'Sélectionnez une date après {0:d}.', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Aujourd\'hui', - startWithMonday: true -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Date invalide entrée.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Entrez {0} ou moins.', - maxExclusiveErrorText: 'Entrez un nombre inférieur à {0}.', - minValueErrorText: 'Entrez {0} ou plus.', - minExclusiveErrorText: 'Entrez un nombre supérieur à {0}.', - inputErrorText: 'Numéro invalide entré.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: "La valeur entrée n'est pas valide.", - minLengthValidationErrorText: 'Entrez {[{0} - {1}]} plus de caractères.', - maxLengthValidationErrorText: 'Utilisez {0} caractères ou moins.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'Le téléchargement est en cours.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Oui", - noText: "Non" -}); \ No newline at end of file diff --git a/packages/cx/locale/nl-nl.js b/packages/cx/locale/nl-nl.js deleted file mode 100644 index b3042b338..000000000 --- a/packages/cx/locale/nl-nl.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'nl-nl'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Dit veld is verplicht.', - validatingText: 'Validatie is bezig ...', - validationExceptionText: 'Er is een probleem opgetreden bij het valideren van de gegevens. Controleer het logboek voor meer details ' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Bezig met laden ...', - queryErrorText: 'Er is een fout opgetreden bij het weergeven van gegevens.', - noResultsText: 'Geen resultaten gevonden', - minQueryLengthMessageText: 'Voer minimaal {0} tekens in.' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'De geselecteerde datum is later dan de laatst toegestane datum {0: d}', - maxExclusiveErrorText: 'De geselecteerde datum moet vóór {0: d}', - minValueErrorText: 'De geselecteerde datum is eerder dan {0: d}', - minExclusiveErrorText: 'De geselecteerde datum moet later zijn dan {0: d}', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Vandaag', - startWithMonday: true -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Ongeldige datum.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Voer {0} of minder in.', - maxExclusiveErrorText: 'Voer een nummer in dat kleiner is dan {0}.', - minValueErrorText: 'Voer {0} of meer in.', - minExclusiveErrorText: 'Voer een getal in dat groter is dan {0}.', - inputErrorText: 'Ongeldig nummer.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'De ingevoerde waarde is ongeldig.', - minLengthValidationErrorText: 'Vul {[{0} - {1}]} extra karakters in.', - maxLengthValidationErrorText: 'Gebruik {0} tekens of minder.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'Upload is bezig.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Ja", - noText: "Nee" -}); \ No newline at end of file diff --git a/packages/cx/locale/pt-pt.js b/packages/cx/locale/pt-pt.js deleted file mode 100644 index 4c444ce96..000000000 --- a/packages/cx/locale/pt-pt.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'pt-pt'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Este campo é obrigatório.', - validatingText: 'A validação está em andamento ...', - validationExceptionText: 'Algo deu errado durante a validação de entrada. Verifique o log para mais detalhes.' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Carregando...', - queryErrorText: 'Ocorreu um erro ao consultar os dados de pesquisa.', - noResultsText: 'Nenhum resultado encontrado.', - minQueryLengthMessageText: 'Digite pelo menos {0} caractere(s).' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Selecione {0:d} ou antes.', - maxExclusiveErrorText: 'Selecione uma data antes de {0:d}.', - minValueErrorText: 'Selecione {0:d} ou posterior.', - minExclusiveErrorText: 'Selecione uma data após {0:d}.', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Hoje', - startWithMonday: false -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Data inválida inserida.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Digite {0} ou menos.', - maxExclusiveErrorText: 'Digite um número menor que {0}.', - minValueErrorText: 'Digite {0} ou mais.', - minExclusiveErrorText: 'Digite um número maior que {0}.', - inputErrorText: 'Número inválido digitado.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'O valor inserido não é válido.', - minLengthValidationErrorText: 'Digite {[{0}-{1}]} mais caractere(s).', - maxLengthValidationErrorText: 'Use {0} caracteres ou menos.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'O upload está em andamento.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Sim", - noText: "Não" -}); \ No newline at end of file diff --git a/packages/cx/locale/sr-latn-ba.js b/packages/cx/locale/sr-latn-ba.js deleted file mode 100644 index 3c61f12b8..000000000 --- a/packages/cx/locale/sr-latn-ba.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Localization } from 'cx/ui'; - -var c = 'sr-latn-ba'; - -// Field -Localization.localize(c, 'cx/widgets/Field', { - requiredText: 'Ovo polje je obavezno.', - validatingText: 'Validacija je u toku...', - validationExceptionText: 'Došlo je do problema prilikom validacije podataka. Provjerite log za više detalja.' -}); - -// LookupField -Localization.localize(c, 'cx/widgets/LookupField', { - loadingText: 'Učitavanje...', - queryErrorText: 'Došlo je do greške kod pribavljanja podataka za prikaz.', - noResultsText: 'Rezultati nisu pronađeni.', - minQueryLengthMessageText: 'Unesite najmanje {0} karakter(a).' -}); - -// In common for Calendar and MonthPicker -const calendarErrorMessages = { - maxValueErrorText: 'Izabrani datum je kasniji od posljednjeg dozvoljenog datuma {0:d}', - maxExclusiveErrorText: 'Izabrani datum bi trebao biti prije {0:d}', - minValueErrorText: 'Izabrani datum je raniji od {0:d}', - minExclusiveErrorText: 'Izabrani datum bi trebao biti kasniji od {0:d}', -}; - -// Calendar -Localization.localize(c, 'cx/widgets/Calendar', { - ...calendarErrorMessages, - todayButtonText: 'Danas', - startWithMonday: true -}); - -// MonthPicker -Localization.localize(c, 'cx/widgets/MonthPicker', calendarErrorMessages); - -// In common for DateField and MonthField -const dateFieldErrorMessages = { - ...calendarErrorMessages, - inputErrorText: 'Neispravan datum.' -}; - -// MonthField -Localization.localize(c, 'cx/widgets/MonthField', dateFieldErrorMessages); - -// DateField -Localization.localize(c, 'cx/widgets/DateField', dateFieldErrorMessages); - -// NumberField -Localization.localize(c, 'cx/widgets/NumberField', { - maxValueErrorText: 'Unesite {0} ili manje.', - maxExclusiveErrorText: 'Unesite broj manji od {0}.', - minValueErrorText: 'Unesite {0} ili više.', - minExclusiveErrorText: 'Unesite broj veći od {0}.', - inputErrorText: 'Neispravan broj.' -}); - -// TextField -Localization.localize(c, 'cx/widgets/TextField', { - validationErrorText: 'Unesena vrijednost nije validna.', - minLengthValidationErrorText: 'Unesite {[{0}-{1}]} dodatnih karaktera.', - maxLengthValidationErrorText: 'Koristite {0} karaktera ili manje.' -}); - -// UploadButton -Localization.localize(c, 'cx/widgets/UploadButton', { - validationErrorText: 'Otpremanje je u toku.' -}); - -// MsgBox -Localization.localize(c, 'cx/widgets/MsgBox', { - yesText: "Da", - noText: "Ne" -}); \ No newline at end of file diff --git a/packages/cx/manifest.js b/packages/cx/manifest.js deleted file mode 100644 index 305e31b61..000000000 --- a/packages/cx/manifest.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/manifest'); \ No newline at end of file diff --git a/packages/cx/package.json b/packages/cx/package.json index 04329f478..594e48936 100644 --- a/packages/cx/package.json +++ b/packages/cx/package.json @@ -1,11 +1,65 @@ { "name": "cx", - "version": "25.11.1", + "version": "26.1.0", "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.", - "main": "index.js", - "jsnext:main": "src/index.js", + "exports": { + "./data": { + "types": "./build/data/index.d.ts", + "default": "./build/data/index.js" + }, + "./data/*": "./build/data/*", + "./util": { + "types": "./build/util/index.d.ts", + "default": "./build/util/index.js" + }, + "./util/*": "./build/util/*", + "./widgets": { + "types": "./build/widgets/index.d.ts", + "default": "./build/widgets/index.js" + }, + "./widgets/*": "./build/widgets/*", + "./ui": { + "types": "./build/ui/index.d.ts", + "default": "./build/ui/index.js" + }, + "./ui/*": "./build/ui/*", + "./hooks": { + "types": "./build/hooks/index.d.ts", + "default": "./build/hooks/index.js" + }, + "./hooks/*": "./build/hooks/*", + "./svg": { + "types": "./build/svg/index.d.ts", + "default": "./build/svg/index.js" + }, + "./svg/*": "./build/svg/*", + "./charts": { + "types": "./build/charts/index.d.ts", + "default": "./build/charts/index.js" + }, + "./charts/*": "./build/charts/*", + "./jsx-runtime": "./build/jsx-runtime.js", + "./jsx-runtime.js": "./build/jsx-runtime.js", + "./jsx-dev-runtime": "./build/jsx-dev-runtime.js", + "./jsx-dev-runtime.js": "./build/jsx-dev-runtime.js", + "./build/*": "./build/*", + "./src/core": "./src/core.d.ts", + "./src/*": "./src/*", + "./sass/*": "./src/*", + "./locale/*": "./build/locale/*", + "./manifest": "./dist/manifest.js", + "./manifest.js": "./dist/manifest.js" + }, + "sideEffects": [ + "./src/widgets/icons/*.tsx", + "./src/widgets/icons/*.ts", + "./build/widgets/icons/*.js" + ], "scripts": { - "build": "node build/index" + "build": "yarn compile && node build", + "compile": "tsc -p tsconfig.compile.json", + "check-types": "tsc --noEmit", + "test": "ts-mocha -p tsconfig.mocha.json" }, "author": "Codaxy", "license": "MIT", @@ -14,19 +68,32 @@ }, "homepage": "https://cxjs.io", "dependencies": { + "@types/route-parser": "^0.1.7", "intl-io": "^0.4.4", "route-parser": "^0.0.5" }, "peerDependencies": { - "@types/react": "*", - "react": "*", - "react-dom": "*" + "@types/react": ">=18", + "@types/react-dom": ">=18", + "cx-react": ">=26", + "react": ">=18", + "react-dom": ">=18" }, "repository": { "type": "git", - "url": "git@github.com:codaxy/cxjs.git" + "url": "git+ssh://git@github.com/codaxy/cxjs.git" }, "devDependencies": { - "react-test-renderer": "^18.3.1" + "@types/jest": "^30.0.0", + "@types/mocha": "^10.0.10", + "@types/node": "^25.0.3", + "@types/react-dom": "^19.2.3", + "@types/react-test-renderer": "^19.1.0", + "mocha": "^11.7.5", + "react-test-renderer": "^19.2.3", + "ts-mocha": "^11.1.0", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" } } diff --git a/packages/cx/src/charts/Bar.d.ts b/packages/cx/src/charts/Bar.d.ts deleted file mode 100644 index 50a4d254b..000000000 --- a/packages/cx/src/charts/Bar.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as Cx from '../core'; -import { ColumnBarBaseProps } from './ColumnBarBase'; - -interface BarProps extends ColumnBarBaseProps { - - /** Base value. Default value is `0`. */ - x0?: Cx.NumberProp, - - /** Size (width) of the column in axis units. */ - size?: Cx.NumberProp, - - /** Set to true to auto calculate size and offset. Available only if the x axis is a category axis. */ - autoSize?: Cx.BooleanProp, - - height?: Cx.NumberProp, - - /** Base CSS class to be applied to the element. Defaults to `bar`. */ - baseClass?: string, - - /** Tooltip configuration. For more info see Tooltips. */ - tooltip?: Cx.StringProp | Cx.StructuredProp, - - /** Selection configuration. */ - selection?: Cx.Config - -} - -export class Bar extends Cx.Widget {} \ No newline at end of file diff --git a/packages/cx/src/charts/Bar.js b/packages/cx/src/charts/Bar.js deleted file mode 100644 index 5b1c73da9..000000000 --- a/packages/cx/src/charts/Bar.js +++ /dev/null @@ -1,90 +0,0 @@ -import {Widget, VDOM} from '../ui/Widget'; -import {ColumnBarBase} from './ColumnBarBase'; -import {Rect} from '../svg/util/Rect'; -import {isDefined} from '../util/isDefined'; - -export class Bar extends ColumnBarBase { - - init() { - if (isDefined(this.height)) - this.size = this.height; - - super.init(); - } - - declareData() { - return super.declareData(...arguments, { - x0: undefined, - size: undefined, - autoSize: undefined - }); - } - - checkValid(data) { - return data.y != null && data.x != null && data.x0 != null - } - - explore(context, instance) { - let {data, xAxis, yAxis} = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) - instance.colorMap.acknowledge(data.colorName); - - if (!data.valid) - return; - - if (data.active) { - - yAxis.acknowledge(data.y, data.size, data.offset); - - if (data.autoSize) - yAxis.book(data.y, data.stacked ? data.stack : data.name); - - if (data.stacked) { - xAxis.stacknowledge(data.stack, data.y, data.x0); - xAxis.stacknowledge(data.stack, data.y, data.x); - } - else { - if (!this.hiddenBase) - xAxis.acknowledge(data.x0); - xAxis.acknowledge(data.x); - } - super.explore(context, instance); - } - } - - calculateRect(instance) { - let {data} = instance; - var {offset, size} = data; - - if (data.autoSize) { - var [index, count] = instance.yAxis.locate(data.y, data.stacked ? data.stack : data.name); - offset = size / count * (index - count / 2 + 0.5); - size = size / count; - } - - var x1 = data.stacked ? instance.xAxis.stack(data.stack, data.y, data.x0) : instance.xAxis.map(data.x0); - var x2 = data.stacked ? instance.xAxis.stack(data.stack, data.y, data.x) : instance.xAxis.map(data.x); - var y1 = instance.yAxis.map(data.y, offset - size / 2); - var y2 = instance.yAxis.map(data.y, offset + size / 2); - - var bounds = new Rect({ - l: Math.min(x1, x2), - r: Math.max(x1, x2), - t: Math.min(y1, y2), - b: Math.max(y1, y2) - }); - - return bounds; - } -} - -Bar.prototype.baseClass = 'bar'; -Bar.prototype.x0 = 0; -Bar.prototype.size = 1; -Bar.prototype.autoSize = false; -Bar.prototype.legendShape = 'bar'; -Bar.prototype.hiddenBase = false; - -Widget.alias('bar', Bar); \ No newline at end of file diff --git a/packages/cx/src/charts/Bar.scss b/packages/cx/src/charts/Bar.scss index 4b61acc6d..be92b54d6 100644 --- a/packages/cx/src/charts/Bar.scss +++ b/packages/cx/src/charts/Bar.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-bar( $name: 'bar', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-rect { stroke-width: 1px; diff --git a/packages/cx/src/charts/Bar.ts b/packages/cx/src/charts/Bar.ts new file mode 100644 index 000000000..143a2fd18 --- /dev/null +++ b/packages/cx/src/charts/Bar.ts @@ -0,0 +1,114 @@ +import { Widget, VDOM } from "../ui/Widget"; +import { ColumnBarBase, ColumnBarBaseConfig, ColumnBarBaseInstance } from "./ColumnBarBase"; +import { Rect } from "../svg/util/Rect"; +import { isDefined } from "../util/isDefined"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp } from "../ui/Prop"; + +export interface BarConfig extends ColumnBarBaseConfig { + /** Base value. Default value is `0`. */ + x0?: NumberProp; + + /** Size (height) of the bar in axis units. */ + size?: NumberProp; + + /** Set to true to auto calculate size and offset. Available only if the y axis is a category axis. */ + autoSize?: BooleanProp; + + /** Alias for size. */ + height?: number; + + /** Hide the base of the bar (x0). */ + hiddenBase?: boolean; +} + +export class Bar extends ColumnBarBase { + declare x0: number; + declare size: number; + declare autoSize: boolean; + declare height: number; + declare hiddenBase: boolean; + + constructor(config: BarConfig) { + super(config); + } + + init(): void { + if (isDefined(this.height)) this.size = this.height; + + super.init(); + } + + declareData(...args: any[]): any { + return super.declareData( + { + x0: undefined, + size: undefined, + autoSize: undefined, + }, + ...args, + ); + } + + checkValid(data: any): boolean { + return data.y != null && data.x != null && data.x0 != null; + } + + explore(context: RenderingContext, instance: ColumnBarBaseInstance): void { + let { data, xAxis, yAxis } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + if (!data.valid) return; + + if (data.active) { + yAxis.acknowledge(data.y, data.size, data.offset); + + if (data.autoSize) yAxis.book(data.y, data.stacked ? data.stack : data.name); + + if (data.stacked) { + xAxis.stacknowledge(data.stack, data.y, data.x0); + xAxis.stacknowledge(data.stack, data.y, data.x); + } else { + if (!this.hiddenBase) xAxis.acknowledge(data.x0); + xAxis.acknowledge(data.x); + } + super.explore(context, instance); + } + } + + calculateRect(instance: ColumnBarBaseInstance): Rect { + let { data } = instance; + var { offset, size } = data; + + if (data.autoSize) { + var [index, count] = instance.yAxis.locate(data.y, data.stacked ? data.stack : data.name); + offset = (size / count) * (index - count / 2 + 0.5); + size = size / count; + } + + var x1 = data.stacked ? instance.xAxis.stack(data.stack, data.y, data.x0) : instance.xAxis.map(data.x0); + var x2 = data.stacked ? instance.xAxis.stack(data.stack, data.y, data.x) : instance.xAxis.map(data.x); + var y1 = instance.yAxis.map(data.y, offset - size / 2); + var y2 = instance.yAxis.map(data.y, offset + size / 2); + + var bounds = new Rect({ + l: Math.min(x1, x2), + r: Math.max(x1, x2), + t: Math.min(y1, y2), + b: Math.max(y1, y2), + }); + + return bounds; + } +} + +Bar.prototype.baseClass = "bar"; +Bar.prototype.x0 = 0; +Bar.prototype.size = 1; +Bar.prototype.autoSize = false; +Bar.prototype.legendShape = "bar"; +Bar.prototype.hiddenBase = false; + +Widget.alias("bar", Bar); diff --git a/packages/cx/src/charts/BarGraph.d.ts b/packages/cx/src/charts/BarGraph.d.ts deleted file mode 100644 index 3fddfbb86..000000000 --- a/packages/cx/src/charts/BarGraph.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as Cx from "../core"; -import { ColumnBarGraphBaseProps } from "./ColumnBarGraphBase"; - -interface BarGraphProps extends ColumnBarGraphBaseProps { - /** Base CSS class to be applied to the element. Defaults to `bargraph`. */ - baseClass?: string; - - /** - * Name of the property which holds the base value. - * Default value is `false`, which means x0 value is not read from the data array. - */ - x0Field?: string | false; -} - -export class BarGraph extends Cx.Widget {} diff --git a/packages/cx/src/charts/BarGraph.js b/packages/cx/src/charts/BarGraph.js deleted file mode 100644 index f5a0e065c..000000000 --- a/packages/cx/src/charts/BarGraph.js +++ /dev/null @@ -1,112 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { ColumnBarGraphBase } from "./ColumnBarGraphBase"; -import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops"; -import { isArray } from "../util/isArray"; - -export class BarGraph extends ColumnBarGraphBase { - explore(context, instance) { - super.explore(context, instance); - - let { data, yAxis, xAxis } = instance; - - if (isArray(data.data)) { - data.data.forEach((p) => { - var x0 = this.x0Field ? p[this.x0Field] : data.x0; - var y = p[this.yField]; - var x = p[this.xField]; - - yAxis.acknowledge(y, data.size, data.offset); - - if (data.autoSize) yAxis.book(y, data.stacked ? data.stack : data.name); - - if (data.stacked) { - xAxis.stacknowledge(data.stack, y, x0); - xAxis.stacknowledge(data.stack, y, x); - } else { - if (!this.hiddenBase) xAxis.acknowledge(x0); - xAxis.acknowledge(x); - } - }); - } - } - - renderGraph(context, instance) { - var { data, yAxis, xAxis, store } = instance; - - if (!isArray(data.data)) return false; - - var isSelected = this.selection.getIsSelectedDelegate(store); - - return data.data.map((p, i) => { - var { offset, size } = data; - - var x0 = this.x0Field ? p[this.x0Field] : data.x0; - var y = p[this.yField]; - var x = p[this.xField]; - - if (data.autoSize) { - var [index, count] = instance.yAxis.locate(y, data.stacked ? data.stack : data.name); - offset = (size / count) * (index - count / 2 + 0.5); - size = size / count; - } - - var y1 = yAxis.map(y, offset - size / 2); - var y2 = yAxis.map(y, offset + size / 2); - var x1 = data.stacked ? xAxis.stack(data.stack, y, x0) : xAxis.map(x0); - var x2 = data.stacked ? xAxis.stack(data.stack, y, x) : xAxis.map(x); - - var color = this.colorIndexField ? p[this.colorIndexField] : data.colorIndex; - var state = { - selected: isSelected(p, i), - selectable: !this.selection.isDummy, - [`color-${color}`]: color != null, - }; - - let mmove, mleave; - - if (this.tooltip) { - mmove = (e) => - tooltipMouseMove(e, instance, this.tooltip, { - target: e.target.parent, - data: { - $record: p, - }, - }); - mleave = (e) => - tooltipMouseLeave(e, instance, this.tooltip, { - target: e.target.parent, - data: { - $record: p, - }, - }); - } - - return ( - { - this.handleClick(e, instance, p, i); - }} - x={Math.min(x1, x2)} - y={Math.min(y1, y2)} - width={Math.abs(x2 - x1)} - height={Math.abs(y2 - y1)} - style={data.style} - onMouseMove={mmove} - onMouseLeave={mleave} - rx={data.borderRadius} - /> - ); - }); - } -} - -BarGraph.prototype.baseClass = "bargraph"; -BarGraph.prototype.x0Field = false; -BarGraph.prototype.x0 = 0; -BarGraph.prototype.legendShape = "bar"; -BarGraph.prototype.hiddenBase = false; -BarGraph.prototype.borderRadius = 0; - -Widget.alias("bargraph", BarGraph); diff --git a/packages/cx/src/charts/BarGraph.scss b/packages/cx/src/charts/BarGraph.scss index de07a39bf..9d4c1688e 100644 --- a/packages/cx/src/charts/BarGraph.scss +++ b/packages/cx/src/charts/BarGraph.scss @@ -1,11 +1,12 @@ +@use "sass:map"; @mixin cx-bargraph( $name: 'bargraph', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-bar { stroke-width: 1px; diff --git a/packages/cx/src/charts/BarGraph.tsx b/packages/cx/src/charts/BarGraph.tsx new file mode 100644 index 000000000..2ed207172 --- /dev/null +++ b/packages/cx/src/charts/BarGraph.tsx @@ -0,0 +1,145 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM } from "../ui/Widget"; +import { ColumnBarGraphBase, ColumnBarGraphBaseConfig, ColumnBarGraphBaseInstance } from "./ColumnBarGraphBase"; +import { tooltipMouseMove, tooltipMouseLeave, TooltipParentInstance } from "../widgets/overlay/tooltip-ops"; +import { isArray } from "../util/isArray"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp } from "../ui/Prop"; + +export interface BarGraphConfig extends ColumnBarGraphBaseConfig { + /** + * Name of the property which holds the base value. + * Default value is `false`, which means x0 value is not read from the data array. + */ + x0Field?: string | false; + + /** Bar base value. Default value is `0`. */ + x0?: NumberProp; + + /** Hide the base of the bar (x0). */ + hiddenBase?: boolean; + + /** Tooltip configuration. */ + tooltip?: any; +} + +export interface BarGraphInstance extends ColumnBarGraphBaseInstance, TooltipParentInstance {} + +export class BarGraph extends ColumnBarGraphBase { + declare x0Field: string | false; + declare x0: number; + declare hiddenBase: boolean; + declare tooltip: any; + + constructor(config: BarGraphConfig) { + super(config); + } + + explore(context: RenderingContext, instance: BarGraphInstance): void { + super.explore(context, instance); + + let { data, yAxis, xAxis } = instance; + + if (isArray(data.data)) { + data.data.forEach((p: any) => { + var x0 = this.x0Field ? p[this.x0Field] : data.x0; + var y = p[this.yField]; + var x = p[this.xField]; + + yAxis.acknowledge(y, data.size, data.offset); + + if (data.autoSize) yAxis.book(y, data.stacked ? data.stack : data.name); + + if (data.stacked) { + xAxis.stacknowledge(data.stack, y, x0); + xAxis.stacknowledge(data.stack, y, x); + } else { + if (!this.hiddenBase) xAxis.acknowledge(x0); + xAxis.acknowledge(x); + } + }); + } + } + + renderGraph(context: RenderingContext, instance: BarGraphInstance): React.ReactNode { + var { data, yAxis, xAxis, store } = instance; + + if (!isArray(data.data)) return false; + + var isSelected = this.selection.getIsSelectedDelegate(store); + + return data.data.map((p: any, i: number) => { + var { offset, size } = data; + + var x0 = this.x0Field ? p[this.x0Field] : data.x0; + var y = p[this.yField]; + var x = p[this.xField]; + + if (data.autoSize) { + var [index, count] = instance.yAxis.locate(y, data.stacked ? data.stack : data.name); + offset = (size / count) * (index - count / 2 + 0.5); + size = size / count; + } + + var y1 = yAxis.map(y, offset - size / 2); + var y2 = yAxis.map(y, offset + size / 2); + var x1 = data.stacked ? xAxis.stack(data.stack, y, x0) : xAxis.map(x0); + var x2 = data.stacked ? xAxis.stack(data.stack, y, x) : xAxis.map(x); + + var color = this.colorIndexField ? p[this.colorIndexField as string] : data.colorIndex; + var state: Record = { + selected: isSelected(p, i), + selectable: !this.selection.isDummy, + [`color-${color}`]: color != null, + }; + + let mmove: ((e: React.MouseEvent) => void) | undefined, + mleave: ((e: React.MouseEvent) => void) | undefined; + + if (this.tooltip) { + mmove = (e) => + tooltipMouseMove(e, instance, this.tooltip, { + target: (e.target as any).parent, + data: { + $record: p, + }, + }); + mleave = (e) => + tooltipMouseLeave(e, instance, this.tooltip, { + target: (e.target as any).parent, + data: { + $record: p, + }, + }); + } + + return ( + { + this.handleClick(e, instance, p, i); + }} + x={Math.min(x1, x2)} + y={Math.min(y1, y2)} + width={Math.abs(x2 - x1)} + height={Math.abs(y2 - y1)} + style={data.style} + onMouseMove={mmove} + onMouseLeave={mleave} + rx={data.borderRadius} + /> + ); + }); + } +} + +BarGraph.prototype.baseClass = "bargraph"; +BarGraph.prototype.x0Field = false; +BarGraph.prototype.x0 = 0; +BarGraph.prototype.legendShape = "bar"; +BarGraph.prototype.hiddenBase = false; +BarGraph.prototype.borderRadius = 0; + +Widget.alias("bargraph", BarGraph); diff --git a/packages/cx/src/charts/BubbleGraph.js b/packages/cx/src/charts/BubbleGraph.js deleted file mode 100644 index b470fd840..000000000 --- a/packages/cx/src/charts/BubbleGraph.js +++ /dev/null @@ -1,93 +0,0 @@ -import {Widget, VDOM} from '../ui/Widget'; -import {Selection} from '../ui/selection/Selection'; -import {CSS} from '../ui/CSS'; -import {isArray} from '../util/isArray'; - -export class BubbleGraph extends Widget { - declareData() { - - var selection = this.selection.configureWidget(this); - - super.declareData(...arguments, { - data: undefined, - bubbleRadius: undefined, - bubbleStyle: { - structured: true - } - }, selection); - } - - init() { - this.selection = Selection.create(this.selection, { - records: this.data - }); - super.init(); - } - - explore(context, instance) { - instance.axes = context.axes; - super.explore(context, instance); - var {data} = instance; - if (isArray(data.data)) { - data.data.forEach(p => { - instance.axes[this.xAxis].acknowledge(p[this.xField]); - instance.axes[this.yAxis].acknowledge(p[this.yField]); - }); - } - } - - prepare(context, instance) { - super.prepare(context, instance); - if (instance.axes[this.xAxis].shouldUpdate || instance.axes[this.yAxis].shouldUpdate) - instance.markShouldUpdate(context); - } - - render(context, instance, key) { - var {data} = instance; - return - {this.renderData(context, instance)} - ; - } - - renderData(context, instance) { - var {data, axes, store} = instance; - - var xAxis = axes[this.xAxis]; - var yAxis = axes[this.yAxis]; - - return isArray(data.data) - && data.data.map((p, i) => { - var selected = this.selection && this.selection.isSelected(store, p, i); - var classes = CSS.element(this.baseClass, 'bubble', { - selected: selected - }); - return {this.onBubbleClick(e, instance, i)}} - /> - }); - } - - onBubbleClick(e, {data, store}, index) { - var bubble = data.data[index]; - this.selection.select(store, bubble, index, { - toggle: e.ctrlKey - }); - } -} - -BubbleGraph.prototype.baseClass = 'bubblegraph'; -BubbleGraph.prototype.xAxis = 'x'; -BubbleGraph.prototype.yAxis = 'y'; - -BubbleGraph.prototype.xField = 'x'; -BubbleGraph.prototype.yField = 'y'; -BubbleGraph.prototype.rField = 'r'; - -BubbleGraph.prototype.bubbleRadius = 10; - -Widget.alias('bubble-graph', BubbleGraph); \ No newline at end of file diff --git a/packages/cx/src/charts/BubbleGraph.scss b/packages/cx/src/charts/BubbleGraph.scss index e9ddee18c..f20f5893b 100644 --- a/packages/cx/src/charts/BubbleGraph.scss +++ b/packages/cx/src/charts/BubbleGraph.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-bubblegraph( $name: 'bubblegraph', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { } diff --git a/packages/cx/src/charts/BubbleGraph.tsx b/packages/cx/src/charts/BubbleGraph.tsx new file mode 100644 index 000000000..2ec40c1da --- /dev/null +++ b/packages/cx/src/charts/BubbleGraph.tsx @@ -0,0 +1,165 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { Selection, SimpleSelection } from "../ui/selection/Selection"; +import type { KeySelection } from "../ui/selection/KeySelection"; +import type { PropertySelection } from "../ui/selection/PropertySelection"; +import { CSS } from "../ui/CSS"; +import { isArray } from "../util/isArray"; +import { Instance } from "../ui/Instance"; +import { RenderingContext, CxChild } from "../ui/RenderingContext"; +import { Prop, StyleProp, DataRecord } from "../ui/Prop"; +import { Create } from "../util/Component"; +import type { ChartRenderingContext } from "./Chart"; + +export interface BubbleGraphConfig extends WidgetConfig { + /** Data array for the bubbles. */ + data?: Prop; + + /** Default bubble radius. Default is 10. */ + bubbleRadius?: number; + + /** Style object applied to all bubbles. */ + bubbleStyle?: StyleProp; + + /** Name of the x-axis. Default is 'x'. */ + xAxis?: string; + + /** Name of the y-axis. Default is 'y'. */ + yAxis?: string; + + /** Name of the field in data objects that contains x values. Default is 'x'. */ + xField?: string; + + /** Name of the field in data objects that contains y values. Default is 'y'. */ + yField?: string; + + /** Name of the field in data objects that contains radius values. Default is 'r'. */ + rField?: string; + + /** Selection configuration. */ + selection?: Create | Create | Create | Create; +} + +export interface BubbleGraphInstance extends Instance { + axes: { [key: string]: any }; +} + +export class BubbleGraph extends Widget { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xField: string; + declare yField: string; + declare rField: string; + declare bubbleRadius: number; + declare selection: Selection; + declare data?: Prop; + + constructor(config?: BubbleGraphConfig) { + super(config); + } + + declareData(...args: any[]) { + var selection = this.selection.configureWidget(this); + + super.declareData( + ...args, + { + data: undefined, + bubbleRadius: undefined, + bubbleStyle: { + structured: true, + }, + }, + selection, + ); + } + + init() { + this.selection = Selection.create(this.selection as Create, { + records: this.data, + }); + super.init(); + } + + explore(context: ChartRenderingContext, instance: BubbleGraphInstance) { + instance.axes = context.axes!; + super.explore(context, instance); + var { data } = instance; + const d = data as any; + if (isArray(d.data)) { + d.data.forEach((p: DataRecord) => { + instance.axes[this.xAxis].acknowledge(p[this.xField]); + instance.axes[this.yAxis].acknowledge(p[this.yField]); + }); + } + } + + prepare(context: ChartRenderingContext, instance: BubbleGraphInstance) { + super.prepare?.(context, instance); + if (instance.axes[this.xAxis].shouldUpdate || (instance.axes as any)[this.yAxis].shouldUpdate) + instance.markShouldUpdate(context); + } + + render(context: ChartRenderingContext, instance: BubbleGraphInstance, key: string): CxChild { + var { data } = instance; + return ( + + {this.renderData(context, instance)} + + ); + } + + renderData(context: ChartRenderingContext, instance: BubbleGraphInstance): any { + var { data, axes, store } = instance; + const d = data as any; + + var xAxis = (axes as any)[this.xAxis]; + var yAxis = (axes as any)[this.yAxis]; + + return ( + isArray(d.data) && + d.data.map((p: DataRecord, i: number) => { + var selected = this.selection && this.selection.isSelected(store, p, i); + var classes = CSS.element(this.baseClass, "bubble", { + selected: selected, + }); + return ( + { + this.onBubbleClick(e, instance, i); + }} + /> + ); + }) + ); + } + + onBubbleClick(e: React.MouseEvent, instance: BubbleGraphInstance, index: number) { + const { data, store } = instance; + const d = data as any; + var bubble = d.data[index]; + this.selection.select(store, bubble, index, { + toggle: e.ctrlKey, + }); + } +} + +BubbleGraph.prototype.baseClass = "bubblegraph"; +BubbleGraph.prototype.xAxis = "x"; +BubbleGraph.prototype.yAxis = "y"; + +BubbleGraph.prototype.xField = "x"; +BubbleGraph.prototype.yField = "y"; +BubbleGraph.prototype.rField = "r"; + +BubbleGraph.prototype.bubbleRadius = 10; + +Widget.alias("bubble-graph", BubbleGraph); diff --git a/packages/cx/src/charts/Chart.d.ts b/packages/cx/src/charts/Chart.d.ts deleted file mode 100644 index e9e15df88..000000000 --- a/packages/cx/src/charts/Chart.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; - -interface ChartProps extends BoundedObjectProps { - /** Axis definition. Each key represent an axis, and each value hold axis configuration. */ - axes?: Cx.Config; - - /** Put axes over data series. */ - axesOnTop?: boolean; -} - -export class Chart extends Cx.Widget {} diff --git a/packages/cx/src/charts/Chart.js b/packages/cx/src/charts/Chart.js deleted file mode 100644 index 295a52fa3..000000000 --- a/packages/cx/src/charts/Chart.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Widget, VDOM, getContent } from "../ui/Widget"; -import { BoundedObject } from "../svg/BoundedObject"; -import { Axis } from "./axis/Axis"; - -export class Chart extends BoundedObject { - init() { - super.init(); - - if (!this.axes) this.axes = {}; - - for (let axis in this.axes) { - this.axes[axis] = Axis.create(this.axes[axis]); - } - } - - explore(context, instance) { - instance.calculators = { ...context.axes }; - - context.push("axes", instance.calculators); - instance.axes = {}; - - //axes need to be registered before children to be processed first - for (let axis in this.axes) { - let axisInstance = instance.getChild(context, this.axes[axis]); - if (axisInstance.scheduleExploreIfVisible(context)) { - instance.axes[axis] = axisInstance; - instance.calculators[axis] = this.axes[axis].report(context, axisInstance); - } - } - - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("axes"); - - for (let axis in instance.axes) { - instance.axes[axis].widget.reportData(context, instance.axes[axis]); - } - } - - prepare(context, instance) { - context.push("axes", instance.calculators); - super.prepare(context, instance); - } - - prepareCleanup(context, instance) { - context.pop("axes"); - super.prepareCleanup(context, instance); - } - - render(context, instance, key) { - let axes = []; - for (let k in instance.axes) { - axes.push(getContent(instance.axes[k].render(context, key + "-axis-" + k))); - } - - let result = []; - - if (!this.axesOnTop) result.push(axes); - - result.push(this.renderChildren(context, instance)); - - if (this.axesOnTop) result.push(axes); - - return result; - } -} - -Chart.prototype.anchors = "0 1 1 0"; -Chart.prototype.styled = true; -Chart.prototype.isPureContainer = true; -Chart.prototype.axesOnTop = false; - -Widget.alias("chart", Chart); diff --git a/packages/cx/src/charts/Chart.ts b/packages/cx/src/charts/Chart.ts new file mode 100644 index 000000000..65fa86dc7 --- /dev/null +++ b/packages/cx/src/charts/Chart.ts @@ -0,0 +1,108 @@ +import { Widget, VDOM, getContent } from "../ui/Widget"; +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance, SvgRenderingContext } from "../svg/BoundedObject"; +import { Axis } from "./axis/Axis"; +import type { NumericAxis } from "./axis/NumericAxis"; +import type { CategoryAxis } from "./axis/CategoryAxis"; +import type { TimeAxis } from "./axis/TimeAxis"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Create } from "../util/Component"; + +/** Typed context interface for chart-related context properties */ +export interface ChartRenderingContext extends SvgRenderingContext { + axes?: Record; +} + +export interface ChartConfig extends BoundedObjectConfig { + /** Axis definition. Each key represent an axis, and each value hold axis configuration. */ + axes?: Record< + string, + Create | Create | Create | Create + >; + + /** Put axes over data series. */ + axesOnTop?: boolean; +} + +export interface ChartInstance extends BoundedObjectInstance { + calculators: Record; + axes: Record; +} + +export class Chart extends BoundedObject { + declare axes: Record; + declare axesOnTop: boolean; + + constructor(config?: ChartConfig) { + super(config); + } + + init(): void { + super.init(); + + if (!this.axes) this.axes = {}; + + for (let axis in this.axes) { + this.axes[axis] = Axis.create(this.axes[axis]); + } + } + + explore(context: ChartRenderingContext, instance: ChartInstance): void { + instance.calculators = { ...context.axes }; + + context.push("axes", instance.calculators); + instance.axes = {}; + + //axes need to be registered before children to be processed first + for (let axis in this.axes) { + let axisInstance = instance.getChild(context, this.axes[axis]); + if (axisInstance.scheduleExploreIfVisible(context)) { + instance.axes[axis] = axisInstance; + instance.calculators[axis] = this.axes[axis].report(context, axisInstance); + } + } + + super.explore(context, instance); + } + + exploreCleanup(context: ChartRenderingContext, instance: ChartInstance): void { + context.pop("axes"); + + for (let axis in instance.axes) { + instance.axes[axis].widget.reportData(context, instance.axes[axis]); + } + } + + prepare(context: ChartRenderingContext, instance: ChartInstance): void { + context.push("axes", instance.calculators); + super.prepare(context, instance); + } + + prepareCleanup(context: ChartRenderingContext, instance: ChartInstance): void { + context.pop("axes"); + super.prepareCleanup(context, instance); + } + + render(context: ChartRenderingContext, instance: ChartInstance, key: string): any[] { + let axes = []; + for (let k in instance.axes) { + axes.push(getContent(instance.axes[k].render(context, key + "-axis-" + k))); + } + + let result = []; + + if (!this.axesOnTop) result.push(axes); + + result.push(this.renderChildren(context, instance)); + + if (this.axesOnTop) result.push(axes); + + return result; + } +} + +Chart.prototype.anchors = "0 1 1 0"; +Chart.prototype.styled = true; +Chart.prototype.isPureContainer = true; +Chart.prototype.axesOnTop = false; + +Widget.alias("chart", Chart); diff --git a/packages/cx/src/charts/ColorMap.d.ts b/packages/cx/src/charts/ColorMap.d.ts deleted file mode 100644 index 68a42f47b..000000000 --- a/packages/cx/src/charts/ColorMap.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Cx from "../core"; -import { PureContainer } from "../ui/PureContainer"; - -interface ColorMapProps extends Cx.WidgetProps { - onGetCache?: string | (() => Cx.Record); - names?: Cx.Prop; - step?: Cx.NumberProp; - offset?: Cx.NumberProp; - size?: Cx.NumberProp; -} - -export class ColorMap extends Cx.Widget { - static Scope: ColorMapScope; -} - -export class ColorMapScope extends PureContainer {} - -export class ColorIndex { - acknowledge(name: string); - map(name: string): number; -} diff --git a/packages/cx/src/charts/ColorMap.js b/packages/cx/src/charts/ColorMap.js deleted file mode 100644 index 8110f77aa..000000000 --- a/packages/cx/src/charts/ColorMap.js +++ /dev/null @@ -1,97 +0,0 @@ -import { Widget } from '../ui/Widget'; -import { PureContainer } from '../ui/PureContainer'; - -export class ColorMap extends Widget { - declareData() { - super.declareData(...arguments, { - names: undefined, - offset: undefined, - step: undefined, - size: undefined, - }) - } - - explore(context, instance) { - if (!context.colorMaps) - context.colorMaps = {}; - - context.getColorMap = (colorMap) => { - let map = context.colorMaps[colorMap]; - if (!map) { - let cache = this.onGetCache ? instance.invoke("onGetCache") : {}; - map = cache[colorMap]; - if (!map) { - let { data } = instance; - map = context.colorMaps[colorMap] = cache[colorMap] = new ColorIndex({ - offset: data.offset, - step: data.step, - size: data.size - }); - } - if (Array.isArray(instance.data.names)) - instance.data.names.forEach(name => map.acknowledge(name)); - } - return map; - } - } - - render() { - return null; - } -} - -ColorMap.prototype.offset = 0; -ColorMap.prototype.step = null; -ColorMap.prototype.size = 16; - -export class ColorMapScope extends PureContainer { - explore(context, instance) { - context.push('colorMaps', instance.colorMaps = {}); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop('colorMaps'); - } - - prepare(context, instance) { - context.push('colorMaps', instance.colorMaps); - } - - prepareCleanup(context, instance) { - context.pop('colorMaps'); - } -} - -ColorMap.Scope = ColorMapScope; -Widget.alias('color-map', ColorMap); - -export class ColorIndex { - constructor({ offset, step, size }) { - this.colorMap = {}; - this.dirty = true; - this.offset = offset; - this.step = step; - this.size = size; - } - - acknowledge(name) { - if (!(name in this.colorMap)) { - this.colorMap[name] = Object.keys(this.colorMap).length; - this.dirty = true; - } - } - - map(name) { - if (this.dirty) { - this.dirty = false; - if (!this.step) { - let n = Object.keys(this.colorMap).length; - this.step = n > 0 ? this.size / n : 1; - } - } - - let index = this.colorMap[name] || 0; - return Math.round(this.offset + this.step * index + this.size) % this.size; - } -} \ No newline at end of file diff --git a/packages/cx/src/charts/ColorMap.ts b/packages/cx/src/charts/ColorMap.ts new file mode 100644 index 000000000..785b47e29 --- /dev/null +++ b/packages/cx/src/charts/ColorMap.ts @@ -0,0 +1,150 @@ +import { Widget, WidgetConfig } from "../ui/Widget"; +import { PureContainer, PureContainerBase, PureContainerConfig } from "../ui/PureContainer"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Prop, NumberProp, DataRecord } from "../ui/Prop"; + +export interface ColorMapConfig extends WidgetConfig { + /** A callback function used to get a cache object for storing color maps across renders. */ + onGetCache?: string | (() => DataRecord); + + /** An array of names to pre-register in the color map. */ + names?: Prop; + + /** The step value for color indexing. If not specified, it's calculated based on the number of entries. */ + step?: NumberProp; + + /** The starting offset for color indexing. Default is 0. */ + offset?: NumberProp; + + /** The size of the color palette. Default is 16. */ + size?: NumberProp; +} + +export interface ColorMapInstance extends Instance { + colorMaps?: { [key: string]: ColorIndex }; +} + +export class ColorMap extends Widget { + declare offset: number; + declare step: number | null; + declare size: number; + declare onGetCache?: ColorMapConfig["onGetCache"]; + + static Scope: typeof ColorMapScope; + + constructor(config?: ColorMapConfig) { + super(config); + } + + declareData(...args: any[]) { + super.declareData(...args, { + names: undefined, + offset: undefined, + step: undefined, + size: undefined, + }); + } + + explore(context: RenderingContext, instance: Instance) { + if (!context.colorMaps) context.colorMaps = {}; + + context.getColorMap = (colorMap: string) => { + let map = (context.colorMaps as any)[colorMap] as ColorIndex | undefined; + if (!map) { + let cache: { [key: string]: ColorIndex } = this.onGetCache ? instance.invoke("onGetCache") : {}; + map = cache[colorMap]; + if (!map) { + let { data } = instance; + const d = data as any; + map = (context.colorMaps as any)[colorMap] = cache[colorMap] = new ColorIndex({ + offset: d.offset, + step: d.step, + size: d.size, + }); + } + const names = (instance.data as any).names; + if (Array.isArray(names)) names.forEach((name: string) => map!.acknowledge(name)); + } + return map; + }; + } + + render() { + return null; + } +} + +ColorMap.prototype.offset = 0; +ColorMap.prototype.step = null; +ColorMap.prototype.size = 16; + +export interface ColorMapScopeConfig extends PureContainerConfig {} + +export class ColorMapScope extends PureContainerBase { + constructor(config?: ColorMapScopeConfig) { + super(config); + } + + explore(context: RenderingContext, instance: ColorMapInstance) { + context.push("colorMaps", (instance.colorMaps = {})); + super.explore(context, instance); + } + + exploreCleanup(context: RenderingContext, instance: ColorMapInstance) { + context.pop("colorMaps"); + } + + prepare(context: RenderingContext, instance: ColorMapInstance) { + context.push("colorMaps", instance.colorMaps); + } + + prepareCleanup(context: RenderingContext, instance: ColorMapInstance) { + context.pop("colorMaps"); + } +} + +ColorMap.Scope = ColorMapScope; +Widget.alias("color-map", ColorMap); + +export interface ColorIndexConfig { + offset: number; + step: number | null; + size: number; +} + +export class ColorIndex { + colorMap: { [key: string]: number }; + dirty: boolean; + offset: number; + step: number | null; + size: number; + + constructor({ offset, step, size }: ColorIndexConfig) { + this.colorMap = {}; + this.dirty = true; + this.offset = offset; + this.step = step; + this.size = size; + } + + acknowledge(name: string) { + if (!(name in this.colorMap)) { + this.colorMap[name] = Object.keys(this.colorMap).length; + this.dirty = true; + } + } + + map(name: string): number { + if (this.dirty) { + this.dirty = false; + if (!this.step) { + let n = Object.keys(this.colorMap).length; + this.step = n > 0 ? this.size / n : 1; + } + } + + let index = this.colorMap[name] || 0; + return Math.round(this.offset + this.step! * index + this.size) % this.size; + } +} diff --git a/packages/cx/src/charts/Column.d.ts b/packages/cx/src/charts/Column.d.ts deleted file mode 100644 index 64a59120a..000000000 --- a/packages/cx/src/charts/Column.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as Cx from "../core"; -import { ColumnBarBaseProps } from "./ColumnBarBase"; - -interface ColumnProps extends ColumnBarBaseProps { - /** Column base value. Default value is `0`. */ - y0?: Cx.NumberProp; - - /** Size (width) of the column in axis units. */ - size?: Cx.NumberProp; - - /** Set to true to auto calculate size and offset. Available only if the x axis is a category axis. */ - autoSize?: Cx.BooleanProp; - - /** Base CSS class to be applied to the element. Defaults to `column`. */ - baseClass?: boolean; - - width?: number; - - /** Selection configuration. */ - selection?: Config; - - /** Tooltip configuration. For more info see Tooltips. */ - tooltip?: Cx.StringProp | Cx.StructuredProp; - - /** Minimum column size in pixels. Useful for indicating very small values. Default value is 0.5. */ - minPixelHeight?: number; -} - -export class Column extends Cx.Widget {} diff --git a/packages/cx/src/charts/Column.js b/packages/cx/src/charts/Column.js deleted file mode 100644 index 7deee86c1..000000000 --- a/packages/cx/src/charts/Column.js +++ /dev/null @@ -1,88 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { ColumnBarBase } from "./ColumnBarBase"; -import { Rect } from "../svg/util/Rect"; -import { isDefined } from "../util/isDefined"; - -export class Column extends ColumnBarBase { - init() { - if (isDefined(this.width)) this.size = this.width; - - super.init(); - } - - declareData() { - return super.declareData(...arguments, { - y0: undefined, - size: undefined, - autoSize: undefined, - }); - } - - checkValid(data) { - return data.x != null && data.y != null && data.y0 != null; - } - - explore(context, instance) { - let { data, xAxis, yAxis } = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - if (!data.valid) return; - - if (data.active) { - xAxis.acknowledge(data.x, data.size, data.offset); - - if (data.autoSize) xAxis.book(data.x, data.stacked ? data.stack : data.name); - - if (data.stacked) { - yAxis.stacknowledge(data.stack, data.x, data.y0); - yAxis.stacknowledge(data.stack, data.x, data.y); - } else { - if (!this.hiddenBase) yAxis.acknowledge(data.y0); - yAxis.acknowledge(data.y); - } - super.explore(context, instance); - } - } - - calculateRect(instance) { - var { data } = instance; - var { offset, size } = data; - - if (data.autoSize) { - var [index, count] = instance.xAxis.locate(data.x, data.stacked ? data.stack : data.name); - offset = (size / count) * (index - count / 2 + 0.5); - size = size / count; - } - - var x1 = instance.xAxis.map(data.x, offset - size / 2); - var x2 = instance.xAxis.map(data.x, offset + size / 2); - var y1 = data.stacked ? instance.yAxis.stack(data.stack, data.x, data.y0) : instance.yAxis.map(data.y0); - var y2 = data.stacked ? instance.yAxis.stack(data.stack, data.x, data.y) : instance.yAxis.map(data.y); - - if (Math.abs(y2 - y1) < this.minPixelHeight) { - if (y1 < y2) y2 = y1 + this.minPixelHeight; - else y2 = y1 - this.minPixelHeight; - } - - var bounds = new Rect({ - l: Math.min(x1, x2), - r: Math.max(x1, x2), - t: Math.min(y1, y2), - b: Math.max(y1, y2), - }); - - return bounds; - } -} - -Column.prototype.baseClass = "column"; -Column.prototype.y0 = 0; -Column.prototype.size = 1; -Column.prototype.autoSize = false; -Column.prototype.legendShape = "column"; -Column.prototype.hiddenBase = false; -Column.prototype.minPixelHeight = 0.5; - -Widget.alias("column", Column); diff --git a/packages/cx/src/charts/Column.scss b/packages/cx/src/charts/Column.scss index 5ec7d0eff..0bad36707 100644 --- a/packages/cx/src/charts/Column.scss +++ b/packages/cx/src/charts/Column.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-column( $name: 'column', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-rect { stroke-width: 1px; diff --git a/packages/cx/src/charts/Column.ts b/packages/cx/src/charts/Column.ts new file mode 100644 index 000000000..3eb3706af --- /dev/null +++ b/packages/cx/src/charts/Column.ts @@ -0,0 +1,124 @@ +import { Widget, VDOM } from "../ui/Widget"; +import { ColumnBarBase, ColumnBarBaseConfig, ColumnBarBaseInstance } from "./ColumnBarBase"; +import { Rect } from "../svg/util/Rect"; +import { isDefined } from "../util/isDefined"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp } from "../ui/Prop"; + +export interface ColumnConfig extends ColumnBarBaseConfig { + /** Column base value. Default value is `0`. */ + y0?: NumberProp; + + /** Size (width) of the column in axis units. */ + size?: NumberProp; + + /** Set to true to auto calculate size and offset. Available only if the x axis is a category axis. */ + autoSize?: BooleanProp; + + /** Alias for size. */ + width?: number; + + /** Minimum column size in pixels. Useful for indicating very small values. Default value is 0.5. */ + minPixelHeight?: number; + + /** Hide the base of the column (y0). */ + hiddenBase?: boolean; +} + +export class Column extends ColumnBarBase { + declare y0: number; + declare size: number; + declare autoSize: boolean; + declare width: number; + declare minPixelHeight: number; + declare hiddenBase: boolean; + + constructor(config: ColumnConfig) { + super(config); + } + + init(): void { + if (isDefined(this.width)) this.size = this.width; + + super.init(); + } + + declareData(...args: any[]): any { + return super.declareData( + { + y0: undefined, + size: undefined, + autoSize: undefined, + }, + ...args, + ); + } + + checkValid(data: any): boolean { + return data.x != null && data.y != null && data.y0 != null; + } + + explore(context: RenderingContext, instance: ColumnBarBaseInstance): void { + let { data, xAxis, yAxis } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + if (!data.valid) return; + + if (data.active) { + xAxis.acknowledge(data.x, data.size, data.offset); + + if (data.autoSize) xAxis.book(data.x, data.stacked ? data.stack : data.name); + + if (data.stacked) { + yAxis.stacknowledge(data.stack, data.x, data.y0); + yAxis.stacknowledge(data.stack, data.x, data.y); + } else { + if (!this.hiddenBase) yAxis.acknowledge(data.y0); + yAxis.acknowledge(data.y); + } + super.explore(context, instance); + } + } + + calculateRect(instance: ColumnBarBaseInstance): Rect { + var { data } = instance; + var { offset, size } = data; + + if (data.autoSize) { + var [index, count] = instance.xAxis.locate(data.x, data.stacked ? data.stack : data.name); + offset = (size / count) * (index - count / 2 + 0.5); + size = size / count; + } + + var x1 = instance.xAxis.map(data.x, offset - size / 2); + var x2 = instance.xAxis.map(data.x, offset + size / 2); + var y1 = data.stacked ? instance.yAxis.stack(data.stack, data.x, data.y0) : instance.yAxis.map(data.y0); + var y2 = data.stacked ? instance.yAxis.stack(data.stack, data.x, data.y) : instance.yAxis.map(data.y); + + if (Math.abs(y2 - y1) < this.minPixelHeight) { + if (y1 < y2) y2 = y1 + this.minPixelHeight; + else y2 = y1 - this.minPixelHeight; + } + + var bounds = new Rect({ + l: Math.min(x1, x2), + r: Math.max(x1, x2), + t: Math.min(y1, y2), + b: Math.max(y1, y2), + }); + + return bounds; + } +} + +Column.prototype.baseClass = "column"; +Column.prototype.y0 = 0; +Column.prototype.size = 1; +Column.prototype.autoSize = false; +Column.prototype.legendShape = "column"; +Column.prototype.hiddenBase = false; +Column.prototype.minPixelHeight = 0.5; + +Widget.alias("column", Column); diff --git a/packages/cx/src/charts/ColumnBarBase.d.ts b/packages/cx/src/charts/ColumnBarBase.d.ts deleted file mode 100644 index 09ac2160b..000000000 --- a/packages/cx/src/charts/ColumnBarBase.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Cx from "../core"; - -interface ColumnBarBaseProps extends Cx.StyledContainerProps { - /** The `x` value binding or expression. */ - x?: Cx.Prop; - - /** The `y` value binding or expression. */ - y?: Cx.Prop; - - disabled?: Cx.BooleanProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Indicate that columns should be stacked on top of the other columns. Default value is `false`. */ - stacked?: Cx.BooleanProp; - - /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ - stack?: Cx.StringProp; - - /** Of center offset of the column. Use this in combination with `size` to align multiple series on the same chart. */ - offset?: Cx.NumberProp; - - /** Border radius of the column/bar. */ - borderRadius?: Cx.NumberProp; - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string; - - /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ - legend?: string | false; - - legendAction?: string; - legendShape?: string; - - /** A value used to identify the group of components participating in hover effect synchronization. */ - hoverChannel?: string; - - /** A value used to uniquely identify the record within the hover sync group. */ - hoverId?: Cx.StringProp; -} - -export class ColumnBarBase extends Cx.Widget {} diff --git a/packages/cx/src/charts/ColumnBarBase.js b/packages/cx/src/charts/ColumnBarBase.js deleted file mode 100644 index efe06a9fb..000000000 --- a/packages/cx/src/charts/ColumnBarBase.js +++ /dev/null @@ -1,176 +0,0 @@ -import { VDOM } from "../ui/Widget"; -import { PureContainer } from "../ui/PureContainer"; -import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops"; -import { Selection } from "../ui/selection/Selection"; -import { withHoverSync } from "../ui/HoverSync"; - -export class ColumnBarBase extends PureContainer { - init() { - this.selection = Selection.create(this.selection); - super.init(); - } - - declareData() { - var selection = this.selection.configureWidget(this); - - return super.declareData(...arguments, selection, { - x: undefined, - y: undefined, - style: { structured: true }, - class: { structured: true }, - className: { structured: true }, - disabled: undefined, - colorIndex: undefined, - colorMap: undefined, - colorName: undefined, - name: undefined, - active: true, - stacked: undefined, - stack: undefined, - offset: undefined, - hoverId: undefined, - borderRadius: undefined, - hidden: undefined, - }); - } - - prepareData(context, instance) { - instance.axes = context.axes; - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - instance.hoverSync = context.hoverSync; - var { data } = instance; - data.valid = this.checkValid(data); - if (!data.colorName && data.name) data.colorName = data.name; - super.prepareData(context, instance); - } - - checkValid(data) { - return true; - } - - prepare(context, instance) { - let { data, colorMap } = instance; - - if (colorMap && data.colorName) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (!data.valid) return; - - if (data.active) { - instance.bounds = this.calculateRect(instance); - instance.cache("bounds", instance.bounds); - if (!instance.bounds.isEqual(instance.cached.bounds)) instance.markShouldUpdate(context); - - context.push("parentRect", instance.bounds); - if (instance.xAxis.shouldUpdate || instance.yAxis.shouldUpdate) instance.markShouldUpdate(context); - } - - if (data.name && context.addLegendEntry) - context.addLegendEntry(this.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - disabled: data.disabled, - selected: this.selection.isInstanceSelected(instance), - style: data.style, - shape: this.legendShape, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - } - - prepareCleanup(context, instance) { - let { data } = instance; - if (data.valid && data.active) context.pop("parentRect"); - } - - onLegendClick(e, instance) { - var allActions = this.legendAction == "auto"; - var { data } = instance; - if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; - - if (allActions || this.legendAction == "select") this.handleClick(e, instance); - } - - calculateRect(context, instance) { - throw new Error("Abstract method."); - } - - render(context, instance, key) { - let { data, bounds } = instance; - - if (!data.active || !data.valid) return null; - - return withHoverSync( - key, - instance.hoverSync, - this.hoverChannel, - data.hoverId, - ({ hover, onMouseMove, onMouseLeave, key }) => { - var stateMods = { - selected: this.selection.isInstanceSelected(instance), - disabled: data.disabled, - selectable: !this.selection.isDummy, - ["color-" + data.colorIndex]: data.colorIndex != null, - hover, - }; - - return ( - - {!data.hidden && ( - { - onMouseMove(e, instance); - tooltipMouseMove(e, instance, this.tooltip); - }} - onMouseLeave={(e) => { - onMouseLeave(e, instance); - tooltipMouseLeave(e, instance, this.tooltip); - }} - onClick={(e) => { - this.handleClick(e, instance); - }} - /> - )} - {this.renderChildren(context, instance)} - - ); - }, - ); - } - - handleClick(e, instance) { - if (!this.selection.isDummy) { - this.selection.selectInstance(instance, { - toggle: e.ctrlKey, - }); - e.stopPropagation(); - e.preventDefault(); - } - } -} - -ColumnBarBase.prototype.xAxis = "x"; -ColumnBarBase.prototype.yAxis = "y"; -ColumnBarBase.prototype.offset = 0; -ColumnBarBase.prototype.legend = "legend"; -ColumnBarBase.prototype.legendAction = "auto"; -ColumnBarBase.prototype.active = true; -ColumnBarBase.prototype.stacked = false; -ColumnBarBase.prototype.stack = "stack"; -ColumnBarBase.prototype.legendShape = "rect"; -ColumnBarBase.prototype.styled = true; -ColumnBarBase.prototype.hoverChannel = "default"; -ColumnBarBase.prototype.borderRadius = 0; -ColumnBarBase.prototype.hidden = false; diff --git a/packages/cx/src/charts/ColumnBarBase.tsx b/packages/cx/src/charts/ColumnBarBase.tsx new file mode 100644 index 000000000..85fe55034 --- /dev/null +++ b/packages/cx/src/charts/ColumnBarBase.tsx @@ -0,0 +1,284 @@ +/** @jsxImportSource react */ + +import { VDOM } from "../ui/Widget"; +import { StyledContainerBase, StyledContainerConfig } from "../ui/Container"; +import { tooltipMouseMove, tooltipMouseLeave, TooltipParentInstance } from "../widgets/overlay/tooltip-ops"; +import { Selection } from "../ui/selection/Selection"; +import { withHoverSync } from "../ui/HoverSync"; +import { Prop, BooleanProp, NumberProp, StringProp } from "../ui/Prop"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Rect } from "../svg/util/Rect"; +import type { ChartRenderingContext } from "./Chart"; + +export interface ColumnBarBaseConfig extends StyledContainerConfig { + /** The `x` value binding or expression. */ + x?: Prop; + + /** The `y` value binding or expression. */ + y?: Prop; + + disabled?: BooleanProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** Indicate that columns should be stacked on top of the other columns. Default value is `false`. */ + stacked?: BooleanProp; + + /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ + stack?: StringProp; + + /** Of center offset of the column. Use this in combination with `size` to align multiple series on the same chart. */ + offset?: NumberProp; + + /** Border radius of the column/bar. */ + borderRadius?: NumberProp; + + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + */ + xAxis?: string; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + */ + yAxis?: string; + + /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ + legend?: string | false; + + legendAction?: string; + legendShape?: string; + + /** A value used to identify the group of components participating in hover effect synchronization. */ + hoverChannel?: string; + + /** A value used to uniquely identify the record within the hover sync group. */ + hoverId?: StringProp; + + /** Hide the bar/column rect. Used for stacked series where only top bar should be visible. */ + hidden?: BooleanProp; + + selection?: any; + + tooltip?: any; +} + +export interface ColumnBarBaseInstance extends Instance, TooltipParentInstance { + axes: Record; + xAxis: any; + yAxis: any; + hoverSync: any; + colorMap: any; + bounds: Rect; +} + +export class ColumnBarBase extends StyledContainerBase { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare offset: number; + declare legend: string | false; + declare legendAction: string; + declare active: boolean; + declare stacked: boolean; + declare stack: string; + declare legendShape: string; + declare hoverChannel: string; + declare borderRadius: number; + declare hidden: boolean; + declare selection: Selection; + declare tooltip: any; + + constructor(config: ColumnBarBaseConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection); + super.init(); + } + + declareData(...args: any[]): any { + var selection = this.selection.configureWidget(this); + + return super.declareData( + selection, + { + x: undefined, + y: undefined, + style: { structured: true }, + class: { structured: true }, + className: { structured: true }, + disabled: undefined, + colorIndex: undefined, + colorMap: undefined, + colorName: undefined, + name: undefined, + active: true, + stacked: undefined, + stack: undefined, + offset: undefined, + hoverId: undefined, + borderRadius: undefined, + hidden: undefined, + }, + ...args, + ); + } + + prepareData(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void { + instance.axes = context.axes!; + instance.xAxis = context.axes![this.xAxis]; + instance.yAxis = context.axes![this.yAxis]; + instance.hoverSync = context.hoverSync; + var { data } = instance; + data.valid = this.checkValid(data); + if (!data.colorName && data.name) data.colorName = data.name; + super.prepareData(context, instance); + } + + checkValid(data: any): boolean { + return true; + } + + prepare(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void { + let { data, colorMap } = instance; + + if (colorMap && data.colorName) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (!data.valid) return; + + if (data.active) { + instance.bounds = this.calculateRect(instance); + instance.cache("bounds", instance.bounds); + if (!instance.bounds.isEqual(instance.cached.bounds)) instance.markShouldUpdate(context); + + context.push("parentRect", instance.bounds); + if (instance.xAxis.shouldUpdate || instance.yAxis.shouldUpdate) instance.markShouldUpdate(context); + } + + if (data.name && context.addLegendEntry) + context.addLegendEntry(this.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + disabled: data.disabled, + selected: this.selection.isInstanceSelected(instance), + style: data.style, + shape: this.legendShape, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + prepareCleanup(context: ChartRenderingContext, instance: ColumnBarBaseInstance): void { + let { data } = instance; + if (data.valid && data.active) context.pop("parentRect"); + } + + onLegendClick(e: MouseEvent, instance: ColumnBarBaseInstance): void { + var allActions = this.legendAction == "auto"; + var { data } = instance; + if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; + + if (allActions || this.legendAction == "select") this.handleClick(e, instance); + } + + calculateRect(instance: ColumnBarBaseInstance): Rect { + throw new Error("Abstract method."); + } + + render(context: RenderingContext, instance: ColumnBarBaseInstance, key: string): React.ReactNode { + let { data, bounds } = instance; + + if (!data.active || !data.valid) return null; + + return withHoverSync( + key, + instance.hoverSync, + this.hoverChannel, + data.hoverId, + ({ hover, onMouseMove, onMouseLeave, key }: any) => { + var stateMods: Record = { + selected: this.selection.isInstanceSelected(instance), + disabled: data.disabled, + selectable: !this.selection.isDummy, + ["color-" + data.colorIndex]: data.colorIndex != null, + hover, + }; + + return ( + + {!data.hidden && ( + { + onMouseMove(e, instance); + tooltipMouseMove(e, instance, this.tooltip); + }} + onMouseLeave={(e) => { + onMouseLeave(e, instance); + tooltipMouseLeave(e, instance, this.tooltip); + }} + onClick={(e) => { + this.handleClick(e, instance); + }} + /> + )} + {this.renderChildren(context, instance)} + + ); + }, + ); + } + + handleClick(e: MouseEvent | React.MouseEvent, instance: ColumnBarBaseInstance): void { + if (!this.selection.isDummy) { + this.selection.selectInstance(instance, { + toggle: e.ctrlKey, + }); + e.stopPropagation(); + e.preventDefault(); + } + } +} + +ColumnBarBase.prototype.xAxis = "x"; +ColumnBarBase.prototype.yAxis = "y"; +ColumnBarBase.prototype.offset = 0; +ColumnBarBase.prototype.legend = "legend"; +ColumnBarBase.prototype.legendAction = "auto"; +ColumnBarBase.prototype.active = true; +ColumnBarBase.prototype.stacked = false; +ColumnBarBase.prototype.stack = "stack"; +ColumnBarBase.prototype.legendShape = "rect"; +ColumnBarBase.prototype.styled = true; +ColumnBarBase.prototype.hoverChannel = "default"; +ColumnBarBase.prototype.borderRadius = 0; +ColumnBarBase.prototype.hidden = false; diff --git a/packages/cx/src/charts/ColumnBarGraphBase.d.ts b/packages/cx/src/charts/ColumnBarGraphBase.d.ts deleted file mode 100644 index 727e2fb4f..000000000 --- a/packages/cx/src/charts/ColumnBarGraphBase.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as Cx from "../core"; -import { KeySelection, PropertySelection } from "../ui/selection"; - -interface ColumnBarGraphBaseProps extends Cx.WidgetProps { - /** - * Data for the graph. Each entry should be an object with at least two properties - * whose names should match the `xField` and `yField` values. - */ - data?: Cx.RecordsProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Size (width) of the column in axis units. */ - size?: Cx.NumberProp; - - /** Of center offset of the column. Use this in combination with `size` to align multiple series on the same chart. */ - offset?: Cx.NumberProp; - - /** Set to true to auto-calculate size and offset. Available only if the x axis is a category axis. */ - autoSize?: Cx.BooleanProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Indicate that columns should be stacked on top of the other columns. Default value is `false`. */ - stacked?: Cx.BooleanProp; - - /** Border radius of the column/bar. */ - borderRadius?: Cx.NumberProp; - - /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ - stack?: Cx.StringProp; - - /** Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string; - - /** Name of the property which holds the x value. Default value is `x`. */ - xField?: string; - - /** Name of the property which holds the y value. Default value is `y`. */ - yField?: string; - - colorIndexField?: boolean; - - /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ - legend?: string | false; - - legendAction?: string; - legendShape?: string; - - /** Selection configuration. */ - selection?: { type: typeof PropertySelection | typeof KeySelection; [prop: string]: any }; -} - -export class ColumnBarGraphBase extends Cx.Widget {} diff --git a/packages/cx/src/charts/ColumnBarGraphBase.js b/packages/cx/src/charts/ColumnBarGraphBase.js deleted file mode 100644 index 025cb7335..000000000 --- a/packages/cx/src/charts/ColumnBarGraphBase.js +++ /dev/null @@ -1,114 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { Selection } from "../ui/selection/Selection"; - -export class ColumnBarGraphBase extends Widget { - init() { - this.selection = Selection.create(this.selection, { - records: this.data, - }); - super.init(); - } - - declareData() { - var selection = this.selection.configureWidget(this); - - super.declareData(selection, ...arguments, { - data: undefined, - colorIndex: undefined, - colorMap: undefined, - colorName: undefined, - name: undefined, - size: undefined, - offset: undefined, - y0: undefined, - x0: undefined, - autoSize: undefined, - active: true, - stacked: undefined, - stack: undefined, - borderRadius: undefined, - }); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.name && !data.colorName) data.colorName = data.name; - - super.prepareData(context, instance); - } - - explore(context, instance) { - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - - var { data } = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - super.explore(context, instance); - } - - prepare(context, instance) { - let { data, colorMap, xAxis, yAxis } = instance; - - if (colorMap && data.name) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (xAxis.shouldUpdate || yAxis.shouldUpdate) instance.markShouldUpdate(context); - - if (data.name && context.addLegendEntry) - context.addLegendEntry(this.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - disabled: data.disabled, - selected: this.selection.isInstanceSelected(instance), - style: data.style, - shape: this.legendShape, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - } - - onLegendClick(e, instance) { - var allActions = this.legendAction == "auto"; - var { data } = instance; - if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); - } - - render(context, instance, key) { - var { data } = instance; - return ( - - {data.active && this.renderGraph(context, instance)} - - ); - } - - handleClick(e, instance, point, index) { - if (this.onClick && instance.invoke("onClick", e, instance, point, index) === false) return; - - if (!this.selection.isDummy) this.selection.select(instance.store, point, index, { toggle: e.ctrlKey }); - } -} - -ColumnBarGraphBase.prototype.xAxis = "x"; -ColumnBarGraphBase.prototype.yAxis = "y"; -ColumnBarGraphBase.prototype.xField = "x"; -ColumnBarGraphBase.prototype.yField = "y"; -ColumnBarGraphBase.prototype.colorIndexField = false; -ColumnBarGraphBase.prototype.size = 1; -ColumnBarGraphBase.prototype.legend = "legend"; -ColumnBarGraphBase.prototype.legendAction = "auto"; -ColumnBarGraphBase.prototype.legendShape = "rect"; -ColumnBarGraphBase.prototype.stack = "stack"; -ColumnBarGraphBase.prototype.stacked = false; -ColumnBarGraphBase.prototype.autoSize = 0; -ColumnBarGraphBase.prototype.offset = 0; -ColumnBarGraphBase.prototype.styled = true; -ColumnBarGraphBase.prototype.borderRadius = 0; diff --git a/packages/cx/src/charts/ColumnBarGraphBase.tsx b/packages/cx/src/charts/ColumnBarGraphBase.tsx new file mode 100644 index 000000000..e8a2b1443 --- /dev/null +++ b/packages/cx/src/charts/ColumnBarGraphBase.tsx @@ -0,0 +1,229 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { Selection } from "../ui/selection/Selection"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp, RecordsProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface ColumnBarGraphBaseConfig extends WidgetConfig { + /** + * Data for the graph. Each entry should be an object with at least two properties + * whose names should match the `xField` and `yField` values. + */ + data?: RecordsProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Size (width) of the column in axis units. */ + size?: NumberProp; + + /** Of center offset of the column. Use this in combination with `size` to align multiple series on the same chart. */ + offset?: NumberProp; + + /** Set to true to auto-calculate size and offset. Available only if the x axis is a category axis. */ + autoSize?: BooleanProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** Indicate that columns should be stacked on top of the other columns. Default value is `false`. */ + stacked?: BooleanProp; + + /** Border radius of the column/bar. */ + borderRadius?: NumberProp; + + /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ + stack?: StringProp; + + /** Column/bar base value for y axis. */ + y0?: NumberProp; + + /** Column/bar base value for x axis. */ + x0?: NumberProp; + + /** Name of the horizontal axis. Default value is `x`. */ + xAxis?: string; + + /** Name of the vertical axis. Default value is `y`. */ + yAxis?: string; + + /** Name of the property which holds the x value. Default value is `x`. */ + xField?: string; + + /** Name of the property which holds the y value. Default value is `y`. */ + yField?: string; + + colorIndexField?: boolean | string; + + /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ + legend?: string | false; + + legendAction?: string; + legendShape?: string; + + /** Selection configuration. */ + selection?: any; + + onClick?: (e: MouseEvent, instance: Instance, point: any, index: number) => void | false; +} + +export interface ColumnBarGraphBaseInstance extends Instance { + xAxis: any; + yAxis: any; + colorMap: any; +} + +export class ColumnBarGraphBase extends Widget { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xField: string; + declare yField: string; + declare colorIndexField: boolean | string; + declare size: number; + declare legend: string | false; + declare legendAction: string; + declare legendShape: string; + declare stack: string; + declare stacked: boolean; + declare autoSize: number | boolean; + declare offset: number; + declare borderRadius: number; + declare selection: Selection; + declare data: any; + declare onClick: ColumnBarGraphBaseConfig["onClick"]; + + constructor(config?: ColumnBarGraphBaseConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection, { + records: this.data, + }); + super.init(); + } + + declareData(...args: any[]): void { + var selection = this.selection.configureWidget(this); + + super.declareData( + selection, + { + data: undefined, + colorIndex: undefined, + colorMap: undefined, + colorName: undefined, + name: undefined, + size: undefined, + offset: undefined, + y0: undefined, + x0: undefined, + autoSize: undefined, + active: true, + stacked: undefined, + stack: undefined, + borderRadius: undefined, + }, + ...args, + ); + } + + prepareData(context: RenderingContext, instance: ColumnBarGraphBaseInstance): void { + let { data } = instance; + + if (data.name && !data.colorName) data.colorName = data.name; + + super.prepareData(context, instance); + } + + explore(context: ChartRenderingContext, instance: ColumnBarGraphBaseInstance): void { + instance.xAxis = context.axes![this.xAxis]; + instance.yAxis = context.axes![this.yAxis]; + + var { data } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + super.explore(context, instance); + } + + prepare(context: ChartRenderingContext, instance: ColumnBarGraphBaseInstance): void { + let { data, colorMap, xAxis, yAxis } = instance; + + if (colorMap && data.name) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (xAxis.shouldUpdate || yAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (data.name && context.addLegendEntry) + context.addLegendEntry(this.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + disabled: data.disabled, + selected: this.selection.isInstanceSelected(instance), + style: data.style, + shape: this.legendShape, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + onLegendClick(e: MouseEvent, instance: ColumnBarGraphBaseInstance): void { + var allActions = this.legendAction == "auto"; + var { data } = instance; + if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); + } + + render(context: RenderingContext, instance: ColumnBarGraphBaseInstance, key: string): React.ReactNode { + var { data } = instance; + return ( + + {data.active && this.renderGraph(context, instance)} + + ); + } + + renderGraph(context: RenderingContext, instance: ColumnBarGraphBaseInstance): React.ReactNode { + throw new Error("Abstract method"); + } + + handleClick(e: React.MouseEvent, instance: ColumnBarGraphBaseInstance, point: any, index: number): void { + if (this.onClick && instance.invoke("onClick", e, instance, point, index) === false) return; + + if (!this.selection.isDummy) this.selection.select(instance.store, point, index, { toggle: e.ctrlKey }); + } +} + +ColumnBarGraphBase.prototype.xAxis = "x"; +ColumnBarGraphBase.prototype.yAxis = "y"; +ColumnBarGraphBase.prototype.xField = "x"; +ColumnBarGraphBase.prototype.yField = "y"; +ColumnBarGraphBase.prototype.colorIndexField = false; +ColumnBarGraphBase.prototype.size = 1; +ColumnBarGraphBase.prototype.legend = "legend"; +ColumnBarGraphBase.prototype.legendAction = "auto"; +ColumnBarGraphBase.prototype.legendShape = "rect"; +ColumnBarGraphBase.prototype.stack = "stack"; +ColumnBarGraphBase.prototype.stacked = false; +ColumnBarGraphBase.prototype.autoSize = 0; +ColumnBarGraphBase.prototype.offset = 0; +ColumnBarGraphBase.prototype.styled = true; +ColumnBarGraphBase.prototype.borderRadius = 0; diff --git a/packages/cx/src/charts/ColumnGraph.d.ts b/packages/cx/src/charts/ColumnGraph.d.ts deleted file mode 100644 index bc554aefa..000000000 --- a/packages/cx/src/charts/ColumnGraph.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Cx from "../core"; -import { ColumnBarGraphBaseProps } from "./ColumnBarGraphBase"; - -interface ColumnGraphProps extends ColumnBarGraphBaseProps { - /** Base CSS class to be applied to the element. Defaults to `columngraph`. */ - baseClass?: string; - - /** - * Name of the property which holds the base value. - * Default value is `false`, which means y0 value is not read from the data array. - */ - y0Field?: string | false; - - /** Column base value. Default value is `0`. */ - y0?: Cx.NumberProp; -} - -export class ColumnGraph extends Cx.Widget {} diff --git a/packages/cx/src/charts/ColumnGraph.js b/packages/cx/src/charts/ColumnGraph.js deleted file mode 100644 index f69b51465..000000000 --- a/packages/cx/src/charts/ColumnGraph.js +++ /dev/null @@ -1,120 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { ColumnBarGraphBase } from "./ColumnBarGraphBase"; -import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops"; -import { isArray } from "../util/isArray"; - -export class ColumnGraph extends ColumnBarGraphBase { - explore(context, instance) { - super.explore(context, instance); - - let { data, xAxis, yAxis } = instance; - - if (isArray(data.data)) { - data.data.forEach((p, index) => { - var y0 = this.y0Field ? p[this.y0Field] : data.y0; - var x = p[this.xField]; - var y = p[this.yField]; - - xAxis.acknowledge(x, data.size, data.offset); - - if (data.autoSize) xAxis.book(x, data.stacked ? data.stack : data.name); - - if (data.stacked) { - yAxis.stacknowledge(data.stack, x, y0); - yAxis.stacknowledge(data.stack, x, y); - } else { - if (!this.hiddenBase) yAxis.acknowledge(y0); - yAxis.acknowledge(y); - } - }); - } - } - - prepare(context, instance) { - super.prepare(context, instance); - let { data } = instance; - if (context.pointReducer && isArray(data.data)) { - data.data.forEach((p, index) => { - context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); - }); - } - } - - renderGraph(context, instance) { - var { data, xAxis, yAxis, store } = instance; - if (!isArray(data.data)) return false; - - var isSelected = this.selection.getIsSelectedDelegate(store); - - return data.data.map((p, i) => { - var { offset, size } = data; - - var y0 = this.y0Field ? p[this.y0Field] : data.y0; - var x = p[this.xField]; - var y = p[this.yField]; - - if (data.autoSize) { - var [index, count] = instance.xAxis.locate(x, data.stacked ? data.stack : data.name); - offset = (size / count) * (index - count / 2 + 0.5); - size = size / count; - } - - var x1 = xAxis.map(x, offset - size / 2); - var x2 = xAxis.map(x, offset + size / 2); - var y1 = data.stacked ? yAxis.stack(data.stack, x, y0) : yAxis.map(y0); - var y2 = data.stacked ? yAxis.stack(data.stack, x, y) : yAxis.map(y); - - var color = this.colorIndexField ? p[this.colorIndexField] : data.colorIndex; - var state = { - selected: isSelected(p, i), - selectable: !this.selection.isDummy, - [`color-${color}`]: color != null, - }; - - let mmove, mleave; - - if (this.tooltip) { - mmove = (e) => - tooltipMouseMove(e, instance, this.tooltip, { - target: e.target.parent, - data: { - $record: p, - }, - }); - mleave = (e) => - tooltipMouseLeave(e, instance, this.tooltip, { - target: e.target.parent, - data: { - $record: p, - }, - }); - } - - return ( - { - this.handleClick(e, instance, p, i); - }} - x={Math.min(x1, x2)} - y={Math.min(y1, y2)} - width={Math.abs(x2 - x1)} - height={Math.abs(y2 - y1)} - style={data.style} - onMouseMove={mmove} - onMouseLeave={mleave} - rx={data.borderRadius} - /> - ); - }); - } -} - -ColumnGraph.prototype.baseClass = "columngraph"; -ColumnGraph.prototype.y0Field = false; -ColumnGraph.prototype.y0 = 0; -ColumnGraph.prototype.legendShape = "column"; -ColumnGraph.prototype.hiddenBase = false; - -Widget.alias("columngraph", ColumnGraph); diff --git a/packages/cx/src/charts/ColumnGraph.scss b/packages/cx/src/charts/ColumnGraph.scss index 4cc22234e..d89005e16 100644 --- a/packages/cx/src/charts/ColumnGraph.scss +++ b/packages/cx/src/charts/ColumnGraph.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-columngraph( $name: 'columngraph', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-column { stroke-width: 1px; diff --git a/packages/cx/src/charts/ColumnGraph.tsx b/packages/cx/src/charts/ColumnGraph.tsx new file mode 100644 index 000000000..2d7ca3c36 --- /dev/null +++ b/packages/cx/src/charts/ColumnGraph.tsx @@ -0,0 +1,153 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM } from "../ui/Widget"; +import { ColumnBarGraphBase, ColumnBarGraphBaseConfig, ColumnBarGraphBaseInstance } from "./ColumnBarGraphBase"; +import { tooltipMouseMove, tooltipMouseLeave, TooltipParentInstance } from "../widgets/overlay/tooltip-ops"; +import { isArray } from "../util/isArray"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp } from "../ui/Prop"; + +export interface ColumnGraphConfig extends ColumnBarGraphBaseConfig { + /** + * Name of the property which holds the base value. + * Default value is `false`, which means y0 value is not read from the data array. + */ + y0Field?: string | false; + + /** Column base value. Default value is `0`. */ + y0?: NumberProp; + + /** Hide the base of the column (y0). */ + hiddenBase?: boolean; + + /** Tooltip configuration. */ + tooltip?: any; +} + +export interface ColumnGraphInstance extends ColumnBarGraphBaseInstance, TooltipParentInstance {} + +export class ColumnGraph extends ColumnBarGraphBase { + declare y0Field: string | false; + declare y0: number; + declare hiddenBase: boolean; + declare tooltip: any; + + constructor(config: ColumnGraphConfig) { + super(config); + } + + explore(context: RenderingContext, instance: ColumnGraphInstance): void { + super.explore(context, instance); + + let { data, xAxis, yAxis } = instance; + + if (isArray(data.data)) { + data.data.forEach((p: any, index: number) => { + var y0 = this.y0Field ? p[this.y0Field] : data.y0; + var x = p[this.xField]; + var y = p[this.yField]; + + xAxis.acknowledge(x, data.size, data.offset); + + if (data.autoSize) xAxis.book(x, data.stacked ? data.stack : data.name); + + if (data.stacked) { + yAxis.stacknowledge(data.stack, x, y0); + yAxis.stacknowledge(data.stack, x, y); + } else { + if (!this.hiddenBase) yAxis.acknowledge(y0); + yAxis.acknowledge(y); + } + }); + } + } + + prepare(context: RenderingContext, instance: ColumnGraphInstance): void { + super.prepare(context, instance); + let { data } = instance; + if (context.pointReducer && isArray(data.data)) { + data.data.forEach((p: any, index: number) => { + context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); + }); + } + } + + renderGraph(context: RenderingContext, instance: ColumnGraphInstance): React.ReactNode { + var { data, xAxis, yAxis, store } = instance; + if (!isArray(data.data)) return false; + + var isSelected = this.selection.getIsSelectedDelegate(store); + + return data.data.map((p: any, i: number) => { + var { offset, size } = data; + + var y0 = this.y0Field ? p[this.y0Field] : data.y0; + var x = p[this.xField]; + var y = p[this.yField]; + + if (data.autoSize) { + var [index, count] = instance.xAxis.locate(x, data.stacked ? data.stack : data.name); + offset = (size / count) * (index - count / 2 + 0.5); + size = size / count; + } + + var x1 = xAxis.map(x, offset - size / 2); + var x2 = xAxis.map(x, offset + size / 2); + var y1 = data.stacked ? yAxis.stack(data.stack, x, y0) : yAxis.map(y0); + var y2 = data.stacked ? yAxis.stack(data.stack, x, y) : yAxis.map(y); + + var color = this.colorIndexField ? p[this.colorIndexField as string] : data.colorIndex; + var state: Record = { + selected: isSelected(p, i), + selectable: !this.selection.isDummy, + [`color-${color}`]: color != null, + }; + + let mmove: ((e: React.MouseEvent) => void) | undefined, + mleave: ((e: React.MouseEvent) => void) | undefined; + + if (this.tooltip) { + mmove = (e) => + tooltipMouseMove(e, instance, this.tooltip, { + target: (e.target as any).parent, + data: { + $record: p, + }, + }); + mleave = (e) => + tooltipMouseLeave(e, instance, this.tooltip, { + target: (e.target as any).parent, + data: { + $record: p, + }, + }); + } + + return ( + { + this.handleClick(e, instance, p, i); + }} + x={Math.min(x1, x2)} + y={Math.min(y1, y2)} + width={Math.abs(x2 - x1)} + height={Math.abs(y2 - y1)} + style={data.style} + onMouseMove={mmove} + onMouseLeave={mleave} + rx={data.borderRadius} + /> + ); + }); + } +} + +ColumnGraph.prototype.baseClass = "columngraph"; +ColumnGraph.prototype.y0Field = false; +ColumnGraph.prototype.y0 = 0; +ColumnGraph.prototype.legendShape = "column"; +ColumnGraph.prototype.hiddenBase = false; + +Widget.alias("columngraph", ColumnGraph); diff --git a/packages/cx/src/charts/Grid.js b/packages/cx/src/charts/Grid.js deleted file mode 100644 index 2583b2224..000000000 --- a/packages/cx/src/charts/Grid.js +++ /dev/null @@ -1,5 +0,0 @@ -import {Gridlines} from './Gridlines'; - -export const Grid = Gridlines; - -console.log('charts/Grid module is deprecated. Use Gridlines instead.'); diff --git a/packages/cx/src/charts/Grid.ts b/packages/cx/src/charts/Grid.ts new file mode 100644 index 000000000..92ba7bc2f --- /dev/null +++ b/packages/cx/src/charts/Grid.ts @@ -0,0 +1,5 @@ +import { Gridlines } from "./Gridlines"; + +export const Grid = Gridlines; + +console.log("charts/Grid module is deprecated. Use Gridlines instead."); diff --git a/packages/cx/src/charts/Gridlines.d.ts b/packages/cx/src/charts/Gridlines.d.ts deleted file mode 100644 index e4d9200c0..000000000 --- a/packages/cx/src/charts/Gridlines.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as Cx from '../core'; -import { BoundedObject, BoundedObjectProps } from '../svg/BoundedObject'; - -interface GridlinesProps extends BoundedObjectProps { - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - * Set to `false` to hide the grid lines in x direction. - */ - xAxis?: string | boolean, - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - * Set to `false` to hide the grid lines in y direction. - */ - yAxis?: string | boolean, - - /** Base CSS class to be applied to the element. Defaults to `gridlines`. */ - baseClass?: string - -} - -export class Gridlines extends Cx.Widget {} \ No newline at end of file diff --git a/packages/cx/src/charts/Gridlines.js b/packages/cx/src/charts/Gridlines.js deleted file mode 100644 index 697564fd5..000000000 --- a/packages/cx/src/charts/Gridlines.js +++ /dev/null @@ -1,49 +0,0 @@ -import {BoundedObject} from '../svg/BoundedObject'; -import {VDOM} from '../ui/Widget'; - -export class Gridlines extends BoundedObject { - - explore(context, instance) { - super.explore(context, instance); - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - } - - prepare(context, instance) { - super.prepare(context, instance); - let {xAxis, yAxis} = instance; - if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) - instance.markShouldUpdate(context); - } - - render(context, instance, key) { - let {data, xAxis, yAxis} = instance; - let {bounds} = data; - let path = '', xTicks, yTicks; - - if (xAxis) { - xTicks = xAxis.mapGridlines(); - xTicks.forEach(x => { - path += `M ${x} ${bounds.t} L ${x} ${bounds.b}`; - }); - } - - if (yAxis) { - yTicks = yAxis.mapGridlines(); - yTicks.forEach(y => { - path += `M ${bounds.l} ${y} L ${bounds.r} ${y}`; - }); - } - - return - - - } -} - -Gridlines.prototype.xAxis = 'x'; -Gridlines.prototype.yAxis = 'y'; -Gridlines.prototype.anchors = '0 1 1 0'; -Gridlines.prototype.baseClass = 'gridlines'; - -BoundedObject.alias('gridlines', Gridlines); \ No newline at end of file diff --git a/packages/cx/src/charts/Gridlines.scss b/packages/cx/src/charts/Gridlines.scss index 709eff8c7..f90e724a8 100644 --- a/packages/cx/src/charts/Gridlines.scss +++ b/packages/cx/src/charts/Gridlines.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-gridlines( $name: 'gridlines', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { shape-rendering: crispEdges; diff --git a/packages/cx/src/charts/Gridlines.tsx b/packages/cx/src/charts/Gridlines.tsx new file mode 100644 index 000000000..44a8ea46d --- /dev/null +++ b/packages/cx/src/charts/Gridlines.tsx @@ -0,0 +1,88 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { VDOM } from "../ui/Widget"; +import { RenderingContext, CxChild } from "../ui/RenderingContext"; +import type { ChartRenderingContext } from "./Chart"; + +export interface GridlinesConfig extends BoundedObjectConfig { + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + * Set to `false` to hide the grid lines in x direction. + */ + xAxis?: string | boolean; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + * Set to `false` to hide the grid lines in y direction. + */ + yAxis?: string | boolean; + + /** Base CSS class to be applied to the element. Defaults to `gridlines`. */ + baseClass?: string; +} + +export interface GridlinesInstance extends BoundedObjectInstance { + xAxis?: any; + yAxis?: any; +} + +export class Gridlines extends BoundedObject { + declare xAxis: string; + declare yAxis: string; + declare anchors: string; + declare baseClass: string; + + constructor(config?: GridlinesConfig) { + super(config); + } + + explore(context: ChartRenderingContext, instance: GridlinesInstance) { + super.explore(context, instance); + instance.xAxis = context.axes?.[this.xAxis]; + instance.yAxis = context.axes?.[this.yAxis]; + } + + prepare(context: ChartRenderingContext, instance: GridlinesInstance) { + super.prepare(context, instance); + let { xAxis, yAxis } = instance; + if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) instance.markShouldUpdate(context); + } + + render(context: RenderingContext, instance: GridlinesInstance, key: string): CxChild { + let { data, xAxis, yAxis } = instance; + let { bounds } = data as any; + let path = "", + xTicks: number[], + yTicks: number[]; + + if (xAxis) { + xTicks = xAxis.mapGridlines(); + xTicks.forEach((x) => { + path += `M ${x} ${bounds.t} L ${x} ${bounds.b}`; + }); + } + + if (yAxis) { + yTicks = yAxis.mapGridlines(); + yTicks.forEach((y) => { + path += `M ${bounds.l} ${y} L ${bounds.r} ${y}`; + }); + } + + return ( + + + + ); + } +} + +Gridlines.prototype.xAxis = "x"; +Gridlines.prototype.yAxis = "y"; +Gridlines.prototype.anchors = "0 1 1 0"; +Gridlines.prototype.baseClass = "gridlines"; + +BoundedObject.alias("gridlines", Gridlines); diff --git a/packages/cx/src/charts/Legend.d.ts b/packages/cx/src/charts/Legend.d.ts deleted file mode 100644 index 376def77c..000000000 --- a/packages/cx/src/charts/Legend.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as Cx from "../core"; - -interface LegendProps extends Cx.HtmlElementProps { - /** Name of the legend. Default is `legend`. */ - name?: string; - - /** Base CSS class to be applied to the element. Defaults to `legend`. */ - baseClass?: string; - - /** Switch to vertical mode. */ - vertical?: boolean; - - /** Size of the svg shape container in pixels. Default value is 20. */ - svgSize?: number; - - /** Shape size in pixels. Default value is 18. */ - shapeSize?: number; - - /** Default shape that will be applied to the all legend items. */ - shape?: Cx.StringProp; - - /** CSS style that will be applied to the legend entry. */ - entryStyle?: Cx.StyleProp; - - /** CSS class that will be applied to the legend entry. */ - entryClass?: Cx.ClassProp; - - /** CSS style that will be applied to the legend entry value segment. */ - valueStyle?: Cx.StyleProp; - - /** CSS class that will be applied to the legend entry value segment. */ - valueClass?: Cx.ClassProp; - - /** Set to true to show values. Mostly used for PieChart legends. */ - showValues?: Cx.BooleanProp; - - /** Format used for values, i.e. n;2 or currency. The default value is s.*/ - valueFormat?: string; -} - -export class Legend extends Cx.Widget { - static Scope(): any; -} - -export class LegendScope extends Cx.Widget {} diff --git a/packages/cx/src/charts/Legend.js b/packages/cx/src/charts/Legend.js deleted file mode 100644 index 6b473a050..000000000 --- a/packages/cx/src/charts/Legend.js +++ /dev/null @@ -1,187 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { HtmlElement } from "../widgets/HtmlElement"; -import { PureContainer } from "../ui/PureContainer"; -import { getShape } from "./shapes"; -import { isUndefined } from "../util/isUndefined"; -import { isNonEmptyArray } from "../util/isNonEmptyArray"; -import { parseStyle } from "../util/parseStyle"; -import { withHoverSync } from "../ui/HoverSync"; -import { Format } from "../util/Format"; - -export class Legend extends HtmlElement { - declareData() { - super.declareData(...arguments, { - shape: undefined, - entryStyle: { structured: true }, - entryClass: { structured: true }, - valueStyle: { structured: true }, - valueClass: { structured: true }, - showValues: undefined, - }); - } - - init() { - this.entryStyle = parseStyle(this.entryStyle); - this.valueStyle = parseStyle(this.valueStyle); - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - data.stateMods = Object.assign(data.stateMods || {}, { - vertical: this.vertical, - }); - super.prepareData(context, instance); - } - - isValidHtmlAttribute(attrName) { - switch (attrName) { - case "shapeSize": - case "svgSize": - case "shape": - case "entryStyle": - case "entryClass": - case "valueStyle": - case "valueClass": - case "showValues": - case "valueFormat": - return false; - - default: - return super.isValidHtmlAttribute(attrName); - } - } - - explore(context, instance) { - if (!context.legends) context.legends = {}; - - instance.legends = context.legends; - - context.addLegendEntry = (legendName, entry) => { - if (!legendName) return; - - //case when all legends are scoped and new entry is added outside the scope - if (!context.legends) return; - - let legend = context.legends[legendName]; - if (!legend) - legend = context.legends[legendName] = { - entries: [], - names: {}, - }; - - if (!legend.names[entry.name]) { - legend.entries.push(entry); - legend.names[entry.name] = entry; - } - }; - - super.explore(context, instance); - } - - renderChildren(context, instance) { - const CSS = this.CSS; - - let entries = instance.legends[this.name] && instance.legends[this.name].entries, - list; - - let { entryClass, entryStyle, shape, valueClass, valueStyle } = instance.data; - let valueFormatter = Format.parse(this.valueFormat); - - let valueClasses = this.showValues ? CSS.expand(CSS.element(this.baseClass, "value"), valueClass) : null; - let entryTextClass = CSS.element(this.baseClass, "entry-text"); - - if (isNonEmptyArray(entries)) { - list = entries.map((e, i) => - withHoverSync(i, e.hoverSync, e.hoverChannel, e.hoverId, ({ onMouseMove, onMouseLeave, hover }) => ( -
- {this.renderShape(e, shape)} -
{e.displayText || e.name}
- {this.showValues && ( -
- {valueFormatter(e.value)} -
- )} -
- )), - ); - } - - return [list, super.renderChildren(context, instance)]; - } - - renderShape(entry, legendEntriesShape) { - const className = this.CSS.element(this.baseClass, "shape", { - [`color-${entry.colorIndex}`]: entry.colorIndex != null && (isUndefined(entry.active) || entry.active), - }); - const shape = getShape(legendEntriesShape || entry.shape || "square"); - - // if the entry has a custom fill or stroke set, use it for both values - let style = { ...entry.style }; - style.fill = style.fill ?? style.stroke; - style.stroke = style.stroke ?? style.fill; - - return ( - - {shape(this.svgSize / 2, this.svgSize / 2, entry.shapeSize || this.shapeSize, { - style, - className, - })} - - ); - } -} - -Legend.prototype.name = "legend"; -Legend.prototype.baseClass = "legend"; -Legend.prototype.vertical = false; -Legend.prototype.memoize = false; -Legend.prototype.shapeSize = 18; -Legend.prototype.shape = null; -Legend.prototype.svgSize = 20; -Legend.prototype.showValues = false; -Legend.prototype.valueFormat = "s"; - -Widget.alias("legend", Legend); - -Legend.Scope = class extends PureContainer { - explore(context, instance) { - context.push("legends", (instance.legends = {})); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("legends"); - } - - prepare(context, instance) { - context.push("legends", instance.legends); - } - - prepareCleanup(context, instance) { - context.pop("legends"); - } -}; - -export const LegendScope = Legend.Scope; diff --git a/packages/cx/src/charts/Legend.scss b/packages/cx/src/charts/Legend.scss index 8a1a42711..7e5fa69c8 100644 --- a/packages/cx/src/charts/Legend.scss +++ b/packages/cx/src/charts/Legend.scss @@ -1,7 +1,9 @@ +@use "sass:map"; + @mixin cx-legend($name: "legend", $besm: $cx-besm) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { display: flex; diff --git a/packages/cx/src/charts/Legend.tsx b/packages/cx/src/charts/Legend.tsx new file mode 100644 index 000000000..b91006e76 --- /dev/null +++ b/packages/cx/src/charts/Legend.tsx @@ -0,0 +1,270 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { HtmlElement, HtmlElementConfig, HtmlElementInstance } from "../widgets/HtmlElement"; +import { PureContainer } from "../ui/PureContainer"; +import { getShape } from "./shapes"; +import { isUndefined } from "../util/isUndefined"; +import { isNonEmptyArray } from "../util/isNonEmptyArray"; +import { parseStyle } from "../util/parseStyle"; +import { withHoverSync } from "../ui/HoverSync"; +import { Format } from "../util/Format"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { StringProp, StyleProp, BooleanProp } from "../ui/Prop"; + +export interface LegendEntryData { + name: string; + displayText?: string; + active?: boolean; + colorIndex?: number; + disabled?: boolean; + selected?: boolean; + style?: any; + shape?: string; + shapeSize?: number; + hoverSync?: any; + hoverChannel?: string; + hoverId?: any; + onClick?: (e: MouseEvent) => void; + value?: number; +} + + +export interface LegendConfig extends HtmlElementConfig { + /** Name of the legend. Default is `legend`. */ + name?: string; + + /** Default shape to use for all entries. */ + shape?: StringProp; + + /** Style applied to each entry. */ + entryStyle?: StyleProp; + + /** CSS class applied to each entry. */ + entryClass?: StringProp; + + /** Style applied to the value display. */ + valueStyle?: StyleProp; + + /** CSS class applied to the value display. */ + valueClass?: StringProp; + + /** Set to `true` to show values next to legend entries. */ + showValues?: BooleanProp; + + /** Format used for displaying values. Default is `s`. */ + valueFormat?: string; + + /** Set to `true` for vertical layout. */ + vertical?: boolean; + + /** Size of the shape in pixels. Default is `18`. */ + shapeSize?: number; + + /** Size of the SVG container in pixels. Default is `20`. */ + svgSize?: number; +} + +export interface LegendInstance extends HtmlElementInstance { + legends: Record }>; +} + +export class Legend extends HtmlElement { + declare baseClass: string; + declare name: string; + declare vertical: boolean; + declare shapeSize: number; + declare shape: string | null; + declare svgSize: number; + declare showValues: boolean; + declare valueFormat: string; + declare entryStyle: any; + declare valueStyle: any; + + static Scope: typeof PureContainer; + + constructor(config: LegendConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + shape: undefined, + entryStyle: { structured: true }, + entryClass: { structured: true }, + valueStyle: { structured: true }, + valueClass: { structured: true }, + showValues: undefined, + }); + } + + init(): void { + this.entryStyle = parseStyle(this.entryStyle); + this.valueStyle = parseStyle(this.valueStyle); + super.init(); + } + + prepareData(context: RenderingContext, instance: LegendInstance): void { + let { data } = instance; + data.stateMods = Object.assign(data.stateMods || {}, { + vertical: this.vertical, + }); + super.prepareData(context, instance); + } + + isValidHtmlAttribute(attrName: string): string | false { + switch (attrName) { + case "shapeSize": + case "svgSize": + case "shape": + case "entryStyle": + case "entryClass": + case "valueStyle": + case "valueClass": + case "showValues": + case "valueFormat": + return false; + + default: + return super.isValidHtmlAttribute(attrName); + } + } + + explore(context: RenderingContext, instance: LegendInstance): void { + if (!context.legends) context.legends = {}; + + instance.legends = context.legends; + + context.addLegendEntry = (legendName: string | false, entry: LegendEntryData) => { + if (!legendName) return; + + //case when all legends are scoped and new entry is added outside the scope + if (!context.legends) return; + + let legend = context.legends[legendName]; + if (!legend) + legend = context.legends[legendName] = { + entries: [], + names: {}, + }; + + if (!legend.names[entry.name]) { + legend.entries.push(entry); + legend.names[entry.name] = entry; + } + }; + + super.explore(context, instance); + } + + renderChildren(context: RenderingContext, instance: LegendInstance): React.ReactNode[] { + const CSS = this.CSS; + + let entries = instance.legends[this.name] && instance.legends[this.name].entries, + list: React.ReactNode; + + let { entryClass, entryStyle, shape, valueClass, valueStyle } = instance.data; + let valueFormatter = Format.parse(this.valueFormat); + + let valueClasses = this.showValues ? CSS.expand(CSS.element(this.baseClass, "value"), valueClass) : undefined; + let entryTextClass = CSS.element(this.baseClass, "entry-text"); + + if (isNonEmptyArray(entries)) { + list = entries.map((e: LegendEntryData, i: number) => + withHoverSync(i, e.hoverSync, e.hoverChannel, e.hoverId, ({ onMouseMove, onMouseLeave, hover }) => ( +
+ {this.renderShape(e, shape)} +
{e.displayText || e.name}
+ {this.showValues && ( +
+ {valueFormatter(e.value)} +
+ )} +
+ )), + ); + } + + return [list, super.renderChildren(context, instance)]; + } + + renderShape(entry: LegendEntryData, legendEntriesShape: string | null | undefined): React.ReactNode { + const className = this.CSS.element(this.baseClass, "shape", { + [`color-${entry.colorIndex}`]: entry.colorIndex != null && (isUndefined(entry.active) || entry.active), + }); + const shape = getShape(legendEntriesShape || entry.shape || "square"); + + // if the entry has a custom fill or stroke set, use it for both values + let style = { ...entry.style }; + style.fill = style.fill ?? style.stroke; + style.stroke = style.stroke ?? style.fill; + + return ( + + {shape(this.svgSize / 2, this.svgSize / 2, entry.shapeSize || this.shapeSize, { + style, + className, + })} + + ); + } +} + +Legend.prototype.name = "legend"; +Legend.prototype.baseClass = "legend"; +Legend.prototype.vertical = false; +Legend.prototype.memoize = false; +Legend.prototype.shapeSize = 18; +Legend.prototype.shape = null; +Legend.prototype.svgSize = 20; +Legend.prototype.showValues = false; +Legend.prototype.valueFormat = "s"; + +Widget.alias("legend", Legend); + +interface LegendScopeInstance extends Instance { + legends: Record }>; +} + +Legend.Scope = class extends PureContainer { + explore(context: RenderingContext, instance: LegendScopeInstance): void { + context.push("legends", (instance.legends = {})); + super.explore(context, instance); + } + + exploreCleanup(context: RenderingContext, instance: LegendScopeInstance): void { + context.pop("legends"); + } + + prepare(context: RenderingContext, instance: LegendScopeInstance): void { + context.push("legends", instance.legends); + } + + prepareCleanup(context: RenderingContext, instance: LegendScopeInstance): void { + context.pop("legends"); + } +}; + +export const LegendScope = Legend.Scope; diff --git a/packages/cx/src/charts/LegendEntry.d.ts b/packages/cx/src/charts/LegendEntry.d.ts deleted file mode 100644 index c55066dcb..000000000 --- a/packages/cx/src/charts/LegendEntry.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as Cx from "../core"; - -interface LegendEntryProps extends Cx.HtmlElementProps { - /** Indicate that entry is selected. */ - selected?: Cx.BooleanProp; - - /** Shape of the symbol. `square`, `circle`, `triangle` etc. */ - shape?: Cx.StringProp; - - /** Size of the symbol in pixels. Default value is `18`. */ - size?: Cx.NumberProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Base CSS class to be applied to the element. No class is applied by default. */ - baseClass?: string; - - legendAction?: string; - - /** Size of the svg shape container in pixels. Default value is 20. */ - svgSize?: number; - - /** - * Applies to rectangular shapes. The horizontal corner radius of the rect. Defaults to ry if ry is specified. - * Value type: |; - * If unit is not specified, it defaults to `px`. - */ - rx?: Cx.StringProp | Cx.NumberProp; - - /** - * Applies to rectangular shapes. The vertical corner radius of the rect. Defaults to rx if rx is specified. - * Value type: |; - * If unit is not specified, it defaults to `px`. - */ - ry?: Cx.StringProp | Cx.NumberProp; - - /** Selection configuration. */ - selection?: Cx.Config; -} - -export class LegendEntry extends Cx.Widget {} diff --git a/packages/cx/src/charts/LegendEntry.js b/packages/cx/src/charts/LegendEntry.js deleted file mode 100644 index ef7127219..000000000 --- a/packages/cx/src/charts/LegendEntry.js +++ /dev/null @@ -1,128 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { getShape } from "./shapes"; -import { Selection } from "../ui/selection/Selection"; -import { stopPropagation } from "../util/eventCallbacks"; -import { isUndefined } from "../util/isUndefined"; -import { Container } from "../ui/Container"; - -export class LegendEntry extends Container { - init() { - this.selection = Selection.create(this.selection); - super.init(); - } - - declareData() { - var selection = this.selection.configureWidget(this); - - super.declareData(...arguments, selection, { - selected: undefined, - shape: undefined, - colorIndex: undefined, - colorMap: undefined, - colorName: undefined, - name: undefined, - active: true, - size: undefined, - rx: undefined, - ry: undefined, - text: undefined, - }); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.name && !data.colorName) data.colorName = data.name; - - super.prepareData(context, instance); - } - - explore(context, instance) { - var { data } = instance; - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - super.explore(context, instance); - } - - prepare(context, instance) { - var { data, colorMap } = instance; - - if (colorMap && data.colorName) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - } - - handleClick(e, instance) { - if (this.onClick && instance.invoke("onClick", e, instance) === false) return; - - e.stopPropagation(); - - var any = this.legendAction == "auto"; - - if (any || this.legendAction == "toggle") if (instance.set("active", !instance.data.active)) return; - - if ((any || this.legendAction == "select") && !this.selection.isDummy) this.selection.selectInstance(instance); - } - - render(context, instance, key) { - let { data } = instance; - let content = !isUndefined(this.text) ? data.text : this.renderChildren(context, instance); - return ( -
{ - this.handleClick(e, instance); - }} - > - {this.renderShape(instance)} - {content != null &&
{content}
} -
- ); - } - - renderShape(instance) { - var entry = instance.data; - var className = this.CSS.element(this.baseClass, "shape", { - disabled: entry.disabled, - selected: entry.selected || this.selection.isInstanceSelected(instance), - [`color-${entry.colorIndex}`]: entry.colorIndex != null && (isUndefined(entry.active) || entry.active), - }); - var shape = getShape(entry.shape || "square"); - - // if the entry has a custom fill or stroke set, use it for both values - let style = { ...entry.style }; - style.fill = style.fill ?? style.stroke; - style.stroke = style.stroke ?? style.fill; - - return ( - - {shape(this.svgSize / 2, this.svgSize / 2, entry.size, { - style, - className, - rx: entry.rx, - ry: entry.ry, - })} - - ); - } -} - -LegendEntry.prototype.baseClass = "legendentry"; -LegendEntry.prototype.shape = "square"; -LegendEntry.prototype.legendAction = "auto"; -LegendEntry.prototype.size = 18; -LegendEntry.prototype.svgSize = 20; -LegendEntry.prototype.styled = true; - -Widget.alias("legend-entry", LegendEntry); diff --git a/packages/cx/src/charts/LegendEntry.scss b/packages/cx/src/charts/LegendEntry.scss index 4c9f48c20..1c93c4de9 100644 --- a/packages/cx/src/charts/LegendEntry.scss +++ b/packages/cx/src/charts/LegendEntry.scss @@ -1,7 +1,9 @@ +@use "sass:map"; + @mixin cx-legendentry($name: "legendentry", $besm: $cx-besm) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { display: inline-flex; diff --git a/packages/cx/src/charts/LegendEntry.tsx b/packages/cx/src/charts/LegendEntry.tsx new file mode 100644 index 000000000..6353f1d3c --- /dev/null +++ b/packages/cx/src/charts/LegendEntry.tsx @@ -0,0 +1,197 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { getShape } from "./shapes"; +import { Selection } from "../ui/selection/Selection"; +import { stopPropagation } from "../util/eventCallbacks"; +import { isUndefined } from "../util/isUndefined"; +import { Container, ContainerConfig } from "../ui/Container"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp } from "../ui/Prop"; + +export interface LegendEntryConfig extends ContainerConfig { + /** Set to `true` if the entry is selected. */ + selected?: BooleanProp; + + /** Shape to display. Default is `square`. */ + shape?: StringProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Name of the entry. */ + name?: StringProp; + + /** Used to indicate if an entry is active or not. */ + active?: BooleanProp; + + /** Size of the shape in pixels. Default is `18`. */ + size?: NumberProp; + + /** Horizontal border radius. */ + rx?: NumberProp; + + /** Vertical border radius. */ + ry?: NumberProp; + + /** Text content of the entry. */ + text?: StringProp; + + /** Action to perform on click. Default is `auto`. */ + legendAction?: string; + + /** Size of the SVG container in pixels. Default is `20`. */ + svgSize?: number; + + /** Selection configuration. */ + selection?: any; + + /** Click event handler. */ + onClick?: (e: React.MouseEvent, instance: Instance) => void | false; +} + +export interface LegendEntryInstance extends Instance { + colorMap: any; +} + +export class LegendEntry extends Container { + declare baseClass: string; + declare shape: string; + declare legendAction: string; + declare size: number; + declare svgSize: number; + declare selection: Selection; + declare text: string; + declare onClick: LegendEntryConfig["onClick"]; + + constructor(config: LegendEntryConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection); + super.init(); + } + + declareData(...args: any[]): void { + var selection = this.selection.configureWidget(this); + + super.declareData(...args, selection, { + selected: undefined, + shape: undefined, + colorIndex: undefined, + colorMap: undefined, + colorName: undefined, + name: undefined, + active: true, + size: undefined, + rx: undefined, + ry: undefined, + text: undefined, + }); + } + + prepareData(context: RenderingContext, instance: LegendEntryInstance): void { + let { data } = instance; + + if (data.name && !data.colorName) data.colorName = data.name; + + super.prepareData(context, instance); + } + + explore(context: RenderingContext, instance: LegendEntryInstance): void { + var { data } = instance; + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + super.explore(context, instance); + } + + prepare(context: RenderingContext, instance: LegendEntryInstance): void { + var { data, colorMap } = instance; + + if (colorMap && data.colorName) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + } + + handleClick(e: React.MouseEvent, instance: LegendEntryInstance): void { + if (this.onClick && instance.invoke("onClick", e, instance) === false) return; + + e.stopPropagation(); + + var any = this.legendAction == "auto"; + + if (any || this.legendAction == "toggle") if (instance.set("active", !instance.data.active)) return; + + if ((any || this.legendAction == "select") && !this.selection.isDummy) this.selection.selectInstance(instance); + } + + render(context: RenderingContext, instance: LegendEntryInstance, key: string): React.ReactNode { + let { data } = instance; + let content = !isUndefined(this.text) ? data.text : this.renderChildren(context, instance); + return ( +
{ + this.handleClick(e, instance); + }} + > + {this.renderShape(instance)} + {content != null &&
{content}
} +
+ ); + } + + renderShape(instance: LegendEntryInstance): React.ReactNode { + var entry = instance.data; + var className = this.CSS.element(this.baseClass, "shape", { + disabled: entry.disabled, + selected: entry.selected || this.selection.isInstanceSelected(instance), + [`color-${entry.colorIndex}`]: entry.colorIndex != null && (isUndefined(entry.active) || entry.active), + }); + var shape = getShape(entry.shape || "square"); + + // if the entry has a custom fill or stroke set, use it for both values + let style = { ...entry.style }; + style.fill = style.fill ?? style.stroke; + style.stroke = style.stroke ?? style.fill; + + return ( + + {shape(this.svgSize / 2, this.svgSize / 2, entry.size, { + style, + className, + rx: entry.rx, + ry: entry.ry, + })} + + ); + } +} + +LegendEntry.prototype.baseClass = "legendentry"; +LegendEntry.prototype.shape = "square"; +LegendEntry.prototype.legendAction = "auto"; +LegendEntry.prototype.size = 18; +LegendEntry.prototype.svgSize = 20; +LegendEntry.prototype.styled = true; + +Widget.alias("legend-entry", LegendEntry); diff --git a/packages/cx/src/charts/LineGraph.d.ts b/packages/cx/src/charts/LineGraph.d.ts deleted file mode 100644 index 9f64e3281..000000000 --- a/packages/cx/src/charts/LineGraph.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as Cx from "../core"; - -interface LineGraphProps extends Cx.WidgetProps { - /** - * Data for the graph. Each entry should be an object with at least two properties - * whose names should match the `xField` and `yField` values. - */ - data?: Cx.RecordsProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** - * Additional CSS classes to be applied to the field. - * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. - */ - class?: Cx.ClassProp; - - /** - * Additional CSS classes to be applied to the field. - * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. - */ - className?: Cx.ClassProp; - - /** Additional styles to be applied to the line element. */ - lineStyle?: Cx.StyleProp; - - /** Additional styles to be applied to the area below the line. */ - areaStyle?: Cx.StyleProp; - - /** Area switch. Default value is `false`. */ - area?: Cx.BooleanProp; - - /** Line switch. By default, the line is shown. Set to `false` to hide the line and draw only the area. */ - line?: Cx.BooleanProp; - - /** Base value used for area charts. Default value is `0`. */ - y0?: Cx.NumberProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ - stack?: Cx.StringProp; - - /** Indicate that columns should be stacked on top of the other columns. Default value is `false`. */ - stacked?: Cx.BooleanProp; - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string; - - /** Name of the property which holds the x value. Default value is `x`. */ - xField?: string; - - /** Name of the property which holds the y value. Default value is `y`. */ - yField?: string; - - /** Base CSS class to be applied to the element. Defaults to `linegraph`. */ - baseClass?: string; - - /** Name of the property which holds the y0 value. Default value is `false`, which means y0 value is not read from the data array. */ - y0Field?: string | false; - - /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ - legend?: string | false; - - legendAction?: string; - legendShape?: string; - - /** Set to true to avoid forcing the vertical axis to accommodate y0 values. */ - hiddenBase?: boolean; - - /** Set to `true` to draw smoothed lines between data points using cubic Bézier curve. - * When enabled, the graph uses control points calculated from neighboring values to create smooth transitions between data points. */ - smooth?: boolean; - - /** Controls the intensity of the smoothing effect applied to Bézier curves when `smooth` is enabled. - * Accepts a number between `0` (straight lines) and `0.4` (maximum smoothing). - * Values outside this range are automatically clamped. Default value is `0.05`. */ - smoothingRatio?: number; -} - -export class LineGraph extends Cx.Widget {} diff --git a/packages/cx/src/charts/LineGraph.js b/packages/cx/src/charts/LineGraph.js deleted file mode 100644 index a39f1c56d..000000000 --- a/packages/cx/src/charts/LineGraph.js +++ /dev/null @@ -1,300 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { isArray } from "../util/isArray"; -import { parseStyle } from "../util/parseStyle"; - -export class LineGraph extends Widget { - declareData() { - super.declareData(...arguments, { - data: undefined, - colorIndex: undefined, - colorMap: undefined, - class: { - structured: true, - }, - className: { - structured: true, - }, - lineStyle: { - structured: true, - }, - areaStyle: { - structured: true, - }, - area: undefined, - line: undefined, - y0: undefined, - name: undefined, - active: true, - stack: undefined, - stacked: undefined, - smooth: undefined, - smoothingRatio: undefined, - }); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.name && !data.colorName) data.colorName = data.name; - - if (data.smooth && data.smoothingRatio != null) { - if (data.smoothingRatio < 0) data.smoothingRatio = 0; - if (data.smoothingRatio > 0.4) data.smoothingRatio = 0.4; - } - - super.prepareData(context, instance); - } - - explore(context, instance) { - let { data } = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - if (data.active) { - instance.axes = context.axes; - instance.xAxis = instance.axes[this.xAxis]; - instance.yAxis = instance.axes[this.yAxis]; - super.explore(context, instance); - if (isArray(data.data)) { - data.data.forEach((p, index) => { - let x = p[this.xField]; - instance.xAxis.acknowledge(x); - if (data.stacked) { - instance.yAxis.stacknowledge(data.stack, x, this.y0Field ? p[this.y0Field] : data.y0); - instance.yAxis.stacknowledge(data.stack, x, p[this.yField]); - } else { - instance.yAxis.acknowledge(p[this.yField]); - if (data.area) { - if (!this.hiddenBase) instance.yAxis.acknowledge(this.y0Field ? p[this.y0Field] : data.y0); - } - } - }); - } - } - } - - prepare(context, instance) { - let { data, colorMap } = instance; - - if (colorMap && data.colorName) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (data.active) { - if (instance.axes[this.xAxis].shouldUpdate || instance.axes[this.yAxis].shouldUpdate) - instance.markShouldUpdate(context); - } - - if (data.name && context.addLegendEntry) { - context.addLegendEntry(this.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - disabled: data.disabled, - style: { - ...parseStyle(data.style), - ...parseStyle(data.areaStyle), - ...parseStyle(data.lineStyle), - }, - shape: this.legendShape, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - } - - if (data.active) { - if (context.pointReducer && isArray(data.data)) { - data.data.forEach((p, index) => { - if (data.area && this.y0Field) - context.pointReducer(p[this.xField], p[this.y0Field], data.name, p, data.data, index); - context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); - }); - } - } - - instance.lineSpans = this.calculateLineSpans(context, instance); - } - - onLegendClick(e, instance) { - let allActions = this.legendAction == "auto"; - let { data } = instance; - if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); - } - - calculateLineSpans(context, instance) { - let { data, xAxis, yAxis } = instance; - let spans = []; - let span = []; - - if (!data.active) return null; - - isArray(data.data) && - data.data.forEach((p) => { - let ax = p[this.xField], - ay = p[this.yField], - ay0 = this.y0Field ? p[this.y0Field] : data.y0, - x, - y, - y0; - - if (ax != null && ay != null && ay0 != null) { - x = xAxis.map(ax); - y0 = data.stacked ? yAxis.stack(data.stack, ax, ay0) : yAxis.map(ay0); - y = data.stacked ? yAxis.stack(data.stack, ax, ay) : yAxis.map(ay); - } - - if (x != null && y != null && y0 != null) span.push({ x, y, y0 }); - else if (span.length > 0) { - spans.push(span); - span = []; - } - }); - - if (span.length > 0) spans.push(span); - return spans; - } - - render(context, instance, key) { - let { data, lineSpans } = instance; - - if (!lineSpans) return null; - - let stateMods = { - ["color-" + data.colorIndex]: data.colorIndex != null, - }; - - let line, area; - const r = data.smoothingRatio; - - let linePath = ""; - if (data.line) { - lineSpans.forEach((span) => { - span.forEach((p, i) => { - linePath += - i == 0 - ? `M ${p.x} ${p.y}` - : !data.smooth || span.length < 2 - ? `L ${p.x} ${p.y}` - : this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r); - }); - }); - - line = ( - - ); - } - - if (data.area) { - let areaPath = ""; - lineSpans.forEach((span) => { - let closePath = ""; - span.forEach((p, i) => { - let segment = ""; - if (i == 0) { - segment = `M ${p.x} ${p.y}`; - - // closing point - closePath = - !data.smooth || span.length < 2 - ? `L ${p.x} ${p.y0}` - : this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0"); - } else { - if (!data.smooth) { - segment = `L ${p.x} ${p.y}`; - closePath = `L ${p.x} ${p.y0}` + closePath; - } else { - segment = this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r, "y"); - - // closing point - if (i < span.length - 1) - closePath = this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0") + closePath; - } - } - areaPath += segment; - }); - - areaPath += `L ${span[span.length - 1].x} ${span[span.length - 1].y0}`; - areaPath += closePath; - areaPath += "Z"; - }); - - area = ( - - ); - } - - return ( - - {line} - {area} - - ); - } - - getCurvedPathSegment(p, points, i1, i2, j1, j2, r, yField = "y") { - const [sx, sy] = this.getControlPoint({ cp: points[i1], pp: points[i2], r, np: p, yField }); - const [ex, ey] = this.getControlPoint({ cp: p, pp: points[j1], np: points[j2], r, reverse: true, yField }); - - return `C ${sx} ${sy}, ${ex} ${ey}, ${p.x} ${p[yField]}`; - } - - getControlPoint({ cp, pp, np, r, reverse, yField = "y" }) { - // When 'current' is the first or last point of the array 'previous' or 'next' don't exist. Replace with 'current'. - const p = pp || cp; - const n = np || cp; - - // Properties of the opposed-line - let { angle, length } = this.getLineInfo(p.x, p[yField], n.x, n[yField]); - // If it is end-control-point, add PI to the angle to go backward - angle = angle + (reverse ? Math.PI : 0); - length = length * r; - // The control point position is relative to the current point - const x = cp.x + Math.cos(angle) * length; - const y = cp[yField] + Math.sin(angle) * length; - return [x, y]; - } - - getLineInfo(p1x, p1y, p2x, p2y) { - const lengthX = p2x - p1x; - const lengthY = p2y - p1y; - - return { - length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)), - angle: Math.atan2(lengthY, lengthX), - }; - } -} - -LineGraph.prototype.xAxis = "x"; -LineGraph.prototype.yAxis = "y"; -LineGraph.prototype.area = false; -LineGraph.prototype.line = true; - -LineGraph.prototype.xField = "x"; -LineGraph.prototype.yField = "y"; -LineGraph.prototype.baseClass = "linegraph"; -LineGraph.prototype.y0 = 0; -LineGraph.prototype.y0Field = false; -LineGraph.prototype.active = true; -LineGraph.prototype.legend = "legend"; -LineGraph.prototype.legendAction = "auto"; -LineGraph.prototype.legendShape = "rect"; -LineGraph.prototype.stack = "stack"; -LineGraph.prototype.hiddenBase = false; - -LineGraph.prototype.smooth = false; -LineGraph.prototype.smoothingRatio = 0.05; - -Widget.alias("line-graph", LineGraph); diff --git a/packages/cx/src/charts/LineGraph.scss b/packages/cx/src/charts/LineGraph.scss index 9b49d4724..2191fc5f5 100644 --- a/packages/cx/src/charts/LineGraph.scss +++ b/packages/cx/src/charts/LineGraph.scss @@ -1,11 +1,12 @@ +@use "sass:map"; @mixin cx-linegraph( $name: 'linegraph', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-line { stroke: gray; diff --git a/packages/cx/src/charts/LineGraph.tsx b/packages/cx/src/charts/LineGraph.tsx new file mode 100644 index 000000000..9d5c17bb5 --- /dev/null +++ b/packages/cx/src/charts/LineGraph.tsx @@ -0,0 +1,455 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { isArray } from "../util/isArray"; +import { parseStyle } from "../util/parseStyle"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp, RecordsProp, StyleProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; +import { ClassProp } from "../ui/Prop"; + +interface LinePoint { + x: number; + y: number; + y0: number; +} + +export interface LineGraphConfig extends WidgetConfig { + /** Data for the graph. Each entry should be an object with at least two properties + * whose names should match the `xField` and `yField` values. + */ + data?: RecordsProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ + stack?: StringProp; + + /** Indicate that values should be stacked on top of the other values. Default value is `false`. */ + stacked?: BooleanProp; + + /** Set to `true` to enable smooth (curved) line rendering. */ + smooth?: BooleanProp; + + /** Controls the curvature of smooth lines. Value should be between 0 and 0.4. Default is 0.05. */ + smoothingRatio?: NumberProp; + + /** Name of the horizontal axis. Default value is `x`. */ + xAxis?: string; + + /** Name of the vertical axis. Default value is `y`. */ + yAxis?: string; + + /** Name of the property which holds the x value. Default value is `x`. */ + xField?: string; + + /** Name of the property which holds the y value. Default value is `y`. */ + yField?: string; + + /** Name of the property which holds the base value. Default value is `false`, meaning y0 is used instead. */ + y0Field?: string | false; + + /** Base value. Default value is `0`. */ + y0?: NumberProp; + + /** Hide the base value. */ + hiddenBase?: boolean; + + /** Set to `true` to enable area rendering. */ + area?: BooleanProp; + + /** Set to `false` to disable line rendering. Default is `true`. */ + line?: BooleanProp; + + /** Style for the line element. */ + lineStyle?: StyleProp; + + /** Style for the area element. */ + areaStyle?: StyleProp; + + /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ + legend?: string | false; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; + + /** Shape to use in legend. */ + legendShape?: string; + + /** + * Additional CSS classes to be applied to the field. + * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. + */ + class?: ClassProp; + + /** + * Additional CSS classes to be applied to the field. + * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. + */ + className?: ClassProp; +} + +export interface LineGraphInstance extends Instance { + xAxis: any; + yAxis: any; + axes: Record; + colorMap: any; + lineSpans: LinePoint[][] | null; +} + +export class LineGraph extends Widget { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xField: string; + declare yField: string; + declare y0Field: string | false; + declare y0: number; + declare hiddenBase: boolean; + declare area: boolean; + declare line: boolean; + declare active: boolean; + declare legend: string | false; + declare legendAction: string; + declare legendShape: string; + declare stack: string; + declare smooth: boolean; + declare smoothingRatio: number; + + constructor(config: LineGraphConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + data: undefined, + colorIndex: undefined, + colorMap: undefined, + class: { + structured: true, + }, + className: { + structured: true, + }, + lineStyle: { + structured: true, + }, + areaStyle: { + structured: true, + }, + area: undefined, + line: undefined, + y0: undefined, + name: undefined, + active: true, + stack: undefined, + stacked: undefined, + smooth: undefined, + smoothingRatio: undefined, + }); + } + + prepareData(context: RenderingContext, instance: LineGraphInstance): void { + let { data } = instance; + + if (data.name && !data.colorName) data.colorName = data.name; + + if (data.smooth && data.smoothingRatio != null) { + if (data.smoothingRatio < 0) data.smoothingRatio = 0; + if (data.smoothingRatio > 0.4) data.smoothingRatio = 0.4; + } + + super.prepareData(context, instance); + } + + explore(context: ChartRenderingContext, instance: LineGraphInstance): void { + let { data } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + if (data.active) { + instance.axes = context.axes!; + instance.xAxis = instance.axes[this.xAxis]; + instance.yAxis = instance.axes[this.yAxis]; + super.explore(context, instance); + if (isArray(data.data)) { + data.data.forEach((p: any) => { + let x = p[this.xField]; + instance.xAxis.acknowledge(x); + if (data.stacked) { + instance.yAxis.stacknowledge(data.stack, x, this.y0Field ? p[this.y0Field] : data.y0); + instance.yAxis.stacknowledge(data.stack, x, p[this.yField]); + } else { + instance.yAxis.acknowledge(p[this.yField]); + if (data.area) { + if (!this.hiddenBase) instance.yAxis.acknowledge(this.y0Field ? p[this.y0Field] : data.y0); + } + } + }); + } + } + } + + prepare(context: ChartRenderingContext, instance: LineGraphInstance): void { + let { data, colorMap } = instance; + + if (colorMap && data.colorName) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (data.active) { + if (instance.axes[this.xAxis].shouldUpdate || instance.axes[this.yAxis].shouldUpdate) + instance.markShouldUpdate(context); + } + + if (data.name && context.addLegendEntry) { + context.addLegendEntry(this.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + disabled: data.disabled, + style: { + ...parseStyle(data.style), + ...parseStyle(data.areaStyle), + ...parseStyle(data.lineStyle), + }, + shape: this.legendShape, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + if (data.active) { + if (context.pointReducer && isArray(data.data)) { + data.data.forEach((p: any, index: number) => { + if (data.area && this.y0Field) + context.pointReducer(p[this.xField], p[this.y0Field], data.name, p, data.data, index); + context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); + }); + } + } + + instance.lineSpans = this.calculateLineSpans(context, instance); + } + + onLegendClick(e: MouseEvent, instance: LineGraphInstance): void { + let allActions = this.legendAction == "auto"; + let { data } = instance; + if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); + } + + calculateLineSpans(context: RenderingContext, instance: LineGraphInstance): LinePoint[][] | null { + let { data, xAxis, yAxis } = instance; + let spans: LinePoint[][] = []; + let span: LinePoint[] = []; + + if (!data.active) return null; + + isArray(data.data) && + data.data.forEach((p: any) => { + let ax = p[this.xField], + ay = p[this.yField], + ay0 = this.y0Field ? p[this.y0Field] : data.y0, + x: number | undefined, + y: number | undefined, + y0: number | undefined; + + if (ax != null && ay != null && ay0 != null) { + x = xAxis.map(ax); + y0 = data.stacked ? yAxis.stack(data.stack, ax, ay0) : yAxis.map(ay0); + y = data.stacked ? yAxis.stack(data.stack, ax, ay) : yAxis.map(ay); + } + + if (x != null && y != null && y0 != null) span.push({ x, y, y0 }); + else if (span.length > 0) { + spans.push(span); + span = []; + } + }); + + if (span.length > 0) spans.push(span); + return spans; + } + + render(context: RenderingContext, instance: LineGraphInstance, key: string): React.ReactNode { + let { data, lineSpans } = instance; + + if (!lineSpans) return null; + + let stateMods: Record = { + ["color-" + data.colorIndex]: data.colorIndex != null, + }; + + let line: React.ReactNode, area: React.ReactNode; + const r = data.smoothingRatio; + + let linePath = ""; + if (data.line) { + lineSpans.forEach((span) => { + span.forEach((p, i) => { + linePath += + i == 0 + ? `M ${p.x} ${p.y}` + : !data.smooth || span.length < 2 + ? `L ${p.x} ${p.y}` + : this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r); + }); + }); + + line = ( + + ); + } + + if (data.area) { + let areaPath = ""; + lineSpans.forEach((span) => { + let closePath = ""; + span.forEach((p, i) => { + let segment = ""; + if (i == 0) { + segment = `M ${p.x} ${p.y}`; + + // closing point + closePath = + !data.smooth || span.length < 2 + ? `L ${p.x} ${p.y0}` + : this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0"); + } else { + if (!data.smooth) { + segment = `L ${p.x} ${p.y}`; + closePath = `L ${p.x} ${p.y0}` + closePath; + } else { + segment = this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r, "y"); + + // closing point + if (i < span.length - 1) + closePath = this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0") + closePath; + } + } + areaPath += segment; + }); + + areaPath += `L ${span[span.length - 1].x} ${span[span.length - 1].y0}`; + areaPath += closePath; + areaPath += "Z"; + }); + + area = ( + + ); + } + + return ( + + {line} + {area} + + ); + } + + getCurvedPathSegment( + p: LinePoint, + points: LinePoint[], + i1: number, + i2: number, + j1: number, + j2: number, + r: number, + yField: "y" | "y0" = "y", + ): string { + const [sx, sy] = this.getControlPoint({ cp: points[i1], pp: points[i2], r, np: p, yField }); + const [ex, ey] = this.getControlPoint({ cp: p, pp: points[j1], np: points[j2], r, reverse: true, yField }); + + return `C ${sx} ${sy}, ${ex} ${ey}, ${p.x} ${p[yField]}`; + } + + getControlPoint({ + cp, + pp, + np, + r, + reverse, + yField = "y", + }: { + cp: LinePoint; + pp: LinePoint | undefined; + np: LinePoint | undefined; + r: number; + reverse?: boolean; + yField?: "y" | "y0"; + }): [number, number] { + // When 'current' is the first or last point of the array 'previous' or 'next' don't exist. Replace with 'current'. + const p = pp || cp; + const n = np || cp; + + // Properties of the opposed-line + let { angle, length } = this.getLineInfo(p.x, p[yField], n.x, n[yField]); + // If it is end-control-point, add PI to the angle to go backward + angle = angle + (reverse ? Math.PI : 0); + length = length * r; + // The control point position is relative to the current point + const x = cp.x + Math.cos(angle) * length; + const y = cp[yField] + Math.sin(angle) * length; + return [x, y]; + } + + getLineInfo(p1x: number, p1y: number, p2x: number, p2y: number): { length: number; angle: number } { + const lengthX = p2x - p1x; + const lengthY = p2y - p1y; + + return { + length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)), + angle: Math.atan2(lengthY, lengthX), + }; + } +} + +LineGraph.prototype.xAxis = "x"; +LineGraph.prototype.yAxis = "y"; +LineGraph.prototype.area = false; +LineGraph.prototype.line = true; + +LineGraph.prototype.xField = "x"; +LineGraph.prototype.yField = "y"; +LineGraph.prototype.baseClass = "linegraph"; +LineGraph.prototype.y0 = 0; +LineGraph.prototype.y0Field = false; +LineGraph.prototype.active = true; +LineGraph.prototype.legend = "legend"; +LineGraph.prototype.legendAction = "auto"; +LineGraph.prototype.legendShape = "rect"; +LineGraph.prototype.stack = "stack"; +LineGraph.prototype.hiddenBase = false; + +LineGraph.prototype.smooth = false; +LineGraph.prototype.smoothingRatio = 0.05; +LineGraph.prototype.styled = true; + +Widget.alias("line-graph", LineGraph); diff --git a/packages/cx/src/charts/Marker.d.ts b/packages/cx/src/charts/Marker.d.ts deleted file mode 100644 index 7a2c44a26..000000000 --- a/packages/cx/src/charts/Marker.d.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; - -interface MarkerProps extends BoundedObjectProps { - /** The `x` value binding or expression. */ - x?: Cx.Prop; - - /** The `y` value binding or expression. */ - y?: Cx.Prop; - - /** Used to indicate if the data should affect axis span. */ - affectsAxes?: Cx.BooleanProp; - - /** Shape kind. `circle`, `square`, `triangle`, etc. */ - shape?: Cx.StringProp; - - disabled?: Cx.BooleanProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.Prop; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - legendColorIndex?: Cx.NumberProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - xOffset?: number; - yOffset?: number; - - /** Size of the shape in pixels. */ - size?: Cx.NumberProp; - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string; - - /** Base CSS class to be applied to the element. Defaults to `marker`. */ - baseClass?: string; - - /** Set to `true` to make the shape draggable along the X axis. */ - draggableX?: boolean; - - /** Set to `true` to make the shape draggable along the Y axis. */ - draggableY?: boolean; - - /** Set to `true` to make the shape draggable along the X and Y axis. */ - draggable?: boolean; - - /** Constrain the marker position to min/max values of the X axis during drag operations. */ - constrainX?: boolean; - - /** Constrain the marker position to min/max values of the Y axis during drag operations. */ - constrainY?: boolean; - - /** When set to `true`, it is equivalent to setting `constrainX` and `constrainY` to true. */ - constrain?: boolean; - - /** Name of the legend to be used. Default is `legend`. */ - legend?: string; - - legendAction?: string; - - /** Tooltip configuration. For more info see Tooltips. */ - tooltip?: Cx.StringProp | Cx.StructuredProp; - - /** Set to true to hide the marker. The marker will still participate in axis range calculations. */ - hidden?: boolean; - - /** Indicate that markers should be stacked horizontally. Default value is `false`. */ - stackedX?: Cx.BooleanProp; - - /** Indicate that markers should be stacked vertically. Default value is `false`. */ - stackedY?: Cx.BooleanProp; - - /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ - stack?: Cx.StringProp; - - /** - * Applies to rectangular shapes. The horizontal corner radius of the rect. Defaults to ry if ry is specified. - * Value type: |; - * If unit is not specified, it defaults to `px`. - */ - rx?: Cx.StringProp | Cx.NumberProp; - - /** - * Applies to rectangular shapes. The vertical corner radius of the rect. Defaults to rx if rx is specified. - * Value type: |; - * If unit is not specified, it defaults to `px`. - */ - ry?: Cx.StringProp | Cx.NumberProp; -} - -export class Marker extends Cx.Widget {} diff --git a/packages/cx/src/charts/Marker.js b/packages/cx/src/charts/Marker.js deleted file mode 100644 index 928bec7a9..000000000 --- a/packages/cx/src/charts/Marker.js +++ /dev/null @@ -1,311 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { BoundedObject } from "../svg/BoundedObject"; -import { Rect } from "../svg/util/Rect"; -import { - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentWillUnmount, - tooltipParentWillReceiveProps, - tooltipParentDidMount, - tooltipParentDidUpdate, -} from "../widgets/overlay/tooltip-ops"; -import { captureMouseOrTouch, getCursorPos } from "../widgets/overlay/captureMouse"; -import { closest } from "../util/DOM"; -import { Selection } from "../ui/selection/Selection"; -import { getShape } from "./shapes"; -import { getTopLevelBoundingClientRect } from "../util/getTopLevelBoundingClientRect"; - -export class Marker extends BoundedObject { - init() { - this.selection = Selection.create(this.selection); - - if (this.draggable) { - this.draggableX = true; - this.draggableY = true; - } - - if (this.constrain) { - this.constrainX = true; - this.constrainY = true; - } - - super.init(); - } - - declareData() { - var selection = this.selection.configureWidget(this); - - return super.declareData(...arguments, selection, { - x: undefined, - y: undefined, - size: undefined, - shape: undefined, - disabled: undefined, - colorMap: undefined, - colorIndex: undefined, - colorName: undefined, - legendColorIndex: undefined, - name: undefined, - active: true, - stack: undefined, - stackedX: undefined, - stackedY: undefined, - rx: undefined, - ry: undefined, - }); - } - - prepareData(context, instance) { - instance.axes = context.axes; - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - let { data } = instance; - data.selected = this.selection.isInstanceSelected(instance); - data.stateMods = { - selected: data.selected, - disabled: data.disabled, - selectable: !this.selection.isDummy, - "draggable-x": this.draggableX && !this.draggableY, - "draggable-y": this.draggableY && !this.draggableX, - "draggable-xy": this.draggableY && this.draggableX, - }; - if (data.name && !data.colorName) data.colorName = data.name; - super.prepareData(context, instance); - } - - calculateBounds(context, instance) { - let { data, xAxis, yAxis } = instance; - - let x, y; - - if (data.x == null || data.y == null) { - let bounds = super.calculateBounds(context, instance); - x = (bounds.l + bounds.r) / 2; - y = (bounds.t + bounds.b) / 2; - } - - if (data.x != null) x = data.stackedX ? xAxis.stack(data.stack, data.y, data.x) : xAxis.map(data.x); - - if (data.y != null) y = data.stackedY ? yAxis.stack(data.stack, data.x, data.y) : yAxis.map(data.y); - - return new Rect({ - l: x - data.size / 2, - r: x + data.size / 2, - t: y - data.size / 2, - b: y + data.size / 2, - }); - } - - explore(context, instance) { - let { data, xAxis, yAxis } = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - if (data.active) { - if (this.affectsAxes) { - if (xAxis && data.x != null) { - if (data.stackedX) xAxis.stacknowledge(data.stack, data.y, data.x); - else xAxis.acknowledge(data.x, 0, this.xOffset); - } - - if (yAxis && data.y != null) { - if (data.stackedY) yAxis.stacknowledge(data.stack, data.x, data.y); - else yAxis.acknowledge(data.y, 0, this.yOffset); - } - } - super.explore(context, instance); - } - } - - prepare(context, instance) { - let { data, xAxis, yAxis, colorMap } = instance; - - if (colorMap && data.colorName) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (data.active) { - if (xAxis && xAxis.shouldUpdate) instance.markShouldUpdate(context); - - if (yAxis && yAxis.shouldUpdate) instance.markShouldUpdate(context); - - if (context.pointReducer) context.pointReducer(data.x, data.y, data.name, data); - } - - super.prepare(context, instance); - - if (data.name && context.addLegendEntry) - context.addLegendEntry(this.legend, { - name: data.name, - active: data.active, - colorIndex: data.legendColorIndex || data.colorIndex, - disabled: data.disabled, - selected: data.selected, - style: data.style, - shape: data.shape, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - } - - onLegendClick(e, instance) { - let allActions = this.legendAction == "auto"; - let { data } = instance; - if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; - - if (allActions || this.legendAction == "select") this.handleClick(e, instance); - } - - render(context, instance, key) { - let { data } = instance; - - if (!data.active || data.x === null || data.y === null) return null; - - return ( - - {this.renderChildren(context, instance)} - - ); - } - - handleMouseDown(e, instance) { - if (this.draggableX || this.draggableY) { - let svgEl = closest(e.target, (el) => el.tagName == "svg"); - if (svgEl) - captureMouseOrTouch( - e, - (e, captureData) => { - this.handleDragMove(e, instance, captureData); - }, - null, - { svgEl, el: e.target }, - e.target.style.cursor, - ); - } else { - if (!this.selection.isDummy) this.selection.selectInstance(instance); - } - } - - handleClick(e, instance) { - if (this.onClick) instance.invoke("onClick", e, instance); - } - - handleDragMove(e, instance, captureData) { - let cursor = getCursorPos(e); - let svgBounds = getTopLevelBoundingClientRect(captureData.svgEl); - let { xAxis, yAxis } = instance; - if (this.draggableX && xAxis) { - let x = xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset); - if (this.constrainX) x = xAxis.constrainValue(x); - instance.set("x", xAxis.encodeValue(x)); - } - if (this.draggableY && yAxis) { - let y = yAxis.trackValue(cursor.clientY - svgBounds.top, this.yOffset); - if (this.constrainY) y = yAxis.constrainValue(y); - instance.set("y", yAxis.encodeValue(y)); - } - tooltipMouseMove(e, instance, this.tooltip, { target: captureData.el }); - } -} - -Marker.prototype.xOffset = 0; -Marker.prototype.yOffset = 0; -Marker.prototype.size = 5; -Marker.prototype.anchors = "0.5 0.5 0.5 0.5"; - -Marker.prototype.xAxis = "x"; -Marker.prototype.yAxis = "y"; - -Marker.prototype.baseClass = "marker"; -Marker.prototype.draggableX = false; -Marker.prototype.draggableY = false; -Marker.prototype.draggable = false; -Marker.prototype.constrainX = false; -Marker.prototype.constrainY = false; -Marker.prototype.constrain = false; -Marker.prototype.legend = "legend"; -Marker.prototype.legendAction = "auto"; -Marker.prototype.shape = "circle"; -Marker.prototype.styled = true; -Marker.prototype.hidden = false; -Marker.prototype.affectsAxes = true; -Marker.prototype.stackedY = false; -Marker.prototype.stackedX = false; -Marker.prototype.stack = "stack"; - -BoundedObject.alias("marker", Marker); - -class MarkerComponent extends VDOM.Component { - shouldComponentUpdate(props) { - return props.shouldUpdate; - } - - render() { - let { instance, children, data } = this.props; - let { widget } = instance; - let { CSS, baseClass } = widget; - let { bounds, shape } = data; - let shapeRenderer = getShape(shape); - let shapeProps = { - className: CSS.element(baseClass, "shape", { - ["color-" + data.colorIndex]: data.colorIndex != null, - selected: data.selected, - }), - style: data.style, - cx: (bounds.l + bounds.r) / 2, - cy: (bounds.t + bounds.b) / 2, - r: data.size / 2, - onMouseMove: (e) => { - tooltipMouseMove(e, instance, widget.tooltip); - }, - onMouseLeave: (e) => { - tooltipMouseLeave(e, instance, widget.tooltip); - }, - onMouseDown: (e) => { - widget.handleMouseDown(e, instance); - }, - onTouchStart: (e) => { - widget.handleMouseDown(e, instance); - }, - onClick: (e) => { - widget.handleClick(e, instance); - }, - }; - - if (shape == "rect" || shape == "square" || shape == "bar" || shape == "column") { - shapeProps.rx = data.rx; - shapeProps.ry = data.ry; - } - - if (widget.tooltip) { - shapeProps.ref = (c) => { - this.el = c; - }; - } - - return ( - - {!widget.hidden && - shapeRenderer((bounds.l + bounds.r) / 2, (bounds.t + bounds.b) / 2, data.size, shapeProps)} - {children} - - ); - } - - componentWillUnmount() { - tooltipParentWillUnmount(this.props.instance); - } - UNSAFE_componentWillReceiveProps(props) { - tooltipParentWillReceiveProps(this.el, props.instance, props.instance.widget.tooltip); - } - componentDidMount() { - tooltipParentDidMount(this.el, this.props.instance, this.props.instance.widget.tooltip); - } - - componentDidUpdate() { - tooltipParentDidUpdate(this.el, this.props.instance, this.props.instance.widget.tooltip); - } -} diff --git a/packages/cx/src/charts/Marker.scss b/packages/cx/src/charts/Marker.scss index 67addc79f..b6f6628f9 100644 --- a/packages/cx/src/charts/Marker.scss +++ b/packages/cx/src/charts/Marker.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-marker( $name: 'marker', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-shape { fill: rgb(128, 128, 128); diff --git a/packages/cx/src/charts/Marker.tsx b/packages/cx/src/charts/Marker.tsx new file mode 100644 index 000000000..f6bcda8b3 --- /dev/null +++ b/packages/cx/src/charts/Marker.tsx @@ -0,0 +1,483 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM } from "../ui/Widget"; +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { Rect } from "../svg/util/Rect"; +import { + tooltipMouseMove, + tooltipMouseLeave, + tooltipParentWillUnmount, + tooltipParentWillReceiveProps, + tooltipParentDidMount, + tooltipParentDidUpdate, + TooltipParentInstance, + TooltipConfig, +} from "../widgets/overlay/tooltip-ops"; +import { captureMouseOrTouch, getCursorPos } from "../widgets/overlay/captureMouse"; +import { closest } from "../util/DOM"; +import { Selection } from "../ui/selection/Selection"; +import { getShape } from "./shapes"; +import { getTopLevelBoundingClientRect } from "../util/getTopLevelBoundingClientRect"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp, StructuredProp } from "../ui/Prop"; +import { Instance } from "../ui/Instance"; +import type { ChartRenderingContext } from "./Chart"; + +export interface MarkerConfig extends BoundedObjectConfig { + /** The `x` value binding or expression. */ + x?: NumberProp | StringProp; + + /** The `y` value binding or expression. */ + y?: NumberProp; + + /** Used to indicate if the data should affect axis span. */ + affectsAxes?: boolean; + + /** Shape kind. `circle`, `square`, `triangle`, etc. */ + shape?: StringProp; + + /** Disabled state. */ + disabled?: BooleanProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp | StringProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Color index to use for the legend entry. */ + legendColorIndex?: NumberProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** X offset. */ + xOffset?: number; + + /** Y offset. */ + yOffset?: number; + + /** Size of the shape in pixels. */ + size?: NumberProp; + + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + */ + xAxis?: string; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + */ + yAxis?: string; + + /** Set to `true` to make the shape draggable along the X axis. */ + draggableX?: boolean; + + /** Set to `true` to make the shape draggable along the Y axis. */ + draggableY?: boolean; + + /** Set to `true` to make the shape draggable along the X and Y axis. */ + draggable?: boolean; + + /** Constrain the marker position to min/max values of the X axis during drag operations. */ + constrainX?: boolean; + + /** Constrain the marker position to min/max values of the Y axis during drag operations. */ + constrainY?: boolean; + + /** When set to `true`, it is equivalent to setting `constrainX` and `constrainY` to true. */ + constrain?: boolean; + + /** Name of the legend to be used. Default is `legend`. */ + legend?: string; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; + + /** Tooltip configuration. For more info see Tooltips. */ + tooltip?: StringProp | StructuredProp; + + /** Set to true to hide the marker. The marker will still participate in axis range calculations. */ + hidden?: boolean; + + /** Indicate that markers should be stacked horizontally. Default value is `false`. */ + stackedX?: BooleanProp; + + /** Indicate that markers should be stacked vertically. Default value is `false`. */ + stackedY?: BooleanProp; + + /** Name of the stack. If multiple stacks are used, each should have a unique name. Default value is `stack`. */ + stack?: StringProp; + + /** + * Applies to rectangular shapes. The horizontal corner radius of the rect. Defaults to ry if ry is specified. + * Value type: |; + * If unit is not specified, it defaults to `px`. + */ + rx?: StringProp | NumberProp; + + /** + * Applies to rectangular shapes. The vertical corner radius of the rect. Defaults to rx if rx is specified. + * Value type: |; + * If unit is not specified, it defaults to `px`. + */ + ry?: StringProp | NumberProp; + + /** Selection configuration. */ + selection?: any; + + /** Click event handler. */ + onClick?: (e: React.MouseEvent, instance: Instance) => void; +} + +export interface MarkerInstance extends BoundedObjectInstance, TooltipParentInstance { + axes: Record; + xAxis: any; + yAxis: any; + colorMap: any; +} + +export class Marker extends BoundedObject { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xOffset: number; + declare yOffset: number; + declare size: number; + declare draggableX: boolean; + declare draggableY: boolean; + declare draggable: boolean; + declare constrainX: boolean; + declare constrainY: boolean; + declare constrain: boolean; + declare legend: string; + declare legendAction: string; + declare shape: string; + declare hidden: boolean; + declare affectsAxes: boolean; + declare stackedY: boolean; + declare stackedX: boolean; + declare stack: string; + declare selection: Selection; + declare tooltip: TooltipConfig; + declare onClick: MarkerConfig["onClick"]; + + constructor(config: MarkerConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection); + + if (this.draggable) { + this.draggableX = true; + this.draggableY = true; + } + + if (this.constrain) { + this.constrainX = true; + this.constrainY = true; + } + + super.init(); + } + + declareData(...args: any[]): void { + var selection = this.selection.configureWidget(this); + + super.declareData(...args, selection, { + x: undefined, + y: undefined, + size: undefined, + shape: undefined, + disabled: undefined, + colorMap: undefined, + colorIndex: undefined, + colorName: undefined, + legendColorIndex: undefined, + name: undefined, + active: true, + stack: undefined, + stackedX: undefined, + stackedY: undefined, + rx: undefined, + ry: undefined, + }); + } + + prepareData(context: RenderingContext, instance: MarkerInstance): void { + instance.axes = context.axes; + instance.xAxis = context.axes[this.xAxis]; + instance.yAxis = context.axes[this.yAxis]; + let { data } = instance; + data.selected = this.selection.isInstanceSelected(instance); + data.stateMods = { + selected: data.selected, + disabled: data.disabled, + selectable: !this.selection.isDummy, + "draggable-x": this.draggableX && !this.draggableY, + "draggable-y": this.draggableY && !this.draggableX, + "draggable-xy": this.draggableY && this.draggableX, + }; + if (data.name && !data.colorName) data.colorName = data.name; + super.prepareData(context, instance); + } + + calculateBounds(context: RenderingContext, instance: MarkerInstance): Rect { + let { data, xAxis, yAxis } = instance; + + let x!: number, y!: number; + + if (data.x == null || data.y == null) { + let bounds = super.calculateBounds(context, instance); + x = (bounds.l + bounds.r) / 2; + y = (bounds.t + bounds.b) / 2; + } + + if (data.x != null) x = data.stackedX ? xAxis.stack(data.stack, data.y, data.x) : xAxis.map(data.x); + + if (data.y != null) y = data.stackedY ? yAxis.stack(data.stack, data.x, data.y) : yAxis.map(data.y); + + return new Rect({ + l: x - data.size / 2, + r: x + data.size / 2, + t: y - data.size / 2, + b: y + data.size / 2, + }); + } + + explore(context: RenderingContext, instance: MarkerInstance): void { + let { data, xAxis, yAxis } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + if (data.active) { + if (this.affectsAxes) { + if (xAxis && data.x != null) { + if (data.stackedX) xAxis.stacknowledge(data.stack, data.y, data.x); + else xAxis.acknowledge(data.x, 0, this.xOffset); + } + + if (yAxis && data.y != null) { + if (data.stackedY) yAxis.stacknowledge(data.stack, data.x, data.y); + else yAxis.acknowledge(data.y, 0, this.yOffset); + } + } + super.explore(context, instance); + } + } + + prepare(context: RenderingContext, instance: MarkerInstance): void { + let { data, xAxis, yAxis, colorMap } = instance; + + if (colorMap && data.colorName) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (data.active) { + if (xAxis && xAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (yAxis && yAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (context.pointReducer) context.pointReducer(data.x, data.y, data.name, data); + } + + super.prepare(context, instance); + + if (data.name && context.addLegendEntry) + context.addLegendEntry(this.legend, { + name: data.name, + active: data.active, + colorIndex: data.legendColorIndex || data.colorIndex, + disabled: data.disabled, + selected: data.selected, + style: data.style, + shape: data.shape, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + onLegendClick(e: MouseEvent, instance: MarkerInstance): void { + let allActions = this.legendAction == "auto"; + let { data } = instance; + if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; + + if (allActions || this.legendAction == "select") this.handleClick(e as unknown as React.MouseEvent, instance); + } + + render(context: RenderingContext, instance: MarkerInstance, key: string): React.ReactNode { + let { data } = instance; + + if (!data.active || data.x === null || data.y === null) return null; + + return ( + + {this.renderChildren(context, instance)} + + ); + } + + handleMouseDown(e: React.MouseEvent | React.TouchEvent, instance: MarkerInstance): void { + if (this.draggableX || this.draggableY) { + let svgEl = closest(e.target as Element, (el) => el.tagName == "svg"); + if (svgEl) + captureMouseOrTouch( + e, + (e, captureData) => { + this.handleDragMove(e, instance, captureData); + }, + undefined, + { svgEl, el: e.target }, + (e.target as HTMLElement).style.cursor + ); + } else { + if (!this.selection.isDummy) this.selection.selectInstance(instance); + } + } + + handleClick(e: React.MouseEvent, instance: MarkerInstance): void { + if (this.onClick) instance.invoke("onClick", e, instance); + } + + handleDragMove(e: MouseEvent | TouchEvent, instance: MarkerInstance, captureData: any): void { + let cursor = getCursorPos(e); + let svgBounds = getTopLevelBoundingClientRect(captureData.svgEl); + let { xAxis, yAxis } = instance; + if (this.draggableX && xAxis) { + let x = xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset); + if (this.constrainX) x = xAxis.constrainValue(x); + instance.set("x", xAxis.encodeValue(x)); + } + if (this.draggableY && yAxis) { + let y = yAxis.trackValue(cursor.clientY - svgBounds.top, this.yOffset); + if (this.constrainY) y = yAxis.constrainValue(y); + instance.set("y", yAxis.encodeValue(y)); + } + tooltipMouseMove(e, instance, this.tooltip, { target: captureData.el }); + } +} + +Marker.prototype.xOffset = 0; +Marker.prototype.yOffset = 0; +Marker.prototype.size = 5; +Marker.prototype.anchors = "0.5 0.5 0.5 0.5"; + +Marker.prototype.xAxis = "x"; +Marker.prototype.yAxis = "y"; + +Marker.prototype.baseClass = "marker"; +Marker.prototype.draggableX = false; +Marker.prototype.draggableY = false; +Marker.prototype.draggable = false; +Marker.prototype.constrainX = false; +Marker.prototype.constrainY = false; +Marker.prototype.constrain = false; +Marker.prototype.legend = "legend"; +Marker.prototype.legendAction = "auto"; +Marker.prototype.shape = "circle"; +Marker.prototype.styled = true; +Marker.prototype.hidden = false; +Marker.prototype.affectsAxes = true; +Marker.prototype.stackedY = false; +Marker.prototype.stackedX = false; +Marker.prototype.stack = "stack"; + +BoundedObject.alias("marker", Marker); + +interface MarkerComponentProps { + instance: MarkerInstance; + data: any; + shouldUpdate?: boolean; + children?: React.ReactNode; +} + +class MarkerComponent extends VDOM.Component { + declare el: SVGElement | null; + + shouldComponentUpdate(props: MarkerComponentProps): boolean { + return props.shouldUpdate ?? true; + } + + render(): React.ReactNode { + let { instance, children, data } = this.props; + let widget = instance.widget as Marker; + let { CSS, baseClass } = widget; + let { bounds, shape } = data; + let shapeRenderer = getShape(shape); + let shapeProps: Record = { + className: CSS.element(baseClass, "shape", { + ["color-" + data.colorIndex]: data.colorIndex != null, + selected: data.selected, + }), + style: data.style, + cx: (bounds.l + bounds.r) / 2, + cy: (bounds.t + bounds.b) / 2, + r: data.size / 2, + onMouseMove: (e: React.MouseEvent) => { + tooltipMouseMove(e, instance, widget.tooltip); + }, + onMouseLeave: (e: React.MouseEvent) => { + tooltipMouseLeave(e, instance, widget.tooltip); + }, + onMouseDown: (e: React.MouseEvent) => { + widget.handleMouseDown(e, instance); + }, + onTouchStart: (e: React.TouchEvent) => { + widget.handleMouseDown(e, instance); + }, + onClick: (e: React.MouseEvent) => { + widget.handleClick(e, instance); + }, + }; + + if (shape == "rect" || shape == "square" || shape == "bar" || shape == "column") { + shapeProps.rx = data.rx; + shapeProps.ry = data.ry; + } + + if (widget.tooltip) { + shapeProps.ref = (c: SVGElement | null) => { + this.el = c; + }; + } + + return ( + + {!widget.hidden && + shapeRenderer((bounds.l + bounds.r) / 2, (bounds.t + bounds.b) / 2, data.size, shapeProps)} + {children} + + ); + } + + componentWillUnmount(): void { + tooltipParentWillUnmount(this.props.instance); + } + + UNSAFE_componentWillReceiveProps(props: MarkerComponentProps): void { + let widget = props.instance.widget as Marker; + tooltipParentWillReceiveProps(this.el!, props.instance, widget.tooltip); + } + + componentDidMount(): void { + let widget = this.props.instance.widget as Marker; + tooltipParentDidMount(this.el!, this.props.instance, widget.tooltip); + } + + componentDidUpdate(): void { + let widget = this.props.instance.widget as Marker; + tooltipParentDidUpdate(this.el!, this.props.instance, widget.tooltip); + } +} diff --git a/packages/cx/src/charts/MarkerLine.d.ts b/packages/cx/src/charts/MarkerLine.d.ts deleted file mode 100644 index 3f4a3e41c..000000000 --- a/packages/cx/src/charts/MarkerLine.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; - -interface MarkerLineProps extends BoundedObjectProps { - /** The `x1` value binding or expression. */ - x1?: Cx.Prop; - - /** The `y1` value binding or expression. */ - y1?: Cx.NumberProp; - - /** The `x2` value binding or expression. */ - x2?: Cx.Prop; - - /** The `y2` value binding or expression. */ - y2?: Cx.NumberProp; - - /** Used to indicate if the data should affect axis span. */ - affectsAxes?: Cx.BooleanProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Name of the legend to be used. Default is `legend`. */ - legend?: Cx.StringProp; - - /** Shared `x1` and `x2` value binding or expression. */ - x?: Cx.Prop; - - /** Shared `y1` and `y2` value binding or expression. */ - y?: Cx.NumberProp; - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the axes configuration if the parent Chart component. Default value is y. - */ - yAxis?: string; - - /** Base CSS class to be applied to the element. Defaults to `markerline`. */ - baseClass?: string; - - legendAction?: string; -} - -export class MarkerLine extends Cx.Widget {} diff --git a/packages/cx/src/charts/MarkerLine.js b/packages/cx/src/charts/MarkerLine.js deleted file mode 100644 index 1a3c28e20..000000000 --- a/packages/cx/src/charts/MarkerLine.js +++ /dev/null @@ -1,128 +0,0 @@ -import {BoundedObject} from '../svg/BoundedObject'; -import {VDOM} from '../ui/Widget'; -import {isDefined} from '../util/isDefined'; -import {Rect} from '../svg/util/Rect'; - -export class MarkerLine extends BoundedObject { - - init() { - if (isDefined(this.x)) - this.x1 = this.x2 = this.x; - - if (isDefined(this.y)) - this.y1 = this.y2 = this.y; - - super.init() - } - - declareData() { - super.declareData(...arguments, { - x1: undefined, - y1: undefined, - x2: undefined, - y2: undefined, - colorIndex: undefined, - active: true, - name: undefined, - legend: undefined - }) - } - - explore(context, instance) { - let { data } = instance; - - let xAxis = (instance.xAxis = context.axes[this.xAxis]); - let yAxis = (instance.yAxis = context.axes[this.yAxis]); - - if (data.active) { - if (this.affectsAxes) { - if (data.x1 != null) xAxis.acknowledge(data.x1); - - if (data.x2 != null) xAxis.acknowledge(data.x2); - - if (data.y1 != null) yAxis.acknowledge(data.y1); - - if (data.y2 != null) yAxis.acknowledge(data.y2); - } - - super.explore(context, instance); - } - } - - prepare(context, instance) { - let {data, xAxis, yAxis} = instance; - - if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) - instance.markShouldUpdate(context); - - super.prepare(context, instance); - - if (data.name && data.legend && context.addLegendEntry) - context.addLegendEntry(data.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - style: data.style, - shape: 'line', - onClick: e=> { this.onLegendClick(e, instance) } - }); - } - - calculateBounds(context, instance) { - let {data, xAxis, yAxis} = instance; - let bounds = super.calculateBounds(context, instance); - - let x1 = bounds.l, x2 = bounds.r, y1 = bounds.t, y2 = bounds.b; - - if (data.x1 != null) - x1 = xAxis.map(data.x1); - - if (data.x2 != null) - x2 = xAxis.map(data.x2); - - if (data.y1 != null) - y1 = yAxis.map(data.y1); - - if (data.y2 != null) - y2 = yAxis.map(data.y2); - - bounds.l = Math.min(x1, x2); - bounds.t = Math.min(y1, y2); - bounds.r = Math.max(x1, x2); - bounds.b = Math.max(y1, y2); - - instance.x1 = x1; - instance.x2 = x2; - instance.y1 = y1; - instance.y2 = y2; - - return bounds; - } - - render(context, instance, key) { - let {data, x1, x2, y1, y2} = instance; - - if (!data.active || data.x1 === null || data.x2 === null || data.y1 === null || data.y2 === null) - return null; - - let stateMods = { - ['color-' + data.colorIndex]: data.colorIndex != null - }; - - return - - {this.renderChildren(context, instance)} - - } -} - -MarkerLine.prototype.xAxis = 'x'; -MarkerLine.prototype.yAxis = 'y'; -MarkerLine.prototype.anchors = '0 1 1 0'; -MarkerLine.prototype.baseClass = 'markerline'; -MarkerLine.prototype.legend = 'legend'; -MarkerLine.prototype.legendAction = 'auto'; -MarkerLine.prototype.affectsAxes = true; - -BoundedObject.alias('marker-line', MarkerLine); - diff --git a/packages/cx/src/charts/MarkerLine.scss b/packages/cx/src/charts/MarkerLine.scss index c31584bf6..221caeddc 100644 --- a/packages/cx/src/charts/MarkerLine.scss +++ b/packages/cx/src/charts/MarkerLine.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-markerline( $name: 'markerline', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-line { stroke: rgb(128, 128, 128); diff --git a/packages/cx/src/charts/MarkerLine.tsx b/packages/cx/src/charts/MarkerLine.tsx new file mode 100644 index 000000000..ad5a21bfa --- /dev/null +++ b/packages/cx/src/charts/MarkerLine.tsx @@ -0,0 +1,214 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { VDOM } from "../ui/Widget"; +import { isDefined } from "../util/isDefined"; +import { Rect } from "../svg/util/Rect"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface MarkerLineConfig extends BoundedObjectConfig { + /** X coordinate for vertical line. */ + x?: NumberProp; + + /** Y coordinate for horizontal line. */ + y?: NumberProp; + + /** Starting X coordinate. */ + x1?: NumberProp; + + /** Starting Y coordinate. */ + y1?: NumberProp; + + /** Ending X coordinate. */ + x2?: NumberProp; + + /** Ending Y coordinate. */ + y2?: NumberProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to indicate if the line is active or not. */ + active?: BooleanProp; + + /** Name of the line as it will appear in the legend. */ + name?: StringProp; + + /** Name of the legend to be used. Default is `legend`. */ + legend?: StringProp; + + /** Name of the horizontal axis. Default value is `x`. */ + xAxis?: string; + + /** Name of the vertical axis. Default value is `y`. */ + yAxis?: string; + + /** Set to `false` to prevent the line from affecting axis bounds. */ + affectsAxes?: boolean; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; +} + +export interface MarkerLineInstance extends BoundedObjectInstance { + xAxis: any; + yAxis: any; + x1: number; + y1: number; + x2: number; + y2: number; +} + +export class MarkerLine extends BoundedObject { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare legend: string; + declare legendAction: string; + declare affectsAxes: boolean; + declare x: number; + declare y: number; + declare x1: number; + declare x2: number; + declare y1: number; + declare y2: number; + + constructor(config: MarkerLineConfig) { + super(config); + } + + init(): void { + if (isDefined(this.x)) this.x1 = this.x2 = this.x; + + if (isDefined(this.y)) this.y1 = this.y2 = this.y; + + super.init(); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + x1: undefined, + y1: undefined, + x2: undefined, + y2: undefined, + colorIndex: undefined, + active: true, + name: undefined, + legend: undefined + }); + } + + explore(context: RenderingContext, instance: MarkerLineInstance): void { + let { data } = instance; + + let xAxis = (instance.xAxis = context.axes[this.xAxis]); + let yAxis = (instance.yAxis = context.axes[this.yAxis]); + + if (data.active) { + if (this.affectsAxes) { + if (data.x1 != null) xAxis.acknowledge(data.x1); + + if (data.x2 != null) xAxis.acknowledge(data.x2); + + if (data.y1 != null) yAxis.acknowledge(data.y1); + + if (data.y2 != null) yAxis.acknowledge(data.y2); + } + + super.explore(context, instance); + } + } + + prepare(context: RenderingContext, instance: MarkerLineInstance): void { + let { data, xAxis, yAxis } = instance; + + if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) instance.markShouldUpdate(context); + + super.prepare(context, instance); + + if (data.name && data.legend && context.addLegendEntry) + context.addLegendEntry(data.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + style: data.style, + shape: "line", + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + calculateBounds(context: RenderingContext, instance: MarkerLineInstance): Rect { + let { data, xAxis, yAxis } = instance; + let bounds = super.calculateBounds(context, instance); + + let x1 = bounds.l, + x2 = bounds.r, + y1 = bounds.t, + y2 = bounds.b; + + if (data.x1 != null) x1 = xAxis.map(data.x1); + + if (data.x2 != null) x2 = xAxis.map(data.x2); + + if (data.y1 != null) y1 = yAxis.map(data.y1); + + if (data.y2 != null) y2 = yAxis.map(data.y2); + + bounds.l = Math.min(x1, x2); + bounds.t = Math.min(y1, y2); + bounds.r = Math.max(x1, x2); + bounds.b = Math.max(y1, y2); + + instance.x1 = x1; + instance.x2 = x2; + instance.y1 = y1; + instance.y2 = y2; + + return bounds; + } + + onLegendClick(e: MouseEvent, instance: MarkerLineInstance): void { + let allActions = this.legendAction == "auto"; + let { data } = instance; + if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); + } + + render(context: RenderingContext, instance: MarkerLineInstance, key: string): React.ReactNode { + let { data, x1, x2, y1, y2 } = instance; + + if (!data.active || data.x1 === null || data.x2 === null || data.y1 === null || data.y2 === null) return null; + + let stateMods: Record = { + ["color-" + data.colorIndex]: data.colorIndex != null, + }; + + return ( + + + {this.renderChildren(context, instance)} + + ); + } +} + +MarkerLine.prototype.xAxis = 'x'; +MarkerLine.prototype.yAxis = 'y'; +MarkerLine.prototype.anchors = '0 1 1 0'; +MarkerLine.prototype.baseClass = 'markerline'; +MarkerLine.prototype.legend = 'legend'; +MarkerLine.prototype.legendAction = 'auto'; +MarkerLine.prototype.affectsAxes = true; + +BoundedObject.alias('marker-line', MarkerLine); + diff --git a/packages/cx/src/charts/MouseTracker.d.ts b/packages/cx/src/charts/MouseTracker.d.ts deleted file mode 100644 index fb9fbc063..000000000 --- a/packages/cx/src/charts/MouseTracker.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Cx from '../core'; -import { BoundedObjectProps } from '../svg/BoundedObject'; - -interface MouseTrackerProps extends BoundedObjectProps { - - /** The binding that is used to store the mouse x coordinate. */ - x?: Cx.NumberProp, - - /** The binding that is used to store the mouse y coordinate. */ - y?: Cx.NumberProp, - - /** Base CSS class to be applied to the element. Defaults to `mousetracker`. */ - baseClass?: string - -} - -export class MouseTracker extends Cx.Widget {} \ No newline at end of file diff --git a/packages/cx/src/charts/MouseTracker.js b/packages/cx/src/charts/MouseTracker.js deleted file mode 100644 index 577494bc1..000000000 --- a/packages/cx/src/charts/MouseTracker.js +++ /dev/null @@ -1,81 +0,0 @@ -import {BoundedObject} from "../svg/BoundedObject"; -import {VDOM} from '../ui/VDOM'; -import {tooltipMouseMove, tooltipMouseLeave} from '../widgets/overlay/tooltip-ops'; -import {closest} from '../util/DOM'; -import {getTopLevelBoundingClientRect} from "../util/getTopLevelBoundingClientRect"; - -export class MouseTracker extends BoundedObject { - declareData() { - return super.declareData(...arguments, { - x: undefined, - y: undefined - }); - } - - explore(context, instance) { - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - super.explore(context, instance); - } - - render(context, instance, key) { - let {data} = instance; - let {bounds} = data; - if (!bounds.valid()) - return null; - - return ( - { - this.handleMouseMove(e, instance) - }} - onMouseLeave={e => { - this.handleMouseLeave(e, instance) - }} - > - - {this.renderChildren(context, instance)} - - ) - } - - handleMouseMove(e, instance) { - let {xAxis, yAxis} = instance; - let svgEl = closest(e.target, el => el.tagName == 'svg'); - let bounds = getTopLevelBoundingClientRect(svgEl); - - if (xAxis) - instance.set('x', xAxis.trackValue(e.clientX - bounds.left)); - - if (yAxis) - instance.set('y', yAxis.trackValue(e.clientY - bounds.top)); - - tooltipMouseMove(e, instance, instance.widget.tooltip); - } - - handleMouseLeave(e, instance) { - let {xAxis, yAxis} = instance; - - tooltipMouseLeave(e, instance, instance.widget.tooltip); - - if (xAxis) - instance.set('x', null); - - if (yAxis) - instance.set('y', null); - } -} - -MouseTracker.prototype.xAxis = 'x'; -MouseTracker.prototype.yAxis = 'y'; -MouseTracker.prototype.anchors = '0 1 1 0'; -MouseTracker.prototype.baseClass = "mousetracker"; \ No newline at end of file diff --git a/packages/cx/src/charts/MouseTracker.tsx b/packages/cx/src/charts/MouseTracker.tsx new file mode 100644 index 000000000..d357f6357 --- /dev/null +++ b/packages/cx/src/charts/MouseTracker.tsx @@ -0,0 +1,112 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { VDOM } from "../ui/VDOM"; +import { tooltipMouseMove, tooltipMouseLeave, TooltipParentInstance } from "../widgets/overlay/tooltip-ops"; +import { closest } from "../util/DOM"; +import { getTopLevelBoundingClientRect } from "../util/getTopLevelBoundingClientRect"; +import { RenderingContext, CxChild } from "../ui/RenderingContext"; +import { NumberProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface MouseTrackerConfig extends BoundedObjectConfig { + /** The binding that is used to store the mouse x coordinate. */ + x?: NumberProp; + + /** The binding that is used to store the mouse y coordinate. */ + y?: NumberProp; + + /** Base CSS class to be applied to the element. Defaults to `mousetracker`. */ + baseClass?: string; + + /** Name of the x-axis. Default is 'x'. */ + xAxis?: string; + + /** Name of the y-axis. Default is 'y'. */ + yAxis?: string; +} + +export interface MouseTrackerInstance extends BoundedObjectInstance, TooltipParentInstance { + xAxis?: any; + yAxis?: any; +} + +export class MouseTracker extends BoundedObject { + declare xAxis: string; + declare yAxis: string; + declare anchors: string; + declare baseClass: string; + + constructor(config?: MouseTrackerConfig) { + super(config); + } + + declareData(...args: any[]) { + return super.declareData(...args, { + x: undefined, + y: undefined, + }); + } + + explore(context: ChartRenderingContext, instance: MouseTrackerInstance) { + instance.xAxis = context.axes?.[this.xAxis]; + instance.yAxis = context.axes?.[this.yAxis]; + super.explore(context, instance); + } + + render(context: RenderingContext, instance: MouseTrackerInstance, key: string): CxChild { + let { data } = instance; + let { bounds } = data as any; + if (!bounds.valid()) return null; + + return ( + { + this.handleMouseMove(e, instance); + }} + onMouseLeave={(e) => { + this.handleMouseLeave(e, instance); + }} + > + + {this.renderChildren(context, instance)} + + ); + } + + handleMouseMove(e: React.MouseEvent, instance: MouseTrackerInstance) { + let { xAxis, yAxis } = instance; + let svgEl = closest(e.target as Element, (el) => el.tagName == "svg"); + let bounds = getTopLevelBoundingClientRect(svgEl!); + + if (xAxis) instance.set("x", xAxis.trackValue(e.clientX - bounds.left)); + + if (yAxis) instance.set("y", yAxis.trackValue(e.clientY - bounds.top)); + + tooltipMouseMove(e, instance, (instance.widget as any).tooltip); + } + + handleMouseLeave(e: React.MouseEvent, instance: MouseTrackerInstance) { + let { xAxis, yAxis } = instance; + + tooltipMouseLeave(e, instance, (instance.widget as any).tooltip); + + if (xAxis) instance.set("x", null); + + if (yAxis) instance.set("y", null); + } +} + +MouseTracker.prototype.xAxis = "x"; +MouseTracker.prototype.yAxis = "y"; +MouseTracker.prototype.anchors = "0 1 1 0"; +MouseTracker.prototype.baseClass = "mousetracker"; diff --git a/packages/cx/src/charts/Pie.js b/packages/cx/src/charts/Pie.js deleted file mode 100644 index 2b7051f1c..000000000 --- a/packages/cx/src/charts/Pie.js +++ /dev/null @@ -1,8 +0,0 @@ -import {PieChart, PieSlice} from './PieChart'; - -import {debug} from '../util/Debug'; - -debug('The Pie class is deprecated. Please use PieChart instead.') - -export const Pie = PieChart; -Pie.Slice = PieSlice; diff --git a/packages/cx/src/charts/Pie.ts b/packages/cx/src/charts/Pie.ts new file mode 100644 index 000000000..9cbda2817 --- /dev/null +++ b/packages/cx/src/charts/Pie.ts @@ -0,0 +1,8 @@ +import { PieChart, PieSlice } from "./PieChart"; + +import { debug } from "../util/Debug"; + +debug("The Pie class is deprecated. Please use PieChart instead."); + +export const Pie = PieChart as typeof PieChart & { Slice: typeof PieSlice }; +Pie.Slice = PieSlice; diff --git a/packages/cx/src/charts/PieChart.d.ts b/packages/cx/src/charts/PieChart.d.ts deleted file mode 100644 index 4dfcd2df2..000000000 --- a/packages/cx/src/charts/PieChart.d.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; -import { PropertySelection, KeySelection } from "../ui/selection"; - -interface PieChartProps extends BoundedObjectProps { - /** Angle in degrees. Default is `360` which represents the full circle. */ - angle?: Cx.NumberProp; - - /** Start angle in degrees. Indicates the starting point of the first stack. Default is `0`. */ - startAngle?: Cx.NumberProp; - - /** When set to `true`, stacks are rendered in clock wise direction. */ - clockwise?: Cx.BooleanProp; - - /** Gap between slices in pixels. Default is `0` which means there is no gap. */ - gap?: Cx.NumberProp; -} - -export class PieChart extends Cx.Widget {} - -interface PieSliceProps extends Cx.StyledContainerProps { - /** Used to indicate whether an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** - * Inner pie radius in percents of the maximum available radius. - * If `percentageRadius` flag is set to false, then the value represents the radius in pixels. Default is 0. - */ - r0?: Cx.NumberProp; - - /** - * Outer pie radius in percents of the maximum available radius. - * If `percentageRadius` flag is set to false, then the value represents the radius in pixels. Default is 50. - */ - r?: Cx.NumberProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** Value in pixels to be used to explode the pie. */ - offset?: Cx.NumberProp; - - value?: Cx.NumberProp; - disabled?: Cx.BooleanProp; - innerPointRadius?: Cx.NumberProp; - outerPointRadius?: Cx.NumberProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Multi-level pie charts consist of multiple stacks. Assign a unique name to each level. Default is `stack`. */ - stack?: Cx.StringProp; - - /** Name of the legend to be used. Default is `legend`. */ - legend?: Cx.StringProp; - - percentageRaidus?: boolean; - - /** Base CSS class to be applied to the element. Defaults to `pieslice`. */ - baseClass?: string; - - legendAction?: string; - - /** Text to be displayed in the legend. The default is copying the `name` value. */ - legendDisplayText?: Cx.StringProp; - - /** Tooltip configuration. For more info see Tooltips. */ - tooltip?: Cx.StringProp | Cx.StructuredProp; - - /** Selection configuration. */ - selection?: { type: typeof PropertySelection | typeof KeySelection; [prop: string]: any }; - - /** A value used to identify the group of components participating in hover effect synchronization. */ - hoverChannel?: string; - - /** A value used to uniquely identify the record within the hover sync group. */ - hoverId?: Cx.StringProp; - - /** Border radius of the slice. Default is 0. */ - borderRadius?: Cx.NumberProp; - - /** Border radius of the slice. Default is 0. */ - br?: Cx.NumberProp; -} - -export class PieSlice extends Cx.Widget {} diff --git a/packages/cx/src/charts/PieChart.js b/packages/cx/src/charts/PieChart.js deleted file mode 100644 index 3928a91c2..000000000 --- a/packages/cx/src/charts/PieChart.js +++ /dev/null @@ -1,530 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { Container } from "../ui/Container"; -import { BoundedObject } from "../svg/BoundedObject"; -import { Rect } from "../svg/util/Rect"; -import { Selection } from "../ui/selection/Selection"; -import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops"; -import { isNumber } from "../util/isNumber"; -import { shallowEquals } from "../util/shallowEquals"; -import { withHoverSync } from "../ui/HoverSync"; - -export class PieChart extends BoundedObject { - declareData() { - super.declareData(...arguments, { - angle: undefined, - startAngle: undefined, - clockwise: undefined, - gap: undefined, - }); - } - - explore(context, instance) { - if (!instance.pie) instance.pie = new PieCalculator(); - let { data } = instance; - instance.pie.reset(data.angle, data.startAngle, data.clockwise, data.gap); - - context.push("pie", instance.pie); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("pie"); - } - - prepare(context, instance) { - this.prepareBounds(context, instance); - let { data, pie } = instance; - pie.measure(data.bounds); - let hash = pie.hash(); - instance.cache("hash", hash); - pie.shouldUpdate = !shallowEquals(hash, instance.cached.hash); - if (!pie.shouldUpdate) instance.markShouldUpdate(context); - super.prepare(context, instance); - } -} - -PieChart.prototype.anchors = "0 1 1 0"; - -class PieCalculator { - reset(angle, startAngle, clockwise, gap) { - if (angle == 360) angle = 359.99; // really hacky way to draw full circles - this.angleTotal = (angle / 180) * Math.PI; - this.startAngle = (startAngle / 180) * Math.PI; - this.clockwise = clockwise; - this.gap = gap; - this.stacks = {}; - } - - acknowledge(stack, value, r, r0, percentageRadius) { - let s = this.stacks[stack]; - if (!s) s = this.stacks[stack] = { total: 0, r0s: this.gap > 0 ? [] : null, r0ps: this.gap > 0 ? [] : null }; - if (value > 0) { - s.total += value; - if (this.gap > 0 && r0 > 0) - if (percentageRadius) s.r0ps.push(r0); - else s.r0s.push(r0); - } - } - - hash() { - return { - angleTotal: this.angleTotal, - startAngle: this.startAngle, - clockwise: this.clockwise, - stacks: Object.keys(this.stacks) - .map((s) => `${this.stacks[s].angleFactor}`) - .join(":"), - cx: this.cx, - cy: this.cy, - R: this.R, - gap: this.gap, - }; - } - - measure(rect) { - this.R = Math.max(0, Math.min(rect.width(), rect.height())) / 2; - for (let s in this.stacks) { - let stack = this.stacks[s]; - let gapAngleTotal = 0; - stack.gap = this.gap; - if (this.gap > 0) { - // gap cannot be larger of two times the smallest r0 - for (let index = 0; index < stack.r0s.length; index++) - if (2 * stack.r0s[index] < stack.gap) stack.gap = 2 * stack.r0s[index]; - for (let index = 0; index < stack.r0ps.length; index++) { - let r0 = (stack.r0ps[index] * this.R) / 100; - if (2 * r0 < stack.gap) stack.gap = 2 * r0; - } - } - while (stack.gap > 0) { - for (let index = 0; index < stack.r0s.length; index++) - gapAngleTotal += 2 * Math.asin(stack.gap / stack.r0s[index] / 2); - - for (let index = 0; index < stack.r0ps.length; index++) - gapAngleTotal += 2 * Math.asin(stack.gap / ((stack.r0ps[index] * this.R) / 100) / 2); - - if (gapAngleTotal < 0.25 * this.angleTotal) break; - stack.gap = stack.gap * 0.95; - gapAngleTotal = 0; - } - if (gapAngleTotal == 0) stack.gap = 0; - stack.angleFactor = stack.total > 0 ? (this.angleTotal - gapAngleTotal) / stack.total : 0; - stack.lastAngle = this.startAngle; - } - this.cx = (rect.l + rect.r) / 2; - this.cy = (rect.t + rect.b) / 2; - } - - map(stack, value, r, r0, percentageRadius) { - if (percentageRadius) { - r = (r * this.R) / 100; - r0 = (r0 * this.R) / 100; - } - let s = this.stacks[stack]; - let angle = value * s.angleFactor; - let startAngle = s.lastAngle; - let clockFactor = this.clockwise ? -1 : 1; - let gapAngle = r0 > 0 && s.gap > 0 ? 2 * Math.asin(s.gap / r0 / 2) : 0; - s.lastAngle += clockFactor * (angle + gapAngle); - let endAngle = startAngle + clockFactor * (angle + gapAngle); - - return { - startAngle, - endAngle: startAngle + clockFactor * (angle + gapAngle), - angle, - midAngle: (startAngle + endAngle) / 2, - gap: s.gap, - cx: this.cx, - cy: this.cy, - R: this.R, - }; - } -} - -function createSvgArc(cx, cy, r0 = 0, r, startAngle, endAngle, br = 0, gap = 0) { - let gap2 = gap / 2; - - if (startAngle > endAngle) { - let s = startAngle; - startAngle = endAngle; - endAngle = s; - } - - let path = []; - // limit br size based on r and r0 - if (br > (r - r0) / 2) br = (r - r0) / 2; - - if (br > 0) { - if (r0 > 0) { - let innerBr = br; - let innerSmallArcAngle = Math.asin((br + gap2) / (r0 + br)); - - // adjust br according to the available area - if (innerSmallArcAngle > (endAngle - startAngle) / 2) { - innerSmallArcAngle = (endAngle - startAngle) / 2; - let sin = Math.sin(innerSmallArcAngle); - innerBr = Math.max((r0 * sin - gap2) / (1 - sin), 0); - } - - let innerHipDiagonal = (r0 + innerBr) * Math.cos(innerSmallArcAngle); - - let innerSmallArc1XFrom = cx + Math.cos(endAngle) * innerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2; - let innerSmallArc1YFrom = cy - Math.sin(endAngle) * innerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2; - - // move from the first small inner arc - path.push(move(innerSmallArc1XFrom, innerSmallArc1YFrom)); - - let innerSmallArc1XTo = cx + Math.cos(endAngle - innerSmallArcAngle) * r0; - let innerSmallArc1YTo = cy - Math.sin(endAngle - innerSmallArcAngle) * r0; - - // add first small inner arc - path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc1XTo, innerSmallArc1YTo)); - - // SECOND ARC - - let innerArcXTo = cx + Math.cos(startAngle + innerSmallArcAngle) * r0; - let innerArcYTo = cy - Math.sin(startAngle + innerSmallArcAngle) * r0; - // add large inner arc - path.push( - arc( - r0, - r0, - 0, - largeArcFlag(endAngle - innerSmallArcAngle - startAngle - innerSmallArcAngle), - 1, - innerArcXTo, - innerArcYTo, - ), - ); - - let innerSmallArc2XTo = - cx + Math.cos(startAngle) * innerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2; - let innerSmallArc2YTo = - cy - Math.sin(startAngle) * innerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2; - // add second small inner arc - path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc2XTo, innerSmallArc2YTo)); - } else { - path.push(move(cx, cy)); - } - - let outerBr = br; - let outerSmallArcAngle = Math.asin((br + gap2) / (r - br)); - - // tweak br according to the available area - if (outerSmallArcAngle > (endAngle - startAngle) / 2) { - outerSmallArcAngle = (endAngle - startAngle) / 2; - let sin = Math.sin(outerSmallArcAngle); - outerBr = Math.max((r * sin - gap2) / (1 + sin), 0); - } - - let outerHipDiagonal = Math.cos(outerSmallArcAngle) * (r - outerBr); - - let outerSmallArc1XFrom = - cx + Math.cos(startAngle) * outerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2; - let outerSmallArc1YFrom = - cy - Math.sin(startAngle) * outerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2; - - let outerSmallArc1XTo = cx + Math.cos(startAngle + outerSmallArcAngle) * r; - let outerSmallArc1YTo = cy - Math.sin(startAngle + outerSmallArcAngle) * r; - - let outerLargeArcXTo = cx + Math.cos(endAngle - outerSmallArcAngle) * r; - let outerLargeArcYTo = cy - Math.sin(endAngle - outerSmallArcAngle) * r; - - let outerSmallArc2XTo = cx + Math.cos(endAngle) * outerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2; - let outerSmallArc2YTo = cy - Math.sin(endAngle) * outerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2; - - path.push( - line(outerSmallArc1XFrom, outerSmallArc1YFrom), - arc(outerBr, outerBr, 0, 0, 0, outerSmallArc1XTo, outerSmallArc1YTo), - arc( - r, - r, - 0, - largeArcFlag(endAngle - outerSmallArcAngle - startAngle - outerSmallArcAngle), - 0, - outerLargeArcXTo, - outerLargeArcYTo, - ), - arc(outerBr, outerBr, 0, 0, 0, outerSmallArc2XTo, outerSmallArc2YTo), - ); - } else { - if (r0 > 0) { - let innerGapAngle = r0 > 0 && gap2 > 0 ? Math.asin(gap2 / r0) : 0; - let innerStartAngle = startAngle + innerGapAngle; - let innerEndAngle = endAngle - innerGapAngle; - let startX = cx + Math.cos(innerEndAngle) * r0; - let startY = cy - Math.sin(innerEndAngle) * r0; - path.push(move(startX, startY)); - - let innerArcToX = cx + Math.cos(innerStartAngle) * r0; - let innerArcToY = cy - Math.sin(innerStartAngle) * r0; - - path.push(arc(r0, r0, 0, largeArcFlag(innerStartAngle - innerEndAngle), 1, innerArcToX, innerArcToY)); - } else { - path.push(move(cx, cy)); - } - - let outerGapAngle = r > 0 && gap2 > 0 ? Math.asin(gap2 / r) : 0; - let outerStartAngle = startAngle + outerGapAngle; - let outerEndAngle = endAngle - outerGapAngle; - let lineToX = cx + Math.cos(outerStartAngle) * r; - let lineToY = cy - Math.sin(outerStartAngle) * r; - path.push(line(lineToX, lineToY)); - - let arcToX = cx + Math.cos(outerEndAngle) * r; - let arcToY = cy - Math.sin(outerEndAngle) * r; - path.push(arc(r, r, 0, largeArcFlag(outerEndAngle - outerStartAngle), 0, arcToX, arcToY)); - } - - path.push(z()); - return path.join(" "); -} - -PieChart.prototype.anchors = "0 1 1 0"; -PieChart.prototype.angle = 360; -PieChart.prototype.startAngle = 0; -PieChart.prototype.gap = 0; - -Widget.alias("pie-slice"); -export class PieSlice extends Container { - init() { - this.selection = Selection.create(this.selection); - if (this.borderRadius) this.br = this.borderRadius; - super.init(); - } - - declareData() { - let selection = this.selection.configureWidget(this); - super.declareData(...arguments, selection, { - active: true, - r0: undefined, - r: undefined, - colorIndex: undefined, - colorMap: undefined, - colorName: undefined, - offset: undefined, - value: undefined, - disabled: undefined, - innerPointRadius: undefined, - outerPointRadius: undefined, - name: undefined, - stack: undefined, - legend: undefined, - hoverId: undefined, - br: undefined, - legendDisplayText: undefined, - }); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.name && !data.colorName) data.colorName = data.name; - - super.prepareData(context, instance); - } - - explore(context, instance) { - instance.pie = context.pie; - if (!instance.pie) throw new Error("Pie.Slice must be placed inside a Pie."); - - let { data } = instance; - - instance.valid = isNumber(data.value) && data.value > 0; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - instance.hoverSync = context.hoverSync; - - if (instance.valid && data.active) { - instance.pie.acknowledge(data.stack, data.value, data.r, data.r0, this.percentageRadius); - super.explore(context, instance); - } - } - - prepare(context, instance) { - let { data, segment, pie, colorMap } = instance; - - if (colorMap && data.colorName) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (instance.valid && data.active) { - let seg = pie.map(data.stack, data.value, data.r, data.r0, this.percentageRadius); - - if ( - !segment || - instance.shouldUpdate || - seg.startAngle != segment.startAngle || - seg.endAngle != segment.endAngle || - pie.shouldUpdate - ) { - if (data.offset > 0) { - seg.ox = seg.cx + Math.cos(seg.midAngle) * data.offset; - seg.oy = seg.cy - Math.sin(seg.midAngle) * data.offset; - } else { - seg.ox = seg.cx; - seg.oy = seg.cy; - } - - seg.radiusMultiplier = 1; - if (this.percentageRadius) seg.radiusMultiplier = seg.R / 100; - - let innerR = data.innerPointRadius != null ? data.innerPointRadius : data.r0; - let outerR = data.outerPointRadius != null ? data.outerPointRadius : data.r; - - let ix = seg.ox + Math.cos(seg.midAngle) * innerR * seg.radiusMultiplier; - let iy = seg.oy - Math.sin(seg.midAngle) * innerR * seg.radiusMultiplier; - let ox = seg.ox + Math.cos(seg.midAngle) * outerR * seg.radiusMultiplier; - let oy = seg.oy - Math.sin(seg.midAngle) * outerR * seg.radiusMultiplier; - - instance.segment = seg; - instance.bounds = new Rect({ - l: ix, - r: ox, - t: iy, - b: oy, - }); - - instance.markShouldUpdate(context); - } - - context.push("parentRect", instance.bounds); - } - - if (data.name && data.legend && context.addLegendEntry) - context.addLegendEntry(data.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - disabled: data.disabled, - selected: this.selection.isInstanceSelected(instance), - style: data.style, - shape: this.legendShape, - hoverId: data.hoverId, - hoverChannel: this.hoverChannel, - hoverSync: instance.hoverSync, - displayText: data.legendDisplayText, - value: data.value, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - } - - prepareCleanup(context, instance) { - if (instance.valid && instance.data.active) { - context.pop("parentRect"); - } - } - - onLegendClick(e, instance) { - let allActions = this.legendAction == "auto"; - let { data } = instance; - if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; - - if (allActions || this.legendAction == "select") this.handleClick(e, instance); - } - - render(context, instance, key) { - let { segment, data } = instance; - if (!instance.valid || !data.active) return null; - - return withHoverSync( - key, - instance.hoverSync, - this.hoverChannel, - data.hoverId, - ({ hover, onMouseMove, onMouseLeave }) => { - let stateMods = { - selected: this.selection.isInstanceSelected(instance), - disabled: data.disabled, - selectable: !this.selection.isDummy, - [`color-${data.colorIndex}`]: data.colorIndex != null, - hover, - }; - - let d = createSvgArc( - segment.ox, - segment.oy, - data.r0 * segment.radiusMultiplier, - data.r * segment.radiusMultiplier, - segment.startAngle, - segment.endAngle, - data.br, - segment.gap, - ); - - return ( - - { - onMouseMove(e, instance); - tooltipMouseMove(e, instance, this.tooltip); - }} - onMouseLeave={(e) => { - onMouseLeave(e, instance); - tooltipMouseLeave(e, instance, this.tooltip); - }} - onClick={(e) => { - this.handleClick(e, instance); - }} - /> - {this.renderChildren(context, instance)} - - ); - }, - ); - } - - handleClick(e, instance) { - if (!this.selection.isDummy) { - this.selection.selectInstance(instance, { - toggle: e.ctrlKey, - }); - e.stopPropagation(); - e.preventDefault(); - } - } -} - -function move(x, y) { - return `M ${x} ${y}`; -} - -function line(x, y) { - return `L ${x} ${y}`; -} - -function z() { - return "Z"; -} - -function arc(rx, ry, xRotation, largeArc, sweep, x, y) { - return `A ${rx} ${ry} ${xRotation} ${largeArc} ${sweep} ${x} ${y}`; -} - -function largeArcFlag(angle) { - return angle > Math.PI || angle < -Math.PI ? 1 : 0; -} - -PieSlice.prototype.offset = 0; -PieSlice.prototype.r0 = 0; -PieSlice.prototype.r = 50; -PieSlice.prototype.percentageRadius = true; -PieSlice.prototype.baseClass = "pieslice"; -PieSlice.prototype.legend = "legend"; -PieSlice.prototype.active = true; -PieSlice.prototype.stack = "stack"; -PieSlice.prototype.legendAction = "auto"; -PieSlice.prototype.legendShape = "circle"; -PieSlice.prototype.hoverChannel = "default"; -PieSlice.prototype.styled = true; -PieSlice.prototype.br = 0; - -Widget.alias("pie-chart", PieChart); diff --git a/packages/cx/src/charts/PieChart.scss b/packages/cx/src/charts/PieChart.scss index e25614917..2ab5097fe 100644 --- a/packages/cx/src/charts/PieChart.scss +++ b/packages/cx/src/charts/PieChart.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-pieslice( $name: 'pieslice', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-slice { stroke-width: 0; diff --git a/packages/cx/src/charts/PieChart.tsx b/packages/cx/src/charts/PieChart.tsx new file mode 100644 index 000000000..0666812ec --- /dev/null +++ b/packages/cx/src/charts/PieChart.tsx @@ -0,0 +1,717 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, WidgetConfig } from "../ui/Widget"; +import { Container, ContainerConfig, StyledContainerConfig } from "../ui/Container"; +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { Rect } from "../svg/util/Rect"; +import { Selection, SimpleSelection } from "../ui/selection/Selection"; +import type { KeySelection } from "../ui/selection/KeySelection"; +import type { PropertySelection } from "../ui/selection/PropertySelection"; +import { tooltipMouseMove, tooltipMouseLeave, TooltipParentInstance } from "../widgets/overlay/tooltip-ops"; +import { isNumber } from "../util/isNumber"; +import { shallowEquals } from "../util/shallowEquals"; +import { withHoverSync } from "../ui/HoverSync"; +import { Instance } from "../ui/Instance"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp } from "../ui/Prop"; +import { CreateConfig } from "../util/Component"; + +export interface PieChartConfig extends BoundedObjectConfig { + /** Total angle of the pie chart in degrees. Default is `360`. */ + angle?: NumberProp; + + /** Starting angle in degrees. Default is `0`. */ + startAngle?: NumberProp; + + /** Set to `true` for clockwise direction. */ + clockwise?: BooleanProp; + + /** Gap between slices in pixels. Default is `0`. */ + gap?: NumberProp; +} + +export interface PieChartInstance extends BoundedObjectInstance { + pie: PieCalculator; +} + +export class PieChart extends BoundedObject { + declare angle: number; + declare startAngle: number; + declare gap: number; + + constructor(config: PieChartConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + angle: undefined, + startAngle: undefined, + clockwise: undefined, + gap: undefined, + }); + } + + explore(context: RenderingContext, instance: PieChartInstance): void { + if (!instance.pie) instance.pie = new PieCalculator(); + let { data } = instance; + instance.pie.reset(data.angle, data.startAngle, data.clockwise, data.gap); + + context.push("pie", instance.pie); + super.explore(context, instance); + } + + exploreCleanup(context: RenderingContext, instance: PieChartInstance): void { + context.pop("pie"); + } + + prepare(context: RenderingContext, instance: PieChartInstance): void { + this.prepareBounds(context, instance); + let { data, pie } = instance; + pie.measure(data.bounds); + let hash = pie.hash(); + instance.cache("hash", hash); + pie.shouldUpdate = !shallowEquals(hash, instance.cached.hash); + if (!pie.shouldUpdate) instance.markShouldUpdate(context); + super.prepare(context, instance); + } +} + +PieChart.prototype.anchors = "0 1 1 0"; + +interface PieStack { + total: number; + r0s: number[] | null; + r0ps: number[] | null; + gap: number; + angleFactor: number; + lastAngle: number; +} + +export interface PieSegment { + startAngle: number; + endAngle: number; + angle: number; + midAngle: number; + gap: number; + cx: number; + cy: number; + R: number; + ox?: number; + oy?: number; + radiusMultiplier?: number; +} + +class PieCalculator { + angleTotal: number = 0; + startAngle: number = 0; + clockwise: boolean = false; + gap: number = 0; + stacks: Record = {}; + cx: number = 0; + cy: number = 0; + R: number = 0; + shouldUpdate: boolean = false; + + reset(angle: number, startAngle: number, clockwise: boolean, gap: number): void { + if (angle == 360) angle = 359.99; // really hacky way to draw full circles + this.angleTotal = (angle / 180) * Math.PI; + this.startAngle = (startAngle / 180) * Math.PI; + this.clockwise = clockwise; + this.gap = gap; + this.stacks = {}; + } + + acknowledge(stack: string, value: number, r: number, r0: number, percentageRadius: boolean): void { + let s = this.stacks[stack]; + if (!s) + s = this.stacks[stack] = { + total: 0, + r0s: this.gap > 0 ? [] : null, + r0ps: this.gap > 0 ? [] : null, + gap: 0, + angleFactor: 0, + lastAngle: 0, + }; + if (value > 0) { + s.total += value; + if (this.gap > 0 && r0 > 0) + if (percentageRadius) s.r0ps!.push(r0); + else s.r0s!.push(r0); + } + } + + hash(): Record { + return { + angleTotal: this.angleTotal, + startAngle: this.startAngle, + clockwise: this.clockwise, + stacks: Object.keys(this.stacks) + .map((s) => `${this.stacks[s].angleFactor}`) + .join(":"), + cx: this.cx, + cy: this.cy, + R: this.R, + gap: this.gap, + }; + } + + measure(rect: Rect): void { + this.R = Math.max(0, Math.min(rect.width(), rect.height())) / 2; + for (let s in this.stacks) { + let stack = this.stacks[s]; + let gapAngleTotal = 0; + stack.gap = this.gap; + if (this.gap > 0) { + // gap cannot be larger of two times the smallest r0 + for (let index = 0; index < stack.r0s!.length; index++) + if (2 * stack.r0s![index] < stack.gap) stack.gap = 2 * stack.r0s![index]; + for (let index = 0; index < stack.r0ps!.length; index++) { + let r0 = (stack.r0ps![index] * this.R) / 100; + if (2 * r0 < stack.gap) stack.gap = 2 * r0; + } + } + while (stack.gap > 0) { + for (let index = 0; index < stack.r0s!.length; index++) + gapAngleTotal += 2 * Math.asin(stack.gap / stack.r0s![index] / 2); + + for (let index = 0; index < stack.r0ps!.length; index++) + gapAngleTotal += 2 * Math.asin(stack.gap / ((stack.r0ps![index] * this.R) / 100) / 2); + + if (gapAngleTotal < 0.25 * this.angleTotal) break; + stack.gap = stack.gap * 0.95; + gapAngleTotal = 0; + } + if (gapAngleTotal == 0) stack.gap = 0; + stack.angleFactor = stack.total > 0 ? (this.angleTotal - gapAngleTotal) / stack.total : 0; + stack.lastAngle = this.startAngle; + } + this.cx = (rect.l + rect.r) / 2; + this.cy = (rect.t + rect.b) / 2; + } + + map(stack: string, value: number, r: number, r0: number, percentageRadius: boolean): PieSegment { + if (percentageRadius) { + r = (r * this.R) / 100; + r0 = (r0 * this.R) / 100; + } + let s = this.stacks[stack]; + let angle = value * s.angleFactor; + let startAngle = s.lastAngle; + let clockFactor = this.clockwise ? -1 : 1; + let gapAngle = r0 > 0 && s.gap > 0 ? 2 * Math.asin(s.gap / r0 / 2) : 0; + s.lastAngle += clockFactor * (angle + gapAngle); + let endAngle = startAngle + clockFactor * (angle + gapAngle); + + return { + startAngle, + endAngle: startAngle + clockFactor * (angle + gapAngle), + angle, + midAngle: (startAngle + endAngle) / 2, + gap: s.gap, + cx: this.cx, + cy: this.cy, + R: this.R, + }; + } +} + +function createSvgArc( + cx: number, + cy: number, + r0: number = 0, + r: number, + startAngle: number, + endAngle: number, + br: number = 0, + gap: number = 0, +): string { + let gap2 = gap / 2; + + if (startAngle > endAngle) { + let s = startAngle; + startAngle = endAngle; + endAngle = s; + } + + let path = []; + // limit br size based on r and r0 + if (br > (r - r0) / 2) br = (r - r0) / 2; + + if (br > 0) { + if (r0 > 0) { + let innerBr = br; + let innerSmallArcAngle = Math.asin((br + gap2) / (r0 + br)); + + // adjust br according to the available area + if (innerSmallArcAngle > (endAngle - startAngle) / 2) { + innerSmallArcAngle = (endAngle - startAngle) / 2; + let sin = Math.sin(innerSmallArcAngle); + innerBr = Math.max((r0 * sin - gap2) / (1 - sin), 0); + } + + let innerHipDiagonal = (r0 + innerBr) * Math.cos(innerSmallArcAngle); + + let innerSmallArc1XFrom = cx + Math.cos(endAngle) * innerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2; + let innerSmallArc1YFrom = cy - Math.sin(endAngle) * innerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2; + + // move from the first small inner arc + path.push(move(innerSmallArc1XFrom, innerSmallArc1YFrom)); + + let innerSmallArc1XTo = cx + Math.cos(endAngle - innerSmallArcAngle) * r0; + let innerSmallArc1YTo = cy - Math.sin(endAngle - innerSmallArcAngle) * r0; + + // add first small inner arc + path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc1XTo, innerSmallArc1YTo)); + + // SECOND ARC + + let innerArcXTo = cx + Math.cos(startAngle + innerSmallArcAngle) * r0; + let innerArcYTo = cy - Math.sin(startAngle + innerSmallArcAngle) * r0; + // add large inner arc + path.push( + arc( + r0, + r0, + 0, + largeArcFlag(endAngle - innerSmallArcAngle - startAngle - innerSmallArcAngle), + 1, + innerArcXTo, + innerArcYTo, + ), + ); + + let innerSmallArc2XTo = + cx + Math.cos(startAngle) * innerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2; + let innerSmallArc2YTo = + cy - Math.sin(startAngle) * innerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2; + // add second small inner arc + path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc2XTo, innerSmallArc2YTo)); + } else { + path.push(move(cx, cy)); + } + + let outerBr = br; + let outerSmallArcAngle = Math.asin((br + gap2) / (r - br)); + + // tweak br according to the available area + if (outerSmallArcAngle > (endAngle - startAngle) / 2) { + outerSmallArcAngle = (endAngle - startAngle) / 2; + let sin = Math.sin(outerSmallArcAngle); + outerBr = Math.max((r * sin - gap2) / (1 + sin), 0); + } + + let outerHipDiagonal = Math.cos(outerSmallArcAngle) * (r - outerBr); + + let outerSmallArc1XFrom = + cx + Math.cos(startAngle) * outerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2; + let outerSmallArc1YFrom = + cy - Math.sin(startAngle) * outerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2; + + let outerSmallArc1XTo = cx + Math.cos(startAngle + outerSmallArcAngle) * r; + let outerSmallArc1YTo = cy - Math.sin(startAngle + outerSmallArcAngle) * r; + + let outerLargeArcXTo = cx + Math.cos(endAngle - outerSmallArcAngle) * r; + let outerLargeArcYTo = cy - Math.sin(endAngle - outerSmallArcAngle) * r; + + let outerSmallArc2XTo = cx + Math.cos(endAngle) * outerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2; + let outerSmallArc2YTo = cy - Math.sin(endAngle) * outerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2; + + path.push( + line(outerSmallArc1XFrom, outerSmallArc1YFrom), + arc(outerBr, outerBr, 0, 0, 0, outerSmallArc1XTo, outerSmallArc1YTo), + arc( + r, + r, + 0, + largeArcFlag(endAngle - outerSmallArcAngle - startAngle - outerSmallArcAngle), + 0, + outerLargeArcXTo, + outerLargeArcYTo, + ), + arc(outerBr, outerBr, 0, 0, 0, outerSmallArc2XTo, outerSmallArc2YTo), + ); + } else { + if (r0 > 0) { + let innerGapAngle = r0 > 0 && gap2 > 0 ? Math.asin(gap2 / r0) : 0; + let innerStartAngle = startAngle + innerGapAngle; + let innerEndAngle = endAngle - innerGapAngle; + let startX = cx + Math.cos(innerEndAngle) * r0; + let startY = cy - Math.sin(innerEndAngle) * r0; + path.push(move(startX, startY)); + + let innerArcToX = cx + Math.cos(innerStartAngle) * r0; + let innerArcToY = cy - Math.sin(innerStartAngle) * r0; + + path.push(arc(r0, r0, 0, largeArcFlag(innerStartAngle - innerEndAngle), 1, innerArcToX, innerArcToY)); + } else { + path.push(move(cx, cy)); + } + + let outerGapAngle = r > 0 && gap2 > 0 ? Math.asin(gap2 / r) : 0; + let outerStartAngle = startAngle + outerGapAngle; + let outerEndAngle = endAngle - outerGapAngle; + let lineToX = cx + Math.cos(outerStartAngle) * r; + let lineToY = cy - Math.sin(outerStartAngle) * r; + path.push(line(lineToX, lineToY)); + + let arcToX = cx + Math.cos(outerEndAngle) * r; + let arcToY = cy - Math.sin(outerEndAngle) * r; + path.push(arc(r, r, 0, largeArcFlag(outerEndAngle - outerStartAngle), 0, arcToX, arcToY)); + } + + path.push(z()); + return path.join(" "); +} + +PieChart.prototype.anchors = "0 1 1 0"; +PieChart.prototype.angle = 360; +PieChart.prototype.startAngle = 0; +PieChart.prototype.gap = 0; + +export interface PieSliceConfig extends StyledContainerConfig { + /** Used to indicate if a slice is active or not. Inactive slices are not rendered. */ + active?: BooleanProp; + + /** Inner radius of the slice. Default is `0`. */ + r0?: NumberProp; + + /** Outer radius of the slice. Default is `50`. */ + r?: NumberProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Offset of the slice from the center. */ + offset?: NumberProp; + + /** Value represented by the slice. */ + value?: NumberProp; + + /** Set to `true` to disable the slice. */ + disabled?: BooleanProp; + + /** Inner radius for point calculations. */ + innerPointRadius?: NumberProp; + + /** Outer radius for point calculations. */ + outerPointRadius?: NumberProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Stack name. Default is `stack`. */ + stack?: StringProp; + + /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ + legend?: StringProp; + + /** ID used for hover synchronization. */ + hoverId?: StringProp; + + /** Border radius for rounded corners. */ + br?: NumberProp; + + /** Alias for br (border radius). */ + borderRadius?: number; + + /** Text to display in the legend. */ + legendDisplayText?: StringProp; + + /** Set to `true` to use percentage-based radius. Default is `true`. */ + percentageRadius?: boolean; + + /** Shape to use in legend. Default is `circle`. */ + legendShape?: string; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; + + /** Hover channel for hover synchronization. Default is `default`. */ + hoverChannel?: string; + + /** Selection configuration. */ + selection?: CreateConfig | CreateConfig | CreateConfig | CreateConfig; + + /** Tooltip configuration. */ + tooltip?: any; +} + +export interface PieSliceInstance extends Instance, TooltipParentInstance { + pie: PieCalculator; + valid: boolean; + colorMap: any; + hoverSync: any; + segment: PieSegment; + bounds: Rect; +} + +Widget.alias("pie-slice"); +export class PieSlice extends Container { + declare baseClass: string; + declare offset: number; + declare r0: number; + declare r: number; + declare percentageRadius: boolean; + declare legend: string; + declare active: boolean; + declare stack: string; + declare legendAction: string; + declare legendShape: string; + declare hoverChannel: string; + declare br: number; + declare borderRadius: number; + declare selection: Selection; + declare tooltip: any; + + constructor(config: PieSliceConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection); + if (this.borderRadius) this.br = this.borderRadius; + super.init(); + } + + declareData(...args: any[]): void { + let selection = this.selection.configureWidget(this); + super.declareData(...args, selection, { + active: true, + r0: undefined, + r: undefined, + colorIndex: undefined, + colorMap: undefined, + colorName: undefined, + offset: undefined, + value: undefined, + disabled: undefined, + innerPointRadius: undefined, + outerPointRadius: undefined, + name: undefined, + stack: undefined, + legend: undefined, + hoverId: undefined, + br: undefined, + legendDisplayText: undefined, + }); + } + + prepareData(context: RenderingContext, instance: PieSliceInstance): void { + let { data } = instance; + + if (data.name && !data.colorName) data.colorName = data.name; + + super.prepareData(context, instance); + } + + explore(context: RenderingContext, instance: PieSliceInstance): void { + instance.pie = context.pie; + if (!instance.pie) throw new Error("Pie.Slice must be placed inside a Pie."); + + let { data } = instance; + + instance.valid = isNumber(data.value) && data.value > 0; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + instance.hoverSync = context.hoverSync; + + if (instance.valid && data.active) { + instance.pie.acknowledge(data.stack, data.value, data.r, data.r0, this.percentageRadius); + super.explore(context, instance); + } + } + + prepare(context: RenderingContext, instance: PieSliceInstance): void { + let { data, segment, pie, colorMap } = instance; + + if (colorMap && data.colorName) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (instance.valid && data.active) { + let seg = pie.map(data.stack, data.value, data.r, data.r0, this.percentageRadius); + + if ( + !segment || + instance.shouldUpdate || + seg.startAngle != segment.startAngle || + seg.endAngle != segment.endAngle || + pie.shouldUpdate + ) { + if (data.offset > 0) { + seg.ox = seg.cx + Math.cos(seg.midAngle) * data.offset; + seg.oy = seg.cy - Math.sin(seg.midAngle) * data.offset; + } else { + seg.ox = seg.cx; + seg.oy = seg.cy; + } + + seg.radiusMultiplier = 1; + if (this.percentageRadius) seg.radiusMultiplier = seg.R / 100; + + let innerR = data.innerPointRadius != null ? data.innerPointRadius : data.r0; + let outerR = data.outerPointRadius != null ? data.outerPointRadius : data.r; + + let ix = seg.ox! + Math.cos(seg.midAngle) * innerR * seg.radiusMultiplier; + let iy = seg.oy! - Math.sin(seg.midAngle) * innerR * seg.radiusMultiplier; + let ox = seg.ox! + Math.cos(seg.midAngle) * outerR * seg.radiusMultiplier; + let oy = seg.oy! - Math.sin(seg.midAngle) * outerR * seg.radiusMultiplier; + + instance.segment = seg; + instance.bounds = new Rect({ + l: ix, + r: ox, + t: iy, + b: oy, + }); + + instance.markShouldUpdate(context); + } + + context.push("parentRect", instance.bounds); + } + + if (data.name && data.legend && context.addLegendEntry) + context.addLegendEntry(data.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + disabled: data.disabled, + selected: this.selection.isInstanceSelected(instance), + style: data.style, + shape: this.legendShape, + hoverId: data.hoverId, + hoverChannel: this.hoverChannel, + hoverSync: instance.hoverSync, + displayText: data.legendDisplayText, + value: data.value, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + prepareCleanup(context: RenderingContext, instance: PieSliceInstance): void { + if (instance.valid && instance.data.active) { + context.pop("parentRect"); + } + } + + onLegendClick(e: MouseEvent | React.MouseEvent, instance: PieSliceInstance): void { + let allActions = this.legendAction == "auto"; + let { data } = instance; + if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return; + + if (allActions || this.legendAction == "select") this.handleClick(e as React.MouseEvent, instance); + } + + render(context: RenderingContext, instance: PieSliceInstance, key: string): React.ReactNode { + let { segment, data } = instance; + if (!instance.valid || !data.active) return null; + + return withHoverSync( + key, + instance.hoverSync, + this.hoverChannel, + data.hoverId, + ({ hover, onMouseMove, onMouseLeave }) => { + let stateMods: Record = { + selected: this.selection.isInstanceSelected(instance), + disabled: data.disabled, + selectable: !this.selection.isDummy, + [`color-${data.colorIndex}`]: data.colorIndex != null, + hover, + }; + + let d = createSvgArc( + segment.ox!, + segment.oy!, + data.r0 * segment.radiusMultiplier!, + data.r * segment.radiusMultiplier!, + segment.startAngle, + segment.endAngle, + data.br, + segment.gap, + ); + + return ( + + { + onMouseMove(e, instance); + tooltipMouseMove(e, instance, this.tooltip); + }} + onMouseLeave={(e) => { + onMouseLeave(e, instance); + tooltipMouseLeave(e, instance, this.tooltip); + }} + onClick={(e) => { + this.handleClick(e, instance); + }} + /> + {this.renderChildren(context, instance)} + + ); + }, + ); + } + + handleClick(e: React.MouseEvent, instance: PieSliceInstance): void { + if (!this.selection.isDummy) { + this.selection.selectInstance(instance, { + toggle: e.ctrlKey, + }); + e.stopPropagation(); + e.preventDefault(); + } + } +} + +function move(x: number, y: number): string { + return `M ${x} ${y}`; +} + +function line(x: number, y: number): string { + return `L ${x} ${y}`; +} + +function z(): string { + return "Z"; +} + +function arc(rx: number, ry: number, xRotation: number, largeArc: number, sweep: number, x: number, y: number): string { + return `A ${rx} ${ry} ${xRotation} ${largeArc} ${sweep} ${x} ${y}`; +} + +function largeArcFlag(angle: number): number { + return angle > Math.PI || angle < -Math.PI ? 1 : 0; +} + +PieSlice.prototype.offset = 0; +PieSlice.prototype.r0 = 0; +PieSlice.prototype.r = 50; +PieSlice.prototype.percentageRadius = true; +PieSlice.prototype.baseClass = "pieslice"; +PieSlice.prototype.legend = "legend"; +PieSlice.prototype.active = true; +PieSlice.prototype.stack = "stack"; +PieSlice.prototype.legendAction = "auto"; +PieSlice.prototype.legendShape = "circle"; +PieSlice.prototype.hoverChannel = "default"; +PieSlice.prototype.styled = true; +PieSlice.prototype.br = 0; + +Widget.alias("pie-chart", PieChart); diff --git a/packages/cx/src/charts/PieLabel.d.ts b/packages/cx/src/charts/PieLabel.d.ts deleted file mode 100644 index 419c8b9ea..000000000 --- a/packages/cx/src/charts/PieLabel.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObjectProps } from "../svg/BoundedObject"; - -interface PieLabelProps extends BoundedObjectProps { - /** Distance in pixels, for which the labels will be separated from the pie chart. Default value is 100px. */ - distance: Cx.NumberProp; - - /** - * Index of the color in the default color palette. - */ - lineColorIndex?: Cx.NumberProp; - - /** A color used to paint the guideline. */ - lineStroke?: Cx.StringProp; - - /** CSS class applied to the line element. */ - lineClass?: Cx.StringProp; - - /** CSS style applied to the line element. */ - lineStyle?: Cx.StringProp; - - /** Base CSS class to be applied to the element. Defaults to `pielabel`. */ - baseClass?: string; -} - -export class PieLabel extends Cx.Widget {} diff --git a/packages/cx/src/charts/PieLabel.js b/packages/cx/src/charts/PieLabel.js deleted file mode 100644 index 4065503b2..000000000 --- a/packages/cx/src/charts/PieLabel.js +++ /dev/null @@ -1,71 +0,0 @@ -import { VDOM } from "../ui/Widget"; -import { BoundedObject } from "../svg/BoundedObject"; -import { Rect } from "../svg/util/Rect"; -import { parseStyle } from "../util/parseStyle"; - -export class PieLabel extends BoundedObject { - init() { - this.lineStyle = parseStyle(this.lineStyle); - super.init(); - } - - declareData(...args) { - super.declareData(...args, { - distance: undefined, - lineStyle: { structured: true }, - lineStroke: undefined, - lineClass: { structured: true }, - lineColorIndex: undefined, - }); - } - - calculateBounds(context, instance) { - var { data } = instance; - var bounds = Rect.add(Rect.add(Rect.multiply(instance.parentRect, data.anchors), data.offset), data.margin); - instance.originalBounds = bounds; - instance.actualBounds = context.placePieLabel(bounds, data.distance); - return new Rect({ t: 0, r: bounds.width(), b: bounds.height(), l: 0 }); - } - - prepare(context, instance) { - super.prepare(context, instance); - if (!context.registerPieLabel) - throw new Error("PieLabel components are allowed only within PieLabelsContainer components."); - let right = instance.parentRect.r > instance.parentRect.l; - context.push("textDirection", right ? "right" : "left"); - context.registerPieLabel(instance); - } - - prepareCleanup(context, instance) { - context.pop("textDirection"); - } - - render(context, instance, key) { - let { originalBounds, actualBounds, data } = instance; - - return ( - - - - {this.renderChildren(context, instance)} - - - ); - } -} - -PieLabel.prototype.distance = 100; -PieLabel.prototype.baseClass = "pielabel"; -PieLabel.prototype.styled = true; diff --git a/packages/cx/src/charts/PieLabel.tsx b/packages/cx/src/charts/PieLabel.tsx new file mode 100644 index 000000000..cb2d1822b --- /dev/null +++ b/packages/cx/src/charts/PieLabel.tsx @@ -0,0 +1,106 @@ +/** @jsxImportSource react */ + +import { VDOM } from "../ui/Widget"; +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { Rect } from "../svg/util/Rect"; +import { parseStyle } from "../util/parseStyle"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, StringProp, StyleProp } from "../ui/Prop"; + +export interface PieLabelConfig extends BoundedObjectConfig { + /** Distance from the pie center. Default is `100`. */ + distance?: NumberProp; + + /** Style for the connecting line. */ + lineStyle?: StyleProp; + + /** Stroke color for the connecting line. */ + lineStroke?: StringProp; + + /** CSS class for the connecting line. */ + lineClass?: StringProp; + + /** Color index for the connecting line. */ + lineColorIndex?: NumberProp; +} + +export interface PieLabelInstance extends BoundedObjectInstance { + originalBounds: Rect; + actualBounds: Rect; + parentRect: Rect; +} + +export class PieLabel extends BoundedObject { + declare baseClass: string; + declare distance: number; + declare lineStyle: any; + + constructor(config: PieLabelConfig) { + super(config); + } + + init(): void { + this.lineStyle = parseStyle(this.lineStyle); + super.init(); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + distance: undefined, + lineStyle: { structured: true }, + lineStroke: undefined, + lineClass: { structured: true }, + lineColorIndex: undefined, + }); + } + + calculateBounds(context: RenderingContext, instance: PieLabelInstance): Rect { + var { data } = instance; + var bounds = Rect.add(Rect.add(Rect.multiply(instance.parentRect, data.anchors), data.offset), data.margin); + instance.originalBounds = bounds; + instance.actualBounds = context.placePieLabel(bounds, data.distance); + return new Rect({ t: 0, r: bounds.width(), b: bounds.height(), l: 0 }); + } + + prepare(context: RenderingContext, instance: PieLabelInstance): void { + super.prepare(context, instance); + if (!context.registerPieLabel) + throw new Error("PieLabel components are allowed only within PieLabelsContainer components."); + let right = instance.parentRect.r > instance.parentRect.l; + context.push("textDirection", right ? "right" : "left"); + context.registerPieLabel(instance); + } + + prepareCleanup(context: RenderingContext, instance: PieLabelInstance): void { + context.pop("textDirection"); + } + + render(context: RenderingContext, instance: PieLabelInstance, key: string): React.ReactNode { + let { originalBounds, actualBounds, data } = instance; + + return ( + + + + {this.renderChildren(context, instance)} + + + ); + } +} + +PieLabel.prototype.distance = 100; +PieLabel.prototype.baseClass = "pielabel"; +PieLabel.prototype.styled = true; diff --git a/packages/cx/src/charts/PieLabelsContainer.d.ts b/packages/cx/src/charts/PieLabelsContainer.d.ts deleted file mode 100644 index 203340c3b..000000000 --- a/packages/cx/src/charts/PieLabelsContainer.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Widget } from "cx/src/core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; - -interface PieLabelsContainerProps extends BoundedObjectProps {} - -export class PieLabelsContainer extends Widget {} diff --git a/packages/cx/src/charts/PieLabelsContainer.js b/packages/cx/src/charts/PieLabelsContainer.js deleted file mode 100644 index d31efa478..000000000 --- a/packages/cx/src/charts/PieLabelsContainer.js +++ /dev/null @@ -1,55 +0,0 @@ -import { BoundedObject } from "../svg/BoundedObject"; -import { Rect } from "../svg/util/Rect"; - -export class PieLabelsContainer extends BoundedObject { - prepare(context, instance) { - super.prepare(context, instance); - let { bounds } = instance.data; - let cx2 = bounds.l + bounds.r; - - context.push("placePieLabel", (labelBounds, distance) => { - let clone = new Rect(labelBounds); - let w = clone.r - clone.l; - if (clone.l + clone.r > cx2) { - clone.r = Math.min(clone.r + distance, bounds.r); - clone.l = clone.r - w; - } else { - clone.l = Math.max(bounds.l, clone.l - distance); - clone.r = clone.l + w; - } - return clone; - }); - - instance.leftLabels = []; - instance.rightLabels = []; - - context.push("registerPieLabel", (label) => { - if (label.actualBounds.l + label.actualBounds.r < cx2) instance.leftLabels.push(label); - else instance.rightLabels.push(label); - }); - } - - prepareCleanup(context, instance) { - context.pop("placePieLabel"); - context.pop("registerPieLabel"); - super.prepareCleanup(context, instance); - this.distributeLabels(instance.leftLabels, instance); - this.distributeLabels(instance.rightLabels, instance); - } - - distributeLabels(labels, instance) { - labels.sort((a, b) => a.actualBounds.t + a.actualBounds.b - (b.actualBounds.t + b.actualBounds.b)); - let totalHeight = labels.reduce((h, l) => h + l.actualBounds.height(), 0); - let { bounds } = instance.data; - let avgHeight = Math.min(totalHeight, bounds.height()) / labels.length; - let at = bounds.t; - for (let i = 0; i < labels.length; i++) { - let ab = labels[i].actualBounds; - ab.t = Math.max(at, Math.min(ab.t, bounds.b - (labels.length - i) * avgHeight)); - ab.b = ab.t + avgHeight; - at = ab.b; - } - } -} - -PieLabelsContainer.prototype.anchors = "0 1 1 0"; diff --git a/packages/cx/src/charts/PieLabelsContainer.ts b/packages/cx/src/charts/PieLabelsContainer.ts new file mode 100644 index 000000000..6971702ec --- /dev/null +++ b/packages/cx/src/charts/PieLabelsContainer.ts @@ -0,0 +1,68 @@ +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { Rect } from "../svg/util/Rect"; +import { RenderingContext } from "../ui/RenderingContext"; +import { PieLabelInstance } from "./PieLabel"; + +export interface PieLabelsContainerConfig extends BoundedObjectConfig {} + +export interface PieLabelsContainerInstance extends BoundedObjectInstance { + leftLabels: PieLabelInstance[]; + rightLabels: PieLabelInstance[]; +} + +export class PieLabelsContainer extends BoundedObject { + constructor(config: PieLabelsContainerConfig) { + super(config); + } + + prepare(context: RenderingContext, instance: PieLabelsContainerInstance): void { + super.prepare(context, instance); + let { bounds } = instance.data; + let cx2 = bounds.l + bounds.r; + + context.push("placePieLabel", (labelBounds: Rect, distance: number): Rect => { + let clone = new Rect(labelBounds); + let w = clone.r - clone.l; + if (clone.l + clone.r > cx2) { + clone.r = Math.min(clone.r + distance, bounds.r); + clone.l = clone.r - w; + } else { + clone.l = Math.max(bounds.l, clone.l - distance); + clone.r = clone.l + w; + } + return clone; + }); + + instance.leftLabels = []; + instance.rightLabels = []; + + context.push("registerPieLabel", (label: PieLabelInstance): void => { + if (label.actualBounds.l + label.actualBounds.r < cx2) instance.leftLabels.push(label); + else instance.rightLabels.push(label); + }); + } + + prepareCleanup(context: RenderingContext, instance: PieLabelsContainerInstance): void { + context.pop("placePieLabel"); + context.pop("registerPieLabel"); + super.prepareCleanup(context, instance); + this.distributeLabels(instance.leftLabels, instance); + this.distributeLabels(instance.rightLabels, instance); + } + + distributeLabels(labels: PieLabelInstance[], instance: PieLabelsContainerInstance): void { + labels.sort((a, b) => a.actualBounds.t + a.actualBounds.b - (b.actualBounds.t + b.actualBounds.b)); + let totalHeight = labels.reduce((h, l) => h + l.actualBounds.height(), 0); + let { bounds } = instance.data; + let avgHeight = Math.min(totalHeight, bounds.height()) / labels.length; + let at = bounds.t; + for (let i = 0; i < labels.length; i++) { + let ab = labels[i].actualBounds; + ab.t = Math.max(at, Math.min(ab.t, bounds.b - (labels.length - i) * avgHeight)); + ab.b = ab.t + avgHeight; + at = ab.b; + } + } +} + +PieLabelsContainer.prototype.anchors = "0 1 1 0"; diff --git a/packages/cx/src/charts/Range.d.ts b/packages/cx/src/charts/Range.d.ts deleted file mode 100644 index f00914568..000000000 --- a/packages/cx/src/charts/Range.d.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as Cx from '../core'; -import { BoundedObject, BoundedObjectProps } from '../svg/BoundedObject'; - -interface RangeProps extends BoundedObjectProps { - - /** The `x1` value binding or expression. */ - x1?: Cx.NumberProp, - - /** The `y1` value binding or expression. */ - y1?: Cx.NumberProp, - - /** The `x2` value binding or expression. */ - x2?: Cx.NumberProp, - - /** The `y2` value binding or expression. */ - y2?: Cx.NumberProp, - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp, - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp, - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp, - - /** Name of the legend to be used. Default is `legend`. */ - legend?: Cx.StringProp, - - invisible?: boolean, - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string, - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string, - - xSize?: number, - ySize?: number, - xOffset?: number, - yOffset?: number, - - /** Base CSS class to be applied to the element. Defaults to `range`. */ - baseClass?: string, - - /** Set to `true` to make the range draggable along the X axis. */ - draggableX?: boolean, - - /** Set to `true` to make the range draggable along the Y axis. */ - draggableY?: boolean, - - /** Set to `true` to make the range draggable along the X and Y axis. */ - draggable?: boolean, - - /** Constrain the range position to min/max values of the X axis during drag operations. */ - constrainX?: boolean, - - /** Constrain the range position to min/max values of the Y axis during drag operations. */ - constrainY?: boolean, - - /** When set to `true`, it is equivalent to setting `constrainX` and `constrainY` to true. */ - constrain?: boolean, - - legendAction?: string, - hidden?: boolean - -} - -export class Range extends Cx.Widget {} \ No newline at end of file diff --git a/packages/cx/src/charts/Range.js b/packages/cx/src/charts/Range.js deleted file mode 100644 index 4ff353e0d..000000000 --- a/packages/cx/src/charts/Range.js +++ /dev/null @@ -1,206 +0,0 @@ -import {BoundedObject} from '../svg/BoundedObject'; -import {VDOM} from '../ui/Widget'; -import {captureMouseOrTouch, getCursorPos} from '../widgets/overlay/captureMouse'; -import {closest} from '../util/DOM'; -import {getTopLevelBoundingClientRect} from "../util/getTopLevelBoundingClientRect"; - -export class Range extends BoundedObject { - declareData() { - super.declareData(...arguments, { - x1: undefined, - y1: undefined, - x2: undefined, - y2: undefined, - colorIndex: undefined, - active: true, - name: undefined, - legend: undefined - }) - } - - explore(context, instance) { - var {data} = instance; - var xAxis = instance.xAxis = context.axes[this.xAxis]; - var yAxis = instance.yAxis = context.axes[this.yAxis]; - - if (data.active) { - if (xAxis) { - if (data.x1 != null) - instance.xAxis.acknowledge(data.x1, this.xSize, this.xOffset); - - if (data.x2 != null) - instance.xAxis.acknowledge(data.x2, this.xSize, this.xOffset); - } - - if (yAxis) { - if (data.y1 != null) - instance.yAxis.acknowledge(data.y1, this.ySize, this.yOffset); - - if (data.y2 != null) - instance.yAxis.acknowledge(data.y2, this.ySize, this.yOffset); - } - - super.explore(context, instance); - } - } - - prepare(context, instance) { - super.prepare(context, instance); - - var {data, xAxis, yAxis} = instance; - - if (xAxis && xAxis.shouldUpdate) - instance.markShouldUpdate(context); - - if (yAxis && yAxis.shouldUpdate) - instance.markShouldUpdate(context); - - if (data.name && data.legend && context.addLegendEntry) - context.addLegendEntry(data.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - style: data.style, - shape: 'rect', - onClick: e => { - this.onLegendClick(e, instance) - } - }); - } - - onLegendClick(e, instance) { - var allActions = this.legendAction == 'auto'; - var {data} = instance; - if (allActions || this.legendAction == 'toggle') - instance.set('active', !data.active); - } - - calculateBounds(context, instance) { - var bounds = super.calculateBounds(context, instance); - var {data, xAxis, yAxis} = instance; - - if (data.x1 != null) - bounds.l = xAxis.map(data.x1, this.xOffset - this.xSize / 2); - - if (data.x2 != null) - bounds.r = xAxis.map(data.x2,this.xOffset + this.xSize / 2); - - if (data.y1 != null) - bounds.t = yAxis.map(data.y1, this.yOffset - this.ySize / 2); - - if (data.y2 != null) - bounds.b = yAxis.map(data.y2, this.yOffset + this.ySize / 2); - - return bounds; - } - - render(context, instance, key) { - var {data} = instance; - - if (!data.active) - return null; - - var {bounds} = data; - var x1 = Math.min(bounds.l, bounds.r), - y1 = Math.min(bounds.t, bounds.b), - x2 = Math.max(bounds.l, bounds.r), - y2 = Math.max(bounds.t, bounds.b); - - var stateMods = { - ['color-' + data.colorIndex]: data.colorIndex != null - }; - - return - { - !this.hidden && this.handleMouseDown(e, instance)} - onTouchStart={e=>this.handleMouseDown(e, instance)} - /> - } - {this.renderChildren(context, instance)} - - } - - handleClick(e, instance) { - if (this.onClick) - instance.invoke("onClick", e, instance); - } - - handleMouseDown(e, instance) { - if (this.draggableX || this.draggableY) { - var svgEl = closest(e.target, el => el.tagName == 'svg'); - var svgBounds = getTopLevelBoundingClientRect(svgEl); - var cursor = getCursorPos(e); - var {data, xAxis, yAxis} = instance; - - var captureData = { - svgBounds, - start: { - x1: data.x1, - x2: data.x2, - y1: data.y1, - y2: data.y2 - } - }; - - if (this.draggableX && xAxis) - captureData.start.x = xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset, this.constrainX); - - if (this.draggableY && yAxis) - captureData.start.y = yAxis.trackValue(cursor.clientY - svgBounds.top, this.yOffset, this.constrainY); - - if (svgEl) - captureMouseOrTouch(e, (e, captureData) => { - this.handleDragMove(e, instance, captureData); - }, null, captureData, e.target.style.cursor); - } - } - - handleDragMove(e, instance, captureData) { - var cursor = getCursorPos(e); - var {xAxis, yAxis} = instance; - var {svgBounds, start} = captureData; - if (this.draggableX && xAxis) { - var dist = xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset, this.constrainX) - captureData.start.x; - var x1v = xAxis.decodeValue(captureData.start.x1); - var x2v = xAxis.decodeValue(captureData.start.x2); - if (this.constrainX) { - if (dist > 0) - dist = Math.min(xAxis.constrainValue(x2v + dist) - x2v, dist); - else - dist = Math.max(xAxis.constrainValue(x1v + dist) - x1v, dist); - } - instance.set('x1', xAxis.encodeValue(x1v + dist)); - instance.set('x2', xAxis.encodeValue(x2v + dist)); - } - - if (this.draggableY && yAxis) { - var dist = yAxis.trackValue(cursor.clientY - svgBounds.left, this.yOffset, this.constrainY) - captureData.start.y; - var y1v = yAxis.decodeValue(captureData.start.y1); - var y2v = yAxis.decodeValue(captureData.start.y2); - if (this.constrainY) - dist = Math.max(yAxis.constrainValue(y1v + dist) - y1v, Math.min(yAxis.constrainValue(y2v + dist) - y2v, dist)); - instance.set('y1', yAxis.encodeValue(y1v + dist)); - instance.set('y2', yAxis.encodeValue(y2v + dist)); - } - } -} - -Range.prototype.invisible = false; -Range.prototype.xAxis = 'x'; -Range.prototype.yAxis = 'y'; -Range.prototype.xSize = 0; -Range.prototype.ySize = 0; -Range.prototype.xOffset = 0; -Range.prototype.yOffset = 0; -Range.prototype.anchors = '0 1 1 0'; -Range.prototype.baseClass = 'range'; -Range.prototype.legend = 'legend'; -Range.prototype.legendAction = 'auto'; - -BoundedObject.alias('range', Range); \ No newline at end of file diff --git a/packages/cx/src/charts/Range.scss b/packages/cx/src/charts/Range.scss index 91acf0eb4..13db4188a 100644 --- a/packages/cx/src/charts/Range.scss +++ b/packages/cx/src/charts/Range.scss @@ -1,11 +1,12 @@ +@use "sass:map"; @mixin cx-range( $name: 'range', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-rect { stroke-width: 0; diff --git a/packages/cx/src/charts/Range.tsx b/packages/cx/src/charts/Range.tsx new file mode 100644 index 000000000..069fbd107 --- /dev/null +++ b/packages/cx/src/charts/Range.tsx @@ -0,0 +1,318 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { VDOM } from "../ui/Widget"; +import { captureMouseOrTouch, getCursorPos } from "../widgets/overlay/captureMouse"; +import { closest } from "../util/DOM"; +import { getTopLevelBoundingClientRect } from "../util/getTopLevelBoundingClientRect"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Rect } from "../svg/util/Rect"; +import { NumberProp, BooleanProp, StringProp } from "../ui/Prop"; +import { Instance } from "../ui/Instance"; +import type { ChartRenderingContext } from "./Chart"; + +export interface RangeConfig extends BoundedObjectConfig { + /** The `x1` value binding or expression. */ + x1?: NumberProp; + + /** The `y1` value binding or expression. */ + y1?: NumberProp; + + /** The `x2` value binding or expression. */ + x2?: NumberProp; + + /** The `y2` value binding or expression. */ + y2?: NumberProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Name of the legend to be used. Default is `legend`. */ + legend?: StringProp; + + /** Set to `true` to hide the range rectangle. */ + invisible?: boolean; + + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + */ + xAxis?: string; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + */ + yAxis?: string; + + /** X size. */ + xSize?: number; + + /** Y size. */ + ySize?: number; + + /** X offset. */ + xOffset?: number; + + /** Y offset. */ + yOffset?: number; + + /** Set to `true` to make the range draggable along the X axis. */ + draggableX?: boolean; + + /** Set to `true` to make the range draggable along the Y axis. */ + draggableY?: boolean; + + /** Set to `true` to make the range draggable along the X and Y axis. */ + draggable?: boolean; + + /** Constrain the range position to min/max values of the X axis during drag operations. */ + constrainX?: boolean; + + /** Constrain the range position to min/max values of the Y axis during drag operations. */ + constrainY?: boolean; + + /** When set to `true`, it is equivalent to setting `constrainX` and `constrainY` to true. */ + constrain?: boolean; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; + + /** Set to `true` to hide the range. */ + hidden?: boolean; + + /** Click event handler. */ + onClick?: (e: React.MouseEvent, instance: Instance) => void; +} + +export interface RangeInstance extends BoundedObjectInstance { + xAxis: any; + yAxis: any; +} + +export class Range extends BoundedObject { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xSize: number; + declare ySize: number; + declare xOffset: number; + declare yOffset: number; + declare legend: string; + declare legendAction: string; + declare invisible: boolean; + declare hidden: boolean; + declare draggableX: boolean; + declare draggableY: boolean; + declare constrainX: boolean; + declare constrainY: boolean; + declare onClick: RangeConfig["onClick"]; + + constructor(config: RangeConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + x1: undefined, + y1: undefined, + x2: undefined, + y2: undefined, + colorIndex: undefined, + active: true, + name: undefined, + legend: undefined, + }); + } + + explore(context: RenderingContext, instance: RangeInstance): void { + var { data } = instance; + var xAxis = (instance.xAxis = context.axes[this.xAxis]); + var yAxis = (instance.yAxis = context.axes[this.yAxis]); + + if (data.active) { + if (xAxis) { + if (data.x1 != null) instance.xAxis.acknowledge(data.x1, this.xSize, this.xOffset); + + if (data.x2 != null) instance.xAxis.acknowledge(data.x2, this.xSize, this.xOffset); + } + + if (yAxis) { + if (data.y1 != null) instance.yAxis.acknowledge(data.y1, this.ySize, this.yOffset); + + if (data.y2 != null) instance.yAxis.acknowledge(data.y2, this.ySize, this.yOffset); + } + + super.explore(context, instance); + } + } + + prepare(context: RenderingContext, instance: RangeInstance): void { + super.prepare(context, instance); + + var { data, xAxis, yAxis } = instance; + + if (xAxis && xAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (yAxis && yAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (data.name && data.legend && context.addLegendEntry) + context.addLegendEntry(data.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + style: data.style, + shape: "rect", + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + } + + onLegendClick(e: MouseEvent, instance: RangeInstance): void { + var allActions = this.legendAction == "auto"; + var { data } = instance; + if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); + } + + calculateBounds(context: RenderingContext, instance: RangeInstance): Rect { + var bounds = super.calculateBounds(context, instance); + var { data, xAxis, yAxis } = instance; + + if (data.x1 != null) bounds.l = xAxis.map(data.x1, this.xOffset - this.xSize / 2); + + if (data.x2 != null) bounds.r = xAxis.map(data.x2, this.xOffset + this.xSize / 2); + + if (data.y1 != null) bounds.t = yAxis.map(data.y1, this.yOffset - this.ySize / 2); + + if (data.y2 != null) bounds.b = yAxis.map(data.y2, this.yOffset + this.ySize / 2); + + return bounds; + } + + render(context: RenderingContext, instance: RangeInstance, key: string): React.ReactNode { + var { data } = instance; + + if (!data.active) return null; + + var { bounds } = data; + var x1 = Math.min(bounds.l, bounds.r), + y1 = Math.min(bounds.t, bounds.b), + x2 = Math.max(bounds.l, bounds.r), + y2 = Math.max(bounds.t, bounds.b); + + var stateMods: Record = { + ["color-" + data.colorIndex]: data.colorIndex != null, + }; + + return ( + + {!this.hidden && ( + this.handleMouseDown(e, instance)} + onTouchStart={(e) => this.handleMouseDown(e, instance)} + /> + )} + {this.renderChildren(context, instance)} + + ); + } + + handleClick(e: React.MouseEvent, instance: RangeInstance): void { + if (this.onClick) instance.invoke("onClick", e, instance); + } + + handleMouseDown(e: React.MouseEvent | React.TouchEvent, instance: RangeInstance): void { + if (this.draggableX || this.draggableY) { + var svgEl = closest(e.target as Element, (el) => el.tagName == "svg"); + var svgBounds = getTopLevelBoundingClientRect(svgEl!); + var cursor = getCursorPos(e); + var { data, xAxis, yAxis } = instance; + + var captureData: any = { + svgBounds, + start: { + x1: data.x1, + x2: data.x2, + y1: data.y1, + y2: data.y2, + }, + }; + + if (this.draggableX && xAxis) + captureData.start.x = xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset, this.constrainX); + + if (this.draggableY && yAxis) + captureData.start.y = yAxis.trackValue(cursor.clientY - svgBounds.top, this.yOffset, this.constrainY); + + if (svgEl) + captureMouseOrTouch( + e, + (e, captureData) => { + this.handleDragMove(e, instance, captureData); + }, + undefined, + captureData, + (e.target as HTMLElement).style.cursor + ); + } + } + + handleDragMove(e: MouseEvent | TouchEvent, instance: RangeInstance, captureData: any): void { + var cursor = getCursorPos(e); + var { xAxis, yAxis } = instance; + var { svgBounds, start } = captureData; + if (this.draggableX && xAxis) { + var dist = + xAxis.trackValue(cursor.clientX - svgBounds.left, this.xOffset, this.constrainX) - captureData.start.x; + var x1v = xAxis.decodeValue(captureData.start.x1); + var x2v = xAxis.decodeValue(captureData.start.x2); + if (this.constrainX) { + if (dist > 0) dist = Math.min(xAxis.constrainValue(x2v + dist) - x2v, dist); + else dist = Math.max(xAxis.constrainValue(x1v + dist) - x1v, dist); + } + instance.set("x1", xAxis.encodeValue(x1v + dist)); + instance.set("x2", xAxis.encodeValue(x2v + dist)); + } + + if (this.draggableY && yAxis) { + var dist = + yAxis.trackValue(cursor.clientY - svgBounds.left, this.yOffset, this.constrainY) - captureData.start.y; + var y1v = yAxis.decodeValue(captureData.start.y1); + var y2v = yAxis.decodeValue(captureData.start.y2); + if (this.constrainY) + dist = Math.max( + yAxis.constrainValue(y1v + dist) - y1v, + Math.min(yAxis.constrainValue(y2v + dist) - y2v, dist) + ); + instance.set("y1", yAxis.encodeValue(y1v + dist)); + instance.set("y2", yAxis.encodeValue(y2v + dist)); + } + } +} + +Range.prototype.invisible = false; +Range.prototype.xAxis = "x"; +Range.prototype.yAxis = "y"; +Range.prototype.xSize = 0; +Range.prototype.ySize = 0; +Range.prototype.xOffset = 0; +Range.prototype.yOffset = 0; +Range.prototype.anchors = "0 1 1 0"; +Range.prototype.baseClass = "range"; +Range.prototype.legend = "legend"; +Range.prototype.legendAction = "auto"; + +BoundedObject.alias("range", Range); diff --git a/packages/cx/src/charts/RangeMarker.d.ts b/packages/cx/src/charts/RangeMarker.d.ts deleted file mode 100644 index 5a2c2541b..000000000 --- a/packages/cx/src/charts/RangeMarker.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as Cx from "../core"; - -interface RangeMarkerProps extends Cx.StyledContainerProps { - /** The `x` value binding or expression. */ - x?: Cx.NumberProp; - - /** The `y` value binding or expression. */ - y?: Cx.NumberProp; - - /** The shape of marker, Could be `min`, `max`, `line`. Default to `line`. */ - shape?: Cx.StringProp; - - /** Switch to vertical mode. */ - vertical?: Cx.BooleanProp; - - /** Size of the range marker. */ - size?: Cx.NumberProp; - - /** Style object applied to the range marker. */ - lineStyle?: Cx.StyleProp; - - /** Class object applied to the range marker. */ - lineClass?: Cx.StyleProp; - - /** Size of vertical or horizontal caps. */ - capSize?: Cx.NumberProp; - - /** The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ - laneOffset?: Cx.NumberProp; - - /** Inflate the range marker.*/ - inflate?: Cx.NumberProp; -} - -export class RangeMarker extends Cx.Widget {} diff --git a/packages/cx/src/charts/RangeMarker.js b/packages/cx/src/charts/RangeMarker.js deleted file mode 100644 index c33a2b36c..000000000 --- a/packages/cx/src/charts/RangeMarker.js +++ /dev/null @@ -1,159 +0,0 @@ -import { BoundedObject } from "../svg/BoundedObject"; -import { Rect } from "../svg/util/Rect"; -import { Widget, VDOM } from "../ui/Widget"; -import { parseStyle } from "../util/parseStyle"; - -export class RangeMarker extends BoundedObject { - declareData() { - super.declareData(...arguments, { - x: undefined, - y: undefined, - shape: undefined, - vertical: undefined, - size: undefined, - laneOffset: undefined, - lineStyle: { structured: true }, - lineClass: { structured: true }, - capSize: undefined, - inflate: undefined, - }); - } - - init() { - this.lineStyle = parseStyle(this.lineStyle); - super.init(); - } - - prepareData(context, instance) { - instance.axes = context.axes; - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - super.prepareData(context, instance); - } - - explore(context, instance) { - let { data, xAxis, yAxis } = instance; - - if (this.affectsAxes) { - if (xAxis && data.x != null) xAxis.acknowledge(data.x, 0, 0); - if (yAxis && data.y != null) yAxis.acknowledge(data.y, 0, 0); - } - - super.explore(context, instance); - } - - calculateBounds(context, instance) { - let { data, xAxis, yAxis } = instance; - - let l, r, t, b; - - if (data.x == null || data.y == null) { - return super.calculateBounds(context, instance); - } - - if (!this.vertical) { - l = xAxis.map(data.x, data.laneOffset - data.size / 2) - data.inflate; - r = xAxis.map(data.x, data.laneOffset + data.size / 2) + data.inflate; - t = b = yAxis.map(data.y); - if (data.shape == "max") { - b += data.capSize; - } else if (data.shape == "min") { - t -= data.capSize; - } - } else { - l = r = xAxis.map(data.x); - t = yAxis.map(data.y, data.laneOffset - data.size / 2) + data.inflate; - b = yAxis.map(data.y, data.laneOffset + data.size / 2) - data.inflate; - if (data.shape == "max") { - l -= data.capSize; - } else if (data.shape == "min") { - r += data.capSize; - } - } - - return new Rect({ - l, - r, - t, - b, - }); - } - - prepare(context, instance) { - super.prepare(context, instance); - } - - render(context, instance, key) { - var { data } = instance; - let { CSS, baseClass } = this; - let { bounds, shape } = data; - - let path = ""; - if (this.vertical) { - switch (shape) { - default: - case "line": - path += `M ${bounds.r} ${bounds.t} `; - path += `L ${bounds.r} ${bounds.b}`; - break; - case "max": - path += `M ${bounds.l} ${bounds.t} `; - path += `L ${bounds.r} ${bounds.t}`; - path += `L ${bounds.r} ${bounds.b}`; - path += `L ${bounds.l} ${bounds.b}`; - break; - case "min": - path += `M ${bounds.r} ${bounds.t} `; - path += `L ${bounds.l} ${bounds.t}`; - path += `L ${bounds.l} ${bounds.b}`; - path += `L ${bounds.r} ${bounds.b}`; - break; - } - } else { - switch (shape) { - default: - case "line": - path += `M ${bounds.r} ${bounds.t} `; - path += `L ${bounds.l} ${bounds.t}`; - break; - case "max": - path += `M ${bounds.l} ${bounds.b} `; - path += `L ${bounds.l} ${bounds.t}`; - path += `L ${bounds.r} ${bounds.t}`; - path += `L ${bounds.r} ${bounds.b}`; - break; - case "min": - path += `M ${bounds.l} ${bounds.t} `; - path += `L ${bounds.l} ${bounds.b}`; - path += `L ${bounds.r} ${bounds.b}`; - path += `L ${bounds.r} ${bounds.t}`; - break; - } - } - - return ( - - - {this.renderChildren(context, instance)} - - ); - } -} - -RangeMarker.prototype.baseClass = "rangemarker"; -RangeMarker.prototype.xAxis = "x"; -RangeMarker.prototype.yAxis = "y"; - -RangeMarker.prototype.shape = "line"; -RangeMarker.prototype.vertical = false; -RangeMarker.prototype.size = 1; -RangeMarker.prototype.laneOffset = 0; -RangeMarker.prototype.capSize = 5; -RangeMarker.prototype.inflate = 0; -RangeMarker.prototype.affectsAxes = true; - -Widget.alias("range-marker", RangeMarker); diff --git a/packages/cx/src/charts/RangeMarker.scss b/packages/cx/src/charts/RangeMarker.scss index 2328ff3a2..55d4fed48 100644 --- a/packages/cx/src/charts/RangeMarker.scss +++ b/packages/cx/src/charts/RangeMarker.scss @@ -1,7 +1,9 @@ +@use "sass:map"; + @mixin cx-rangemarker($name: "rangemarker", $besm: $cx-besm, $range-marker-color: $cx-default-range-marker-color) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-path { stroke: $range-marker-color; diff --git a/packages/cx/src/charts/RangeMarker.tsx b/packages/cx/src/charts/RangeMarker.tsx new file mode 100644 index 000000000..65d1f9586 --- /dev/null +++ b/packages/cx/src/charts/RangeMarker.tsx @@ -0,0 +1,233 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { Rect } from "../svg/util/Rect"; +import { Widget, VDOM } from "../ui/Widget"; +import { parseStyle } from "../util/parseStyle"; +import { RenderingContext } from "../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp, StyleProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface RangeMarkerConfig extends BoundedObjectConfig { + /** The `x` value binding or expression. */ + x?: NumberProp; + + /** The `y` value binding or expression. */ + y?: NumberProp; + + /** The shape of marker, Could be `min`, `max`, `line`. Default to `line`. */ + shape?: StringProp; + + /** Switch to vertical mode. */ + vertical?: BooleanProp; + + /** Size of the range marker. */ + size?: NumberProp; + + /** The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ + laneOffset?: NumberProp; + + /** Style object applied to the range marker. */ + lineStyle?: StyleProp; + + /** Class object applied to the range marker. */ + lineClass?: StringProp; + + /** Size of vertical or horizontal caps. */ + capSize?: NumberProp; + + /** Inflate the range marker. */ + inflate?: NumberProp; + + /** Set to `true` to allow the range marker to affect axis bounds. */ + affectsAxes?: boolean; + + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + */ + xAxis?: string; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + */ + yAxis?: string; +} + +export interface RangeMarkerInstance extends BoundedObjectInstance { + axes: Record; + xAxis: any; + yAxis: any; +} + +export class RangeMarker extends BoundedObject { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare shape: string; + declare vertical: boolean; + declare size: number; + declare laneOffset: number; + declare capSize: number; + declare inflate: number; + declare affectsAxes: boolean; + declare lineStyle: any; + + constructor(config: RangeMarkerConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData(...args, { + x: undefined, + y: undefined, + shape: undefined, + vertical: undefined, + size: undefined, + laneOffset: undefined, + lineStyle: { structured: true }, + lineClass: { structured: true }, + capSize: undefined, + inflate: undefined, + }); + } + + init(): void { + this.lineStyle = parseStyle(this.lineStyle); + super.init(); + } + + prepareData(context: RenderingContext, instance: RangeMarkerInstance): void { + instance.axes = context.axes; + instance.xAxis = context.axes[this.xAxis]; + instance.yAxis = context.axes[this.yAxis]; + super.prepareData(context, instance); + } + + explore(context: RenderingContext, instance: RangeMarkerInstance): void { + let { data, xAxis, yAxis } = instance; + + if (this.affectsAxes) { + if (xAxis && data.x != null) xAxis.acknowledge(data.x, 0, 0); + if (yAxis && data.y != null) yAxis.acknowledge(data.y, 0, 0); + } + + super.explore(context, instance); + } + + calculateBounds(context: RenderingContext, instance: RangeMarkerInstance): Rect { + let { data, xAxis, yAxis } = instance; + + let l: number, r: number, t: number, b: number; + + if (data.x == null || data.y == null) { + return super.calculateBounds(context, instance); + } + + if (!this.vertical) { + l = xAxis.map(data.x, data.laneOffset - data.size / 2) - data.inflate; + r = xAxis.map(data.x, data.laneOffset + data.size / 2) + data.inflate; + t = b = yAxis.map(data.y); + if (data.shape == "max") { + b += data.capSize; + } else if (data.shape == "min") { + t -= data.capSize; + } + } else { + l = r = xAxis.map(data.x); + t = yAxis.map(data.y, data.laneOffset - data.size / 2) + data.inflate; + b = yAxis.map(data.y, data.laneOffset + data.size / 2) - data.inflate; + if (data.shape == "max") { + l -= data.capSize; + } else if (data.shape == "min") { + r += data.capSize; + } + } + + return new Rect({ + l, + r, + t, + b, + }); + } + + prepare(context: RenderingContext, instance: RangeMarkerInstance): void { + super.prepare(context, instance); + } + + render(context: RenderingContext, instance: RangeMarkerInstance, key: string): React.ReactNode { + var { data } = instance; + let { CSS, baseClass } = this; + let { bounds, shape } = data; + + let path = ""; + if (this.vertical) { + switch (shape) { + default: + case "line": + path += `M ${bounds.r} ${bounds.t} `; + path += `L ${bounds.r} ${bounds.b}`; + break; + case "max": + path += `M ${bounds.l} ${bounds.t} `; + path += `L ${bounds.r} ${bounds.t}`; + path += `L ${bounds.r} ${bounds.b}`; + path += `L ${bounds.l} ${bounds.b}`; + break; + case "min": + path += `M ${bounds.r} ${bounds.t} `; + path += `L ${bounds.l} ${bounds.t}`; + path += `L ${bounds.l} ${bounds.b}`; + path += `L ${bounds.r} ${bounds.b}`; + break; + } + } else { + switch (shape) { + default: + case "line": + path += `M ${bounds.r} ${bounds.t} `; + path += `L ${bounds.l} ${bounds.t}`; + break; + case "max": + path += `M ${bounds.l} ${bounds.b} `; + path += `L ${bounds.l} ${bounds.t}`; + path += `L ${bounds.r} ${bounds.t}`; + path += `L ${bounds.r} ${bounds.b}`; + break; + case "min": + path += `M ${bounds.l} ${bounds.t} `; + path += `L ${bounds.l} ${bounds.b}`; + path += `L ${bounds.r} ${bounds.b}`; + path += `L ${bounds.r} ${bounds.t}`; + break; + } + } + + return ( + + + {this.renderChildren(context, instance)} + + ); + } +} + +RangeMarker.prototype.baseClass = "rangemarker"; +RangeMarker.prototype.xAxis = "x"; +RangeMarker.prototype.yAxis = "y"; + +RangeMarker.prototype.shape = "line"; +RangeMarker.prototype.vertical = false; +RangeMarker.prototype.size = 1; +RangeMarker.prototype.laneOffset = 0; +RangeMarker.prototype.capSize = 5; +RangeMarker.prototype.inflate = 0; +RangeMarker.prototype.affectsAxes = true; + +Widget.alias("range-marker", RangeMarker); diff --git a/packages/cx/src/charts/ScatterGraph.d.ts b/packages/cx/src/charts/ScatterGraph.d.ts deleted file mode 100644 index 64bc3b93f..000000000 --- a/packages/cx/src/charts/ScatterGraph.d.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Cx from "../core"; -import { PropertySelection, KeySelection } from "../ui/selection"; - -interface ScatterGraphProps extends Cx.StyledContainerProps { - /** - * Data for the graph. Each entry should be an object with at least two properties - * whose names should match the `xField` and `yField` values. - */ - data?: Cx.RecordsProp; - - /** Size (width) of the column in axis units. */ - size?: Cx.NumberProp; - - shape?: Cx.StringProp; - - /** Index of a color from the standard palette of colors. 0-15. */ - colorIndex?: Cx.NumberProp; - - /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ - colorMap?: Cx.StringProp; - - /** Name used to resolve the color. If not provided, `name` is used instead. */ - colorName?: Cx.StringProp; - - /** Name of the item as it will appear in the legend. */ - name?: Cx.StringProp; - - /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ - active?: Cx.BooleanProp; - - /** Base CSS class to be applied to the element. Defaults to `scattergraph`. */ - baseClass?: string; - - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - */ - xAxis?: string; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - */ - yAxis?: string; - - /** Name of the property which holds the x value. Default value is `x`. */ - xField?: string; - - /** Name of the property which holds the y value. Default value is `y`. */ - yField?: string; - - /** Name of the property which holds the size value. Do not set if `size` is used. */ - sizeField?: string | false; - - /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ - legend?: string | false; - - legendAction?: string; - - /** Selection configuration. */ - selection?: { type: typeof PropertySelection | typeof KeySelection; [prop: string]: any }; -} - -export class ScatterGraph extends Cx.Widget {} diff --git a/packages/cx/src/charts/ScatterGraph.js b/packages/cx/src/charts/ScatterGraph.js deleted file mode 100644 index 2eb9b22ba..000000000 --- a/packages/cx/src/charts/ScatterGraph.js +++ /dev/null @@ -1,164 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { Selection } from "../ui/selection/Selection"; -import { CSS } from "../ui/CSS"; -import { getShape } from "./shapes"; -import { isArray } from "../util/isArray"; - -export class ScatterGraph extends Widget { - init() { - this.selection = Selection.create(this.selection, { - records: this.data, - }); - super.init(); - } - - declareData() { - var selection = this.selection.configureWidget(this); - - super.declareData( - ...arguments, - { - data: undefined, - size: undefined, - shape: undefined, - colorIndex: undefined, - colorMap: undefined, - colorName: undefined, - name: undefined, - active: true, - }, - selection, - ); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.name && !data.colorName) data.colorName = data.name; - - super.prepareData(context, instance); - } - - explore(context, instance) { - super.explore(context, instance); - - var xAxis = (instance.xAxis = context.axes[this.xAxis]); - var yAxis = (instance.yAxis = context.axes[this.yAxis]); - - var { data } = instance; - - instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); - if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); - - if (data.active && isArray(data.data)) { - data.data.forEach((p) => { - xAxis.acknowledge(p[this.xField]); - yAxis.acknowledge(p[this.yField]); - }); - } - } - - prepare(context, instance) { - var { data, xAxis, yAxis, colorMap } = instance; - - if (xAxis.shouldUpdate || yAxis.shouldUpdate) instance.markShouldUpdate(context); - - if (colorMap && data.name) { - data.colorIndex = colorMap.map(data.colorName); - if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); - } - - if (data.name && context.addLegendEntry) - context.addLegendEntry(this.legend, { - name: data.name, - active: data.active, - colorIndex: data.colorIndex, - disabled: data.disabled, - style: data.style, - shape: data.shape, - onClick: (e) => { - this.onLegendClick(e, instance); - }, - }); - - if (data.active) { - if (context.pointReducer && isArray(data.data)) { - data.data.forEach((p, index) => { - context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); - }); - } - } - } - - onLegendClick(e, instance) { - var allActions = this.legendAction == "auto"; - var { data } = instance; - if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); - } - - render(context, instance, key) { - var { data } = instance; - return ( - - {this.renderData(context, instance)} - - ); - } - - renderData(context, instance) { - var { data, xAxis, yAxis, store } = instance; - - if (!data.active) return null; - - var shape = getShape(data.shape); - - var isSelected = this.selection.getIsSelectedDelegate(store); - - return ( - isArray(data.data) && - data.data.map((p, i) => { - var classes = CSS.element(this.baseClass, "shape", { - selected: isSelected(p, i), - selectable: !this.selection.isDummy, - [`color-${data.colorIndex}`]: data.colorIndex != null, - }); - - var cx = xAxis.map(p[this.xField]), - cy = yAxis.map(p[this.yField]), - size = this.sizeField ? p[this.sizeField] : data.size; - - return shape(cx, cy, size, { - key: i, - className: classes, - style: p.style || data.style, - onClick: (e) => { - this.handleItemClick(e, instance, i); - }, - }); - }) - ); - } - - handleItemClick(e, { data, store }, index) { - var bubble = data.data[index]; - this.selection.select(store, bubble, index, { - toggle: e.ctrlKey, - }); - } -} - -ScatterGraph.prototype.baseClass = "scattergraph"; -ScatterGraph.prototype.xAxis = "x"; -ScatterGraph.prototype.yAxis = "y"; - -ScatterGraph.prototype.xField = "x"; -ScatterGraph.prototype.yField = "y"; -ScatterGraph.prototype.sizeField = false; -ScatterGraph.prototype.shape = "circle"; - -ScatterGraph.prototype.size = 10; -ScatterGraph.prototype.legend = "legend"; -ScatterGraph.prototype.legendAction = "auto"; -ScatterGraph.prototype.styled = true; - -Widget.alias("scatter-graph", ScatterGraph); diff --git a/packages/cx/src/charts/ScatterGraph.scss b/packages/cx/src/charts/ScatterGraph.scss index b55d0479d..1093c5f14 100644 --- a/packages/cx/src/charts/ScatterGraph.scss +++ b/packages/cx/src/charts/ScatterGraph.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-scattergraph( $name: 'scattergraph', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-shape { fill: rgb(128, 128, 128); diff --git a/packages/cx/src/charts/ScatterGraph.tsx b/packages/cx/src/charts/ScatterGraph.tsx new file mode 100644 index 000000000..aa1dc2ddb --- /dev/null +++ b/packages/cx/src/charts/ScatterGraph.tsx @@ -0,0 +1,245 @@ +/** @jsxImportSource react */ + +import { CSS } from "../ui/CSS"; +import { Instance } from "../ui/Instance"; +import { BooleanProp, NumberProp, RecordsProp, StringProp } from "../ui/Prop"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Widget, WidgetConfig, WidgetStyleConfig } from "../ui/Widget"; +import { Selection } from "../ui/selection/Selection"; +import { isArray } from "../util/isArray"; +import type { ChartRenderingContext } from "./Chart"; +import { getShape } from "./shapes"; + +export interface ScatterGraphConfig extends WidgetConfig, WidgetStyleConfig { + /** Data for the graph. Each entry should be an object with at least two properties + * whose names should match the `xField` and `yField` values. + */ + data?: RecordsProp; + + /** Size of the scatter points. */ + size?: NumberProp; + + /** Shape of the scatter points. Default is `circle`. */ + shape?: StringProp; + + /** Index of a color from the standard palette of colors. 0-15. */ + colorIndex?: NumberProp; + + /** Used to automatically assign a color based on the `name` and the contextual `ColorMap` widget. */ + colorMap?: StringProp; + + /** Name used to resolve the color. If not provided, `name` is used instead. */ + colorName?: StringProp; + + /** Name of the item as it will appear in the legend. */ + name?: StringProp; + + /** Used to indicate if an item is active or not. Inactive items are shown only in the legend. */ + active?: BooleanProp; + + /** Name of the horizontal axis. Default value is `x`. */ + xAxis?: string; + + /** Name of the vertical axis. Default value is `y`. */ + yAxis?: string; + + /** Name of the property which holds the x value. Default value is `x`. */ + xField?: string; + + /** Name of the property which holds the y value. Default value is `y`. */ + yField?: string; + + /** Name of the property which holds the size value. */ + sizeField?: string | false; + + /** Name of the legend to be used. Default is `legend`. Set to `false` to hide the legend entry. */ + legend?: string | false; + + /** Action to perform on legend item click. Default is `auto`. */ + legendAction?: string; + + /** Selection configuration. */ + selection?: any; +} + +export interface ScatterGraphInstance extends Instance { + xAxis: any; + yAxis: any; + colorMap: any; +} + +export class ScatterGraph extends Widget { + declare baseClass: string; + declare xAxis: string; + declare yAxis: string; + declare xField: string; + declare yField: string; + declare sizeField: string | false; + declare size: number; + declare shape: string; + declare legend: string | false; + declare legendAction: string; + declare selection: Selection; + declare data: any; + + constructor(config: ScatterGraphConfig) { + super(config); + } + + init(): void { + this.selection = Selection.create(this.selection, { + records: this.data, + }); + super.init(); + } + + declareData(...args: any[]): void { + var selection = this.selection.configureWidget(this); + + super.declareData( + ...args, + { + data: undefined, + size: undefined, + shape: undefined, + colorIndex: undefined, + colorMap: undefined, + colorName: undefined, + name: undefined, + active: true, + }, + selection, + ); + } + + prepareData(context: RenderingContext, instance: ScatterGraphInstance): void { + let { data } = instance; + + if (data.name && !data.colorName) data.colorName = data.name; + + super.prepareData(context, instance); + } + + explore(context: ChartRenderingContext, instance: ScatterGraphInstance): void { + super.explore(context, instance); + + var xAxis = (instance.xAxis = context.axes![this.xAxis]); + var yAxis = (instance.yAxis = context.axes![this.yAxis]); + + var { data } = instance; + + instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap); + if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName); + + if (data.active && isArray(data.data)) { + data.data.forEach((p: any) => { + xAxis.acknowledge(p[this.xField]); + yAxis.acknowledge(p[this.yField]); + }); + } + } + + prepare(context: ChartRenderingContext, instance: ScatterGraphInstance): void { + var { data, xAxis, yAxis, colorMap } = instance; + + if (xAxis.shouldUpdate || yAxis.shouldUpdate) instance.markShouldUpdate(context); + + if (colorMap && data.name) { + data.colorIndex = colorMap.map(data.colorName); + if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context); + } + + if (data.name && context.addLegendEntry) + context.addLegendEntry(this.legend, { + name: data.name, + active: data.active, + colorIndex: data.colorIndex, + disabled: data.disabled, + style: data.style, + shape: data.shape, + onClick: (e: MouseEvent) => { + this.onLegendClick(e, instance); + }, + }); + + if (data.active) { + if (context.pointReducer && isArray(data.data)) { + data.data.forEach((p: any, index: number) => { + context.pointReducer(p[this.xField], p[this.yField], data.name, p, data.data, index); + }); + } + } + } + + onLegendClick(e: MouseEvent, instance: ScatterGraphInstance): void { + var allActions = this.legendAction == "auto"; + var { data } = instance; + if (allActions || this.legendAction == "toggle") instance.set("active", !data.active); + } + + render(context: RenderingContext, instance: ScatterGraphInstance, key: string): React.ReactNode { + var { data } = instance; + return ( + + {this.renderData(context, instance)} + + ); + } + + renderData(context: RenderingContext, instance: ScatterGraphInstance): React.ReactNode { + var { data, xAxis, yAxis, store } = instance; + + if (!data.active) return null; + + var shape = getShape(data.shape); + + var isSelected = this.selection.getIsSelectedDelegate(store); + + return ( + isArray(data.data) && + data.data.map((p: any, i: number) => { + var classes = CSS.element(this.baseClass, "shape", { + selected: isSelected(p, i), + selectable: !this.selection.isDummy, + [`color-${data.colorIndex}`]: data.colorIndex != null, + }); + + var cx = xAxis.map(p[this.xField]), + cy = yAxis.map(p[this.yField]), + size = this.sizeField ? p[this.sizeField as string] : data.size; + + return shape(cx, cy, size, { + key: i, + className: classes, + style: p.style || data.style, + onClick: (e: React.MouseEvent) => { + this.handleItemClick(e, instance, i); + }, + }); + }) + ); + } + + handleItemClick(e: React.MouseEvent, { data, store }: ScatterGraphInstance, index: number): void { + var bubble = data.data[index]; + this.selection.select(store, bubble, index, { + toggle: e.ctrlKey, + }); + } +} + +ScatterGraph.prototype.baseClass = "scattergraph"; +ScatterGraph.prototype.xAxis = "x"; +ScatterGraph.prototype.yAxis = "y"; + +ScatterGraph.prototype.xField = "x"; +ScatterGraph.prototype.yField = "y"; +ScatterGraph.prototype.sizeField = false; +ScatterGraph.prototype.shape = "circle"; + +ScatterGraph.prototype.size = 10; +ScatterGraph.prototype.legend = "legend"; +ScatterGraph.prototype.legendAction = "auto"; +ScatterGraph.prototype.styled = true; + +Widget.alias("scatter-graph", ScatterGraph); diff --git a/packages/cx/src/charts/Swimlane.d.ts b/packages/cx/src/charts/Swimlane.d.ts deleted file mode 100644 index a7194925f..000000000 --- a/packages/cx/src/charts/Swimlane.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObjectProps } from "../svg"; - -interface SwimlaneProps extends BoundedObjectProps { - /** The `x` value binding or expression. */ - x?: Cx.Prop; - - /** The `y` value binding or expression. */ - y?: Cx.Prop; - - /** Represents a swimlane size. */ - size?: Cx.NumberProp; - - /** Switch to vertical swimlanes. */ - vertical?: boolean; - - /**The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ - laneOffset?: Cx.NumberProp; - - /** Style object applied to the swimlanes. */ - laneStyle?: StyleProp; -} - -export class Swimlane extends Cx.Widget {} diff --git a/packages/cx/src/charts/Swimlane.js b/packages/cx/src/charts/Swimlane.js deleted file mode 100644 index 59a2c5031..000000000 --- a/packages/cx/src/charts/Swimlane.js +++ /dev/null @@ -1,140 +0,0 @@ -import { BoundedObject } from "../svg/BoundedObject"; -import { parseStyle } from "../util/parseStyle"; -import { VDOM } from "../ui/Widget"; -import { Rect } from "../svg/util/Rect"; - -export class Swimlane extends BoundedObject { - init() { - this.laneStyle = parseStyle(this.laneStyle); - super.init(); - } - - declareData(...args) { - super.declareData(...args, { - size: undefined, - laneOffset: undefined, - laneStyle: { structured: true }, - vertical: undefined, - x: undefined, - y: undefined, - }); - } - - explore(context, instance) { - let { data } = instance; - super.explore(context, instance); - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - - if (data.vertical) { - instance.xAxis.acknowledge(data.x, data.size, data.laneOffset); - } else { - instance.yAxis.acknowledge(data.y, data.size, data.laneOffset); - } - } - - prepare(context, instance) { - super.prepare(context, instance); - instance.bounds = this.calculateRect(instance); - instance.cache("bounds", instance.bounds); - if (!instance.bounds.isEqual(instance.cached.bounds)) instance.markShouldUpdate(context); - - context.push("parentRect", instance.bounds); - } - - calculateRect(instance) { - var { data } = instance; - var { size, laneOffset } = data; - - if (data.vertical) { - var x1 = instance.xAxis.map(data.x, laneOffset - size / 2); - var x2 = instance.xAxis.map(data.x, laneOffset + size / 2); - var bounds = new Rect({ - l: Math.min(x1, x2), - r: Math.max(x1, x2), - t: data.bounds.t, - b: data.bounds.b, - }); - } else { - var y1 = instance.yAxis.map(data.y, laneOffset - size / 2); - var y2 = instance.yAxis.map(data.y, laneOffset + size / 2); - var bounds = new Rect({ - l: data.bounds.l, - r: data.bounds.r, - t: Math.min(y1, y2), - b: Math.max(y1, y2), - }); - } - - return bounds; - } - - render(context, instance, key) { - let { data, xAxis, yAxis, bounds } = instance; - let { CSS, baseClass } = this; - - let axis = this.vertical ? xAxis : yAxis; - if (!axis) return null; - - let min, max, valueFunction; - if (axis.scale) { - min = axis.scale.min; - max = axis.scale.max; - let clamp = (value) => [Math.max(min, Math.min(max, value)), 0]; - valueFunction = (value, offset) => clamp(value + offset); - } else if (axis.valueList) { - min = 0; - max = axis.valueList.length; - valueFunction = (value, offset) => [axis.valueList[value], offset]; - } - if (!(min < max)) return null; - - let rectClass = CSS.element(baseClass, "lane"); - - if (this.vertical) { - let c1 = axis.map(...valueFunction(data.x, -data.size / 2 + data.laneOffset)); - let c2 = axis.map(...valueFunction(data.x, +data.size / 2 + data.laneOffset)); - return ( - - - {this.renderChildren(context, instance)}; - - ); - } else { - let c1 = axis.map(...valueFunction(data.y, -data.size / 2 + data.laneOffset)); - let c2 = axis.map(...valueFunction(data.y, +data.size / 2 + data.laneOffset)); - return ( - - - {this.renderChildren(context, instance)}; - - ); - } - } -} - -Swimlane.prototype.xAxis = "x"; -Swimlane.prototype.yAxis = "y"; -Swimlane.prototype.anchors = "0 1 1 0"; -Swimlane.prototype.baseClass = "swimlane"; -Swimlane.prototype.size = 0.5; -Swimlane.prototype.laneOffset = 0; -Swimlane.prototype.vertical = false; - -BoundedObject.alias("swimlane", Swimlane); diff --git a/packages/cx/src/charts/Swimlane.scss b/packages/cx/src/charts/Swimlane.scss index 2e9b4ea51..dba9f3ae4 100644 --- a/packages/cx/src/charts/Swimlane.scss +++ b/packages/cx/src/charts/Swimlane.scss @@ -1,7 +1,9 @@ +@use "sass:map"; + @mixin cx-swimlane($name: "swimlane", $besm: $cx-besm, $lane-color: $cx-default-swimlanes-lane-background-color) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-lane { fill: $lane-color; diff --git a/packages/cx/src/charts/Swimlane.tsx b/packages/cx/src/charts/Swimlane.tsx new file mode 100644 index 000000000..91037f0e1 --- /dev/null +++ b/packages/cx/src/charts/Swimlane.tsx @@ -0,0 +1,195 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { parseStyle } from "../util/parseStyle"; +import { VDOM } from "../ui/Widget"; +import { Rect } from "../svg/util/Rect"; +import { RenderingContext, CxChild } from "../ui/RenderingContext"; +import { Prop, NumberProp, StyleProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface SwimlaneConfig extends BoundedObjectConfig { + /** The `x` value binding or expression. */ + x?: Prop; + + /** The `y` value binding or expression. */ + y?: Prop; + + /** Represents a swimlane size. */ + size?: NumberProp; + + /** Switch to vertical swimlanes. */ + vertical?: boolean; + + /** The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ + laneOffset?: NumberProp; + + /** Style object applied to the swimlanes. */ + laneStyle?: StyleProp; + + /** Name of the x-axis. Default is 'x'. */ + xAxis?: string; + + /** Name of the y-axis. Default is 'y'. */ + yAxis?: string; +} + +export interface SwimlaneInstance extends BoundedObjectInstance { + xAxis?: any; + yAxis?: any; + bounds?: Rect; +} + +export class Swimlane extends BoundedObject { + declare xAxis: string; + declare yAxis: string; + declare anchors: string; + declare baseClass: string; + declare size: number; + declare laneOffset: number; + declare vertical: boolean; + declare laneStyle: any; + declare CSS: any; + + constructor(config?: SwimlaneConfig) { + super(config); + } + + init() { + this.laneStyle = parseStyle(this.laneStyle); + super.init(); + } + + declareData(...args: any[]) { + super.declareData(...args, { + size: undefined, + laneOffset: undefined, + laneStyle: { structured: true }, + vertical: undefined, + x: undefined, + y: undefined, + }); + } + + explore(context: ChartRenderingContext, instance: SwimlaneInstance) { + let { data } = instance; + super.explore(context, instance); + instance.xAxis = context.axes?.[this.xAxis]; + instance.yAxis = context.axes?.[this.yAxis]; + + const d = data as any; + if (d.vertical) { + instance.xAxis.acknowledge(d.x, d.size, d.laneOffset); + } else { + instance.yAxis.acknowledge(d.y, d.size, d.laneOffset); + } + } + + prepare(context: RenderingContext, instance: SwimlaneInstance) { + super.prepare(context, instance); + instance.bounds = this.calculateRect(instance); + instance.cache("bounds", instance.bounds); + if (!instance.bounds.isEqual((instance.cached as any).bounds)) instance.markShouldUpdate(context); + + context.push("parentRect", instance.bounds); + } + + calculateRect(instance: SwimlaneInstance): Rect { + var { data } = instance; + const d = data as any; + var { size, laneOffset } = d; + let bounds: Rect; + + if (d.vertical) { + var x1 = instance.xAxis.map(d.x, laneOffset - size / 2); + var x2 = instance.xAxis.map(d.x, laneOffset + size / 2); + bounds = new Rect({ + l: Math.min(x1, x2), + r: Math.max(x1, x2), + t: d.bounds.t, + b: d.bounds.b, + }); + } else { + var y1 = instance.yAxis.map(d.y, laneOffset - size / 2); + var y2 = instance.yAxis.map(d.y, laneOffset + size / 2); + bounds = new Rect({ + l: d.bounds.l, + r: d.bounds.r, + t: Math.min(y1, y2), + b: Math.max(y1, y2), + }); + } + + return bounds; + } + + render(context: RenderingContext, instance: SwimlaneInstance, key: string): CxChild { + let { data, xAxis, yAxis, bounds } = instance; + let { CSS, baseClass } = this; + const d = data as any; + + let axis = this.vertical ? xAxis : yAxis; + if (!axis) return null; + + let min: number, max: number, valueFunction: (value: any, offset: number) => [any, number]; + if (axis.scale) { + min = axis.scale.min; + max = axis.scale.max; + let clamp = (value: number): [number, number] => [Math.max(min, Math.min(max, value)), 0]; + valueFunction = (value, offset) => clamp(value + offset); + } else if (axis.valueList) { + min = 0; + max = axis.valueList.length; + valueFunction = (value, offset) => [axis.valueList[value], offset]; + } + if (!(min! < max!)) return null; + + let rectClass = CSS.element(baseClass, "lane"); + + if (this.vertical) { + let c1 = axis.map(...valueFunction!(d.x, -d.size / 2 + d.laneOffset)); + let c2 = axis.map(...valueFunction!(d.x, +d.size / 2 + d.laneOffset)); + return ( + + + {this.renderChildren(context, instance)}; + + ); + } else { + let c1 = axis.map(...valueFunction!(d.y, -d.size / 2 + d.laneOffset)); + let c2 = axis.map(...valueFunction!(d.y, +d.size / 2 + d.laneOffset)); + return ( + + + {this.renderChildren(context, instance)}; + + ); + } + } +} + +Swimlane.prototype.xAxis = "x"; +Swimlane.prototype.yAxis = "y"; +Swimlane.prototype.anchors = "0 1 1 0"; +Swimlane.prototype.baseClass = "swimlane"; +Swimlane.prototype.size = 0.5; +Swimlane.prototype.laneOffset = 0; +Swimlane.prototype.vertical = false; + +BoundedObject.alias("swimlane", Swimlane); diff --git a/packages/cx/src/charts/Swimlanes.d.ts b/packages/cx/src/charts/Swimlanes.d.ts deleted file mode 100644 index 5b627a607..000000000 --- a/packages/cx/src/charts/Swimlanes.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as Cx from "../core"; -import { BoundedObject, BoundedObjectProps } from "../svg/BoundedObject"; - -interface SwimlanesProps extends BoundedObjectProps { - /** - * Name of the horizontal axis. The value should match one of the horizontal axes set - * in the `axes` configuration of the parent `Chart` component. Default value is `x`. - * Set to `false` to hide the grid lines in x direction. - */ - xAxis?: string | boolean; - - /** - * Name of the vertical axis. The value should match one of the vertical axes set - * in the `axes` configuration if the parent `Chart` component. Default value is `y`. - * Set to `false` to hide the grid lines in y direction. - */ - yAxis?: string | boolean; - - /** Base CSS class to be applied to the element. Defaults to `swimlanes`. */ - baseClass?: string; - - /** Represents a swimlane size. */ - size?: Cx.NumberProp; - - /** - * Represents a swimlane step. Define a step on which a swimlane will be rendered. (eg. step 2 will render - * every second swimlane in the chart.) - */ - step?: Cx.NumberProp; - - /** Switch to vertical swimlanes. */ - vertical?: boolean; - - /**The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ - laneOffset?: Cx.NumberProp; - - /** Style object applied to the swimlanes. */ - laneStyle?: StyleProp; -} - -export class Swimlanes extends Cx.Widget {} diff --git a/packages/cx/src/charts/Swimlanes.js b/packages/cx/src/charts/Swimlanes.js deleted file mode 100644 index 0192ccace..000000000 --- a/packages/cx/src/charts/Swimlanes.js +++ /dev/null @@ -1,114 +0,0 @@ -import { BoundedObject } from "../svg/BoundedObject"; -import { parseStyle } from "../util/parseStyle"; -import { VDOM } from "../ui/Widget"; - -export class Swimlanes extends BoundedObject { - init() { - this.laneStyle = parseStyle(this.laneStyle); - super.init(); - } - declareData(...args) { - super.declareData(...args, { - size: undefined, - step: undefined, - laneOffset: undefined, - laneStyle: { structured: true }, - }); - } - - explore(context, instance) { - super.explore(context, instance); - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - } - - prepare(context, instance) { - super.prepare(context, instance); - let { xAxis, yAxis } = instance; - if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) instance.markShouldUpdate(context); - } - - render(context, instance, key) { - let { data, xAxis, yAxis } = instance; - let { bounds } = data; - let { CSS, baseClass } = this; - - if (data.step <= 0 || data.size <= 0) return; - - let axis = this.vertical ? xAxis : yAxis; - - if (!axis) return null; - - let min, max, valueFunction; - - if (axis.scale) { - min = axis.scale.min; - max = axis.scale.max; - let clamp = (value) => [Math.max(min, Math.min(max, value)), 0]; - valueFunction = (value, offset) => clamp(value + offset); - } else if (axis.valueList) { - min = 0; - max = axis.valueList.length; - valueFunction = (value, offset) => [axis.valueList[value], offset]; - } - - if (!(min < max)) return null; - - let rects = []; - - let at = Math.ceil(min / data.step) * data.step; - let index = 0; - - let rectClass = CSS.element(baseClass, "lane"); - - while (at - data.size / 2 < max) { - let c1 = axis.map(...valueFunction(at, -data.size / 2 + data.laneOffset)); - let c2 = axis.map(...valueFunction(at, +data.size / 2 + data.laneOffset)); - if (this.vertical) { - rects.push( - , - ); - } else { - rects.push( - , - ); - } - - at += data.step; - } - - return ( - - {rects} - - ); - } -} - -Swimlanes.prototype.xAxis = "x"; -Swimlanes.prototype.yAxis = "y"; -Swimlanes.prototype.anchors = "0 1 1 0"; -Swimlanes.prototype.baseClass = "swimlanes"; -Swimlanes.prototype.size = 0.5; -Swimlanes.prototype.laneOffset = 0; -Swimlanes.prototype.step = 1; -Swimlanes.prototype.vertical = false; -Swimlanes.prototype.styled = true; - -BoundedObject.alias("swimlanes", Swimlanes); diff --git a/packages/cx/src/charts/Swimlanes.scss b/packages/cx/src/charts/Swimlanes.scss index 71bf6130d..45b824adc 100644 --- a/packages/cx/src/charts/Swimlanes.scss +++ b/packages/cx/src/charts/Swimlanes.scss @@ -1,7 +1,9 @@ +@use "sass:map"; + @mixin cx-swimlanes($name: "swimlanes", $besm: $cx-besm, $lane-color: $cx-default-swimlanes-lane-background-color) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$element}#{$name}-lane { fill: $lane-color; diff --git a/packages/cx/src/charts/Swimlanes.tsx b/packages/cx/src/charts/Swimlanes.tsx new file mode 100644 index 000000000..34b3b32e9 --- /dev/null +++ b/packages/cx/src/charts/Swimlanes.tsx @@ -0,0 +1,179 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../svg/BoundedObject"; +import { parseStyle } from "../util/parseStyle"; +import { VDOM } from "../ui/Widget"; +import { RenderingContext, CxChild } from "../ui/RenderingContext"; +import { NumberProp, StyleProp } from "../ui/Prop"; +import type { ChartRenderingContext } from "./Chart"; + +export interface SwimlanesConfig extends BoundedObjectConfig { + /** + * Name of the horizontal axis. The value should match one of the horizontal axes set + * in the `axes` configuration of the parent `Chart` component. Default value is `x`. + * Set to `false` to hide the grid lines in x direction. + */ + xAxis?: string | boolean; + + /** + * Name of the vertical axis. The value should match one of the vertical axes set + * in the `axes` configuration if the parent `Chart` component. Default value is `y`. + * Set to `false` to hide the grid lines in y direction. + */ + yAxis?: string | boolean; + + /** Base CSS class to be applied to the element. Defaults to `swimlanes`. */ + baseClass?: string; + + /** Represents a swimlane size. */ + size?: NumberProp; + + /** + * Represents a swimlane step. Define a step on which a swimlane will be rendered. (eg. step 2 will render + * every second swimlane in the chart.) + */ + step?: NumberProp; + + /** Switch to vertical swimlanes. */ + vertical?: boolean; + + /** The laneOffset property adjusts the positioning of lane elements, enhancing their alignment and readability. */ + laneOffset?: NumberProp; + + /** Style object applied to the swimlanes. */ + laneStyle?: StyleProp; +} + +export interface SwimlanesInstance extends BoundedObjectInstance { + xAxis?: any; + yAxis?: any; +} + +export class Swimlanes extends BoundedObject { + declare xAxis: string; + declare yAxis: string; + declare anchors: string; + declare baseClass: string; + declare size: number; + declare laneOffset: number; + declare step: number; + declare vertical: boolean; + declare styled: boolean; + declare laneStyle: any; + declare CSS: any; + + constructor(config?: SwimlanesConfig) { + super(config); + } + + init() { + this.laneStyle = parseStyle(this.laneStyle); + super.init(); + } + + declareData(...args: any[]) { + super.declareData(...args, { + size: undefined, + step: undefined, + laneOffset: undefined, + laneStyle: { structured: true }, + }); + } + + explore(context: ChartRenderingContext, instance: SwimlanesInstance) { + super.explore(context, instance); + instance.xAxis = context.axes?.[this.xAxis]; + instance.yAxis = context.axes?.[this.yAxis]; + } + + prepare(context: RenderingContext, instance: SwimlanesInstance) { + super.prepare(context, instance); + let { xAxis, yAxis } = instance; + if ((xAxis && xAxis.shouldUpdate) || (yAxis && yAxis.shouldUpdate)) instance.markShouldUpdate(context); + } + + render(context: RenderingContext, instance: SwimlanesInstance, key: string): CxChild { + let { data, xAxis, yAxis } = instance; + const d = data as any; + let { bounds } = d; + let { CSS, baseClass } = this; + + if (d.step <= 0 || d.size <= 0) return null; + + let axis = this.vertical ? xAxis : yAxis; + + if (!axis) return null; + + let min: number, max: number, valueFunction: (value: number, offset: number) => [any, number]; + + if (axis.scale) { + min = axis.scale.min; + max = axis.scale.max; + let clamp = (value: number): [number, number] => [Math.max(min, Math.min(max, value)), 0]; + valueFunction = (value, offset) => clamp(value + offset); + } else if (axis.valueList) { + min = 0; + max = axis.valueList.length; + valueFunction = (value, offset) => [axis.valueList[value], offset]; + } + + if (!(min! < max!)) return null; + + let rects: React.ReactElement[] = []; + + let at = Math.ceil(min! / d.step) * d.step; + let index = 0; + + let rectClass = CSS.element(baseClass, "lane"); + + while (at - d.size / 2 < max!) { + let c1 = axis.map(...valueFunction!(at, -d.size / 2 + d.laneOffset)); + let c2 = axis.map(...valueFunction!(at, +d.size / 2 + d.laneOffset)); + if (this.vertical) { + rects.push( + + ); + } else { + rects.push( + + ); + } + + at += d.step; + } + + return ( + + {rects} + + ); + } +} + +Swimlanes.prototype.xAxis = "x"; +Swimlanes.prototype.yAxis = "y"; +Swimlanes.prototype.anchors = "0 1 1 0"; +Swimlanes.prototype.baseClass = "swimlanes"; +Swimlanes.prototype.size = 0.5; +Swimlanes.prototype.laneOffset = 0; +Swimlanes.prototype.step = 1; +Swimlanes.prototype.vertical = false; +Swimlanes.prototype.styled = true; + +BoundedObject.alias("swimlanes", Swimlanes); diff --git a/packages/cx/src/charts/axis/Axis.d.ts b/packages/cx/src/charts/axis/Axis.d.ts deleted file mode 100644 index 072ddb692..000000000 --- a/packages/cx/src/charts/axis/Axis.d.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Instance } from "./../../ui/Instance.d"; -import * as Cx from "../../core"; -import { BoundedObject, BoundedObjectProps } from "../../svg/BoundedObject"; - -export interface AxisProps extends BoundedObjectProps { - /** Set to `true` for vertical axes. */ - vertical?: boolean; - - /** Used as a secondary axis. Displayed at the top/right. */ - secondary?: boolean; - - /** When set to `true`, the values are displayed in descending order. */ - inverted?: Cx.BooleanProp; - - /** When set to `true`, rendering of visual elements of the axis, such as ticks and labels, is skipped, but their function is preserved. */ - hidden?: boolean; - - /** Size of the axis tick line. Defaults to 3. */ - tickSize?: number; - - /** Distance between ticks and the axis. Default is 0. Use negative values for offset to make ticks appear on both sides of the axis. */ - tickOffset?: number; - - /** The smallest distance between two ticks on the axis. Defaults to 25. */ - minTickDistance?: number; - - /** The smallest distance between two labels on the vertical axis. Defaults to 40. */ - minLabelDistanceVertical?: number; - - /** The smallest distance between two labels on the horizontal axis. Defaults to 50. */ - minLabelDistanceHorizontal?: number; - - /** Distance between labels and the axis. Defaults to 10. */ - labelOffset?: number | string; - - /** Label rotation angle in degrees. */ - labelRotation?: Cx.Prop; - - /** Label text-anchor value. Allowed values are start, end and middle. Default value is set based on the value of vertical and secondary flags. */ - labelAnchor?: "start" | "end" | "middle" | "auto"; - - /** Horizontal text offset. */ - labelDx?: number | string; - - /** Vertical text offset which can be used for vertical alignment. */ - labelDy?: number | string; - - /** Set to `true` to break long labels into multiple lines. Default value is `false`. Text is split at space characters. See also `labelMaxLineLength` and `labelLineCountDyFactor`. */ - labelWrap?: boolean; - - /** - * Used for vertical adjustment of multi-line labels. Default value is `auto` which means - * that value is initialized based on axis configuration. Value `0` means that label will grow towards - * the bottom of the screen. Value `-1` will make labels to grow towards the top of the screen. - * `-0.5` will make labels vertically centered. - */ - labelLineCountDyFactor?: number | string; - - /** - * Used for vertical adjustment of multi-line labels. Default value is 1 which means - * that labels are stacked without any space between them. Value of 1.4 will add 40% of the label height as a space between labels. - */ - labelLineHeight?: number | string; - - /** If `labelWrap` is on, this number is used as a measure to split labels into multiple lines. Default value is `10`. */ - labelMaxLineLength?: number; - - /** Set to true to hide the axis labels. */ - hideLabels?: boolean; - - /** Set to true to hide the axis line. */ - hideLine?: boolean; - - /** Set to true to hide the axis ticks. */ - hideTicks?: boolean; - - /** Additional CSS style to be applied to the axis line. */ - lineStyle?: Cx.StyleProp; - - /** Additional CSS style to be applied to the axis ticks. */ - tickStyle?: Cx.StyleProp; - - /** Additional CSS style to be applied to the axis labels. */ - labelStyle?: Cx.StyleProp; - - /** Additional CSS class to be applied to the axis line. */ - lineClass?: Cx.ClassProp; - - /** Additional CSS class to be applied to the axis ticks. */ - tickClass?: Cx.ClassProp; - - /** Additional CSS class to be applied to the axis labels. */ - labelClass?: Cx.ClassProp; - - onMeasured?: (info: any, instance: Instance) => void; - - /** A function used to create a formatter function for axis labels. See Complex Labels example in the CxJS documentation for more info. */ - onCreateLabelFormatter?: - | string - | (( - context: any, - instance: Instance, - ) => ( - formattedValue: string, - value: any, - { tickIndex, serieIndex }: { tickIndex: number; serieIndex: number }, - ) => { text: string; style?: any; className?: string }[]); - - /** Distance between the even labels and the axis. */ - alternateLabelOffset?: number | string; -} - -export class Axis extends BoundedObject {} diff --git a/packages/cx/src/charts/axis/Axis.js b/packages/cx/src/charts/axis/Axis.js deleted file mode 100644 index 5ef772b93..000000000 --- a/packages/cx/src/charts/axis/Axis.js +++ /dev/null @@ -1,288 +0,0 @@ -import { BoundedObject } from "../../svg/BoundedObject"; -import { VDOM } from "../../ui/Widget"; -import { isUndefined } from "../../util/isUndefined"; -import { parseStyle } from "../../util/parseStyle"; - -export class Axis extends BoundedObject { - init() { - if (this.labelAnchor == "auto") this.labelAnchor = this.vertical ? (this.secondary ? "start" : "end") : "middle"; - - if (this.labelDx == "auto") this.labelDx = 0; - - if (this.labelDy == "auto") this.labelDy = this.vertical ? "0.4em" : this.secondary ? 0 : "0.8em"; - - if (isUndefined(this.minLabelDistance)) - this.minLabelDistance = this.vertical ? this.minLabelDistanceVertical : this.minLabelDistanceHorizontal; - - if (this.labelLineCountDyFactor == "auto") - this.labelLineCountDyFactor = this.vertical ? -this.labelLineHeight / 2 : this.secondary ? -1 : 0; - - this.lineStyle = parseStyle(this.lineStyle); - this.tickStyle = parseStyle(this.tickStyle); - this.labelStyle = parseStyle(this.labelStyle); - - super.init(); - } - - declareData() { - super.declareData( - { - anchors: undefined, - hideLabels: undefined, - hideLine: undefined, - hideTicks: undefined, - labelRotation: undefined, - labelAnchor: undefined, - lineStyle: undefined, - lineClass: undefined, - labelStyle: undefined, - labelClass: undefined, - tickStyle: undefined, - tickClass: undefined, - }, - ...arguments, - ); - } - - prepareData(context, instance) { - super.prepareData(context, instance); - if (this.onCreateLabelFormatter) - instance.labelFormatter = instance.invoke("onCreateLabelFormatter", context, instance); - } - - report(context, instance) { - return instance.calculator; - } - - reportData(context, instance) {} - - renderTicksAndLabels(context, instance, valueFormatter, minLabelDistance) { - if (this.hidden) return false; - - var { data, calculator, labelFormatter } = instance; - var { bounds } = data; - let { CSS, baseClass } = this; - var size = calculator.findTickSize(minLabelDistance); - - var labelClass = CSS.expand(CSS.element(baseClass, "label"), data.labelClass); - var offsetClass = CSS.element(baseClass, "label-offset"); - - var x1, - y1, - x2, - y2, - tickSize = this.tickSize, - tickOffset = this.tickOffset; - - if (this.vertical) { - x1 = x2 = this.secondary ? bounds.r : bounds.l; - y1 = bounds.b; - y2 = bounds.t; - } else { - x1 = bounds.l; - x2 = bounds.r; - y1 = y2 = this.secondary ? bounds.t : bounds.b; - } - - var res = [null, null]; - - if (!data.hideLine) { - res[0] = ( - - ); - } - - var t = []; - if (!!size && !data.hideLabels) { - var ticks = calculator.getTicks([size]); - ticks.forEach((serie, si) => { - serie.forEach((v, i) => { - var s = calculator.map(v); - - if (this.secondary) { - x1 = this.vertical ? bounds.r + tickOffset : s; - y1 = this.vertical ? s : bounds.t - tickOffset; - x2 = this.vertical ? bounds.r + tickOffset + tickSize : s; - y2 = this.vertical ? s : bounds.t - tickOffset - tickSize; - } else { - x1 = this.vertical ? bounds.l - tickOffset : s; - y1 = this.vertical ? s : bounds.b + tickOffset; - x2 = this.vertical ? bounds.l - tickOffset - tickSize : s; - y2 = this.vertical ? s : bounds.b + tickOffset + tickSize; - } - - if (!this.useGridlineTicks) t.push(`M ${x1} ${y1} L ${x2} ${y2}`); - - var x, y; - let labelOffset = - this.alternateLabelOffset != null && i % 2 == 1 ? this.alternateLabelOffset : this.labelOffset; - - if (this.secondary) { - x = this.vertical ? bounds.r + labelOffset : s; - y = this.vertical ? s : bounds.t - labelOffset; - } else { - x = this.vertical ? bounds.l - labelOffset : s; - y = this.vertical ? s : bounds.b + labelOffset; - } - - var transform = data.labelRotation ? `rotate(${data.labelRotation} ${x} ${y})` : null; - var formattedValue = valueFormatter(v); - var lines = labelFormatter - ? labelFormatter(formattedValue, v, { tickIndex: si, serieIndex: i }) - : this.wrapLines(formattedValue); - res.push( - - {this.renderLabels(lines, x, this.labelDy, this.labelDx, offsetClass)} - , - ); - }); - }); - } - - if (!data.hideTicks) { - if (this.useGridlineTicks) { - let gridlines = calculator.mapGridlines(); - gridlines.forEach((s, i) => { - if (this.secondary) { - x1 = this.vertical ? bounds.r + tickOffset : s; - y1 = this.vertical ? s : bounds.t - tickOffset; - x2 = this.vertical ? bounds.r + tickOffset + tickSize : s; - y2 = this.vertical ? s : bounds.t - tickOffset - tickSize; - } else { - x1 = this.vertical ? bounds.l - tickOffset : s; - y1 = this.vertical ? s : bounds.b + tickOffset; - x2 = this.vertical ? bounds.l - tickOffset - tickSize : s; - y2 = this.vertical ? s : bounds.b + tickOffset + tickSize; - } - t.push(`M ${x1} ${y1} L ${x2} ${y2}`); - }); - } - - res[1] = ( - - ); - } - - return res; - } - - wrapLines(str) { - if (!this.labelWrap || typeof str != "string") return [{ text: str }]; - - let parts = str.split(" "); - if (parts.length == 0) return null; - - let lines = []; - let line = null; - for (let i = 0; i < parts.length; i++) { - if (!line) line = parts[i]; - else if (parts[i].length + line.length < this.labelMaxLineLength) line += " " + parts[i]; - else { - lines.push({ text: line }); - line = parts[i]; - } - } - lines.push({ text: line }); - return lines; - } - - renderLabels(lines, x, dy, dx, offsetClass) { - let offset = this.labelLineCountDyFactor * (lines.length - 1); - let result = []; - - if (lines.length > 1 && dy != null) { - result.push( - - _ - , - ); - } - - lines.forEach((p, i) => { - let data = - p.data != null - ? Object.entries(p.data).reduce((acc, [key, val]) => { - acc[`data-${key}`] = val; - return acc; - }, {}) - : null; - result.push( - 1 ? `${i == 0 ? offset : this.labelLineHeight}em` : dy} - x={x} - style={p.style} - className={p.className} - dx={dx} - {...data} - > - {p.text} - , - ); - }); - return result; - } - - prepare(context, instance) { - super.prepare(context, instance); - var { bounds } = instance.data; - var [a, b] = !this.vertical ? [bounds.l, bounds.r] : [bounds.b, bounds.t]; - instance.calculator.measure(a, b); - if (this.onMeasured) instance.invoke("onMeasured", instance.calculator.hash(), instance); - if (!instance.calculator.isSame(instance.cached.axis)) instance.markShouldUpdate(context); - } - - cleanup(context, instance) { - var { cached, calculator } = instance; - cached.axis = calculator.hash(); - } -} - -Axis.prototype.anchors = "0 1 1 0"; -Axis.prototype.styled = true; -Axis.prototype.vertical = false; -Axis.prototype.secondary = false; -Axis.prototype.inverted = false; -Axis.prototype.hidden = false; -Axis.prototype.hideLabels = false; -Axis.prototype.hideTicks = false; -Axis.prototype.hideLine = false; - -Axis.prototype.tickSize = 3; -Axis.prototype.tickOffset = 0; -Axis.prototype.minTickDistance = 25; -Axis.prototype.minLabelDistanceVertical = 40; -Axis.prototype.minLabelDistanceHorizontal = 50; -Axis.prototype.labelOffset = 10; -Axis.prototype.alternateLabelOffset = null; -Axis.prototype.labelRotation = 0; -Axis.prototype.labelAnchor = "auto"; -Axis.prototype.labelDx = "auto"; -Axis.prototype.labelDy = "auto"; -Axis.prototype.labelWrap = false; -Axis.prototype.labelLineCountDyFactor = "auto"; -Axis.prototype.labelLineHeight = 1; -Axis.prototype.labelMaxLineLength = 10; - -Axis.namespace = "ui.svg.chart.axis"; diff --git a/packages/cx/src/charts/axis/Axis.tsx b/packages/cx/src/charts/axis/Axis.tsx new file mode 100644 index 000000000..498cfb6f4 --- /dev/null +++ b/packages/cx/src/charts/axis/Axis.tsx @@ -0,0 +1,444 @@ +/** @jsxImportSource react */ + +import { BoundedObject, BoundedObjectConfig, BoundedObjectInstance } from "../../svg/BoundedObject"; +import { VDOM } from "../../ui/Widget"; +import { isUndefined } from "../../util/isUndefined"; +import { parseStyle } from "../../util/parseStyle"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { Instance } from "../../ui/Instance"; +import { BooleanProp, StyleProp, ClassProp, Prop } from "../../ui/Prop"; + +export interface AxisConfig extends BoundedObjectConfig { + /** Set to `true` for vertical axes. */ + vertical?: boolean; + + /** Used as a secondary axis. Displayed at the top/right. */ + secondary?: boolean; + + /** When set to `true`, the values are displayed in descending order. */ + inverted?: BooleanProp; + + /** When set to `true`, rendering of visual elements of the axis, such as ticks and labels, is skipped, but their function is preserved. */ + hidden?: boolean; + + /** Size of the axis tick line. Defaults to 3. */ + tickSize?: number; + + /** Distance between ticks and the axis. Default is 0. Use negative values for offset to make ticks appear on both sides of the axis. */ + tickOffset?: number; + + /** The smallest distance between two ticks on the axis. Defaults to 25. */ + minTickDistance?: number; + + /** The smallest distance between two labels on the vertical axis. Defaults to 40. */ + minLabelDistanceVertical?: number; + + /** The smallest distance between two labels on the horizontal axis. Defaults to 50. */ + minLabelDistanceHorizontal?: number; + + /** Distance between labels and the axis. Defaults to 10. */ + labelOffset?: number | string; + + /** Label rotation angle in degrees. */ + labelRotation?: Prop; + + /** Label text-anchor value. Allowed values are start, end and middle. Default value is set based on the value of vertical and secondary flags. */ + labelAnchor?: "start" | "end" | "middle" | "auto"; + + /** Horizontal text offset. */ + labelDx?: number | string; + + /** Vertical text offset which can be used for vertical alignment. */ + labelDy?: number | string; + + /** Set to `true` to break long labels into multiple lines. Default value is `false`. Text is split at space characters. See also `labelMaxLineLength` and `labelLineCountDyFactor`. */ + labelWrap?: boolean; + + /** + * Used for vertical adjustment of multi-line labels. Default value is `auto` which means + * that value is initialized based on axis configuration. Value `0` means that label will grow towards + * the bottom of the screen. Value `-1` will make labels to grow towards the top of the screen. + * `-0.5` will make labels vertically centered. + */ + labelLineCountDyFactor?: number | string; + + /** + * Used for vertical adjustment of multi-line labels. Default value is 1 which means + * that labels are stacked without any space between them. Value of 1.4 will add 40% of the label height as a space between labels. + */ + labelLineHeight?: number | string; + + /** If `labelWrap` is on, this number is used as a measure to split labels into multiple lines. Default value is `10`. */ + labelMaxLineLength?: number; + + /** Set to true to hide the axis labels. */ + hideLabels?: boolean; + + /** Set to true to hide the axis line. */ + hideLine?: boolean; + + /** Set to true to hide the axis ticks. */ + hideTicks?: boolean; + + /** Additional CSS style to be applied to the axis line. */ + lineStyle?: StyleProp; + + /** Additional CSS style to be applied to the axis ticks. */ + tickStyle?: StyleProp; + + /** Additional CSS style to be applied to the axis labels. */ + labelStyle?: StyleProp; + + /** Additional CSS class to be applied to the axis line. */ + lineClass?: ClassProp; + + /** Additional CSS class to be applied to the axis ticks. */ + tickClass?: ClassProp; + + /** Additional CSS class to be applied to the axis labels. */ + labelClass?: ClassProp; + + onMeasured?: (info: any, instance: Instance) => void; + + /** A function used to create a formatter function for axis labels. */ + onCreateLabelFormatter?: + | string + | (( + context: any, + instance: Instance, + ) => ( + formattedValue: string, + value: any, + info: { tickIndex: number; serieIndex: number }, + ) => { text: string; style?: any; className?: string }[]); + + /** Distance between the even labels and the axis. */ + alternateLabelOffset?: number | string; + + useGridlineTicks?: boolean; +} + +export interface AxisInstance extends BoundedObjectInstance { + calculator: any; + labelFormatter?: any; + cached: { axis?: any }; +} + +export class Axis extends BoundedObject { + declare baseClass: string; + declare vertical: boolean; + declare secondary: boolean; + declare inverted: boolean; + declare hidden: boolean; + declare hideLabels: boolean; + declare hideTicks: boolean; + declare hideLine: boolean; + declare tickSize: number; + declare tickOffset: number; + declare minTickDistance: number; + declare minLabelDistance: number; + declare minLabelDistanceVertical: number; + declare minLabelDistanceHorizontal: number; + declare labelOffset: number; + declare alternateLabelOffset: number | null; + declare labelRotation: number; + declare labelAnchor: string; + declare labelDx: number | string; + declare labelDy: number | string; + declare labelWrap: boolean; + declare labelLineCountDyFactor: number | string; + declare labelLineHeight: number; + declare labelMaxLineLength: number; + declare lineStyle: any; + declare tickStyle: any; + declare labelStyle: any; + declare useGridlineTicks: boolean; + declare onCreateLabelFormatter: AxisConfig["onCreateLabelFormatter"]; + declare onMeasured: AxisConfig["onMeasured"]; + + constructor(config?: AxisConfig) { + super(config); + } + + init(): void { + if (this.labelAnchor == "auto") this.labelAnchor = this.vertical ? (this.secondary ? "start" : "end") : "middle"; + + if (this.labelDx == "auto") this.labelDx = 0; + + if (this.labelDy == "auto") this.labelDy = this.vertical ? "0.4em" : this.secondary ? 0 : "0.8em"; + + if (isUndefined(this.minLabelDistance)) + this.minLabelDistance = this.vertical ? this.minLabelDistanceVertical : this.minLabelDistanceHorizontal; + + if (this.labelLineCountDyFactor == "auto") + this.labelLineCountDyFactor = this.vertical ? -this.labelLineHeight / 2 : this.secondary ? -1 : 0; + + this.lineStyle = parseStyle(this.lineStyle); + this.tickStyle = parseStyle(this.tickStyle); + this.labelStyle = parseStyle(this.labelStyle); + + super.init(); + } + + declareData(...args: any[]): void { + super.declareData( + { + anchors: undefined, + hideLabels: undefined, + hideLine: undefined, + hideTicks: undefined, + labelRotation: undefined, + labelAnchor: undefined, + lineStyle: undefined, + lineClass: undefined, + labelStyle: undefined, + labelClass: undefined, + tickStyle: undefined, + tickClass: undefined, + }, + ...args, + ); + } + + prepareData(context: RenderingContext, instance: AxisInstance): void { + super.prepareData(context, instance); + if (this.onCreateLabelFormatter) + instance.labelFormatter = instance.invoke("onCreateLabelFormatter", context, instance); + } + + report(context: RenderingContext, instance: AxisInstance): any { + return instance.calculator; + } + + reportData(context: RenderingContext, instance: AxisInstance): void {} + + renderTicksAndLabels(context: RenderingContext, instance: AxisInstance, valueFormatter: (v: any) => string, minLabelDistance: number): any { + if (this.hidden) return false; + + var { data, calculator, labelFormatter } = instance; + var { bounds } = data; + let { CSS, baseClass } = this; + var size = calculator.findTickSize(minLabelDistance); + + var labelClass = CSS.expand(CSS.element(baseClass, "label"), data.labelClass); + var offsetClass = CSS.element(baseClass, "label-offset"); + + var x1, + y1, + x2, + y2, + tickSize = this.tickSize, + tickOffset = this.tickOffset; + + if (this.vertical) { + x1 = x2 = this.secondary ? bounds.r : bounds.l; + y1 = bounds.b; + y2 = bounds.t; + } else { + x1 = bounds.l; + x2 = bounds.r; + y1 = y2 = this.secondary ? bounds.t : bounds.b; + } + + var res: any[] = [null, null]; + + if (!data.hideLine) { + res[0] = ( + + ); + } + + var t: string[] = []; + if (!!size && !data.hideLabels) { + var ticks = calculator.getTicks([size]); + ticks.forEach((serie: any[], si: number) => { + serie.forEach((v: any, i: number) => { + var s = calculator.map(v); + + if (this.secondary) { + x1 = this.vertical ? bounds.r + tickOffset : s; + y1 = this.vertical ? s : bounds.t - tickOffset; + x2 = this.vertical ? bounds.r + tickOffset + tickSize : s; + y2 = this.vertical ? s : bounds.t - tickOffset - tickSize; + } else { + x1 = this.vertical ? bounds.l - tickOffset : s; + y1 = this.vertical ? s : bounds.b + tickOffset; + x2 = this.vertical ? bounds.l - tickOffset - tickSize : s; + y2 = this.vertical ? s : bounds.b + tickOffset + tickSize; + } + + if (!this.useGridlineTicks) t.push(`M ${x1} ${y1} L ${x2} ${y2}`); + + var x, y; + let labelOffset = + this.alternateLabelOffset != null && i % 2 == 1 ? this.alternateLabelOffset : this.labelOffset; + + if (this.secondary) { + x = this.vertical ? bounds.r + labelOffset : s; + y = this.vertical ? s : bounds.t - labelOffset; + } else { + x = this.vertical ? bounds.l - labelOffset : s; + y = this.vertical ? s : bounds.b + labelOffset; + } + + var transform = data.labelRotation ? `rotate(${data.labelRotation} ${x} ${y})` : undefined; + var formattedValue = valueFormatter(v); + var lines = labelFormatter + ? labelFormatter(formattedValue, v, { tickIndex: si, serieIndex: i }) + : this.wrapLines(formattedValue); + res.push( + + {this.renderLabels(lines, x, this.labelDy, this.labelDx, offsetClass)} + , + ); + }); + }); + } + + if (!data.hideTicks) { + if (this.useGridlineTicks) { + let gridlines = calculator.mapGridlines(); + gridlines.forEach((s: number, i: number) => { + if (this.secondary) { + x1 = this.vertical ? bounds.r + tickOffset : s; + y1 = this.vertical ? s : bounds.t - tickOffset; + x2 = this.vertical ? bounds.r + tickOffset + tickSize : s; + y2 = this.vertical ? s : bounds.t - tickOffset - tickSize; + } else { + x1 = this.vertical ? bounds.l - tickOffset : s; + y1 = this.vertical ? s : bounds.b + tickOffset; + x2 = this.vertical ? bounds.l - tickOffset - tickSize : s; + y2 = this.vertical ? s : bounds.b + tickOffset + tickSize; + } + t.push(`M ${x1} ${y1} L ${x2} ${y2}`); + }); + } + + res[1] = ( + + ); + } + + return res; + } + + wrapLines(str: any): { text: string; style?: any; className?: string }[] | null { + if (!this.labelWrap || typeof str != "string") return [{ text: str }]; + + let parts = str.split(" "); + if (parts.length == 0) return null; + + let lines: { text: string }[] = []; + let line: string | null = null; + for (let i = 0; i < parts.length; i++) { + if (!line) line = parts[i]; + else if (parts[i].length + line.length < this.labelMaxLineLength) line += " " + parts[i]; + else { + lines.push({ text: line }); + line = parts[i]; + } + } + if (line) lines.push({ text: line }); + return lines; + } + + renderLabels(lines: { text: string; style?: any; className?: string; data?: Record }[], x: number, dy: number | string, dx: number | string, offsetClass: string): React.ReactNode[] { + let offset = (this.labelLineCountDyFactor as number) * (lines.length - 1); + let result = []; + + if (lines.length > 1 && dy != null) { + result.push( + + _ + , + ); + } + + lines.forEach((p, i) => { + let data = + p.data != null + ? Object.entries(p.data).reduce((acc, [key, val]) => { + acc[`data-${key}`] = val; + return acc; + }, {} as Record) + : null; + result.push( + 1 ? `${i == 0 ? offset : this.labelLineHeight}em` : dy} + x={x} + style={p.style} + className={p.className} + dx={dx} + {...data} + > + {p.text} + , + ); + }); + return result; + } + + prepare(context: RenderingContext, instance: AxisInstance): void { + super.prepare(context, instance); + var { bounds } = instance.data; + var [a, b] = !this.vertical ? [bounds.l, bounds.r] : [bounds.b, bounds.t]; + instance.calculator.measure(a, b); + if (this.onMeasured) instance.invoke("onMeasured", instance.calculator.hash(), instance); + if (!instance.calculator.isSame(instance.cached.axis)) instance.markShouldUpdate(context); + } + + cleanup(context: RenderingContext, instance: AxisInstance): void { + var { cached, calculator } = instance; + cached.axis = calculator.hash(); + } +} + +Axis.prototype.anchors = "0 1 1 0"; +Axis.prototype.styled = true; +Axis.prototype.vertical = false; +Axis.prototype.secondary = false; +Axis.prototype.inverted = false; +Axis.prototype.hidden = false; +Axis.prototype.hideLabels = false; +Axis.prototype.hideTicks = false; +Axis.prototype.hideLine = false; + +Axis.prototype.tickSize = 3; +Axis.prototype.tickOffset = 0; +Axis.prototype.minTickDistance = 25; +Axis.prototype.minLabelDistanceVertical = 40; +Axis.prototype.minLabelDistanceHorizontal = 50; +Axis.prototype.labelOffset = 10; +Axis.prototype.alternateLabelOffset = null; +Axis.prototype.labelRotation = 0; +Axis.prototype.labelAnchor = "auto"; +Axis.prototype.labelDx = "auto"; +Axis.prototype.labelDy = "auto"; +Axis.prototype.labelWrap = false; +Axis.prototype.labelLineCountDyFactor = "auto"; +Axis.prototype.labelLineHeight = 1; +Axis.prototype.labelMaxLineLength = 10; + +Axis.namespace = "ui.svg.chart.axis"; diff --git a/packages/cx/src/charts/axis/CategoryAxis.d.ts b/packages/cx/src/charts/axis/CategoryAxis.d.ts deleted file mode 100644 index 6bd261d7f..000000000 --- a/packages/cx/src/charts/axis/CategoryAxis.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Cx from "../../core"; -import { AxisProps } from "./Axis"; - -interface CategoryAxisProps extends AxisProps { - /** Uniform axes provide exact size and offset for all entries, while non-uniform axes adapt their size and offset to the number of entries under each category. */ - uniform?: Cx.BooleanProp; - - /** Names corresponding the given `values`. For example, `values` may be 0 .. 11 and `names` could be Jan .. Dec. */ - names?: Cx.Prop; - - /** Values used to initialize the axis. If an object is provided, keys are used for values and values are used for names. */ - values?: Cx.Prop; - - /** Sometimes, there is not enough data and each category takes a lot of space. `minSize` can be used to add fake entries up to the specified number, so everything looks normal. */ - minSize?: Cx.NumberProp; - - /** Base CSS class to be applied to the element. Defaults to `categoryaxis`. */ - baseClass?: string; - - /** Output value that can be used to calculate chart dimensions based on discovered category values. */ - categoryCount?: Binding | Cx.AccessorChain | Cx.GetSet; - - /** Set to true to show ticks aligned with gridlines instead of labels. Default is false. */ - useGridlineTicks?: boolean; - - /** Additional label formatting. No format is set by default, values appear as is. Useful when values are not strings or string values that are too long.*/ - format?: Cx.StringProp; -} - -export class CategoryAxis extends Cx.Widget {} diff --git a/packages/cx/src/charts/axis/CategoryAxis.js b/packages/cx/src/charts/axis/CategoryAxis.js deleted file mode 100644 index c0ed1d796..000000000 --- a/packages/cx/src/charts/axis/CategoryAxis.js +++ /dev/null @@ -1,241 +0,0 @@ -import { Axis } from "./Axis"; -import { VDOM } from "../../ui/Widget"; -import { isUndefined } from "../../util/isUndefined"; -import { isArray } from "../../util/isArray"; -import { Format } from "../../util/Format"; - -export class CategoryAxis extends Axis { - declareData() { - super.declareData(...arguments, { - inverted: undefined, - uniform: undefined, - names: undefined, - values: undefined, - minSize: undefined, - categoryCount: undefined, - format: undefined, - }); - } - - initInstance(context, instance) { - instance.calculator = new CategoryScale(); - } - - explore(context, instance) { - super.explore(context, instance); - var { values, names, inverted, uniform, minSize } = instance.data; - instance.calculator.reset(inverted, uniform, values, names, minSize, this.minTickDistance, this.minLabelDistance); - } - - reportData(context, instance) { - instance.set("categoryCount", instance.calculator.valueList.length); - } - - render(context, instance, key) { - var { data, calculator } = instance; - - if (!data.bounds.valid()) return null; - - let labelGetter = (v) => calculator.names[v] ?? v; - let labelFormatter = labelGetter; - if (data.format) { - let formatter = Format.parse(data.format); - labelFormatter = (v) => formatter(labelGetter(v)); - } - return ( - - {this.renderTicksAndLabels(context, instance, labelFormatter)} - - ); - } -} - -CategoryAxis.prototype.baseClass = "categoryaxis"; -CategoryAxis.prototype.anchors = "0 1 1 0"; -CategoryAxis.prototype.vertical = false; -CategoryAxis.prototype.inverted = false; -CategoryAxis.prototype.uniform = false; -CategoryAxis.prototype.labelOffset = 10; -CategoryAxis.prototype.labelRotation = 0; -CategoryAxis.prototype.labelAnchor = "auto"; -CategoryAxis.prototype.labelDx = "auto"; -CategoryAxis.prototype.labelDy = "auto"; -CategoryAxis.prototype.minSize = 1; -CategoryAxis.prototype.minLabelDistanceHorizontal = 0; -CategoryAxis.prototype.minLabelDistanceVertical = 0; -CategoryAxis.prototype.minTickDistance = 0; - -Axis.alias("category", CategoryAxis); - -class CategoryScale { - reset(inverted, uniform, values, names, minSize, minTickDistance, minLabelDistance) { - this.padding = 0.5; - delete this.min; - delete this.max; - delete this.minValue; - delete this.maxValue; - this.minSize = minSize; - this.valuesMap = {}; - this.valueList = []; - this.inverted = inverted; - this.uniform = uniform; - this.valueStacks = {}; - this.names = {}; - this.minTickDistance = minTickDistance; - this.minLabelDistance = minLabelDistance; - - if (values) { - if (isArray(values)) values.forEach((v) => this.acknowledge(v)); - else if (typeof values == "object") - for (var k in values) { - this.acknowledge(k); - this.names[k] = values[k]; - } - } - - if (names) { - if (isArray(names)) { - values = values || []; - names.forEach((name, index) => { - var value = values[index]; - this.names[value != null ? value : index] = name; - }); - } else this.names = names; - } - } - - decodeValue(n) { - return n; - } - - encodeValue(v) { - return v; - } - - map(v, offset = 0) { - var index = this.valuesMap[v] || 0; - - return this.origin + (index + offset - this.min + this.padding) * this.factor; - } - - measure(a, b) { - this.a = a; - this.b = b; - - if (this.min == null) this.min = this.minValue || 0; - - if (this.max == null) this.max = !isNaN(this.maxValue) ? this.maxValue : 100; - - var sign = this.inverted ? -1 : 1; - - if (this.max - this.min + 1 < this.minSize) { - this.factor = (sign * (this.b - this.a)) / this.minSize; - this.origin = (this.b + this.a) * 0.5 - (this.factor * (this.max - this.min + 1)) / 2; - } else { - this.factor = (sign * (this.b - this.a)) / (this.max - this.min + 2 * this.padding); - this.origin = (this.a * (1 + sign)) / 2 + (this.b * (1 - sign)) / 2; //a || b - } - - this.tickSizes = []; - let tickMultiplier = [1, 2, 5]; - let absFactor = Math.abs(this.factor); - for (let base = 1; base < 10000 && this.tickSizes.length < 2; base *= 10) { - for (let m of tickMultiplier) { - if (base * m * absFactor >= this.minTickDistance && this.tickSizes.length == 0) - this.tickSizes.push(base * m); - if (base * m * absFactor >= this.minLabelDistance) { - this.tickSizes.push(base * m); - break; - } - } - } - } - - hash() { - return { - origin: this.origin, - factor: this.factor, - min: this.min, - minSize: this.minSize, - padding: this.padding, - values: this.valueList.join(":"), - names: JSON.stringify(this.names), - }; - } - - isSame(x) { - var h = this.hash(); - var same = x && !Object.keys(h).some((k) => x[k] !== h[k]); - this.shouldUpdate = !same; - return same; - } - - acknowledge(value, width = 0, offset = 0) { - var index = this.valuesMap[value]; - if (isUndefined(index)) { - index = this.valueList.length; - this.valueList.push(value); - this.valuesMap[value] = index; - } - - if (this.minValue == null || index < this.minValue) { - this.minValue = index; - this.padding = Math.max(this.padding, Math.abs(offset - width / 2)); - } - - if (this.maxValue == null || index > this.maxValue) { - this.maxValue = index; - this.padding = Math.max(this.padding, Math.abs(offset + width / 2)); - } - } - - book(value, name) { - if (this.uniform) value = 0; - - var stack = this.valueStacks[value]; - if (!stack) - stack = this.valueStacks[value] = { - index: {}, - count: 0, - }; - if (!stack.index.hasOwnProperty(name)) stack.index[name] = stack.count++; - } - - locate(value, name) { - if (this.uniform) value = 0; - - var stack = this.valueStacks[value]; - if (!stack) return [0, 1]; - - return [stack.index[name], stack.count]; - } - - trackValue(v, offset = 0, constrain = false) { - let index = Math.round((v - this.origin) / this.factor - offset + this.min - this.padding); - if (index < this.min) index = this.min; - if (index > this.max) index = this.max; - return this.valueList[index]; - } - - findTickSize(minPxDist) { - for (let tickSize of this.tickSizes) if (tickSize * Math.abs(this.factor) >= minPxDist) return tickSize; - return 1; - } - - getTickSizes() { - return this.tickSizes; - } - - getTicks(tickSizes) { - return tickSizes.map((size) => this.valueList.filter((_, i) => i % size == 0)); - } - - mapGridlines() { - let result = []; - if (this.tickSizes.length == 0) return result; - let step = this.tickSizes[0]; - for (let index = this.min; index <= this.max + 1; index += step) - result.push(this.origin + (index - 0.5 - this.min + this.padding) * this.factor); - return result; - } -} diff --git a/packages/cx/src/charts/axis/CategoryAxis.scss b/packages/cx/src/charts/axis/CategoryAxis.scss index ad6a7fdad..e1d14c788 100644 --- a/packages/cx/src/charts/axis/CategoryAxis.scss +++ b/packages/cx/src/charts/axis/CategoryAxis.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-categoryaxis( $name: 'categoryaxis', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-axis(); diff --git a/packages/cx/src/charts/axis/CategoryAxis.tsx b/packages/cx/src/charts/axis/CategoryAxis.tsx new file mode 100644 index 000000000..09d5840cf --- /dev/null +++ b/packages/cx/src/charts/axis/CategoryAxis.tsx @@ -0,0 +1,313 @@ +/** @jsxImportSource react */ + +import { Axis, AxisConfig, AxisInstance } from "./Axis"; +import { VDOM } from "../../ui/Widget"; +import { isUndefined } from "../../util/isUndefined"; +import { isArray } from "../../util/isArray"; +import { Format } from "../../util/Format"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { BooleanProp, NumberProp, StringProp, Prop } from "../../ui/Prop"; +import { Binding } from "../../data/Binding"; +import { GetSet } from "../../ui/Prop"; +import { AccessorChain } from "../../data/createAccessorModelProxy"; + +export interface CategoryAxisConfig extends AxisConfig { + /** Uniform axes provide exact size and offset for all entries. */ + uniform?: BooleanProp; + + /** Names corresponding the given `values`. */ + names?: Prop>; + + /** Values used to initialize the axis. */ + values?: Prop>; + + /** Min number of entries. */ + minSize?: NumberProp; + + /** Base CSS class. Defaults to `categoryaxis`. */ + baseClass?: string; + + /** Output value for category count. */ + categoryCount?: Binding | AccessorChain | GetSet; + + /** Show ticks aligned with gridlines. */ + useGridlineTicks?: boolean; + + /** Additional label formatting. */ + format?: StringProp; +} + +export class CategoryAxis extends Axis { + declare uniform: boolean; + declare minSize: number; + + constructor(config: CategoryAxisConfig) { + super(config); + } + + declareData(...args: any[]): void { + super.declareData( + { + inverted: undefined, + uniform: undefined, + names: undefined, + values: undefined, + minSize: undefined, + categoryCount: undefined, + format: undefined, + }, + ...args, + ); + } + + initInstance(context: RenderingContext, instance: AxisInstance): void { + instance.calculator = new CategoryScale(); + } + + explore(context: RenderingContext, instance: AxisInstance): void { + super.explore(context, instance); + var { values, names, inverted, uniform, minSize } = instance.data; + instance.calculator.reset(inverted, uniform, values, names, minSize, this.minTickDistance, this.minLabelDistance); + } + + reportData(context: RenderingContext, instance: AxisInstance): void { + instance.set("categoryCount", instance.calculator.valueList.length); + } + + render(context: RenderingContext, instance: AxisInstance, key: string): React.ReactNode { + var { data, calculator } = instance; + + if (!data.bounds.valid()) return null; + + let labelGetter = (v: any) => calculator.names[v] ?? v; + let labelFormatter = labelGetter; + if (data.format) { + let formatter = Format.parse(data.format); + labelFormatter = (v: any) => formatter(labelGetter(v)); + } + return ( + + {this.renderTicksAndLabels(context, instance, labelFormatter, this.minLabelDistance)} + + ); + } +} + +CategoryAxis.prototype.baseClass = "categoryaxis"; +CategoryAxis.prototype.anchors = "0 1 1 0"; +CategoryAxis.prototype.vertical = false; +CategoryAxis.prototype.inverted = false; +CategoryAxis.prototype.uniform = false; +CategoryAxis.prototype.labelOffset = 10; +CategoryAxis.prototype.labelRotation = 0; +CategoryAxis.prototype.labelAnchor = "auto"; +CategoryAxis.prototype.labelDx = "auto"; +CategoryAxis.prototype.labelDy = "auto"; +CategoryAxis.prototype.minSize = 1; +CategoryAxis.prototype.minLabelDistanceHorizontal = 0; +CategoryAxis.prototype.minLabelDistanceVertical = 0; +CategoryAxis.prototype.minTickDistance = 0; + +Axis.alias("category", CategoryAxis); + +class CategoryScale { + padding: number; + min?: number; + max?: number; + minValue?: number; + maxValue?: number; + minSize: number; + valuesMap: Record; + valueList: any[]; + inverted: boolean; + uniform: boolean; + valueStacks: Record; count: number }>; + names: Record; + minTickDistance: number; + minLabelDistance: number; + origin: number; + factor: number; + tickSizes: number[]; + a: number; + b: number; + shouldUpdate: boolean; + + reset( + inverted: boolean, + uniform: boolean, + values: any, + names: any, + minSize: number, + minTickDistance: number, + minLabelDistance: number, + ): void { + this.padding = 0.5; + this.min = undefined; + this.max = undefined; + this.minValue = undefined; + this.maxValue = undefined; + this.minSize = minSize; + this.valuesMap = {}; + this.valueList = []; + this.inverted = inverted; + this.uniform = uniform; + this.valueStacks = {}; + this.names = {}; + this.minTickDistance = minTickDistance; + this.minLabelDistance = minLabelDistance; + + if (values) { + if (isArray(values)) values.forEach((v: any) => this.acknowledge(v)); + else if (typeof values == "object") + for (var k in values) { + this.acknowledge(k); + this.names[k] = values[k]; + } + } + + if (names) { + if (isArray(names)) { + values = values || []; + names.forEach((name: any, index: number) => { + var value = values[index]; + this.names[value != null ? value : index] = name; + }); + } else this.names = names; + } + } + + decodeValue(n: any): any { + return n; + } + + encodeValue(v: any): any { + return v; + } + + map(v: any, offset: number = 0): number { + var index = this.valuesMap[v] || 0; + + return this.origin + (index + offset - this.min! + this.padding) * this.factor; + } + + measure(a: number, b: number): void { + this.a = a; + this.b = b; + + if (this.min == null) this.min = this.minValue || 0; + + if (this.max == null) this.max = this.maxValue != null && !isNaN(this.maxValue) ? this.maxValue : 100; + + var sign = this.inverted ? -1 : 1; + + if (this.max! - this.min! + 1 < this.minSize) { + this.factor = (sign * (this.b - this.a)) / this.minSize; + this.origin = (this.b + this.a) * 0.5 - (this.factor * (this.max! - this.min! + 1)) / 2; + } else { + this.factor = (sign * (this.b - this.a)) / (this.max! - this.min! + 2 * this.padding); + this.origin = (this.a * (1 + sign)) / 2 + (this.b * (1 - sign)) / 2; //a || b + } + + this.tickSizes = []; + let tickMultiplier = [1, 2, 5]; + let absFactor = Math.abs(this.factor); + for (let base = 1; base < 10000 && this.tickSizes.length < 2; base *= 10) { + for (let m of tickMultiplier) { + if (base * m * absFactor >= this.minTickDistance && this.tickSizes.length == 0) + this.tickSizes.push(base * m); + if (base * m * absFactor >= this.minLabelDistance) { + this.tickSizes.push(base * m); + break; + } + } + } + } + + hash(): Record { + return { + origin: this.origin, + factor: this.factor, + min: this.min, + minSize: this.minSize, + padding: this.padding, + values: this.valueList.join(":"), + names: JSON.stringify(this.names), + }; + } + + isSame(x: any): boolean { + var h = this.hash(); + var same = x && !Object.keys(h).some((k) => x[k] !== h[k]); + this.shouldUpdate = !same; + return same; + } + + acknowledge(value: any, width: number = 0, offset: number = 0): void { + var index = this.valuesMap[value]; + if (isUndefined(index)) { + index = this.valueList.length; + this.valueList.push(value); + this.valuesMap[value] = index; + } + + if (this.minValue == null || index < this.minValue) { + this.minValue = index; + this.padding = Math.max(this.padding, Math.abs(offset - width / 2)); + } + + if (this.maxValue == null || index > this.maxValue) { + this.maxValue = index; + this.padding = Math.max(this.padding, Math.abs(offset + width / 2)); + } + } + + book(value: any, name: string): void { + if (this.uniform) value = 0; + + var stack = this.valueStacks[value]; + if (!stack) + stack = this.valueStacks[value] = { + index: {}, + count: 0, + }; + if (!stack.index.hasOwnProperty(name)) stack.index[name] = stack.count++; + } + + locate(value: any, name: string): [number, number] { + if (this.uniform) value = 0; + + var stack = this.valueStacks[value]; + if (!stack) return [0, 1]; + + return [stack.index[name], stack.count]; + } + + trackValue(v: number, offset: number = 0, constrain: boolean = false): any { + let index = Math.round((v - this.origin) / this.factor - offset + this.min! - this.padding); + if (index < this.min!) index = this.min!; + if (index > this.max!) index = this.max!; + return this.valueList[index]; + } + + findTickSize(minPxDist: number): number { + for (let tickSize of this.tickSizes) if (tickSize * Math.abs(this.factor) >= minPxDist) return tickSize; + return 1; + } + + getTickSizes(): number[] { + return this.tickSizes; + } + + getTicks(tickSizes: number[]): any[][] { + return tickSizes.map((size) => this.valueList.filter((_, i) => i % size == 0)); + } + + mapGridlines(): number[] { + let result: number[] = []; + if (this.tickSizes.length == 0) return result; + let step = this.tickSizes[0]; + for (let index = this.min!; index <= this.max! + 1; index += step) + result.push(this.origin + (index - 0.5 - this.min! + this.padding) * this.factor); + return result; + } +} diff --git a/packages/cx/src/charts/axis/NumericAxis.d.ts b/packages/cx/src/charts/axis/NumericAxis.d.ts deleted file mode 100644 index b5ba9a690..000000000 --- a/packages/cx/src/charts/axis/NumericAxis.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Cx from "../../core"; -import { AxisProps } from "./Axis"; - -interface NumericAxisProps extends AxisProps { - /** Minimum value. */ - min?: Cx.NumberProp; - - /** Maximum value. */ - max?: Cx.NumberProp; - - /** Set to `true` to normalize the input range. */ - normalized?: Cx.BooleanProp; - - /** Number used to divide values before rendering axis labels. Default value is `1`. */ - labelDivisor?: Cx.NumberProp; - - /** Base CSS class to be applied to the element. Defaults to `numericaxis`. */ - baseClass?: string; - - tickDivisions?: Array; - - /** A number ranged between `0-2`. `0` means that the range is aligned with the lowest ticks. Default value is `1`, which means that the range is aligned with medium ticks. Use value `2` to align with major ticks. */ - snapToTicks?: 0 | 1 | 2; - - /** Value format. Default is `n`. */ - format?: Cx.StringProp; - - /** Size of a zone reserved for labels for both lower and upper end of the axis. */ - deadZone?: Cx.NumberProp; - - /** Size of a zone reserved for labels near the upper (higher) end of the axis. */ - upperDeadZone?: Cx.NumberProp; - - /** Size of a zone reserved for labels near the lower end of the axis. */ - lowerDeadZone?: Cx.NumberProp; - - /** Specifies minimum value increment between labels. Useful when formatting is not flexible enough, i.e. set to 1 for integer axes to avoid duplicate labels. */ - minLabelTickSize?: number; -} - -export class NumericAxis extends Cx.Widget { - static XY(): { - x: { type: NumericAxis }; - y: { type: NumericAxis; vertical: true }; - }; -} diff --git a/packages/cx/src/charts/axis/NumericAxis.js b/packages/cx/src/charts/axis/NumericAxis.js deleted file mode 100644 index 089f8d029..000000000 --- a/packages/cx/src/charts/axis/NumericAxis.js +++ /dev/null @@ -1,351 +0,0 @@ -import { Axis } from "./Axis"; -import { VDOM } from "../../ui/Widget"; -import { Stack } from "./Stack"; -import { Format } from "../../util/Format"; -import { isNumber } from "../../util/isNumber"; - -export class NumericAxis extends Axis { - init() { - if (this.deadZone) { - this.lowerDeadZone = this.deadZone; - this.upperDeadZone = this.deadZone; - } - super.init(); - } - - declareData() { - super.declareData(...arguments, { - min: undefined, - max: undefined, - normalized: undefined, - inverted: undefined, - labelDivisor: undefined, - format: undefined, - lowerDeadZone: undefined, - upperDeadZone: undefined, - }); - } - - initInstance(context, instance) { - instance.calculator = new NumericScale(); - } - - explore(context, instance) { - super.explore(context, instance); - let { min, max, normalized, inverted, lowerDeadZone, upperDeadZone } = instance.data; - instance.calculator.reset( - min, - max, - this.snapToTicks, - this.tickDivisions, - this.minTickDistance, - this.minTickStep, - this.minLabelDistance, - this.minLabelTickSize, - normalized, - inverted, - lowerDeadZone, - upperDeadZone, - ); - } - - render(context, instance, key) { - let { data } = instance; - - if (!data.bounds.valid()) return null; - - let baseFormatter = Format.parse(data.format); - let formatter = data.labelDivisor != 1 ? (v) => baseFormatter(v / data.labelDivisor) : baseFormatter; - - return ( - - {this.renderTicksAndLabels(context, instance, formatter, this.minLabelDistance)} - - ); - } - - static XY() { - return { - x: { type: NumericAxis }, - y: { type: NumericAxis, vertical: true }, - }; - } -} - -NumericAxis.prototype.baseClass = "numericaxis"; -NumericAxis.prototype.tickDivisions = [ - [1, 2, 10, 20, 100], - [1, 5, 10, 20, 100], -]; - -NumericAxis.prototype.snapToTicks = 1; -NumericAxis.prototype.normalized = false; -NumericAxis.prototype.format = "n"; -NumericAxis.prototype.labelDivisor = 1; -NumericAxis.prototype.minLabelTickSize = 0; -NumericAxis.prototype.minTickStep = 0; - -Axis.alias("numeric", NumericAxis); - -class NumericScale { - reset( - min, - max, - snapToTicks, - tickDivisions, - minTickDistance, - minTickStep, - minLabelDistance, - minLabelTickSize, - normalized, - inverted, - lowerDeadZone, - upperDeadZone, - ) { - this.min = min; - this.max = max; - this.snapToTicks = snapToTicks; - this.tickDivisions = tickDivisions; - this.minLabelDistance = minLabelDistance; - this.minLabelTickSize = minLabelTickSize; - this.minTickDistance = minTickDistance; - this.minTickStep = minTickStep; - this.tickSizes = []; - this.normalized = normalized; - this.inverted = inverted; - delete this.minValue; - delete this.maxValue; - this.stacks = {}; - this.lowerDeadZone = lowerDeadZone || 0; - this.upperDeadZone = upperDeadZone || 0; - } - - map(v, offset = 0) { - return this.origin + (v + offset - this.scale.min + this.scale.minPadding) * this.scale.factor; - } - - decodeValue(n) { - return n; - } - - encodeValue(v) { - return v; - } - - constrainValue(v) { - return Math.max(this.scale.min, Math.min(this.scale.max, v)); - } - - trackValue(v, offset = 0, constrain = false) { - let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding; - if (constrain) value = this.constrainValue(v); - return value; - } - - hash() { - let r = { - origin: this.origin, - factor: this.scale.factor, - min: this.scale.min, - max: this.scale.max, - minPadding: this.scale.minPadding, - maxPadding: this.scale.maxPadding, - }; - r.stacks = Object.keys(this.stacks) - .map((s) => this.stacks[s].info?.join(",")) - .join(":"); - return r; - } - - isSame(x) { - let hash = this.hash(); - let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]); - this.shouldUpdate = !same; - return same; - } - - measure(a, b) { - this.a = a; - this.b = b; - - if (this.minValue != null && this.min == null) this.min = this.minValue; - if (this.maxValue != null && this.max == null) this.max = this.maxValue; - - for (let s in this.stacks) { - let info = this.stacks[s].measure(this.normalized); - let [min, max, invalid] = info; - if (this.min == null || min < this.min) this.min = min; - if (this.max == null || max > this.max) this.max = max; - this.stacks[s].info = info; - } - - if (this.min == null) this.min = 0; - if (this.max == null) this.max = this.normalized ? 1 : 100; - - if (this.min == this.max) { - if (this.min == 0) { - this.min = -1; - this.max = 1; - } else { - let delta = Math.abs(this.min) * 0.1; - this.min -= delta; - this.max += delta; - } - } - - this.origin = this.inverted ? this.b : this.a; - - this.scale = this.getScale(); - - this.calculateTicks(); - } - - getScale(tickSizes) { - let { min, max } = this; - let smin = min; - let smax = max; - - let tickSize; - if (tickSizes && isNumber(this.snapToTicks) && tickSizes.length > 0) { - tickSize = tickSizes[Math.min(tickSizes.length - 1, this.snapToTicks)]; - smin = Math.floor(smin / tickSize) * tickSize; - smax = Math.ceil(smax / tickSize) * tickSize; - } else { - if (this.minValue === min) smin = this.minValuePadded; - if (this.maxValue === max) smax = this.maxValuePadded; - } - - let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0; - let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0; - - let sign = this.b > this.a ? 1 : -1; - - let factor = - smin < smax - ? (Math.abs(this.b - this.a) - this.lowerDeadZone - this.upperDeadZone) / - (smax - smin + minPadding + maxPadding) - : 0; - - if (factor < 0) factor = 0; - - if (factor > 0 && (this.lowerDeadZone > 0 || this.upperDeadZone > 0)) { - while (factor * (min - smin) < this.lowerDeadZone) smin -= this.lowerDeadZone / factor; - - while (factor * (smax - max) < this.upperDeadZone) smax += this.upperDeadZone / factor; - - if (tickSize > 0 && isNumber(this.snapToTicks)) { - smin = Math.floor(smin / tickSize) * tickSize; - smax = Math.ceil(smax / tickSize) * tickSize; - minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0; - maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0; - } - - factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; - } - - return { - factor: sign * (this.inverted ? -factor : factor), - min: smin, - max: smax, - minPadding, - maxPadding, - }; - } - - acknowledge(value, width = 0, offset = 0) { - if (value == null) return; - - if (this.minValue == null || value < this.minValue) { - this.minValue = value; - this.minValuePadded = value + offset - width / 2; - } - if (this.maxValue == null || value > this.maxValue) { - this.maxValue = value; - this.maxValuePadded = value + offset + width / 2; - } - } - - getStack(name) { - let s = this.stacks[name]; - if (!s) s = this.stacks[name] = new Stack(); - return s; - } - - stacknowledge(name, ordinal, value) { - return this.getStack(name).acknowledge(ordinal, value); - } - - stack(name, ordinal, value) { - let v = this.getStack(name).stack(ordinal, value); - return v != null ? this.map(v) : null; - } - - findTickSize(minPxDist) { - return this.tickSizes.find((a) => a >= this.minLabelTickSize && a * Math.abs(this.scale.factor) >= minPxDist); - } - - getTickSizes() { - return this.tickSizes; - } - - calculateTicks() { - let dist = this.minLabelDistance / Math.abs(this.scale.factor); - let unit = Math.pow(10, Math.floor(Math.log10(dist))); - - let bestLabelDistance = Infinity; - let bestTicks = []; - let bestScale = this.scale; - - for (let i = 0; i < this.tickDivisions.length; i++) { - let divs = this.tickDivisions[i]; - let tickSizes = divs.filter((ts) => ts >= this.minTickStep).map((ts) => ts * unit); - let scale = this.getScale(tickSizes); - tickSizes.forEach((size, level) => { - let labelDistance = size * Math.abs(scale.factor); - if (labelDistance >= this.minLabelDistance && labelDistance < bestLabelDistance) { - bestScale = scale; - bestTicks = tickSizes; - bestLabelDistance = labelDistance; - } - }); - } - this.scale = bestScale; - this.tickSizes = bestTicks.filter( - (ts) => ts >= this.minTickStep && ts * Math.abs(bestScale.factor) >= this.minTickDistance, - ); - if (this.tickSizes.length > 0) { - let max = this.tickSizes[this.tickSizes.length - 1]; - this.tickSizes.push(2 * max); - this.tickSizes.push(5 * max); - this.tickSizes.push(10 * max); - let min = this.tickSizes[0]; - let dist = min * Math.abs(bestScale.factor) >= this.minTickDistance; - if (min / 10 >= this.minTickStep && dist / 10 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 10); - else if (min / 5 >= this.minTickStep && dist / 5 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 5); - else if (min / 2 >= this.minTickStep && dist / 2 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 2); - } - } - - getTicks(tickSizes) { - return tickSizes.map((size) => { - let start = Math.ceil((this.scale.min - this.scale.minPadding) / size); - let end = Math.floor((this.scale.max + this.scale.maxPadding) / size); - let result = []; - for (let i = start; i <= end; i++) result.push(i * size + 0); - return result; - }); - } - - mapGridlines() { - let size = this.tickSizes[0]; - let start = Math.ceil((this.scale.min - this.scale.minPadding) / size); - let end = Math.floor((this.scale.max + this.scale.maxPadding) / size); - let result = []; - for (let i = start; i <= end; i++) result.push(this.map(i * size)); - return result; - } - - book() { - Console.warn("NumericAxis does not support the autoSize flag for column and bar graphs."); - } -} diff --git a/packages/cx/src/charts/axis/NumericAxis.scss b/packages/cx/src/charts/axis/NumericAxis.scss index 30760c180..9dc0d19cc 100644 --- a/packages/cx/src/charts/axis/NumericAxis.scss +++ b/packages/cx/src/charts/axis/NumericAxis.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-numericaxis( $name: 'numericaxis', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-axis(); diff --git a/packages/cx/src/charts/axis/NumericAxis.tsx b/packages/cx/src/charts/axis/NumericAxis.tsx new file mode 100644 index 000000000..bdc541c31 --- /dev/null +++ b/packages/cx/src/charts/axis/NumericAxis.tsx @@ -0,0 +1,437 @@ +/** @jsxImportSource react */ + +import { Axis, AxisConfig, AxisInstance } from "./Axis"; +import { VDOM } from "../../ui/Widget"; +import { Stack } from "./Stack"; +import { Format } from "../../util/Format"; +import { isNumber } from "../../util/isNumber"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { NumberProp, BooleanProp, StringProp } from "../../ui/Prop"; +import { Console } from "../../util/Console"; + +export interface NumericAxisConfig extends AxisConfig { + /** Minimum value. */ + min?: NumberProp; + + /** Maximum value. */ + max?: NumberProp; + + /** Set to `true` to normalize the input range. */ + normalized?: BooleanProp; + + /** Number used to divide values before rendering axis labels. Default value is `1`. */ + labelDivisor?: NumberProp; + + /** Base CSS class to be applied to the element. Defaults to `numericaxis`. */ + baseClass?: string; + + tickDivisions?: Array; + + /** A number ranged between `0-2`. `0` means that the range is aligned with the lowest ticks. Default value is `1`, which means that the range is aligned with medium ticks. Use value `2` to align with major ticks. */ + snapToTicks?: 0 | 1 | 2; + + /** Value format. Default is `n`. */ + format?: StringProp; + + /** Size of a zone reserved for labels for both lower and upper end of the axis. */ + deadZone?: NumberProp; + + /** Size of a zone reserved for labels near the upper (higher) end of the axis. */ + upperDeadZone?: NumberProp; + + /** Size of a zone reserved for labels near the lower end of the axis. */ + lowerDeadZone?: NumberProp; + + /** Specifies minimum value increment between labels. Useful when formatting is not flexible enough, i.e. set to 1 for integer axes to avoid duplicate labels. */ + minLabelTickSize?: number; + + minTickStep?: number; +} + +export class NumericAxis extends Axis { + declare deadZone: number; + declare lowerDeadZone: number; + declare upperDeadZone: number; + declare snapToTicks: number; + declare tickDivisions: number[][]; + declare minLabelTickSize: number; + declare minTickStep: number; + declare format: string; + declare labelDivisor: number; + declare normalized: boolean; + + constructor(config: NumericAxisConfig) { + super(config); + } + + init(): void { + if (this.deadZone) { + this.lowerDeadZone = this.deadZone; + this.upperDeadZone = this.deadZone; + } + super.init(); + } + + declareData(...args: any[]): void { + super.declareData( + { + min: undefined, + max: undefined, + normalized: undefined, + inverted: undefined, + labelDivisor: undefined, + format: undefined, + lowerDeadZone: undefined, + upperDeadZone: undefined, + }, + ...args, + ); + } + + initInstance(context: RenderingContext, instance: AxisInstance): void { + instance.calculator = new NumericScale(); + } + + explore(context: RenderingContext, instance: AxisInstance): void { + super.explore(context, instance); + let { min, max, normalized, inverted, lowerDeadZone, upperDeadZone } = instance.data; + instance.calculator.reset( + min, + max, + this.snapToTicks, + this.tickDivisions, + this.minTickDistance, + this.minTickStep, + this.minLabelDistance, + this.minLabelTickSize, + normalized, + inverted, + lowerDeadZone, + upperDeadZone, + ); + } + + render(context: RenderingContext, instance: AxisInstance, key: string): React.ReactNode { + let { data } = instance; + + if (!data.bounds.valid()) return null; + + let baseFormatter = Format.parse(data.format); + let formatter = data.labelDivisor != 1 ? (v: number) => baseFormatter(v / data.labelDivisor) : baseFormatter; + + return ( + + {this.renderTicksAndLabels(context, instance, formatter, this.minLabelDistance)} + + ); + } + + static XY() { + return { + x: { type: NumericAxis }, + y: { type: NumericAxis, vertical: true }, + }; + } +} + +NumericAxis.prototype.baseClass = "numericaxis"; +NumericAxis.prototype.tickDivisions = [ + [1, 2, 10, 20, 100], + [1, 5, 10, 20, 100], +]; + +NumericAxis.prototype.snapToTicks = 1; +NumericAxis.prototype.normalized = false; +NumericAxis.prototype.format = "n"; +NumericAxis.prototype.labelDivisor = 1; +NumericAxis.prototype.minLabelTickSize = 0; +NumericAxis.prototype.minTickStep = 0; + +Axis.alias("numeric", NumericAxis); + +class NumericScale { + min: number; + max: number; + snapToTicks: number; + tickDivisions: number[][]; + minLabelDistance: number; + minLabelTickSize: number; + minTickDistance: number; + minTickStep: number; + tickSizes: number[]; + normalized: boolean; + inverted: boolean; + minValue?: number; + maxValue?: number; + minValuePadded: number; + maxValuePadded: number; + stacks: Record; + lowerDeadZone: number; + upperDeadZone: number; + origin: number; + scale: { factor: number; min: number; max: number; minPadding: number; maxPadding: number }; + a: number; + b: number; + shouldUpdate: boolean; + + reset( + min: number, + max: number, + snapToTicks: number, + tickDivisions: number[][], + minTickDistance: number, + minTickStep: number, + minLabelDistance: number, + minLabelTickSize: number, + normalized: boolean, + inverted: boolean, + lowerDeadZone: number, + upperDeadZone: number, + ): void { + this.min = min; + this.max = max; + this.snapToTicks = snapToTicks; + this.tickDivisions = tickDivisions; + this.minLabelDistance = minLabelDistance; + this.minLabelTickSize = minLabelTickSize; + this.minTickDistance = minTickDistance; + this.minTickStep = minTickStep; + this.tickSizes = []; + this.normalized = normalized; + this.inverted = inverted; + delete this.minValue; + delete this.maxValue; + this.stacks = {}; + this.lowerDeadZone = lowerDeadZone || 0; + this.upperDeadZone = upperDeadZone || 0; + } + + map(v: number, offset: number = 0): number { + return this.origin + (v + offset - this.scale.min + this.scale.minPadding) * this.scale.factor; + } + + decodeValue(n: number): number { + return n; + } + + encodeValue(v: number): number { + return v; + } + + constrainValue(v: number): number { + return Math.max(this.scale.min, Math.min(this.scale.max, v)); + } + + trackValue(v: number, offset: number = 0, constrain: boolean = false): number { + let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding; + if (constrain) value = this.constrainValue(v); + return value; + } + + hash(): any { + let r: any = { + origin: this.origin, + factor: this.scale.factor, + min: this.scale.min, + max: this.scale.max, + minPadding: this.scale.minPadding, + maxPadding: this.scale.maxPadding, + }; + r.stacks = Object.keys(this.stacks) + .map((s) => this.stacks[s].info?.join(",")) + .join(":"); + return r; + } + + isSame(x: any): boolean { + let hash = this.hash(); + let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]); + this.shouldUpdate = !same; + return same; + } + + measure(a: number, b: number): void { + this.a = a; + this.b = b; + + if (this.minValue != null && this.min == null) this.min = this.minValue; + if (this.maxValue != null && this.max == null) this.max = this.maxValue; + + for (let s in this.stacks) { + let info = this.stacks[s].measure(this.normalized); + let [min, max] = info; + if (this.min == null || min < this.min) this.min = min; + if (this.max == null || max > this.max) this.max = max; + this.stacks[s].info = info; + } + + if (this.min == null) this.min = 0; + if (this.max == null) this.max = this.normalized ? 1 : 100; + + if (this.min == this.max) { + if (this.min == 0) { + this.min = -1; + this.max = 1; + } else { + let delta = Math.abs(this.min) * 0.1; + this.min -= delta; + this.max += delta; + } + } + + this.origin = this.inverted ? this.b : this.a; + + this.scale = this.getScale(); + + this.calculateTicks(); + } + + getScale(tickSizes?: number[]): { factor: number; min: number; max: number; minPadding: number; maxPadding: number } { + let { min, max } = this; + let smin = min; + let smax = max; + + let tickSize; + if (tickSizes && isNumber(this.snapToTicks) && tickSizes.length > 0) { + tickSize = tickSizes[Math.min(tickSizes.length - 1, this.snapToTicks)]; + smin = Math.floor(smin / tickSize) * tickSize; + smax = Math.ceil(smax / tickSize) * tickSize; + } else { + if (this.minValue === min) smin = this.minValuePadded; + if (this.maxValue === max) smax = this.maxValuePadded; + } + + let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0; + let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0; + + let sign = this.b > this.a ? 1 : -1; + + let factor = + smin < smax + ? (Math.abs(this.b - this.a) - this.lowerDeadZone - this.upperDeadZone) / + (smax - smin + minPadding + maxPadding) + : 0; + + if (factor < 0) factor = 0; + + if (factor > 0 && (this.lowerDeadZone > 0 || this.upperDeadZone > 0)) { + while (factor * (min - smin) < this.lowerDeadZone) smin -= this.lowerDeadZone / factor; + + while (factor * (smax - max) < this.upperDeadZone) smax += this.upperDeadZone / factor; + + if (tickSize! > 0 && isNumber(this.snapToTicks)) { + smin = Math.floor(smin / tickSize!) * tickSize!; + smax = Math.ceil(smax / tickSize!) * tickSize!; + minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0; + maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0; + } + + factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; + } + + return { + factor: sign * (this.inverted ? -factor : factor), + min: smin, + max: smax, + minPadding, + maxPadding, + }; + } + + acknowledge(value: number, width: number = 0, offset: number = 0): void { + if (value == null) return; + + if (this.minValue == null || value < this.minValue) { + this.minValue = value; + this.minValuePadded = value + offset - width / 2; + } + if (this.maxValue == null || value > this.maxValue) { + this.maxValue = value; + this.maxValuePadded = value + offset + width / 2; + } + } + + getStack(name: string): Stack { + let s = this.stacks[name]; + if (!s) s = this.stacks[name] = new Stack(); + return s; + } + + stacknowledge(name: string, ordinal: any, value: any): any { + return this.getStack(name).acknowledge(ordinal, value); + } + + stack(name: string, ordinal: any, value: any): number | null { + let v = this.getStack(name).stack(ordinal, value); + return v != null ? this.map(v) : null; + } + + findTickSize(minPxDist: number): number | undefined { + return this.tickSizes.find((a) => a >= this.minLabelTickSize && a * Math.abs(this.scale.factor) >= minPxDist); + } + + getTickSizes(): number[] { + return this.tickSizes; + } + + calculateTicks(): void { + let dist = this.minLabelDistance / Math.abs(this.scale.factor); + let unit = Math.pow(10, Math.floor(Math.log10(dist))); + + let bestLabelDistance = Infinity; + let bestTicks: number[] = []; + let bestScale = this.scale; + + for (let i = 0; i < this.tickDivisions.length; i++) { + let divs = this.tickDivisions[i]; + let tickSizes = divs.filter((ts) => ts >= this.minTickStep).map((ts) => ts * unit); + let scale = this.getScale(tickSizes); + tickSizes.forEach((size, level) => { + let labelDistance = size * Math.abs(scale.factor); + if (labelDistance >= this.minLabelDistance && labelDistance < bestLabelDistance) { + bestScale = scale; + bestTicks = tickSizes; + bestLabelDistance = labelDistance; + } + }); + } + this.scale = bestScale; + this.tickSizes = bestTicks.filter( + (ts) => ts >= this.minTickStep && ts * Math.abs(bestScale.factor) >= this.minTickDistance, + ); + if (this.tickSizes.length > 0) { + let max = this.tickSizes[this.tickSizes.length - 1]; + this.tickSizes.push(2 * max); + this.tickSizes.push(5 * max); + this.tickSizes.push(10 * max); + let min = this.tickSizes[0]; + let minDist = min * Math.abs(bestScale.factor); + if (min / 10 >= this.minTickStep && minDist / 10 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 10); + else if (min / 5 >= this.minTickStep && minDist / 5 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 5); + else if (min / 2 >= this.minTickStep && minDist / 2 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 2); + } + } + + getTicks(tickSizes: number[]): number[][] { + return tickSizes.map((size) => { + let start = Math.ceil((this.scale.min - this.scale.minPadding) / size); + let end = Math.floor((this.scale.max + this.scale.maxPadding) / size); + let result: number[] = []; + for (let i = start; i <= end; i++) result.push(i * size + 0); + return result; + }); + } + + mapGridlines(): number[] { + let size = this.tickSizes[0]; + let start = Math.ceil((this.scale.min - this.scale.minPadding) / size); + let end = Math.floor((this.scale.max + this.scale.maxPadding) / size); + let result: number[] = []; + for (let i = start; i <= end; i++) result.push(this.map(i * size)); + return result; + } + + book(): void { + Console.warn("NumericAxis does not support the autoSize flag for column and bar graphs."); + } +} diff --git a/packages/cx/src/charts/axis/Stack.d.ts b/packages/cx/src/charts/axis/Stack.d.ts deleted file mode 100644 index 936c89ad4..000000000 --- a/packages/cx/src/charts/axis/Stack.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class Stack { - - reset() : void; - - acknowledge(ordinal: string, value?: number) : void; - - measure(normalized: boolean) : [number, number]; - - stack(ordinal: string, value?: number) : number | null; - -} \ No newline at end of file diff --git a/packages/cx/src/charts/axis/Stack.js b/packages/cx/src/charts/axis/Stack.js deleted file mode 100644 index e730893ec..000000000 --- a/packages/cx/src/charts/axis/Stack.js +++ /dev/null @@ -1,55 +0,0 @@ -export class Stack { - constructor() { - this.reset(); - } - - reset() { - this.stacks = {}; - this.values = {}; - this.normalized = false; - this.invalid = {}; - } - - acknowledge(ordinal, value) { - if (value != null) { - let s = this.stacks[ordinal]; - if (!s) this.stacks[ordinal] = s = { total: 0, min: 0, max: 0 }; - s.total += value; - if (s.total < s.min) s.min = s.total; - if (s.total > s.max) s.max = s.total; - } else { - this.invalid[ordinal] = true; - } - } - - measure(normalized) { - if (normalized) { - this.normalized = true; - return [0, 1]; - } - - let max = 0, - min = 0; - for (let key in this.stacks) { - if (this.stacks[key].max > max) max = this.stacks[key].max; - if (this.stacks[key].min < min) min = this.stacks[key].min; - } - return [min, max]; - } - - stack(ordinal, value) { - if (value == null || this.invalid[ordinal]) return null; - - let base = this.values[ordinal] || 0; - - let result = (this.values[ordinal] = base + value); - - if (!this.normalized) return result; - - let total = this.stacks[ordinal].total; - - if (total > 0) return result / total; - - return null; - } -} diff --git a/packages/cx/src/charts/axis/Stack.ts b/packages/cx/src/charts/axis/Stack.ts new file mode 100644 index 000000000..6eac2b4d4 --- /dev/null +++ b/packages/cx/src/charts/axis/Stack.ts @@ -0,0 +1,61 @@ +export class Stack { + stacks: Record; + values: Record; + normalized: boolean; + invalid: Record; + info?: any[]; + + constructor() { + this.reset(); + } + + reset(): void { + this.stacks = {}; + this.values = {}; + this.normalized = false; + this.invalid = {}; + } + + acknowledge(ordinal: string, value: number | null): void { + if (value != null) { + let s = this.stacks[ordinal]; + if (!s) this.stacks[ordinal] = s = { total: 0, min: 0, max: 0 }; + s.total += value; + if (s.total < s.min) s.min = s.total; + if (s.total > s.max) s.max = s.total; + } else { + this.invalid[ordinal] = true; + } + } + + measure(normalized: boolean): [number, number] { + if (normalized) { + this.normalized = true; + return [0, 1]; + } + + let max = 0, + min = 0; + for (let key in this.stacks) { + if (this.stacks[key].max > max) max = this.stacks[key].max; + if (this.stacks[key].min < min) min = this.stacks[key].min; + } + return [min, max]; + } + + stack(ordinal: string, value: number | null): number | null { + if (value == null || this.invalid[ordinal]) return null; + + let base = this.values[ordinal] || 0; + + let result = (this.values[ordinal] = base + value); + + if (!this.normalized) return result; + + let total = this.stacks[ordinal].total; + + if (total > 0) return result / total; + + return null; + } +} diff --git a/packages/cx/src/charts/axis/TimeAxis.d.ts b/packages/cx/src/charts/axis/TimeAxis.d.ts deleted file mode 100644 index cf4566449..000000000 --- a/packages/cx/src/charts/axis/TimeAxis.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Cx from "../../core"; -import { AxisProps } from "./Axis"; - -interface TimeAxisProps extends AxisProps { - /** Minimum value. */ - min?: Cx.NumberProp; - - /** Maximum value. */ - max?: Cx.NumberProp; - - /** Base CSS class to be applied to the element. Defaults to `timeaxis`. */ - baseClass?: string; - - /** A number ranged between `0-2`. `0` means that the range is aligned with the lowest ticks. Default value is `1`, which means that the range is aligned with medium ticks. Use value `2` to align with major ticks. */ - snapToTicks?: 0 | 1 | 2 | false; - - tickDivisions?: { [prop: string]: Array }; - minLabelDistance?: number; - minTickUnit?: string; - - /** Set to true to apply precise label distances from minLabelDistanceFormatOverride based on the resolved label format. */ - useLabelDistanceFormatOverrides?: boolean; - - /** Mapping of formats to label distances, i.e. { "datetime;YYYYMM": 80 } */ - minLabelDistanceFormatOverride?: Record; - - /** Axis labels format string override. */ - format?: string; -} - -export class TimeAxis extends Cx.Widget {} diff --git a/packages/cx/src/charts/axis/TimeAxis.js b/packages/cx/src/charts/axis/TimeAxis.js deleted file mode 100644 index ee7927aa8..000000000 --- a/packages/cx/src/charts/axis/TimeAxis.js +++ /dev/null @@ -1,611 +0,0 @@ -import { Axis } from "./Axis"; -import { VDOM } from "../../ui/Widget"; -import { Stack } from "./Stack"; -import { Format } from "../../ui/Format"; -import { isNumber } from "../../util/isNumber"; -import { zeroTime } from "../../util/date/zeroTime"; -import { Console } from "../../util/Console"; -import { parseDateInvariant } from "../../util"; - -Format.registerFactory("yearOrMonth", (format) => { - let year = Format.parse("datetime;yyyy"); - let month = Format.parse("datetime;MMM"); - return function (date) { - let d = parseDateInvariant(date); - if (d.getMonth() == 0) return year(d); - else return month(d); - }; -}); - -Format.registerFactory("monthOrDay", (format) => { - let month = Format.parse("datetime;MMM"); - let day = Format.parse("datetime;dd"); - return function (date) { - let d = parseDateInvariant(date); - if (d.getDate() == 1) return month(d); - else return day(d); - }; -}); - -export class TimeAxis extends Axis { - init() { - if (this.labelAnchor == "auto") this.labelAnchor = this.vertical ? (this.secondary ? "start" : "end") : "start"; - - if (this.labelDx == "auto") this.labelDx = this.vertical ? 0 : "5px"; - - if (this.deadZone) { - this.lowerDeadZone = this.deadZone; - this.upperDeadZone = this.deadZone; - } - - this.minLabelDistanceFormatOverride = { - ...this.minLabelDistanceFormatOverrideDefaults, - ...this.minLabelDistanceFormatOverride, - }; - - super.init(); - } - - declareData() { - super.declareData(...arguments, { - anchors: undefined, - min: undefined, - max: undefined, - inverted: undefined, - lowerDeadZone: undefined, - upperDeadZone: undefined, - }); - } - - initInstance(context, instance) { - instance.calculator = new TimeScale(); - } - - explore(context, instance) { - super.explore(context, instance); - let { min, max, normalized, inverted, lowerDeadZone, upperDeadZone } = instance.data; - instance.calculator.reset( - min, - max, - this.snapToTicks, - this.tickDivisions, - Math.max(1, this.minTickDistance), - Math.max(1, this.minLabelDistance), - normalized, - inverted, - this.minTickUnit, - lowerDeadZone, - upperDeadZone, - this.decode, - this.useLabelDistanceFormatOverrides ? this.minLabelDistanceFormatOverride : {}, - this.format, - ); - } - - render(context, instance, key) { - let { data, cached, calculator } = instance; - - cached.axis = calculator.hash(); - - if (!data.bounds.valid()) return null; - - let format = calculator.resolvedFormat; - let minLabelDistance = calculator.resolvedMinLabelDistance; - let formatter = Format.parse(format); - - return ( - - {this.renderTicksAndLabels(context, instance, formatter, minLabelDistance)} - - ); - } -} - -Axis.alias("time", TimeAxis); - -TimeAxis.prototype.baseClass = "timeaxis"; -TimeAxis.prototype.tickDivisions = { - second: [[1, 5, 15, 30]], - minute: [[1, 5, 15, 30]], - hour: [ - [1, 2, 4, 8], - [1, 3, 6, 12], - ], - day: [[1]], - week: [[1]], - month: [[1, 3, 6]], - year: [ - [1, 2, 10], - [1, 5, 10], - [5, 10, 50], - [10, 50, 100], - ], -}; - -const TimeFormats = { - fullDateAndTime: "datetime;yyyy MMM dd HH mm ss n", - shortMonthDate: "datetime;yyyy MMM dd", -}; - -TimeAxis.prototype.snapToTicks = 0; -TimeAxis.prototype.tickSize = 15; -TimeAxis.prototype.minLabelDistance = 60; -TimeAxis.prototype.minTickDistance = 60; -TimeAxis.prototype.minTickUnit = "second"; -TimeAxis.prototype.useLabelDistanceFormatOverrides = false; -TimeAxis.prototype.minLabelDistanceFormatOverrideDefaults = { - [TimeFormats.fullDateAndTime]: 150, - [TimeFormats.shortMonthDate]: 90, -}; - -function monthNumber(date) { - return date.getFullYear() * 12 + date.getMonth() + (date.getDate() - 1) / 31; -} - -function yearNumber(date) { - return monthNumber(date) / 12; -} - -const milliSeconds = { - second: 1000, - minute: 60 * 1000, - hour: 3600 * 1000, - day: 3600 * 24 * 1000, - week: 3600 * 24 * 7 * 1000, - month: 3600 * 24 * 30 * 1000, - year: 3600 * 24 * 365 * 1000, -}; - -class TimeScale { - reset( - min, - max, - snapToTicks, - tickDivisions, - minTickDistance, - minLabelDistance, - normalized, - inverted, - minTickUnit, - lowerDeadZone, - upperDeadZone, - decode, - minLabelDistanceFormatOverride, - format, - ) { - this.dateCache = {}; - this.min = min != null ? this.decodeValue(min) : null; - this.max = max != null ? this.decodeValue(max) : null; - this.snapToTicks = snapToTicks; - this.tickDivisions = tickDivisions; - this.minLabelDistance = minLabelDistance; - this.minTickDistance = minTickDistance; - this.tickSizes = []; - this.normalized = normalized; - this.minTickUnit = minTickUnit; - this.inverted = inverted; - this.lowerDeadZone = lowerDeadZone || 0; - this.upperDeadZone = upperDeadZone || 0; - delete this.minValue; - delete this.maxValue; - delete this.minValuePadded; - delete this.maxValuePadded; - this.stacks = {}; - this.decode = decode; - this.minLabelDistanceFormatOverride = minLabelDistanceFormatOverride; - this.format = format; - } - - decodeValue(date) { - if (date instanceof Date) return date.getTime(); - - switch (typeof date) { - case "string": - let v = this.dateCache[date]; - if (!v) { - if (this.decode) date = this.decode(date); - v = this.dateCache[date] = parseDateInvariant(date).getTime(); - } - return v; - - case "number": - return parseDateInvariant(date).getTime(); - } - } - - encodeValue(v) { - return new Date(v).toISOString(); - } - - getFormat(unit, scale) { - switch (unit) { - case "year": - return "datetime;yyyy"; - - case "month": - if (new Date(scale.min).getFullYear() != new Date(scale.max).getFullYear()) return "yearOrMonth"; - return "datetime;yyyy MMM"; - - case "week": - return "datetime;MMMdd"; - - case "day": - if ( - new Date(scale.min).getFullYear() != new Date(scale.max).getFullYear() || - new Date(scale.min).getMonth() != new Date(scale.max).getMonth() - ) - return "monthOrDay"; - - return TimeFormats.shortMonthDate; - - case "hour": - return "datetime;HH mm n"; - - case "minute": - return "datetime;HH mm n"; - - case "second": - return "datetime;mm ss"; - - default: - return TimeFormats.fullDateAndTime; - } - } - - map(v, offset = 0) { - return this.origin + (this.decodeValue(v) + offset - this.scale.min + this.scale.minPadding) * this.scale.factor; - } - - constrainValue(v) { - return Math.max(this.scale.min, Math.min(this.scale.max, v)); - } - - trackValue(v, offset = 0, constrain = false) { - let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding; - if (constrain) value = this.constrainValue(value); - return value; - } - - hash() { - let r = { - origin: this.origin, - factor: this.scale.factor, - min: this.scale.min, - max: this.scale.max, - minPadding: this.scale.minPadding, - maxPadding: this.scale.maxPadding, - }; - r.stacks = Object.keys(this.stacks) - .map((s) => this.stacks[s].info.join(",")) - .join(":"); - return r; - } - - isSame(x) { - let hash = this.hash(); - let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]); - this.shouldUpdate = !same; - return same; - } - - measure(a, b) { - this.a = a; - this.b = b; - - for (let s in this.stacks) { - let info = this.stacks[s].measure(this.normalized); - let [min, max, invalid] = info; - if (this.minValue == null || min < this.minValue) this.minValue = min; - if (this.max == null || max > this.maxValue) this.maxValue = max; - this.stacks[s].info = info; - } - - if (this.min == null) { - if (this.minValue != null) this.min = this.minValue; - else this.min = 0; - } - - if (this.max == null) { - if (this.maxValue != null) this.max = this.maxValue; - else this.max = this.normalized ? 1 : 100; - } - - this.origin = this.inverted ? this.b : this.a; - - this.calculateTicks(); - if (this.scale == null) { - this.scale = this.getScale(); - } - } - - getTimezoneOffset(date) { - return date.getTimezoneOffset() * 60 * 1000; - } - - getScale(tickSize, measure, minRange = 1000) { - let { min, max, upperDeadZone, lowerDeadZone } = this; - - let smin = min; - let smax = max; - - if (tickSize) { - let minDate = new Date(min); - let maxDate = new Date(max); - - switch (measure) { - case "second": - case "minute": - case "hour": - case "day": - default: - let minOffset = this.getTimezoneOffset(minDate); - let maxOffset = this.getTimezoneOffset(maxDate); - let mondayOffset = 4 * milliSeconds.day; //new Date(0).getDay() => 4 - smin = Math.floor((smin - minOffset - mondayOffset) / tickSize) * tickSize + minOffset + mondayOffset; - smax = Math.ceil((smax - maxOffset - mondayOffset) / tickSize) * tickSize + maxOffset + mondayOffset; - break; - - case "month": - tickSize /= milliSeconds.month; - let minMonth = monthNumber(minDate); - let maxMonth = monthNumber(maxDate); - minMonth = Math.floor(minMonth / tickSize) * tickSize; - maxMonth = Math.ceil(maxMonth / tickSize) * tickSize; - smin = new Date(Math.floor(minMonth / 12), minMonth % 12, 1).getTime(); - smax = new Date(Math.floor(maxMonth / 12), maxMonth % 12, 1).getTime(); - break; - - case "year": - tickSize /= milliSeconds.year; - let minYear = yearNumber(minDate); - let maxYear = yearNumber(maxDate); - minYear = Math.floor(minYear / tickSize) * tickSize; - maxYear = Math.ceil(maxYear / tickSize) * tickSize; - smin = new Date(minYear, 0, 1).getTime(); - smax = new Date(maxYear, 0, 1).getTime(); - break; - } - } else { - if (this.minValue == min) smin = this.minValuePadded; - if (this.maxValue == max) smax = this.maxValuePadded; - } - - if (smax - smin < minRange) { - let delta = (minRange - (smax - smin)) / 2; - smin -= delta; - smax += delta; - } - - //padding should be activated only if using min/max obtained from the data - let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0; - let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0; - - let factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; - if (factor > 0 && (upperDeadZone > 0 || lowerDeadZone > 0)) { - smin -= lowerDeadZone / factor; - smax += upperDeadZone / factor; - minPadding = this.minValuePadded != null ? Math.max(0, smin - this.minValuePadded) : 0; - maxPadding = this.maxValuePadded != null ? Math.max(0, this.maxValuePadded - smax) : 0; - factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; - } - - let sign = this.b > this.a ? 1 : -1; - - return { - factor: sign * (this.inverted ? -factor : factor), - min: smin, - max: smax, - minPadding, - maxPadding, - }; - } - - acknowledge(value, width = 0, offset = 0) { - value = this.decodeValue(value); - if (this.minValue == null || value + offset - width / 2 < this.minValuePadded) { - this.minValue = value; - this.minValuePadded = value + offset - width / 2; - } - if (this.maxValue == null || value + offset + width / 2 > this.maxValuePadded) { - this.maxValue = value; - this.maxValuePadded = value + offset + width / 2; - } - } - - getStack(name) { - let s = this.stacks[name]; - if (!s) s = this.stacks[name] = new Stack(); - return s; - } - - stacknowledge(name, ordinal, value) { - return this.getStack(name).acknowledge(ordinal, value); - } - - stack(name, ordinal, value) { - let v = this.getStack(name).stack(ordinal, value); - return v != null ? this.map(v) : null; - } - - findTickSize(minPxDist) { - return this.tickSizes.find(({ size, noLabels }) => !noLabels && size * Math.abs(this.scale.factor) >= minPxDist); - } - - getTickSizes() { - return this.tickSizes; - } - - calculateTicks() { - let minReached = false; - - let minRange = 1000; - - for (let unit in milliSeconds) { - if (!minReached) { - if (unit == this.minTickUnit) minReached = true; - else continue; - } - - let unitSize = milliSeconds[unit]; - let divisions = this.tickDivisions[unit]; - - if (this.tickSizes.length > 0) { - //add ticks from higher levels - this.tickSizes.push(...divisions[0].map((s) => ({ size: s * unitSize, measure: unit }))); - break; - } - - let bestLabelDistance = Infinity; - let bestMinLabelDistance = this.minLabelDistance; - let bestTicks = []; - let bestScale = null; - let bestFormat = null; - - this.tickMeasure = unit; - - for (let i = 0; i < divisions.length; i++) { - let divs = divisions[i]; - for (let d = 0; d < divs.length; d++) { - //if (useSnapToTicks && d < Math.min(divs.length - 1, this.snapToTicks)) continue; - let tickSize = divs[d] * unitSize; - let scale = this.getScale(null, unit, tickSize); - let format = this.format ?? this.getFormat(unit, scale); - let minLabelDistance = this.minLabelDistanceFormatOverride[format] ?? this.minLabelDistance; - let labelDistance = tickSize * Math.abs(scale.factor); - if (labelDistance >= minLabelDistance && labelDistance < bestLabelDistance) { - bestScale = scale; - bestTicks = divs.map((s) => s * unitSize); - bestLabelDistance = labelDistance; - bestFormat = format; - bestMinLabelDistance = minLabelDistance; - minRange = tickSize; - } - } - } - - this.scale = bestScale; - this.tickSizes = bestTicks - .filter((ts) => ts * Math.abs(bestScale.factor) >= this.minTickDistance) - .map((size) => ({ size, measure: this.tickMeasure })); - this.resolvedFormat = bestFormat; - this.resolvedMinLabelDistance = bestMinLabelDistance; - } - - let lowerTickUnit = null; - switch (this.tickMeasure) { - case "year": - lowerTickUnit = "month"; - break; - case "month": - lowerTickUnit = "day"; - break; - case "week": - lowerTickUnit = "day"; - break; - case "day": - lowerTickUnit = "hour"; - break; - case "hour": - lowerTickUnit = "minute"; - break; - case "minute": - lowerTickUnit = "second"; - break; - } - - if (lowerTickUnit && this.minTickUnit && milliSeconds[lowerTickUnit] < milliSeconds[this.minTickUnit]) - lowerTickUnit = this.minTickUnit == this.tickMeasure ? null : this.minTickUnit; - - if (lowerTickUnit != null && this.scale) { - let bestMinorTickSize = Infinity; - let divisions = this.tickDivisions[lowerTickUnit]; - let unitSize = milliSeconds[lowerTickUnit]; - for (let i = 0; i < divisions.length; i++) { - let divs = divisions[i]; - for (let d = 0; d < divs.length; d++) { - let tickSize = divs[d] * unitSize; - if (tickSize * Math.abs(this.scale.factor) >= this.minTickDistance && tickSize < bestMinorTickSize) { - bestMinorTickSize = tickSize; - } - } - } - if (bestMinorTickSize != Infinity) { - this.tickSizes.unshift({ size: bestMinorTickSize, measure: lowerTickUnit, noLabels: true }); - if (this.tickSizes.length > 1) { - let labelStep = this.tickSizes[1].size; - let lowerScale = this.getScale(null, lowerTickUnit, minRange); - if (lowerScale.max - lowerScale.min >= labelStep) this.scale = lowerScale; - } - } - } - - if (isNumber(this.snapToTicks) && this.snapToTicks >= 0 && this.tickSizes.length > 0) { - let tickSize = this.tickSizes[Math.min(this.tickSizes.length - 1, this.snapToTicks)]; - this.scale = this.getScale(tickSize.size, tickSize.measure, minRange); - } - } - - getTicks(tickSizes) { - return tickSizes.map(({ size, measure }) => { - let result = [], - start, - end, - minDate, - maxDate; - if (measure == "year") { - size /= milliSeconds.year; - minDate = new Date(this.scale.min - this.scale.minPadding); - maxDate = new Date(this.scale.max + this.scale.maxPadding); - start = Math.ceil(yearNumber(minDate) / size) * size; - end = Math.floor(yearNumber(maxDate) / size) * size; - for (let i = start; i <= end; i += size) result.push(new Date(i, 0, 1).getTime()); - } else if (measure == "month") { - size /= milliSeconds.month; - minDate = new Date(this.scale.min - this.scale.minPadding); - maxDate = new Date(this.scale.max + this.scale.maxPadding); - start = Math.ceil(monthNumber(minDate) / size) * size; - end = Math.floor(monthNumber(maxDate) / size) * size; - for (let i = start; i <= end; i += size) result.push(new Date(Math.floor(i / 12), i % 12, 1).getTime()); - } else if (measure == "day" || measure == "week") { - let multiplier = measure == "week" ? 7 : 1; - size /= milliSeconds.day; - minDate = new Date(this.scale.min - this.scale.minPadding); - maxDate = new Date(this.scale.max + this.scale.maxPadding); - let date = zeroTime(minDate); - while (date.getTime() < minDate.getTime()) date.setDate(date.getDate() + 1); - if (measure == "week") { - //start on monday - while (date.getDay() != 1) { - date.setDate(date.getDate() + 1); - } - } - while (date.getTime() <= maxDate.getTime()) { - result.push(date); - date = new Date(date); - date.setDate(date.getDate() + multiplier); - } - } else { - let minOffset = this.getTimezoneOffset(new Date(this.scale.min - this.scale.minPadding)); - let mondayOffset = 4 * milliSeconds.day; - let date = - Math.ceil((this.scale.min - this.scale.minPadding - minOffset - mondayOffset) / size) * size + - minOffset + - mondayOffset; - while (date <= this.scale.max + this.scale.maxPadding) { - result.push(date); - date += size; - } - } - return result; - }); - } - - mapGridlines() { - if (this.tickSizes.length == 0) return []; - return this.getTicks([this.tickSizes[0]])[0].map((x) => this.map(x)); - } - - book() { - Console.warn("TimeAxis does not support the autoSize flag for column and bar graphs."); - } -} diff --git a/packages/cx/src/charts/axis/TimeAxis.scss b/packages/cx/src/charts/axis/TimeAxis.scss index b23f8a843..b43201a90 100644 --- a/packages/cx/src/charts/axis/TimeAxis.scss +++ b/packages/cx/src/charts/axis/TimeAxis.scss @@ -1,11 +1,12 @@ +@use "sass:map"; @mixin cx-timeaxis( $name: 'timeaxis', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-axis(); diff --git a/packages/cx/src/charts/axis/TimeAxis.tsx b/packages/cx/src/charts/axis/TimeAxis.tsx new file mode 100644 index 000000000..a7e394f85 --- /dev/null +++ b/packages/cx/src/charts/axis/TimeAxis.tsx @@ -0,0 +1,718 @@ +/** @jsxImportSource react */ + +import { Axis, AxisConfig, AxisInstance } from "./Axis"; +import { VDOM } from "../../ui/Widget"; +import { Stack } from "./Stack"; +import { Format } from "../../ui/Format"; +import { isNumber } from "../../util/isNumber"; +import { zeroTime } from "../../util/date/zeroTime"; +import { Console } from "../../util/Console"; +import { parseDateInvariant } from "../../util"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { NumberProp, StringProp } from "../../ui/Prop"; + +export interface TimeAxisConfig extends AxisConfig { + /** Minimum value. */ + min?: NumberProp; + + /** Maximum value. */ + max?: NumberProp; + + /** Base CSS class to be applied to the element. Defaults to `timeaxis`. */ + baseClass?: string; + + /** A number ranged between `0-2`. `0` means that the range is aligned with the lowest ticks. Default value is `1`, which means that the range is aligned with medium ticks. Use value `2` to align with major ticks. */ + snapToTicks?: 0 | 1 | 2 | false; + + tickDivisions?: Record; + minTickUnit?: string; + + /** Set to true to apply precise label distances from minLabelDistanceFormatOverride based on the resolved label format. */ + useLabelDistanceFormatOverrides?: boolean; + + /** Mapping of formats to label distances, i.e. { "datetime;YYYYMM": 80 } */ + minLabelDistanceFormatOverride?: Record; + minLabelDistanceFormatOverrideDefaults?: Record; + + /** Axis labels format string override. */ + format?: StringProp; + + /** Custom date decoder function. */ + decode?: (date: string) => string; + + /** Size of a zone reserved for labels for both lower and upper end of the axis. */ + deadZone?: number; + + /** Size of a zone reserved for labels near the lower end of the axis. */ + lowerDeadZone?: number; + + /** Size of a zone reserved for labels near the upper (higher) end of the axis. */ + upperDeadZone?: number; +} + +Format.registerFactory("yearOrMonth", (format: string) => { + let year = Format.parse("datetime;yyyy"); + let month = Format.parse("datetime;MMM"); + return function (date: any): string { + let d = parseDateInvariant(date); + if (d.getMonth() == 0) return year(d); + else return month(d); + }; +}); + +Format.registerFactory("monthOrDay", (format: string) => { + let month = Format.parse("datetime;MMM"); + let day = Format.parse("datetime;dd"); + return function (date: any): string { + let d = parseDateInvariant(date); + if (d.getDate() == 1) return month(d); + else return day(d); + }; +}); + +export class TimeAxis extends Axis { + declare deadZone: number; + declare lowerDeadZone: number; + declare upperDeadZone: number; + declare snapToTicks: number | false; + declare tickDivisions: Record; + declare minTickUnit: string; + declare useLabelDistanceFormatOverrides: boolean; + declare minLabelDistanceFormatOverride: Record; + declare minLabelDistanceFormatOverrideDefaults: Record; + declare format: string; + declare decode: TimeAxisConfig["decode"]; + + constructor(config: TimeAxisConfig) { + super(config); + } + + init(): void { + if (this.labelAnchor == "auto") this.labelAnchor = this.vertical ? (this.secondary ? "start" : "end") : "start"; + + if (this.labelDx == "auto") this.labelDx = this.vertical ? 0 : "5px"; + + if (this.deadZone) { + this.lowerDeadZone = this.deadZone; + this.upperDeadZone = this.deadZone; + } + + this.minLabelDistanceFormatOverride = { + ...this.minLabelDistanceFormatOverrideDefaults, + ...this.minLabelDistanceFormatOverride, + }; + + super.init(); + } + + declareData(...args: any[]): void { + super.declareData( + { + anchors: undefined, + min: undefined, + max: undefined, + inverted: undefined, + lowerDeadZone: undefined, + upperDeadZone: undefined, + }, + ...args, + ); + } + + initInstance(context: RenderingContext, instance: AxisInstance): void { + instance.calculator = new TimeScale(); + } + + explore(context: RenderingContext, instance: AxisInstance): void { + super.explore(context, instance); + let { min, max, normalized, inverted, lowerDeadZone, upperDeadZone } = instance.data; + instance.calculator.reset( + min, + max, + this.snapToTicks, + this.tickDivisions, + Math.max(1, this.minTickDistance), + Math.max(1, this.minLabelDistance), + normalized, + inverted, + this.minTickUnit, + lowerDeadZone, + upperDeadZone, + this.decode, + this.useLabelDistanceFormatOverrides ? this.minLabelDistanceFormatOverride : {}, + this.format, + ); + } + + render(context: RenderingContext, instance: AxisInstance, key: string): React.ReactNode { + let { data, cached, calculator } = instance; + + cached.axis = calculator.hash(); + + if (!data.bounds.valid()) return null; + + let format = calculator.resolvedFormat; + let minLabelDistance = calculator.resolvedMinLabelDistance; + let formatter = Format.parse(format); + + return ( + + {this.renderTicksAndLabels(context, instance, formatter, minLabelDistance)} + + ); + } +} + +Axis.alias("time", TimeAxis); + +TimeAxis.prototype.baseClass = "timeaxis"; +TimeAxis.prototype.tickDivisions = { + second: [[1, 5, 15, 30]], + minute: [[1, 5, 15, 30]], + hour: [ + [1, 2, 4, 8], + [1, 3, 6, 12], + ], + day: [[1]], + week: [[1]], + month: [[1, 3, 6]], + year: [ + [1, 2, 10], + [1, 5, 10], + [5, 10, 50], + [10, 50, 100], + ], +}; + +const TimeFormats = { + fullDateAndTime: "datetime;yyyy MMM dd HH mm ss n", + shortMonthDate: "datetime;yyyy MMM dd", +}; + +TimeAxis.prototype.snapToTicks = 0; +TimeAxis.prototype.tickSize = 15; +TimeAxis.prototype.minLabelDistance = 60; +TimeAxis.prototype.minTickDistance = 60; +TimeAxis.prototype.minTickUnit = "second"; +TimeAxis.prototype.useLabelDistanceFormatOverrides = false; +TimeAxis.prototype.minLabelDistanceFormatOverrideDefaults = { + [TimeFormats.fullDateAndTime]: 150, + [TimeFormats.shortMonthDate]: 90, +}; + +function monthNumber(date: Date): number { + return date.getFullYear() * 12 + date.getMonth() + (date.getDate() - 1) / 31; +} + +function yearNumber(date: Date): number { + return monthNumber(date) / 12; +} + +const milliSeconds: Record = { + second: 1000, + minute: 60 * 1000, + hour: 3600 * 1000, + day: 3600 * 24 * 1000, + week: 3600 * 24 * 7 * 1000, + month: 3600 * 24 * 30 * 1000, + year: 3600 * 24 * 365 * 1000, +}; + +interface TickSize { + size: number; + measure: string; + noLabels?: boolean; +} + +interface TimeScaleRange { + factor: number; + min: number; + max: number; + minPadding: number; + maxPadding: number; +} + +class TimeScale { + dateCache: Record; + min: number | null; + max: number | null; + snapToTicks: number | false; + tickDivisions: Record; + minLabelDistance: number; + minTickDistance: number; + tickSizes: TickSize[]; + normalized: boolean; + minTickUnit: string; + inverted: boolean; + lowerDeadZone: number; + upperDeadZone: number; + minValue?: number; + maxValue?: number; + minValuePadded?: number; + maxValuePadded?: number; + stacks: Record; + decode?: (date: string) => string; + minLabelDistanceFormatOverride: Record; + format: string; + origin: number; + scale: TimeScaleRange; + a: number; + b: number; + tickMeasure: string; + resolvedFormat: string | null; + resolvedMinLabelDistance: number; + shouldUpdate: boolean; + + reset( + min: any, + max: any, + snapToTicks: number | false, + tickDivisions: Record, + minTickDistance: number, + minLabelDistance: number, + normalized: boolean, + inverted: boolean, + minTickUnit: string, + lowerDeadZone: number, + upperDeadZone: number, + decode: ((date: string) => string) | undefined, + minLabelDistanceFormatOverride: Record, + format: string, + ): void { + this.dateCache = {}; + this.min = min != null ? this.decodeValue(min) : null; + this.max = max != null ? this.decodeValue(max) : null; + this.snapToTicks = snapToTicks; + this.tickDivisions = tickDivisions; + this.minLabelDistance = minLabelDistance; + this.minTickDistance = minTickDistance; + this.tickSizes = []; + this.normalized = normalized; + this.minTickUnit = minTickUnit; + this.inverted = inverted; + this.lowerDeadZone = lowerDeadZone || 0; + this.upperDeadZone = upperDeadZone || 0; + this.minValue = undefined; + this.maxValue = undefined; + this.minValuePadded = undefined; + this.maxValuePadded = undefined; + this.stacks = {}; + this.decode = decode; + this.minLabelDistanceFormatOverride = minLabelDistanceFormatOverride; + this.format = format; + } + + decodeValue(date: any): number { + if (date instanceof Date) return date.getTime(); + + switch (typeof date) { + case "string": + let v = this.dateCache[date]; + if (!v) { + if (this.decode) date = this.decode(date); + v = this.dateCache[date] = parseDateInvariant(date).getTime(); + } + return v; + + case "number": + return parseDateInvariant(date).getTime(); + } + return 0; + } + + encodeValue(v: number): string { + return new Date(v).toISOString(); + } + + getFormat(unit: string, scale: TimeScaleRange): string { + switch (unit) { + case "year": + return "datetime;yyyy"; + + case "month": + if (new Date(scale.min).getFullYear() != new Date(scale.max).getFullYear()) return "yearOrMonth"; + return "datetime;yyyy MMM"; + + case "week": + return "datetime;MMMdd"; + + case "day": + if ( + new Date(scale.min).getFullYear() != new Date(scale.max).getFullYear() || + new Date(scale.min).getMonth() != new Date(scale.max).getMonth() + ) + return "monthOrDay"; + + return TimeFormats.shortMonthDate; + + case "hour": + return "datetime;HH mm n"; + + case "minute": + return "datetime;HH mm n"; + + case "second": + return "datetime;mm ss"; + + default: + return TimeFormats.fullDateAndTime; + } + } + + map(v: any, offset: number = 0): number { + return this.origin + (this.decodeValue(v) + offset - this.scale.min + this.scale.minPadding) * this.scale.factor; + } + + constrainValue(v: number): number { + return Math.max(this.scale.min, Math.min(this.scale.max, v)); + } + + trackValue(v: number, offset: number = 0, constrain: boolean = false): number { + let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding; + if (constrain) value = this.constrainValue(value); + return value; + } + + hash(): Record { + let r: any = { + origin: this.origin, + factor: this.scale.factor, + min: this.scale.min, + max: this.scale.max, + minPadding: this.scale.minPadding, + maxPadding: this.scale.maxPadding, + }; + r.stacks = Object.keys(this.stacks) + .map((s) => this.stacks[s].info?.join(",")) + .join(":"); + return r; + } + + isSame(x: any): boolean { + let hash = this.hash(); + let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]); + this.shouldUpdate = !same; + return same; + } + + measure(a: number, b: number): void { + this.a = a; + this.b = b; + + for (let s in this.stacks) { + let info = this.stacks[s].measure(this.normalized); + let [min, max] = info; + if (this.minValue == null || min < this.minValue) this.minValue = min; + if (this.max == null || max > this.maxValue!) this.maxValue = max; + this.stacks[s].info = info; + } + + if (this.min == null) { + if (this.minValue != null) this.min = this.minValue; + else this.min = 0; + } + + if (this.max == null) { + if (this.maxValue != null) this.max = this.maxValue; + else this.max = this.normalized ? 1 : 100; + } + + this.origin = this.inverted ? this.b : this.a; + + this.calculateTicks(); + if (this.scale == null) { + this.scale = this.getScale(); + } + } + + getTimezoneOffset(date: Date): number { + return date.getTimezoneOffset() * 60 * 1000; + } + + getScale(tickSize?: number | null, measure?: string, minRange: number = 1000): TimeScaleRange { + let { min, max, upperDeadZone, lowerDeadZone } = this; + + let smin: number = min!; + let smax: number = max!; + + if (tickSize) { + let minDate = new Date(min!); + let maxDate = new Date(max!); + + switch (measure) { + case "second": + case "minute": + case "hour": + case "day": + default: + let minOffset = this.getTimezoneOffset(minDate); + let maxOffset = this.getTimezoneOffset(maxDate); + let mondayOffset = 4 * milliSeconds.day; //new Date(0).getDay() => 4 + smin = Math.floor((smin - minOffset - mondayOffset) / tickSize) * tickSize + minOffset + mondayOffset; + smax = Math.ceil((smax - maxOffset - mondayOffset) / tickSize) * tickSize + maxOffset + mondayOffset; + break; + + case "month": + tickSize /= milliSeconds.month; + let minMonth = monthNumber(minDate); + let maxMonth = monthNumber(maxDate); + minMonth = Math.floor(minMonth / tickSize) * tickSize; + maxMonth = Math.ceil(maxMonth / tickSize) * tickSize; + smin = new Date(Math.floor(minMonth / 12), minMonth % 12, 1).getTime(); + smax = new Date(Math.floor(maxMonth / 12), maxMonth % 12, 1).getTime(); + break; + + case "year": + tickSize /= milliSeconds.year; + let minYear = yearNumber(minDate); + let maxYear = yearNumber(maxDate); + minYear = Math.floor(minYear / tickSize) * tickSize; + maxYear = Math.ceil(maxYear / tickSize) * tickSize; + smin = new Date(minYear, 0, 1).getTime(); + smax = new Date(maxYear, 0, 1).getTime(); + break; + } + } else { + if (this.minValue == min) smin = this.minValuePadded!; + if (this.maxValue == max) smax = this.maxValuePadded!; + } + + if (smax - smin < minRange) { + let delta = (minRange - (smax - smin)) / 2; + smin -= delta; + smax += delta; + } + + //padding should be activated only if using min/max obtained from the data + let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded!) : 0; + let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded! - smax) : 0; + + let factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; + if (factor > 0 && (upperDeadZone > 0 || lowerDeadZone > 0)) { + smin -= lowerDeadZone / factor; + smax += upperDeadZone / factor; + minPadding = this.minValuePadded != null ? Math.max(0, smin - this.minValuePadded) : 0; + maxPadding = this.maxValuePadded != null ? Math.max(0, this.maxValuePadded - smax) : 0; + factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0; + } + + let sign = this.b > this.a ? 1 : -1; + + return { + factor: sign * (this.inverted ? -factor : factor), + min: smin, + max: smax, + minPadding, + maxPadding, + }; + } + + acknowledge(value: any, width: number = 0, offset: number = 0): void { + value = this.decodeValue(value); + if (this.minValue == null || value + offset - width / 2 < this.minValuePadded!) { + this.minValue = value; + this.minValuePadded = value + offset - width / 2; + } + if (this.maxValue == null || value + offset + width / 2 > this.maxValuePadded!) { + this.maxValue = value; + this.maxValuePadded = value + offset + width / 2; + } + } + + getStack(name: string): Stack { + let s = this.stacks[name]; + if (!s) s = this.stacks[name] = new Stack(); + return s; + } + + stacknowledge(name: string, ordinal: string, value: number | null): void { + return this.getStack(name).acknowledge(ordinal, value); + } + + stack(name: string, ordinal: string, value: number | null): number | null { + let v = this.getStack(name).stack(ordinal, value); + return v != null ? this.map(v) : null; + } + + findTickSize(minPxDist: number): TickSize | undefined { + return this.tickSizes.find(({ size, noLabels }) => !noLabels && size * Math.abs(this.scale.factor) >= minPxDist); + } + + getTickSizes(): TickSize[] { + return this.tickSizes; + } + + calculateTicks(): void { + let minReached = false; + + let minRange = 1000; + + for (let unit in milliSeconds) { + if (!minReached) { + if (unit == this.minTickUnit) minReached = true; + else continue; + } + + let unitSize = milliSeconds[unit]; + let divisions = this.tickDivisions[unit]; + + if (this.tickSizes.length > 0) { + //add ticks from higher levels + this.tickSizes.push(...divisions[0].map((s) => ({ size: s * unitSize, measure: unit }))); + break; + } + + let bestLabelDistance = Infinity; + let bestMinLabelDistance = this.minLabelDistance; + let bestTicks: number[] = []; + let bestScale: TimeScaleRange | null = null; + let bestFormat: string | null = null; + + this.tickMeasure = unit; + + for (let i = 0; i < divisions.length; i++) { + let divs = divisions[i]; + for (let d = 0; d < divs.length; d++) { + //if (useSnapToTicks && d < Math.min(divs.length - 1, this.snapToTicks)) continue; + let tickSize = divs[d] * unitSize; + let scale = this.getScale(null, unit, tickSize); + let format = this.format ?? this.getFormat(unit, scale); + let minLabelDistance = this.minLabelDistanceFormatOverride[format] ?? this.minLabelDistance; + let labelDistance = tickSize * Math.abs(scale.factor); + if (labelDistance >= minLabelDistance && labelDistance < bestLabelDistance) { + bestScale = scale; + bestTicks = divs.map((s) => s * unitSize); + bestLabelDistance = labelDistance; + bestFormat = format; + bestMinLabelDistance = minLabelDistance; + minRange = tickSize; + } + } + } + + this.scale = bestScale!; + this.tickSizes = bestTicks + .filter((ts) => ts * Math.abs(bestScale!.factor) >= this.minTickDistance) + .map((size) => ({ size, measure: this.tickMeasure })); + this.resolvedFormat = bestFormat; + this.resolvedMinLabelDistance = bestMinLabelDistance; + } + + let lowerTickUnit: string | null = null; + switch (this.tickMeasure) { + case "year": + lowerTickUnit = "month"; + break; + case "month": + lowerTickUnit = "day"; + break; + case "week": + lowerTickUnit = "day"; + break; + case "day": + lowerTickUnit = "hour"; + break; + case "hour": + lowerTickUnit = "minute"; + break; + case "minute": + lowerTickUnit = "second"; + break; + } + + if (lowerTickUnit && this.minTickUnit && milliSeconds[lowerTickUnit] < milliSeconds[this.minTickUnit]) + lowerTickUnit = this.minTickUnit == this.tickMeasure ? null : this.minTickUnit; + + if (lowerTickUnit != null && this.scale) { + let bestMinorTickSize = Infinity; + let divisions = this.tickDivisions[lowerTickUnit]; + let unitSize = milliSeconds[lowerTickUnit]; + for (let i = 0; i < divisions.length; i++) { + let divs = divisions[i]; + for (let d = 0; d < divs.length; d++) { + let tickSize = divs[d] * unitSize; + if (tickSize * Math.abs(this.scale.factor) >= this.minTickDistance && tickSize < bestMinorTickSize) { + bestMinorTickSize = tickSize; + } + } + } + if (bestMinorTickSize != Infinity) { + this.tickSizes.unshift({ size: bestMinorTickSize, measure: lowerTickUnit, noLabels: true }); + if (this.tickSizes.length > 1) { + let labelStep = this.tickSizes[1].size; + let lowerScale = this.getScale(null, lowerTickUnit, minRange); + if (lowerScale.max - lowerScale.min >= labelStep) this.scale = lowerScale; + } + } + } + + if (isNumber(this.snapToTicks) && this.snapToTicks >= 0 && this.tickSizes.length > 0) { + let tickSize = this.tickSizes[Math.min(this.tickSizes.length - 1, this.snapToTicks)]; + this.scale = this.getScale(tickSize.size, tickSize.measure, minRange); + } + } + + getTicks(tickSizes: TickSize[]): (number | Date)[][] { + return tickSizes.map(({ size, measure }) => { + let result: (number | Date)[] = [], + start: number, + end: number, + minDate: Date, + maxDate: Date; + if (measure == "year") { + size /= milliSeconds.year; + minDate = new Date(this.scale.min - this.scale.minPadding); + maxDate = new Date(this.scale.max + this.scale.maxPadding); + start = Math.ceil(yearNumber(minDate) / size) * size; + end = Math.floor(yearNumber(maxDate) / size) * size; + for (let i = start; i <= end; i += size) result.push(new Date(i, 0, 1).getTime()); + } else if (measure == "month") { + size /= milliSeconds.month; + minDate = new Date(this.scale.min - this.scale.minPadding); + maxDate = new Date(this.scale.max + this.scale.maxPadding); + start = Math.ceil(monthNumber(minDate) / size) * size; + end = Math.floor(monthNumber(maxDate) / size) * size; + for (let i = start; i <= end; i += size) result.push(new Date(Math.floor(i / 12), i % 12, 1).getTime()); + } else if (measure == "day" || measure == "week") { + let multiplier = measure == "week" ? 7 : 1; + size /= milliSeconds.day; + minDate = new Date(this.scale.min - this.scale.minPadding); + maxDate = new Date(this.scale.max + this.scale.maxPadding); + let date = zeroTime(minDate); + while (date.getTime() < minDate.getTime()) date.setDate(date.getDate() + 1); + if (measure == "week") { + //start on monday + while (date.getDay() != 1) { + date.setDate(date.getDate() + 1); + } + } + while (date.getTime() <= maxDate.getTime()) { + result.push(date); + date = new Date(date); + date.setDate(date.getDate() + multiplier); + } + } else { + let minOffset = this.getTimezoneOffset(new Date(this.scale.min - this.scale.minPadding)); + let mondayOffset = 4 * milliSeconds.day; + let date = + Math.ceil((this.scale.min - this.scale.minPadding - minOffset - mondayOffset) / size) * size + + minOffset + + mondayOffset; + while (date <= this.scale.max + this.scale.maxPadding) { + result.push(date); + date += size; + } + } + return result; + }); + } + + mapGridlines(): number[] { + if (this.tickSizes.length == 0) return []; + return this.getTicks([this.tickSizes[0]])[0].map((x) => this.map(x)); + } + + book(): void { + Console.warn("TimeAxis does not support the autoSize flag for column and bar graphs."); + } +} diff --git a/packages/cx/src/charts/axis/index.d.ts b/packages/cx/src/charts/axis/index.d.ts deleted file mode 100644 index 721df104d..000000000 --- a/packages/cx/src/charts/axis/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Axis'; -export * from './NumericAxis'; -export * from './CategoryAxis'; -export * from './TimeAxis'; \ No newline at end of file diff --git a/packages/cx/src/charts/axis/index.js b/packages/cx/src/charts/axis/index.js deleted file mode 100644 index 721df104d..000000000 --- a/packages/cx/src/charts/axis/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Axis'; -export * from './NumericAxis'; -export * from './CategoryAxis'; -export * from './TimeAxis'; \ No newline at end of file diff --git a/packages/cx/src/charts/axis/index.ts b/packages/cx/src/charts/axis/index.ts new file mode 100644 index 000000000..2cede96fe --- /dev/null +++ b/packages/cx/src/charts/axis/index.ts @@ -0,0 +1,4 @@ +export * from "./Axis"; +export * from "./NumericAxis"; +export * from "./CategoryAxis"; +export * from "./TimeAxis"; \ No newline at end of file diff --git a/packages/cx/src/charts/helpers/MinMaxFinder.d.ts b/packages/cx/src/charts/helpers/MinMaxFinder.d.ts deleted file mode 100644 index af2e7038d..000000000 --- a/packages/cx/src/charts/helpers/MinMaxFinder.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Cx from "../../core"; -import { PointReducerProps } from "./PointReducer"; - -interface MinMaxFinderProps extends PointReducerProps { - /* A binding used to receive the x value of the point with the minimum value */ - minX?: Cx.Bind | Cx.AccessorChain; - - /* A binding used to receive the y value of the point with the minimum value */ - minY?: Cx.Bind | Cx.AccessorChain; - - /* A binding used to receive the x value of the point with the maximum value */ - maxX?: Cx.Bind | Cx.AccessorChain; - - /* A binding used to receive the x value of the point with the maximum value */ - maxY?: Cx.Bind | Cx.AccessorChain; - - /* An object used for filtering data points. Available as accumulator.params inside the onMap function. */ - params?: Cx.StructuredProp; -} - -/** Find minimum and maximum points of a point series */ -export class MinMaxFinder extends Cx.Widget {} diff --git a/packages/cx/src/charts/helpers/MinMaxFinder.js b/packages/cx/src/charts/helpers/MinMaxFinder.js deleted file mode 100644 index e7c2c18ff..000000000 --- a/packages/cx/src/charts/helpers/MinMaxFinder.js +++ /dev/null @@ -1,36 +0,0 @@ -import { PointReducer } from "./PointReducer"; - -export class MinMaxFinder extends PointReducer { - declareData() { - return super.declareData(...arguments, { - minX: undefined, - minY: undefined, - maxX: undefined, - maxY: undefined, - params: { - structured: true, - }, - }); - } - - onInitAccumulator(acc, { data }) { - acc.params = data.params; - acc.min = { x: null, y: null }; - acc.max = { x: null, y: null }; - } - - onMap(acc, x, y, name, p) { - if (y != null && (acc.max.y == null || acc.max.y < y)) acc.max = { x, y, p }; - - if (y != null && (acc.min.y == null || acc.min.y > y)) acc.min = { x, y, p }; - } - - onReduce(acc, instance) { - instance.set("minX", acc.min.x); - instance.set("minY", acc.min.y); - instance.set("minRecord", acc.min.p); - instance.set("maxX", acc.max.x); - instance.set("maxY", acc.max.y); - instance.set("maxRecord", acc.max.p); - } -} diff --git a/packages/cx/src/charts/helpers/MinMaxFinder.ts b/packages/cx/src/charts/helpers/MinMaxFinder.ts new file mode 100644 index 000000000..9815505db --- /dev/null +++ b/packages/cx/src/charts/helpers/MinMaxFinder.ts @@ -0,0 +1,66 @@ +import { PointReducer, PointReducerConfig, PointReducerInstance, PointReducerAccumulator } from "./PointReducer"; +import { Bind, StructuredProp } from "../../ui/Prop"; +import { AccessorChain } from "../../data/createAccessorModelProxy"; + +export interface MinMaxAccumulator extends PointReducerAccumulator { + params: any; + min: { x: any; y: any; p?: any }; + max: { x: any; y: any; p?: any }; +} + +export interface MinMaxFinderConfig extends PointReducerConfig { + /** A binding used to receive the x value of the point with the minimum value */ + minX?: Bind | AccessorChain; + + /** A binding used to receive the y value of the point with the minimum value */ + minY?: Bind | AccessorChain; + + /** A binding used to receive the x value of the point with the maximum value */ + maxX?: Bind | AccessorChain; + + /** A binding used to receive the y value of the point with the maximum value */ + maxY?: Bind | AccessorChain; + + /** An object used for filtering data points. Available as accumulator.params inside the onMap function. */ + params?: StructuredProp; +} + +/** Find minimum and maximum points of a point series */ +export class MinMaxFinder extends PointReducer { + constructor(config?: MinMaxFinderConfig) { + super(config); + } + + declareData(...args: any[]) { + super.declareData(...args, { + minX: undefined, + minY: undefined, + maxX: undefined, + maxY: undefined, + params: { + structured: true, + }, + }); + } + + onInitAccumulator = (acc: MinMaxAccumulator, { data }: PointReducerInstance) => { + acc.params = (data as any).params; + acc.min = { x: null, y: null }; + acc.max = { x: null, y: null }; + }; + + onMap = (acc: MinMaxAccumulator, x: any, y: any, name: string, p: any) => { + if (y != null && (acc.max.y == null || acc.max.y < y)) acc.max = { x, y, p }; + + if (y != null && (acc.min.y == null || acc.min.y > y)) acc.min = { x, y, p }; + }; + + onReduce = (acc: MinMaxAccumulator, instance: PointReducerInstance) => { + instance.set("minX", acc.min.x); + instance.set("minY", acc.min.y); + instance.set("minRecord", acc.min.p); + instance.set("maxX", acc.max.x); + instance.set("maxY", acc.max.y); + instance.set("maxRecord", acc.max.p); + }; +} diff --git a/packages/cx/src/charts/helpers/PointReducer.d.ts b/packages/cx/src/charts/helpers/PointReducer.d.ts deleted file mode 100644 index 27fccea0a..000000000 --- a/packages/cx/src/charts/helpers/PointReducer.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Cx from "../../core"; -import { Instance } from "../../ui/Instance"; - -export interface PointReducerProps extends Cx.PureContainerProps { - /** A callback function used to initialize the accumulator. */ - onInitAccumulator?: (accumulator: Cx.Record, instance?: Instance) => void; - - /** A callback function used to collect information about all data points. */ - onMap?: (accumulator: Cx.Record, x?: number, y?: number, name?: string) => void; - - /** A callback function used to process accumulated information and write results. */ - onReduce?: (accumulator: Cx.Record, instance?: Instance) => void; - - /** Parameters that trigger filter predicate re-creation. */ - filterParams?: StructuredProp; - - /** A callback function used to create a predicate for filtering points. */ - onCreatePointFilter?: ( - filterParams: any, - instance: Instance, - ) => (x: number, y: number, name: string, data: any, array?: any[], index?: number) => boolean; -} - -export class PointReducer extends Cx.Widget {} diff --git a/packages/cx/src/charts/helpers/PointReducer.js b/packages/cx/src/charts/helpers/PointReducer.js deleted file mode 100644 index 1456c9167..000000000 --- a/packages/cx/src/charts/helpers/PointReducer.js +++ /dev/null @@ -1,61 +0,0 @@ -import { PureContainer } from "../../ui/PureContainer"; - -export class PointReducer extends PureContainer { - declareData() { - super.declareData(...arguments, { - filterParams: { - structured: true, - }, - }); - } - - prepareData(context, instance) { - super.prepareData(context, instance); - - instance.resetAccumulator = () => { - let accumulator = {}; - if (this.onInitAccumulator) { - instance.invoke("onInitAccumulator", accumulator, instance); - instance.accumulator = accumulator; - } - }; - - if (this.onCreatePointFilter) - instance.pointFilter = instance.invoke("onCreatePointFilter", instance.data.filterParams, instance); - } - - explore(context, instance) { - instance.resetAccumulator(); - - let parentPointReducer = context.pointReducer; - instance.parentPointTracker = parentPointReducer; - - let pointFilter = instance.pointFilter; - let accumulator = instance.accumulator; - let onMap = this.onMap && instance.getCallback("onMap"); - instance.pointReducer = (x, y, name, data, array, index) => { - if (!pointFilter || pointFilter(x, y, name, data, array, index)) - onMap(accumulator, x, y, name, data, array, index); - if (parentPointReducer) parentPointReducer(x, y, name, data, array, index); - }; - instance.write = () => { - if (this.onReduce) instance.invoke("onReduce", accumulator, instance); - }; - - context.push("pointReducer", instance.pointReducer); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("pointReducer"); - } - - prepare(context, instance) { - context.push("pointReducer", instance.pointReducer); - } - - prepareCleanup(context, instance) { - context.pop("pointReducer"); - instance.write(); - } -} diff --git a/packages/cx/src/charts/helpers/PointReducer.ts b/packages/cx/src/charts/helpers/PointReducer.ts new file mode 100644 index 000000000..ce53cba6e --- /dev/null +++ b/packages/cx/src/charts/helpers/PointReducer.ts @@ -0,0 +1,135 @@ +import { PureContainerBase, PureContainerConfig } from "../../ui/PureContainer"; +import { Instance } from "../../ui/Instance"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { DataRecord, StructuredProp } from "../../ui/Prop"; + +export type PointReducerFunction = ( + x: any, + y: any, + name: string, + data: any, + array?: any[], + index?: number +) => void; + +export interface PointReducerAccumulator { + [key: string]: any; +} + +export interface PointReducerInstance + extends Instance { + resetAccumulator: () => void; + pointFilter?: (x: any, y: any, name: string, data: any, array?: any[], index?: number) => boolean; + accumulator?: TAccumulator; + parentPointTracker?: PointReducerFunction; + pointReducer?: PointReducerFunction; + write?: () => void; +} + +export interface PointReducerConfig extends PureContainerConfig { + /** A callback function used to initialize the accumulator. */ + onInitAccumulator?: string | ((accumulator: DataRecord, instance?: Instance) => void); + + /** A callback function used to collect information about all data points. */ + onMap?: + | string + | ((accumulator: DataRecord, x?: any, y?: any, name?: string, data?: any, array?: any[], index?: number) => void); + + /** A callback function used to process accumulated information and write results. */ + onReduce?: string | ((accumulator: DataRecord, instance?: Instance) => void); + + /** Parameters that trigger filter predicate re-creation. */ + filterParams?: StructuredProp; + + /** A callback function used to create a predicate for filtering points. */ + onCreatePointFilter?: + | string + | (( + filterParams: any, + instance: Instance + ) => (x: number, y: number, name: string, data: any, array?: any[], index?: number) => boolean); +} + +export class PointReducer< + TAccumulator extends PointReducerAccumulator = PointReducerAccumulator, +> extends PureContainerBase> { + declare onCreatePointFilter?: PointReducerConfig["onCreatePointFilter"]; + // These can be either config properties (string/function) or overridden methods in subclasses + declare onInitAccumulator?: (acc: TAccumulator, instance: PointReducerInstance) => void; + declare onMap?: ( + acc: TAccumulator, + x: any, + y: any, + name: string, + data?: any, + array?: any[], + index?: number + ) => void; + declare onReduce?: (acc: TAccumulator, instance: PointReducerInstance) => void; + + constructor(config?: PointReducerConfig) { + super(config); + } + + declareData(...args: any[]) { + super.declareData(...args, { + filterParams: { + structured: true, + }, + }); + } + + prepareData(context: RenderingContext, instance: PointReducerInstance) { + super.prepareData(context, instance); + + instance.resetAccumulator = () => { + let accumulator = {} as TAccumulator; + if (this.onInitAccumulator) { + instance.invoke("onInitAccumulator", accumulator, instance); + instance.accumulator = accumulator; + } + }; + + if (this.onCreatePointFilter) + instance.pointFilter = instance.invoke( + "onCreatePointFilter", + (instance.data as any).filterParams, + instance + ) as PointReducerInstance["pointFilter"]; + } + + explore(context: RenderingContext, instance: PointReducerInstance) { + instance.resetAccumulator(); + + let parentPointReducer = context.pointReducer as PointReducerFunction | undefined; + instance.parentPointTracker = parentPointReducer; + + let pointFilter = instance.pointFilter; + let accumulator = instance.accumulator; + let onMap = this.onMap && instance.getCallback("onMap"); + instance.pointReducer = (x, y, name, data, array, index) => { + if (!pointFilter || pointFilter(x, y, name, data, array, index)) + onMap?.(accumulator, x, y, name, data, array, index); + if (parentPointReducer) parentPointReducer(x, y, name, data, array, index); + }; + instance.write = () => { + if (this.onReduce) instance.invoke("onReduce", accumulator, instance); + }; + + context.push("pointReducer", instance.pointReducer); + super.explore(context, instance); + } + + exploreCleanup(context: RenderingContext, instance: PointReducerInstance) { + context.pop("pointReducer"); + } + + prepare(context: RenderingContext, instance: PointReducerInstance) { + context.push("pointReducer", instance.pointReducer); + } + + prepareCleanup(context: RenderingContext, instance: PointReducerInstance) { + context.pop("pointReducer"); + instance.write?.(); + } +} diff --git a/packages/cx/src/charts/helpers/SnapPointFinder.d.ts b/packages/cx/src/charts/helpers/SnapPointFinder.d.ts deleted file mode 100644 index 270f42d90..000000000 --- a/packages/cx/src/charts/helpers/SnapPointFinder.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Cx from "../../core"; -import { PointReducerProps } from "./PointReducer"; - -interface SnapPointFinderProps extends PointReducerProps { - /* Cursor X value. */ - cursorX?: Cx.NumberProp; - - /* Cursor Y value */ - cursorY?: Cx.NumberProp; - - /* A binding used to receive the x value of the nearest point.*/ - snapX?: Cx.Bind | Cx.AccessorChain | Cx.AccessorChain; - - /* A binding used to receive the y value of the nearest point. */ - snapY?: Cx.Bind | Cx.AccessorChain | Cx.AccessorChain; - - /* A binding used to receive the record prop */ - snapRecord?: Cx.Prop; - - /* Maximum distance between cursor and the snap point. Default value is 50. Adjust accordingly for large distances, e.g. set to Infinity when using TimeAxis */ - maxDistance?: number; - - /* A function used to convert x values into numeric format. Commonly used with dates. */ - convertX?: (value: number | string) => number; - - /* A function used to convert y values into numeric format. Commonly used with dates. */ - convertY?: (value: number | string) => number; -} - -export class SnapPointFinder extends Cx.Widget {} diff --git a/packages/cx/src/charts/helpers/SnapPointFinder.js b/packages/cx/src/charts/helpers/SnapPointFinder.js deleted file mode 100644 index fdc02fa75..000000000 --- a/packages/cx/src/charts/helpers/SnapPointFinder.js +++ /dev/null @@ -1,69 +0,0 @@ -import { PointReducer } from "./PointReducer"; - -export class SnapPointFinder extends PointReducer { - declareData() { - return super.declareData(...arguments, { - cursorX: undefined, - cursorY: undefined, - snapX: undefined, - snapY: undefined, - snapRecord: undefined, - maxDistance: undefined, - }); - } - - explore(context, instance) { - instance.xAxis = context.axes[this.xAxis]; - instance.yAxis = context.axes[this.yAxis]; - super.explore(context, instance); - } - - onInitAccumulator(acc, { data, xAxis, yAxis }) { - acc.cursor = { - x: data.cursorX, - y: data.cursorY, - mapped: false, - }; - acc.dist = data.maxDistance > 0 ? Math.pow(data.maxDistance, 2) : Number.POSITIVE_INFINITY; - acc.snapX = null; - acc.snapY = null; - acc.xAxis = xAxis; - acc.yAxis = yAxis; - } - - onMap(acc, x, y, name, p) { - let { xAxis, yAxis, cursor } = acc; - - if (!cursor.mapped) { - cursor.mappedX = cursor.x != null ? xAxis?.map(this.convertX(cursor.x)) : null; - cursor.mappedY = cursor.y != null ? yAxis?.map(this.convertY(cursor.y)) : null; - cursor.mapped = true; - } - - let d = null; - let cx = x != null ? xAxis?.map(this.convertX(x)) : null; - let cy = y != null ? yAxis?.map(this.convertY(y)) : null; - - if (cursor.mappedX != null && cx != null) d = (d || 0) + Math.pow(Math.abs(cx - cursor.mappedX), 2); - if (cursor.mappedY != null && cy != null) d = (d || 0) + Math.pow(Math.abs(cy - cursor.mappedY), 2); - - if (d != null && d < acc.dist) { - acc.dist = d; - acc.snapX = x; - acc.snapY = y; - acc.snapRecord = p; - } - } - - onReduce(acc, instance) { - instance.set("snapX", acc.snapX); - instance.set("snapY", acc.snapY); - instance.set("snapRecord", acc.snapRecord); - } -} - -SnapPointFinder.prototype.maxDistance = 50; -SnapPointFinder.prototype.convertX = (x) => x; -SnapPointFinder.prototype.convertY = (y) => y; -SnapPointFinder.prototype.xAxis = "x"; -SnapPointFinder.prototype.yAxis = "y"; diff --git a/packages/cx/src/charts/helpers/SnapPointFinder.ts b/packages/cx/src/charts/helpers/SnapPointFinder.ts new file mode 100644 index 000000000..60af1e844 --- /dev/null +++ b/packages/cx/src/charts/helpers/SnapPointFinder.ts @@ -0,0 +1,136 @@ +import { PointReducer, PointReducerConfig, PointReducerInstance, PointReducerAccumulator } from "./PointReducer"; +import { RenderingContext } from "../../ui/RenderingContext"; +import { NumberProp, Bind, Prop, DataRecord } from "../../ui/Prop"; +import { AccessorChain } from "../../data/createAccessorModelProxy"; + +export interface SnapAccumulator extends PointReducerAccumulator { + cursor: { + x: number | null; + y: number | null; + mapped: boolean; + mappedX?: number | null; + mappedY?: number | null; + }; + dist: number; + snapX: any; + snapY: any; + snapRecord?: any; + xAxis: any; + yAxis: any; +} + +export interface SnapPointFinderInstance extends PointReducerInstance { + xAxis?: any; + yAxis?: any; +} + +export interface SnapPointFinderConfig extends PointReducerConfig { + /** Cursor X value. */ + cursorX?: NumberProp; + + /** Cursor Y value */ + cursorY?: NumberProp; + + /** A binding used to receive the x value of the nearest point.*/ + snapX?: Bind | AccessorChain | AccessorChain; + + /** A binding used to receive the y value of the nearest point. */ + snapY?: Bind | AccessorChain | AccessorChain; + + /** A binding used to receive the record prop */ + snapRecord?: Prop; + + /** Maximum distance between cursor and the snap point. Default value is 50. Adjust accordingly for large distances, e.g. set to Infinity when using TimeAxis */ + maxDistance?: number; + + /** A function used to convert x values into numeric format. Commonly used with dates. */ + convertX?: (value: number | string) => number; + + /** A function used to convert y values into numeric format. Commonly used with dates. */ + convertY?: (value: number | string) => number; + + /** Name of the x-axis. Default is 'x'. */ + xAxis?: string; + + /** Name of the y-axis. Default is 'y'. */ + yAxis?: string; +} + +export class SnapPointFinder extends PointReducer { + declare maxDistance: number; + declare convertX: (value: any) => number; + declare convertY: (value: any) => number; + declare xAxis: string; + declare yAxis: string; + + constructor(config?: SnapPointFinderConfig) { + super(config); + } + + declareData(...args: any[]) { + super.declareData(...args, { + cursorX: undefined, + cursorY: undefined, + snapX: undefined, + snapY: undefined, + snapRecord: undefined, + maxDistance: undefined, + }); + } + + explore(context: RenderingContext, instance: SnapPointFinderInstance) { + instance.xAxis = (context.axes as any)?.[this.xAxis]; + instance.yAxis = (context.axes as any)?.[this.yAxis]; + super.explore(context, instance); + } + + onInitAccumulator = (acc: SnapAccumulator, { data, xAxis, yAxis }: SnapPointFinderInstance) => { + const d = data as any; + acc.cursor = { + x: d.cursorX, + y: d.cursorY, + mapped: false, + }; + acc.dist = d.maxDistance > 0 ? Math.pow(d.maxDistance, 2) : Number.POSITIVE_INFINITY; + acc.snapX = null; + acc.snapY = null; + acc.xAxis = xAxis; + acc.yAxis = yAxis; + }; + + onMap = (acc: SnapAccumulator, x: any, y: any, name: string, p: any) => { + let { xAxis, yAxis, cursor } = acc; + + if (!cursor.mapped) { + cursor.mappedX = cursor.x != null ? xAxis?.map(this.convertX(cursor.x)) : null; + cursor.mappedY = cursor.y != null ? yAxis?.map(this.convertY(cursor.y)) : null; + cursor.mapped = true; + } + + let d: number | null = null; + let cx = x != null ? xAxis?.map(this.convertX(x)) : null; + let cy = y != null ? yAxis?.map(this.convertY(y)) : null; + + if (cursor.mappedX != null && cx != null) d = (d || 0) + Math.pow(Math.abs(cx - cursor.mappedX), 2); + if (cursor.mappedY != null && cy != null) d = (d || 0) + Math.pow(Math.abs(cy - cursor.mappedY), 2); + + if (d != null && d < acc.dist) { + acc.dist = d; + acc.snapX = x; + acc.snapY = y; + acc.snapRecord = p; + } + }; + + onReduce = (acc: SnapAccumulator, instance: PointReducerInstance) => { + instance.set("snapX", acc.snapX); + instance.set("snapY", acc.snapY); + instance.set("snapRecord", acc.snapRecord); + }; +} + +SnapPointFinder.prototype.maxDistance = 50; +SnapPointFinder.prototype.convertX = (x) => x; +SnapPointFinder.prototype.convertY = (y) => y; +SnapPointFinder.prototype.xAxis = "x"; +SnapPointFinder.prototype.yAxis = "y"; diff --git a/packages/cx/src/charts/helpers/ValueAtFinder.d.ts b/packages/cx/src/charts/helpers/ValueAtFinder.d.ts deleted file mode 100644 index 3440d6a63..000000000 --- a/packages/cx/src/charts/helpers/ValueAtFinder.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Cx from "../../core"; -import { PointReducerProps } from "./PointReducer"; - -interface ValueAtFinderProps extends PointReducerProps { - /* X axis probe value. */ - at?: Cx.NumberProp | Cx.StringProp; - - /* A binding used to receive the measured y axis value */ - value?: Cx.Bind | Cx.AccessorChain; - - /* A function used to convert x values into numeric format. Commonly used with dates. */ - convert?: (value: number | string) => number; -} - -/** Calculate value at a given point on the graph */ -export class ValueAtFinder extends Cx.Widget {} diff --git a/packages/cx/src/charts/helpers/ValueAtFinder.js b/packages/cx/src/charts/helpers/ValueAtFinder.js deleted file mode 100644 index e035cb191..000000000 --- a/packages/cx/src/charts/helpers/ValueAtFinder.js +++ /dev/null @@ -1,46 +0,0 @@ -import { PointReducer } from "./PointReducer"; - -export class ValueAtFinder extends PointReducer { - declareData() { - return super.declareData(...arguments, { - at: undefined, - value: undefined, - }); - } - - onInitAccumulator(acc, { data }) { - acc.at = this.convert(data.at); - } - - onMap(acc, x, y, name) { - let cx = this.convert(x); - let d = cx - acc.at; - if (d <= 0 && (!acc.left || acc.left.d < d)) { - acc.left = { - x: cx, - y, - d, - }; - } - if (d >= 0 && (!acc.right || acc.right.d > d)) { - acc.right = { - x: cx, - y, - d, - }; - } - } - - onReduce(acc, instance) { - let y = null; - if (acc.left && acc.right) { - if (acc.left.x == acc.right.x) y = acc.left.y; - else if (acc.left.y != null && acc.right.y != null) { - y = acc.left.y + ((acc.right.y - acc.left.y) * (acc.at - acc.left.x)) / (acc.right.x - acc.left.x); - } - } - instance.set("value", y); - } -} - -ValueAtFinder.prototype.convert = (x) => x; diff --git a/packages/cx/src/charts/helpers/ValueAtFinder.ts b/packages/cx/src/charts/helpers/ValueAtFinder.ts new file mode 100644 index 000000000..186732632 --- /dev/null +++ b/packages/cx/src/charts/helpers/ValueAtFinder.ts @@ -0,0 +1,72 @@ +import { PointReducer, PointReducerConfig, PointReducerInstance, PointReducerAccumulator } from "./PointReducer"; +import { NumberProp, StringProp, Bind } from "../../ui/Prop"; +import { AccessorChain } from "../../data/createAccessorModelProxy"; + +export interface ValueAtAccumulator extends PointReducerAccumulator { + at: number; + left?: { x: number; y: number; d: number }; + right?: { x: number; y: number; d: number }; +} + +export interface ValueAtFinderConfig extends PointReducerConfig { + /** X axis probe value. */ + at?: NumberProp | StringProp; + + /** A binding used to receive the measured y axis value */ + value?: Bind | AccessorChain; + + /** A function used to convert x values into numeric format. Commonly used with dates. */ + convert?: (value: number | string) => number; +} + +/** Calculate value at a given point on the graph */ +export class ValueAtFinder extends PointReducer { + declare convert: (value: any) => number; + + constructor(config?: ValueAtFinderConfig) { + super(config); + } + + declareData(...args: any[]) { + super.declareData(...args, { + at: undefined, + value: undefined, + }); + } + + onInitAccumulator = (acc: ValueAtAccumulator, { data }: PointReducerInstance) => { + acc.at = this.convert((data as any).at); + }; + + onMap = (acc: ValueAtAccumulator, x: any, y: any, name: string) => { + let cx = this.convert(x); + let d = cx - acc.at; + if (d <= 0 && (!acc.left || acc.left.d < d)) { + acc.left = { + x: cx, + y, + d, + }; + } + if (d >= 0 && (!acc.right || acc.right.d > d)) { + acc.right = { + x: cx, + y, + d, + }; + } + }; + + onReduce = (acc: ValueAtAccumulator, instance: PointReducerInstance) => { + let y: number | null = null; + if (acc.left && acc.right) { + if (acc.left.x == acc.right.x) y = acc.left.y; + else if (acc.left.y != null && acc.right.y != null) { + y = acc.left.y + ((acc.right.y - acc.left.y) * (acc.at - acc.left.x)) / (acc.right.x - acc.left.x); + } + } + instance.set("value", y); + }; +} + +ValueAtFinder.prototype.convert = (x) => x; diff --git a/packages/cx/src/charts/helpers/index.d.ts b/packages/cx/src/charts/helpers/index.d.ts deleted file mode 100644 index ff5a2097a..000000000 --- a/packages/cx/src/charts/helpers/index.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './PointReducer'; -export * from './MinMaxFinder'; -export * from './SnapPointFinder'; -export * from './ValueAtFinder'; diff --git a/packages/cx/src/charts/helpers/index.js b/packages/cx/src/charts/helpers/index.js deleted file mode 100644 index ff5a2097a..000000000 --- a/packages/cx/src/charts/helpers/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './PointReducer'; -export * from './MinMaxFinder'; -export * from './SnapPointFinder'; -export * from './ValueAtFinder'; diff --git a/packages/cx/src/charts/helpers/index.ts b/packages/cx/src/charts/helpers/index.ts new file mode 100644 index 000000000..b2838fb56 --- /dev/null +++ b/packages/cx/src/charts/helpers/index.ts @@ -0,0 +1,4 @@ +export * from "./PointReducer"; +export * from "./MinMaxFinder"; +export * from "./SnapPointFinder"; +export * from "./ValueAtFinder"; diff --git a/packages/cx/src/charts/index.d.ts b/packages/cx/src/charts/index.d.ts deleted file mode 100644 index d7ce86391..000000000 --- a/packages/cx/src/charts/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -export * from "./Chart"; -export * from "./PieChart"; -export * from "./Pie"; -export * from "./PieLabel"; -export * from "./PieLabelsContainer"; -export * from "./Column"; -export * from "./Bar"; -export * from "./Legend"; -export * from "./LegendEntry"; -export * from "./ColorMap"; -export * from "./Marker"; -export * from "./MarkerLine"; -export * from "./Range"; -export * from "./Gridlines"; -export * from "./Swimlanes"; -export * from "./LineGraph"; -export * from "./ColumnGraph"; -export * from "./BarGraph"; -export * from "./ScatterGraph"; -export * from "./BubbleGraph"; -export * from "./shapes"; -export * from "./MouseTracker"; -export * from "./RangeMarker"; -export * from "./Swimlane"; - -export * from "./axis/index"; -export * from "./helpers/index"; diff --git a/packages/cx/src/charts/index.js b/packages/cx/src/charts/index.ts similarity index 100% rename from packages/cx/src/charts/index.js rename to packages/cx/src/charts/index.ts diff --git a/packages/cx/src/charts/palette.scss b/packages/cx/src/charts/palette.scss index 1994fbb80..9abf7811b 100644 --- a/packages/cx/src/charts/palette.scss +++ b/packages/cx/src/charts/palette.scss @@ -3,6 +3,8 @@ //hover:100 700 //selected: 300 900 //disabled: 0 100 +@use "sass:map"; +@use "sass:list"; $cx-default-palette-colors: rgba(244, 67, 54, 1) rgba(233, 30, 99, 1) rgba(156, 39, 176, 1) rgba(103, 58, 183, 1) rgba(63, 81, 181, 1) rgba(33, 150, 243, 1) rgba(3, 169, 244, 1) rgba(0, 188, 212, 1) rgba(0, 150, 136, 1) @@ -36,12 +38,12 @@ $cx-default-palette-stroke-blacken: 10% !default; $palette-stroke-blacken: $cx-default-palette-stroke-blacken, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); - @for $i from 1 through length($palette-colors) { - $c: nth($palette-colors, 1 + (($i - 1) * 1) % length($palette-colors)); + @for $i from 1 through list.length($palette-colors) { + $c: list.nth($palette-colors, 1 + (($i - 1) * 1) % list.length($palette-colors)); .#{$state}color-#{$i - 1} { $fill: cx-blacken(cx-whiten($c, $palette-fill-whiten), $palette-fill-blacken); diff --git a/packages/cx/src/charts/shapes.d.ts b/packages/cx/src/charts/shapes.d.ts deleted file mode 100644 index 081ac0c47..000000000 --- a/packages/cx/src/charts/shapes.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Cx from '../core'; - -type ShapeRender = (cx: number, cy: number, size: number, props?: Cx.Config, options?: Cx.Config) => JSX.Element; - -export function registerShape(name: string, callback: (cx: number, cy: number, size: number) => any); - -export function getShape(shapeName: string): string; - -export function getAvailableShapes(): string[]; - -export const circle: ShapeRender; - -export const square: ShapeRender; - -export const bar: ShapeRender; - -export const column: ShapeRender; - -export const line: ShapeRender; - -export const vline: ShapeRender; - -export const triangle: ShapeRender; \ No newline at end of file diff --git a/packages/cx/src/charts/shapes.js b/packages/cx/src/charts/shapes.js deleted file mode 100644 index 36c65e42e..000000000 --- a/packages/cx/src/charts/shapes.js +++ /dev/null @@ -1,79 +0,0 @@ -import {VDOM} from '../ui/Widget'; -import {debug} from '../util/Debug'; - -var shapes = {}; -var warnings = {}; - -export function registerShape(name, callback) { - shapes[name] = callback; -} - -export function getShape(shapeName) { - if (shapes[shapeName]) - return shapes[shapeName]; - - if (!warnings[shapeName]) { - warnings[shapeName] = true; - debug(`Unknown shape '${shapeName}'. Using square instead.`); - } - - return shapes['square']; -} - -export function getAvailableShapes() { - return Object.keys(shapes); -} - -export function circle(cx, cy, size, props, options) { - return -} -registerShape('circle', circle); - -export function square(cx, cy, size, props, options) { - size *= 0.9; - return -} -registerShape('square', square); -registerShape('rect', square); - -export function bar(cx, cy, size, props, options) { - size *= 0.9; - return -} -registerShape('bar', bar); - -export function column(cx, cy, size, props, options) { - size *= 0.9; - return -} -registerShape('column', column); - -export function line(cx, cy, size, props, options) { - size *= 0.9; - return -} -registerShape('line', line); -registerShape('hline', line); - -export function vline(cx, cy, size, props, options) { - size *= 0.9; - return -} -registerShape('vline', vline); - - - -export function triangle(cx, cy, size, props, options) { - size *= 1.29; - var d = ''; - var cos = Math.cos(Math.PI / 6); - var sin = Math.sin(Math.PI / 6); - d += `M ${cx} ${cy - size/2} `; - d += `L ${cx + cos * size / 2} ${cy + sin * size / 2} `; - d += `L ${cx - cos * size / 2} ${cy + sin * size / 2} `; - d += `Z`; - return -} - -registerShape('triangle', triangle); - diff --git a/packages/cx/src/charts/shapes.tsx b/packages/cx/src/charts/shapes.tsx new file mode 100644 index 000000000..cb91087ad --- /dev/null +++ b/packages/cx/src/charts/shapes.tsx @@ -0,0 +1,86 @@ +/** @jsxImportSource react */ + +import { VDOM } from "../ui/Widget"; +import { debug } from "../util/Debug"; +import { Config } from "../ui/Prop"; + +export type ShapeRender = ( + cx: number, + cy: number, + size: number, + props?: Config, + options?: Config +) => React.ReactElement; + +var shapes: { [key: string]: ShapeRender } = {}; +var warnings: { [key: string]: boolean } = {}; + +export function registerShape(name: string, callback: ShapeRender): void { + shapes[name] = callback; +} + +export function getShape(shapeName: string): ShapeRender { + if (shapes[shapeName]) return shapes[shapeName]; + + if (!warnings[shapeName]) { + warnings[shapeName] = true; + debug(`Unknown shape '${shapeName}'. Using square instead.`); + } + + return shapes["square"]; +} + +export function getAvailableShapes(): string[] { + return Object.keys(shapes); +} + +export function circle(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + return ; +} +registerShape("circle", circle); + +export function square(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 0.9; + return ; +} +registerShape("square", square); +registerShape("rect", square); + +export function bar(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 0.9; + return ; +} +registerShape("bar", bar); + +export function column(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 0.9; + return ; +} +registerShape("column", column); + +export function line(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 0.9; + return ; +} +registerShape("line", line); +registerShape("hline", line); + +export function vline(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 0.9; + return ; +} +registerShape("vline", vline); + +export function triangle(cx: number, cy: number, size: number, props?: Config, options?: Config): React.ReactElement { + size *= 1.29; + var d = ""; + var cos = Math.cos(Math.PI / 6); + var sin = Math.sin(Math.PI / 6); + d += `M ${cx} ${cy - size / 2} `; + d += `L ${cx + (cos * size) / 2} ${cy + (sin * size) / 2} `; + d += `L ${cx - (cos * size) / 2} ${cy + (sin * size) / 2} `; + d += `Z`; + return ; +} + +registerShape("triangle", triangle); diff --git a/packages/cx/src/charts/variables.scss b/packages/cx/src/charts/variables.scss index e3f558f26..ae6b92a94 100644 --- a/packages/cx/src/charts/variables.scss +++ b/packages/cx/src/charts/variables.scss @@ -1,9 +1,10 @@ +@use "sass:map"; @import "axis/variables"; $cx-default-swimlanes-lane-background-color: #f1f1f1; $cx-default-range-marker-color: #696969; -$cx-dependencies: map-merge( +$cx-dependencies: map.merge( $cx-dependencies, ( "cx/charts/Bar": "cx/charts/palette", diff --git a/packages/cx/src/core.d.ts b/packages/cx/src/core.d.ts index 9e20f5b42..594325f31 100644 --- a/packages/cx/src/core.d.ts +++ b/packages/cx/src/core.d.ts @@ -2,80 +2,73 @@ export = Cx; export as namespace Cx; import * as React from "react"; - +import { Instance } from "./ui/Instance"; +import { RenderingContext } from "./ui/RenderingContext"; +import { AccessorChain as AccessorChainType } from "./data/createAccessorModelProxy"; +import { Selector as SelectorType } from "./data/Selector"; +import type { + Bind as BindType, + Tpl as TplType, + Expr as ExprType, + Binding as BindingType, + GetSet as GetSetType, + Prop as PropType, + StructuredSelector as StructuredSelectorType, + DataRecord, + Config as ConfigType, + StructuredProp as StructuredPropType, + StringProp as StringPropType, + StyleProp as StylePropType, + NumberProp as NumberPropType, + BooleanProp as BooleanPropType, + ClassProp as ClassPropType, + RecordsProp as RecordsPropType, + SortersProp as SortersPropType, + UnknownProp as UnknownPropType, + RecordAlias as RecordAliasType, + SortDirection as SortDirectionType, + Sorter as SorterType, + CollatorOptions as CollatorOptionsType, +} from "./ui/Prop"; + +/** @deprecated */ declare namespace Cx { - type Bind = { - bind: string; - defaultValue?: any; - throttle?: number; - debounce?: number; - }; - - type Tpl = { - tpl: string; - }; - - type Expr = { - expr: string; - set?: (value: any, instance?: any) => boolean; - throttle?: number; - debounce?: number; - }; - - type Binding = Bind | Tpl | Expr; - - type Selector = (data: any) => T; - - type GetSet = { - get: Selector; - set?: (value: T, instance?: any) => boolean; - throttle?: number; - debounce?: number; - }; - - interface StructuredSelector { - [prop: string]: Selector; - } + // Re-export AccessorChain type from createAccessorModelProxy + type AccessorChain = AccessorChainType; - interface AccessorChainMethods { - toString(): string; - valueOf(): string; - nameOf(): string; - } + // Re-export Selector type from data/Selector + type Selector = SelectorType; - type AccessorChainMap = { [prop in keyof M]: AccessorChain }; + // Re-export binding types from Prop.ts + type Bind = BindType; + type Tpl = TplType; + type Expr = ExprType; + type Binding = BindingType; + type GetSet = GetSetType; - type AccessorChain = { - toString(): string; - valueOf(): string; - nameOf(): string; - } & Omit, keyof AccessorChainMethods>; + // Re-export types from Prop.ts + type Prop = PropType; - type Prop = T | Binding | Selector | AccessorChain | GetSet; + interface StructuredSelector extends StructuredSelectorType {} - interface Record { - [prop: string]: any; - } + interface Record extends DataRecord {} - interface Config { - [prop: string]: any; - } + interface Config extends ConfigType {} - interface StructuredProp { - [prop: string]: Prop; - } + interface StructuredProp extends StructuredPropType {} - type StringProp = Prop; - type StyleProp = Prop | StructuredProp; - type NumberProp = Prop; - type BooleanProp = Prop; - type ClassProp = Prop | StructuredProp; - type RecordsProp = Prop; - type SortersProp = Prop; - type UnknownProp = Prop; + type StringProp = StringPropType; + type StyleProp = StylePropType; + type NumberProp = NumberPropType; + type BooleanProp = BooleanPropType; + type ClassProp = ClassPropType; + type RecordsProp = RecordsPropType; + type SortersProp = SortersPropType; + type UnknownProp = UnknownPropType; - type RecordAlias = string | { toString(): string }; + type RecordAlias = RecordAliasType; + /** @deprecated */ interface WidgetProps { /** Inner layout used to display children inside the widget. */ layout?: any; @@ -119,6 +112,7 @@ declare namespace Cx { onDestroy?(): void; } + /** @deprecated */ interface PureContainerProps extends WidgetProps { /** Keep whitespace in text based children. Default is `false`. See also `trimWhitespace`. */ ws?: boolean; @@ -138,6 +132,7 @@ declare namespace Cx { plainText?: boolean; } + /** @deprecated */ interface StyledContainerProps extends PureContainerProps { /** * Additional CSS classes to be applied to the element. @@ -158,6 +153,7 @@ declare namespace Cx { styles?: StyleProp; } + /** @deprecated */ interface HtmlElementProps extends StyledContainerProps { /** Id of the element */ id?: Cx.StringProp | Cx.NumberProp; @@ -168,143 +164,19 @@ declare namespace Cx { /** Tooltip configuration. */ tooltip?: StringProp | StructuredProp; - onMouseDown?: string | ((event: MouseEvent, instance: any) => void); - onMouseMove?: string | ((event: MouseEvent, instance: any) => void); - onMouseUp?: string | ((event: MouseEvent, instance: any) => void); - onTouchStart?: string | ((event: TouchEvent, instance: any) => void); - onTouchMove?: string | ((event: TouchEvent, instance: any) => void); - onTouchEnd?: string | ((event: TouchEvent, instance: any) => void); - onClick?: string | ((event: MouseEvent, instance: any) => void); - onContextMenu?: string | ((event: MouseEvent, instance: any) => void); - } - - type SortDirection = "ASC" | "DESC"; - - interface Sorter { - field?: string; - value?: (Record) => any; - direction: SortDirection; - } - - interface CollatorOptions { - localeMatcher?: "lookup" | "best fit"; - usage?: "sort" | "search"; - sensitivity?: "base" | "accent" | "case" | "variant"; - ignorePunctuation?: boolean; - numeric?: boolean; - caseFirst?: "upper" | "lower" | "false"; - } - - class Widget

{ - props: P; - state: any; - context: any; - refs: any; - - constructor(props: P); - - render(); - - setState(state: any); - - forceUpdate(); - - static create(typeAlias?: any, config?: Cx.Config, more?: Cx.Config): any; - } -} - -declare global { - namespace JSX { - interface IntrinsicElements { - cx: any; - } - - interface IntrinsicAttributes { - /** Inner layout used to display children inside the widget. */ - layout?: any; - - /** Outer (wrapper) layout used to display the widget in. */ - outerLayout?: any; - - /** Name of the ContentPlaceholder that should be used to display the widget. */ - putInto?: string; - - /** Name of the ContentPlaceholder that should be used to display the widget. */ - contentFor?: string; - - /** Controller. */ - controller?: any; - - /** Visibility of the widget. Defaults to `true`. */ - visible?: Cx.BooleanProp; - - /** Visibility of the widget. Defaults to `true`. */ - if?: Cx.BooleanProp; - - /** Appearance modifier. For example, mod="big" will add the CSS class `.cxm-big` to the block element. */ - mod?: Cx.StringProp | Cx.Prop | Cx.StructuredProp; - - /** Cache render output. Default is `true`. */ - memoize?: Cx.BooleanProp; - - /** Tooltip configuration. */ - tooltip?: Cx.StringProp | Cx.StructuredProp; - } + // onMouseDown?: string | ((event: MouseEvent, instance: any) => void); + // onMouseMove?: string | ((event: MouseEvent, instance: any) => void); + // onMouseUp?: string | ((event: MouseEvent, instance: any) => void); + // onTouchStart?: string | ((event: TouchEvent, instance: any) => void); + // onTouchMove?: string | ((event: TouchEvent, instance: any) => void); + // onTouchEnd?: string | ((event: TouchEvent, instance: any) => void); + // onClick?: string | ((event: MouseEvent, instance: any) => void); + // onContextMenu?: string | ((event: MouseEvent, instance: any) => void); } - interface JSON { - /* JSON doesn't support symbol keys, and number keys - * are coerced to strings, even in arrays */ - - /** - * Converts a JavaScript Object Notation (JSON) string into an object. - * @param text A valid JSON string. - * @param reviver A function that transforms the results. This function is called for each member of the object. - * If a member contains nested objects, the nested objects are transformed before the parent object is. - */ - parse(text: string, reviver?: (this: unknown, key: string, value: unknown) => unknown): unknown; + type SortDirection = SortDirectionType; - /** - * Converts a JavaScript value to a JavaScript Object Notation (JSON) string. - * @param value A JavaScript value, usually an object or array, to be converted. - * @param replacer A function that transforms the results. - * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. - */ - stringify( - text: unknown, - replacer?: (this: unknown, key: string, value: unknown) => unknown, - space?: string | number, - ): string; - } - - interface ArrayConstructor { - isArray(a: unknown): a is unknown[]; - } - - interface Body { - json(): Promise; - } -} - -declare module "react" { - interface ClassAttributes extends Cx.PureContainerProps { - class?: Cx.ClassProp; - styles?: Cx.StyleProp; - text?: Cx.StringProp | Cx.NumberProp; - innerText?: Cx.StringProp; - html?: Cx.StringProp; - innerHtml?: Cx.StringProp; - tooltip?: Cx.StringProp | Cx.StructuredProp; - } - namespace React { - interface ImgHTMLAttributes extends HTMLAttributes { - alt?: Cx.StringProp | undefined; - src?: Cx.StringProp | undefined; - } - } + interface Sorter extends SorterType {} - //this doesn't work, however, it would be nice if it does - // interface EventHandler> { - // (event: E, instance?: any): void; - // } + interface CollatorOptions extends CollatorOptionsType {} } diff --git a/packages/cx/src/data/AggregateFunction.d.ts b/packages/cx/src/data/AggregateFunction.d.ts deleted file mode 100644 index b9198bd1e..000000000 --- a/packages/cx/src/data/AggregateFunction.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -interface Aggregator { - process(value: number, weight?: number); - getResult(): number; -} - -export class AggregateFunction { - static sum(): Aggregator; - - static avg(): Aggregator; - - static count(): Aggregator; - - static distinct(): Aggregator; - - static min(): Aggregator; - - static max(): Aggregator; - - static last(): Aggregator; -} diff --git a/packages/cx/src/data/AggregateFunction.js b/packages/cx/src/data/AggregateFunction.js deleted file mode 100644 index f7dd57fa0..000000000 --- a/packages/cx/src/data/AggregateFunction.js +++ /dev/null @@ -1,145 +0,0 @@ -export class AggregateFunction { - static sum() { - return new Sum(); - } - - static avg() { - return new Avg(); - } - - static count() { - return new Count(); - } - - static distinct() { - return new Distinct(); - } - - static min() { - return new Min(); - } - - static max() { - return new Max(); - } - - static last() { - return new LastValue(); - } -} - -class Sum { - process(value) { - this.empty = false; - if (!isNaN(value)) this.result += value; - else this.invalid = true; - } - - getResult() { - if (this.invalid) return null; - return this.result; - } -} - -Sum.prototype.result = 0; -Sum.prototype.empty = true; - -class Avg { - process(value, count = 1) { - this.empty = false; - if (!isNaN(value) && !isNaN(count)) { - this.result += value * count; - this.count += count; - } else this.invalid = true; - } - - getResult() { - if (this.empty || this.invalid || this.count == 0) return null; - return this.result / this.count; - } -} - -Avg.prototype.result = 0; -Avg.prototype.count = 0; -Avg.prototype.empty = true; - -class Count { - process(value) { - if (value != null) this.result++; - } - - getResult() { - return this.result; - } -} - -Count.prototype.result = 0; - -class Distinct { - constructor() { - this.values = {}; - } - - process(value) { - if (value == null || this.values[value]) return; - this.values[value] = true; - this.empty = false; - this.result++; - } - - getResult() { - if (this.empty || this.invalid) return null; - return this.result; - } -} - -Distinct.prototype.result = 0; -Distinct.prototype.empty = true; - -class Max { - process(value) { - if (!isNaN(value)) { - if (this.empty) this.result = value; - else if (value > this.result) this.result = value; - this.empty = false; - } else if (value != null) this.invalid = true; - } - - getResult() { - if (this.empty || this.invalid) return null; - return this.result; - } -} - -Max.prototype.result = 0; -Max.prototype.empty = true; - -class Min { - process(value) { - if (!isNaN(value)) { - if (this.empty) this.result = value; - else if (value < this.result) this.result = value; - this.empty = false; - } else if (value != null) this.invalid = true; - } - - getResult() { - if (this.empty || this.invalid) return null; - return this.result; - } -} - -Min.prototype.result = 0; -Min.prototype.empty = true; - -class LastValue { - process(value) { - this.result = value; - } - - getResult() { - return this.result; - } -} - -LastValue.prototype.result = null; diff --git a/packages/cx/src/data/AggregateFunction.ts b/packages/cx/src/data/AggregateFunction.ts new file mode 100644 index 000000000..185cffaac --- /dev/null +++ b/packages/cx/src/data/AggregateFunction.ts @@ -0,0 +1,171 @@ +export class AggregateFunction { + static sum() { + return new Sum(); + } + + static avg() { + return new Avg(); + } + + static count() { + return new Count(); + } + + static distinct() { + return new Distinct(); + } + + static min() { + return new Min(); + } + + static max() { + return new Max(); + } + + static last() { + return new LastValue(); + } +} + +class Sum { + result: number = 0; + empty: boolean = true; + invalid?: boolean; + + process(value: number): void { + this.empty = false; + if (!isNaN(value)) this.result += value; + else this.invalid = true; + } + + getResult() { + if (this.invalid) return null; + return this.result; + } +} + +Sum.prototype.result = 0; +Sum.prototype.empty = true; + +class Avg { + result: number = 0; + count: number = 0; + empty: boolean = true; + invalid?: boolean; + + process(value: number, count: number = 1): void { + this.empty = false; + if (!isNaN(value) && !isNaN(count)) { + this.result += value * count; + this.count += count; + } else this.invalid = true; + } + + getResult() { + if (this.empty || this.invalid || this.count == 0) return null; + return this.result / this.count; + } +} + +Avg.prototype.result = 0; +Avg.prototype.count = 0; +Avg.prototype.empty = true; + +class Count { + result: number = 0; + + process(value: any): void { + if (value != null) this.result++; + } + + getResult() { + return this.result; + } +} + +Count.prototype.result = 0; + +class Distinct { + values: {[key: string]: boolean} = {}; + empty: boolean = true; + result: number = 0; + invalid?: boolean; + + constructor() { + this.values = {}; + } + + process(value: any): void { + if (value == null || this.values[value]) return; + this.values[value] = true; + this.empty = false; + this.result++; + } + + getResult() { + if (this.empty || this.invalid) return null; + return this.result; + } +} + +Distinct.prototype.result = 0; +Distinct.prototype.empty = true; + +class Max { + result: number = 0; + empty: boolean = true; + invalid?: boolean; + + process(value: number): void { + if (!isNaN(value)) { + if (this.empty) this.result = value; + else if (value > this.result) this.result = value; + this.empty = false; + } else if (value != null) this.invalid = true; + } + + getResult() { + if (this.empty || this.invalid) return null; + return this.result; + } +} + +Max.prototype.result = 0; +Max.prototype.empty = true; + +class Min { + result: number = 0; + empty: boolean = true; + invalid?: boolean; + + process(value: number): void { + if (!isNaN(value)) { + if (this.empty) this.result = value; + else if (value < this.result) this.result = value; + this.empty = false; + } else if (value != null) this.invalid = true; + } + + getResult() { + if (this.empty || this.invalid) return null; + return this.result; + } +} + +Min.prototype.result = 0; +Min.prototype.empty = true; + +class LastValue { + result: any = null; + + process(value: any): void { + this.result = value; + } + + getResult() { + return this.result; + } +} + +LastValue.prototype.result = null; diff --git a/packages/cx/src/data/ArrayElementView.d.ts b/packages/cx/src/data/ArrayElementView.d.ts deleted file mode 100644 index 2b5e494df..000000000 --- a/packages/cx/src/data/ArrayElementView.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {Accessor} from "./getAccessor"; -import {AugmentedViewBase} from "./AugmentedViewBase"; - -export abstract class ArrayElementView extends AugmentedViewBase { - arrayAccessor: Accessor; - immutable: boolean; - recordAlias: string; - indexAlias: string; - lengthAlias: string; - - setIndex(itemIndex: number): void; -} - diff --git a/packages/cx/src/data/ArrayElementView.js b/packages/cx/src/data/ArrayElementView.js deleted file mode 100644 index a1a262819..000000000 --- a/packages/cx/src/data/ArrayElementView.js +++ /dev/null @@ -1,64 +0,0 @@ -import { AugmentedViewBase } from "./AugmentedViewBase"; -import { isArray } from "../util/isArray"; -import { Binding } from "./Binding"; - -export class ArrayElementView extends AugmentedViewBase { - constructor(config) { - super(config); - this.hasNestedAliases = - this.recordAlias.indexOf(".") >= 0 || this.indexAlias.indexOf(".") >= 0 || this.lengthAlias.indexOf(".") >= 0; - this.recordBinding = Binding.get(this.recordAlias); - if (this.hasNestedAliases) { - this.indexBinding = Binding.get(this.indexAlias); - this.lengthAlias = Binding.get(this.lengthAlias); - } - } - - getExtraKeyBinding(key) { - if (!key.startsWith(this.recordAlias)) return null; - if (key.length == this.recordAlias.length || key[this.recordAlias.length] == ".") return this.recordBinding; - return null; - } - - deleteExtraKeyValue(key) { - if (key != this.recordAlias) throw new Error(`Field ${key} cannot be deleted.`); - const array = this.arrayAccessor.get(this.store.getData()); - if (!array) return false; - const newArray = [...array.slice(0, this.itemIndex), ...array.slice(this.itemIndex + 1)]; - return this.arrayAccessor.set(newArray, this.store); - } - - setExtraKeyValue(key, value) { - if (key != this.recordAlias) throw new Error(`Field ${key} is read-only.`); - const array = this.arrayAccessor.get(this.store.getData()); - if (!array || value === array[this.itemIndex]) return false; - const newArray = [...array.slice(0, this.itemIndex), value, ...array.slice(this.itemIndex + 1)]; - return this.arrayAccessor.set(newArray, this.store); - } - - embedAugmentData(result, parentStoreData) { - let array = this.arrayAccessor.get(parentStoreData); - if (!isArray(array)) return; - if (!this.hasNestedAliases) { - result[this.recordAlias] = array[this.itemIndex]; - result[this.indexAlias] = this.itemIndex; - result[this.lengthAlias] = array.length; - } else { - let copy = result; - copy = this.recordBinding.set(copy, array[this.itemIndex]); - copy = this.indexBinding.set(copy, this.itemIndex); - copy = this.lengthAlias.set(copy, array.length); - result[this.recordBinding.parts[0]] = copy[this.recordBinding.parts[0]]; - result[this.indexBinding.parts[0]] = copy[this.indexBinding.parts[0]]; - result[this.lengthAlias.parts[0]] = copy[this.lengthAlias.parts[0]]; - } - } - - setIndex(itemIndex) { - this.itemIndex = itemIndex; - } -} - -ArrayElementView.prototype.recordAlias = "$record"; -ArrayElementView.prototype.indexAlias = "$index"; -ArrayElementView.prototype.lengthAlias = "$length"; diff --git a/packages/cx/src/data/ArrayElementView.spec.js b/packages/cx/src/data/ArrayElementView.spec.js deleted file mode 100644 index cb2f50ec8..000000000 --- a/packages/cx/src/data/ArrayElementView.spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import assert from "assert"; -import { ArrayElementView } from "./ArrayElementView"; -import { getAccessor } from "./getAccessor"; -import { Store } from "./Store"; - -describe("ArrayElementView", function () { - it("exposes the element as under the $record alias", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); - let record = elementView.get("$record"); - assert.equal(record, letters[1]); - }); - - it("changes of the $record are propagated", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); - elementView.set("$record.letter", "C"); - assert.deepEqual(store.get("letters"), [letters[0], { letter: "C" }]); - }); - - it("removes the element if the $record is deleted", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); - elementView.delete("$record"); - assert.deepEqual(store.get("letters"), [...letters[0]]); - }); - - it("exposes the element as under the given alias", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ - store, - itemIndex: 1, - arrayAccessor: getAccessor({ bind: "letters" }), - recordAlias: "$letter", - }); - let record = elementView.get("$letter"); - assert.equal(record, letters[1]); - }); - - it("aliases allow nesting", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ - store, - itemIndex: 1, - arrayAccessor: getAccessor({ bind: "letters" }), - recordAlias: "$iter.letter", - indexAlias: "$iter.index.letter", - }); - let record = elementView.get("$iter.letter"); - assert.equal(record, letters[1]); - assert.equal(elementView.get("$iter.index.letter"), 1); - - elementView.set("$iter.letter.letter", "C"); - assert.equal(store.get("letters.1.letter"), "C"); - }); - - it("when immutable preserves the parent data object", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters } }); - let elementView = new ArrayElementView({ - store, - itemIndex: 1, - arrayAccessor: getAccessor({ bind: "letters" }), - immutable: true, - }); - let record = elementView.get("$record"); - assert.equal(record, letters[1]); - assert(!store.getData().hasOwnProperty("$record")); - }); - - it("respects the parent's store sealed flag", function () { - let letters = [{ letter: "A" }, { letter: "B" }]; - let store = new Store({ data: { letters }, sealed: true }); - let elementView = new ArrayElementView({ - store, - itemIndex: 1, - arrayAccessor: getAccessor({ bind: "letters" }), - }); - let record = elementView.get("$record"); - assert.equal(record, letters[1]); - assert(!store.getData().hasOwnProperty("$record")); - }); -}); diff --git a/packages/cx/src/data/ArrayElementView.spec.ts b/packages/cx/src/data/ArrayElementView.spec.ts new file mode 100644 index 000000000..573360d49 --- /dev/null +++ b/packages/cx/src/data/ArrayElementView.spec.ts @@ -0,0 +1,88 @@ +import assert from "assert"; +import { ArrayElementView } from "./ArrayElementView"; +import { getAccessor } from "./getAccessor"; +import { Store } from "./Store"; + +describe("ArrayElementView", function () { + it("exposes the element as under the $record alias", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); + let record = elementView.get("$record"); + assert.equal(record, letters[1]); + }); + + it("changes of the $record are propagated", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); + elementView.set("$record.letter", "C"); + assert.deepEqual(store.get("letters"), [letters[0], { letter: "C" }]); + }); + + it("removes the element if the $record is deleted", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ store, itemIndex: 1, arrayAccessor: getAccessor({ bind: "letters" }) }); + elementView.delete("$record"); + assert.deepEqual(store.get("letters"), [letters[0]]); + }); + + it("exposes the element as under the given alias", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ + store, + itemIndex: 1, + arrayAccessor: getAccessor({ bind: "letters" }), + recordAlias: "$letter", + }); + let record = elementView.get("$letter"); + assert.equal(record, letters[1]); + }); + + it("aliases allow nesting", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ + store, + itemIndex: 1, + arrayAccessor: getAccessor({ bind: "letters" }), + recordAlias: "$iter.letter", + indexAlias: "$iter.index.letter", + }); + let record = elementView.get("$iter.letter"); + assert.equal(record, letters[1]); + assert.equal(elementView.get("$iter.index.letter"), 1); + + elementView.set("$iter.letter.letter", "C"); + assert.equal(store.get("letters.1.letter"), "C"); + }); + + it("when immutable preserves the parent data object", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters } }); + let elementView = new ArrayElementView({ + store, + itemIndex: 1, + arrayAccessor: getAccessor({ bind: "letters" }), + immutable: true, + }); + let record = elementView.get("$record"); + assert.equal(record, letters[1]); + assert(!store.getData().hasOwnProperty("$record")); + }); + + it("respects the parent's store sealed flag", function () { + let letters = [{ letter: "A" }, { letter: "B" }]; + let store = new Store({ data: { letters }, sealed: true }); + let elementView = new ArrayElementView({ + store, + itemIndex: 1, + arrayAccessor: getAccessor({ bind: "letters" }), + }); + let record = elementView.get("$record"); + assert.equal(record, letters[1]); + assert(!store.getData().hasOwnProperty("$record")); + }); +}); diff --git a/packages/cx/src/data/ArrayElementView.ts b/packages/cx/src/data/ArrayElementView.ts new file mode 100644 index 000000000..ec9298309 --- /dev/null +++ b/packages/cx/src/data/ArrayElementView.ts @@ -0,0 +1,90 @@ +import { AugmentedViewBase } from "./AugmentedViewBase"; +import { isArray } from "../util/isArray"; +import { Binding } from "./Binding"; +import { View } from "./View"; + +export interface ArrayElementViewConfig { + store: View; + arrayAccessor: any; + immutable?: boolean; + recordAlias?: string; + indexAlias?: string; + lengthAlias?: string; + hasNestedAliases?: boolean; + recordBinding?: any; + indexBinding?: any; + lengthBinding?: any; + itemIndex: number; + sealed?: boolean; +} + +export class ArrayElementView extends AugmentedViewBase { + declare arrayAccessor: any; + declare recordAlias: string; + declare indexAlias: string; + declare lengthAlias: string; + declare hasNestedAliases?: boolean; + declare recordBinding?: any; + declare indexBinding?: any; + declare lengthBinding?: any; + declare itemIndex: number; + + constructor(config: ArrayElementViewConfig) { + super(config); + this.hasNestedAliases = + this.recordAlias.indexOf(".") >= 0 || this.indexAlias.indexOf(".") >= 0 || this.lengthAlias.indexOf(".") >= 0; + this.recordBinding = Binding.get(this.recordAlias); + if (this.hasNestedAliases) { + this.indexBinding = Binding.get(this.indexAlias); + this.lengthBinding = Binding.get(this.lengthAlias); + } + } + + getExtraKeyBinding(key: string): any { + if (!key.startsWith(this.recordAlias)) return null; + if (key.length == this.recordAlias.length || key[this.recordAlias.length] == ".") return this.recordBinding; + return null; + } + + deleteExtraKeyValue(key: string): boolean { + if (key != this.recordAlias) throw new Error(`Field ${key} cannot be deleted.`); + const array = this.arrayAccessor.get(this.store.getData()); + if (!array) return false; + const newArray = [...array.slice(0, this.itemIndex), ...array.slice(this.itemIndex + 1)]; + return this.arrayAccessor.set(newArray, this.store); + } + + setExtraKeyValue(key: string, value: any): boolean { + if (key != this.recordAlias) throw new Error(`Field ${key} is read-only.`); + const array = this.arrayAccessor.get(this.store.getData()); + if (!array || value === array[this.itemIndex]) return false; + const newArray = [...array.slice(0, this.itemIndex), value, ...array.slice(this.itemIndex + 1)]; + return this.arrayAccessor.set(newArray, this.store); + } + + embedAugmentData(result: any, parentStoreData: any): void { + let array = this.arrayAccessor.get(parentStoreData); + if (!isArray(array)) return; + if (!this.hasNestedAliases) { + result[this.recordAlias] = array[this.itemIndex]; + result[this.indexAlias] = this.itemIndex; + result[this.lengthAlias] = array.length; + } else { + let copy = result; + copy = this.recordBinding.set(copy, array[this.itemIndex]); + copy = this.indexBinding.set(copy, this.itemIndex); + copy = this.lengthBinding.set(copy, array.length); + result[this.recordBinding.parts[0]] = copy[this.recordBinding.parts[0]]; + result[this.indexBinding.parts[0]] = copy[this.indexBinding.parts[0]]; + result[this.lengthBinding.parts[0]] = copy[this.lengthBinding.parts[0]]; + } + } + + setIndex(itemIndex: number): void { + this.itemIndex = itemIndex; + } +} + +ArrayElementView.prototype.recordAlias = "$record"; +ArrayElementView.prototype.indexAlias = "$index"; +ArrayElementView.prototype.lengthAlias = "$length"; diff --git a/packages/cx/src/data/ArrayRef.d.ts b/packages/cx/src/data/ArrayRef.d.ts deleted file mode 100644 index b4b906e59..000000000 --- a/packages/cx/src/data/ArrayRef.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Ref} from "./Ref"; - -export class ArrayRef extends Ref { - append(...args: T[]); - - insert(index, ...args: T[]); - - filter(predicate: (item: T, index?: number) => boolean); - - move(fromIndex: number, toIndex: number); - - clear(); - - sort(compare: (a: T, b: T) => number); -} \ No newline at end of file diff --git a/packages/cx/src/data/ArrayRef.js b/packages/cx/src/data/ArrayRef.js deleted file mode 100644 index e7fd89895..000000000 --- a/packages/cx/src/data/ArrayRef.js +++ /dev/null @@ -1,35 +0,0 @@ -import {Ref} from "./Ref"; -import {append} from "./ops/append"; -import {moveElement} from "./ops/moveElement"; -import {insertElement} from "./ops/insertElement"; - -export class ArrayRef extends Ref { - append(...args) { - this.update(append, ...args); - } - - insert(index, ...args) { - this.update(insertElement, ...args) - } - - filter(predicate) { - this.update(array => array.filter(a => predicate(a))); - } - - move(fromIndex, toIndex) { - this.update(moveElement, fromIndex, toIndex); - } - - clear() { - this.set([]); - } - - sort(compare) { - let data = this.get(); - if (!data) - return; - let x = [...data]; - x.sort(compare); - this.set(x); - } -} \ No newline at end of file diff --git a/packages/cx/src/data/ArrayRef.ts b/packages/cx/src/data/ArrayRef.ts new file mode 100644 index 000000000..4ad066c6f --- /dev/null +++ b/packages/cx/src/data/ArrayRef.ts @@ -0,0 +1,35 @@ +import {Ref} from "./Ref"; +import {append} from "./ops/append"; +import {moveElement} from "./ops/moveElement"; +import {insertElement} from "./ops/insertElement"; + +export class ArrayRef extends Ref { + append(...args: T[]): void { + this.update(append, ...args); + } + + insert(index: number, ...args: T[]): void { + this.update(insertElement, ...args) + } + + filter(predicate: (item: T, index?: number) => boolean): void { + this.update(array => array.filter(a => predicate(a))); + } + + move(fromIndex: number, toIndex: number): void { + this.update(moveElement, fromIndex, toIndex); + } + + clear(): void { + this.set([]); + } + + sort(compare: (a: T, b: T) => number): void { + let data = this.get(); + if (!data) + return; + let x = [...data]; + x.sort(compare); + this.set(x); + } +} \ No newline at end of file diff --git a/packages/cx/src/data/AugmentedViewBase.d.ts b/packages/cx/src/data/AugmentedViewBase.d.ts deleted file mode 100644 index abe08e8ed..000000000 --- a/packages/cx/src/data/AugmentedViewBase.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { View, ViewConfig } from "./View"; -import * as Cx from "../core"; - -export interface AugmentedViewBaseConfig extends ViewConfig { - immutable?: boolean; -} - -export abstract class AugmentedViewBase extends View { - constructor(config: AugmentedViewBaseConfig); - - protected abstract setExtraKeyValue(key: string, value: any): boolean; - - protected abstract deleteExtraKeyValue(key: string): boolean; - - protected abstract isExtraKey(key: string): boolean; - - protected abstract embedAugmentData(result: Cx.Record, parentStoreData: Cx.Record): void; -} diff --git a/packages/cx/src/data/AugmentedViewBase.js b/packages/cx/src/data/AugmentedViewBase.js deleted file mode 100644 index cf3acaabc..000000000 --- a/packages/cx/src/data/AugmentedViewBase.js +++ /dev/null @@ -1,77 +0,0 @@ -import { View } from "./View"; -import { Binding } from "./Binding"; - -export class AugmentedViewBase extends View { - getData() { - if (this.sealed && this.meta.version === this.cache.version && this.meta === this.store.meta) - return this.cache.result; - let parentStoreData = this.store.getData(); - let result = this.getBaseData(parentStoreData); - this.embedAugmentData(result, parentStoreData); - this.cache.result = result; - this.cache.parentStoreData = parentStoreData; - this.cache.version = this.meta.version; - this.meta = this.store.meta; - return this.cache.result; - } - - getBaseData(parentStoreData) { - if (this.sealed || this.immutable || this.store.sealed) return { ...parentStoreData }; - return parentStoreData; - } - - embedAugmentData(result, parentStoreData) { - throw new Error("abstract"); - } - - isExtraKey(key) { - throw new Error("abstract"); - } - - // Stores which need to support nested aliases should override this method - getExtraKeyBinding(key) { - let binding = Binding.get(key); - return this.isExtraKey(binding.parts[0]) ? Binding.get(binding.parts[0]) : null; - } - - setExtraKeyValue(key, value) { - throw new Error("abstract"); - } - - deleteExtraKeyValue(key) { - throw new Error("abstract"); - } - - setItem(path, value) { - let extraKeyBinding = this.getExtraKeyBinding(path); - if (extraKeyBinding) { - let binding = Binding.get(path); - let newValue = value; - if (binding.parts.length > extraKeyBinding.parts.length) { - let data = {}; - this.embedAugmentData(data, this.store.getData()); - let binding = Binding.get(path); - data = binding.set(data, value); - newValue = extraKeyBinding.value(data); - } - return this.setExtraKeyValue(extraKeyBinding.path, newValue); - } - return super.setItem(path, value); - } - - deleteItem(path) { - let extraKeyBinding = this.getExtraKeyBinding(path); - if (extraKeyBinding) { - if (path == extraKeyBinding.path) return this.deleteExtraKeyValue(extraKeyBinding.path); - let data = {}; - this.embedAugmentData(data, this.store.getData()); - let binding = Binding.get(path); - data = binding.delete(data); - let newValue = extraKeyBinding.value(data); - return this.setExtraKeyValue(extraKeyBinding.path, newValue); - } - return super.deleteItem(path); - } -} - -AugmentedViewBase.prototype.immutable = false; diff --git a/packages/cx/src/data/AugmentedViewBase.ts b/packages/cx/src/data/AugmentedViewBase.ts new file mode 100644 index 000000000..247d6c7a5 --- /dev/null +++ b/packages/cx/src/data/AugmentedViewBase.ts @@ -0,0 +1,88 @@ +import { View, ViewConfig } from "./View"; +import { Binding } from "./Binding"; + +export interface AugmentedViewBaseConfig extends ViewConfig { + store: View; +} + +export class AugmentedViewBase extends View { + declare immutable: boolean; + declare store: View; + + constructor(config: AugmentedViewBaseConfig) { + super(config); + } + + getData() { + if (this.sealed && this.meta.version === this.cache.version && this.meta === this.store.meta) + return this.cache.result; + let parentStoreData = this.store.getData(); + let result = this.getBaseData(parentStoreData); + this.embedAugmentData(result, parentStoreData); + this.cache.result = result; + this.cache.parentStoreData = parentStoreData; + this.cache.version = this.meta.version; + this.meta = this.store.meta; + return this.cache.result; + } + + protected getBaseData(parentStoreData: any): any { + if (this.sealed || this.immutable || this.store.sealed) return { ...parentStoreData }; + return parentStoreData; + } + + protected embedAugmentData(result: any, parentStoreData: any): void { + throw new Error("abstract"); + } + + protected isExtraKey(key: string): boolean { + throw new Error("abstract"); + } + + // Stores which need to support nested aliases should override this method + protected getExtraKeyBinding(key: string): any { + let binding = Binding.get(key); + return this.isExtraKey(binding.parts[0]) ? Binding.get(binding.parts[0]) : null; + } + + protected setExtraKeyValue(key: string, value: any): boolean { + throw new Error("abstract"); + } + + protected deleteExtraKeyValue(key: string): boolean { + throw new Error("abstract"); + } + + setItem(path: string, value: any): boolean { + let extraKeyBinding = this.getExtraKeyBinding(path); + if (extraKeyBinding) { + let binding = Binding.get(path); + let newValue = value; + if (binding.parts.length > extraKeyBinding.parts.length) { + let data = {}; + this.embedAugmentData(data, this.store.getData()); + let binding = Binding.get(path); + data = binding.set(data, value); + newValue = extraKeyBinding.value(data); + } + return this.setExtraKeyValue(extraKeyBinding.path, newValue); + } + return super.setItem(path, value); + } + + deleteItem(path: string): boolean { + let extraKeyBinding = this.getExtraKeyBinding(path); + if (extraKeyBinding) { + if (path == extraKeyBinding.path) return this.deleteExtraKeyValue(extraKeyBinding.path); + let data = {}; + this.embedAugmentData(data, this.store.getData()); + let binding = Binding.get(path); + data = binding.delete(data); + let newValue = extraKeyBinding.value(data); + return this.setExtraKeyValue(extraKeyBinding.path, newValue); + } + return super.deleteItem(path); + } +} + +AugmentedViewBase.prototype.immutable = false; diff --git a/packages/cx/src/data/Binding.d.ts b/packages/cx/src/data/Binding.d.ts deleted file mode 100644 index 9fbc5d3ff..000000000 --- a/packages/cx/src/data/Binding.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Record, AccessorChain, Bind } from "../core"; - -export class Binding { - constructor(path: string); - - readonly path: string; - - set(state: Record, value: V): Record; - - delete(state: Record): Record; - - value(state: Record): any; - - static get(path: string): Binding; - static get(bind: Bind): Binding; - static get(chain: AccessorChain): Binding; -} - -export function isBinding(value: any): boolean; diff --git a/packages/cx/src/data/Binding.js b/packages/cx/src/data/Binding.js deleted file mode 100644 index 45f9d4da1..000000000 --- a/packages/cx/src/data/Binding.js +++ /dev/null @@ -1,76 +0,0 @@ -let bindingCache = {}; -import { isString } from "../util/isString"; -import { isObject } from "../util/isObject"; -import { isAccessorChain } from "./createAccessorModelProxy"; - -export class Binding { - constructor(path) { - this.path = path; - this.parts = path.split("."); - let body = "return x"; - for (let i = 0; i < this.parts.length; i++) body += '?.["' + this.parts[i] + '"]'; - this.value = new Function("x", body); - } - - set(state, value) { - let cv = this.value(state); - if (cv === value) return state; - - let ns = Object.assign({}, state); - let o = ns; - - for (let i = 0; i < this.parts.length; i++) { - let part = this.parts[i]; - let no = i == this.parts.length - 1 ? value : Object.assign({}, o[part]); - o[part] = no; - o = no; - } - - return ns; - } - - delete(state) { - let ns = Object.assign({}, state); - let o = ns; - let part; - - for (let i = 0; i < this.parts.length - 1; i++) { - part = this.parts[i]; - let no = Object.assign({}, o[part]); - o[part] = no; - o = no; - } - - part = this.parts[this.parts.length - 1]; - if (!o.hasOwnProperty(part)) return state; - - delete o[part]; - - return ns; - } - - static get(path) { - if (isString(path)) { - let b = bindingCache[path]; - if (b) return b; - - b = new Binding(path); - bindingCache[path] = b; - return b; - } - - if (isObject(path) && isString(path.bind)) return this.get(path.bind); - - if (path instanceof Binding) return path; - - if (isAccessorChain(path)) return this.get(path.toString()); - - throw new Error("Invalid binding definition provided."); - } -} - -export function isBinding(value) { - if (isObject(value) && isString(value.bind)) return true; - if (value && value.isAccessorChain) return true; - return value instanceof Binding; -} diff --git a/packages/cx/src/data/Binding.spec.js b/packages/cx/src/data/Binding.spec.ts similarity index 100% rename from packages/cx/src/data/Binding.spec.js rename to packages/cx/src/data/Binding.spec.ts diff --git a/packages/cx/src/data/Binding.ts b/packages/cx/src/data/Binding.ts new file mode 100644 index 000000000..72f366bae --- /dev/null +++ b/packages/cx/src/data/Binding.ts @@ -0,0 +1,104 @@ +import { isString } from "../util/isString"; +import { isObject } from "../util/isObject"; +import { isAccessorChain } from "./createAccessorModelProxy"; +import { AccessorChain } from "./createAccessorModelProxy"; +import { hasStringAtKey, hasValueAtKey } from "../util/hasKey"; + +let bindingCache: Record> = {}; + +export interface BindingObject { + bind: string; + defaultValue?: any; + throttle?: number; + debounce?: number; +} + +export type BindingInput = string | BindingObject | Binding | AccessorChain; + +export type ValueGetter = (state: any) => T | undefined; + +export class Binding { + public readonly path: string; + public readonly parts: readonly string[]; + public readonly value: ValueGetter; + + constructor(path: string) { + this.path = path; + this.parts = path.split(".") as readonly string[]; + let body = "return x"; + for (let i = 0; i < this.parts.length; i++) body += '?.["' + this.parts[i] + '"]'; + this.value = new Function("x", body) as ValueGetter; + } + + set = any>(state: S, value: T): S { + const cv = this.value(state); + if (cv === value) return state; + + const ns = Object.assign({}, state) as S; + let o: any = ns; + + for (let i = 0; i < this.parts.length; i++) { + const part = this.parts[i]; + const no = i === this.parts.length - 1 ? value : Object.assign({}, o[part]); + o[part] = no; + o = no; + } + + return ns; + } + + delete = any>(state: S): S { + const ns = Object.assign({}, state) as S; + let o: any = ns; + let part: string; + + for (let i = 0; i < this.parts.length - 1; i++) { + part = this.parts[i]; + const no = Object.assign({}, o[part]); + o[part] = no; + o = no; + } + + part = this.parts[this.parts.length - 1]; + if (!o.hasOwnProperty(part)) return state; + + delete o[part]; + + return ns; + } + + static get(path: BindingInput): Binding { + if (isString(path)) { + let b = bindingCache[path] as Binding | undefined; + if (b) return b; + + b = new Binding(path); + bindingCache[path] = b; + return b; + } + + if (isBindingObject(path)) return this.get(path.bind); + + if (path instanceof Binding) return path as Binding; + + if (isAccessorChain(path)) return this.get((path as AccessorChain).toString()); + + throw new Error("Invalid binding definition provided."); + } +} + +export function isBinding(value: unknown): value is BindingInput { + if (isObject(value)) { + if (hasStringAtKey(value, "bind")) return true; + if (hasValueAtKey(value, "isAccessorChain", true)) return true; + } + return value instanceof Binding; +} + +export type BindingValue = B extends Binding ? T : unknown; + +export function isBindingObject(value: unknown): value is BindingObject { + return isObject(value) && hasStringAtKey(value, "bind"); +} + +export { bindingCache }; diff --git a/packages/cx/src/data/ExposedRecordView.d.ts b/packages/cx/src/data/ExposedRecordView.d.ts deleted file mode 100644 index 29a170d0f..000000000 --- a/packages/cx/src/data/ExposedRecordView.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {View, ViewConfig} from './View'; - -interface ExposedRecordViewConfig extends ViewConfig { - itemIndex?: number; - immutable?: boolean; -} - -export class ExposedRecordView extends View { - constructor(config?: ExposedRecordViewConfig); - - setIndex(index: number); - - setStore(store: View); -} diff --git a/packages/cx/src/data/ExposedRecordView.js b/packages/cx/src/data/ExposedRecordView.js deleted file mode 100644 index e84642b8a..000000000 --- a/packages/cx/src/data/ExposedRecordView.js +++ /dev/null @@ -1,75 +0,0 @@ -import { View } from "./View"; -import { Binding } from "./Binding"; - -export class ExposedRecordView extends View { - getData() { - if ( - this.sealed && - this.meta.version === this.cache.version && - this.cache.itemIndex === this.itemIndex && - this.meta === this.store.meta - ) - return this.cache.result; - - this.cache.result = this.embed(this.store.getData()); - this.cache.version = this.meta.version; - this.cache.itemIndex = this.itemIndex; - this.meta = this.store.meta; - return this.cache.result; - } - - embed(data) { - const collection = this.collectionBinding.value(data); - const record = collection[this.itemIndex]; - const copy = this.sealed || this.immutable || this.store.sealed ? { ...data } : data; - copy[this.recordName] = record; - if (this.indexName) copy[this.indexName] = this.itemIndex; - return copy; - } - - setIndex(index) { - this.itemIndex = index; - } - - setItem(path, value) { - if (path == this.recordName || path.indexOf(this.recordName + ".") == 0) { - const storeData = this.store.getData(); - const collection = this.collectionBinding.value(storeData); - const data = this.embed(storeData); - const d = Binding.get(path).set(data, value); - if (d === data) return false; - const record = d[this.recordName]; - const newCollection = [ - ...collection.slice(0, this.itemIndex), - record, - ...collection.slice(this.itemIndex + 1), - ]; - return this.store.setItem(this.collectionBinding.path, newCollection); - } - return this.store.setItem(path, value); - } - - deleteItem(path) { - let storeData, collection, newCollection; - - if (path == this.recordName) { - storeData = this.store.getData(); - collection = this.collectionBinding.value(storeData); - newCollection = [...collection.slice(0, this.itemIndex), ...collection.slice(this.itemIndex + 1)]; - return this.store.setItem(this.collectionBinding.path, newCollection); - } else if (path.indexOf(this.recordName + ".") == 0) { - storeData = this.store.getData(); - collection = this.collectionBinding.value(storeData); - const data = this.embed(storeData); - const d = Binding.get(path).delete(data); - if (d === data) return false; - const record = d[this.recordName]; - newCollection = [...collection.slice(0, this.itemIndex), record, ...collection.slice(this.itemIndex + 1)]; - return this.store.setItem(this.collectionBinding.path, newCollection); - } - - return this.store.deleteItem(path); - } -} - -ExposedRecordView.prototype.immutable = false; diff --git a/packages/cx/src/data/ExposedRecordView.ts b/packages/cx/src/data/ExposedRecordView.ts new file mode 100644 index 000000000..175a18952 --- /dev/null +++ b/packages/cx/src/data/ExposedRecordView.ts @@ -0,0 +1,95 @@ +import { View } from "./View"; +import { Binding } from "./Binding"; + +export interface ExposedRecordViewConfig { + store: View; + itemIndex: number; + immutable?: boolean; + collectionBinding: any; + recordName: string; + indexName: string; +} + +export class ExposedRecordView extends View { + declare store: View; + itemIndex: number; + immutable: boolean; + collectionBinding: Binding; + recordName: string; + indexName: string; + + constructor(config: ExposedRecordViewConfig) { + super(config); + } + + getData(): any { + if ( + this.sealed && + this.meta.version === this.cache.version && + this.cache.itemIndex === this.itemIndex && + this.meta === this.store.meta + ) + return this.cache.result; + + this.cache.result = this.embed(this.store.getData()); + this.cache.version = this.meta.version; + this.cache.itemIndex = this.itemIndex; + this.meta = this.store.meta; + return this.cache.result; + } + + embed(data: any) { + const collection = this.collectionBinding.value(data); + const record = collection[this.itemIndex]; + const copy = this.sealed || this.immutable || this.store.sealed ? { ...data } : data; + copy[this.recordName] = record; + if (this.indexName) copy[this.indexName] = this.itemIndex; + return copy; + } + + setIndex(index: number) { + this.itemIndex = index; + } + + setItem(path: string, value: any) { + if (path == this.recordName || path.indexOf(this.recordName + ".") == 0) { + const storeData = this.store.getData(); + const collection = this.collectionBinding.value(storeData); + const data = this.embed(storeData); + const d = Binding.get(path).set(data, value); + if (d === data) return false; + const record = d[this.recordName]; + const newCollection = [ + ...collection.slice(0, this.itemIndex), + record, + ...collection.slice(this.itemIndex + 1), + ]; + return this.store.setItem(this.collectionBinding.path, newCollection); + } + return this.store.setItem(path, value); + } + + deleteItem(path: string) { + let storeData, collection, newCollection; + + if (path == this.recordName) { + storeData = this.store.getData(); + collection = this.collectionBinding.value(storeData); + newCollection = [...collection.slice(0, this.itemIndex), ...collection.slice(this.itemIndex + 1)]; + return this.store.setItem(this.collectionBinding.path, newCollection); + } else if (path.indexOf(this.recordName + ".") == 0) { + storeData = this.store.getData(); + collection = this.collectionBinding.value(storeData); + const data = this.embed(storeData); + const d = Binding.get(path).delete(data); + if (d === data) return false; + const record = d[this.recordName]; + newCollection = [...collection.slice(0, this.itemIndex), record, ...collection.slice(this.itemIndex + 1)]; + return this.store.setItem(this.collectionBinding.path, newCollection); + } + + return this.store.deleteItem(path); + } +} + +ExposedRecordView.prototype.immutable = false; diff --git a/packages/cx/src/data/ExposedValueView.d.ts b/packages/cx/src/data/ExposedValueView.d.ts deleted file mode 100644 index 5636a001d..000000000 --- a/packages/cx/src/data/ExposedValueView.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { View, ViewConfig } from "./View"; - -import { Binding } from "./Binding"; - -interface ExposedValueViewConfig extends ViewConfig { - containerBinding: Binding; - recordName?: string; - immutable?: boolean; -} - -export class ExposedValueView extends View { - constructor(config?: ExposedValueViewConfig); - - setKey(key: string); - - getKey(): string; - - setStore(store: View); -} diff --git a/packages/cx/src/data/ExposedValueView.js b/packages/cx/src/data/ExposedValueView.js deleted file mode 100644 index e6e78c003..000000000 --- a/packages/cx/src/data/ExposedValueView.js +++ /dev/null @@ -1,73 +0,0 @@ -import { View } from "./View"; -import { Binding } from "./Binding"; - -export class ExposedValueView extends View { - getData() { - if ( - this.sealed && - this.meta.version === this.cache.version && - this.cache.key === this.key && - this.meta == this.store.meta - ) - return this.cache.result; - - let data = this.store.getData(); - let container = this.containerBinding.value(data) || {}; - let record = container[this.key]; - - this.cache.version = this.meta.version; - this.cache.key = this.key; - this.cache.result = this.sealed || this.immutable || this.store.sealed ? { ...data } : data; - this.cache.result[this.recordName] = record; - this.meta = this.store.meta; - return this.cache.result; - } - - setKey(key) { - this.key = key; - } - - getKey() { - return this.key; - } - - setItem(path, value) { - if (path == this.recordName || path.indexOf(this.recordName + ".") == 0) { - var data = this.getData(); - var d = Binding.get(path).set(data, value); - if (d === data) return false; - var container = this.containerBinding.value(d); - var record = d[this.recordName]; - var newContainer = Object.assign({}, container); - newContainer[this.key] = record; - return this.store.setItem(this.containerBinding.path, newContainer); - } - return this.store.setItem(path, value); - } - - deleteItem(path) { - var data, container, newContainer; - - if (path == this.recordName) { - data = this.getData(); - container = this.containerBinding.value(data); - if (!container || !container.hasOwnProperty(path)) return false; - newContainer = Object.assign({}, container); - delete newContainer[this.key]; - this.store.set(this.containerBinding.path, newContainer); - } else if (path.indexOf(this.recordName + ".") == 0) { - data = this.getData(); - var d = Binding.get(path).delete(data); - if (d === data) return false; - container = this.containerBinding.value(d); - var record = d[this.recordName]; - newContainer = Object.assign({}, container); - newContainer[this.key] = record; - return this.store.setItem(this.containerBinding.path, newContainer); - } - - return this.store.deleteItem(path); - } -} - -ExposedValueView.prototype.immutable = false; diff --git a/packages/cx/src/data/ExposedValueView.ts b/packages/cx/src/data/ExposedValueView.ts new file mode 100644 index 000000000..5e105be71 --- /dev/null +++ b/packages/cx/src/data/ExposedValueView.ts @@ -0,0 +1,89 @@ +import { View, ViewConfig } from "./View"; +import { Binding } from "./Binding"; + +export interface ExposedValueViewConfig extends ViewConfig { + containerBinding: Binding; + key?: string | null; + recordName?: string; +} + +export class ExposedValueView extends View { + declare key: string; + declare containerBinding: Binding; + declare recordName: string; + declare immutable?: boolean; + declare store: View; + + constructor(config: ExposedValueViewConfig) { + super(config); + } + + getData(): any { + if ( + this.sealed && + this.meta.version === this.cache.version && + this.cache.key === this.key && + this.meta == this.store.meta + ) + return this.cache.result; + + let data = this.store.getData(); + let container = this.containerBinding.value(data) || {}; + let record = container[this.key]; + + this.cache.version = this.meta.version; + this.cache.key = this.key; + this.cache.result = this.sealed || this.immutable || this.store.sealed ? { ...data } : data; + this.cache.result[this.recordName] = record; + this.meta = this.store.meta; + return this.cache.result; + } + + setKey(key: string) { + this.key = key; + } + + getKey() { + return this.key; + } + + setItem(path: string, value: any) { + if (path == this.recordName || path.indexOf(this.recordName + ".") == 0) { + var data = this.getData(); + var d = Binding.get(path).set(data, value); + if (d === data) return false; + var container = this.containerBinding.value(d); + var record = d[this.recordName]; + var newContainer = Object.assign({}, container); + newContainer[this.key] = record; + return this.store.setItem(this.containerBinding.path, newContainer); + } + return this.store.setItem(path, value); + } + + deleteItem(path: string) { + var data, container, newContainer; + + if (path == this.recordName) { + data = this.getData(); + container = this.containerBinding.value(data); + if (!container || !container.hasOwnProperty(path)) return false; + newContainer = Object.assign({}, container); + delete newContainer[this.key]; + this.store.set(this.containerBinding.path, newContainer); + } else if (path.indexOf(this.recordName + ".") == 0) { + data = this.getData(); + var d = Binding.get(path).delete(data); + if (d === data) return false; + container = this.containerBinding.value(d); + var record = d[this.recordName]; + newContainer = Object.assign({}, container); + newContainer[this.key] = record; + return this.store.setItem(this.containerBinding.path, newContainer); + } + + return this.store.deleteItem(path); + } +} + +ExposedValueView.prototype.immutable = false; diff --git a/packages/cx/src/data/Expression.d.ts b/packages/cx/src/data/Expression.d.ts deleted file mode 100644 index d0c083414..000000000 --- a/packages/cx/src/data/Expression.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Selector } from "../core"; - -export function expression(str: string): Selector; - -export class Expression { - static get(str: string): Selector; - - static compile(str: string): Selector; - - static registerHelper(name: string, helper); - - static expandFatArrows: boolean; -} - -export function invalidateExpressionCache(): void; - -export function setGetExpressionCacheCallback(callback: () => {}): void; diff --git a/packages/cx/src/data/Expression.js b/packages/cx/src/data/Expression.js deleted file mode 100644 index 84803d0a9..000000000 --- a/packages/cx/src/data/Expression.js +++ /dev/null @@ -1,229 +0,0 @@ -import { computable } from "./computable"; -import { Format } from "../util/Format"; -import { Binding } from "./Binding"; - -import { quoteStr } from "../util/quote"; -import { isFunction } from "../util/isFunction"; -import { isValidIdentifierName } from "../util/isValidIdentifierName"; - -/* - Helper usage example - - Expression.registerHelper('_', _); - let e = Expression.compile('_.min({data})'); - */ - -let helpers = {}, - helperNames = [], - helperValues = [], - expFatArrows = null; - -function getExpr(expr) { - if (expr.memoize) return expr; - - function memoize() { - let lastValue, - lastRunBindings = {}, - lastRunResults = {}, - getters = {}, - currentData, - len = -1; - - let get = function (bindingWithFormat) { - let getter = getters[bindingWithFormat]; - if (!getter) { - let binding = bindingWithFormat, - format; - let colonIndex = bindingWithFormat.indexOf(":"); - if (colonIndex != -1) { - format = Format.parse(bindingWithFormat.substring(colonIndex + 1)); - binding = bindingWithFormat.substring(0, colonIndex); - } else { - let nullSeparatorIndex = bindingWithFormat.indexOf(":"); - if (nullSeparatorIndex != -1) { - format = Format.parse(bindingWithFormat.substring(nullSeparatorIndex)); - binding = bindingWithFormat.substring(0, nullSeparatorIndex - 1); - } - } - let b = Binding.get(binding); - getter = (data) => { - let value = b.value(data); - lastRunBindings[len] = b.value; - lastRunResults[len] = value; - len++; - return value; - }; - - if (format) { - let valueGetter = getter; - getter = (data) => format(valueGetter(data)); - } - - getters[bindingWithFormat] = getter; - } - return getter(currentData); - }; - - return function (data) { - let i = 0; - for (; i < len; i++) if (lastRunBindings[i](data) !== lastRunResults[i]) break; - if (i !== len) { - len = 0; - currentData = data; - lastValue = expr(get); - } - return lastValue; - }; - } - - let result = memoize(); - result.memoize = memoize; - return result; -} - -export function expression(str) { - if (isFunction(str)) return getExpr(str); - - let cache = getExpressionCache(); - let r = cache[str]; - if (r) return r; - - let quote = false; - - let termStart = -1, - curlyBrackets = 0, - percentExpression; - - let fb = ["return ("]; - - let args = {}; - let formats = []; - let subExprCount = 0; - let invalidNameCount = 0; - - for (let i = 0; i < str.length; i++) { - let c = str[i]; - switch (c) { - case "{": - if (curlyBrackets > 0 && !quote) curlyBrackets++; - else if (!quote && termStart < 0 && (str[i + 1] != "{" || str[i - 1] == "%")) { - termStart = i + 1; - curlyBrackets = 1; - percentExpression = str[i - 1] == "%"; - if (percentExpression) fb.pop(); //% - } else if (termStart < 0 && (quote || str[i - 1] != "{")) fb.push(c); - break; - - case "}": - if (termStart >= 0) { - if (quote) continue; - if (--curlyBrackets == 0) { - let term = str.substring(termStart, i); - let formatStart = 0; - if (term[0] == "[") formatStart = term.indexOf("]"); - let colon = term.indexOf(":", formatStart > 0 ? formatStart : 0); - let binding = colon == -1 ? term : term.substring(0, colon); - let format = colon == -1 ? null : term.substring(colon + 1); - if (colon == -1) { - let nullSepIndex = binding.indexOf("|", formatStart); - if (nullSepIndex != -1) { - format = binding.substring(nullSepIndex); - binding = binding.substring(0, nullSepIndex); - } - } - let argName = binding.replace(/\./g, "_"); - if (!isValidIdentifierName(argName)) argName = "inv" + ++invalidNameCount; - if (percentExpression || (binding[0] == "[" && binding[binding.length - 1] == "]")) { - argName = `expr${++subExprCount}`; - args[argName] = expression(percentExpression ? binding : binding.substring(1, binding.length - 1)); - } else args[argName] = binding; - if (format) { - let formatter = "fmt" + formats.length; - fb.push(formatter, "(", argName, ", ", quoteStr(format), ")"); - formats.push(Format.parse(format)); - } else fb.push(argName); - termStart = -1; - } - } else fb.push(c); - - break; - - case '"': - case "'": - if (!quote) quote = c; - else if (quote == c) { - let at = i - 1; - let slashCount = 0; - while (at >= 0 && str[at] === "\\") { - slashCount++; - at--; - } - if (slashCount % 2 == 0) quote = false; - } - - if (curlyBrackets == 0) fb.push(c); - - break; - - default: - if (termStart < 0) fb.push(c); - break; - } - } - - fb.push(")"); - - let body = fb.join(""); - - if (expFatArrows) body = expFatArrows(body); - - //console.log(body); - let keys = Object.keys(args); - - try { - let compute = new Function("fmt", ...formats.map((f, i) => "fmt" + i), ...helperNames, ...keys, body).bind( - Format, - Format.value, - ...formats, - ...helperValues, - ); - - let selector = computable(...keys.map((k) => args[k]), compute); - cache[str] = selector; - return selector; - } catch (err) { - throw new Error(`Failed to parse expression: '${str}'. Error: ${err.message}`); - } -} - -export const Expression = { - get: function (str) { - return expression(str); - }, - - compile: function (str) { - return this.get(str).memoize(); - }, - - registerHelper: function (name, helper) { - helpers[name] = helper; - helperNames = Object.keys(helpers); - helperValues = helperNames.map((n) => helpers[n]); - }, -}; - -export function plugFatArrowExpansion(impl) { - expFatArrows = impl; -} - -export function invalidateExpressionCache() { - expCache = {}; -} - -let expCache = {}; - -let getExpressionCache = () => expCache; - -export function setGetExpressionCacheCallback(callback) { - getExpressionCache = callback; -} diff --git a/packages/cx/src/data/Expression.spec.js b/packages/cx/src/data/Expression.spec.js deleted file mode 100644 index 2f40295e8..000000000 --- a/packages/cx/src/data/Expression.spec.js +++ /dev/null @@ -1,229 +0,0 @@ -import { Expression } from "./Expression"; -import assert from "assert"; -import { StringTemplate } from "./StringTemplate"; - -describe("Expression", function () { - describe("#compile()", function () { - it("returns a selector", function () { - let e = Expression.compile("{person.name}"); - let state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "Jim"); - }); - - it("ignores bindings in strings", function () { - let e = Expression.compile('"{person.name}"'); - let state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "{person.name}"); - }); - - it("allows mixed curly braces in strings", () => { - let e = Expression.compile('"Hello {" + "person.name}"'); - assert.equal(e(), "Hello {person.name}"); - }); - - it("properly encodes quotes #1", function () { - let e = Expression.compile('"\'"'); - let state = {}; - assert.equal(e(state), "'"); - }); - - it("properly encodes quotes #2", function () { - let e = Expression.compile('"\\""'); - let state = {}; - assert.equal(e(state), '"'); - }); - - it("properly encodes quotes #3", function () { - let e = Expression.compile('"\\\\\\""'); - let state = {}; - assert.equal(e(state), '\\"'); - }); - - it("properly encodes quotes #4", function () { - let e = Expression.compile('"\\\\"'); - let state = {}; - assert.equal(e(state), "\\"); - }); - }); - - describe("allow subexpressions", function () { - it("in square brackets", function () { - let e = Expression.compile("{[{person.name}]}"); - let state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "Jim"); - }); - - it("in square brackets", function () { - let e = Expression.compile("{[{person.alias} || {person.name}]}"); - let state = { - person: { - name: "Jim", - alias: "J", - }, - }; - assert.equal(e(state), "J"); - }); - - it("prefixed with % sign", function () { - let e = Expression.compile("%{1+1}"); - assert.equal(e(), "2"); - }); - - it("can be formatted", function () { - let e = Expression.compile("{[1+1]:f;2}"); - assert.equal(e(), "2.00"); - }); - - it("n level deep", function () { - let e = Expression.compile("{[{[{[1+1]}]}]:f;2}"); - assert.equal(e(), "2.00"); - }); - - it("with complex math inside", function () { - let e = Expression.compile("%{{data}.reduce((a,b)=>a+b, 0):f;2}"); - let state = { - data: [1, 2, 3], - }; - assert.equal(e(state), "6.00"); - }); - - it("with a conditional operator inside", function () { - let e = Expression.compile('{[true ? "T" : "F"]}'); - assert.equal(e(), "T"); - }); - - it("with string interpolation inside", function () { - let e = Expression.compile("{[`${{test}}`]}"); - assert.equal(e({ test: "T" }), "T"); - }); - }); - - describe("are working", function () { - it("function expressions with get", function () { - let e = Expression.get((get) => get("a") + get("b")); - assert.equal(e({ a: 1, b: 2 }), 3); - }); - - it("are properly memoized", function () { - let inv = 0; - let e = Expression.get((get) => { - inv++; - return get("a") + get("b"); - }).memoize(); - - assert.equal(e({ a: 1, b: 1 }), 2); - assert.equal(inv, 1); - - assert.equal(e({ a: 1, b: 1 }), 2); - assert.equal(inv, 1); - - assert.equal(e({ a: 1, b: 2 }), 3); - assert.equal(inv, 2); - - assert.equal(e({ a: 1, b: 2 }), 3); - assert.equal(inv, 2); - - assert.equal(e({ a: 2, b: 2 }), 4); - assert.equal(inv, 3); - }); - - it("formatting can be used inside bindings", function () { - let e = Expression.get((get) => get("name:prefix;Hello ")); - assert(e({ name: "CxJS" }), "Hello CxJS"); - }); - - it("allows using the fmt function inside", function () { - let e = Expression.compile('{[fmt({text}, "prefix;Hello ")]}'); - assert.equal(e({ text: "CxJS" }), "Hello CxJS"); - }); - - it("allows using dashes inside names", function () { - let e = Expression.compile("{framework-name}"); - assert.equal(e({ "framework-name": "CxJS" }), "CxJS"); - }); - - it("allows using spaces and other characters inside names", function () { - let e = Expression.compile("{1nv@lid js ident1fier}"); - assert.equal(e({ "1nv@lid js ident1fier": "CxJS" }), "CxJS"); - }); - - it("allows strings in subexpressions", () => { - let e = Expression.compile("{['1']}"); - assert.equal(e(), "1"); - - let e2 = Expression.compile('%{"1"}'); - assert.equal(e2(), "1"); - }); - - it("allows string templates in nested expressions", () => { - Expression.registerHelper("tpl", StringTemplate.format); - let e = Expression.compile("tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})"); - assert.equal(e({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); - - let e2 = Expression.compile("{[tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})]}"); - assert.equal(e2({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); - - // we're going to need a better parser for this - // let e3 = Expression.compile("%{tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})}"); - // assert.equal(e3({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); - }); - - it("string templates can be in the data", () => { - Expression.registerHelper("tpl", StringTemplate.format); - let e = Expression.compile("tpl({template}, {value}, {percentage})"); - assert.equal(e({ value: 1, percentage: 0.02, template: "{0:n;2} {1:p;2:wrap;(;)}" }), "1.00 (2.00%)"); - }); - - // it('are properly memoized with proxies', function () { - // let inv = 0; - // let e = Expression.get(d => { inv++; return d.a + d.b}).memoize(); - // - // assert.equal(e({ a: 1, b: 1 }), 2); - // assert.equal(inv, 1); - // - // assert.equal(e({ a: 1, b: 1 }), 2); - // assert.equal(inv, 1); - // - // assert.equal(e({ a: 1, b: 2 }), 3); - // assert.equal(inv, 2); - // - // assert.equal(e({ a: 1, b: 2 }), 3); - // assert.equal(inv, 2); - // - // assert.equal(e({ a: 2, b: 2 }), 4); - // assert.equal(inv, 3); - // }); - // - // it('are properly memoized with proxies and deep data', function () { - // let inv = 0; - // let e = Expression.get(d => { inv++; return d.v.a + d.v.b}).memoize(); - // - // assert.equal(e({ v: { a: 1, b: 1 }}), 2); - // assert.equal(inv, 1); - // - // assert.equal(e({ v: { a: 1, b: 1 }}), 2); - // assert.equal(inv, 1); - // - // assert.equal(e({ v: { a: 1, b: 2 }}), 3); - // assert.equal(inv, 2); - // - // assert.equal(e({ v: { a: 1, b: 2 }}), 3); - // assert.equal(inv, 2); - // - // assert.equal(e({ v: { a: 2, b: 2 }}), 4); - // assert.equal(inv, 3); - // }); - }); -}); diff --git a/packages/cx/src/data/Expression.spec.ts b/packages/cx/src/data/Expression.spec.ts new file mode 100644 index 000000000..a1112812c --- /dev/null +++ b/packages/cx/src/data/Expression.spec.ts @@ -0,0 +1,229 @@ +import { Expression } from "./Expression"; +import assert from "assert"; +import { StringTemplate } from "./StringTemplate"; + +describe("Expression", function () { + describe("#compile()", function () { + it("returns a selector", function () { + let e = Expression.compile("{person.name}"); + let state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "Jim"); + }); + + it("ignores bindings in strings", function () { + let e = Expression.compile('"{person.name}"'); + let state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "{person.name}"); + }); + + it("allows mixed curly braces in strings", () => { + let e = Expression.compile('"Hello {" + "person.name}"'); + assert.equal(e({}), "Hello {person.name}"); + }); + + it("properly encodes quotes #1", function () { + let e = Expression.compile('"\'"'); + let state = {}; + assert.equal(e(state), "'"); + }); + + it("properly encodes quotes #2", function () { + let e = Expression.compile('"\\""'); + let state = {}; + assert.equal(e(state), '"'); + }); + + it("properly encodes quotes #3", function () { + let e = Expression.compile('"\\\\\\""'); + let state = {}; + assert.equal(e(state), '\\"'); + }); + + it("properly encodes quotes #4", function () { + let e = Expression.compile('"\\\\"'); + let state = {}; + assert.equal(e(state), "\\"); + }); + }); + + describe("allow subexpressions", function () { + it("in square brackets", function () { + let e = Expression.compile("{[{person.name}]}"); + let state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "Jim"); + }); + + it("in square brackets", function () { + let e = Expression.compile("{[{person.alias} || {person.name}]}"); + let state = { + person: { + name: "Jim", + alias: "J", + }, + }; + assert.equal(e(state), "J"); + }); + + it("prefixed with % sign", function () { + let e = Expression.compile("%{1+1}"); + assert.equal(e({}), "2"); + }); + + it("can be formatted", function () { + let e = Expression.compile("{[1+1]:f;2}"); + assert.equal(e({}), "2.00"); + }); + + it("n level deep", function () { + let e = Expression.compile("{[{[{[1+1]}]}]:f;2}"); + assert.equal(e({}), "2.00"); + }); + + it("with complex math inside", function () { + let e = Expression.compile("%{{data}.reduce((a,b)=>a+b, 0):f;2}"); + let state = { + data: [1, 2, 3], + }; + assert.equal(e(state), "6.00"); + }); + + it("with a conditional operator inside", function () { + let e = Expression.compile('{[true ? "T" : "F"]}'); + assert.equal(e({}), "T"); + }); + + it("with string interpolation inside", function () { + let e = Expression.compile("{[`${{test}}`]}"); + assert.equal(e({ test: "T" }), "T"); + }); + }); + + describe("are working", function () { + it("function expressions with get", function () { + let e = Expression.get((get) => get("a") + get("b")); + assert.equal(e({ a: 1, b: 2 }), 3); + }); + + it("are properly memoized", function () { + let inv = 0; + let e = Expression.get((get) => { + inv++; + return get("a") + get("b"); + }).memoize(); + + assert.equal(e({ a: 1, b: 1 }), 2); + assert.equal(inv, 1); + + assert.equal(e({ a: 1, b: 1 }), 2); + assert.equal(inv, 1); + + assert.equal(e({ a: 1, b: 2 }), 3); + assert.equal(inv, 2); + + assert.equal(e({ a: 1, b: 2 }), 3); + assert.equal(inv, 2); + + assert.equal(e({ a: 2, b: 2 }), 4); + assert.equal(inv, 3); + }); + + it("formatting can be used inside bindings", function () { + let e = Expression.get((get) => get("name:prefix;Hello ")); + assert(e({ name: "CxJS" }), "Hello CxJS"); + }); + + it("allows using the fmt function inside", function () { + let e = Expression.compile('{[fmt({text}, "prefix;Hello ")]}'); + assert.equal(e({ text: "CxJS" }), "Hello CxJS"); + }); + + it("allows using dashes inside names", function () { + let e = Expression.compile("{framework-name}"); + assert.equal(e({ "framework-name": "CxJS" }), "CxJS"); + }); + + it("allows using spaces and other characters inside names", function () { + let e = Expression.compile("{1nv@lid js ident1fier}"); + assert.equal(e({ "1nv@lid js ident1fier": "CxJS" }), "CxJS"); + }); + + it("allows strings in subexpressions", () => { + let e = Expression.compile("{['1']}"); + assert.equal(e({}), "1"); + + let e2 = Expression.compile('%{"1"}'); + assert.equal(e({}), "1"); + }); + + it("allows string templates in nested expressions", () => { + Expression.registerHelper("tpl", StringTemplate.format); + let e = Expression.compile("tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})"); + assert.equal(e({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); + + let e2 = Expression.compile("{[tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})]}"); + assert.equal(e2({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); + + // we're going to need a better parser for this + // let e3 = Expression.compile("%{tpl('{0:n;2} {1:p;2:wrap;(;)}', {value}, {percentage})}"); + // assert.equal(e3({ value: 1, percentage: 0.02 }), "1.00 (2.00%)"); + }); + + it("string templates can be in the data", () => { + Expression.registerHelper("tpl", StringTemplate.format); + let e = Expression.compile("tpl({template}, {value}, {percentage})"); + assert.equal(e({ value: 1, percentage: 0.02, template: "{0:n;2} {1:p;2:wrap;(;)}" }), "1.00 (2.00%)"); + }); + + // it('are properly memoized with proxies', function () { + // let inv = 0; + // let e = Expression.get(d => { inv++; return d.a + d.b}).memoize(); + // + // assert.equal(e({ a: 1, b: 1 }), 2); + // assert.equal(inv, 1); + // + // assert.equal(e({ a: 1, b: 1 }), 2); + // assert.equal(inv, 1); + // + // assert.equal(e({ a: 1, b: 2 }), 3); + // assert.equal(inv, 2); + // + // assert.equal(e({ a: 1, b: 2 }), 3); + // assert.equal(inv, 2); + // + // assert.equal(e({ a: 2, b: 2 }), 4); + // assert.equal(inv, 3); + // }); + // + // it('are properly memoized with proxies and deep data', function () { + // let inv = 0; + // let e = Expression.get(d => { inv++; return d.v.a + d.v.b}).memoize(); + // + // assert.equal(e({ v: { a: 1, b: 1 }}), 2); + // assert.equal(inv, 1); + // + // assert.equal(e({ v: { a: 1, b: 1 }}), 2); + // assert.equal(inv, 1); + // + // assert.equal(e({ v: { a: 1, b: 2 }}), 3); + // assert.equal(inv, 2); + // + // assert.equal(e({ v: { a: 1, b: 2 }}), 3); + // assert.equal(inv, 2); + // + // assert.equal(e({ v: { a: 2, b: 2 }}), 4); + // assert.equal(inv, 3); + // }); + }); +}); diff --git a/packages/cx/src/data/Expression.ts b/packages/cx/src/data/Expression.ts new file mode 100644 index 000000000..4cec12efb --- /dev/null +++ b/packages/cx/src/data/Expression.ts @@ -0,0 +1,233 @@ +import { computable } from "./computable"; +import { Format } from "../util/Format"; +import { Binding } from "./Binding"; + +import { quoteStr } from "../util/quote"; +import { isFunction } from "../util/isFunction"; +import { isValidIdentifierName } from "../util/isValidIdentifierName"; +import { MemoSelector, Selector } from "./Selector"; + +/* + Helper usage example + + Expression.registerHelper('_', _); + let e = Expression.compile('_.min({data})'); + */ + +let helpers: Record = {}, + helperNames: string[] = [], + helperValues: any[] = [], + expFatArrows: null | ((body: string) => string) = null; + +function getExpr(expr: Selector): MemoSelector { + if (expr.memoize) return expr as MemoSelector; + + function memoize(): Selector { + let lastValue: any, + lastRunBindings: Record = {}, + lastRunResults: Record = {}, + getters: Record = {}, + currentData: any, + len = -1; + + let get = function (bindingWithFormat: string) { + let getter = getters[bindingWithFormat]; + if (!getter) { + let binding = bindingWithFormat, + format; + let colonIndex = bindingWithFormat.indexOf(":"); + if (colonIndex != -1) { + format = Format.parse(bindingWithFormat.substring(colonIndex + 1)); + binding = bindingWithFormat.substring(0, colonIndex); + } else { + let nullSeparatorIndex = bindingWithFormat.indexOf(":"); + if (nullSeparatorIndex != -1) { + format = Format.parse(bindingWithFormat.substring(nullSeparatorIndex)); + binding = bindingWithFormat.substring(0, nullSeparatorIndex - 1); + } + } + let b = Binding.get(binding); + getter = (data) => { + let value = b.value(data); + lastRunBindings[len] = b.value; + lastRunResults[len] = value; + len++; + return value; + }; + + if (format) { + let valueGetter = getter; + getter = (data) => format(valueGetter(data)); + } + + getters[bindingWithFormat] = getter; + } + return getter(currentData); + }; + + return function (data) { + let i = 0; + for (; i < len; i++) if (lastRunBindings[i](data) !== lastRunResults[i]) break; + if (i !== len) { + len = 0; + currentData = data; + lastValue = expr(get); + } + return lastValue; + }; + } + + let result: Selector = memoize(); + result.memoize = memoize; + return result as MemoSelector; +} + +export function expression(str: string | Selector): MemoSelector { + if (isFunction(str)) return getExpr(str); + + let cache = getExpressionCache(); + let r = cache[str]; + if (r) return r; + + let quote: string | false = false; + + let termStart = -1, + curlyBrackets = 0, + percentExpression; + + let fb = ["return ("]; + + let args: Record = {}; + let formats = []; + let subExprCount = 0; + let invalidNameCount = 0; + + for (let i = 0; i < str.length; i++) { + let c = str[i]; + switch (c) { + case "{": + if (curlyBrackets > 0 && !quote) curlyBrackets++; + else if (!quote && termStart < 0 && (str[i + 1] != "{" || str[i - 1] == "%")) { + termStart = i + 1; + curlyBrackets = 1; + percentExpression = str[i - 1] == "%"; + if (percentExpression) fb.pop(); //% + } else if (termStart < 0 && (quote || str[i - 1] != "{")) fb.push(c); + break; + + case "}": + if (termStart >= 0) { + if (quote) continue; + if (--curlyBrackets == 0) { + let term = str.substring(termStart, i); + let formatStart = 0; + if (term[0] == "[") formatStart = term.indexOf("]"); + let colon = term.indexOf(":", formatStart > 0 ? formatStart : 0); + let binding = colon == -1 ? term : term.substring(0, colon); + let format = colon == -1 ? null : term.substring(colon + 1); + if (colon == -1) { + let nullSepIndex = binding.indexOf("|", formatStart); + if (nullSepIndex != -1) { + format = binding.substring(nullSepIndex); + binding = binding.substring(0, nullSepIndex); + } + } + let argName = binding.replace(/\./g, "_"); + if (!isValidIdentifierName(argName)) argName = "inv" + ++invalidNameCount; + if (percentExpression || (binding[0] == "[" && binding[binding.length - 1] == "]")) { + argName = `expr${++subExprCount}`; + args[argName] = expression(percentExpression ? binding : binding.substring(1, binding.length - 1)); + } else args[argName] = binding; + if (format) { + let formatter = "fmt" + formats.length; + fb.push(formatter, "(", argName, ", ", quoteStr(format), ")"); + formats.push(Format.parse(format)); + } else fb.push(argName); + termStart = -1; + } + } else fb.push(c); + + break; + + case '"': + case "'": + if (!quote) quote = c; + else if (quote == c) { + let at = i - 1; + let slashCount = 0; + while (at >= 0 && str[at] === "\\") { + slashCount++; + at--; + } + if (slashCount % 2 == 0) quote = false; + } + + if (curlyBrackets == 0) fb.push(c); + + break; + + default: + if (termStart < 0) fb.push(c); + break; + } + } + + fb.push(")"); + + let body = fb.join(""); + + if (expFatArrows) body = expFatArrows(body); + + //console.log(body); + let keys = Object.keys(args); + + try { + let compute = new Function("fmt", ...formats.map((f, i) => "fmt" + i), ...helperNames, ...keys, body).bind( + Format, + Format.value, + ...formats, + ...helperValues, + ); + + let selector = computable(...keys.map((k) => args[k]), compute); + cache[str] = selector; + return selector; + } catch (err) { + throw new Error(`Failed to parse expression: '${str}'. ${err}`); + } +} + +export type GetFunction = (bindingPath: string) => any; +export type SelectorFunction = (get: GetFunction) => any; + +export const Expression = { + get: function (str: string | SelectorFunction): MemoSelector { + return expression(str); + }, + + compile: function (str: string | SelectorFunction): Selector { + return this.get(str).memoize(); + }, + + registerHelper: function (name: string, helper: any) { + helpers[name] = helper; + helperNames = Object.keys(helpers); + helperValues = helperNames.map((n) => helpers[n]); + }, +}; + +export function plugFatArrowExpansion(impl: (body: string) => string) { + expFatArrows = impl; +} + +export function invalidateExpressionCache() { + expCache = {}; +} + +let expCache: Record = {}; + +let getExpressionCache = () => expCache; + +export function setGetExpressionCacheCallback(callback: () => Record) { + getExpressionCache = callback; +} diff --git a/packages/cx/src/data/Grouper.d.ts b/packages/cx/src/data/Grouper.d.ts deleted file mode 100644 index d4d439058..000000000 --- a/packages/cx/src/data/Grouper.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { StructuredProp, Record, StringProp } from '../core'; - -interface GroupingResult { - key: Record, - name: string, - records: Record[], - indexes: number[], - aggregates: Record -} - -export class Grouper { - constructor(key: StructuredProp, aggregates?: StructuredProp, dataGetter?: (any) => any, nameGetter?: StringProp); - - reset(); - - process(record: Record, index: number): void; - - processAll(records: Record[], indexes?: number[]): void; - - getResults(): GroupingResult[] -} diff --git a/packages/cx/src/data/Grouper.js b/packages/cx/src/data/Grouper.js deleted file mode 100644 index f719772d9..000000000 --- a/packages/cx/src/data/Grouper.js +++ /dev/null @@ -1,144 +0,0 @@ -import { getSelector } from "./getSelector"; -import { AggregateFunction } from "./AggregateFunction"; -import { Binding } from "./Binding"; - -/* - 'column': { - index: 0, - sort: 'asc', - group: true - aggregate: 'count' - } - */ - -export class Grouper { - constructor(key, aggregates, dataGetter, nameGetter) { - this.keys = Object.keys(key).map((keyField) => { - let isSimpleField = keyField.indexOf(".") === -1; - let keySetter; - if (isSimpleField) { - // use simple field setter wherever possible - keySetter = function set(result, value) { - result[keyField] = value; - return result; - }; - } else { - // for complex paths, use deep setter - let binding = Binding.get(keyField); - keySetter = (result, value) => binding.set(result, value); - } - return { - name: keyField, - keySetter, - value: getSelector(key[keyField]), - }; - }); - if (nameGetter) this.nameGetter = getSelector(nameGetter); - this.dataGetter = dataGetter || ((x) => x); - this.aggregates = - aggregates && - transformValues(aggregates, (prop) => { - if (!AggregateFunction[prop.type]) throw new Error(`Unknown aggregate function '${prop.type}'.`); - - return { - value: getSelector(prop.value), - weight: getSelector(prop.weight ?? 1), - type: prop.type, - }; - }); - this.reset(); - } - - reset() { - this.groups = this.initGroup(this.keys.length == 0); - } - - initGroup(leaf) { - if (!leaf) return {}; - - return { - records: [], - indexes: [], - aggregates: - this.aggregates && - transformValues(this.aggregates, (prop) => { - let f = AggregateFunction[prop.type]; - return { - processor: f(), - value: prop.value, - weight: prop.weight, - }; - }), - }; - } - - process(record, index) { - let data = this.dataGetter(record); - let key = this.keys.map((k) => k.value(data)); - let g = this.groups; - for (let i = 0; i < key.length; i++) { - let sg = g[key[i]]; - if (!sg) { - sg = g[key[i]] = this.initGroup(i + 1 == key.length); - if (this.nameGetter) sg.name = this.nameGetter(data); - } - g = sg; - } - - g.records.push(record); - g.indexes.push(index); - - if (g.aggregates) { - for (let k in g.aggregates) - g.aggregates[k].processor.process(g.aggregates[k].value(data), g.aggregates[k].weight(data)); - } - } - - processAll(records, indexes) { - if (indexes) { - for (let i = 0; i < records.length; i++) this.process(records[i], indexes[i]); - } else records.forEach((r, i) => this.process(r, i)); - } - - report(g, path, level, results) { - if (level == this.keys.length) { - let key = {}; - this.keys.forEach((k, i) => { - key = k.keySetter(key, path[i]); - }); - results.push({ - key: key, - name: g.name, - records: g.records, - indexes: g.indexes, - aggregates: resolveKeyPaths(transformValues(g.aggregates, (p) => p.processor.getResult())), - }); - } else { - Object.keys(g).forEach((k) => this.report(g[k], [...path, k], level + 1, results)); - } - } - - getResults() { - let g = this.groups; - let results = []; - this.report(g, [], 0, results); - return results; - } -} - -// transform object values using a function -function transformValues(o, f) { - let res = {}; - for (let k in o) res[k] = f(o[k], k); - return res; -} - -// transform keys like 'a.b.c' into nested objects -function resolveKeyPaths(o) { - let res = {}; - for (let k in o) { - if (k.indexOf(".") > 0) res = Binding.get(k).set(res, o[k]); - else res[k] = o[k]; - } - return res; -} diff --git a/packages/cx/src/data/Grouper.spec.js b/packages/cx/src/data/Grouper.spec.ts similarity index 100% rename from packages/cx/src/data/Grouper.spec.js rename to packages/cx/src/data/Grouper.spec.ts diff --git a/packages/cx/src/data/Grouper.ts b/packages/cx/src/data/Grouper.ts new file mode 100644 index 000000000..b8bff724f --- /dev/null +++ b/packages/cx/src/data/Grouper.ts @@ -0,0 +1,158 @@ +import { getSelector } from "./getSelector"; +import { AggregateFunction } from "./AggregateFunction"; +import { Binding } from "./Binding"; + +/* + 'column': { + index: 0, + sort: 'asc', + group: true + aggregate: 'count' + } + */ + +export interface GroupResult { + key: any; + name?: any; + records: any[]; + indexes: number[]; + aggregates?: Record; +} + +export class Grouper { + keys: any[]; + nameGetter?: any; + dataGetter: any; + aggregates?: any; + groups: any; + + constructor(key: any, aggregates?: any, dataGetter?: any, nameGetter?: any) { + this.keys = Object.keys(key).map((keyField) => { + let isSimpleField = keyField.indexOf(".") === -1; + let keySetter; + if (isSimpleField) { + // use simple field setter wherever possible + keySetter = function set(result: any, value: any) { + result[keyField] = value; + return result; + }; + } else { + // for complex paths, use deep setter + let binding = Binding.get(keyField); + keySetter = (result: any, value: any) => binding.set(result, value); + } + return { + name: keyField, + keySetter, + value: getSelector(key[keyField]), + }; + }); + if (nameGetter) this.nameGetter = getSelector(nameGetter); + this.dataGetter = dataGetter || ((x: any) => x); + this.aggregates = + aggregates && + transformValues(aggregates, (prop) => { + if (!(prop.type in AggregateFunction)) throw new Error(`Unknown aggregate function '${prop.type}'.`); + + return { + value: getSelector(prop.value), + weight: getSelector(prop.weight ?? 1), + type: prop.type, + }; + }); + this.reset(); + } + + reset() { + this.groups = this.initGroup(this.keys.length == 0); + } + + initGroup(leaf: boolean) { + if (!leaf) return {}; + + return { + records: [], + indexes: [], + aggregates: + this.aggregates && + transformValues(this.aggregates, (prop) => { + let f = (AggregateFunction as any)[prop.type]; + return { + processor: f(), + value: prop.value, + weight: prop.weight, + }; + }), + }; + } + + process(record: any, index: number) { + let data = this.dataGetter(record); + let key = this.keys.map((k) => k.value(data)); + let g = this.groups; + for (let i = 0; i < key.length; i++) { + let sg = g[key[i]]; + if (!sg) { + sg = g[key[i]] = this.initGroup(i + 1 == key.length); + if (this.nameGetter) sg.name = this.nameGetter(data); + } + g = sg; + } + + g.records.push(record); + g.indexes.push(index); + + if (g.aggregates) { + for (let k in g.aggregates) + g.aggregates[k].processor.process(g.aggregates[k].value(data), g.aggregates[k].weight(data)); + } + } + + processAll(records: any[], indexes?: number[]) { + if (indexes) { + for (let i = 0; i < records.length; i++) this.process(records[i], indexes[i]); + } else records.forEach((r: any, i: number) => this.process(r, i)); + } + + report(g: any, path: any[], level: number, results: GroupResult[]) { + if (level == this.keys.length) { + let key = {}; + this.keys.forEach((k: any, i: number) => { + key = k.keySetter(key, path[i]); + }); + results.push({ + key: key, + name: g.name, + records: g.records, + indexes: g.indexes, + aggregates: resolveKeyPaths(transformValues(g.aggregates, (p) => p.processor.getResult())), + }); + } else { + Object.keys(g).forEach((k) => this.report(g[k], [...path, k], level + 1, results)); + } + } + + getResults(): GroupResult[] { + let g = this.groups; + let results: GroupResult[] = []; + this.report(g, [], 0, results); + return results; + } +} + +// transform object values using a function +function transformValues(o: Record, f: (v: any, k?: string) => any): Record { + let res: Record = {}; + for (let k in o) res[k] = f(o[k], k); + return res; +} + +// transform keys like 'a.b.c' into nested objects +function resolveKeyPaths(o: Record) { + let res: Record = {}; + for (let k in o) { + if (k.indexOf(".") > 0) res = Binding.get(k).set(res, o[k]); + else res[k] = o[k]; + } + return res; +} diff --git a/packages/cx/src/data/NestedDataView.d.ts b/packages/cx/src/data/NestedDataView.d.ts deleted file mode 100644 index 293a869b8..000000000 --- a/packages/cx/src/data/NestedDataView.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AugmentedViewBase, AugmentedViewBaseConfig } from "./AugmentedViewBase"; -import { StructuredDataAccessor } from "./StructuredDataAccessor"; - -export interface NestedDataViewConfig extends AugmentedViewBaseConfig { - nestedData?: StructuredDataAccessor; -} - -export class NestedDataView extends AugmentedViewBase { - constructor(config: NestedDataViewConfig); - nestedData: StructuredDataAccessor; - - protected setExtraKeyValue(key: string, value: any): boolean; - - protected deleteExtraKeyValue(key: string): boolean; - - protected isExtraKey(key: string): boolean; - - protected embedAugmentData(result: Cx.Record, parentStoreData: Cx.Record): void; -} diff --git a/packages/cx/src/data/NestedDataView.js b/packages/cx/src/data/NestedDataView.js deleted file mode 100644 index e180b21d7..000000000 --- a/packages/cx/src/data/NestedDataView.js +++ /dev/null @@ -1,22 +0,0 @@ -import { AugmentedViewBase } from "../data/AugmentedViewBase"; - -export class NestedDataView extends AugmentedViewBase { - embedAugmentData(result, parentStoreData) { - if (this.nestedData) { - let nested = this.nestedData.getSelector()(parentStoreData); - for (let key in nested) result[key] = nested[key]; - } - } - - isExtraKey(key) { - return this.nestedData && this.nestedData.containsKey(key); - } - - setExtraKeyValue(key, value) { - this.nestedData.setItem(key, value); - } - - deleteExtraKeyValue(key) { - this.setExtraKeyValue(key, undefined); - } -} diff --git a/packages/cx/src/data/NestedDataView.ts b/packages/cx/src/data/NestedDataView.ts new file mode 100644 index 000000000..3525ea60b --- /dev/null +++ b/packages/cx/src/data/NestedDataView.ts @@ -0,0 +1,43 @@ +import { View, ViewConfig } from "./View"; +import { AugmentedViewBase } from "../data/AugmentedViewBase"; + +export interface StructuredDataAccessor { + getSelector(): (data: object) => Record; + get(): object; + setItem(key: string, value: any): boolean; + containsKey(key: string): boolean; + getKeys(): string[]; +} + +export interface NestedDataViewConfig extends ViewConfig { + nestedData?: StructuredDataAccessor; + store: View; +} + +export class NestedDataView extends AugmentedViewBase { + declare nestedData?: StructuredDataAccessor; + + constructor(config: NestedDataViewConfig) { + super(config); + } + + protected embedAugmentData(result: Record, parentStoreData: Record): void { + if (this.nestedData) { + let nested = this.nestedData.getSelector()(parentStoreData); + for (let key in nested) result[key] = nested[key]; + } + } + + protected isExtraKey(key: string): boolean { + return !!this.nestedData && this.nestedData.containsKey(key); + } + + protected setExtraKeyValue(key: string, value: any): boolean { + if (!this.nestedData) throw new Error("Internal error. This should not happen."); + return this.nestedData.setItem(key, value); + } + + protected deleteExtraKeyValue(key: string): boolean { + return this.setExtraKeyValue(key, undefined); + } +} diff --git a/packages/cx/src/data/ReadOnlyDataView.d.ts b/packages/cx/src/data/ReadOnlyDataView.d.ts deleted file mode 100644 index e16f257ce..000000000 --- a/packages/cx/src/data/ReadOnlyDataView.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {View, ViewConfig} from './View'; - -interface ReadOnlyDataViewConfig extends ViewConfig { - data?: any; - immutable?: boolean; -} - -export class ReadOnlyDataView extends View { - constructor(config?: ReadOnlyDataViewConfig); - - setData(data: any); - - setStore(store: View); -} diff --git a/packages/cx/src/data/ReadOnlyDataView.js b/packages/cx/src/data/ReadOnlyDataView.js deleted file mode 100644 index 405611075..000000000 --- a/packages/cx/src/data/ReadOnlyDataView.js +++ /dev/null @@ -1,27 +0,0 @@ -import {View} from './View'; - -export class ReadOnlyDataView extends View { - - getData() { - if (this.sealed && this.meta.version === this.cache.version && this.cache.data === this.data) - return this.cache.result; - - let data = this.store.getData(); - this.cache.result = this.sealed || this.immutable || this.store.sealed - ? Object.assign({}, data, this.getAdditionalData(data)) - : Object.assign(data, this.getAdditionalData(data)); - this.cache.version = this.meta.version; - this.cache.data = this.data; - return this.cache.result; - } - - getAdditionalData() { - return this.data; - } - - setData(data) { - this.data = data; - } -} - -ReadOnlyDataView.prototype.immutable = false; \ No newline at end of file diff --git a/packages/cx/src/data/ReadOnlyDataView.ts b/packages/cx/src/data/ReadOnlyDataView.ts new file mode 100644 index 000000000..445bb6cff --- /dev/null +++ b/packages/cx/src/data/ReadOnlyDataView.ts @@ -0,0 +1,39 @@ +import { View, ViewConfig } from "./View"; + +export interface ReadOnlyDataViewConfig extends ViewConfig { + data?: any; +} + +export class ReadOnlyDataView extends View { + declare store: View; + declare data?: any; + declare immutable?: boolean; + + constructor(config?: ReadOnlyDataViewConfig) { + super(config); + } + + getData(): any { + if (this.sealed && this.meta.version === this.cache.version && this.cache.data === this.data) + return this.cache.result; + + let data = this.store.getData(); + this.cache.result = + this.sealed || this.immutable || this.store.sealed + ? Object.assign({}, data, this.getAdditionalData(data)) + : Object.assign(data, this.getAdditionalData(data)); + this.cache.version = this.meta.version; + this.cache.data = this.data; + return this.cache.result; + } + + getAdditionalData(data?: any): any { + return this.data; + } + + setData(data: any): void { + this.data = data; + } +} + +ReadOnlyDataView.prototype.immutable = false; diff --git a/packages/cx/src/data/Ref.d.ts b/packages/cx/src/data/Ref.d.ts deleted file mode 100644 index 1d42a195d..000000000 --- a/packages/cx/src/data/Ref.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { View } from "./View"; - -interface RefConfig { - store: View; - path: string; -} - -export class Ref { - constructor(config: RefConfig); - - init(value: T): boolean; - - set(value: T): boolean; - - delete(): boolean; - - get(): T; - - toggle(): boolean; - - update(updateFn: (currentValue: T, ...args) => T, ...args): boolean; - - ref(path: string): Ref; -} diff --git a/packages/cx/src/data/Ref.js b/packages/cx/src/data/Ref.js deleted file mode 100644 index d3249431f..000000000 --- a/packages/cx/src/data/Ref.js +++ /dev/null @@ -1,79 +0,0 @@ -import { isFunction } from "../util/isFunction"; -import { Component } from "../util/Component"; -import { Binding } from "./Binding"; - -export class Ref extends Component { - constructor(config) { - super(config); - this.get = this.get.bind(this); - if (this.set) this.set = this.set.bind(this); - } - - get() { - throw new Error("Ref's get method is not implemented."); - } - - init(value) { - if (this.get() === undefined) this.set(value); - } - - toggle() { - this.set(!this.get()); - } - - update(cb, ...args) { - this.set(cb(this.get(), ...args)); - } - - as(config) { - return Ref.create(config, { - get: this.get, - set: this.set, - }); - } - - ref(path) { - let binding = Binding.get(path); - return Ref.create({ - get: () => binding.value(this.get()), - set: (value) => { - let data = this.get(); - let newData = binding.set(data, value); - if (data === newData) return false; - return this.set(newData); - }, - }); - } - - //allows the function to be passed as a selector, e.g. to computable or addTrigger - memoize() { - return this.get; - } -} - -Ref.prototype.isRef = true; - -Ref.factory = function (alias, config, more) { - if (isFunction(alias)) { - let cfg = { - ...config, - ...more, - }; - - if (cfg.store) Object.assign(cfg, cfg.store.getMethods()); - - let result = alias(cfg); - if (result instanceof Ref) return result; - - return this.create({ - ...config, - ...more, - ...result, - }); - } - - return this.create({ - ...config, - ...more, - }); -}; diff --git a/packages/cx/src/data/Ref.spec.js b/packages/cx/src/data/Ref.spec.js deleted file mode 100644 index 30de5eedf..000000000 --- a/packages/cx/src/data/Ref.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -import assert from "assert"; -import { Store } from "./Store"; -import { Ref } from "./Ref"; -import { append } from "./ops/append"; -import { StoreRef } from "./StoreRef"; - -const getStore = () => { - return new Store({ - data: { - a: 3, - item: { - firstName: "Jack", - }, - array: [], - }, - }); -}; - -describe("Ref", () => { - it("can init data", () => { - let store = getStore(); - let b = store.ref("b", 1); - assert.equal(store.get("b"), 1); - }); - - it("can set data", () => { - let store = getStore(); - let b = store.ref("b", 1); - b.set(2); - assert.equal(store.get("b"), 2); - }); - - it("can delete data", () => { - let store = getStore(); - let b = store.ref("item"); - b.delete(); - assert.equal(store.get("item"), undefined); - }); - - it("can cast itself to a ref of another type", () => { - class ArrayRef extends StoreRef { - append(...args) { - this.update(append, ...args); - } - } - let store = getStore(); - let array = store.ref("array").as(ArrayRef); - array.append(1, 2, 3); - assert.deepEqual(array.get(), [1, 2, 3]); - }); - - it("can extend itself in a functional way", () => { - let store = getStore(); - let array = store.ref("array").as(({ update, set, path }) => ({ - append(...args) { - update(path, append, ...args); - }, - - clear() { - set(path, []); - }, - })); - array.append(1, 2, 3); - assert.deepEqual(array.get(), [1, 2, 3]); - array.clear(); - assert.deepEqual(array.get(), []); - }); - - it("can get subrefs", () => { - let store = getStore(); - let person = new Ref({ - get: () => store.get("person"), - set: (value) => store.set("person", value), - }); - let name = person.ref("name"); - name.set("John"); - assert.equal(name.get(), "John"); - }); -}); diff --git a/packages/cx/src/data/Ref.spec.ts b/packages/cx/src/data/Ref.spec.ts new file mode 100644 index 000000000..dfed5a124 --- /dev/null +++ b/packages/cx/src/data/Ref.spec.ts @@ -0,0 +1,79 @@ +import assert from "assert"; +import { Store } from "./Store"; +import { Ref } from "./Ref"; +import { append } from "./ops/append"; +import { StoreRef } from "./StoreRef"; + +const getStore = () => { + return new Store({ + data: { + a: 3, + item: { + firstName: "Jack", + }, + array: [], + }, + }); +}; + +describe("Ref", () => { + it("can init data", () => { + let store = getStore(); + let b = store.ref("b", 1); + assert.equal(store.get("b"), 1); + }); + + it("can set data", () => { + let store = getStore(); + let b = store.ref("b", 1); + b.set(2); + assert.equal(store.get("b"), 2); + }); + + it("can delete data", () => { + let store = getStore(); + let b = store.ref("item"); + b.delete(); + assert.equal(store.get("item"), undefined); + }); + + it("can cast itself to a ref of another type", () => { + class ArrayRef extends StoreRef { + append(...args: any[]) { + this.update(append, ...args); + } + } + let store = getStore(); + let array = store.ref("array").as(ArrayRef); + (array as ArrayRef).append(1, 2, 3); + assert.deepEqual(array.get(), [1, 2, 3]); + }); + + it("can extend itself in a functional way", () => { + let store = getStore(); + let array = store.ref("array").as(({ update, set, path }: { update: any; set: any; path: string }) => ({ + append(...args: any[]) { + update(path, append, ...args); + }, + + clear() { + set(path, []); + }, + })); + (array as any).append(1, 2, 3); + assert.deepEqual(array.get(), [1, 2, 3]); + (array as any).clear(); + assert.deepEqual(array.get(), []); + }); + + it("can get subrefs", () => { + let store = getStore(); + let person = new Ref({ + get: () => store.get("person"), + set: (value) => store.set("person", value), + }); + let name = person.ref("name"); + name.set("John"); + assert.equal(name.get(), "John"); + }); +}); diff --git a/packages/cx/src/data/Ref.ts b/packages/cx/src/data/Ref.ts new file mode 100644 index 000000000..7448e9b96 --- /dev/null +++ b/packages/cx/src/data/Ref.ts @@ -0,0 +1,104 @@ +import { isFunction } from "../util/isFunction"; +import { Component } from "../util/Component"; +import { Binding } from "./Binding"; +import { View } from "./View"; +import { CanMemoize } from "./Selector"; + +export interface RefConfig { + store?: View; + path?: string; + get?: () => T; + set?: (value: T) => boolean; +} + +export class Ref extends Component implements CanMemoize { + declare isRef?: boolean; + + constructor(config: RefConfig) { + super(config); + this.get = this.get.bind(this); + if (this.set) this.set = this.set.bind(this); + } + + get(): T { + throw new Error("Ref's get method is not implemented."); + } + + set(value: T): boolean { + throw new Error("Ref's set method is not implemented."); + } + + delete(): boolean { + throw new Error("Ref's delete method is not implemented."); + } + + // Component.init() override - no-op for compatibility + init(): void; + // Initialize ref value if it's currently undefined + init(value: T): boolean; + init(value?: T): boolean | void { + if (value === undefined) return; // Component.init() compatibility + if (this.get() === undefined) return this.set(value); + return false; + } + + toggle(): boolean { + return this.set(!this.get() as T); + } + + update(cb: (currentValue: T, ...args: any[]) => T, ...args: any[]): boolean { + return this.set(cb(this.get(), ...args)); + } + + as(config: any): Ref { + return Ref.create(config, { + get: this.get, + set: this.set, + }); + } + + ref(path: string): Ref { + let binding = Binding.get(path); + return Ref.create({ + get: () => binding.value(this.get()), + set: (value: any) => { + let data = this.get(); + let newData = binding.set(data as Record, value); + if (data === newData) return false; + return this.set(newData as T); + }, + }) as Ref; + } + + //allows the function to be passed as a selector, e.g. to computable or addTrigger + memoize(): () => T { + return this.get; + } +} + +Ref.prototype.isRef = true; + +Ref.factory = function (alias: any, config?: any, more?: any): Ref { + if (isFunction(alias)) { + let cfg = { + ...config, + ...more, + }; + + if (cfg.store) Object.assign(cfg, cfg.store.getMethods()); + + let result = alias(cfg); + if (result instanceof Ref) return result; + + return this.create({ + ...config, + ...more, + ...result, + }); + } + + return this.create({ + ...config, + ...more, + }); +}; diff --git a/packages/cx/src/data/Selector.ts b/packages/cx/src/data/Selector.ts new file mode 100644 index 000000000..d5333ecfc --- /dev/null +++ b/packages/cx/src/data/Selector.ts @@ -0,0 +1,10 @@ +export interface Selector { + (data: any): T; + memoize?: (warmupData?: unknown) => Selector; +} + +export interface CanMemoize { + memoize(warmupData?: unknown): Selector; +} + +export type MemoSelector = Selector & CanMemoize; diff --git a/packages/cx/src/data/Store.d.ts b/packages/cx/src/data/Store.d.ts deleted file mode 100644 index 6a476d6e0..000000000 --- a/packages/cx/src/data/Store.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { View, ViewConfig } from "./View"; - -interface StoreConfig extends ViewConfig { - async?: boolean; - data?: D; -} - -export class Store extends View { - constructor(config?: StoreConfig); - - unsubscribeAll(): void; - - async: boolean; -} diff --git a/packages/cx/src/data/Store.js b/packages/cx/src/data/Store.js deleted file mode 100644 index fd3fe5a74..000000000 --- a/packages/cx/src/data/Store.js +++ /dev/null @@ -1,46 +0,0 @@ -import {Binding} from './Binding'; -import {SubscribableView} from './SubscribableView'; - -export class Store extends SubscribableView { - constructor(config = {}) { - super(config); - this.data = config.data || {}; - this.meta = { - version: 0 - } - } - - getData() { - return this.data; - } - - setItem(path, value) { - let next = Binding.get(path).set(this.data, value); - if (next != this.data) { - this.data = next; - this.meta.version++; - this.notify(path); - return true; - } - return false; - } - - deleteItem(path) { - let next = Binding.get(path).delete(this.data); - if (next != this.data) { - this.data = next; - this.meta.version++; - this.notify(path); - return true; - } - return false; - } - - clear() { - this.data = {}; - this.meta.version++; - this.notify(); - } -} - -Store.prototype.async = false; diff --git a/packages/cx/src/data/Store.spec.js b/packages/cx/src/data/Store.spec.js deleted file mode 100644 index e22522d5e..000000000 --- a/packages/cx/src/data/Store.spec.js +++ /dev/null @@ -1,22 +0,0 @@ -import assert from 'assert'; -import {Store} from './Store'; - -describe('Store', () => { - - const getStore = () => { - return new Store({ - data: { - a: 3, - item: { - firstName: 'Jack' - } - } - }); - }; - - it('changes version on each update', ()=> { - let store = getStore(); - store.set('a', 4); - assert.equal(store.getMeta().version, 1); - }); -}); diff --git a/packages/cx/src/data/Store.spec.ts b/packages/cx/src/data/Store.spec.ts new file mode 100644 index 000000000..fa557c6cd --- /dev/null +++ b/packages/cx/src/data/Store.spec.ts @@ -0,0 +1,22 @@ +import assert from 'assert'; +import { Store } from './Store'; + +describe('Store', () => { + + const getStore = () => { + return new Store({ + data: { + a: 3, + item: { + firstName: 'Jack' + } + } + }); + }; + + it('changes version on each update', ()=> { + let store = getStore(); + store.set('a', 4); + assert.equal(store.getMeta().version, 1); + }); +}); diff --git a/packages/cx/src/data/Store.ts b/packages/cx/src/data/Store.ts new file mode 100644 index 000000000..617765502 --- /dev/null +++ b/packages/cx/src/data/Store.ts @@ -0,0 +1,52 @@ +import { Binding } from "./Binding"; +import { SubscribableView, SubscribableViewConfig } from "./SubscribableView"; + +export interface StoreConfig extends Omit { + data?: D; +} + +export class Store = any> extends SubscribableView { + data: D; + + constructor(config: StoreConfig = {}) { + super(config); + this.data = config.data ?? ({} as D); + this.meta = { + version: 0, + }; + } + + getData(): D { + return this.data; + } + + setItem(path: string, value: any): boolean { + let next = Binding.get(path).set(this.data, value); + if (next != this.data) { + this.data = next; + this.meta.version++; + this.notify(path); + return true; + } + return false; + } + + deleteItem(path: string): boolean { + let next = Binding.get(path).delete(this.data); + if (next != this.data) { + this.data = next; + this.meta.version++; + this.notify(path); + return true; + } + return false; + } + + clear(): void { + this.data = {} as D; + this.meta.version++; + this.notify(); + } +} + +Store.prototype.async = false; diff --git a/packages/cx/src/data/StoreProxy.d.ts b/packages/cx/src/data/StoreProxy.d.ts deleted file mode 100644 index f963d4238..000000000 --- a/packages/cx/src/data/StoreProxy.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {View} from "./View"; - -export class StoreProxy extends View { - constructor(getStore: () => View) -} \ No newline at end of file diff --git a/packages/cx/src/data/StoreProxy.js b/packages/cx/src/data/StoreProxy.js deleted file mode 100644 index 95863177f..000000000 --- a/packages/cx/src/data/StoreProxy.js +++ /dev/null @@ -1,17 +0,0 @@ -import {View} from "./View"; - -export class StoreProxy extends View { - constructor(getStore) { - super({ - store: getStore() - }); - - Object.defineProperty(this, "store", { - get: getStore - }); - } - - getData() { - return this.store.getData(); - } -} \ No newline at end of file diff --git a/packages/cx/src/data/StoreProxy.ts b/packages/cx/src/data/StoreProxy.ts new file mode 100644 index 000000000..3fe110132 --- /dev/null +++ b/packages/cx/src/data/StoreProxy.ts @@ -0,0 +1,19 @@ +import { View } from "./View"; + +export class StoreProxy extends View { + declare store: View; + + constructor(getStore: () => View) { + super({ + store: getStore(), + }); + + Object.defineProperty(this, "store", { + get: getStore, + }); + } + + getData(): any { + return this.store.getData(); + } +} diff --git a/packages/cx/src/data/StoreRef.js b/packages/cx/src/data/StoreRef.js deleted file mode 100644 index 2f7dba934..000000000 --- a/packages/cx/src/data/StoreRef.js +++ /dev/null @@ -1,54 +0,0 @@ -import { isAccessorChain } from "./createAccessorModelProxy"; -import { Ref } from "./Ref"; - -export class StoreRef extends Ref { - constructor(config) { - super(config); - if (isAccessorChain(this.path)) this.path = this.path.toString(); - } - - get() { - return this.store.get(this.path); - } - - set(value) { - return this.store.set(this.path, value); - } - - init(value) { - return this.store.init(this.path, value); - } - - toggle() { - return this.store.toggle(this.path); - } - - delete() { - return this.store.delete(this.path); - } - - update(...args) { - return this.store.update(this.path, ...args); - } - - //allows the function to be passed as a selector, e.g. to computable or addTrigger - memoize() { - return this.get; - } - - ref(path) { - return new StoreRef({ - path: `${this.path}.${path}`, - store: this.store, - }); - } - - as(config) { - return StoreRef.create(config, { - path: this.path, - store: this.store, - get: this.get, - set: this.set, - }); - } -} diff --git a/packages/cx/src/data/StoreRef.spec.js b/packages/cx/src/data/StoreRef.spec.js deleted file mode 100644 index 2bcc5cbb9..000000000 --- a/packages/cx/src/data/StoreRef.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import assert from "assert"; -import { Store } from "./Store"; -import { StoreRef } from "./StoreRef"; - -const getStore = () => { - return new Store({ - data: { - person: { - name: "Jack", - }, - }, - }); -}; - -describe("StoreRef", () => { - it("can access child refs", () => { - let store = getStore(); - let person = store.ref("person"); - let name = person.ref("name"); - assert.equal(name.get("person"), "Jack"); - name.set("John"); - assert.equal(name.get("person"), "John"); - }); -}); diff --git a/packages/cx/src/data/StoreRef.spec.ts b/packages/cx/src/data/StoreRef.spec.ts new file mode 100644 index 000000000..6e4ae10cc --- /dev/null +++ b/packages/cx/src/data/StoreRef.spec.ts @@ -0,0 +1,24 @@ +import assert from "assert"; +import { Store } from "./Store"; +import { StoreRef } from "./StoreRef"; + +const getStore = () => { + return new Store({ + data: { + person: { + name: "Jack", + }, + }, + }); +}; + +describe("StoreRef", () => { + it("can access child refs", () => { + let store = getStore(); + let person = store.ref("person"); + let name = person.ref("name"); + assert.equal(name.get(), "Jack"); + name.set("John"); + assert.equal(name.get(), "John"); + }); +}); diff --git a/packages/cx/src/data/StoreRef.ts b/packages/cx/src/data/StoreRef.ts new file mode 100644 index 000000000..786431c7f --- /dev/null +++ b/packages/cx/src/data/StoreRef.ts @@ -0,0 +1,66 @@ +import { isAccessorChain } from "./createAccessorModelProxy"; +import { Ref, RefConfig } from "./Ref"; +import { View } from "./View"; + +interface StoreRefConfig extends RefConfig { + store: View; + path: string; +} + +export class StoreRef extends Ref { + declare store: View; + declare path: string; + + constructor(config: StoreRefConfig) { + super(config); + if (isAccessorChain(this.path)) this.path = this.path.toString(); + } + + get() { + return this.store.get(this.path); + } + + set(value: T): boolean { + return this.store.set(this.path, value); + } + + init(): void; + init(value: T): boolean; + init(value?: T): boolean | void { + if (value === undefined) return; + return this.store.init(this.path, value); + } + + toggle() { + return this.store.toggle(this.path); + } + + delete() { + return this.store.delete(this.path); + } + + update(cb: (currentValue: T, ...args: any[]) => T, ...args: any[]): boolean { + return this.store.update(this.path, cb, ...args); + } + + //allows the function to be passed as a selector, e.g. to computable or addTrigger + memoize() { + return this.get; + } + + ref(path: string): StoreRef { + return new StoreRef({ + path: `${this.path}.${path}`, + store: this.store, + }); + } + + as(config: RefConfig) { + return StoreRef.create(config, { + path: this.path, + store: this.store, + get: this.get, + set: this.set, + }); + } +} diff --git a/packages/cx/src/data/StringTemplate.d.ts b/packages/cx/src/data/StringTemplate.d.ts deleted file mode 100644 index 6fc87f638..000000000 --- a/packages/cx/src/data/StringTemplate.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Selector } from "../core"; - -export function stringTemplate(str: string): Selector; - -export class StringTemplate { - static get(str: string): Selector; - - static compile(str: string): Selector; - - static format(format: string, ...args): string; -} - -export function invalidateStringTemplateCache(): void; - -export function setGetStringTemplateCacheCallback(callback: () => {}): void; diff --git a/packages/cx/src/data/StringTemplate.js b/packages/cx/src/data/StringTemplate.js deleted file mode 100644 index 727467781..000000000 --- a/packages/cx/src/data/StringTemplate.js +++ /dev/null @@ -1,92 +0,0 @@ -import { expression } from "./Expression"; - -import { quoteStr } from "../util/quote"; - -function plus(str) { - return str.length ? str + " + " : str; -} - -export function stringTemplate(str) { - let tplCache = getTplCache(); - let expr = tplCache[str]; - if (expr) return expr; - - expr = ""; - - let termStart = -1, - quoteStart = 0, - term, - bracketsOpen = 0, - percentSign; - - for (let i = 0; i < str.length; i++) { - switch (str[i]) { - case "{": - if (termStart < 0) { - if (str[i + 1] == "{" && str[i - 1] != "%") { - expr = plus(expr) + quoteStr(str.substring(quoteStart, i) + "{"); - i++; - quoteStart = i + 1; - } else { - termStart = i + 1; - percentSign = str[i - 1] == "%"; - if (i > quoteStart) expr = plus(expr) + quoteStr(str.substring(quoteStart, percentSign ? i - 1 : i)); - bracketsOpen = 1; - quoteStart = i; // for the case where the brackets are not closed - } - } else bracketsOpen++; - break; - - case "}": - if (termStart >= 0) { - if (--bracketsOpen == 0) { - term = str.substring(termStart, i); - if (term.indexOf(":") == -1) { - let nullSepIndex = term.indexOf("|"); - if (nullSepIndex == -1) term += ":s"; - else term = term.substring(0, nullSepIndex) + ":s" + term.substring(nullSepIndex); - } - expr = plus(expr) + (percentSign ? "%{" : "{") + term + "}"; - termStart = -1; - quoteStart = i + 1; - bracketsOpen = 0; - } - } else if (str[i + 1] == "}") { - expr = plus(expr) + quoteStr(str.substring(quoteStart, i) + "}"); - i++; - quoteStart = i + 1; - } - break; - } - } - - if (quoteStart < str.length || expr.length == 0) expr = plus(expr) + quoteStr(str.substring(quoteStart)); - - return (tplCache[str] = expression(expr)); -} - -export const StringTemplate = { - get: function (str) { - return stringTemplate(str); - }, - - compile: function (str) { - return stringTemplate(str).memoize(); - }, - - format: function (format, ...args) { - return stringTemplate(format)(args); - }, -}; - -let tplCache = {}; - -let getTplCache = () => tplCache; - -export function invalidateStringTemplateCache() { - tplCache = {}; -} - -export function setGetStringTemplateCacheCallback(callback) { - getTplCache = callback; -} diff --git a/packages/cx/src/data/StringTemplate.spec.js b/packages/cx/src/data/StringTemplate.spec.js deleted file mode 100644 index b5dc3672b..000000000 --- a/packages/cx/src/data/StringTemplate.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import { StringTemplate } from "./StringTemplate"; -import assert from "assert"; - -describe("StringTemplate", function () { - describe("#compile()", function () { - it("returns a selector", function () { - var e = StringTemplate.compile("Hello {person.name}"); - var state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "Hello Jim"); - }); - - it("allows empty strings", function () { - let e = StringTemplate.compile(""); - assert.equal(e(), ""); - }); - - it("properly encodes ' and \"", function () { - var e = StringTemplate.compile('It\'s "working"!'); - assert.equal(e({}), 'It\'s "working"!'); - }); - - it("allows \\ before a binding", function () { - var e = StringTemplate.compile("t\\{person.name}"); - assert.equal(e({ person: { name: "Ogi" } }), "t\\Ogi"); - }); - - it("supports multi-line strings", function () { - var e = StringTemplate.compile("a\nb"); - assert.equal(e(), "a\nb"); - - var e = StringTemplate.compile("a\r\nb"); - assert.equal(e(), "a\r\nb"); - }); - }); - - describe("double brackets are used to escape brackets", function () { - it("double brackets are preserved", function () { - var e = StringTemplate.compile("Hello {{person.name}}"); - var state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "Hello {person.name}"); - }); - - it("triple brackets are converted to single brackets and a binding", function () { - var e = StringTemplate.compile("Hello {{{person.name}}}"); - var state = { - person: { - name: "Jim", - }, - }; - assert.equal(e(state), "Hello {Jim}"); - }); - - it("open brackets are ignored", function () { - var e = StringTemplate.compile("B { A"); - assert.equal(e({}), "B { A"); - }); - }); - - describe("supports formatting", function () { - it("with colon", function () { - var e = StringTemplate.compile("{str:suffix;kg}"); - assert.equal(e({ str: "5" }), "5kg"); - }); - - it("with multiple formats", function () { - var e = StringTemplate.compile("{str:suffix;kg:wrap;(;)}"); - assert.equal(e({ str: "5" }), "(5kg)"); - }); - - it("with null values", function () { - var e = StringTemplate.compile("{str:suffix;kg:|N/A}"); - assert.equal(e({ str: null }), "N/A"); - }); - - it("of null values", function () { - var e = StringTemplate.compile("{str|N/A}"); - assert.equal(e({ str: null }), "N/A"); - }); - }); - - describe("properly handles backslashes", function () { - it("in a string", function () { - var e = StringTemplate.compile("a\\b"); - assert.equal(e(), "a\\b"); - }); - - it("before a special character", function () { - var e = StringTemplate.compile("\\t"); - assert.equal(e(), "\\t"); - }); - }); - - describe("supports expressions", function () { - it("using []", function () { - var e = StringTemplate.compile("1 + 2 = {[1+2]}"); - assert.equal(e(), "1 + 2 = 3"); - }); - - it("using %", function () { - var e = StringTemplate.compile("1 + 2 = %{1+2}"); - assert.equal(e(), "1 + 2 = 3"); - }); - - it("with subexpressions", function () { - var e = StringTemplate.compile("1 + 2 = {[%{1+2}]}"); - assert.equal(e(), "1 + 2 = 3"); - }); - - it("with a conditional operator", function () { - var e = StringTemplate.compile("1 + 2 = {[true ? 3 : 2]:s}"); - assert.equal(e(), "1 + 2 = 3"); - }); - - it("with sub-expression formatting", function () { - var e = StringTemplate.compile("{[!!{person.age} ? {person.age:suffix; years old} : 'Age unknown']}"); - var state = { - person: { - age: 32, - }, - }; - assert.equal(e(state), "32 years old"); - }); - }); -}); diff --git a/packages/cx/src/data/StringTemplate.spec.ts b/packages/cx/src/data/StringTemplate.spec.ts new file mode 100644 index 000000000..20e2ad954 --- /dev/null +++ b/packages/cx/src/data/StringTemplate.spec.ts @@ -0,0 +1,132 @@ +import { StringTemplate } from "./StringTemplate"; +import assert from "assert"; + +describe("StringTemplate", function () { + describe("#compile()", function () { + it("returns a selector", function () { + var e = StringTemplate.compile("Hello {person.name}"); + var state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "Hello Jim"); + }); + + it("allows empty strings", function () { + let e = StringTemplate.compile(""); + assert.equal(e({}), ""); + }); + + it("properly encodes ' and \"", function () { + var e = StringTemplate.compile('It\'s "working"!'); + assert.equal(e({}), 'It\'s "working"!'); + }); + + it("allows \\ before a binding", function () { + var e = StringTemplate.compile("t\\{person.name}"); + assert.equal(e({ person: { name: "Ogi" } }), "t\\Ogi"); + }); + + it("supports multi-line strings", function () { + var e = StringTemplate.compile("a\nb"); + assert.equal(e({}), "a\nb"); + + var e = StringTemplate.compile("a\r\nb"); + assert.equal(e({}), "a\r\nb"); + }); + }); + + describe("double brackets are used to escape brackets", function () { + it("double brackets are preserved", function () { + var e = StringTemplate.compile("Hello {{person.name}}"); + var state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "Hello {person.name}"); + }); + + it("triple brackets are converted to single brackets and a binding", function () { + var e = StringTemplate.compile("Hello {{{person.name}}}"); + var state = { + person: { + name: "Jim", + }, + }; + assert.equal(e(state), "Hello {Jim}"); + }); + + it("open brackets are ignored", function () { + var e = StringTemplate.compile("B { A"); + assert.equal(e({}), "B { A"); + }); + }); + + describe("supports formatting", function () { + it("with colon", function () { + var e = StringTemplate.compile("{str:suffix;kg}"); + assert.equal(e({ str: "5" }), "5kg"); + }); + + it("with multiple formats", function () { + var e = StringTemplate.compile("{str:suffix;kg:wrap;(;)}"); + assert.equal(e({ str: "5" }), "(5kg)"); + }); + + it("with null values", function () { + var e = StringTemplate.compile("{str:suffix;kg:|N/A}"); + assert.equal(e({ str: null }), "N/A"); + }); + + it("of null values", function () { + var e = StringTemplate.compile("{str|N/A}"); + assert.equal(e({ str: null }), "N/A"); + }); + }); + + describe("properly handles backslashes", function () { + it("in a string", function () { + var e = StringTemplate.compile("a\\b"); + assert.equal(e({}), "a\\b"); + }); + + it("before a special character", function () { + var e = StringTemplate.compile("\\t"); + assert.equal(e({}), "\\t"); + }); + }); + + describe("supports expressions", function () { + it("using []", function () { + var e = StringTemplate.compile("1 + 2 = {[1+2]}"); + assert.equal(e({}), "1 + 2 = 3"); + }); + + it("using %", function () { + var e = StringTemplate.compile("1 + 2 = %{1+2}"); + assert.equal(e({}), "1 + 2 = 3"); + }); + + it("with subexpressions", function () { + var e = StringTemplate.compile("1 + 2 = {[%{1+2}]}"); + assert.equal(e({}), "1 + 2 = 3"); + }); + + it("with a conditional operator", function () { + var e = StringTemplate.compile("1 + 2 = {[true ? 3 : 2]:s}"); + assert.equal(e({}), "1 + 2 = 3"); + }); + + it("with sub-expression formatting", function () { + var e = StringTemplate.compile("{[!!{person.age} ? {person.age:suffix; years old} : 'Age unknown']}"); + var state = { + person: { + age: 32, + }, + }; + assert.equal(e(state), "32 years old"); + }); + }); +}); diff --git a/packages/cx/src/data/StringTemplate.ts b/packages/cx/src/data/StringTemplate.ts new file mode 100644 index 000000000..249aa45c1 --- /dev/null +++ b/packages/cx/src/data/StringTemplate.ts @@ -0,0 +1,93 @@ +import { expression } from "./Expression"; +import { MemoSelector } from "./Selector"; + +import { quoteStr } from "../util/quote"; + +function plus(str: string) { + return str.length ? str + " + " : str; +} + +export function stringTemplate(str: string): MemoSelector { + let tplCache = getTplCache(); + let cached = tplCache[str]; + if (cached) return cached; + + let expr = ""; + + let termStart = -1, + quoteStart = 0, + term: string, + bracketsOpen = 0, + percentSign: boolean = false; + + for (let i = 0; i < str.length; i++) { + switch (str[i]) { + case "{": + if (termStart < 0) { + if (str[i + 1] == "{" && str[i - 1] != "%") { + expr = plus(expr) + quoteStr(str.substring(quoteStart, i) + "{"); + i++; + quoteStart = i + 1; + } else { + termStart = i + 1; + percentSign = str[i - 1] == "%"; + if (i > quoteStart) expr = plus(expr) + quoteStr(str.substring(quoteStart, percentSign ? i - 1 : i)); + bracketsOpen = 1; + quoteStart = i; // for the case where the brackets are not closed + } + } else bracketsOpen++; + break; + + case "}": + if (termStart >= 0) { + if (--bracketsOpen == 0) { + term = str.substring(termStart, i); + if (term.indexOf(":") == -1) { + let nullSepIndex = term.indexOf("|"); + if (nullSepIndex == -1) term += ":s"; + else term = term.substring(0, nullSepIndex) + ":s" + term.substring(nullSepIndex); + } + expr = plus(expr) + (percentSign ? "%{" : "{") + term + "}"; + termStart = -1; + quoteStart = i + 1; + bracketsOpen = 0; + } + } else if (str[i + 1] == "}") { + expr = plus(expr) + quoteStr(str.substring(quoteStart, i) + "}"); + i++; + quoteStart = i + 1; + } + break; + } + } + + if (quoteStart < str.length || expr.length == 0) expr = plus(expr) + quoteStr(str.substring(quoteStart)); + + return (tplCache[str] = expression(expr)); +} + +export const StringTemplate = { + get: function (str: string) { + return stringTemplate(str); + }, + + compile: function (str: string) { + return stringTemplate(str).memoize(); + }, + + format: function (format: string, ...args: any[]) { + return stringTemplate(format)(args); + }, +}; + +let tplCache: Record = {}; + +let getTplCache = () => tplCache; + +export function invalidateStringTemplateCache() { + tplCache = {}; +} + +export function setGetStringTemplateCacheCallback(callback: () => Record) { + getTplCache = callback; +} diff --git a/packages/cx/src/data/StructuredDataAccessor.d.ts b/packages/cx/src/data/StructuredDataAccessor.d.ts deleted file mode 100644 index 722606b67..000000000 --- a/packages/cx/src/data/StructuredDataAccessor.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface StructuredDataAccessor { - getSelector(): (data: Cx.Record) => Cx.Record; - get(): Cx.Record; - setItem(key: string, value: any): boolean; - containsKey(key): string; - getKeys(): string[]; -} diff --git a/packages/cx/src/data/StructuredSelector.d.ts b/packages/cx/src/data/StructuredSelector.d.ts deleted file mode 100644 index 92b44bc7f..000000000 --- a/packages/cx/src/data/StructuredSelector.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { StructuredProp, Record, Selector } from "../core"; -import { View } from "./View"; - -interface StructuredSelectorConfig { - props: StructuredProp; - values: Record; -} - -export class StructuredSelector { - constructor(config: StructuredSelectorConfig); - - init(store: View); - - create(memoize: boolean = true): Selector; - - createStoreSelector(): (store: View) => Record; -} diff --git a/packages/cx/src/data/StructuredSelector.js b/packages/cx/src/data/StructuredSelector.js deleted file mode 100644 index 8e5cad3fe..000000000 --- a/packages/cx/src/data/StructuredSelector.js +++ /dev/null @@ -1,132 +0,0 @@ -import { Binding } from "./Binding"; -import { Expression } from "./Expression"; -import { StringTemplate } from "./StringTemplate"; -import { createStructuredSelector } from "../data/createStructuredSelector"; -import { getSelector } from "../data/getSelector"; -import { isFunction } from "../util/isFunction"; -import { isUndefined } from "../util/isUndefined"; -import { isDefined } from "../util/isDefined"; -import { isArray } from "../util/isArray"; -import { isAccessorChain } from "./createAccessorModelProxy"; -import { isString } from "../util/isString"; - -function defaultValue(pv) { - if (typeof pv == "object" && pv && pv.structured) return pv.defaultValue; - - return pv; -} - -function getSelectorConfig(props, values, nameMap) { - let functions = {}, - structures = {}, - defaultValues = {}, - constants, - p, - v, - pv, - constant = true; - - for (p in props) { - v = values[p]; - pv = props[p]; - - if (isUndefined(v) && pv && (pv.bind || pv.tpl || pv.expr)) v = pv; - - if (v === null) { - if (!constants) constants = {}; - constants[p] = null; - } else if (typeof v == "object") { - if (v.bind) { - if (isUndefined(v.defaultValue) && v != pv) v.defaultValue = defaultValue(pv); - if (isDefined(v.defaultValue)) defaultValues[v.bind] = v.defaultValue; - nameMap[p] = v.bind; - functions[p] = Binding.get(v.bind).value; - constant = false; - } else if (v.expr) { - functions[p] = Expression.get(v.expr); - constant = false; - } else if (v.get) { - functions[p] = v.get; - constant = false; - } else if (isString(v.tpl)) { - functions[p] = StringTemplate.get(v.tpl); - constant = false; - } else if (pv && typeof pv == "object" && pv.structured) { - if (isArray(v)) functions[p] = getSelector(v); - else { - let s = getSelectorConfig(v, v, {}); - structures[p] = s; - Object.assign(defaultValues, s.defaultValues); - } - constant = false; - } else { - if (!constants) constants = {}; - constants[p] = v; - } - } else if (isFunction(v)) { - if (isAccessorChain(v)) { - let path = v.toString(); - nameMap[p] = path; - functions[p] = Binding.get(path).value; - } else functions[p] = v; - constant = false; - } else { - if (isUndefined(v)) { - if (isUndefined(pv)) continue; - v = defaultValue(pv); - } - - if (isUndefined(v)) continue; - - if (!constants) constants = {}; - - constants[p] = v; - } - } - - return { - functions, - structures, - defaultValues, - constants, - constant, - }; -} - -function createSelector({ functions, structures, constants, defaultValues }) { - let selector = {}; - - for (let n in functions) { - selector[n] = functions[n]; - } - - for (let n in structures) selector[n] = createSelector(structures[n]); - - return createStructuredSelector(selector, constants); -} - -export class StructuredSelector { - constructor({ props, values }) { - this.nameMap = {}; - this.config = getSelectorConfig(props, values, this.nameMap); - } - - init(store) { - store.init(this.config.defaultValues); - } - - create(memoize = true) { - let selector = createSelector(this.config); - if (memoize && selector.memoize) return selector.memoize(); - return selector; - } - - createStoreSelector() { - if (this.config.constant) { - let result = { ...this.config.constants }; - return () => result; - } - let selector = this.create(); - return (store) => selector(store.getData()); - } -} diff --git a/packages/cx/src/data/StructuredSelector.spec.js b/packages/cx/src/data/StructuredSelector.spec.js deleted file mode 100644 index 9044fa5fa..000000000 --- a/packages/cx/src/data/StructuredSelector.spec.js +++ /dev/null @@ -1,113 +0,0 @@ -import { StructuredSelector } from "./StructuredSelector"; -import assert from "assert"; -import { createAccessorModelProxy } from "./createAccessorModelProxy"; - -describe("StructuredSelector", function () { - describe("#create()", function () { - it("constants", function () { - var x = {}; - var s = new StructuredSelector({ - props: { - a: undefined, - b: undefined, - }, - values: { - a: 1, - b: 2, - }, - }).create(x); - - assert.deepEqual(s(x), { a: 1, b: 2 }); - }); - - it("bindings", function () { - var x = { a: 1, b: 2 }; - var s = new StructuredSelector({ - props: { - a: undefined, - b: undefined, - }, - values: { - a: { bind: "b" }, - b: { bind: "a" }, - }, - }).create(x); - - assert.deepEqual(s(x), { a: 2, b: 1 }); - }); - - it("templates", function () { - var x = { a: 1, b: 2 }; - var s = new StructuredSelector({ - props: { - a: undefined, - b: undefined, - }, - values: { - a: { tpl: "b{a}" }, - b: { tpl: "a{b}" }, - }, - }).create(x); - - assert.deepEqual(s(x), { a: "b1", b: "a2" }); - }); - - it("structured", function () { - var x = { a: 1, b: 2 }; - var s = new StructuredSelector({ - props: { - a: { - structured: true, - }, - b: undefined, - }, - values: { - a: { - x: { expr: "{a} == 1" }, - y: { expr: "{b} == 1" }, - }, - b: { tpl: "a{b}" }, - }, - }).create(x); - - assert.deepEqual(s(x), { a: { x: true, y: false }, b: "a2" }); - }); - }); - - it("structures do not change if data doesn't change", function () { - var x = { a: 1, b: 2 }; - var s = new StructuredSelector({ - props: { - a: { - structured: true, - }, - }, - values: { - a: { - x: { expr: "{a} == 1" }, - y: { expr: "{b} == 1" }, - }, - b: { tpl: "a{b}" }, - }, - }).create(x); - - let r1 = s(x); - let r2 = s(x); - - assert.equal(r1, r2); - }); - - it("structures do not change if data doesn't change", function () { - var x = { a: { b: 2 } }; - var m = createAccessorModelProxy(); - var s = new StructuredSelector({ - props: { - b: undefined, - }, - values: { - b: m.a.b, - }, - }).create(x); - assert.deepEqual(s(x), { b: 2 }); - }); -}); diff --git a/packages/cx/src/data/StructuredSelector.spec.ts b/packages/cx/src/data/StructuredSelector.spec.ts new file mode 100644 index 000000000..7a146bae3 --- /dev/null +++ b/packages/cx/src/data/StructuredSelector.spec.ts @@ -0,0 +1,113 @@ +import { StructuredSelector } from "./StructuredSelector"; +import assert from "assert"; +import { createAccessorModelProxy } from "./createAccessorModelProxy"; + +describe("StructuredSelector", function () { + describe("#create()", function () { + it("constants", function () { + var x = {}; + var s = new StructuredSelector({ + props: { + a: undefined, + b: undefined, + }, + values: { + a: 1, + b: 2, + }, + }).create(); + + assert.deepEqual(s(x), { a: 1, b: 2 }); + }); + + it("bindings", function () { + var x = { a: 1, b: 2 }; + var s = new StructuredSelector({ + props: { + a: undefined, + b: undefined, + }, + values: { + a: { bind: "b" }, + b: { bind: "a" }, + }, + }).create(); + + assert.deepEqual(s(x), { a: 2, b: 1 }); + }); + + it("templates", function () { + var x = { a: 1, b: 2 }; + var s = new StructuredSelector({ + props: { + a: undefined, + b: undefined, + }, + values: { + a: { tpl: "b{a}" }, + b: { tpl: "a{b}" }, + }, + }).create(); + + assert.deepEqual(s(x), { a: "b1", b: "a2" }); + }); + + it("structured", function () { + var x = { a: 1, b: 2 }; + var s = new StructuredSelector({ + props: { + a: { + structured: true, + }, + b: undefined, + }, + values: { + a: { + x: { expr: "{a} == 1" }, + y: { expr: "{b} == 1" }, + }, + b: { tpl: "a{b}" }, + }, + }).create(); + + assert.deepEqual(s(x), { a: { x: true, y: false }, b: "a2" }); + }); + }); + + it("structures do not change if data doesn't change", function () { + var x = { a: 1, b: 2 }; + var s = new StructuredSelector({ + props: { + a: { + structured: true, + }, + }, + values: { + a: { + x: { expr: "{a} == 1" }, + y: { expr: "{b} == 1" }, + }, + b: { tpl: "a{b}" }, + }, + }).create(); + + let r1 = s(x); + let r2 = s(x); + + assert.equal(r1, r2); + }); + + it("accessor model proxy works", function () { + var x = { a: { b: 2 } }; + var m = createAccessorModelProxy(); + var s = new StructuredSelector({ + props: { + b: undefined, + }, + values: { + b: m.a.b, + }, + }).create(); + assert.deepEqual(s(x), { b: 2 }); + }); +}); diff --git a/packages/cx/src/data/StructuredSelector.ts b/packages/cx/src/data/StructuredSelector.ts new file mode 100644 index 000000000..b56731f4b --- /dev/null +++ b/packages/cx/src/data/StructuredSelector.ts @@ -0,0 +1,146 @@ +import { Binding } from "./Binding"; +import { Expression } from "./Expression"; +import { StringTemplate } from "./StringTemplate"; +import { createStructuredSelector } from "../data/createStructuredSelector"; +import { getSelector } from "../data/getSelector"; +import { isFunction } from "../util/isFunction"; +import { isUndefined } from "../util/isUndefined"; +import { isDefined } from "../util/isDefined"; +import { isArray } from "../util/isArray"; +import { isAccessorChain } from "./createAccessorModelProxy"; +import { isString } from "../util/isString"; +import { View } from "./View"; + +function defaultValue(pv: any) { + if (typeof pv == "object" && pv && pv.structured) return pv.defaultValue; + + return pv; +} + +function getSelectorConfig(props: any, values: any, nameMap: any) { + let functions: Record = {}, + structures: Record = {}, + defaultValues: Record = {}, + constants: Record | undefined, + p: string, + v: any, + pv: any, + constant = true; + + for (p in props) { + v = values[p]; + pv = props[p]; + + if (isUndefined(v) && pv && (pv.bind || pv.tpl || pv.expr)) v = pv; + + if (v === null) { + if (!constants) constants = {}; + constants[p] = null; + } else if (typeof v == "object") { + if (v.bind) { + if (isUndefined(v.defaultValue) && v != pv) v.defaultValue = defaultValue(pv); + if (isDefined(v.defaultValue)) defaultValues[v.bind] = v.defaultValue; + nameMap[p] = v.bind; + functions[p] = Binding.get(v.bind).value; + constant = false; + } else if (v.expr) { + functions[p] = Expression.get(v.expr); + constant = false; + } else if (v.get) { + functions[p] = v.get; + constant = false; + } else if (isString(v.tpl)) { + functions[p] = StringTemplate.get(v.tpl); + constant = false; + } else if (pv && typeof pv == "object" && pv.structured) { + if (isArray(v)) functions[p] = getSelector(v); + else { + let s = getSelectorConfig(v, v, {}); + structures[p] = s; + Object.assign(defaultValues, s.defaultValues); + } + constant = false; + } else { + if (!constants) constants = {}; + constants[p] = v; + } + } else if (isFunction(v)) { + if (isAccessorChain(v)) { + let path = v.toString(); + nameMap[p] = path; + functions[p] = Binding.get(path).value; + } else functions[p] = v; + constant = false; + } else { + if (isUndefined(v)) { + if (isUndefined(pv)) continue; + v = defaultValue(pv); + } + + if (isUndefined(v)) continue; + + if (!constants) constants = {}; + + constants[p] = v; + } + } + + return { + functions, + structures, + defaultValues, + constants, + constant, + }; +} + +function createSelector({ + functions, + structures, + constants, + defaultValues, +}: { + functions: any; + structures: any; + constants: any; + defaultValues: any; +}) { + let selector: Record = {}; + + for (let n in functions) { + selector[n] = functions[n]; + } + + for (let n in structures) selector[n] = createSelector(structures[n]); + + return createStructuredSelector(selector, constants); +} + +export class StructuredSelector { + nameMap: Record; + config: any; + + constructor({ props, values }: { props: any; values: any }) { + this.nameMap = {}; + this.config = getSelectorConfig(props, values, this.nameMap); + } + + init(store: View) { + store.init(this.config.defaultValues); + } + + create(memoize = true) { + let selector = createSelector(this.config); + if (memoize && selector.memoize) return selector.memoize(); + return selector; + } + + createStoreSelector() { + if (this.config.constant) { + let result = { ...this.config.constants }; + return () => result; + } + let selector = this.create(); + return (store: View) => selector(store.getData()); + } +} diff --git a/packages/cx/src/data/SubscribableView.d.ts b/packages/cx/src/data/SubscribableView.d.ts deleted file mode 100644 index 043247611..000000000 --- a/packages/cx/src/data/SubscribableView.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {View, ViewConfig} from './View'; - - - -export class SubscribableView extends View { - constructor(config?: ViewConfig); - - unsubscribeAll(): void; - - async: boolean; -} diff --git a/packages/cx/src/data/SubscribableView.js b/packages/cx/src/data/SubscribableView.js deleted file mode 100644 index 43ec41008..000000000 --- a/packages/cx/src/data/SubscribableView.js +++ /dev/null @@ -1,54 +0,0 @@ -import { View } from "./View"; -import { SubscriberList } from "../util/SubscriberList"; - -export class SubscribableView extends View { - constructor(config) { - super(config); - this.subscribers = new SubscriberList(); - this.changes = []; - } - - subscribe(callback) { - return this.subscribers.subscribe(callback); - } - - unsubscribeAll() { - this.subscribers.clear(); - } - - doNotify(path) { - if (this.notificationsSuspended) return; - - if (!this.async) { - this.subscribers.notify([path]); - } else { - this.changes.push(path || ""); - if (!this.scheduled) { - this.scheduled = true; - setTimeout(() => { - this.scheduled = false; - let changes = this.changes; - this.changes = []; - this.subscribers.notify(changes); - }, 0); - } - } - } - - silently(callback) { - this.notificationsSuspended = (this.notificationsSuspended || 0) + 1; - let wasDirty = this.dirty, - dirty; - this.dirty = false; - try { - callback(this); - } finally { - this.notificationsSuspended--; - dirty = this.dirty; - this.dirty = wasDirty; - } - return dirty; - } -} - -SubscribableView.prototype.async = false; diff --git a/packages/cx/src/data/SubscribableView.ts b/packages/cx/src/data/SubscribableView.ts new file mode 100644 index 000000000..0ffd8b220 --- /dev/null +++ b/packages/cx/src/data/SubscribableView.ts @@ -0,0 +1,63 @@ +import { View, ViewConfig } from "./View"; +import { SubscriberList } from "../util/SubscriberList"; + +export interface SubscribableViewConfig extends ViewConfig { + async?: boolean; +} + +export class SubscribableView extends View { + subscribers?: any; + changes: string[]; + declare async: boolean; + scheduled: boolean; + + constructor(config?: SubscribableViewConfig) { + super(config); + this.subscribers = new SubscriberList(); + this.changes = []; + } + + subscribe(callback: () => void) { + return this.subscribers.subscribe(callback); + } + + unsubscribeAll() { + this.subscribers.clear(); + } + + doNotify(path: string) { + if (this.notificationsSuspended) return; + + if (!this.async) { + this.subscribers.notify([path]); + } else { + this.changes.push(path || ""); + if (!this.scheduled) { + this.scheduled = true; + setTimeout(() => { + this.scheduled = false; + let changes = this.changes; + this.changes = []; + this.subscribers.notify(changes); + }, 0); + } + } + } + + silently(callback: (store?: View) => void) { + this.notificationsSuspended = (this.notificationsSuspended || 0) + 1; + let wasDirty = this.dirty, + dirty; + this.dirty = false; + try { + callback(this); + } finally { + this.notificationsSuspended--; + dirty = this.dirty; + this.dirty = wasDirty; + } + return dirty; + } +} + +SubscribableView.prototype.async = false; diff --git a/packages/cx/src/data/View.d.ts b/packages/cx/src/data/View.d.ts deleted file mode 100644 index 344f101cd..000000000 --- a/packages/cx/src/data/View.d.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { AccessorChain } from "../core"; -import { Binding } from "./Binding"; -import { Ref } from "./Ref"; - -declare type Path = string | Binding; - -export interface ViewConfig { - store?: View; - - /* When set, the root data object of the parent store will be preserved (no virtual properties will be added), i.e. $record. */ - immutable?: boolean; - - /* When set, instructs the child views not to modify its data object (same effect as setting immutable on child stores). */ - sealed?: boolean; -} - -export interface ViewMethods> { - getData(): D; - - init(path: Path, value: any): boolean; - init(path: AccessorChain, value: V): boolean; - - set(path: Path, value: any): boolean; - set>(changes: T): boolean; - set(path: AccessorChain, value: V): boolean; - - get(path: Path): any; - get(paths: Path[]): any[]; - get(...paths: Path[]): any[]; - get(path: AccessorChain): V; - get(...paths: { [K in keyof T]: AccessorChain }): T; - get(paths: { [K in keyof T]: AccessorChain }): T; - - /** - * Removes data from the Store. - * @param paths - One or more paths to be deleted. - * @return {boolean} - */ - delete(path: Path): boolean; - delete(paths: Path[]): boolean; - delete(...paths: Path[]): boolean; - delete(path: AccessorChain): boolean; - delete(...paths: { [K in keyof T]: AccessorChain }): boolean; - delete(paths: { [K in keyof T]: AccessorChain }): boolean; - - toggle(path: Path): boolean; - - update(updateFn: (currentValue: D, ...args) => D, ...args): boolean; - update(path: Path, updateFn: (currentValue: any, ...args) => any, ...args): boolean; - update( - path: AccessorChain, - updateFn: (currentValue: V, ...args: A) => V, - ...args: A - ): boolean; - - /** - * Mutates the content of the store using Immer - */ - mutate(updateFn: (currentValue: D, ...args) => D, ...args): boolean; - mutate(path: Path, updateFn: (currentValue: any, ...args) => any, ...args): boolean; - mutate( - path: AccessorChain, - updateFn: (currentValue: V, ...args: A) => void, - ...args: A - ): boolean; - - ref(path: string | AccessorChain, defaultValue?: T): Ref; -} - -export class View implements ViewMethods { - constructor(config?: ViewConfig); - - getData(): D; - - init(path: Path, value: any): boolean; - init(path: AccessorChain, value: V): boolean; - - set(path: Path, value: any): boolean; - set>(changes: T): boolean; - set(path: AccessorChain, value: V): boolean; - - /** - * Copies the value stored under the `from` path and saves it under the `to` path. - * @param from - Origin path. - * @param to - Destination path. - */ - copy(from: Path, to: Path): boolean; - copy(from: AccessorChain, to: AccessorChain): boolean; - - move(from: Path, to: Path): boolean; - move(from: AccessorChain, to: AccessorChain): boolean; - - /** - * Removes data from the Store. - * @param paths - Any number or an array of paths to be deleted. - * @return {boolean} - */ - delete(path: Path): boolean; - delete(paths: Path[]): boolean; - delete(...paths: Path[]): boolean; - delete(path: AccessorChain): boolean; - delete(...paths: { [K in keyof T]: AccessorChain }): boolean; - delete(paths: { [K in keyof T]: AccessorChain }): boolean; - - clear(): void; - - get(path: Path): any; - get(paths: Path[]): any; - get(...paths: Path[]): any; - get(path: AccessorChain): V; - get(...paths: { [K in keyof T]: AccessorChain }): T; - get(paths: { [K in keyof T]: AccessorChain }): T; - - toggle(path: Path): boolean; - toggle(path: AccessorChain): boolean; - - update(updateFn: (currentValue: D, ...args) => any, ...args): boolean; - update(path: Path, updateFn: (currentValue: any, ...args) => any, ...args): boolean; - update( - path: AccessorChain, - updateFn: (currentValue: V, ...args: A) => V, - ...args: A - ): boolean; - - mutate(updateFn: (currentValue: D, ...args) => void, ...args): boolean; - mutate(path: Path, updateFn: (currentValue: any, ...args) => void, ...args): boolean; - mutate( - path: AccessorChain, - updateFn: (currentValue: V, ...args: A) => void, - ...args: A - ): boolean; - - /** - * `batch` method can be used to perform multiple Store operations silently - * and re-render the application only once afterwards. The Store instance - * is passed to the `callback` function. - * @param callback - Function that will perform multiple Store operations - * @return {boolean} - */ - batch(callback: (store: View) => void): boolean; - - silently(callback: () => void): boolean; - - notify(path?: string): void; - - subscribe(callback: (changes?) => void): () => void; - - load(data: Record): boolean; - - dispatch(action): void; - - getMethods(): ViewMethods; - - ref(path: string | AccessorChain, defaultValue?: T): Ref; -} diff --git a/packages/cx/src/data/View.js b/packages/cx/src/data/View.js deleted file mode 100644 index 74f438952..000000000 --- a/packages/cx/src/data/View.js +++ /dev/null @@ -1,182 +0,0 @@ -import { Binding } from "./Binding"; -import { isArray } from "../util/isArray"; -import { isDefined } from "../util/isDefined"; -import { StoreRef } from "./StoreRef"; -import { isObject } from "../util/isObject"; -import { isFunction } from "../util/isFunction"; - -export class View { - constructor(config) { - Object.assign(this, config); - this.cache = { - version: -1, - }; - if (this.store) this.setStore(this.store); - } - - getData() { - throw new Error("abstract method"); - } - - init(path, value) { - if (typeof path == "object" && path != null) { - let changed = false; - for (let key in path) - if (path.hasOwnProperty(key) && this.get(key) === undefined && this.setItem(key, path[key])) changed = true; - return changed; - } - let binding = Binding.get(path); - if (this.get(binding.path) === undefined) return this.setItem(binding.path, value); - return false; - } - - set(path, value) { - if (isObject(path)) { - let changed = false; - for (let key in path) if (path.hasOwnProperty(key) && this.setItem(key, path[key])) changed = true; - return changed; - } - let binding = Binding.get(path); - return this.setItem(binding.path, value); - } - - copy(from, to) { - let value = this.get(from); - this.set(to, value); - } - - move(from, to) { - this.batch(() => { - this.copy(from, to); - this.delete(from); - }); - } - - //protected - setItem(path, value) { - if (this.store) return this.store.setItem(path, value); - throw new Error("abstract method"); - } - - delete(path) { - if (arguments.length > 1) path = Array.from(arguments); - if (isArray(path)) return path.map((arg) => this.delete(arg)).some(Boolean); - - let binding = Binding.get(path); - return this.deleteItem(binding.path); - } - - //protected - deleteItem(path) { - if (this.store) return this.store.deleteItem(path); - - throw new Error("abstract method"); - } - - clear() { - if (this.store) return this.store.clear(); - - throw new Error("abstract method"); - } - - get(path) { - let storeData = this.getData(); - - if (arguments.length > 1) path = Array.from(arguments); - - if (isArray(path)) return path.map((arg) => Binding.get(arg).value(storeData)); - - return Binding.get(path).value(storeData); - } - - toggle(path) { - return this.set(path, !this.get(path)); - } - - update(path, updateFn, ...args) { - if (arguments.length == 1 && isFunction(path)) - return this.load(path.apply(null, [this.getData(), updateFn, ...args])); - return this.set(path, updateFn.apply(null, [this.get(path), ...args])); - } - - batch(callback) { - let dirty = this.silently(callback); - if (dirty) this.notify(); - return dirty; - } - - silently(callback) { - if (this.store) return this.store.silently(callback); - - throw new Error("abstract method"); - } - - notify(path) { - if (this.notificationsSuspended) this.dirty = true; - else this.doNotify(path); - } - - doNotify(path) { - if (this.store) return this.store.notify(path); - - throw new Error("abstract method"); - } - - subscribe(callback) { - if (this.store) return this.store.subscribe(callback); - - throw new Error("abstract method"); - } - - load(data) { - return this.batch((store) => { - for (let key in data) store.set(key, data[key]); - }); - } - - dispatch(action) { - if (this.store) return this.store.dispatch(action); - - throw new Error("The underlying store doesn't support dispatch."); - } - - getMeta() { - return this.meta; - } - - setStore(store) { - this.store = store; - this.meta = store.getMeta(); - } - - ref(path, defaultValue) { - if (isDefined(defaultValue)) this.init(path, defaultValue); - return StoreRef.create({ - store: this, - path, - }); - } - - getMethods() { - return { - getData: this.getData.bind(this), - set: this.set.bind(this), - get: this.get.bind(this), - update: this.update.bind(this), - delete: this.delete.bind(this), - toggle: this.toggle.bind(this), - init: this.init.bind(this), - ref: this.ref.bind(this), - mutate: this.ref.bind(this), - }; - } -} - -View.prototype.sealed = false; //indicate that data should be copied before virtual items are added - -//Immer integration point -View.prototype.mutate = function () { - throw new Error( - "Mutate requires Immer. Please install 'immer' and 'cx-immer' packages and enable store mutation by calling enableImmerMutate()." - ); -}; diff --git a/packages/cx/src/data/View.spec.js b/packages/cx/src/data/View.spec.ts similarity index 100% rename from packages/cx/src/data/View.spec.js rename to packages/cx/src/data/View.spec.ts diff --git a/packages/cx/src/data/View.ts b/packages/cx/src/data/View.ts new file mode 100644 index 000000000..b8cb4f8cd --- /dev/null +++ b/packages/cx/src/data/View.ts @@ -0,0 +1,289 @@ +import { Binding } from "./Binding"; +import { isArray } from "../util/isArray"; +import { isDefined } from "../util/isDefined"; +import { StoreRef } from "./StoreRef"; +import { isObject } from "../util/isObject"; +import { isFunction } from "../util/isFunction"; +import { AccessorChain } from "./createAccessorModelProxy"; +import { Ref } from "./Ref"; + +type Path = string | Binding; + +export interface ViewConfig { + store?: View; + immutable?: boolean; + sealed?: boolean; +} + +export interface ViewMethods> { + getData(): D; + + init(path: Path, value: unknown): boolean; + init(path: AccessorChain, value: V): boolean; + init>(initObject: T): boolean; + + set(path: Path, value: any): boolean; + set>(changes: T): boolean; + set(path: AccessorChain, value: V): boolean; + + get(path: Path): any; + get(paths: Path[]): any[]; + get(...paths: Path[]): any[]; + get(path: AccessorChain): V; + get(...paths: { [K in keyof T]: AccessorChain }): T; + get(paths: { [K in keyof T]: AccessorChain }): T; + + /** + * Removes data from the Store. + * @param paths - One or more paths to be deleted. + * @return {boolean} + */ + delete(path: Path): boolean; + delete(paths: Path[]): boolean; + delete(...paths: Path[]): boolean; + delete(path: AccessorChain): boolean; + delete(...paths: { [K in keyof T]: AccessorChain }): boolean; + delete(paths: { [K in keyof T]: AccessorChain }): boolean; + + toggle(path: Path): boolean; + toggle(path: AccessorChain): boolean; + + update(updateFn: (currentValue: D, ...args: any[]) => any, ...args: any): boolean; + update(path: Path, updateFn: (currentValue: any, ...args: A) => any, ...args: A): boolean; + update( + path: AccessorChain, + updateFn: (currentValue: V, ...args: A) => V, + ...args: A + ): boolean; + + /** + * Mutates the content of the store using Immer + */ + mutate(updateFn: (currentValue: D, ...args: any[]) => void, ...args: any[]): boolean; + mutate(path: Path, updateFn: (currentValue: any, ...args: A) => void, ...args: A): boolean; + mutate( + path: AccessorChain, + updateFn: (currentValue: V, ...args: A) => void, + ...args: A + ): boolean; + + ref(path: string | AccessorChain, defaultValue?: T): Ref; +} + +export class View implements ViewMethods { + declare store?: View; + declare meta: any; + declare sealed: boolean; + cache: { version: number; data?: any; result?: any; itemIndex?: number; key?: string; parentStoreData?: any }; + notificationsSuspended: number = 0; + dirty: boolean = false; + + constructor(config?: ViewConfig) { + Object.assign(this, config); + this.cache = { + version: -1, + }; + if (this.store) this.setStore(this.store); + } + + getData(): D { + throw new Error("abstract method"); + } + + init(path: Path, value: unknown): boolean; + init(path: AccessorChain, value: V): boolean; + init>(initObject: T): boolean; + init(path: any, value?: any): boolean { + if (typeof path == "object" && path != null) { + let changed = false; + for (let key in path) + if (path.hasOwnProperty(key) && this.get(key) === undefined && this.setItem(key, path[key])) changed = true; + return changed; + } + let binding = Binding.get(path); + if (this.get(binding.path) === undefined) return this.setItem(binding.path, value); + return false; + } + + set(path: Path, value: any): boolean; + set>(changes: T): boolean; + set(path: AccessorChain, value: V): boolean; + set(path: any, value?: any): boolean { + if (typeof path == "object" && path != null) { + let changed = false; + for (let key in path) if (path.hasOwnProperty(key) && this.setItem(key, path[key])) changed = true; + return changed; + } + let binding = Binding.get(path); + return this.setItem(binding.path, value); + } + + copy(from: Path, to: Path): boolean; + copy(from: AccessorChain, to: AccessorChain): boolean; + copy(from: any, to: any): boolean { + let value = this.get(from); + return this.set(to, value); + } + + move(from: Path, to: Path): boolean; + move(from: AccessorChain, to: AccessorChain): boolean; + move(from: any, to: any): boolean { + return this.batch(() => { + this.copy(from, to); + this.delete(from); + }); + } + + //protected + setItem(path: string, value: any): boolean { + if (this.store) return this.store.setItem(path, value); + throw new Error("abstract method"); + } + + delete(path: Path): boolean; + delete(paths: Path[]): boolean; + delete(...paths: Path[]): boolean; + delete(path: AccessorChain): boolean; + delete(...paths: { [K in keyof T]: AccessorChain }): boolean; + delete(paths: { [K in keyof T]: AccessorChain }): boolean; + delete(path?: any): boolean { + if (arguments.length > 1) path = Array.from(arguments); + if (isArray(path)) return path.map((arg) => this.delete(arg as Path)).some(Boolean); + + let binding = Binding.get(path); + return this.deleteItem(binding.path); + } + + //protected + deleteItem(path: string): boolean { + if (this.store) return this.store.deleteItem(path); + + throw new Error("abstract method"); + } + + clear(): void { + if (this.store) return this.store.clear(); + + throw new Error("abstract method"); + } + + get(path: Path): any; + get(paths: Path[]): any[]; + get(...paths: Path[]): any[]; + get(path: AccessorChain): V; + get(...paths: { [K in keyof T]: AccessorChain }): T; + get(paths: { [K in keyof T]: AccessorChain }): T; + get(path?: any, ...args: any[]): any { + let storeData = this.getData(); + + if (arguments.length > 1) path = Array.from(arguments); + + if (isArray(path)) return path.map((arg) => Binding.get(arg as any).value(storeData)); + + return Binding.get(path).value(storeData); + } + + toggle(path: Path): boolean; + toggle(path: AccessorChain): boolean; + toggle(path: any): boolean { + return this.set(path, !this.get(path)); + } + + update(updateFn: (currentValue: D, ...args: any[]) => any, ...args: any): boolean; + update(path: Path, updateFn: (currentValue: any, ...args: A) => any, ...args: A): boolean; + update( + path: AccessorChain, + updateFn: (currentValue: V, ...args: A) => V, + ...args: A + ): boolean; + update(path: any, updateFn?: any, ...args: any[]): boolean { + if (arguments.length == 1 && isFunction(path)) + return this.load(path.apply(null, [this.getData(), updateFn, ...args])); + return this.set(path, updateFn.apply(null, [this.get(path), ...args])); + } + + mutate(updateFn: (currentValue: D, ...args: any[]) => void, ...args: any[]): boolean; + mutate(path: Path, updateFn: (currentValue: any, ...args: A) => void, ...args: A): boolean; + mutate( + path: AccessorChain, + updateFn: (currentValue: V, ...args: A) => void, + ...args: A + ): boolean; + mutate(path?: any, updateFn?: any, ...args: any[]): boolean { + throw new Error( + "Mutate requires Immer. Please install 'immer' and 'cx-immer' packages and enable store mutation by calling enableImmerMutate().", + ); + } + + batch(callback: (store?: View) => void): boolean { + let dirty = this.silently(callback); + if (dirty) this.notify(); + return dirty; + } + + silently(callback: (store?: View) => void): boolean { + if (this.store) return this.store.silently(callback); + + throw new Error("abstract method"); + } + + notify(path?: string): void { + if (this.notificationsSuspended) this.dirty = true; + else this.doNotify(path); + } + + doNotify(path?: string): void { + if (this.store) return this.store.notify(path); + + throw new Error("abstract method"); + } + + subscribe(callback: (changes?: any) => void): () => void { + if (this.store) return this.store.subscribe(callback); + + throw new Error("abstract method"); + } + + load(data: Record): boolean { + return this.set(data); + } + + dispatch(action: any): void { + if (this.store) return this.store.dispatch(action); + + throw new Error("The underlying store doesn't support dispatch."); + } + + getMeta() { + return this.meta; + } + + setStore(store: View) { + this.store = store; + this.meta = store.getMeta(); + } + + ref(path: string | AccessorChain, defaultValue?: T): Ref { + if (isDefined(defaultValue)) this.init(path as any, defaultValue); + return StoreRef.create({ + store: this, + path, + }) as Ref; + } + + getMethods(): ViewMethods { + return { + getData: this.getData.bind(this), + set: this.set.bind(this), + get: this.get.bind(this), + update: this.update.bind(this), + delete: this.delete.bind(this), + toggle: this.toggle.bind(this), + init: this.init.bind(this), + ref: this.ref.bind(this), + mutate: this.mutate.bind(this), + }; + } +} + +View.prototype.sealed = false; //indicate that data should be copied before virtual items are added diff --git a/packages/cx/src/data/ZoomIntoPropertyView.d.ts b/packages/cx/src/data/ZoomIntoPropertyView.d.ts deleted file mode 100644 index 178626525..000000000 --- a/packages/cx/src/data/ZoomIntoPropertyView.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {View, ViewConfig} from './View'; -import {Binding} from './Binding'; - -interface ZoomIntoPropertyViewConfig extends ViewConfig { - rootName?: string, - binding: Binding -} - -export class ZoomIntoPropertyView extends View { - constructor(config?: ZoomIntoPropertyViewConfig); -} diff --git a/packages/cx/src/data/ZoomIntoPropertyView.js b/packages/cx/src/data/ZoomIntoPropertyView.js deleted file mode 100644 index 43168efa3..000000000 --- a/packages/cx/src/data/ZoomIntoPropertyView.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Binding } from "./Binding"; -import { NestedDataView } from "./NestedDataView"; - -export class ZoomIntoPropertyView extends NestedDataView { - getBaseData(parentStoreData) { - let x = this.binding.value(parentStoreData); - if (x != null && typeof x != "object") throw new Error("Zoomed value must be an object."); - return { - ...x, - }; - } - - embedAugmentData(result, parentStoreData) { - result[this.rootName] = parentStoreData; - super.embedAugmentData(result, parentStoreData); - } - - setItem(path, value) { - if (path.indexOf(this.rootName + ".") == 0) this.store.setItem(path.substring(this.rootName.length + 1), value); - else if (this.isExtraKey(Binding.get(path).parts[0])) - super.setItem(path, value); - else super.setItem(this.binding.path + "." + path, value); - } - - deleteItem(path) { - if (path.indexOf(this.rootName + ".") == 0) this.store.deleteItem(path.substring(this.rootName.length + 1)); - else if (this.isExtraKey(Binding.get(path).parts[0])) - super.deleteItem(path); - else super.deleteItem(this.binding.path + "." + path); - } -} - -ZoomIntoPropertyView.prototype.rootName = "$root"; diff --git a/packages/cx/src/data/ZoomIntoPropertyView.spec.js b/packages/cx/src/data/ZoomIntoPropertyView.spec.js deleted file mode 100644 index f1b178d65..000000000 --- a/packages/cx/src/data/ZoomIntoPropertyView.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import assert from 'assert'; - -import {Binding} from './Binding'; -import {Store} from './Store'; -import {ZoomIntoPropertyView} from './ZoomIntoPropertyView'; - -describe('ZoomIntoPropertyView', () => { - - const getZoom = () => { - let store = new Store({ - data: { - a: 3, - item: { - firstName: 'Jack' - } - } - }); - - return new ZoomIntoPropertyView({ - store: store, - binding: Binding.get('item') - }); - }; - - it('allows direct access to properties of zoomed object', ()=> { - let zoom = getZoom(); - assert.equal(zoom.get('firstName'), 'Jack'); - }); - - it('allows access to outside data through $root', ()=> { - let zoom = getZoom(); - assert.equal(zoom.get('$root.a'), 3); - }); - - it('correctly sets zoomed object', ()=> { - let zoom = getZoom(); - zoom.set('firstName', 'Joe'); - assert.equal(zoom.get('firstName'), 'Joe'); - }); - - it('correctly sets zoomed values', ()=> { - let zoom = getZoom(); - zoom.set('firstName', 'Joe'); - assert.equal(zoom.get('firstName'), 'Joe'); - assert.equal(zoom.get('$root.item.firstName'), 'Joe'); - }); - - it('correctly sets outside values', ()=> { - let zoom = getZoom(); - zoom.set('$root.a', 6); - assert.equal(zoom.get('$root.a'), 6); - }); - - it('correctly deletes zoomed values', ()=> { - let zoom = getZoom(); - zoom.delete('firstName'); - assert.equal(zoom.get('firstName'), undefined); - }); - - it('correctly deletes outside values', ()=> { - let zoom = getZoom(); - zoom.delete('$root.a'); - assert.equal(zoom.get('$root.a'), undefined); - }); -}); diff --git a/packages/cx/src/data/ZoomIntoPropertyView.spec.ts b/packages/cx/src/data/ZoomIntoPropertyView.spec.ts new file mode 100644 index 000000000..5cb6a42e4 --- /dev/null +++ b/packages/cx/src/data/ZoomIntoPropertyView.spec.ts @@ -0,0 +1,64 @@ +import assert from "assert"; + +import { Binding } from "./Binding"; +import { Store } from "./Store"; +import { ZoomIntoPropertyView } from "./ZoomIntoPropertyView"; + +describe("ZoomIntoPropertyView", () => { + const getZoom = () => { + let store = new Store({ + data: { + a: 3, + item: { + firstName: "Jack", + }, + }, + }); + + return new ZoomIntoPropertyView({ + store: store, + binding: Binding.get("item"), + }); + }; + + it("allows direct access to properties of zoomed object", () => { + let zoom = getZoom(); + assert.equal(zoom.get("firstName"), "Jack"); + }); + + it("allows access to outside data through $root", () => { + let zoom = getZoom(); + assert.equal(zoom.get("$root.a"), 3); + }); + + it("correctly sets zoomed object", () => { + let zoom = getZoom(); + zoom.set("firstName", "Joe"); + assert.equal(zoom.get("firstName"), "Joe"); + }); + + it("correctly sets zoomed values", () => { + let zoom = getZoom(); + zoom.set("firstName", "Joe"); + assert.equal(zoom.get("firstName"), "Joe"); + assert.equal(zoom.get("$root.item.firstName"), "Joe"); + }); + + it("correctly sets outside values", () => { + let zoom = getZoom(); + zoom.set("$root.a", 6); + assert.equal(zoom.get("$root.a"), 6); + }); + + it("correctly deletes zoomed values", () => { + let zoom = getZoom(); + zoom.delete("firstName"); + assert.equal(zoom.get("firstName"), undefined); + }); + + it("correctly deletes outside values", () => { + let zoom = getZoom(); + zoom.delete("$root.a"); + assert.equal(zoom.get("$root.a"), undefined); + }); +}); diff --git a/packages/cx/src/data/ZoomIntoPropertyView.ts b/packages/cx/src/data/ZoomIntoPropertyView.ts new file mode 100644 index 000000000..57ee73836 --- /dev/null +++ b/packages/cx/src/data/ZoomIntoPropertyView.ts @@ -0,0 +1,45 @@ +import { Binding } from "./Binding"; +import { NestedDataView, NestedDataViewConfig } from "./NestedDataView"; + +export interface ZoomIntoPropertyViewConfig extends NestedDataViewConfig { + binding: Binding; + rootName?: string; +} + +export class ZoomIntoPropertyView extends NestedDataView { + declare binding: Binding; + declare rootName: string; + + constructor(config: ZoomIntoPropertyViewConfig) { + super(config); + } + + protected getBaseData(parentStoreData: any): any { + let x = this.binding.value(parentStoreData); + if (x != null && typeof x != "object") throw new Error("Zoomed value must be an object."); + return { + ...x, + }; + } + + protected embedAugmentData(result: any, parentStoreData: any): void { + result[this.rootName] = parentStoreData; + super.embedAugmentData(result, parentStoreData); + } + + setItem(path: string, value: any): boolean { + if (path.indexOf(this.rootName + ".") == 0) + return this.store.setItem(path.substring(this.rootName.length + 1), value); + if (this.isExtraKey(Binding.get(path).parts[0])) return super.setItem(path, value); + return super.setItem(this.binding.path + "." + path, value); + } + + deleteItem(path: string): boolean { + if (path.indexOf(this.rootName + ".") == 0) + return this.store.deleteItem(path.substring(this.rootName.length + 1)); + if (this.isExtraKey(Binding.get(path).parts[0])) return super.deleteItem(path); + return super.deleteItem(this.binding.path + "." + path); + } +} + +ZoomIntoPropertyView.prototype.rootName = "$root"; diff --git a/packages/cx/src/data/comparer.d.ts b/packages/cx/src/data/comparer.d.ts deleted file mode 100644 index 74e391f58..000000000 --- a/packages/cx/src/data/comparer.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Sorter} from '../core'; - -export function getComparer(sorters: Sorter[], dataAccessor?: (any) => any, compare?: (a: any, b: any) => number) : (a: any, b: any) => number; - -export function indexSorter(sorters: Sorter[], dataAccessor?: (any) => any, compare?: (a: any, b: any) => number) : (data: any[]) => number[]; - -export function sorter(sorters: Sorter[], dataAccessor?: (any) => any, compare?: (a: any, b: any) => number) : (data: any[]) => any[]; diff --git a/packages/cx/src/data/comparer.js b/packages/cx/src/data/comparer.js deleted file mode 100644 index 4313fe8db..000000000 --- a/packages/cx/src/data/comparer.js +++ /dev/null @@ -1,54 +0,0 @@ -import { getSelector } from "./getSelector"; -import { isDefined } from "../util/isDefined"; -import { defaultCompare } from "./defaultCompare"; - -export function getComparer(sorters, dataAccessor, comparer) { - let resolvedSorters = (sorters || []).map((s) => { - let selector = isDefined(s.value) ? getSelector(s.value) : s.field ? (x) => x[s.field] : () => null; - return { - getter: dataAccessor ? (x) => selector(dataAccessor(x)) : selector, - factor: s.direction && s.direction[0].toLowerCase() == "d" ? -1 : 1, - compare: s.comparer || s.compare || comparer || defaultCompare, - }; - }); - - return function (a, b) { - let d, av, bv; - for (let i = 0; i < resolvedSorters.length; i++) { - d = resolvedSorters[i]; - av = d.getter(a); - bv = d.getter(b); - - // show nulls always on the bottom - if (av == null) { - if (bv == null) continue; - else return 1; - } - if (bv == null) return -1; - - let r = d.compare(av, bv); - if (r == 0) continue; - return d.factor * r; - } - return 0; - }; -} - -export function indexSorter(sorters, dataAccessor, compare) { - let cmp = getComparer(sorters, dataAccessor, compare); - return function (data) { - let result = Array.from({ length: data.length }, (v, k) => k); - result.sort((ia, ib) => cmp(data[ia], data[ib])); - return result; - }; -} - -export function sorter(sorters, dataAccessor, compare) { - let cmp = getComparer(sorters, dataAccessor, compare); - - return function (data) { - let result = [...data]; - result.sort(cmp); - return result; - }; -} diff --git a/packages/cx/src/data/comparer.spec.js b/packages/cx/src/data/comparer.spec.ts similarity index 100% rename from packages/cx/src/data/comparer.spec.js rename to packages/cx/src/data/comparer.spec.ts diff --git a/packages/cx/src/data/comparer.ts b/packages/cx/src/data/comparer.ts new file mode 100644 index 000000000..6e7fdf6cc --- /dev/null +++ b/packages/cx/src/data/comparer.ts @@ -0,0 +1,78 @@ +import { getSelector } from "./getSelector"; +import { isDefined } from "../util/isDefined"; +import { defaultCompare } from "./defaultCompare"; + +interface Sorter { + value?: any; + field?: string; + direction?: string; + comparer?: (a: any, b: any) => number; + compare?: (a: any, b: any) => number; +} + +export function getComparer( + sorters: Sorter[], + dataAccessor?: (x: any) => any, + comparer?: (a: any, b: any) => number, +): (a: any, b: any) => number { + let resolvedSorters = (sorters || []).map((s) => { + let selector = isDefined(s.value) + ? getSelector(s.value) + : s.field + ? (x: Record) => x[s.field!] + : () => null; + return { + getter: dataAccessor ? (x: any) => selector(dataAccessor(x)) : selector, + factor: s.direction && s.direction[0].toLowerCase() == "d" ? -1 : 1, + compare: s.comparer || s.compare || comparer || defaultCompare, + }; + }); + + return function (a, b) { + let d, av, bv; + for (let i = 0; i < resolvedSorters.length; i++) { + d = resolvedSorters[i]; + av = d.getter(a); + bv = d.getter(b); + + // show nulls always on the bottom + if (av == null) { + if (bv == null) continue; + else return 1; + } + if (bv == null) return -1; + + let r = d.compare(av, bv); + if (r == 0) continue; + return d.factor * r; + } + return 0; + }; +} + +export function indexSorter( + sorters: Sorter[], + dataAccessor?: (x: any) => any, + compare?: (a: any, b: any) => number, +): (data: any[]) => number[] { + let cmp = getComparer(sorters, dataAccessor, compare); + return function (data) { + let result = Array.from({ length: data.length }, (v, k) => k); + result.sort((ia, ib) => cmp(data[ia], data[ib])); + return result; + }; +} + +export function sorter( + sorters: Sorter[], + dataAccessor?: (x: any) => any, + compare?: (a: any, b: any) => number, +): (data: any[]) => any[] { + let cmp = getComparer(sorters, dataAccessor, compare); + + return function (data) { + let result = [...data]; + result.sort(cmp); + return result; + }; +} diff --git a/packages/cx/src/data/computable.d.ts b/packages/cx/src/data/computable.d.ts deleted file mode 100644 index 79807ffdf..000000000 --- a/packages/cx/src/data/computable.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { AccessorChain, Record } from "../core"; - -interface Computable { - (data: Record): V; - memoize(warmupData?: Record): (data: Record) => any; -} - -export function computable(callback: () => any): Computable; -export function computable(p1: string, computeFn: (v1: any) => any): Computable; -export function computable(p1: string, p2: string, computeFn: (v1: any, v2: any) => any): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - computeFn: (v1: any, v2: any, v3: any) => any -): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - p4: string, - computeFn: (v1: any, v2: any, v3: any, v4: any) => any -): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - computeFn: (v1: any, v2: any, v3: any, v4: any, v5: any) => any -): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - computeFn: (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any) => any -): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - p7: string, - computeFn: (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any) => any -): Computable; -export function computable( - p1: string, - p2: string, - p3: string, - p4: string, - p5: string, - p6: string, - p7: string, - p8: string, - computeFn: (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, v8: any) => any -): Computable; - -export function computable(arg1: AccessorChain, compute: (v1: V1) => R): Computable; -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - compute: (v1: V1, v2: V2) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - arg4: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3, v4: V4) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - arg4: AccessorChain, - arg5: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - arg4: AccessorChain, - arg5: AccessorChain, - arg6: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - arg4: AccessorChain, - arg5: AccessorChain, - arg6: AccessorChain, - arg7: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7) => R -): Computable; - -export function computable( - arg1: AccessorChain, - arg2: AccessorChain, - arg3: AccessorChain, - arg4: AccessorChain, - arg5: AccessorChain, - arg6: AccessorChain, - arg7: AccessorChain, - arg8: AccessorChain, - compute: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6, v7: V7, v8: V8) => R -): Computable; diff --git a/packages/cx/src/data/computable.js b/packages/cx/src/data/computable.js deleted file mode 100644 index ceefdecc1..000000000 --- a/packages/cx/src/data/computable.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Binding } from "./Binding"; -import { isString } from "../util/isString"; -import { isFunction } from "../util/isFunction"; -import { isAccessorChain } from "./createAccessorModelProxy"; - -export function computable(...selectorsAndCompute) { - if (selectorsAndCompute.length == 0) - throw new Error("computable requires at least a compute function to be passed in arguments."); - - let compute = selectorsAndCompute[selectorsAndCompute.length - 1]; - if (typeof compute != "function") throw new Error("Last argument to the computable function should be a function."); - - let inputs = [], - a; - - for (let i = 0; i + 1 < selectorsAndCompute.length; i++) { - a = selectorsAndCompute[i]; - if (isString(a) || isAccessorChain(a)) inputs.push(Binding.get(a.toString()).value); - else if (a.memoize) inputs.push(a.memoize()); - else if (isFunction(a)) inputs.push(a); - else throw new Error(`Invalid selector type '${typeof a}' received.`); - } - - function memoize(warmupData) { - let lastValue, - lastArgs = warmupData && inputs.map((s) => s(warmupData)); - - return function (data) { - let dirty = false; - - if (!lastArgs) { - lastArgs = Array.from({ length: inputs.length }); - dirty = true; - } - - for (let i = 0; i < inputs.length; i++) { - let v = inputs[i](data); - if (v === lastArgs[i]) continue; - lastArgs[i] = v; - dirty = true; - } - - if (dirty) lastValue = compute.apply(null, lastArgs); - - return lastValue; - }; - } - - let selector = (data) => - compute.apply( - null, - inputs.map((s) => s(data)) - ); - selector.memoize = memoize; - return selector; -} diff --git a/packages/cx/src/data/computable.spec.js b/packages/cx/src/data/computable.spec.js deleted file mode 100644 index 327d21e0a..000000000 --- a/packages/cx/src/data/computable.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { computable } from "./computable"; -import assert from "assert"; -import { createAccessorModelProxy } from "./createAccessorModelProxy"; - -describe("computable", function () { - it("creates a selector", function () { - let state = { person: { name: "Joe" } }; - let nameLength = computable("person.name", (name) => name.length); - assert.equal(nameLength(state), 3); - }); - - it("fires every time if not memoized", function () { - let state = { person: { name: "Joe" } }; - let fired = 0; - let nameLength = computable("person.name", (name) => { - fired++; - return name.length; - }); - assert.equal(nameLength(state), 3); - assert.equal(nameLength(state), 3); - assert.equal(fired, 2); - }); - - it("fires once if memoized and data has not changed", function () { - let state = { person: { name: "Joe" } }; - let fired = 0; - let nameLength = computable("person.name", (name) => { - fired++; - return name.length; - }).memoize(); - assert.equal(nameLength(state), 3); - assert.equal(nameLength(state), 3); - assert.equal(fired, 1); - }); - - //weird but that's how triggers work and - it("memoize with warmup data will not call compute", function () { - let state = { person: { name: "Joe" } }; - let fired = 0; - let nameLength = computable("person.name", (name) => { - fired++; - return name.length; - }).memoize(state); - assert.equal(nameLength(state), undefined); - assert.equal(nameLength(state), undefined); - assert.equal(fired, 0); - }); - - it("works with accessors", function () { - var m = createAccessorModelProxy(); - let state = { person: { name: "Joe" } }; - let nameLength = computable(m.person.name, (name) => name.length); - assert.equal(nameLength(state), 3); - }); -}); diff --git a/packages/cx/src/data/computable.spec.ts b/packages/cx/src/data/computable.spec.ts new file mode 100644 index 000000000..07b1de068 --- /dev/null +++ b/packages/cx/src/data/computable.spec.ts @@ -0,0 +1,87 @@ +import { computable } from "./computable"; +import assert from "assert"; +import { createAccessorModelProxy } from "./createAccessorModelProxy"; + +describe("computable", function () { + it("creates a selector", function () { + let state = { person: { name: "Joe" } }; + let nameLength = computable("person.name", (name: string) => name.length); + assert.equal(nameLength(state), 3); + }); + + it("fires every time if not memoized", function () { + let state = { person: { name: "Joe" } }; + let fired = 0; + let nameLength = computable("person.name", (name: string) => { + fired++; + return name.length; + }); + assert.equal(nameLength(state), 3); + assert.equal(nameLength(state), 3); + assert.equal(fired, 2); + }); + + it("fires once if memoized and data has not changed", function () { + let state = { person: { name: "Joe" } }; + let fired = 0; + let nameLength = computable("person.name", (name: string) => { + fired++; + return name.length; + }).memoize(); + assert.equal(nameLength(state), 3); + assert.equal(nameLength(state), 3); + assert.equal(fired, 1); + }); + + //weird but that's how triggers work and + it("memoize with warmup data will not call compute", function () { + let state = { person: { name: "Joe" } }; + let fired = 0; + let nameLength = computable("person.name", (name: string) => { + fired++; + return name.length; + }).memoize(state); + assert.equal(nameLength(state), undefined); + assert.equal(nameLength(state), undefined); + assert.equal(fired, 0); + }); + + it("works with accessors", function () { + var m = createAccessorModelProxy<{ person: { name: string } }>(); + let state = { person: { name: "Joe" } }; + let nameLength = computable(m.person.name, (name) => { + // name should be inferred as string + const typedName: string = name; + // @ts-expect-error - name should not be number + const wrongType: number = name; + return typedName.length; + }); + assert.equal(nameLength(state), 3); + }); + + it("works with array length accessor", function () { + var m = createAccessorModelProxy<{ items: string[] }>(); + let state = { items: ["a", "b", "c"] }; + let itemCount = computable(m.items.length, (length) => { + // length should be inferred as number + const typedLength: number = length; + // @ts-expect-error - length should not be string + const wrongType: string = length; + return typedLength; + }); + assert.equal(itemCount(state), 3); + }); + + it("resolves AccessorChain and nested properties to any", function () { + var m = createAccessorModelProxy<{ data: any }>(); + let state = { data: { nested: { value: 42 } } }; + let result = computable(m.data.nested.value, (value) => { + // value should be any, so all assignments should work + const asString: string = value; + const asNumber: number = value; + const asBoolean: boolean = value; + return value; + }); + assert.equal(result(state), 42); + }); +}); diff --git a/packages/cx/src/data/computable.ts b/packages/cx/src/data/computable.ts new file mode 100644 index 000000000..2b7552f9c --- /dev/null +++ b/packages/cx/src/data/computable.ts @@ -0,0 +1,69 @@ +import { Binding } from "./Binding"; +import { isString } from "../util/isString"; +import { isFunction } from "../util/isFunction"; +import { AccessorChain, isAccessorChain } from "./createAccessorModelProxy"; +import { CanMemoize, MemoSelector, Selector } from "./Selector"; +import { Ref } from "./Ref"; + +export type ComputableSelector = string | Selector | AccessorChain | CanMemoize; + +// Helper type to infer the value type from a selector, string, or accessor chain +export type InferSelectorValue = + T extends Selector ? R : T extends AccessorChain ? R : T extends string ? any : never; + +// Generic function with proper type inference for selectors +export function computable( + ...args: [...T, (...values: { [K in keyof T]: InferSelectorValue }) => R] +): MemoSelector; + +export function computable(...selectorsAndCompute: any[]): MemoSelector { + if (selectorsAndCompute.length == 0) + throw new Error("computable requires at least a compute function to be passed in arguments."); + + let compute = selectorsAndCompute[selectorsAndCompute.length - 1]; + if (typeof compute != "function") throw new Error("Last argument to the computable function should be a function."); + + let inputs: Selector[] = [], + a; + + for (let i = 0; i + 1 < selectorsAndCompute.length; i++) { + a = selectorsAndCompute[i]; + if (isString(a) || isAccessorChain(a)) inputs.push(Binding.get(a.toString()).value); + else if (a.memoize) inputs.push(a.memoize()); + else if (isFunction(a)) inputs.push(a); + else throw new Error(`Invalid selector type '${typeof a}' received.`); + } + + function memoize(warmupData: any) { + let lastValue: any, + lastArgs = warmupData && inputs.map((s) => s(warmupData)); + + return function (data: any) { + let dirty = false; + + if (!lastArgs) { + lastArgs = Array.from({ length: inputs.length }); + dirty = true; + } + + for (let i = 0; i < inputs.length; i++) { + let v = inputs[i](data); + if (v === lastArgs[i]) continue; + lastArgs[i] = v; + dirty = true; + } + + if (dirty) lastValue = compute.apply(null, lastArgs); + + return lastValue; + }; + } + + let selector: Selector = (data) => + compute.apply( + null, + inputs.map((s) => s(data)), + ); + selector.memoize = memoize; + return selector as MemoSelector; +} diff --git a/packages/cx/src/data/createAccessorModelProxy.d.ts b/packages/cx/src/data/createAccessorModelProxy.d.ts deleted file mode 100644 index 675e6e434..000000000 --- a/packages/cx/src/data/createAccessorModelProxy.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AccessorChain } from "../core"; - -export function isAccessorChain(value: any): boolean; -export function isAccessorChain(value: any): value is AccessorChain; - -export function createAccessorModelProxy(basePath: string = ""): AccessorChain; diff --git a/packages/cx/src/data/createAccessorModelProxy.js b/packages/cx/src/data/createAccessorModelProxy.js deleted file mode 100644 index fa7e36bb4..000000000 --- a/packages/cx/src/data/createAccessorModelProxy.js +++ /dev/null @@ -1,43 +0,0 @@ -const emptyFn = () => {}; - -export function createAccessorModelProxy(chain = "") { - let lastOp = null; - - const proxy = new Proxy(emptyFn, { - get: (_, name) => { - if (typeof name !== "string") return proxy; - - switch (name) { - case "isAccessorChain": - return true; - - case "toString": - case "valueOf": - case "nameOf": - lastOp = name; - return proxy; - } - - let newChain = chain; - if (newChain.length > 0) newChain += "."; - newChain += name; - return createAccessorModelProxy(newChain); - }, - - apply() { - switch (lastOp) { - case "nameOf": - const lastDotIndex = chain.lastIndexOf("."); - return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain; - - default: - return chain; - } - }, - }); - return proxy; -} - -export function isAccessorChain(value) { - return value != null && !!value.isAccessorChain; -} diff --git a/packages/cx/src/data/createAccessorModelProxy.spec.tsx b/packages/cx/src/data/createAccessorModelProxy.spec.tsx index 181500e4e..528ccbe32 100644 --- a/packages/cx/src/data/createAccessorModelProxy.spec.tsx +++ b/packages/cx/src/data/createAccessorModelProxy.spec.tsx @@ -1,4 +1,6 @@ -import { createAccessorModelProxy } from "./createAccessorModelProxy"; +import { createAccessorModelProxy, AccessorChain } from "./createAccessorModelProxy"; +import { Prop, ResolvePropType } from "../ui/Prop"; +import { Widget, WidgetConfig } from "../ui/Widget"; import assert from "assert"; interface Model { @@ -8,6 +10,13 @@ interface Model { streetNumber: number; }; "@crazy": string; + users: User[]; +} + +interface User { + id: number; + name: string; + email: string; } describe("createAccessorModelProxy", () => { @@ -41,4 +50,96 @@ describe("createAccessorModelProxy", () => { let model = createAccessorModelProxy(); assert.strictEqual(model["@crazy"].nameOf(), "@crazy"); }); + + it("AccessorChain allows access to any property", () => { + // When using an untyped model (any), all property access should be allowed + let model = createAccessorModelProxy(); + + // These should all be valid - no TypeScript errors + assert.strictEqual(model.foo.toString(), "foo"); + assert.strictEqual(model.bar.baz.toString(), "bar.baz"); + assert.strictEqual(model.deeply.nested.property.toString(), "deeply.nested.property"); + assert.strictEqual(model.anyName.anyChild.anyGrandchild.toString(), "anyName.anyChild.anyGrandchild"); + }); + + it("ResolvePropType extracts correct types from AccessorChain", () => { + let model = createAccessorModelProxy(); + + // ResolvePropType should extract the inner type from AccessorChain + type ResolvedString = ResolvePropType; + type ResolvedNumber = ResolvePropType; + type ResolvedUsers = ResolvePropType; + + // These assignments verify the types are correctly inferred + // If types are wrong, TypeScript will error + const str: ResolvedString = "test"; + const num: ResolvedNumber = 42; + const users: ResolvedUsers = [{ id: 1, name: "John", email: "john@example.com" }]; + + // Verify array element type is preserved + type UserArray = ResolvePropType>; + const userArray: UserArray = [{ id: 1, name: "Test", email: "test@test.com" }]; + + // Runtime assertion to make the test meaningful + assert.ok(true); + }); + + it("generic widget infers type from AccessorChain in JSX", () => { + let model = createAccessorModelProxy(); + + // Simulates a generic widget config like LookupFieldConfig + interface GenericWidgetConfig extends WidgetConfig { + items?: AccessorChain; + onProcess?: (item: T) => void; + } + + // Simulates a CxJS widget class + class GenericWidget extends Widget> {} + + // T should be inferred as User from model.users (AccessorChain) + let widget = ( + + { + // user should be typed as User + const id: number = user.id; + const name: string = user.name; + const email: string = user.email; + }} + /> + + ); + + assert.ok(true); + }); + + it("generic widget infers type from Prop in JSX", () => { + let model = createAccessorModelProxy(); + + // Uses Prop like LookupFieldConfig does + interface GenericWidgetConfig extends WidgetConfig { + items?: Prop; + onProcess?: (item: T) => void; + } + + class GenericWidget extends Widget> {} + + // T should be inferred as User from model.users (AccessorChain) + let widget = ( + + { + // user should be typed as User + const id: number = user.id; + const name: string = user.name; + const email: string = user.email; + }} + /> + + ); + + assert.ok(true); + }); }); diff --git a/packages/cx/src/data/createAccessorModelProxy.ts b/packages/cx/src/data/createAccessorModelProxy.ts new file mode 100644 index 000000000..11581627f --- /dev/null +++ b/packages/cx/src/data/createAccessorModelProxy.ts @@ -0,0 +1,66 @@ +interface AccessorChainMethods { + toString(): string; + valueOf(): unknown; + nameOf(): string; +} + +type AccessorChainMap = { + [prop in Exclude]: AccessorChain; +}; + +// Check if a type is `any` using the intersection trick +type IsAny = 0 extends 1 & T ? true : false; + +export type AccessorChain = { + toString(): string; + valueOf(): M | undefined; + nameOf(): string; +} & (IsAny extends true + ? { [key: string]: any } // Allow any property access for `any` type + : M extends object + ? AccessorChainMap + : {}); + +const emptyFn = () => {}; + +export function createAccessorModelProxy(chain: string = ""): AccessorChain { + let lastOp: string | null = null; + + const proxy = new Proxy(emptyFn, { + get: (_, name: string | symbol) => { + if (typeof name !== "string") return proxy; + + switch (name) { + case "isAccessorChain": + return true; + + case "toString": + case "valueOf": + case "nameOf": + lastOp = name; + return proxy; + } + + let newChain = chain; + if (newChain.length > 0) newChain += "."; + newChain += name; + return createAccessorModelProxy(newChain); + }, + + apply(): string { + switch (lastOp) { + case "nameOf": + const lastDotIndex = chain.lastIndexOf("."); + return lastDotIndex > 0 ? chain.substring(lastDotIndex + 1) : chain; + + default: + return chain; + } + }, + }); + return proxy as unknown as AccessorChain; +} + +export function isAccessorChain(value: unknown): value is AccessorChain { + return value != null && !!(value as any).isAccessorChain; +} diff --git a/packages/cx/src/data/createStructuredSelector.d.ts b/packages/cx/src/data/createStructuredSelector.d.ts deleted file mode 100644 index 9acd6659f..000000000 --- a/packages/cx/src/data/createStructuredSelector.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Record, StructuredSelector, Selector } from '../core'; - -export function createStructuredSelector(selector: StructuredSelector, constants?: Record): Selector; \ No newline at end of file diff --git a/packages/cx/src/data/createStructuredSelector.js b/packages/cx/src/data/createStructuredSelector.js deleted file mode 100644 index 877cdee00..000000000 --- a/packages/cx/src/data/createStructuredSelector.js +++ /dev/null @@ -1,43 +0,0 @@ -export function createStructuredSelector(selector, constants) { - let keys = Object.keys(selector); - if (keys.length == 0) return () => constants; - - function memoize() { - let lastResult = Object.assign({}, constants); - - let memoizedSelectors = {}; - - keys.forEach((key) => { - memoizedSelectors[key] = selector[key].memoize ? selector[key].memoize() : selector[key]; - lastResult[key] = undefined; - }); - - return function (data) { - let result = lastResult, - k, - v, - i; - for (i = 0; i < keys.length; i++) { - k = keys[i]; - v = memoizedSelectors[k](data); - if (result === lastResult) { - if (v === lastResult[k]) continue; - result = Object.assign({}, lastResult); - } - result[k] = v; - } - return (lastResult = result); - }; - } - - function evaluate(data) { - let result = Object.assign({}, constants); - for (let i = 0; i < keys.length; i++) { - result[keys[i]] = selector[keys[i]](data); - } - return result; - } - - evaluate.memoize = memoize; - return evaluate; -} diff --git a/packages/cx/src/data/createStructuredSelector.spec.js b/packages/cx/src/data/createStructuredSelector.spec.js deleted file mode 100644 index 1c59a73cd..000000000 --- a/packages/cx/src/data/createStructuredSelector.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import {createStructuredSelector} from './createStructuredSelector'; -import assert from 'assert'; -import {getSelector} from "./getSelector"; -import {computable} from "./computable"; - -describe('createStructuredSelector', function() { - - it('works', function () { - let structure = { - name: getSelector({bind: "name"}) - }; - let state = {name: 'Joe'}; - let selector = createStructuredSelector(structure); - assert.deepEqual(selector(state), {name: "Joe"}); - }); - - it('does not memoize by default', function () { - let computes = 0; - let structure = { - name: computable("name", name => { - computes++; - return name; - }) - }; - let state = {name: 'Joe'}; - let selector = createStructuredSelector(structure); - assert.deepEqual(selector(state), {name: "Joe"}); - selector(state); - assert.deepEqual(computes, 2); - }); - - it('supports memoize', function () { - let computes = 0; - let structure = { - name: computable("name", name => { - computes++; - return name; - }) - }; - let state = {name: 'Joe'}; - let selector = createStructuredSelector(structure).memoize(); - assert.deepEqual(selector(state), {name: "Joe"}); - selector(state); - assert.deepEqual(computes, 1); - }); -}); diff --git a/packages/cx/src/data/createStructuredSelector.spec.ts b/packages/cx/src/data/createStructuredSelector.spec.ts new file mode 100644 index 000000000..f13377e05 --- /dev/null +++ b/packages/cx/src/data/createStructuredSelector.spec.ts @@ -0,0 +1,45 @@ +import { createStructuredSelector } from "./createStructuredSelector"; +import assert from "assert"; +import { getSelector } from "./getSelector"; +import { computable } from "./computable"; + +describe("createStructuredSelector", function () { + it("works", function () { + let structure = { + name: getSelector({ bind: "name" }), + }; + let state = { name: "Joe" }; + let selector = createStructuredSelector(structure); + assert.deepEqual(selector(state), { name: "Joe" }); + }); + + it("does not memoize by default", function () { + let computes = 0; + let structure = { + name: computable("name", (name) => { + computes++; + return name; + }), + }; + let state = { name: "Joe" }; + let selector = createStructuredSelector(structure); + assert.deepEqual(selector(state), { name: "Joe" }); + selector(state); + assert.deepEqual(computes, 2); + }); + + it("supports memoize", function () { + let computes = 0; + let structure = { + name: computable("name", (name) => { + computes++; + return name; + }), + }; + let state = { name: "Joe" }; + let selector = createStructuredSelector(structure).memoize(); + assert.deepEqual(selector(state), { name: "Joe" }); + selector(state); + assert.deepEqual(computes, 1); + }); +}); diff --git a/packages/cx/src/data/createStructuredSelector.ts b/packages/cx/src/data/createStructuredSelector.ts new file mode 100644 index 000000000..e8ae0c558 --- /dev/null +++ b/packages/cx/src/data/createStructuredSelector.ts @@ -0,0 +1,62 @@ +import { MemoSelector, Selector } from "./Selector"; + +export interface StructuredSelectorConfig { + [prop: string]: Selector; +} + +// Helper type to infer result type from selector config +type InferStructuredSelectorResult = { + [K in keyof T]: T[K] extends Selector ? R : any; +} & C; + +export function createStructuredSelector = {}>( + selector: S, + constants?: C, +): MemoSelector> { + let keys = Object.keys(selector); + constants = constants ?? ({} as C); + if (keys.length == 0) { + let result: Selector = () => constants; + result.memoize = () => result; + return result as MemoSelector>; + } + + function memoize() { + let lastResult: Record = Object.assign({}, constants); + + let memoizedSelectors: Record = {}; + + keys.forEach((key) => { + memoizedSelectors[key] = selector[key].memoize ? selector[key].memoize() : selector[key]; + lastResult[key] = undefined; + }); + + return function (data: any) { + let result = lastResult, + k, + v, + i; + for (i = 0; i < keys.length; i++) { + k = keys[i]; + v = memoizedSelectors[k](data); + if (result === lastResult) { + if (v === lastResult[k]) continue; + result = Object.assign({}, lastResult); + } + result[k] = v; + } + return (lastResult = result); + }; + } + + let result: Selector = function evaluate(data: any) { + let result: Record = Object.assign({}, constants); + for (let i = 0; i < keys.length; i++) { + result[keys[i]] = selector[keys[i]](data); + } + return result; + }; + + result.memoize = memoize; + return result as MemoSelector>; +} diff --git a/packages/cx/src/data/defaultCompare.d.ts b/packages/cx/src/data/defaultCompare.d.ts deleted file mode 100644 index 8acd1eb27..000000000 --- a/packages/cx/src/data/defaultCompare.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function defaultCompare(av, bv) : number; \ No newline at end of file diff --git a/packages/cx/src/data/defaultCompare.js b/packages/cx/src/data/defaultCompare.js deleted file mode 100644 index d899fb62d..000000000 --- a/packages/cx/src/data/defaultCompare.js +++ /dev/null @@ -1,15 +0,0 @@ -export function defaultCompare(av, bv) { - if (av == null) { - if (bv == null) - return 0; - return -1; - } - - if (bv == null || av > bv) - return 1; - - if (av < bv) - return -1; - - return 0; -} \ No newline at end of file diff --git a/packages/cx/src/data/defaultCompare.ts b/packages/cx/src/data/defaultCompare.ts new file mode 100644 index 000000000..aa56a5659 --- /dev/null +++ b/packages/cx/src/data/defaultCompare.ts @@ -0,0 +1,15 @@ +export function defaultCompare(av: any, bv: any): number { + if (av == null) { + if (bv == null) + return 0; + return -1; + } + + if (bv == null || av > bv) + return 1; + + if (av < bv) + return -1; + + return 0; +} \ No newline at end of file diff --git a/packages/cx/src/data/diff/diffArrays.d.ts b/packages/cx/src/data/diff/diffArrays.d.ts deleted file mode 100644 index 5ccdf5c4b..000000000 --- a/packages/cx/src/data/diff/diffArrays.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Record } from '../../core'; - -interface BeforeAfterPair { - before: Record, - after: Record -} - -interface ArrayDiff { - added: Record[], - unchanged: Record[], - removed: Record[], - changed: BeforeAfterPair[] -} - -export function diffArrays(oldArray: Record[], newArray: Record[], keyFn: (Record) => any) : ArrayDiff; diff --git a/packages/cx/src/data/diff/diffArrays.js b/packages/cx/src/data/diff/diffArrays.js deleted file mode 100644 index 3c9c0e7a2..000000000 --- a/packages/cx/src/data/diff/diffArrays.js +++ /dev/null @@ -1,41 +0,0 @@ -import {isUndefined} from "../../util/isUndefined"; - -export function diffArrays(oldArray, newArray, keyFn) { - if (!keyFn) - keyFn = e => e; - - var map = new Map(); - - for (let i = 0; i < oldArray.length; i++) - map.set(keyFn(oldArray[i]), oldArray[i]); - - var added = [], - changed = [], - unchanged = []; - - for (let i = 0; i < newArray.length; i++) { - let el = newArray[i]; - let k = keyFn(el); - let old = map.get(k); - if (isUndefined(old)) - added.push(el); - else { - map.delete(k); - if (el == old) - unchanged.push(el); - else changed.push({ - before: old, - after: el - }); - } - } - - var removed = Array.from(map.values()); - - return { - added, - changed, - unchanged, - removed - } -} diff --git a/packages/cx/src/data/diff/diffArrays.ts b/packages/cx/src/data/diff/diffArrays.ts new file mode 100644 index 000000000..9fddee5e0 --- /dev/null +++ b/packages/cx/src/data/diff/diffArrays.ts @@ -0,0 +1,49 @@ +import { isUndefined } from "../../util/isUndefined"; + +type KeyFn = (item: T) => any; + +interface DiffResult { + added: T[]; + changed: { before: T; after: T }[]; + unchanged: T[]; + removed: T[]; +} + +export function diffArrays(oldArray: T[], newArray: T[], keyFn: KeyFn = (e) => e): DiffResult { + const map = new Map(); + + oldArray.forEach((item) => { + const key = keyFn(item); + map.set(key, item); + }); + + const added: T[] = []; + const changed: { before: T; after: T }[] = []; + const unchanged: T[] = []; + + newArray.forEach((item) => { + const key = keyFn(item); + const oldItem = map.get(key); + + if (isUndefined(oldItem)) { + added.push(item); + } else { + map.delete(key); + + if (item === oldItem) { + unchanged.push(item); + } else { + changed.push({ before: oldItem, after: item }); + } + } + }); + + const removed = Array.from(map.values()); + + return { + added, + changed, + unchanged, + removed, + }; +} diff --git a/packages/cx/src/data/diff/diffs.spec.js b/packages/cx/src/data/diff/diffs.spec.js deleted file mode 100644 index d9d49a3dc..000000000 --- a/packages/cx/src/data/diff/diffs.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import assert from 'assert'; -import {diffArrays} from './diffArrays'; - - -describe('diffArrays', function() { - it('should detect new elements', function () { - var a = []; - var b = [{k: 1}, {k: 2}] - var d = diffArrays(a, b); - assert.deepEqual(d, { - added: [{k: 1}, {k: 2}], - removed: [], - unchanged: [], - changed: [] - }); - }); - - it('should detect removed elements', function () { - var a = []; - var b = [{k: 1}, {k: 2}] - var d = diffArrays(b, a); - assert.deepEqual(d, { - removed: [{k: 1}, {k: 2}], - added: [], - unchanged: [], - changed: [] - }); - }); - - it('should detect changed elements', function () { - var a = [{k: 1, v: 2}]; - var b = [{k: 1}, {k: 2}] - var d = diffArrays(a, b, x=>x.k); - assert.deepEqual(d, { - added: [{k: 2}], - removed: [], - unchanged: [], - changed: [{ - after: { - k: 1 - }, - before: { - k: 1, v: 2 - } - }] - }); - }); -}); - diff --git a/packages/cx/src/data/diff/diffs.spec.ts b/packages/cx/src/data/diff/diffs.spec.ts new file mode 100644 index 000000000..e9bddaeec --- /dev/null +++ b/packages/cx/src/data/diff/diffs.spec.ts @@ -0,0 +1,49 @@ +import assert from 'assert'; +import {diffArrays} from './diffArrays'; + + +describe('diffArrays', function() { + it('should detect new elements', function () { + var a: {k: number, v?: number}[] = []; + var b = [{k: 1}, {k: 2}] + var d = diffArrays(a, b); + assert.deepEqual(d, { + added: [{k: 1}, {k: 2}], + removed: [], + unchanged: [], + changed: [] + }); + }); + + it('should detect removed elements', function () { + var a: {k: number}[] = []; + var b = [{k: 1}, {k: 2}] + var d = diffArrays(b, a); + assert.deepEqual(d, { + removed: [{k: 1}, {k: 2}], + added: [], + unchanged: [], + changed: [] + }); + }); + + it('should detect changed elements', function () { + var a = [{k: 1, v: 2}]; + var b = [{k: 1}, {k: 2}] + var d = diffArrays(a, b, x=>x.k); + assert.deepEqual(d, { + added: [{k: 2}], + removed: [], + unchanged: [], + changed: [{ + after: { + k: 1 + }, + before: { + k: 1, v: 2 + } + }] + }); + }); +}); + diff --git a/packages/cx/src/data/diff/index.d.ts b/packages/cx/src/data/diff/index.d.ts deleted file mode 100644 index 34c357099..000000000 --- a/packages/cx/src/data/diff/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './diffArrays'; diff --git a/packages/cx/src/data/diff/index.js b/packages/cx/src/data/diff/index.js deleted file mode 100644 index 34c357099..000000000 --- a/packages/cx/src/data/diff/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './diffArrays'; diff --git a/packages/cx/src/data/diff/index.ts b/packages/cx/src/data/diff/index.ts new file mode 100644 index 000000000..12dbfe94e --- /dev/null +++ b/packages/cx/src/data/diff/index.ts @@ -0,0 +1 @@ +export * from "./diffArrays"; diff --git a/packages/cx/src/data/enableFatArrowExpansion.d.ts b/packages/cx/src/data/enableFatArrowExpansion.d.ts deleted file mode 100644 index a28a89b4d..000000000 --- a/packages/cx/src/data/enableFatArrowExpansion.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function enableFatArrowExpansion(); diff --git a/packages/cx/src/data/enableFatArrowExpansion.js b/packages/cx/src/data/enableFatArrowExpansion.js deleted file mode 100644 index 8d0166693..000000000 --- a/packages/cx/src/data/enableFatArrowExpansion.js +++ /dev/null @@ -1,6 +0,0 @@ -import {expandFatArrows} from '../util/expandFatArrows'; -import {plugFatArrowExpansion} from './Expression'; - -export function enableFatArrowExpansion() { - plugFatArrowExpansion(expandFatArrows); -} diff --git a/packages/cx/src/data/enableFatArrowExpansion.ts b/packages/cx/src/data/enableFatArrowExpansion.ts new file mode 100644 index 000000000..9926017fe --- /dev/null +++ b/packages/cx/src/data/enableFatArrowExpansion.ts @@ -0,0 +1,6 @@ +import { expandFatArrows } from '../util/expandFatArrows'; +import { plugFatArrowExpansion } from './Expression'; + +export function enableFatArrowExpansion(): void { + plugFatArrowExpansion(expandFatArrows); +} diff --git a/packages/cx/src/data/getAccessor.d.ts b/packages/cx/src/data/getAccessor.d.ts deleted file mode 100644 index 7f959e40b..000000000 --- a/packages/cx/src/data/getAccessor.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {View} from "./View"; - -export interface Accessor { - get: (data: any) => any; - set?: (value: any, store: View) => boolean; - bindInstance?(instance: any): Accessor; -} - -export function getAccessor(accessor) : Accessor; \ No newline at end of file diff --git a/packages/cx/src/data/getAccessor.js b/packages/cx/src/data/getAccessor.js deleted file mode 100644 index 25f8d4312..000000000 --- a/packages/cx/src/data/getAccessor.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Binding, isBinding } from "./Binding"; -import { isSelector } from "./isSelector"; -import { getSelector } from "./getSelector"; -import { isObject } from "../util/isObject"; -import { isAccessorChain } from "./createAccessorModelProxy"; - -/* - Accessor provides a common ground between refs and bindings. - Refs offer simplicity, bindings have better performance with more arguments. - Accessor works as a common interface which works with both patterns. - */ - -export function getAccessor(accessor, options) { - if (accessor == null) return null; - - if (isObject(accessor)) { - if (accessor.isAccessor || accessor.isRef) return accessor; - if (isBinding(accessor)) { - let binding = Binding.get(accessor); - return { - get: binding.value, - set: (v, store) => store.set(binding.path, v), - isAccessor: true, - }; - } - } - - if (isAccessorChain(accessor)) { - let binding = Binding.get(accessor); - return { - get: binding.value, - set: (v, store) => store.set(binding.path, v), - isAccessor: true, - }; - } - - if (isSelector(accessor)) { - let selector = getSelector(accessor); - if (accessor && accessor.set) - return { - get: selector, - isAccessor: true, - bindInstance(instance) { - return { - get: selector, - set: (value) => accessor.set(value, instance), - isAccessor: true, - }; - }, - }; - - return { - get: selector, - isAccessor: true, - }; - } - - return { - get: () => accessor, - }; -} diff --git a/packages/cx/src/data/getAccessor.spec.js b/packages/cx/src/data/getAccessor.spec.js deleted file mode 100644 index 0aed40921..000000000 --- a/packages/cx/src/data/getAccessor.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import assert from "assert"; -import { createAccessorModelProxy } from "./createAccessorModelProxy"; -import { getAccessor } from "./getAccessor"; - -describe("getAccessor", function () { - it("works with accessor chains", function () { - let m = createAccessorModelProxy(); - let accessor = getAccessor(m.a.b); - assert(typeof accessor.set == "function"); - }); -}); diff --git a/packages/cx/src/data/getAccessor.spec.ts b/packages/cx/src/data/getAccessor.spec.ts new file mode 100644 index 000000000..ed3faf224 --- /dev/null +++ b/packages/cx/src/data/getAccessor.spec.ts @@ -0,0 +1,11 @@ +import assert from "assert"; +import { createAccessorModelProxy } from "./createAccessorModelProxy"; +import { getAccessor } from "./getAccessor"; + +describe("getAccessor", function () { + it("works with accessor chains", function () { + let m = createAccessorModelProxy<{ a: { b: any } }>(); + let accessor = getAccessor(m.a.b); + assert(typeof accessor.set == "function"); + }); +}); diff --git a/packages/cx/src/data/getAccessor.ts b/packages/cx/src/data/getAccessor.ts new file mode 100644 index 000000000..24fa2fa81 --- /dev/null +++ b/packages/cx/src/data/getAccessor.ts @@ -0,0 +1,74 @@ +import { Binding, isBinding } from "./Binding"; +import { isSelector } from "./isSelector"; +import { getSelector } from "./getSelector"; +import { isObject } from "../util/isObject"; +import { AccessorChain, isAccessorChain } from "./createAccessorModelProxy"; +import { Prop } from "../ui/Prop"; +import { View } from "./View"; + +/* + Accessor provides a common ground between refs and bindings. + Refs offer simplicity, bindings have better performance with more arguments. + Accessor works as a common interface which works with both patterns. + */ + +export interface Accessor { + get: (data: any) => T; + set?: (value: T, store: View) => boolean; + bindInstance?(instance: any): Accessor; + isAccessor?: boolean; + isRef?: boolean; +} + +export function getAccessor(accessor: Prop): Accessor; +export function getAccessor(accessor: Accessor): Accessor; + +export function getAccessor(accessor: any): Accessor | undefined { + if (accessor == null) return undefined; + + if (isObject(accessor)) { + if ("isAccessor" in accessor || "isRef" in accessor) return accessor as Accessor; + if (isBinding(accessor)) { + let binding = Binding.get(accessor); + return { + get: binding.value, + set: (v, store) => store.set(binding.path, v), + isAccessor: true, + }; + } + } + + if (isAccessorChain(accessor)) { + let binding = Binding.get(accessor); + return { + get: binding.value, + set: (v, store) => store.set(binding.path, v), + isAccessor: true, + }; + } + + if (isSelector(accessor)) { + let selector = getSelector(accessor); + if (accessor && accessor.set) + return { + get: selector, + isAccessor: true, + bindInstance(instance) { + return { + get: selector, + set: (value) => accessor.set(value, instance), + isAccessor: true, + }; + }, + }; + + return { + get: selector, + isAccessor: true, + }; + } + + return { + get: () => accessor, + }; +} diff --git a/packages/cx/src/data/getSelector.d.ts b/packages/cx/src/data/getSelector.d.ts deleted file mode 100644 index ce0874d6d..000000000 --- a/packages/cx/src/data/getSelector.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Selector } from '../core'; - -export function getSelector(config: any) : Selector; diff --git a/packages/cx/src/data/getSelector.js b/packages/cx/src/data/getSelector.js deleted file mode 100644 index 77a4e5764..000000000 --- a/packages/cx/src/data/getSelector.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Binding } from "./Binding"; -import { Expression } from "./Expression"; -import { StringTemplate } from "./StringTemplate"; -import { isArray } from "../util/isArray"; -import { createStructuredSelector } from "./createStructuredSelector"; -import { isSelector } from "./isSelector"; -import { isAccessorChain } from "./createAccessorModelProxy"; -import { isString } from "../util/isString"; - -var undefinedF = () => undefined; -var nullF = () => null; - -export function getSelector(config) { - if (config === undefined) return undefinedF; - if (config === null) return nullF; - - switch (typeof config) { - case "object": - if (isArray(config)) { - let selectors = config.map((x) => getSelector(x)); - return (data) => selectors.map((elementSelector) => elementSelector(data)); - } - - //toString converts accessor chains to binding paths - if (config.bind) return Binding.get(config.bind.toString()).value; - - if (isString(config.tpl)) return StringTemplate.get(config.tpl); - - if (config.expr) return Expression.get(config.expr); - - if (config.get) return config.get; - - let selectors = {}; - let constants = {}; - - for (let key in config) { - if (isSelector(config[key])) selectors[key] = getSelector(config[key]); - else constants[key] = config[key]; - } - return createStructuredSelector(selectors, constants); - - case "function": - if (isAccessorChain(config)) return Binding.get(config.toString()).value; - return config; - - default: - return () => config; - } -} diff --git a/packages/cx/src/data/getSelector.spec.js b/packages/cx/src/data/getSelector.spec.js deleted file mode 100644 index 42401c1c5..000000000 --- a/packages/cx/src/data/getSelector.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { getSelector } from "./getSelector"; -import assert from "assert"; -import { createAccessorModelProxy } from "./createAccessorModelProxy"; - -describe("getSelector", function () { - it("array of selectors converts each element into a selector", function () { - let arraySelector = [{ bind: "name" }, { expr: "{name}" }]; - let state = { name: "Joe" }; - let selector = getSelector(arraySelector); - assert.deepEqual(selector(state), ["Joe", "Joe"]); - }); - - it("get can be used for selectors that have set defined too ", function () { - let selector = getSelector({ get: (data) => data.name, set: () => {} }); - assert.deepEqual(selector({ name: "Jack" }), "Jack"); - }); - - it("0 is a valid selector", function () { - let selector = getSelector(0); - assert.deepEqual(selector({}), 0); - }); - - it("false is a valid selector", function () { - let selector = getSelector(false); - assert.deepEqual(selector({}), false); - }); - - it("undefined is a valid selector", function () { - let selector = getSelector(undefined); - assert(selector({}) === undefined); - }); - - it("null is a valid selector", function () { - let selector = getSelector(null); - assert(selector({}) === null); - }); - - it("works with accessor chains", function () { - let m = createAccessorModelProxy(); - let selector = getSelector(m.a.b); - assert(selector({ a: { b: 1 } }) === 1); - }); -}); diff --git a/packages/cx/src/data/getSelector.spec.ts b/packages/cx/src/data/getSelector.spec.ts new file mode 100644 index 000000000..6f04cf98c --- /dev/null +++ b/packages/cx/src/data/getSelector.spec.ts @@ -0,0 +1,43 @@ +import { getSelector } from "./getSelector"; +import assert from "assert"; +import { createAccessorModelProxy } from "./createAccessorModelProxy"; + +describe("getSelector", function () { + it("array of selectors converts each element into a selector", function () { + let arraySelector = [{ bind: "name" }, { expr: "{name}" }]; + let state = { name: "Joe" }; + let selector = getSelector(arraySelector); + assert.deepEqual(selector(state), ["Joe", "Joe"]); + }); + + it("get can be used for selectors that have set defined too ", function () { + let selector = getSelector({ get: (data: any) => data.name, set: () => {} }); + assert.deepEqual(selector({ name: "Jack" }), "Jack"); + }); + + it("0 is a valid selector", function () { + let selector = getSelector(0); + assert.deepEqual(selector({}), 0); + }); + + it("false is a valid selector", function () { + let selector = getSelector(false); + assert.deepEqual(selector({}), false); + }); + + it("undefined is a valid selector", function () { + let selector = getSelector(undefined); + assert(selector({}) === undefined); + }); + + it("null is a valid selector", function () { + let selector = getSelector(null); + assert(selector({}) === null); + }); + + it("works with accessor chains", function () { + let m = createAccessorModelProxy<{ a: { b: any } }>(); + let selector = getSelector(m.a.b); + assert(selector({ a: { b: 1 } }) === 1); + }); +}); diff --git a/packages/cx/src/data/getSelector.ts b/packages/cx/src/data/getSelector.ts new file mode 100644 index 000000000..cef38b35c --- /dev/null +++ b/packages/cx/src/data/getSelector.ts @@ -0,0 +1,66 @@ +import { expr } from "cx/ui"; +import { Binding } from "./Binding"; +import { Expression } from "./Expression"; +import { StringTemplate } from "./StringTemplate"; +import { isArray } from "../util/isArray"; +import { createStructuredSelector, StructuredSelectorConfig } from "./createStructuredSelector"; +import { isSelector } from "./isSelector"; +import { isAccessorChain } from "./createAccessorModelProxy"; +import { isString } from "../util/isString"; +import { hasKey, hasStringAtKey, hasFunctionAtKey } from "../util/hasKey"; +import { isFunction } from "../util"; + +type Selector = (data: any) => T; + +var undefinedF = () => undefined; +var nullF = () => null; + +export function getSelector(config: unknown): Selector { + if (config === undefined) return undefinedF; + if (config === null) return nullF; + + switch (typeof config) { + case "object": + if (isArray(config)) { + let selectors = config.map((x) => getSelector(x)); + return (data) => selectors.map((elementSelector) => elementSelector(data)); + } + + //toString converts accessor chains to binding paths + if (hasKey(config, "bind") && config.bind != null) return Binding.get(config.bind.toString()).value; + + if (hasStringAtKey(config, "tpl")) return StringTemplate.get(config.tpl); + + if (hasKey(config, "expr") && (isString(config.expr) || isFunction(config.expr))) + return Expression.get(config.expr as any); + + if (hasFunctionAtKey(config, "get")) return config.get; + + let selectors: StructuredSelectorConfig = {}; + let constants: Record = {}; + + let obj = config as Record; + for (let key in obj) { + switch (typeof obj[key]) { + case "bigint": + case "boolean": + case "number": + case "string": + constants[key] = obj[key]; + break; + default: + if (isSelector(obj[key])) selectors[key] = getSelector(obj[key]); + else constants[key] = obj[key]; + break; + } + } + return createStructuredSelector(selectors, constants); + + case "function": + if (isAccessorChain(config)) return Binding.get(config.toString()).value; + return config as Selector; + + default: + return () => config; + } +} diff --git a/packages/cx/src/data/index.d.ts b/packages/cx/src/data/index.d.ts deleted file mode 100644 index 42bfcf848..000000000 --- a/packages/cx/src/data/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -export * from "./Binding"; -export * from "./Expression"; -export * from "./StringTemplate"; -export * from "./View"; -export * from "./SubscribableView"; -export * from "./Store"; -export * from "./ExposedRecordView"; -export * from "./ExposedValueView"; -export * from "./ReadOnlyDataView"; -export * from "./ZoomIntoPropertyView"; -export * from "./StructuredSelector"; -export * from "./computable"; -export * from "./getSelector"; -export * from "./isSelector"; -export * from "./Grouper"; -export * from "./comparer"; -export * from "./enableFatArrowExpansion"; -export * from "./ops/index"; -export * from "./diff/index"; -export * from "./Ref"; -export * from "./ArrayRef"; -export * from "./StoreProxy"; -export * from "./AugmentedViewBase"; -export * from "./ArrayElementView"; -export * from "./getAccessor"; -export * from "./defaultCompare"; -export * from "./NestedDataView"; -export * from "./StructuredDataAccessor"; - -export * from "./createAccessorModelProxy"; diff --git a/packages/cx/src/data/index.js b/packages/cx/src/data/index.js deleted file mode 100644 index ae6f5e056..000000000 --- a/packages/cx/src/data/index.js +++ /dev/null @@ -1,29 +0,0 @@ -export * from './Binding'; -export * from './Expression'; -export * from './StringTemplate'; -export * from './View'; -export * from './SubscribableView'; -export * from './Store'; -export * from './ExposedRecordView'; -export * from './ExposedValueView'; -export * from './ReadOnlyDataView'; -export * from './ZoomIntoPropertyView'; -export * from './StructuredSelector'; -export * from './computable'; -export * from './getSelector'; -export * from './isSelector'; -export * from './Grouper'; -export * from './comparer'; -export * from './enableFatArrowExpansion'; -export * from './ops/index'; -export * from './diff/index'; -export * from './Ref'; -export * from './ArrayRef'; -export * from './StoreProxy'; -export * from "./AugmentedViewBase"; -export * from "./ArrayElementView"; -export * from "./getAccessor"; -export * from "./defaultCompare"; -export * from "./NestedDataView"; - -export * from "./createAccessorModelProxy"; \ No newline at end of file diff --git a/packages/cx/src/data/index.ts b/packages/cx/src/data/index.ts new file mode 100644 index 000000000..7c08fe1d8 --- /dev/null +++ b/packages/cx/src/data/index.ts @@ -0,0 +1,30 @@ +export * from "./Binding"; +export * from "./Expression"; +export * from "./StringTemplate"; +export * from "./View"; +export * from "./SubscribableView"; +export * from "./Store"; +export * from "./ExposedRecordView"; +export * from "./ExposedValueView"; +export * from "./ReadOnlyDataView"; +export * from "./ZoomIntoPropertyView"; +export * from "./StructuredSelector"; +export * from "./computable"; +export * from "./getSelector"; +export * from "./isSelector"; +export * from "./Grouper"; +export * from "./comparer"; +export * from "./enableFatArrowExpansion"; +export * from "./ops/index"; +export * from "./diff/index"; +export * from "./Ref"; +export * from "./ArrayRef"; +export * from "./StoreProxy"; +export * from "./AugmentedViewBase"; +export * from "./ArrayElementView"; +export * from "./getAccessor"; +export * from "./defaultCompare"; +export * from "./NestedDataView"; + +export * from "./createAccessorModelProxy"; +export * from "./Selector"; diff --git a/packages/cx/src/data/isSelector.d.ts b/packages/cx/src/data/isSelector.d.ts deleted file mode 100644 index d5ec0e316..000000000 --- a/packages/cx/src/data/isSelector.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function isSelector(config: any) : boolean; diff --git a/packages/cx/src/data/isSelector.js b/packages/cx/src/data/isSelector.js deleted file mode 100644 index 60fbcc800..000000000 --- a/packages/cx/src/data/isSelector.js +++ /dev/null @@ -1,26 +0,0 @@ -export function isSelector(config) { - - if (config == null) - return true; - - switch (typeof config) { - case 'object': - if (config.type || config.$type) - return false; - return !!(config.bind || config.tpl || config.expr || config.get); - - case 'function': - return true; - - case 'string': - return true; - - case 'number': - return true; - - case 'boolean': - return true; - } - - return false; -} diff --git a/packages/cx/src/data/isSelector.ts b/packages/cx/src/data/isSelector.ts new file mode 100644 index 000000000..fbd140f82 --- /dev/null +++ b/packages/cx/src/data/isSelector.ts @@ -0,0 +1,26 @@ +export function isSelector(config: any): boolean { + + if (config == null) + return true; + + switch (typeof config) { + case 'object': + if (config.type || config.$type) + return false; + return !!(config.bind || config.tpl || config.expr || config.get); + + case 'function': + return true; + + case 'string': + return true; + + case 'number': + return true; + + case 'boolean': + return true; + } + + return false; +} diff --git a/packages/cx/src/data/ops/append.d.ts b/packages/cx/src/data/ops/append.d.ts deleted file mode 100644 index a3cbd6e4f..000000000 --- a/packages/cx/src/data/ops/append.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function append(array: T[], ...items: T[]): T[]; diff --git a/packages/cx/src/data/ops/append.js b/packages/cx/src/data/ops/append.js deleted file mode 100644 index c8a52851a..000000000 --- a/packages/cx/src/data/ops/append.js +++ /dev/null @@ -1,7 +0,0 @@ -export function append(array, ...items) { - if (items.length == 0) - return array; - if (!array) - return items; - return [...array, ...items]; -} diff --git a/packages/cx/src/data/ops/append.spec.js b/packages/cx/src/data/ops/append.spec.js deleted file mode 100644 index 8867eccae..000000000 --- a/packages/cx/src/data/ops/append.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import {Store} from '../Store'; -import {append} from './append'; -import assert from 'assert'; - -describe('append', function() { - it('should add elements to an array', function () { - let store = new Store({ - data: { - array: [] - } - }); - - assert(store.update('array', append, 1)); - assert.deepEqual(store.get('array'), [1]); - }); - - it('should work with undefined arrays', function () { - let store = new Store(); - assert(store.update('array', append, 1)); - assert.deepEqual(store.get('array'), [1]); - }); - - it('accepts multiple arguments', function () { - let store = new Store(); - assert(store.update('array', append, 1, 2, 3)); - assert.deepEqual(store.get('array'), [1, 2, 3]); - }); -}); diff --git a/packages/cx/src/data/ops/append.spec.ts b/packages/cx/src/data/ops/append.spec.ts new file mode 100644 index 000000000..40df1b418 --- /dev/null +++ b/packages/cx/src/data/ops/append.spec.ts @@ -0,0 +1,28 @@ +import assert from 'assert'; +import { Store } from '../Store'; +import { append } from './append'; + +describe('append', function() { + it('should add elements to an array', function () { + let store = new Store({ + data: { + array: [] + } + }); + + assert(store.update('array', append, 1)); + assert.deepEqual(store.get('array'), [1]); + }); + + it('should work with undefined arrays', function () { + let store = new Store(); + assert(store.update('array', append, 1)); + assert.deepEqual(store.get('array'), [1]); + }); + + it('accepts multiple arguments', function () { + let store = new Store(); + assert(store.update('array', append, 1, 2, 3)); + assert.deepEqual(store.get('array'), [1, 2, 3]); + }); +}); diff --git a/packages/cx/src/data/ops/append.ts b/packages/cx/src/data/ops/append.ts new file mode 100644 index 000000000..15971f1aa --- /dev/null +++ b/packages/cx/src/data/ops/append.ts @@ -0,0 +1,5 @@ +export function append(array: T[], ...items: T[]): T[] { + if (items.length === 0) return array ?? []; + if (!array) return items; + return [...array, ...items]; +} diff --git a/packages/cx/src/data/ops/filter.d.ts b/packages/cx/src/data/ops/filter.d.ts deleted file mode 100644 index 26bb4ffb9..000000000 --- a/packages/cx/src/data/ops/filter.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function filter(array: T[], callback: (item: T, index: number, array: T[]) => boolean): T[]; diff --git a/packages/cx/src/data/ops/filter.js b/packages/cx/src/data/ops/filter.js deleted file mode 100644 index d4d82c58d..000000000 --- a/packages/cx/src/data/ops/filter.js +++ /dev/null @@ -1,8 +0,0 @@ -export function filter(array, callback) { - if (array == null) - return array; - let result = array.filter(callback); - if (result.length == array.length) - return array; - return result; -} diff --git a/packages/cx/src/data/ops/filter.spec.js b/packages/cx/src/data/ops/filter.spec.js deleted file mode 100644 index 22cc79098..000000000 --- a/packages/cx/src/data/ops/filter.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import {Store} from '../Store'; -import {filter} from './filter'; -import assert from 'assert'; - -describe('filter', function() { - it('should filter array elements', function () { - let store = new Store({ - data: { - array: [1, 2, 3] - } - }); - - assert(store.update('array', filter, x => x > 1)); - assert.deepEqual(store.get('array'), [2, 3]); - }); - - it('should work with undefined arrays', function () { - let store = new Store(); - assert.equal(false, store.update('array', filter, x => x > 1)); - assert.deepEqual(store.get('array'), undefined); - }); - - it('returns same array if all elements satisfy condition', function () { - let array = [1, 2, 3]; - let store = new Store({ data: { array }}); - assert.equal(false, store.update('array', filter, x => x > 0)); - assert(store.get('array') === array); - }); -}); diff --git a/packages/cx/src/data/ops/filter.spec.ts b/packages/cx/src/data/ops/filter.spec.ts new file mode 100644 index 000000000..f5429a514 --- /dev/null +++ b/packages/cx/src/data/ops/filter.spec.ts @@ -0,0 +1,35 @@ +import assert from "assert"; +import { Store } from "../Store"; +import { filter } from "./filter"; + +describe("filter", function () { + it("should filter array elements", function () { + let store = new Store({ + data: { + array: [1, 2, 3], + }, + }); + + assert(store.update("array", filter, (x: number) => x > 1)); + assert.deepEqual(store.get("array"), [2, 3]); + }); + + it("should work with undefined arrays", function () { + let store = new Store(); + assert.equal( + false, + store.update("array", filter, (x) => x > 1), + ); + assert.deepEqual(store.get("array"), undefined); + }); + + it("returns same array if all elements satisfy condition", function () { + let array = [1, 2, 3]; + let store = new Store({ data: { array } }); + assert.equal( + false, + store.update("array", filter, (x) => x > 0), + ); + assert(store.get("array") === array); + }); +}); diff --git a/packages/cx/src/data/ops/filter.ts b/packages/cx/src/data/ops/filter.ts new file mode 100644 index 000000000..d826852be --- /dev/null +++ b/packages/cx/src/data/ops/filter.ts @@ -0,0 +1,9 @@ +export function filter(array: T[], callback: (item: T, index: number, array: T[]) => boolean): T[] { + if (array == null) return array; + + const result = array.filter(callback); + + if (result.length === array.length) return array; + + return result; +} diff --git a/packages/cx/src/data/ops/findTreeNode.d.ts b/packages/cx/src/data/ops/findTreeNode.d.ts deleted file mode 100644 index b37712414..000000000 --- a/packages/cx/src/data/ops/findTreeNode.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function findTreeNode(array: T[], criteria: (item?: T) => boolean, childrenField?: string): T | false; diff --git a/packages/cx/src/data/ops/findTreeNode.js b/packages/cx/src/data/ops/findTreeNode.js deleted file mode 100644 index cbb06a105..000000000 --- a/packages/cx/src/data/ops/findTreeNode.js +++ /dev/null @@ -1,15 +0,0 @@ -export function findTreeNode(array, criteria, childrenField = '$children') { - if (!Array.isArray(array)) - return false; - - for (let i = 0; i < array.length; i++) { - if (criteria(array[i])) - return array[i]; - - let child = findTreeNode(array[i][childrenField], criteria, childrenField); - if (child) - return child; - } - - return false; -} diff --git a/packages/cx/src/data/ops/findTreeNode.spec.js b/packages/cx/src/data/ops/findTreeNode.spec.js deleted file mode 100644 index 08cc8f448..000000000 --- a/packages/cx/src/data/ops/findTreeNode.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import {Store} from '../Store'; -import {findTreeNode} from './findTreeNode'; -import assert from 'assert'; - -describe('removeTreeNodes', function() { - it('removes all nodes that satisfy criteria', function () { - let array = [{ - id: 'n1', - value: 1, - children: [{ - id: 'n11', - value: 2 - }, { - id: 'n12', - value: 3 - }] - }]; - - let node = findTreeNode(array, x => x.value == 3, 'children'); - - assert(node); - assert.equal(node.value, 3) - }); -}); diff --git a/packages/cx/src/data/ops/findTreeNode.spec.ts b/packages/cx/src/data/ops/findTreeNode.spec.ts new file mode 100644 index 000000000..58d70aea6 --- /dev/null +++ b/packages/cx/src/data/ops/findTreeNode.spec.ts @@ -0,0 +1,23 @@ +import assert from 'assert'; +import { findTreeNode } from './findTreeNode'; + +describe('removeTreeNodes', function() { + it('removes all nodes that satisfy criteria', function () { + let array = [{ + id: 'n1', + value: 1, + children: [{ + id: 'n11', + value: 2 + }, { + id: 'n12', + value: 3 + }] + }]; + + let node = findTreeNode(array, x => x.value == 3, 'children'); + + assert(node); + assert.equal(node.value, 3) + }); +}); diff --git a/packages/cx/src/data/ops/findTreeNode.ts b/packages/cx/src/data/ops/findTreeNode.ts new file mode 100644 index 000000000..57efc28c7 --- /dev/null +++ b/packages/cx/src/data/ops/findTreeNode.ts @@ -0,0 +1,14 @@ +export function findTreeNode(array: T[], criteria: (node: T) => boolean, childrenField: keyof T): T | false { + if (!Array.isArray(array)) return false; + + for (const node of array) { + if (criteria(node)) return node; + + const children = node[childrenField] as T[]; + + const found = findTreeNode(children, criteria, childrenField); + if (found) return found; + } + + return false; +} diff --git a/packages/cx/src/data/ops/findTreePath.d.ts b/packages/cx/src/data/ops/findTreePath.d.ts deleted file mode 100644 index 4eab1b198..000000000 --- a/packages/cx/src/data/ops/findTreePath.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function findTreePath( - array: T[], - criteria: (item: T) => boolean, - childrenField = "$children", - currentPath: T[] = [] -): T[] | false; diff --git a/packages/cx/src/data/ops/findTreePath.js b/packages/cx/src/data/ops/findTreePath.js deleted file mode 100644 index c0f5da72e..000000000 --- a/packages/cx/src/data/ops/findTreePath.js +++ /dev/null @@ -1,16 +0,0 @@ -export function findTreePath(array, criteria, childrenField = "$children", currentPath = []) { - if (!Array.isArray(array)) return false; - - for (let i = 0; i < array.length; i++) { - currentPath.push(array[i]); - - if (criteria(array[i])) return currentPath; - - let childPath = findTreePath(array[i][childrenField], criteria, childrenField, currentPath); - if (childPath) return childPath; - - currentPath.pop(); - } - - return false; -} diff --git a/packages/cx/src/data/ops/findTreePath.ts b/packages/cx/src/data/ops/findTreePath.ts new file mode 100644 index 000000000..8bfb8239a --- /dev/null +++ b/packages/cx/src/data/ops/findTreePath.ts @@ -0,0 +1,23 @@ +export function findTreePath( + array: T[] | undefined, + criteria: (node: T) => boolean, + childrenField: keyof T = "$children" as keyof T, + currentPath: T[] = [], +): T[] | false { + if (!Array.isArray(array)) return false; + + for (const node of array) { + currentPath.push(node); + + if (criteria(node)) return [...currentPath]; + + const children = node[childrenField] as T[] | undefined; + + const childPath = findTreePath(children, criteria, childrenField, currentPath); + if (childPath) return childPath; + + currentPath.pop(); + } + + return false; +} diff --git a/packages/cx/src/data/ops/index.d.ts b/packages/cx/src/data/ops/index.d.ts deleted file mode 100644 index bc37e944e..000000000 --- a/packages/cx/src/data/ops/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "./append"; -export * from "./merge"; -export * from "./filter"; -export * from "./updateArray"; -export * from "./updateTree"; -export * from "./removeTreeNodes"; -export * from "./findTreeNode"; -export * from "./moveElement"; -export * from "./insertElement"; -export * from "./findTreePath"; diff --git a/packages/cx/src/data/ops/index.js b/packages/cx/src/data/ops/index.ts similarity index 100% rename from packages/cx/src/data/ops/index.js rename to packages/cx/src/data/ops/index.ts diff --git a/packages/cx/src/data/ops/insertElement.d.ts b/packages/cx/src/data/ops/insertElement.d.ts deleted file mode 100644 index b257d1e5c..000000000 --- a/packages/cx/src/data/ops/insertElement.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function insertElement(array: T[], index: number, ...args: T[]): T[]; diff --git a/packages/cx/src/data/ops/insertElement.js b/packages/cx/src/data/ops/insertElement.js deleted file mode 100644 index 2b18a4ea6..000000000 --- a/packages/cx/src/data/ops/insertElement.js +++ /dev/null @@ -1,3 +0,0 @@ -export function insertElement(array, index, ...args) { - return [...array.slice(0, index), ...args, ...array.slice(index)]; -} \ No newline at end of file diff --git a/packages/cx/src/data/ops/insertElement.ts b/packages/cx/src/data/ops/insertElement.ts new file mode 100644 index 000000000..a87a5a908 --- /dev/null +++ b/packages/cx/src/data/ops/insertElement.ts @@ -0,0 +1,3 @@ +export function insertElement(array: T[], index: number, ...elements: T[]): T[] { + return [...array.slice(0, index), ...elements, ...array.slice(index)]; +} diff --git a/packages/cx/src/data/ops/merge.d.ts b/packages/cx/src/data/ops/merge.d.ts deleted file mode 100644 index ef29f5c4c..000000000 --- a/packages/cx/src/data/ops/merge.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Record } from '../../core'; - -export function merge(item: Record, data: Record) : Record; diff --git a/packages/cx/src/data/ops/merge.js b/packages/cx/src/data/ops/merge.js deleted file mode 100644 index a1e506dcd..000000000 --- a/packages/cx/src/data/ops/merge.js +++ /dev/null @@ -1,9 +0,0 @@ -import {Binding} from '../Binding'; - -export function merge(item, data) { - let result = item; - if (data) - for (let key in data) - result = Binding.get(key).set(result, data[key]); - return result; -} diff --git a/packages/cx/src/data/ops/merge.spec.js b/packages/cx/src/data/ops/merge.spec.js deleted file mode 100644 index 806471ae3..000000000 --- a/packages/cx/src/data/ops/merge.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import {Store} from '../Store'; -import {merge} from './merge'; -import assert from 'assert'; - -describe('merge', function() { - it('performs multiple set operations', function () { - let store = new Store({ - data: { - person: { firstName: 'John', lastName: 'Doe'} - } - }); - - assert(store.update('person', merge, { firstName: 'Johny', age: 18 })); - assert.deepEqual(store.get('person'), { firstName: 'Johny', lastName: 'Doe', age: 18 }); - }); - - it('does not modify data if not necessary', function () { - let store = new Store({ - data: { - person: { firstName: 'John', lastName: 'Doe'} - } - }); - - assert(store.update('person', merge, { firstName: 'John' }) == false); - assert.deepEqual(store.get('person'), { firstName: 'John', lastName: 'Doe' }); - }); -}); diff --git a/packages/cx/src/data/ops/merge.spec.ts b/packages/cx/src/data/ops/merge.spec.ts new file mode 100644 index 000000000..7c259ef10 --- /dev/null +++ b/packages/cx/src/data/ops/merge.spec.ts @@ -0,0 +1,27 @@ +import { Store } from "../Store"; +import { merge } from "./merge"; +import assert from "assert"; + +describe("merge", function () { + it("performs multiple set operations", function () { + let store = new Store({ + data: { + person: { firstName: "John", lastName: "Doe" }, + }, + }); + + assert(store.update("person", merge, { firstName: "Johny", age: 18 })); + assert.deepEqual(store.get("person"), { firstName: "Johny", lastName: "Doe", age: 18 }); + }); + + it("does not modify data if not necessary", function () { + let store = new Store({ + data: { + person: { firstName: "John", lastName: "Doe" }, + }, + }); + + assert(store.update("person", merge, { firstName: "John" }) == false); + assert.deepEqual(store.get("person"), { firstName: "John", lastName: "Doe" }); + }); +}); diff --git a/packages/cx/src/data/ops/merge.ts b/packages/cx/src/data/ops/merge.ts new file mode 100644 index 000000000..fce309f22 --- /dev/null +++ b/packages/cx/src/data/ops/merge.ts @@ -0,0 +1,13 @@ +import { Binding } from "../Binding"; + +export function merge(item: T, data?: Partial): T { + let result = item; + + if (data) { + for (const key in data) { + result = Binding.get(key).set(result, data[key] as any); + } + } + + return result; +} diff --git a/packages/cx/src/data/ops/moveElement.d.ts b/packages/cx/src/data/ops/moveElement.d.ts deleted file mode 100644 index 5ad3cd408..000000000 --- a/packages/cx/src/data/ops/moveElement.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function moveElement(array: T[], sourceIndex: number, targetIndex: number): T[]; diff --git a/packages/cx/src/data/ops/moveElement.js b/packages/cx/src/data/ops/moveElement.js deleted file mode 100644 index 30dd6595c..000000000 --- a/packages/cx/src/data/ops/moveElement.js +++ /dev/null @@ -1,14 +0,0 @@ -export function moveElement(array, sourceIndex, targetIndex) { - if (targetIndex == sourceIndex) return array; - - let el = array[sourceIndex]; - let res = [...array]; - if (sourceIndex < targetIndex) { - for (let i = sourceIndex; i + 1 < targetIndex; i++) res[i] = res[i + 1]; - targetIndex--; - } else { - for (let i = sourceIndex; i > targetIndex; i--) res[i] = res[i - 1]; - } - res[targetIndex] = el; - return res; -} \ No newline at end of file diff --git a/packages/cx/src/data/ops/moveElement.ts b/packages/cx/src/data/ops/moveElement.ts new file mode 100644 index 000000000..0358fdcd2 --- /dev/null +++ b/packages/cx/src/data/ops/moveElement.ts @@ -0,0 +1,21 @@ +export function moveElement(array: T[], sourceIndex: number, targetIndex: number): T[] { + if (sourceIndex === targetIndex) return array; + + const result = [...array]; + const element = result[sourceIndex]; + + if (sourceIndex < targetIndex) { + for (let i = sourceIndex; i < targetIndex - 1; i++) { + result[i] = result[i + 1]; + } + result[targetIndex - 1] = result[targetIndex]; + targetIndex--; + } else { + for (let i = sourceIndex; i > targetIndex; i--) { + result[i] = result[i - 1]; + } + } + + result[targetIndex] = element; + return result; +} diff --git a/packages/cx/src/data/ops/removeTreeNodes.d.ts b/packages/cx/src/data/ops/removeTreeNodes.d.ts deleted file mode 100644 index 79065ed85..000000000 --- a/packages/cx/src/data/ops/removeTreeNodes.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function removeTreeNodes( - array: T[], - criteria: (item?: T, index?: number) => boolean, - childrenField?: string -): T[]; diff --git a/packages/cx/src/data/ops/removeTreeNodes.js b/packages/cx/src/data/ops/removeTreeNodes.js deleted file mode 100644 index 149d9c0ff..000000000 --- a/packages/cx/src/data/ops/removeTreeNodes.js +++ /dev/null @@ -1,5 +0,0 @@ -import { updateTree } from "./updateTree"; - -export function removeTreeNodes(array, criteria, childrenField = "$children") { - return updateTree(array, null, () => false, childrenField, criteria); -} diff --git a/packages/cx/src/data/ops/removeTreeNodes.spec.js b/packages/cx/src/data/ops/removeTreeNodes.spec.js deleted file mode 100644 index 022626559..000000000 --- a/packages/cx/src/data/ops/removeTreeNodes.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import {Store} from '../Store'; -import {removeTreeNodes} from './removeTreeNodes'; -import assert from 'assert'; - -describe('removeTreeNodes', function() { - it('removes all nodes that satisfy criteria', function () { - let store = new Store({ - data: {array: [{ - id: 'n1', - value: 1, - children: [{ - id: 'n11', - value: 2 - }, { - id: 'n12', - value: 3 - }] - }]} - }); - - assert(store.update('array', removeTreeNodes, x => x.value > 1, 'children')); - assert.deepEqual(store.get('array'), [{ - id: 'n1', - value: 1, - children: [] - }]); - }); -}); diff --git a/packages/cx/src/data/ops/removeTreeNodes.spec.ts b/packages/cx/src/data/ops/removeTreeNodes.spec.ts new file mode 100644 index 000000000..5e5a371cf --- /dev/null +++ b/packages/cx/src/data/ops/removeTreeNodes.spec.ts @@ -0,0 +1,37 @@ +import { Store } from "../Store"; +import { removeTreeNodes } from "./removeTreeNodes"; +import assert from "assert"; + +describe("removeTreeNodes", function () { + it("removes all nodes that satisfy criteria", function () { + let store = new Store({ + data: { + array: [ + { + id: "n1", + value: 1, + children: [ + { + id: "n11", + value: 2, + }, + { + id: "n12", + value: 3, + }, + ], + }, + ], + }, + }); + + assert(store.update("array", removeTreeNodes, (x: any) => x.value > 1, "children")); + assert.deepEqual(store.get("array"), [ + { + id: "n1", + value: 1, + children: [], + }, + ]); + }); +}); diff --git a/packages/cx/src/data/ops/removeTreeNodes.ts b/packages/cx/src/data/ops/removeTreeNodes.ts new file mode 100644 index 000000000..cefd811aa --- /dev/null +++ b/packages/cx/src/data/ops/removeTreeNodes.ts @@ -0,0 +1,15 @@ +import { updateTree } from "./updateTree"; + +export function removeTreeNodes( + array: T[] | undefined, + criteria: (node: T) => boolean, + childrenField: keyof T, +): T[] | undefined { + return updateTree( + array, + (x) => x, + () => false, + childrenField, + criteria, + ); +} diff --git a/packages/cx/src/data/ops/updateArray.d.ts b/packages/cx/src/data/ops/updateArray.d.ts deleted file mode 100644 index 4256583a7..000000000 --- a/packages/cx/src/data/ops/updateArray.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function updateArray( - array: T[], - updateCallback: (item: T, index?: number) => T, - itemFilter?: (item: T, index?: number) => boolean, - removeFilter?: (item: T, index?: number) => boolean -) : T[]; \ No newline at end of file diff --git a/packages/cx/src/data/ops/updateArray.js b/packages/cx/src/data/ops/updateArray.js deleted file mode 100644 index b6432f6c7..000000000 --- a/packages/cx/src/data/ops/updateArray.js +++ /dev/null @@ -1,24 +0,0 @@ -export function updateArray(array, updateCallback, itemFilter, removeFilter) { - - if (!array) - return array; - - let newArray = []; - let dirty = false; - - for (let index = 0; index < array.length; index++) { - let item = array[index]; - if (removeFilter && removeFilter(item, index)) { - dirty = true; - continue; - } - if (!itemFilter || itemFilter(item, index)) { - let newItem = updateCallback(item, index); - newArray.push(newItem); - if (newItem !== item) - dirty = true; - } else - newArray.push(item); - } - return dirty ? newArray : array; -} diff --git a/packages/cx/src/data/ops/updateArray.spec.js b/packages/cx/src/data/ops/updateArray.spec.js deleted file mode 100644 index b284df61d..000000000 --- a/packages/cx/src/data/ops/updateArray.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import {Store} from '../Store'; -import {updateArray} from './updateArray'; -import assert from 'assert'; - -describe('updateArray', function() { - it('updates all elements that satisfy criteria', function () { - let store = new Store({ - data: {array: [1, 2, 3, 4, 5, 6]} - }); - - assert(store.update('array', updateArray, x => x + 1, x => x > 3)); - assert.deepEqual(store.get('array'), [1, 2, 3, 5, 6, 7]); - }); - - it('does not modify data if not necessary', function () { - let array = [1, 2, 3, 4, 5, 6]; - let store = new Store({ - data: {array} - }); - - assert(false === store.update('array', updateArray, x => x + 1, x => x > 10)); - assert(store.get('array') === array); - }); - - it('works with null or undefined', function () { - let store = new Store(); - assert(false === store.update('array', updateArray, x => x + 1, x => x > 0)); - }); - - it('can remove elements given the criteria', function () { - let store = new Store({ - data: {array: [1, 2, 3, 4, 5, 6]} - }); - - assert(store.update('array', updateArray, x => x + 1, x => x > 3, x => x <= 3)); - assert.deepEqual(store.get('array'), [5, 6, 7]); - }); -}); diff --git a/packages/cx/src/data/ops/updateArray.spec.ts b/packages/cx/src/data/ops/updateArray.spec.ts new file mode 100644 index 000000000..a78d84497 --- /dev/null +++ b/packages/cx/src/data/ops/updateArray.spec.ts @@ -0,0 +1,69 @@ +import { Store } from "../Store"; +import { updateArray } from "./updateArray"; +import assert from "assert"; + +describe("updateArray", function () { + it("updates all elements that satisfy criteria", function () { + let store = new Store({ + data: { array: [1, 2, 3, 4, 5, 6] }, + }); + + assert( + store.update( + "array", + updateArray, + (x) => x + 1, + (x) => x > 3, + ), + ); + assert.deepEqual(store.get("array"), [1, 2, 3, 5, 6, 7]); + }); + + it("does not modify data if not necessary", function () { + let array = [1, 2, 3, 4, 5, 6]; + let store = new Store({ + data: { array }, + }); + + assert( + false === + store.update( + "array", + updateArray, + (x) => x + 1, + (x) => x > 10, + ), + ); + assert(store.get("array") === array); + }); + + it("works with null or undefined", function () { + let store = new Store(); + assert( + false === + store.update( + "array", + updateArray, + (x) => x + 1, + (x) => x > 0, + ), + ); + }); + + it("can remove elements given the criteria", function () { + let store = new Store({ + data: { array: [1, 2, 3, 4, 5, 6] }, + }); + + assert( + store.update( + "array", + updateArray, + (x) => x + 1, + (x) => x > 3, + (x) => x <= 3, + ), + ); + assert.deepEqual(store.get("array"), [5, 6, 7]); + }); +}); diff --git a/packages/cx/src/data/ops/updateArray.ts b/packages/cx/src/data/ops/updateArray.ts new file mode 100644 index 000000000..9794160c7 --- /dev/null +++ b/packages/cx/src/data/ops/updateArray.ts @@ -0,0 +1,31 @@ +export function updateArray( + array: T[] | undefined, + updateCallback: (item: T, index: number) => T, + itemFilter?: null | ((item: T, index: number) => boolean), + removeFilter?: (item: T, index: number) => boolean, +): T[] | undefined { + if (!array) return array; + + const newArray: T[] = []; + let dirty: boolean = false; + + for (let index = 0; index < array.length; index++) { + const item = array[index]; + + if (removeFilter && removeFilter(item, index)) { + dirty = true; + continue; + } + + if (!itemFilter || itemFilter(item, index)) { + const newItem = updateCallback(item, index); + newArray.push(newItem); + + if (newItem !== item) dirty = true; + } else { + newArray.push(item); + } + } + + return dirty ? newArray : array; +} diff --git a/packages/cx/src/data/ops/updateTree.d.ts b/packages/cx/src/data/ops/updateTree.d.ts deleted file mode 100644 index f16960ddf..000000000 --- a/packages/cx/src/data/ops/updateTree.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function updateTree( - array: T[], - updateCallback: (item?: T, index?: number) => T, - itemFilter?: (item?: T, index?: number) => boolean, - childrenField?: string, - removeFilter?: (item?: T, index?: number) => boolean -): T[]; diff --git a/packages/cx/src/data/ops/updateTree.js b/packages/cx/src/data/ops/updateTree.js deleted file mode 100644 index 49dd66e70..000000000 --- a/packages/cx/src/data/ops/updateTree.js +++ /dev/null @@ -1,25 +0,0 @@ -import { updateArray } from './updateArray'; - -export function updateTree(array, updateCallback, itemFilter, childrenField, removeFilter) { - return updateArray(array, item => { - if (!itemFilter || itemFilter(item)) - item = updateCallback(item); - - let children = item[childrenField]; - if (!Array.isArray(children)) - return item; - - let updatedChildren = updateTree( - children, - updateCallback, - itemFilter, - childrenField, - removeFilter - ); - - if (updatedChildren != children) - return { ...item, [childrenField]: updatedChildren }; - - return item; - }, null, removeFilter); -} diff --git a/packages/cx/src/data/ops/updateTree.spec.js b/packages/cx/src/data/ops/updateTree.spec.js deleted file mode 100644 index b8e82a714..000000000 --- a/packages/cx/src/data/ops/updateTree.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Store } from '../Store'; -import { updateTree } from './updateTree'; -import assert from 'assert'; - -describe('updateTree', function () { - it('updates all nodes that satisfy criteria', function () { - let store = new Store({ - data: { - array: [{ - id: 'n1', - value: 1, - children: [{ - id: 'n11', - value: 2 - }, { - id: 'n12', - value: 3 - }] - }] - } - }); - - assert(store.update('array', updateTree, x => ({ ...x, value: x.value + 1 }), x => x.value > 1, 'children')); - assert.deepEqual(store.get('array'), [{ - id: 'n1', - value: 1, - children: [{ - id: 'n11', - value: 3 - }, { - id: 'n12', - value: 4 - }] - }]); - }); -}); diff --git a/packages/cx/src/data/ops/updateTree.spec.ts b/packages/cx/src/data/ops/updateTree.spec.ts new file mode 100644 index 000000000..ae658dbcb --- /dev/null +++ b/packages/cx/src/data/ops/updateTree.spec.ts @@ -0,0 +1,54 @@ +import { Store } from "../Store"; +import { updateTree } from "./updateTree"; +import assert from "assert"; + +describe("updateTree", function () { + it("updates all nodes that satisfy criteria", function () { + let store = new Store({ + data: { + array: [ + { + id: "n1", + value: 1, + children: [ + { + id: "n11", + value: 2, + }, + { + id: "n12", + value: 3, + }, + ], + }, + ], + }, + }); + + assert( + store.update( + "array", + updateTree, + (x) => ({ ...x, value: x.value + 1 }), + (x) => x.value > 1, + "children", + ), + ); + assert.deepEqual(store.get("array"), [ + { + id: "n1", + value: 1, + children: [ + { + id: "n11", + value: 3, + }, + { + id: "n12", + value: 4, + }, + ], + }, + ]); + }); +}); diff --git a/packages/cx/src/data/ops/updateTree.ts b/packages/cx/src/data/ops/updateTree.ts new file mode 100644 index 000000000..519bcf5f2 --- /dev/null +++ b/packages/cx/src/data/ops/updateTree.ts @@ -0,0 +1,23 @@ +import { updateArray } from "./updateArray"; + +export function updateTree( + array: T[] | undefined, + updateCallback: (item: T) => T, + itemFilter: ((item: T) => boolean) | null, + childrenField: keyof T, + removeFilter?: (item: T) => boolean, +): T[] | undefined { + return updateArray( + array, + (item: T) => { + if (!itemFilter || itemFilter(item)) item = updateCallback(item); + const children = item[childrenField]; + if (!Array.isArray(children)) return item; + const updatedChildren = updateTree(children, updateCallback, itemFilter, childrenField, removeFilter); + if (updatedChildren != children) return { ...item, [childrenField]: updatedChildren }; + return item; + }, + null, + removeFilter, + ); +} diff --git a/packages/cx/src/data/test-types.ts b/packages/cx/src/data/test-types.ts new file mode 100644 index 000000000..4119d111c --- /dev/null +++ b/packages/cx/src/data/test-types.ts @@ -0,0 +1,7 @@ +import { AccessorChain } from "./createAccessorModelProxy"; + +// Test what AccessorChain looks like +type Test1 = AccessorChain; +type Test2 = AccessorChain["length"]; + +// The issue: does AccessorChain properly map array properties? diff --git a/packages/cx/src/hooks/createLocalStorageRef.d.ts b/packages/cx/src/hooks/createLocalStorageRef.d.ts deleted file mode 100644 index 14ca3f2af..000000000 --- a/packages/cx/src/hooks/createLocalStorageRef.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Ref } from "../data"; - -export function createLocalStorageRef(key: string): Ref; diff --git a/packages/cx/src/hooks/createLocalStorageRef.js b/packages/cx/src/hooks/createLocalStorageRef.js deleted file mode 100644 index 690c8dc51..000000000 --- a/packages/cx/src/hooks/createLocalStorageRef.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useStore } from "./store"; -import { Ref } from "../data/Ref"; - -export function createLocalStorageRef(key) { - let store = useStore(); - - return new Ref({ - get() { - let json = localStorage.getItem(key); - if (!json) return localStorage.hasOwnProperty(key) ? null : undefined; - return JSON.parse(json); - }, - set(value) { - if (value === undefined) localStorage.removeItem(key); - else localStorage.setItem(key, JSON.stringify(value)); - store.meta.version++; - store.notify(); - }, - }); -} diff --git a/packages/cx/src/hooks/createLocalStorageRef.ts b/packages/cx/src/hooks/createLocalStorageRef.ts new file mode 100644 index 000000000..7310d4590 --- /dev/null +++ b/packages/cx/src/hooks/createLocalStorageRef.ts @@ -0,0 +1,21 @@ +import { useStore } from "./store"; +import { Ref } from "../data/Ref"; + +export function createLocalStorageRef(key: string): Ref { + let store = useStore(); + + return new Ref({ + get(): T { + let json = localStorage.getItem(key); + if (!json) return localStorage.hasOwnProperty(key) ? (null as any) : (undefined as any); + return JSON.parse(json) as T; + }, + set(value: T): boolean { + if (value === undefined) localStorage.removeItem(key); + else localStorage.setItem(key, JSON.stringify(value)); + store.meta.version++; + store.notify(); + return true; + }, + }); +} diff --git a/packages/cx/src/hooks/index.js b/packages/cx/src/hooks/index.js deleted file mode 100644 index 5678843ba..000000000 --- a/packages/cx/src/hooks/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./store"; -export * from "./useEffect"; -export * from "./useInterval"; -export * from "./useTrigger"; -export * from "./useState"; -export * from "./createLocalStorageRef"; -export * from "./resolveCallback"; -export * from "./invokeCallback"; \ No newline at end of file diff --git a/packages/cx/src/hooks/index.d.ts b/packages/cx/src/hooks/index.ts similarity index 100% rename from packages/cx/src/hooks/index.d.ts rename to packages/cx/src/hooks/index.ts diff --git a/packages/cx/src/hooks/invokeCallback.d.ts b/packages/cx/src/hooks/invokeCallback.d.ts deleted file mode 100644 index a43b7feff..000000000 --- a/packages/cx/src/hooks/invokeCallback.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Instance } from "./../ui/Instance.d"; -import { isFunction } from "../util/isFunction"; -import { isString } from "../util/isString"; - -export function invokeCallback(instance: Instance, callback: string | ((...args: any) => any), ...args: any[]): any; diff --git a/packages/cx/src/hooks/invokeCallback.js b/packages/cx/src/hooks/invokeCallback.js deleted file mode 100644 index 5f23ffe4d..000000000 --- a/packages/cx/src/hooks/invokeCallback.js +++ /dev/null @@ -1,7 +0,0 @@ -import { isFunction } from "../util/isFunction"; -import { isString } from "../util/isString"; - -export function invokeCallback(instance, callback, ...args) { - if (isString(callback)) return instance.invokeControllerMethod(callback, ...args); - if (isFunction(callback)) return callback(...args); -} \ No newline at end of file diff --git a/packages/cx/src/hooks/invokeCallback.spec.js b/packages/cx/src/hooks/invokeCallback.spec.js deleted file mode 100644 index 6a9d91b3e..000000000 --- a/packages/cx/src/hooks/invokeCallback.spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import { useTrigger } from "./useTrigger"; -import { createFunctionalComponent } from "../ui/createFunctionalComponent"; -import { Store } from "../data/Store"; -import renderer from "react-test-renderer"; -import { HtmlElement } from "../widgets/HtmlElement"; -import { VDOM } from "../ui/VDOM"; -import { Cx } from "../ui/Cx"; -import assert from "assert"; -import { resolveCallback } from "./resolveCallback"; -import { invokeCallback } from "./invokeCallback"; - -describe("invokeCallback", () => { - it("works with functions", () => { - const FComp = createFunctionalComponent(({ onTest }) => { - invokeCallback(null, onTest, "works"); - return ( - -

- - ); - }); - - let store = new Store(); - let value; - const component = renderer.create( { value = v } }} store={store} subscribe immediate />); - - component.toJSON(); - assert.equal(value, "works"); - }); - - it("works with controller methods", () => { - const FComp = createFunctionalComponent(({ onTest }) => { - return ( - -
{ invokeCallback(instance, onTest, "works") }} /> - - ); - }); - - let store = new Store(); - let value; - const component = renderer.create(); - - component.toJSON(); - assert.equal(value, "works"); - }); -}); diff --git a/packages/cx/src/hooks/invokeCallback.spec.tsx b/packages/cx/src/hooks/invokeCallback.spec.tsx new file mode 100644 index 000000000..ca7f1c64a --- /dev/null +++ b/packages/cx/src/hooks/invokeCallback.spec.tsx @@ -0,0 +1,59 @@ +import assert from "assert"; +import { Store } from "../data/Store"; +import { createFunctionalComponent } from "../ui/createFunctionalComponent"; +import { createTestRenderer } from "../util/test/createTestRenderer"; +import { invokeCallback } from "./invokeCallback"; + +describe("invokeCallback", () => { + it("works with functions", async () => { + const FComp = createFunctionalComponent(({ onTest }: { onTest: (v: any) => void }) => { + invokeCallback(null!, onTest, "works"); + return ( + +
+ + ); + }); + + let store = new Store(); + let value; + const component = await createTestRenderer(store, { + type: FComp, + onTest: (v: any) => { + value = v; + }, + }); + + component.toJSON(); + assert.equal(value, "works"); + }); + + it("works with controller methods", async () => { + const FComp = createFunctionalComponent(({ onTest }: { onTest: (v: any) => void }) => { + return ( + +
{ + invokeCallback(instance, onTest, "works"); + }} + /> + + ); + }); + + let store = new Store(); + let value; + const component = await createTestRenderer(store, { + type: FComp, + onTest: "onTest", + controller: { + onTest(v: any) { + value = v; + }, + }, + }); + + component.toJSON(); + assert.equal(value, "works"); + }); +}); diff --git a/packages/cx/src/hooks/invokeCallback.ts b/packages/cx/src/hooks/invokeCallback.ts new file mode 100644 index 000000000..4a22adb00 --- /dev/null +++ b/packages/cx/src/hooks/invokeCallback.ts @@ -0,0 +1,8 @@ +import { isFunction } from "../util/isFunction"; +import { isString } from "../util/isString"; +import { Instance } from "../ui/Instance"; + +export function invokeCallback(instance: Instance, callback: string | ((...args: any[]) => any), ...args: any[]): any { + if (isString(callback)) return instance.invokeControllerMethod(callback, ...args); + if (isFunction(callback)) return callback(...args); +} diff --git a/packages/cx/src/hooks/resolveCallback.d.ts b/packages/cx/src/hooks/resolveCallback.d.ts deleted file mode 100644 index c3f7001f7..000000000 --- a/packages/cx/src/hooks/resolveCallback.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Instance } from "./../ui/Instance.d"; - -export function resolveCallback(callback: string | ((...args) => any), instance?: Instance): (...args) => any; diff --git a/packages/cx/src/hooks/resolveCallback.js b/packages/cx/src/hooks/resolveCallback.js deleted file mode 100644 index 83031915d..000000000 --- a/packages/cx/src/hooks/resolveCallback.js +++ /dev/null @@ -1,12 +0,0 @@ -import { isFunction } from "../util/isFunction"; -import { isString } from "../util/isString"; -import { getCurrentInstance } from "../ui/createFunctionalComponent"; - -export function resolveCallback(callback, instance) { - if (isFunction(callback)) - return callback; - if (isString(callback)) { - if (!instance) instance = getCurrentInstance(); - return (...args) => instance.invokeControllerMethod(callback, ...args); - } -} \ No newline at end of file diff --git a/packages/cx/src/hooks/resolveCallback.spec.js b/packages/cx/src/hooks/resolveCallback.spec.js deleted file mode 100644 index dbc6760e1..000000000 --- a/packages/cx/src/hooks/resolveCallback.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { useTrigger } from "./useTrigger"; -import { createFunctionalComponent } from "../ui/createFunctionalComponent"; -import { Store } from "../data/Store"; -import renderer from "react-test-renderer"; -import { HtmlElement } from "../widgets/HtmlElement"; -import { VDOM } from "../ui/VDOM"; -import { Cx } from "../ui/Cx"; -import assert from "assert"; -import { resolveCallback } from "./resolveCallback"; - -describe("resolveCallback", () => { - it("works with functions", () => { - const FComp = createFunctionalComponent(({ onTest }) => { - let callback = resolveCallback(onTest); - callback("works"); - return ( - -
- - ); - }); - - let store = new Store(); - let value; - const component = renderer.create( { value = v } }} store={store} subscribe immediate />); - - component.toJSON(); - assert.equal(value, "works"); - }); - - it("works with controller methods", () => { - const FComp = createFunctionalComponent(({ onTest }) => { - let callback = resolveCallback(onTest); - return ( - -
{ callback("works"); }} /> - - ); - }); - - let store = new Store(); - let value; - const component = renderer.create(); - - component.toJSON(); - assert.equal(value, "works"); - }); -}); diff --git a/packages/cx/src/hooks/resolveCallback.spec.tsx b/packages/cx/src/hooks/resolveCallback.spec.tsx new file mode 100644 index 000000000..9402c9847 --- /dev/null +++ b/packages/cx/src/hooks/resolveCallback.spec.tsx @@ -0,0 +1,71 @@ +import assert from "assert"; +import { Store } from "../data/Store"; +import { createFunctionalComponent } from "../ui/createFunctionalComponent"; +import { createTestRenderer } from "../util/test/createTestRenderer"; +import { resolveCallback } from "./resolveCallback"; + +describe("resolveCallback", () => { + it("works with functions", async () => { + const FComp = createFunctionalComponent(({ onTest }: { onTest: (value: string) => void }) => { + let callback = resolveCallback(onTest); + assert(typeof callback === "function"); + callback("works"); + return ( + +
+ + ); + }); + + let store = new Store(); + let value; + const component = await createTestRenderer( + store, + + { + value = v; + }} + /> + , + ); + + component.toJSON(); + assert.equal(value, "works"); + }); + + it("works with controller methods", async () => { + const FComp = createFunctionalComponent(({ onTest }: { onTest: string | ((value: string) => void) }) => { + let callback = resolveCallback(onTest); + assert(typeof callback === "function"); + return ( + +
{ + callback("works"); + }} + /> + + ); + }); + + let store = new Store(); + let value; + const component = await createTestRenderer( + store, + + + , + ); + + component.toJSON(); + assert.equal(value, "works"); + }); +}); diff --git a/packages/cx/src/hooks/resolveCallback.ts b/packages/cx/src/hooks/resolveCallback.ts new file mode 100644 index 000000000..601f7a141 --- /dev/null +++ b/packages/cx/src/hooks/resolveCallback.ts @@ -0,0 +1,16 @@ +import { isFunction } from "../util/isFunction"; +import { isString } from "../util/isString"; +import { getCurrentInstance } from "../ui/createFunctionalComponent"; +import { Instance } from "../ui/Instance"; + +export function resolveCallback( + callback: string | ((...args: any[]) => any), + instance?: Instance, +): ((...args: any[]) => any) | undefined { + if (isFunction(callback)) return callback; + if (isString(callback)) { + if (!instance) instance = getCurrentInstance(); + return (...args: any[]) => instance!.invokeControllerMethod(callback, ...args); + } + return undefined; +} diff --git a/packages/cx/src/hooks/store.d.ts b/packages/cx/src/hooks/store.d.ts deleted file mode 100644 index d98e13513..000000000 --- a/packages/cx/src/hooks/store.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Selector } from "../core"; -import { ViewMethods, View } from "../data"; - -export function useStore(): View; - -export function useStoreMethods(): ViewMethods; - -export function ref(info: any): Selector; \ No newline at end of file diff --git a/packages/cx/src/hooks/store.js b/packages/cx/src/hooks/store.js deleted file mode 100644 index 11168491f..000000000 --- a/packages/cx/src/hooks/store.js +++ /dev/null @@ -1,32 +0,0 @@ -import { getCurrentInstance } from "../ui/createFunctionalComponent"; -import { getSelector } from "../data/getSelector"; -import { isObject } from "../util/isObject"; -import { expression } from "../data/Expression"; -import { stringTemplate } from "../data/StringTemplate"; -import { Ref } from "../data/Ref"; - -export function useStore() { - return getCurrentInstance().store; -} - -export function useStoreMethods() { - return getCurrentInstance().store.getMethods(); -} - -export function ref(info) { - if (isObject(info)) { - if (info.bind) return useStore().ref(info.bind, info.defaultValue); - if (info.get) return info; - if (info.expr) { - let store = useStore(); - let selector = expression(info.expr).memoize(); - return new Ref({ get: () => selector(store.getData()), set: info.set }); - } - if (info.tpl) { - let store = useStore(); - let selector = stringTemplate(info.tpl).memoize(); - return new Ref({ get: () => selector(store.getData()), set: info.set }); - } - } - return getSelector(info); -} diff --git a/packages/cx/src/hooks/store.spec.js b/packages/cx/src/hooks/store.spec.js deleted file mode 100644 index eb0957c8a..000000000 --- a/packages/cx/src/hooks/store.spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { ref } from "./store"; -import { createFunctionalComponent } from "../ui/createFunctionalComponent"; -import { Store } from "../data/Store"; -import renderer from "react-test-renderer"; -import { HtmlElement } from "../widgets/HtmlElement"; -import { VDOM } from "../ui/VDOM"; -import { Cx } from "../ui/Cx"; -import assert from "assert"; -import { computable } from "../data"; - -describe("ref", () => { - it("allows store references in functional components", () => { - const FComp = createFunctionalComponent(({}) => { - let testValue = ref({ bind: "x", defaultValue: 10 }); - return ( - -
- - ); - }); - - let store = new Store(); - - const component = renderer.create(); - - let tree = component.toJSON(); - assert.deepEqual(tree, { - type: "div", - children: ["10"], - props: {}, - }); - }); - - it("can be used to adapt any prop passed to a functional component", () => { - const FComp = createFunctionalComponent(({ value }) => { - return ( - -
`x${value}`)} /> - - ); - }); - - let store = new Store({ data: { value: 100 } }); - - function test(value, expectation) { - const component = renderer.create( - - - - ); - let tree = component.toJSON(); - assert.deepEqual(tree, { - type: "div", - children: [expectation], - props: {}, - }); - } - - test({ bind: "value" }, "x100"); - test({ expr: "{value}" }, "x100"); - test({ tpl: "{value:n;2}" }, "x100.00"); - test(200, "x200"); - test(() => 500, "x500"); - test( - computable("value", (value) => value + 100), - "x200" - ); - test(null, "xnull"); - test(undefined, "xundefined"); - test(0, "x0"); - test(false, "xfalse"); - }); -}); diff --git a/packages/cx/src/hooks/store.spec.tsx b/packages/cx/src/hooks/store.spec.tsx new file mode 100644 index 000000000..b48f44011 --- /dev/null +++ b/packages/cx/src/hooks/store.spec.tsx @@ -0,0 +1,67 @@ +import assert from "assert"; +import { Prop } from "../ui/Prop"; +import { createTestRenderer } from "../util/test/createTestRenderer"; +import { computable } from "../data"; +import { Store } from "../data/Store"; +import { createFunctionalComponent } from "../ui/createFunctionalComponent"; +import { ref } from "./store"; + +describe("ref", () => { + it("allows store references in functional components", async () => { + const FComp = createFunctionalComponent(({}) => { + let testValue = ref({ bind: "x", defaultValue: 10 }); + return ( + +
+ + ); + }); + + let store = new Store(); + + const component = await createTestRenderer(store, FComp); + + let tree = component.toJSON(); + assert.deepEqual(tree, { + type: "div", + children: ["10"], + props: {}, + }); + }); + + it("can be used to adapt any prop passed to a functional component", async () => { + const FComp = createFunctionalComponent(({ value }: { value: Prop }) => { + return ( + +
`x${value}`)} /> + + ); + }); + + let store = new Store({ data: { value: 100 } }); + + async function test(value: Prop, expectation: any) { + const component = await createTestRenderer(store, ); + let tree = component.toJSON(); + assert.deepEqual(tree, { + type: "div", + children: [expectation], + props: {}, + }); + } + + await test({ bind: "value" }, "x100"); + await test({ expr: "{value}" }, "x100"); + await test({ tpl: "{value:n;2}" }, "x100.00"); + await test(200, "x200"); + await test(() => 500, "x500"); + await test( + computable("value", (value: string) => value + 100), + "x200", + ); + await test(null, "xnull"); + await test(undefined, "xundefined"); + await test(0, "x0"); + await test(false, "xfalse"); + }); +}); diff --git a/packages/cx/src/hooks/store.ts b/packages/cx/src/hooks/store.ts new file mode 100644 index 000000000..cb0df20cd --- /dev/null +++ b/packages/cx/src/hooks/store.ts @@ -0,0 +1,46 @@ +import { Selector } from "../data/Selector"; +import { BindingObject, isBindingObject } from "../data/Binding"; +import { expression } from "../data/Expression"; +import { getSelector } from "../data/getSelector"; +import { Ref } from "../data/Ref"; +import { stringTemplate } from "../data/StringTemplate"; +import { View } from "../data/View"; +import { getCurrentInstance } from "../ui/createFunctionalComponent"; +import { GetSet, Prop } from "../ui/Prop"; +import { hasFunctionAtKey, hasStringAtKey } from "../util/hasKey"; +import { isObject } from "../util/isObject"; + +export function useStore(): View { + return getCurrentInstance().store; +} + +export function useStoreMethods(): ReturnType { + return getCurrentInstance().store.getMethods(); +} + +export type RefInput = Prop | Ref; + +export function ref(input: BindingObject): GetSet; +export function ref(input: Ref): GetSet; +export function ref(input: Prop): Selector; +export function ref(info: unknown): GetSet | Selector { + if (isObject(info)) { + if (isBindingObject(info)) return useStore().ref(info.bind, info.defaultValue); + if (hasFunctionAtKey(info, "get")) return info as unknown as Ref; + if (hasFunctionAtKey(info, "memoize")) return info as unknown as Selector; + if (hasStringAtKey(info, "expr")) { + let store = useStore(); + let selector = expression(info.expr).memoize(); + return new Ref({ get: () => selector(store.getData()), set: (info as any).set }) as unknown as Selector; + } + if (hasStringAtKey(info, "tpl")) { + let store = useStore(); + let selector = stringTemplate(info.tpl).memoize(); + return new Ref({ + get: () => selector(store.getData()) as T, + set: (info as any).set, + }) as unknown as Selector; + } + } + return getSelector(info); +} diff --git a/packages/cx/src/hooks/useEffect.d.ts b/packages/cx/src/hooks/useEffect.d.ts deleted file mode 100644 index 696d48b12..000000000 --- a/packages/cx/src/hooks/useEffect.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function useEffect(callback: () => () => void): void; - -export function useCleanup(cleanupCallback: () => void): () => void; \ No newline at end of file diff --git a/packages/cx/src/hooks/useEffect.js b/packages/cx/src/hooks/useEffect.js deleted file mode 100644 index 05c0d1319..000000000 --- a/packages/cx/src/hooks/useEffect.js +++ /dev/null @@ -1,15 +0,0 @@ -import {getCurrentInstance} from "../ui/createFunctionalComponent"; - -export function useEffect(callback) { - let destroyCallback = callback(); - if (destroyCallback) - getCurrentInstance().subscribeOnDestroy(destroyCallback); -} - -export function useCleanup(cleanupCallback) { - let unsubscribe = getCurrentInstance().subscribeOnDestroy(cleanupCallback); - return () => { - unsubscribe(); - cleanupCallback(); - } -} \ No newline at end of file diff --git a/packages/cx/src/hooks/useEffect.ts b/packages/cx/src/hooks/useEffect.ts new file mode 100644 index 000000000..4e697c070 --- /dev/null +++ b/packages/cx/src/hooks/useEffect.ts @@ -0,0 +1,14 @@ +import { getCurrentInstance } from "../ui/createFunctionalComponent"; + +export function useEffect(callback: () => (() => void) | void): void { + let destroyCallback = callback(); + if (destroyCallback) getCurrentInstance().subscribeOnDestroy(destroyCallback); +} + +export function useCleanup(cleanupCallback: () => void): () => void { + let unsubscribe = getCurrentInstance().subscribeOnDestroy(cleanupCallback); + return () => { + unsubscribe(); + cleanupCallback(); + }; +} diff --git a/packages/cx/src/hooks/useInterval.d.ts b/packages/cx/src/hooks/useInterval.d.ts deleted file mode 100644 index ffa016625..000000000 --- a/packages/cx/src/hooks/useInterval.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function useInterval(callback: () => void, timeout: number): () => void; \ No newline at end of file diff --git a/packages/cx/src/hooks/useInterval.js b/packages/cx/src/hooks/useInterval.js deleted file mode 100644 index a21ba84e3..000000000 --- a/packages/cx/src/hooks/useInterval.js +++ /dev/null @@ -1,8 +0,0 @@ -import {useCleanup} from "./useEffect"; - -export function useInterval(callback, timeout) { - let timer = setInterval(callback, timeout); - return useCleanup(() => { - clearInterval(timer); - }); -} \ No newline at end of file diff --git a/packages/cx/src/hooks/useInterval.ts b/packages/cx/src/hooks/useInterval.ts new file mode 100644 index 000000000..f53124d71 --- /dev/null +++ b/packages/cx/src/hooks/useInterval.ts @@ -0,0 +1,8 @@ +import { useCleanup } from "./useEffect"; + +export function useInterval(callback: () => void, timeout: number): () => void { + let timer = setInterval(callback, timeout); + return useCleanup(() => { + clearInterval(timer); + }); +} diff --git a/packages/cx/src/hooks/useState.d.ts b/packages/cx/src/hooks/useState.d.ts deleted file mode 100644 index 1c78f1717..000000000 --- a/packages/cx/src/hooks/useState.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Ref } from "../data"; - -export function useState(defaultValue?: any): Ref \ No newline at end of file diff --git a/packages/cx/src/hooks/useState.js b/packages/cx/src/hooks/useState.js deleted file mode 100644 index e18ba65a9..000000000 --- a/packages/cx/src/hooks/useState.js +++ /dev/null @@ -1,16 +0,0 @@ -import { getCurrentInstance } from "../ui/createFunctionalComponent"; -import { Ref } from "../data/Ref"; - -let key = 0; - -export function useState(defaultValue) { - let instance = getCurrentInstance(); - let storeKey = '_state' + (++key); - instance.setState({ - [storeKey]: defaultValue - }); - return new Ref({ - get: () => instance.state[storeKey], - set: value => instance.setState({ [storeKey]: value }) - }); -} \ No newline at end of file diff --git a/packages/cx/src/hooks/useState.ts b/packages/cx/src/hooks/useState.ts new file mode 100644 index 000000000..ce130ee52 --- /dev/null +++ b/packages/cx/src/hooks/useState.ts @@ -0,0 +1,20 @@ +import { getCurrentInstance } from "../ui/createFunctionalComponent"; +import { Ref } from "../data/Ref"; + +let key: number = 0; + +export function useState(defaultValue?: T): Ref { + let instance = getCurrentInstance(); + let storeKey = "_state" + ++key; + instance.setState({ + [storeKey]: defaultValue, + }); + + return new Ref({ + get: (): T => instance.state[storeKey], + set: (value: T): boolean => { + instance.setState({ [storeKey]: value }); + return true; + }, + }); +} diff --git a/packages/cx/src/hooks/useTrigger.d.ts b/packages/cx/src/hooks/useTrigger.d.ts deleted file mode 100644 index b29d7350a..000000000 --- a/packages/cx/src/hooks/useTrigger.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Path } from "../data"; - -export function addExploreCallback(callback: any): () => void; - -export function useTrigger(args: Array, callback: (...args) => void, autoRun?: boolean): () => void; \ No newline at end of file diff --git a/packages/cx/src/hooks/useTrigger.js b/packages/cx/src/hooks/useTrigger.js deleted file mode 100644 index a7c70b4bd..000000000 --- a/packages/cx/src/hooks/useTrigger.js +++ /dev/null @@ -1,20 +0,0 @@ -import { getCurrentInstance } from "../ui/createFunctionalComponent"; -import { isArray } from "../util/isArray"; -import { computable } from "../data/computable"; -import { useStore } from "./store"; - -export function addExploreCallback(callback) { - let instance = getCurrentInstance(); - if (!instance.computables) instance.computables = []; - instance.computables.push(callback); - return () => { - instance.computables = instance.computables.filter((cb) => cb !== callback); - }; -} - -export function useTrigger(args, callback, autoRun) { - if (!isArray(args)) throw new Error("First argument to addTrigger should be an array."); - let store = useStore(); - let selector = computable(...args, callback).memoize(!autoRun && store.getData()); - return addExploreCallback(selector); -} diff --git a/packages/cx/src/hooks/useTrigger.spec.js b/packages/cx/src/hooks/useTrigger.spec.js deleted file mode 100644 index 6b77d6e29..000000000 --- a/packages/cx/src/hooks/useTrigger.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import { useTrigger } from "./useTrigger"; -import { createFunctionalComponent } from "../ui/createFunctionalComponent"; -import { Store } from "../data/Store"; -import renderer from "react-test-renderer"; -import { HtmlElement } from "../widgets/HtmlElement"; -import { VDOM } from "../ui/VDOM"; -import { Cx } from "../ui/Cx"; -import assert from "assert"; - -describe("useTrigger", () => { - it("works", () => { - let last = null; - - const FComp = createFunctionalComponent(({ ...props }) => { - useTrigger(["test"], (test) => { - last = test; - }); - - return ( - -
- - ); - }); - - let store = new Store(); - let test = store.ref("test", 1); - - const component = renderer.create(); - - component.toJSON(); - assert.equal(last, null); //trigger did not fire because it didn't receive true as the last argument - - test.set(2); - component.toJSON(); - assert.equal(last, 2); - }); - - it("fires immediately if the last argument is true", () => { - let last = null; - - const FComp = createFunctionalComponent(({ ...props }) => { - useTrigger( - ["test"], - (test) => { - last = test; - }, - true - ); - - return ( - -
- - ); - }); - - let store = new Store(); - let test = store.ref("test", 1); - - const component = renderer.create(); - - component.toJSON(); - assert.equal(last, 1); - - test.set(2); - component.toJSON(); - assert.equal(last, 2); - }); - - it("accepts refs as arguments", () => { - let last = null; - - let store = new Store(); - let test = store.ref("test", 1); - - const FComp = createFunctionalComponent(({ ...props }) => { - useTrigger( - [test], - (test) => { - last = test; - }, - true - ); - - return ( - -
- - ); - }); - - const component = renderer.create(); - - component.toJSON(); - assert.equal(last, 1); - - test.set(2); - component.toJSON(); - assert.equal(last, 2); - }); -}); diff --git a/packages/cx/src/hooks/useTrigger.spec.tsx b/packages/cx/src/hooks/useTrigger.spec.tsx new file mode 100644 index 000000000..3d3e82d1b --- /dev/null +++ b/packages/cx/src/hooks/useTrigger.spec.tsx @@ -0,0 +1,105 @@ +import assert from "assert"; +import { createTestRenderer, act } from "../util/test/createTestRenderer"; +import { Store } from "../data/Store"; +import { createFunctionalComponent } from "../ui/createFunctionalComponent"; +import { useTrigger } from "./useTrigger"; + +describe("useTrigger", () => { + it("works", async () => { + let last = null; + + const FComp = createFunctionalComponent(({ ...props }) => { + useTrigger(["test"], (test) => { + last = test; + }); + + return ( + +
+ + ); + }); + + let store = new Store(); + let test = store.ref("test", 1); + + const component = await createTestRenderer(store, FComp); + + component.toJSON(); + assert.equal(last, null); //trigger did not fire because it didn't receive true as the last argument + + await act(async () => { + test.set(2); + }); + component.toJSON(); + assert.equal(last, 2); + }); + + it("fires immediately if the last argument is true", async () => { + let last = null; + + const FComp = createFunctionalComponent(({ ...props }) => { + useTrigger( + ["test"], + (test) => { + last = test; + }, + true, + ); + + return ( + +
+ + ); + }); + + let store = new Store(); + let test = store.ref("test", 1); + + const component = await createTestRenderer(store, FComp); + + component.toJSON(); + assert.equal(last, 1); + + await act(async () => { + test.set(2); + }); + component.toJSON(); + assert.equal(last, 2); + }); + + it("accepts refs as arguments", async () => { + let last = null; + + let store = new Store(); + let test = store.ref("test", 1); + + const FComp = createFunctionalComponent(({ ...props }) => { + useTrigger( + [test], + (test) => { + last = test; + }, + true, + ); + + return ( + +
+ + ); + }); + + const component = await createTestRenderer(store, FComp); + + component.toJSON(); + assert.equal(last, 1); + + await act(async () => { + test.set(2); + }); + component.toJSON(); + assert.equal(last, 2); + }); +}); diff --git a/packages/cx/src/hooks/useTrigger.ts b/packages/cx/src/hooks/useTrigger.ts new file mode 100644 index 000000000..39d35cceb --- /dev/null +++ b/packages/cx/src/hooks/useTrigger.ts @@ -0,0 +1,26 @@ +import { getCurrentInstance } from "../ui/createFunctionalComponent"; +import { isArray } from "../util/isArray"; +import { computable, ComputableSelector } from "../data/computable"; +import { useStore } from "./store"; + +export function addExploreCallback(callback: (...args: any[]) => any): () => void { + let instance = getCurrentInstance(); + if (!instance.computables) instance.computables = []; + instance.computables.push(callback); + return () => { + if (instance.computables) { + instance.computables = instance.computables.filter((cb: unknown) => cb !== callback); + } + }; +} + +export function useTrigger( + args: ComputableSelector[], + callback: (...args: any[]) => void, + autoRun?: boolean, +): () => void { + if (!isArray(args)) throw new Error("First argument to useTrigger should be an array."); + let store = useStore(); + let selector = computable(...args, callback).memoize(!autoRun && store.getData()); + return addExploreCallback(selector); +} diff --git a/packages/cx/src/index.js b/packages/cx/src/index.ts similarity index 100% rename from packages/cx/src/index.js rename to packages/cx/src/index.ts diff --git a/packages/cx/src/jsx-dev-runtime.ts b/packages/cx/src/jsx-dev-runtime.ts new file mode 100644 index 000000000..1785fb0f3 --- /dev/null +++ b/packages/cx/src/jsx-dev-runtime.ts @@ -0,0 +1,4 @@ +import { jsx, jsxs } from "./jsx-runtime"; + +export const jsxDEV = jsx; +export const jsxsDEV = jsxs; diff --git a/packages/cx/src/jsx-runtime.spec.tsx b/packages/cx/src/jsx-runtime.spec.tsx new file mode 100644 index 000000000..47ab1a125 --- /dev/null +++ b/packages/cx/src/jsx-runtime.spec.tsx @@ -0,0 +1,431 @@ +import assert from "assert"; +import { Button } from "./widgets/Button"; +import { Widget, WidgetConfig } from "./ui/Widget"; +import { StringProp, NumberProp, BooleanProp } from "./core"; + +// Minimal mock implementations to avoid side effects from importing real widgets + +interface TextFieldConfig extends WidgetConfig { + value?: StringProp; + placeholder?: StringProp; + disabled?: BooleanProp; + readOnly?: BooleanProp; + required?: BooleanProp; + minLength?: NumberProp; + maxLength?: NumberProp; + validationErrorText?: StringProp; +} + +class TextField extends Widget {} + +interface NumberFieldConfig extends WidgetConfig { + value?: NumberProp; + placeholder?: StringProp; + minValue?: NumberProp; + maxValue?: NumberProp; + format?: StringProp; + disabled?: BooleanProp; + increment?: NumberProp; +} + +class NumberField extends Widget {} +import { Checkbox } from "./widgets/form/Checkbox"; +import { bind } from "./ui/bind"; +import { expr } from "./ui/expr"; +import { createFunctionalComponent } from "./ui/createFunctionalComponent"; + +/** + * These tests verify that widget props are correctly typed in JSX. + * Negative tests use @ts-expect-error to verify that incorrect types are rejected. + */ + +describe("jsx-runtime type inference", () => { + describe("TextField", () => { + it("accepts correct prop types", () => { + const widget = ( + + + + ); + assert.ok(widget); + }); + + it("accepts binding for string props", () => { + const widget = ( + + + + ); + assert.ok(widget); + }); + + it("accepts expressions for boolean props", () => { + const widget = ( + + + + ); + assert.ok(widget); + }); + + it("rejects number for placeholder (expects string)", () => { + const widget = ( + + {/* @ts-expect-error - placeholder should be StringProp, not a number */} + + + ); + assert.ok(widget); + }); + + it("rejects string for minLength (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - minLength should be NumberProp, not a string */} + + + ); + assert.ok(widget); + }); + + it("rejects string for maxLength (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - maxLength should be NumberProp, not a string */} + + + ); + assert.ok(widget); + }); + + it("rejects number for disabled (expects boolean)", () => { + const widget = ( + + {/* @ts-expect-error - disabled should be BooleanProp, not a number */} + + + ); + assert.ok(widget); + }); + + it("rejects string for readOnly (expects boolean)", () => { + const widget = ( + + {/* @ts-expect-error - readOnly should be BooleanProp, not a string */} + + + ); + assert.ok(widget); + }); + + it("rejects non-existent properties", () => { + const widget = ( + + {/* @ts-expect-error - nonExistentProp does not exist on TextFieldConfig */} + + + ); + assert.ok(widget); + }); + }); + + describe("NumberField", () => { + it("accepts correct prop types", () => { + const widget = ( + + + + ); + assert.ok(widget); + }); + + it("accepts bindings for number props", () => { + const widget = ( + + + + ); + assert.ok(widget); + }); + + it("rejects string for value (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - value should be NumberProp, not a literal string */} + + + ); + assert.ok(widget); + }); + + it("rejects string for minValue (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - minValue should be NumberProp, not a string */} + + + ); + assert.ok(widget); + }); + + it("rejects string for maxValue (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - maxValue should be NumberProp, not a string */} + + + ); + assert.ok(widget); + }); + + it("rejects boolean for increment (expects number)", () => { + const widget = ( + + {/* @ts-expect-error - increment should be NumberProp, not a boolean */} + + + ); + assert.ok(widget); + }); + + it("rejects number for format (expects string)", () => { + const widget = ( + + {/* @ts-expect-error - format should be StringProp, not a number */} + + + ); + assert.ok(widget); + }); + }); + + describe("Button", () => { + it("accepts correct prop types", () => { + const widget = ( + + +
+ ); +} + +// Class component +export class ReactClassComponent extends React.Component< + { label: string; children?: React.ReactNode }, + { active: boolean } +> { + constructor(props: { label: string; children?: React.ReactNode }) { + super(props); + this.state = { active: false }; + } + + render() { + return ( +
+ +
{this.props.children}
+
+ ); + } +} + +// Pure component +export class ReactPureComponent extends React.PureComponent<{ value: string }> { + render() { + return {this.props.value}; + } +} + +// Function component with useRef and useEffect +export function ReactRefEffectComponent(props: { onMount?: (element: HTMLDivElement | null) => void }) { + const divRef = React.useRef(null); + const mountedRef = React.useRef(false); + + React.useEffect(() => { + mountedRef.current = true; + props.onMount?.(divRef.current); + return () => { + mountedRef.current = false; + }; + }, []); + + return ( +
+ Component with ref and effect +
+ ); +} + +// Function component with useEffect that updates state +export function ReactEffectStateComponent(props: { value: string }) { + const [processed, setProcessed] = React.useState(""); + const renderCountRef = React.useRef(0); + + React.useEffect(() => { + setProcessed(`Processed: ${props.value}`); + }, [props.value]); + + renderCountRef.current += 1; + + return ( +
+ {processed} + {renderCountRef.current} +
+ ); +} + +// Component for testing prop translation +export function ReactPropsComponent(props: { + text: string; + count: number; + enabled: boolean; + tags?: string[]; + onClick?: () => void; +}) { + return ( +
+ {props.text} + {props.count} + {props.enabled ? "yes" : "no"} + {props.tags && {props.tags.join(", ")}} + {props.onClick && } +
+ ); +} diff --git a/packages/cx/src/widgets/HtmlElement.spec.js b/packages/cx/src/widgets/HtmlElement.spec.js deleted file mode 100644 index 3d63bb213..000000000 --- a/packages/cx/src/widgets/HtmlElement.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -import { Cx } from '../ui/Cx'; -import { VDOM } from '../ui/Widget'; -import { HtmlElement } from './HtmlElement'; -import { Store } from '../data/Store'; - -import renderer from 'react-test-renderer'; -import assert from 'assert'; - -describe('HtmlElement', () => { - - it('renders textual content provided through the text property', () => { - - let widget = -
- ; - - let store = new Store({ - data: { - text: 'Test' - } - }); - - const component = renderer.create( - - ); - - let tree = component.toJSON(); - assert.equal(tree.type, 'div'); - assert.deepEqual(tree.children, ['Test']); - }); - - it('allows spread bindings', () => { - - let store = new Store({ - data: { - title: 'title' - } - }); - - const component = renderer.create( - - Link - - ); - - let tree = component.toJSON(); - assert.deepEqual(tree, { - type: 'a', - children: ['Link'], - props: { - href: '#', - title: 'title' - } - }) - }); -}); - diff --git a/packages/cx/src/widgets/HtmlElement.spec.tsx b/packages/cx/src/widgets/HtmlElement.spec.tsx new file mode 100644 index 000000000..375b65c7c --- /dev/null +++ b/packages/cx/src/widgets/HtmlElement.spec.tsx @@ -0,0 +1,89 @@ +import { Store } from "../data/Store"; +import assert from "assert"; +import { createTestRenderer } from "../util/test/createTestRenderer"; +import { bind } from "../ui/bind"; +import { VDOM } from "../ui/Widget"; + +describe("HtmlElement", () => { + it("renders textual content provided through the text property", async () => { + let widget = ( + +
+ + ); + + let store = new Store({ + data: { + text: "Test", + }, + }); + + const component = await createTestRenderer(store, widget); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.deepEqual(tree.children, ["Test"]); + }); + + it("allows spread bindings", async () => { + let store = new Store({ + data: { + title: "title", + }, + }); + + const component = await createTestRenderer( + store, + + + Link + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.deepEqual(tree, { + type: "a", + children: ["Link"], + props: { + href: "#", + title: "title", + }, + }); + }); + + it("supports SVG elements with camelCase attributes", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "svg"); + assert(tree.children && tree.children.length === 1, "Expected one child"); + let path = tree.children[0] as any; + assert.equal(path.type, "path"); + assert.equal(path.props.d, "M200,176V64a23.9,23.9,0,0,0-24-24H40"); + assert.equal(path.props.fill, "none"); + assert.equal(path.props.stroke, "#343434"); + assert.equal(path.props.strokeLinecap, "round"); + assert.equal(path.props.strokeLinejoin, "round"); + assert.equal(path.props.strokeWidth, "12"); + }); +}); diff --git a/packages/cx/src/widgets/HtmlElement.tsx b/packages/cx/src/widgets/HtmlElement.tsx new file mode 100644 index 000000000..59ef9e8a3 --- /dev/null +++ b/packages/cx/src/widgets/HtmlElement.tsx @@ -0,0 +1,407 @@ +/** @jsxImportSource react */ + +import type { JSX as ReactJSX } from "react"; +import { Url } from "../ui/app/Url"; +import { ChildNode, StyledContainerBase, StyledContainerConfig } from "../ui/Container"; +import type { RenderProps, WidgetData } from "../ui/Instance"; +import { Instance } from "../ui/Instance"; +import { BooleanProp, ClassProp, NumberProp, Prop, StringProp, StructuredProp } from "../ui/Prop"; +import type { CxChild, RenderingContext } from "../ui/RenderingContext"; +import { VDOM, Widget } from "../ui/Widget"; +import { debug } from "../util/Debug"; +import { isArray } from "../util/isArray"; +import { isDefined } from "../util/isDefined"; +import { isString } from "../util/isString"; +import { isUndefined } from "../util/isUndefined"; +import { autoFocus } from "./autoFocus"; +import type { TooltipInstance } from "./overlay/Tooltip"; +import type { TooltipConfig, TooltipProp } from "./overlay/tooltip-ops"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentDidUpdate, + TooltipParentInstance, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, +} from "./overlay/tooltip-ops"; + +const isDataAttribute = (attr: string): string | false => (attr.indexOf("data-") === 0 ? attr.substring(5) : false); + +export let urlAttributes: Record = { + "a.href": true, + "img.src": true, + "iframe.src": true, +}; + +type ReactIntrinsicElements = ReactJSX.IntrinsicElements; + +// Check if a key is an event handler (starts with "on" and is a function) +// Use NonNullable to handle optional event handlers (T | undefined) +type IsEventHandler = K extends `on${string}` ? (NonNullable extends Function ? true : false) : false; + +// CxJS event handler type - can be string (controller method) or callback with Instance +type CxEventHandler = T extends (event: infer E) => any + ? string | ((event: E, instance: Instance) => void) + : T extends undefined + ? undefined + : string | T; + +// Transform React element props to CxJS props +// Note: For string literal union props (like SVG's strokeLinecap), we also accept `string` +// because TypeScript widens string literals to `string` in JSX attribute syntax. +// This is a known TypeScript behavior where `` infers "round" as string. +type TransformHtmlElementProps = { + [K in keyof T]: K extends "children" + ? ChildNode | ChildNode[] + : K extends "className" | "class" + ? ClassProp + : IsEventHandler extends true + ? CxEventHandler + : string extends T[K] + ? Prop // Plain string props - no change needed + : NonNullable extends string + ? Prop | string // String literal unions - accept string for JSX compatibility + : Prop; +}; + +/** Base HtmlElement configuration - core CxJS properties for extension by widgets */ +export interface HtmlElementConfigBase extends StyledContainerConfig { + id?: StringProp | NumberProp; + + /** HTML tag name */ + tag?: string; + + /** Inner text contents. */ + text?: StringProp | NumberProp; + + /** Inner html contents. */ + innerHtml?: StringProp; + + /** Inner html contents. */ + html?: StringProp; + + /** Tooltip configuration. */ + tooltip?: StringProp | TooltipConfig; + + /** Additional attributes to be applied. */ + attrs?: StructuredProp; + + /** Additional data attributes. */ + data?: StructuredProp; + + //** Set to true to automatically focus the element when mounted. */ + autoFocus?: BooleanProp; + + //** Callback to receive the HTMLElement where this component is mounted. */ + onRef?: string | ((element: HTMLElement | null, instance: Instance) => void); +} + +/** HtmlElement configuration with tag-specific attributes and events */ +export type HtmlElementConfig = Omit & + TransformHtmlElementProps & { tag?: Tag }; + +export class HtmlElementInstance = HtmlElement> + extends Instance + implements TooltipParentInstance +{ + events?: Record any>; + declare tooltips: { [key: string]: TooltipInstance }; +} + +export class HtmlElement< + Config extends HtmlElementConfigBase = HtmlElementConfig, + InstanceType extends HtmlElementInstance = HtmlElementInstance, +> extends StyledContainerBase { + declare public tag?: string; + declare public html?: string; + declare public innerText?: string; + declare public text?: string; + declare public innerHtml?: string; + declare public attrs?: Record; + declare public data?: Record; + declare public events?: Record unknown>; + declare public urlAttributes?: string[]; + declare public extraProps?: Record; + declare public tooltip?: TooltipProp; + declare public onRef?: string | ((element: HTMLElement | null, instance: Instance) => void); + declare public autoFocus?: boolean | string; + [key: string]: unknown; // Index signature for dynamic properties + + constructor(config?: Config) { + super(config); + + if (isUndefined(this.jsxAttributes) && config) + this.jsxAttributes = Object.keys(config).filter(this.isValidHtmlAttribute.bind(this)); + } + + declareData(...args: Record[]): void { + const data: Record = { + text: undefined, + innerHtml: undefined, + attrs: { + structured: true, + }, + data: { + structured: true, + }, + autoFocus: undefined, + }; + + let name: string | false; + + this.urlAttributes = []; + + if (this.jsxAttributes) { + this.jsxAttributes.forEach((attr) => { + if (urlAttributes[`${this.tag}.${attr}`]) this.urlAttributes!.push(attr); + + if ((name = isDataAttribute(attr))) { + if (!this.data) this.data = {}; + this.data[name] = this[attr]; + } else if ((name = this.isValidHtmlAttribute(attr)) && !data.hasOwnProperty(name)) { + if (name.indexOf("on") === 0) { + if (this[attr]) { + if (!this.events) this.events = {}; + this.events[name] = this[attr] as (e: Event, instance: Instance) => unknown; + } + } else { + if (!this.attrs) this.attrs = {}; + this.attrs[name] = this[attr]; + } + } + }); + } + + if (this.urlAttributes.length === 0) delete this.urlAttributes; + + // Combine args array with data object for super call + super.declareData(...args, data); + } + + isValidHtmlAttribute(attrName: string): string | false { + switch (attrName) { + case "tag": + case "type": + case "$type": + case "$props": + case "text": + case "layout": + case "class": + case "className": + case "style": + case "controller": + case "outerLayout": + case "items": + case "children": + case "visible": + case "if": + case "mod": + case "putInto": + case "contentFor": + case "trimWhitespace": + case "preserveWhitespace": + case "ws": + case "plainText": + case "vertical": + case "memoize": + case "onInit": + case "onExplore": + case "onDestroy": + case "onRef": + case "html": + case "innerText": + case "baseClass": + case "CSS": + case "tooltip": + case "styles": + case "jsxAttributes": + case "jsxSpread": + case "instance": + case "store": + case "autoFocus": + case "vdomKey": + return false; + + default: + if (isDataAttribute(attrName)) return false; + break; + } + + return attrName; + } + + init(): void { + if (this.html) this.innerHtml = this.html; + + if (this.innerText) this.text = this.innerText; + + super.init(); + } + + prepareData(context: RenderingContext, instance: InstanceType): void { + const { data } = instance; + if (this.urlAttributes && data.attrs) { + data.attrs = { ...data.attrs }; + this.urlAttributes.forEach((attr: string) => { + const attrValue = (data.attrs as Record)[attr]; + if (isString(attrValue)) { + (data.attrs as Record)[attr] = Url.resolve(attrValue); + } + }); + } + super.prepareData(context, instance); + } + + attachProps(context: RenderingContext, instance: InstanceType, props: RenderProps): void { + Object.assign(props, this.extraProps); + + if (!isString(this.tag)) props.instance = instance; + } + + render(context: RenderingContext, instance: InstanceType, key: string): React.ReactNode { + //rebind events to pass instance + if (this.events && !instance.events) { + instance.events = {}; + for (const eventName in this.events) { + const handler = this.events[eventName]; + instance.events[eventName] = (e: React.SyntheticEvent) => instance.invoke(eventName, e, instance); + } + } + + const { data, events } = instance; + + const props: RenderProps = Object.assign( + { + key: key, + }, + data.attrs, + events, + ); + + if (data.classNames) props.className = data.classNames as string; + + if (data.style) props.style = data.style as Record; + + let children: CxChild; + if (isDefined(data.text)) children = data.text; + else if (isString(data.innerHtml)) { + props.dangerouslySetInnerHTML = { __html: data.innerHtml }; + } else { + children = this.renderChildren(context, instance); + if (children && isArray(children) && children.length === 0) children = undefined; + } + + props.children = children; + + this.attachProps(context, instance, props); + + if (this.tooltip || this.onRef || this.autoFocus) + return ( + + {props.children as React.ReactNode} + + ); + + return VDOM.createElement(this.tag!, props, props.children as React.ReactNode); + } +} + +HtmlElement.prototype.tag = "div"; + +interface ContainerComponentProps { + tag: string | React.ComponentType; + props: RenderProps; + children: React.ReactNode; + instance: HtmlElementInstance; + data: WidgetData; + key: string; +} + +class ContainerComponent extends VDOM.Component { + el: HTMLElement | null = null; + declare ref: (c: HTMLElement | null) => void; + + constructor(props: ContainerComponentProps) { + super(props); + this.ref = (c: HTMLElement | null) => { + this.el = c; + const { instance } = this.props; + const widget = instance.widget as HtmlElement; + if (widget.onRef) { + instance.invoke("onRef", c, instance); + } + }; + } + + render(): React.ReactNode { + const { tag, props, children, instance } = this.props; + const widget = instance.widget as HtmlElement; + + props.ref = this.ref; + + if (widget.tooltip) { + const { onMouseLeave, onMouseMove } = props; + + props.onMouseLeave = (e: React.MouseEvent) => { + tooltipMouseLeave(e, instance, widget.tooltip!); + if (onMouseLeave) onMouseLeave(e); + }; + props.onMouseMove = (e: React.MouseEvent) => { + tooltipMouseMove(e, instance, widget.tooltip!); + if (onMouseMove) onMouseMove(e); + }; + } + + return VDOM.createElement(tag, props, children); + } + + componentWillUnmount(): void { + tooltipParentWillUnmount(this.props.instance); + } + + UNSAFE_componentWillReceiveProps(props: ContainerComponentProps): void { + const widget = this.props.instance.widget as HtmlElement; + if (this.el && widget.tooltip) { + tooltipParentWillReceiveProps(this.el, props.instance, widget.tooltip); + } + } + + componentDidMount(): void { + const widget = this.props.instance.widget as HtmlElement; + if (this.el && widget.tooltip) { + tooltipParentDidMount(this.el, this.props.instance, widget.tooltip); + } + autoFocus(this.el, this); + } + + componentDidUpdate(): void { + const widget = this.props.instance.widget as HtmlElement; + if (this.el && widget.tooltip) { + tooltipParentDidUpdate(this.el, this.props.instance, widget.tooltip); + } + autoFocus(this.el, this); + } +} + +const originalWidgetFactory = Widget.factory; + +//support for React components +Widget.factory = function ( + type: string | React.ComponentType | undefined, + config?: Record, + more?: Record, +) { + const typeType = typeof type; + + if (typeType === "undefined") { + debug("Creating a widget of unknown type.", config, more); + return new HtmlElement(Object.assign({}, config, more)); + } + + if (typeType === "function") return HtmlElement.create(HtmlElement, { tag: type }, config); + + return originalWidgetFactory.call(Widget, type as string, config, more); +}; + +Widget.alias("html-element", HtmlElement); diff --git a/packages/cx/src/widgets/Icon.d.ts b/packages/cx/src/widgets/Icon.d.ts deleted file mode 100644 index d12d7a5e7..000000000 --- a/packages/cx/src/widgets/Icon.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as Cx from "../core"; - -interface IconProps extends Cx.WidgetProps { - /** Name under which the icon is registered. */ - name?: Cx.StringProp; - - /** Additional CSS classes to be applied to the field. - * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. */ - className?: Cx.ClassProp; - - /** Additional CSS classes to be applied to the field. - * If an object is provided, all keys with a "truthy" value will be added to the CSS class list. */ - class?: Cx.ClassProp; - - /** Style object applied to the wrapper div. Used for setting the dimensions of the field. */ - style?: Cx.StyleProp; - - /** Base CSS class to be applied to the element. Default is `icon`. */ - baseClass?: string; -} - -export class Icon extends Cx.Widget { - static restoreDefaultIcons(); - - static clear(); - - static register(name: string, icon: any, defaultIcon?: boolean); - - static unregister(...args: string[]); - - static registerFactory(factory: (name: string, props: { [key: string]: any }) => any); - - static render(name: string, props: { [key: string]: any }); -} diff --git a/packages/cx/src/widgets/Icon.js b/packages/cx/src/widgets/Icon.js deleted file mode 100644 index 445b6ee6e..000000000 --- a/packages/cx/src/widgets/Icon.js +++ /dev/null @@ -1,50 +0,0 @@ -import { Widget, VDOM } from '../ui/Widget'; -import { registerIcon, registerIconFactory, clearIcons, unregisterIcon, renderIcon, restoreDefaultIcons } from './icons/registry'; -import "./icons/index"; - - -export class Icon extends Widget { - declareData() { - super.declareData(...arguments, { - name: undefined - }) - } - - render(context, instance, key) { - let {data} = instance; - return renderIcon(data.name, { - key: key, - className: data.classNames, - style: data.style - }); - } - - static register(name, icon, defaultIcon = false) { - return registerIcon(name, icon, defaultIcon); - } - - static unregister(...args) { - return unregisterIcon(...args); - } - - static render(name, props) { - return renderIcon(name, props); - } - - static clear() { - return clearIcons(); - } - - static registerFactory(factory) { - return registerIconFactory(factory); - } - - static restoreDefaultIcons() { - restoreDefaultIcons(); - } -} - -Icon.prototype.baseClass = "icon"; -Icon.prototype.styled = true; - -Widget.alias('icon', Icon); diff --git a/packages/cx/src/widgets/Icon.scss b/packages/cx/src/widgets/Icon.scss index 08fab7aa2..bc5b61763 100644 --- a/packages/cx/src/widgets/Icon.scss +++ b/packages/cx/src/widgets/Icon.scss @@ -1,8 +1,10 @@ +@use "sass:map"; + @mixin cx-icon($name: "icon", $size: $cx-default-icon-size, $besm: $cx-besm) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { width: $size; diff --git a/packages/cx/src/widgets/Icon.ts b/packages/cx/src/widgets/Icon.ts new file mode 100644 index 000000000..a37175533 --- /dev/null +++ b/packages/cx/src/widgets/Icon.ts @@ -0,0 +1,64 @@ +import { Widget, VDOM, WidgetConfig, WidgetStyleConfig } from "../ui/Widget"; +import { + registerIcon, + registerIconFactory, + clearIcons, + unregisterIcon, + renderIcon, + restoreDefaultIcons, +} from "./icons/registry"; +import "./icons/index"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Instance } from "../ui/Instance"; +import { StringProp } from "../ui/Prop"; + +export interface IconConfig extends WidgetConfig, WidgetStyleConfig { + /** Name under which the icon is registered. */ + name?: StringProp; +} + +export class Icon extends Widget { + declareData(...args: Record[]): void { + super.declareData(...args, { + name: undefined, + }); + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + let { data } = instance; + return renderIcon(data.name, { + key: key, + className: data.classNames, + style: data.style, + }); + } + + static register(name: string, icon: any, defaultIcon: boolean = false) { + return registerIcon(name, icon, defaultIcon); + } + + static unregister(...args: string[]) { + return unregisterIcon(...args); + } + + static render(name: string, props: Record): React.ReactNode { + return renderIcon(name, props); + } + + static clear() { + return clearIcons(); + } + + static registerFactory(factory: (name: string, props: Record) => any) { + return registerIconFactory(factory); + } + + static restoreDefaultIcons() { + restoreDefaultIcons(); + } +} + +Icon.prototype.baseClass = "icon"; +Icon.prototype.styled = true; + +Widget.alias('icon', Icon); diff --git a/packages/cx/src/widgets/List.d.ts b/packages/cx/src/widgets/List.d.ts deleted file mode 100644 index 4ffb5924b..000000000 --- a/packages/cx/src/widgets/List.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -import * as React from "react"; -import { - BooleanProp, - ClassProp, - CollatorOptions, - Config, - Prop, - RecordAlias, - SortersProp, - StringProp, - StructuredProp, - StyledContainerProps, - StyleProp, - Widget, -} from "../core"; -import { Instance } from "./../ui/Instance"; - -type KeyDownPipe = (event: KeyboardEvent) => void; - -type PipeKeyDownCallback = (pipe: KeyDownPipe) => void; - -interface ListProps extends StyledContainerProps { - /** An array of records to be displayed in the list. */ - records?: Prop; - - /** Used for sorting the list. */ - sorters?: SortersProp; - - /** A binding used to store the name of the field used for sorting the collection. Available only if `sorters` are not used. */ - sortField?: StringProp; - - /** A binding used to store the sort direction. Available only if `sorters` are not used. Possible values are `"ASC"` and `"DESC"`. Defaults to `"ASC"`. */ - sortDirection?: StringProp; - - /** CSS style that will be applied to all list items. */ - itemStyle?: StyleProp; - - /** CSS class that will be applied to all list items. */ - itemClass?: ClassProp; - - /** CSS class that will be applied to all list items. */ - itemClassName?: ClassProp; - - emptyText?: StringProp; - - /** Grouping configuration. */ - grouping?: Config; - - recordName?: RecordAlias; - indexName?: RecordAlias; - - /** Base CSS class to be applied to the element. Defaults to 'list'. */ - baseClass?: string; - - focusable?: boolean; - focused?: boolean; - itemPad?: boolean; - cached?: boolean; - - /** Selection configuration. */ - selection?: Config; - - /** Parameters that affect filtering */ - filterParams?: StructuredProp; - - /** Callback to create a filter function for given filter params. */ - onCreateFilter?: (filterParams: any, instance: Instance) => (record: T) => boolean; - - /** Scrolls selection into the view. Default value is false. */ - scrollSelectionIntoView?: boolean; - - /** Options for data sorting. See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator */ - sortOptions?: CollatorOptions; - - /** Parameter used for disabling specific items in the list. */ - itemDisabled?: BooleanProp; - - /** Lists in this mode perform selection automatically without offering cursor navigation. */ - selectMode?: boolean; - - /** If this field is set to true pressing the Tab key will select the item under cursor. */ - selectOnTab?: boolean; - - /** Callback to be invoked when the list is being scrolled. Useful for loading additional items. */ - onScroll?: (event: Event, instance: Instance) => void; - - onItemClick?: string | ((e: React.SyntheticEvent, instance: Instance) => void); - - onItemDoubleClick?: string | ((e: React.SyntheticEvent, instance: Instance) => void); - - pipeKeyDown?: string | PipeKeyDownCallback; - - keyField?: string; -} - -export class List extends Widget> {} diff --git a/packages/cx/src/widgets/List.js b/packages/cx/src/widgets/List.js deleted file mode 100644 index 1573c1d66..000000000 --- a/packages/cx/src/widgets/List.js +++ /dev/null @@ -1,594 +0,0 @@ -import { Widget, VDOM, getContent } from "../ui/Widget"; -import { PureContainer } from "../ui/PureContainer"; -import { GroupAdapter } from "../ui/adapter/GroupAdapter"; -import { Binding, isBinding } from "../data/Binding"; -import { Selection } from "../ui/selection/Selection"; -import { KeyCode } from "../util/KeyCode"; -import { scrollElementIntoView } from "../util/scrollElementIntoView"; -import { FocusManager, oneFocusOut, offFocusOut, preventFocusOnTouch } from "../ui/FocusManager"; -import { isString } from "../util/isString"; -import { isArray } from "../util/isArray"; -import { getAccessor } from "../data/getAccessor"; -import { batchUpdates } from "../ui/batchUpdates"; -import { Container } from "../ui/Container"; -import { addEventListenerWithOptions } from "../util/addEventListenerWithOptions"; - -/* - - renders list of items - - focusable (keyboard navigation) - - selection - - fake focus - list appears focused and receives keyboard inputs redirected from other control (dropdown scenario) - */ - -export class List extends Widget { - init() { - if (this.recordAlias) this.recordName = this.recordAlias; - - if (this.indexAlias) this.indexName = this.indexAlias; - - this.adapter = GroupAdapter.create(this.adapter || GroupAdapter, { - recordName: this.recordName, - indexName: this.indexName, - recordsAccessor: getAccessor(this.records), - keyField: this.keyField, - sortOptions: this.sortOptions, - }); - - this.child = ListItem.create({ - layout: this.layout, - items: this.items, - children: this.children, - styled: true, - class: this.itemClass, - className: this.itemClassName, - style: this.itemStyle, - disabled: this.itemDisabled, - ...this.item, - }); - - delete this.children; - - this.selection = Selection.create(this.selection, { - records: this.records, - }); - - super.init(); - - if (this.grouping) { - this.groupBy(this.grouping); - } - } - - initInstance(context, instance) { - this.adapter.initInstance(context, instance); - } - - declareData() { - let selection = this.selection.configureWidget(this); - - super.declareData( - selection, - { - records: undefined, - sorters: undefined, - sortField: undefined, - sortDirection: undefined, - filterParams: { - structured: true, - }, - itemStyle: { - structured: true, - }, - emptyText: undefined, - tabIndex: undefined, - }, - ...arguments, - ); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.sortField) - data.sorters = [ - { - field: data.sortField, - direction: data.sortDirection || "ASC", - }, - ]; - this.adapter.sort(data.sorters); - - let filter = null; - if (this.onCreateFilter) filter = instance.invoke("onCreateFilter", data.filterParams, instance); - else if (this.filter) filter = (item) => this.filter(item, data.filterParams); - this.adapter.setFilter(filter); - instance.mappedRecords = this.adapter.getRecords(context, instance, data.records, instance.store); - - data.stateMods = Object.assign(data.stateMods || {}, { - selectable: !this.selection.isDummy || this.onItemClick, - empty: instance.mappedRecords.length == 0, - }); - - super.prepareData(context, instance); - } - - applyParentStore(instance) { - super.applyParentStore(instance); - - // force prepareData to execute again and propagate the store change to the records - if (instance.cached) delete instance.cached.rawData; - } - - explore(context, instance, data) { - let instances = []; - let isSelected = this.selection.getIsSelectedDelegate(instance.store); - instance.mappedRecords.forEach((record) => { - if (record.type == "data") { - let itemInstance = instance.getChild(context, this.child, record.key, record.store); - itemInstance.record = record; - itemInstance.selected = isSelected(record.data, record.index); - - let changed = false; - if (itemInstance.cache("recordData", record.data)) changed = true; - if (itemInstance.cache("selected", itemInstance.selected)) changed = true; - - if (this.cached && !changed && itemInstance.visible && !itemInstance.childStateDirty) { - instances.push(itemInstance); - itemInstance.shouldUpdate = false; - } else if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); - } else if (record.type == "group-header" && record.grouping.header) { - let itemInstance = instance.getChild(context, record.grouping.header, record.key, record.store); - itemInstance.record = record; - if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); - } else if (record.type == "group-footer" && record.grouping.footer) { - let itemInstance = instance.getChild(context, record.grouping.footer, record.key, record.store); - itemInstance.record = record; - if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); - } - }); - instance.instances = instances; - } - - render(context, instance, key) { - let items = instance.instances.map((x, i) => ({ - instance: x, - key: x.record.key, - type: x.record.type, - content: getContent(x.render(context)), - })); - return ( - - ); - } - - groupBy(grouping) { - if (!isArray(grouping)) { - if (isString(grouping) || typeof grouping == "object") return this.groupBy([grouping]); - throw new Error("DynamicGrouping should be an array of grouping objects"); - } - - grouping = grouping.map((g, i) => { - if (isString(g)) { - return { - key: { - [g]: { - bind: this.recordName + "." + g, - }, - }, - }; - } - return g; - }); - - grouping.forEach((g) => { - if (g.header) g.header = Widget.create(g.header); - - if (g.footer) g.footer = Widget.create(g.footer); - }); - - this.adapter.groupBy(grouping); - this.update(); - } -} - -List.prototype.recordName = "$record"; -List.prototype.indexName = "$index"; -List.prototype.baseClass = "list"; -List.prototype.focusable = true; -List.prototype.focused = false; -List.prototype.itemPad = true; -List.prototype.cached = false; -List.prototype.styled = true; -List.prototype.scrollSelectionIntoView = false; -List.prototype.selectMode = false; -List.prototype.selectOnTab = false; - -Widget.alias("list", List); - -class ListComponent extends VDOM.Component { - constructor(props) { - super(props); - let { widget } = props.instance; - let { focused } = widget; - this.state = { - cursor: focused && props.selectable ? 0 : -1, - focused: focused, - }; - - this.handleItemMouseDown = this.handleItemMouseDown.bind(this); - this.handleItemDoubleClick = this.handleItemDoubleClick.bind(this); - this.handleItemClick = this.handleItemClick.bind(this); - } - - shouldComponentUpdate(props, state) { - return props.instance.shouldUpdate || state != this.state; - } - - componentDidMount() { - let { instance } = this.props; - let { widget } = instance; - if (widget.pipeKeyDown) { - instance.invoke("pipeKeyDown", this.handleKeyDown.bind(this), instance); - this.showCursor(); - } - - if (widget.autoFocus) FocusManager.focus(this.el); - - if (widget.onScroll) { - this.unsubscribeScroll = addEventListenerWithOptions( - this.el, - "scroll", - (event) => { - instance.invoke("onScroll", event, instance); - }, - { passive: true }, - ); - } - - this.componentDidUpdate(); - } - - UNSAFE_componentWillReceiveProps(props) { - if (this.state.focused && props.instance.widget.selectMode) this.showCursor(true, props.items); - else if (this.state.cursor >= props.items.length) this.moveCursor(props.items.length - 1); - else if (this.state.focused && this.state.cursor < 0) this.moveCursor(0); - } - - componentWillUnmount() { - let { instance } = this.props; - let { widget } = instance; - offFocusOut(this); - if (widget.pipeKeyDown) instance.invoke("pipeKeyDown", null, instance); - } - - handleItemMouseDown(e) { - let index = Number(e.currentTarget.dataset.recordIndex); - this.moveCursor(index); - if (e.shiftKey) e.preventDefault(); - - this.moveCursor(index, { - select: true, - selectOptions: { - toggle: e.ctrlKey && !e.shiftKey, - add: e.ctrlKey && e.shiftKey, - }, - selectRange: e.shiftKey, - }); - } - - handleItemClick(e) { - let { instance, items } = this.props; - let index = Number(e.currentTarget.dataset.recordIndex); - let item = items[this.cursorChildIndex[index]]; - if (instance.invoke("onItemClick", e, item.instance) === false) return; - - this.moveCursor(index, { - select: true, - selectOptions: { - toggle: e.ctrlKey && !e.shiftKey, - add: e.ctrlKey && e.shiftKey, - }, - selectRange: e.shiftKey, - }); - } - - handleItemDoubleClick(e) { - let { instance, items } = this.props; - let index = Number(e.currentTarget.dataset.recordIndex); - let item = items[this.cursorChildIndex[index]]; - instance.invoke("onItemDoubleClick", e, item.instance); - } - - render() { - let { instance, items, selectable } = this.props; - let { data, widget } = instance; - let { CSS, baseClass } = widget; - let itemStyle = CSS.parseStyle(data.itemStyle); - this.cursorChildIndex = []; - let cursorIndex = 0; - - let onDblClick, onClick; - - if (widget.onItemClick) onClick = this.handleItemClick; - - if (widget.onItemDoubleClick) onDblClick = this.handleItemDoubleClick; - - let children = - items.length > 0 && - items.map((x, i) => { - let { data, selected } = x.instance; - let className; - - if (x.type == "data") { - let ind = cursorIndex++; - - this.cursorChildIndex.push(i); - className = CSS.element(baseClass, "item", { - selected: selected, - cursor: ind == this.state.cursor, - pad: widget.itemPad, - disabled: data.disabled, - }); - - return ( -
  • - {x.content} -
  • - ); - } else { - return ( -
  • - {x.content} -
  • - ); - } - }); - - if (!children && data.emptyText) { - children =
  • {data.emptyText}
  • ; - } - - return ( -
      { - this.el = el; - }} - className={CSS.expand(data.classNames, CSS.state({ focused: this.state.focused }))} - style={data.style} - tabIndex={widget.focusable && selectable && items.length > 0 ? data.tabIndex || 0 : null} - onMouseDown={preventFocusOnTouch} - onKeyDown={this.handleKeyDown.bind(this)} - onMouseLeave={this.handleMouseLeave.bind(this)} - onFocus={this.onFocus.bind(this)} - onBlur={this.onBlur.bind(this)} - > - {children} -
    - ); - } - - componentDidUpdate() { - let { widget } = this.props.instance; - if (widget.scrollSelectionIntoView) { - //The timeout is reqired for use-cases when parent needs to do some measuring that affect scrollbars, i.e. LookupField. - setTimeout(() => this.scrollElementIntoView(), 0); - } - } - - scrollElementIntoView() { - if (!this.el) return; //unmount - let { widget } = this.props.instance; - let { CSS, baseClass } = widget; - let selectedRowSelector = `.${CSS.element(baseClass, "item")}.${CSS.state("selected")}`; - let firstSelectedRow = this.el.querySelector(selectedRowSelector); - if (firstSelectedRow != this.selectedEl) { - if (firstSelectedRow) scrollElementIntoView(firstSelectedRow, true, false, 0, this.el); - this.selectedEl = firstSelectedRow; - } - } - - moveCursor(index, { focused, hover, scrollIntoView, select, selectRange, selectOptions } = {}) { - let { instance, selectable } = this.props; - if (!selectable) return; - - let { widget } = instance; - let newState = {}; - if (widget.focused) focused = true; - - if (focused != null && this.state.focused != focused) newState.focused = focused; - - //ignore mouse enter/leave events (support with a flag if a feature request comes) - if (!hover) newState.cursor = index; - - //batch updates to avoid flickering between selection and cursor changes - batchUpdates(() => { - if (select || widget.selectMode) { - let start = selectRange && this.state.selectionStart >= 0 ? this.state.selectionStart : index; - if (start < 0) start = index; - this.selectRange(start, index, selectOptions); - if (!selectRange) newState.selectionStart = index; - } - if (Object.keys(newState).length > 0) { - this.setState(newState, () => { - if (scrollIntoView) { - let item = this.el.children[this.cursorChildIndex[index]]; - if (item) scrollElementIntoView(item); - } - }); - } - }); - } - - selectRange(from, to, options) { - let { instance, items } = this.props; - let { widget } = instance; - - if (from > to) { - let tmp = from; - from = to; - to = tmp; - } - - let selection = [], - indexes = []; - - for (let cursor = from; cursor <= to; cursor++) { - let item = items[this.cursorChildIndex[cursor]]; - if (item) { - let { record, data } = item.instance; - if (data.disabled) continue; - selection.push(record.data); - indexes.push(record.index); - } - } - - widget.selection.selectMultiple(instance.store, selection, indexes, options); - } - - showCursor(force, newItems) { - if (!force && this.state.cursor >= 0) return; - - let items = newItems || this.props.items; - let index = -1, - firstSelected = -1, - firstValid = -1; - for (let i = 0; i < items.length; i++) { - let item = items[i]; - if (isDataItem(item)) { - index++; - - if (!isItemDisabled(item) && firstValid == -1) firstValid = index; - if (item.instance.selected) { - firstSelected = index; - break; - } - } - } - this.moveCursor(firstSelected != -1 ? firstSelected : firstValid, { - focusedport: true, - }); - } - - onFocus() { - let { widget } = this.props.instance; - - FocusManager.nudge(); - this.showCursor(widget.selectMode); - - if (!widget.focused) - oneFocusOut(this, this.el, () => { - this.moveCursor(-1, { focused: false }); - }); - - this.setState({ - focused: true, - }); - } - - onBlur() { - FocusManager.nudge(); - } - - handleMouseLeave() { - let { widget } = this.props.instance; - if (!widget.focused) this.moveCursor(-1, { hover: true }); - } - - handleKeyDown(e) { - let { instance, items } = this.props; - let { widget } = instance; - - if (this.onKeyDown && instance.invoke("onKeyDown", e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.tab: - case KeyCode.enter: - if (!widget.selectOnTab && e.keyCode == KeyCode.tab) break; - let item = items[this.cursorChildIndex[this.state.cursor]]; - if (item && widget.onItemClick && instance.invoke("onItemClick", e, item.instance) === false) return; - this.moveCursor(this.state.cursor, { - select: true, - selectOptions: { - toggle: e.ctrlKey && !e.shiftKey, - add: e.ctrlKey && e.shiftKey, - }, - selectRange: e.shiftKey, - }); - break; - - case KeyCode.down: - for (let index = this.state.cursor + 1; index < this.cursorChildIndex.length; index++) { - let item = items[this.cursorChildIndex[index]]; - if (!isItemSelectable(item)) continue; - this.moveCursor(index, { - focused: true, - scrollIntoView: true, - select: e.shiftKey, - selectRange: e.shiftKey, - }); - e.stopPropagation(); - e.preventDefault(); - break; - } - break; - - case KeyCode.up: - for (let index = this.state.cursor - 1; index >= 0; index--) { - let item = items[this.cursorChildIndex[index]]; - if (!isItemSelectable(item)) continue; - this.moveCursor(index, { - focused: true, - scrollIntoView: true, - select: e.shiftKey, - selectRange: e.shiftKey, - }); - e.stopPropagation(); - e.preventDefault(); - break; - } - break; - - case KeyCode.a: - if (!e.ctrlKey || !widget.selection.multiple) return; - - this.selectRange(0, this.cursorChildIndex.length); - - e.stopPropagation(); - e.preventDefault(); - break; - } - } -} - -class ListItem extends Container { - declareData(...args) { - super.declareData(...args, { - disabled: undefined, - }); - } -} - -function isItemSelectable(item) { - return isDataItem(item) && !isItemDisabled(item); -} - -function isDataItem(item) { - return item?.type == "data"; -} - -function isItemDisabled(item) { - return item?.instance.data.disabled; -} diff --git a/packages/cx/src/widgets/List.scss b/packages/cx/src/widgets/List.scss index 3cd6debab..9087e5e02 100644 --- a/packages/cx/src/widgets/List.scss +++ b/packages/cx/src/widgets/List.scss @@ -1,8 +1,10 @@ +@use "sass:map"; + @mixin cx-list($name: "list", $besm: $cx-besm) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { list-style: none; diff --git a/packages/cx/src/widgets/List.tsx b/packages/cx/src/widgets/List.tsx new file mode 100644 index 000000000..b901dc312 --- /dev/null +++ b/packages/cx/src/widgets/List.tsx @@ -0,0 +1,793 @@ +/**@jsxImportSource react */ +import { Instance } from "../ui/Instance"; +import type { RenderingContext } from "../ui/RenderingContext"; +import { getAccessor } from "../data/getAccessor"; +import { GroupAdapter, GroupingConfig } from "../ui/adapter/GroupAdapter"; +import { batchUpdates } from "../ui/batchUpdates"; +import { Container, StyledContainerBase, StyledContainerConfig } from "../ui/Container"; +import { FocusManager, offFocusOut, oneFocusOut, preventFocusOnTouch } from "../ui/FocusManager"; +import { Selection } from "../ui/selection/Selection"; +import { VDOM, Widget, getContent } from "../ui/Widget"; +import { addEventListenerWithOptions } from "../util/addEventListenerWithOptions"; +import { isArray } from "../util/isArray"; +import { isString } from "../util/isString"; +import { KeyCode } from "../util/KeyCode"; +import { scrollElementIntoView } from "../util/scrollElementIntoView"; +import { + BooleanProp, + ClassProp, + CollatorOptions, + Prop, + RecordAlias, + StringProp, + StructuredProp, + StyleProp, +} from "../ui/Prop"; +import { Create } from "../util/Component"; + +export interface ListConfig extends StyledContainerConfig { + /** An array of records to be displayed in the list. */ + records?: Prop; + + /** Record alias. Default is `$record`. */ + recordName?: RecordAlias; + + /** Record alias. Alias for `recordName`. Default is `$record`. */ + recordAlias?: RecordAlias; + + /** Index alias. Default is `$index`. */ + indexName?: RecordAlias; + + /** Index alias. Alias for `indexName`. Default is `$index`. */ + indexAlias?: RecordAlias; + + /** Style to be applied to each list item. */ + itemStyle?: StyleProp; + + /** CSS class to be applied to each list item. */ + itemClass?: ClassProp; + + /** CSS class to be applied to each list item. */ + itemClassName?: ClassProp; + + /** Text to be displayed when the list is empty. */ + emptyText?: StringProp; + + /** A grouping definition, field name, or an array of grouping level definitions. */ + grouping?: string | GroupingConfig | (string | GroupingConfig)[]; + + /** Base CSS class to be applied to the element. Default is 'list'. */ + baseClass?: string; + + /** Set to `true` to allow focus on the list. */ + focusable?: BooleanProp; + + /** Boolean for focus state. */ + focused?: BooleanProp; + + /** Boolean for item padding. */ + itemPad?: BooleanProp; + + /** Set to `true` to enable row caching. */ + cached?: BooleanProp; + + /** Selection configuration. */ + selection?: StructuredProp; + + /** Parameters that affect filtering. */ + filterParams?: StructuredProp; + + /** Callback to create a filter function for given filter params. */ + onCreateFilter?: (filterParams: any, instance?: Instance) => (record: T) => boolean; + + /** Scroll selection into the view. Default value is false. */ + scrollSelectionIntoView?: BooleanProp; + + /** Options for data sorting. */ + sortOptions?: CollatorOptions; + + /** Parameter for disabling specific items. */ + itemDisabled?: BooleanProp; + + /** Automatic selection without cursor navigation. */ + selectMode?: BooleanProp; + + /** Tab key selects item under cursor. */ + selectOnTab?: BooleanProp; + + /** Callback invoked during scrolling. */ + onScroll?: string | ((event: Event, instance: Instance) => void); + + /** Callback for item click. */ + onItemClick?: string | ((e: React.MouseEvent, instance: Instance) => void); + + /** Callback for item double click. */ + onItemDoubleClick?: string | ((e: React.MouseEvent, instance: Instance) => void); + + /** Callback for keyboard down events. */ + pipeKeyDown?: string | ((handler: ((e: React.KeyboardEvent) => void) | null, instance: Instance) => void); + + /** A field used to get the unique identifier of the record. */ + keyField?: string; + + /** Data adapter used to convert data in list of records. Used to enable grouping and tree operations. */ + dataAdapter?: Create; +} + +/* + - renders list of items + - focusable (keyboard navigation) + - selection + - fake focus - list appears focused and receives keyboard inputs redirected from other control (dropdown scenario) + */ + +export class List extends StyledContainerBase { + constructor(config?: ListConfig) { + super(config); + } + + declare public recordAlias?: string; + declare public recordName: string; + declare public indexAlias?: string; + declare public indexName: string; + declare public adapter: GroupAdapter; + declare public child: ListItem; + declare public selection: Selection; + declare public itemClass?: string; + declare public itemClassName?: string; + declare public itemStyle?: string; + declare public itemDisabled?: boolean; + declare public item?: Widget; + declare public layout?: any; + declare public keyField?: string; + declare public records?: any[]; + declare public sortOptions?: any; + declare public grouping?: string | GroupingConfig | (string | GroupingConfig)[]; + declare public focusable?: boolean; + declare public focused?: boolean; + declare public itemPad?: boolean; + declare public cached?: boolean; + declare public scrollSelectionIntoView?: boolean; + declare public selectMode?: boolean; + declare public selectOnTab?: boolean; + declare public pipeKeyDown?: + | string + | ((handler: ((e: React.KeyboardEvent) => void) | null, instance: Instance) => void); + declare public autoFocus?: boolean; + declare public baseClass: string; + declare public filter?: (item: unknown, filterParams: Record) => boolean; + declare public onCreateFilter?: ( + filterParams: Record, + instance: Instance, + ) => (record: unknown) => boolean; + declare public onItemClick?: (e: React.MouseEvent, instance: Instance) => void; + declare public onItemDoubleClick?: (e: React.MouseEvent, instance: Instance) => void; + declare public onKeyDown?: (e: React.KeyboardEvent, instance: Instance) => void; + declare public onScroll?: (event: Event, instance: Instance) => void; + declare public onFocus?: (event: FocusEvent, instance: Instance) => void; + declare public onBlur?: (event: FocusEvent, instance: Instance) => void; + declare public onMouseLeave?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseEnter?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseMove?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseUp?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseDown?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseOver?: (event: React.MouseEvent, instance: Instance) => void; + declare public onMouseOut?: (event: React.MouseEvent, instance: Instance) => void; + + init() { + if (this.recordAlias) this.recordName = this.recordAlias; + + if (this.indexAlias) this.indexName = this.indexAlias; + + this.adapter = GroupAdapter.create(this.adapter || GroupAdapter, { + recordName: this.recordName, + indexName: this.indexName, + recordsAccessor: getAccessor(this.records), + keyField: this.keyField, + sortOptions: this.sortOptions, + }); + + this.child = ListItem.create({ + layout: this.layout, + items: this.items, + children: this.children, + styled: true, + class: this.itemClass, + className: this.itemClassName, + style: this.itemStyle, + disabled: this.itemDisabled, + ...this.item, + }) as ListItem; + + delete this.children; + + this.selection = Selection.create(this.selection, { + records: this.records, + }); + + super.init(); + + if (this.grouping) { + this.groupBy(this.grouping); + } + } + + initInstance(context: RenderingContext, instance: Instance) { + this.adapter.initInstance(context, instance); + } + + declareData() { + let selection = this.selection.configureWidget(this); + + super.declareData( + selection, + { + records: undefined, + sorters: undefined, + sortField: undefined, + sortDirection: undefined, + filterParams: { + structured: true, + }, + itemStyle: { + structured: true, + }, + emptyText: undefined, + tabIndex: undefined, + }, + ...arguments, + ); + } + + prepareData(context: RenderingContext, instance: Instance) { + let { data } = instance; + + if (data.sortField) + data.sorters = [ + { + field: data.sortField, + direction: data.sortDirection || "ASC", + }, + ]; + this.adapter.sort(data.sorters); + + let filter = null; + if (this.onCreateFilter) filter = instance.invoke("onCreateFilter", data.filterParams, instance); + else if (this.filter) filter = (item: unknown) => this.filter!(item, data.filterParams); + this.adapter.setFilter(filter); + instance.mappedRecords = this.adapter.getRecords(context, instance, data.records, instance.store); + + data.stateMods = Object.assign(data.stateMods || {}, { + selectable: !this.selection.isDummy || this.onItemClick, + empty: instance.mappedRecords.length == 0, + }); + + super.prepareData(context, instance); + } + + applyParentStore(instance: Instance) { + super.applyParentStore(instance); + + // force prepareData to execute again and propagate the store change to the records + if (instance.cached) delete instance.cached.rawData; + } + + explore(context: RenderingContext, instance: Instance): void { + let instances: Instance[] = []; + let isSelected = this.selection.getIsSelectedDelegate(instance.store); + instance.mappedRecords!.forEach((record) => { + if (record.type == "data") { + let itemInstance = instance.getChild(context, this.child, record.key, record.store); + itemInstance.record = record; + itemInstance.selected = isSelected(record.data, record.index); + + let changed = false; + if (itemInstance.cache("recordData", record.data)) changed = true; + if (itemInstance.cache("selected", itemInstance.selected)) changed = true; + + if (this.cached && !changed && itemInstance.visible && !itemInstance.childStateDirty) { + instances.push(itemInstance); + itemInstance.shouldUpdate = false; + } else if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); + } else if (record.type == "group-header" && record.grouping.header) { + let itemInstance = instance.getChild(context, record.grouping.header, record.key, record.store); + itemInstance.record = record; + if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); + } else if (record.type == "group-footer" && record.grouping.footer) { + let itemInstance = instance.getChild(context, record.grouping.footer, record.key, record.store); + itemInstance.record = record; + if (itemInstance.scheduleExploreIfVisible(context)) instances.push(itemInstance); + } + }); + instance.instances = instances; + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + let items = instance.instances!.map((x) => ({ + instance: x, + key: x.record.key, + type: x.record.type, + content: getContent(x.render(context)), + })); + return ( + + ); + } + + groupBy(grouping: string | GroupingConfig | (string | GroupingConfig)[]): void { + if (!isArray(grouping)) { + if (isString(grouping) || typeof grouping == "object") return this.groupBy([grouping]); + throw new Error("DynamicGrouping should be an array of grouping objects"); + } + + let normalized = grouping.map((g) => { + if (isString(g)) { + return { + key: { + [g]: { + bind: this.recordName + "." + g, + }, + }, + } as GroupingConfig; + } + return g; + }); + + normalized.forEach((g) => { + if (g.header) g.header = Widget.create(g.header); + + if (g.footer) g.footer = Widget.create(g.footer); + }); + + this.adapter.groupBy(normalized); + this.update(); + } +} + +List.prototype.recordName = "$record"; +List.prototype.indexName = "$index"; +List.prototype.baseClass = "list"; +List.prototype.focusable = true; +List.prototype.focused = false; +List.prototype.itemPad = true; +List.prototype.cached = false; +List.prototype.styled = true; +List.prototype.scrollSelectionIntoView = false; +List.prototype.selectMode = false; +List.prototype.selectOnTab = false; + +Widget.alias("list", List); + +interface ListItemData { + instance: Instance; + key: string; + type: string; + content: React.ReactNode; +} + +interface ListComponentProps { + instance: Instance; + items: ListItemData[]; + selectable: boolean | ((e: MouseEvent, instance: Instance) => void); +} + +interface ListComponentState { + cursor: number; + focused: boolean; + selectionStart?: number; +} + +class ListComponent extends VDOM.Component { + declare el?: HTMLUListElement; + cursorChildIndex: number[] = []; + declare selectedEl?: Element | null; + unsubscribeScroll?: () => void; + onKeyDown?: (e: React.KeyboardEvent, instance: Instance) => boolean | void; + + constructor(props: ListComponentProps) { + super(props); + let { widget } = props.instance; + let { focused } = widget as unknown as { focused: boolean }; + this.state = { + cursor: focused && props.selectable ? 0 : -1, + focused: focused, + }; + + this.handleItemMouseDown = this.handleItemMouseDown.bind(this); + this.handleItemDoubleClick = this.handleItemDoubleClick.bind(this); + this.handleItemClick = this.handleItemClick.bind(this); + } + + shouldComponentUpdate(props: ListComponentProps, state: ListComponentState): boolean { + return props.instance.shouldUpdate || state != this.state; + } + + componentDidMount(): void { + let { instance } = this.props; + let { widget } = instance as unknown as { widget: List }; + if (widget.pipeKeyDown) { + instance.invoke("pipeKeyDown", this.handleKeyDown.bind(this), instance); + this.showCursor(); + } + + if (widget.autoFocus && this.el) FocusManager.focus(this.el); + + if (widget.onScroll && this.el) { + this.unsubscribeScroll = addEventListenerWithOptions( + this.el, + "scroll", + (event) => { + instance.invoke("onScroll", event, instance); + }, + { passive: true }, + ); + } + + this.componentDidUpdate(); + } + + UNSAFE_componentWillReceiveProps(props: ListComponentProps): void { + if (this.state.focused && (props.instance.widget as List).selectMode) this.showCursor(true, props.items); + else if (this.state.cursor >= props.items.length) this.moveCursor(props.items.length - 1); + else if (this.state.focused && this.state.cursor < 0) this.moveCursor(0); + } + + componentWillUnmount(): void { + let { instance } = this.props; + let { widget } = instance as unknown as { widget: List }; + offFocusOut(this); + if (widget.pipeKeyDown) instance.invoke("pipeKeyDown", null, instance); + } + + handleItemMouseDown(e: React.MouseEvent): void { + let index = Number((e.currentTarget as HTMLElement).dataset.recordIndex); + this.moveCursor(index); + if (e.shiftKey) e.preventDefault(); + + this.moveCursor(index, { + select: true, + selectOptions: { + toggle: e.ctrlKey && !e.shiftKey, + add: e.ctrlKey && e.shiftKey, + }, + selectRange: e.shiftKey, + }); + } + + handleItemClick(e: React.MouseEvent): void { + let { instance, items } = this.props; + let index = Number((e.currentTarget as HTMLElement).dataset.recordIndex); + let item = items[this.cursorChildIndex[index]]; + if (instance.invoke("onItemClick", e, item.instance) === false) return; + + this.moveCursor(index, { + select: true, + selectOptions: { + toggle: e.ctrlKey && !e.shiftKey, + add: e.ctrlKey && e.shiftKey, + }, + selectRange: e.shiftKey, + }); + } + + handleItemDoubleClick(e: React.MouseEvent): void { + let { instance, items } = this.props; + let index = Number((e.currentTarget as HTMLElement).dataset.recordIndex); + let item = items[this.cursorChildIndex[index]]; + instance.invoke("onItemDoubleClick", e, item.instance); + } + + render(): React.ReactNode { + let { instance, items, selectable } = this.props; + let { data, widget } = instance as unknown as { data: Record; widget: List }; + let { CSS, baseClass } = widget; + let itemStyle = CSS.parseStyle(data.itemStyle); + this.cursorChildIndex = []; + let cursorIndex = 0; + + let onDblClick: ((e: React.MouseEvent) => void) | undefined; + let onClick: ((e: React.MouseEvent) => void) | undefined; + + if (widget.onItemClick) onClick = this.handleItemClick; + + if (widget.onItemDoubleClick) onDblClick = this.handleItemDoubleClick; + + let children: React.ReactNode = + items.length > 0 && + items.map((x, i) => { + let { data, selected } = x.instance as { data: any; selected?: boolean }; + let className; + + if (x.type == "data") { + let ind = cursorIndex++; + + this.cursorChildIndex.push(i); + className = CSS.element(baseClass, "item", { + selected: selected, + cursor: ind == this.state.cursor, + pad: widget.itemPad, + disabled: data.disabled, + }); + + return ( +
  • + {x.content} +
  • + ); + } else { + return ( +
  • + {x.content} +
  • + ); + } + }); + + if (!children && data.emptyText) { + children =
  • {data.emptyText}
  • ; + } + + return ( +
      { + this.el = el as HTMLUListElement; + }} + className={CSS.expand(data.classNames, CSS.state({ focused: this.state.focused }))} + style={data.style} + tabIndex={widget.focusable && selectable && items.length > 0 ? data.tabIndex || 0 : undefined} + onMouseDown={preventFocusOnTouch} + onKeyDown={this.handleKeyDown.bind(this)} + onMouseLeave={this.handleMouseLeave.bind(this)} + onFocus={this.onFocus.bind(this)} + onBlur={this.onBlur.bind(this)} + > + {children} +
    + ); + } + + componentDidUpdate(): void { + let { widget } = this.props.instance as unknown as { widget: List }; + if (widget.scrollSelectionIntoView) { + //The timeout is reqired for use-cases when parent needs to do some measuring that affect scrollbars, i.e. LookupField. + setTimeout(() => this.scrollElementIntoView(), 0); + } + } + + scrollElementIntoView(): void { + if (!this.el) return; //unmount + let { widget } = this.props.instance as unknown as { widget: List }; + let { CSS, baseClass } = widget; + let selectedRowSelector = `.${CSS.element(baseClass, "item")}.${CSS.state("selected")}`; + let firstSelectedRow = this.el.querySelector(selectedRowSelector); + if (firstSelectedRow != this.selectedEl) { + if (firstSelectedRow) scrollElementIntoView(firstSelectedRow, true, false, 0, this.el); + this.selectedEl = firstSelectedRow; + } + } + + moveCursor( + index: number, + { + focused, + hover, + scrollIntoView, + select, + selectRange, + selectOptions, + }: { + focused?: boolean; + hover?: boolean; + scrollIntoView?: boolean; + select?: boolean; + selectRange?: boolean; + selectOptions?: { toggle?: boolean; add?: boolean }; + } = {}, + ): void { + let { instance, selectable } = this.props; + if (!selectable) return; + + let { widget } = instance as unknown as { widget: List }; + let newState: Partial = {}; + if (widget.focused) focused = true; + + if (focused != null && this.state.focused != focused) newState.focused = focused; + + //ignore mouse enter/leave events (support with a flag if a feature request comes) + if (!hover) newState.cursor = index; + + //batch updates to avoid flickering between selection and cursor changes + batchUpdates(() => { + if (select || widget.selectMode) { + let start: number | undefined = + selectRange && this.state.selectionStart !== undefined && this.state.selectionStart >= 0 + ? this.state.selectionStart + : index; + if (start < 0) start = index; + this.selectRange(start, index, selectOptions); + if (!selectRange) newState.selectionStart = index; + } + if (Object.keys(newState).length > 0) { + this.setState(newState as ListComponentState, () => { + if (scrollIntoView && this.el) { + let item = this.el.children[this.cursorChildIndex[index]]; + if (item) scrollElementIntoView(item); + } + }); + } + }); + } + + selectRange(from: number, to: number, options?: { toggle?: boolean; add?: boolean }): void { + let { instance, items } = this.props; + let { widget } = instance as unknown as { widget: List }; + + if (from > to) { + let tmp = from; + from = to; + to = tmp; + } + + let selection: any[] = [], + indexes: number[] = []; + + for (let cursor = from; cursor <= to; cursor++) { + let item = items[this.cursorChildIndex[cursor]]; + if (item) { + let { record, data } = item.instance as { record: any; data: any }; + if (data.disabled) continue; + selection.push(record.data); + indexes.push(record.index); + } + } + + widget.selection.selectMultiple(instance.store, selection, indexes, options); + } + + showCursor(force?: boolean, newItems?: ListItemData[]): void { + if (!force && this.state.cursor >= 0) return; + + let items = newItems || this.props.items; + let index = -1, + firstSelected = -1, + firstValid = -1; + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (isDataItem(item)) { + index++; + + if (!isItemDisabled(item) && firstValid == -1) firstValid = index; + if ((item.instance as any).selected) { + firstSelected = index; + break; + } + } + } + this.moveCursor(firstSelected != -1 ? firstSelected : firstValid, { + focused: true, + }); + } + + onFocus(): void { + let { widget } = this.props.instance as unknown as { widget: List }; + + FocusManager.nudge(); + this.showCursor(widget.selectMode); + + if (!widget.focused && this.el) + oneFocusOut(this, this.el, () => { + this.moveCursor(-1, { focused: false }); + }); + + this.setState({ + focused: true, + }); + } + + onBlur(): void { + FocusManager.nudge(); + } + + handleMouseLeave(): void { + let { widget } = this.props.instance as unknown as { widget: List }; + if (!widget.focused) this.moveCursor(-1, { hover: true }); + } + + handleKeyDown(e: React.KeyboardEvent): void { + let { instance, items } = this.props; + let { widget } = instance as unknown as { widget: List }; + + if (this.onKeyDown && instance.invoke("onKeyDown", e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.tab: + case KeyCode.enter: + if (!widget.selectOnTab && e.keyCode == KeyCode.tab) break; + let item = items[this.cursorChildIndex[this.state.cursor]]; + if (item && widget.onItemClick && instance.invoke("onItemClick", e, item.instance) === false) return; + this.moveCursor(this.state.cursor, { + select: true, + selectOptions: { + toggle: e.ctrlKey && !e.shiftKey, + add: e.ctrlKey && e.shiftKey, + }, + selectRange: e.shiftKey, + }); + break; + + case KeyCode.down: + for (let index = this.state.cursor + 1; index < this.cursorChildIndex.length; index++) { + let item = items[this.cursorChildIndex[index]]; + if (!isItemSelectable(item)) continue; + this.moveCursor(index, { + focused: true, + scrollIntoView: true, + select: e.shiftKey, + selectRange: e.shiftKey, + }); + e.stopPropagation(); + e.preventDefault(); + break; + } + break; + + case KeyCode.up: + for (let index = this.state.cursor - 1; index >= 0; index--) { + let item = items[this.cursorChildIndex[index]]; + if (!isItemSelectable(item)) continue; + this.moveCursor(index, { + focused: true, + scrollIntoView: true, + select: e.shiftKey, + selectRange: e.shiftKey, + }); + e.stopPropagation(); + e.preventDefault(); + break; + } + break; + + case KeyCode.a: + if (!e.ctrlKey || !widget.selection.multiple) return; + + this.selectRange(0, this.cursorChildIndex.length); + + e.stopPropagation(); + e.preventDefault(); + break; + } + } +} + +class ListItem extends Container { + declareData(...args: Record[]) { + super.declareData(...args, { + disabled: undefined, + }); + } +} + +function isItemSelectable(item: any) { + return isDataItem(item) && !isItemDisabled(item); +} + +function isDataItem(item: any) { + return item?.type == "data"; +} + +function isItemDisabled(item: any) { + return item?.instance.data.disabled; +} diff --git a/packages/cx/src/widgets/ProgressBar.d.ts b/packages/cx/src/widgets/ProgressBar.d.ts deleted file mode 100644 index fb801a406..000000000 --- a/packages/cx/src/widgets/ProgressBar.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Cx from '../core'; -import {Instance} from '../ui/Instance'; - -interface ProgressBarProps extends Cx.StyledContainerProps { - - /** Progress value, a number between `0` and `1`. Default value is `0`. */ - value?: Cx.NumberProp, - - /** Defaults to `false`. Set to `true` to make it look disabled. */ - disabled?: Cx.BooleanProp, - - /** Progress bar annotation. */ - text?: Cx.StringProp, - -} - -export class ProgressBar extends Cx.Widget {} diff --git a/packages/cx/src/widgets/ProgressBar.js b/packages/cx/src/widgets/ProgressBar.js deleted file mode 100644 index 224a54b9b..000000000 --- a/packages/cx/src/widgets/ProgressBar.js +++ /dev/null @@ -1,46 +0,0 @@ -import {Widget, VDOM} from '../ui/Widget'; -import {parseStyle} from '../util/parseStyle'; -import {isNumber} from '../util/isNumber'; - -export class ProgressBar extends Widget { - - declareData() { - return super.declareData({ - disabled: undefined, - text: undefined, - value: undefined - }, ...arguments) - } - - render(context, instance, key) { - let { widget, data } = instance; - let { text, value, disabled } = data; - let {CSS, baseClass} = widget; - - if (!isNumber(value)) value = 0; - - return ( -
    -
    1 ? 1 : value < 0 ? 0 : value)*100}%` - }} - /> -
    - { text } -
    -
    - ) - } -} - -ProgressBar.prototype.styled = true; -ProgressBar.prototype.disabled = false; -ProgressBar.prototype.baseClass = 'progressbar'; diff --git a/packages/cx/src/widgets/ProgressBar.scss b/packages/cx/src/widgets/ProgressBar.scss index 8ba668102..0d196dc61 100644 --- a/packages/cx/src/widgets/ProgressBar.scss +++ b/packages/cx/src/widgets/ProgressBar.scss @@ -1,3 +1,4 @@ +@use "sass:map"; @mixin cx-progressbar( $name: 'progressbar', @@ -6,10 +7,10 @@ $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { box-sizing: border-box; diff --git a/packages/cx/src/widgets/ProgressBar.tsx b/packages/cx/src/widgets/ProgressBar.tsx new file mode 100644 index 000000000..c7289cd55 --- /dev/null +++ b/packages/cx/src/widgets/ProgressBar.tsx @@ -0,0 +1,66 @@ +/** @jsxImportSource react */ +import { Widget, VDOM, WidgetConfig, WidgetStyleConfig } from "../ui/Widget"; +import { parseStyle } from "../util/parseStyle"; +import { isNumber } from "../util/isNumber"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Instance } from "../ui/Instance"; +import { NumberProp, BooleanProp, StringProp } from "../ui/Prop"; + +export interface ProgressBarConfig extends WidgetConfig, WidgetStyleConfig { + /** Progress value, a number between `0` and `1`. Default value is `0`. */ + value?: NumberProp; + + /** Defaults to `false`. Set to `true` to make it look disabled. */ + disabled?: BooleanProp; + + /** Progress bar annotation. */ + text?: StringProp; +} + +export class ProgressBar extends Widget { + declare baseClass: string; + declare disabled?: BooleanProp; + + constructor(config?: ProgressBarConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + disabled: undefined, + text: undefined, + value: undefined, + }, + ...args, + ); + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + let { widget, data } = instance; + let { text, value, disabled } = data; + let { CSS, baseClass } = widget; + + if (!isNumber(value)) value = 0; + + return ( +
    +
    1 ? 1 : (value as number) < 0 ? 0 : (value as number)) * 100}%`, + }} + /> +
    {text}
    +
    + ); + } +} + +ProgressBar.prototype.styled = true; +ProgressBar.prototype.disabled = false; +ProgressBar.prototype.baseClass = "progressbar"; diff --git a/packages/cx/src/widgets/ReactElementWrapper.spec.tsx b/packages/cx/src/widgets/ReactElementWrapper.spec.tsx new file mode 100644 index 000000000..f0f6fd79f --- /dev/null +++ b/packages/cx/src/widgets/ReactElementWrapper.spec.tsx @@ -0,0 +1,452 @@ +import { Store } from "../data/Store"; +import assert from "assert"; +import { createTestRenderer, act } from "../util/test/createTestRenderer"; +import { VDOM } from "../ui/Widget"; +import { + ReactFunctionComponent, + ReactCounterComponent, + ReactClassComponent, + ReactPureComponent, + ReactRefEffectComponent, + ReactEffectStateComponent, + ReactPropsComponent, +} from "./HtmlElement.spec.helpers"; +import { Controller } from "../ui/Controller"; +import { createAccessorModelProxy } from "../data/createAccessorModelProxy"; +import { ReactElementWrapper } from "./ReactElementWrapper"; + +describe("ReactElementWrapper", () => { + it("renders React components as tag", async () => { + class MyReactComponent extends VDOM.Component { + render() { + return VDOM.createElement("div", { className: "my-component" }, this.props.children); + } + } + + let store = new Store(); + + const component = await createTestRenderer( + store, + + + Child content + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.equal(tree.props.className, "my-component"); + assert(tree.children && tree.children.length === 1, "Expected one child"); + }); + + it("renders React function components with props", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + Child content + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.equal(tree.props.className, "react-function-component"); + assert(tree.children && tree.children.length === 2, "Expected two children (h3 and div)"); + + let h3 = tree.children[0] as any; + assert.equal(h3.type, "h3"); + assert.deepEqual(h3.children, ["Test Title"]); + + let contentDiv = tree.children[1] as any; + assert.equal(contentDiv.type, "div"); + assert.equal(contentDiv.props.className, "content"); + }); + + it("renders React function components with hooks", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.equal(tree.props.className, "react-counter"); + assert(tree.children && tree.children.length === 2, "Expected two children (span and button)"); + + let span = tree.children[0] as any; + assert.equal(span.type, "span"); + assert.equal(span.props.className, "count"); + assert.deepEqual(span.children, ["5"]); + + let button = tree.children[1] as any; + assert.equal(button.type, "button"); + }); + + it("renders React class components with props", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + Class child content + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert(tree.props.className.includes("react-class-component"), "Expected react-class-component class"); + + assert(tree.children && tree.children.length === 2, "Expected two children (label and div)"); + + let label = tree.children[0] as any; + assert.equal(label.type, "label"); + assert.deepEqual(label.children, ["Test Label"]); + + let bodyDiv = tree.children[1] as any; + assert.equal(bodyDiv.type, "div"); + assert.equal(bodyDiv.props.className, "body"); + }); + + it("renders React PureComponent", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "span"); + assert.equal(tree.props.className, "react-pure-component"); + assert.deepEqual(tree.children, ["Pure Value"]); + }); + + it("renders React function component with useRef and useEffect", async () => { + let store = new Store(); + let mountedElement: HTMLDivElement | null = null; + + const component = await createTestRenderer( + store, + + { + mountedElement = el; + }} + /> + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.equal(tree.props.className, "react-ref-effect-component"); + assert.deepEqual(tree.children, ["Component with ref and effect"]); + }); + + it("renders React function component with useEffect that updates state", async () => { + let store = new Store(); + + const component = await createTestRenderer( + store, + + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + assert.equal(tree.props.className, "react-effect-state-component"); + assert(tree.children && tree.children.length === 2, "Expected two children"); + + let processedSpan = tree.children[0] as any; + assert.equal(processedSpan.type, "span"); + assert.equal(processedSpan.props.className, "processed"); + // After act(), useEffect should have run and updated state + assert.deepEqual(processedSpan.children, ["Processed: Test"]); + }); + + it("translates CxJS accessor bindings to React component props", async () => { + interface StoreModel { + text: string; + count: number; + enabled: boolean; + tags: string[]; + } + + let $store = createAccessorModelProxy(); + + // First verify jsx transform output + const widget = ( + + + + ); + + assert.equal(widget.$type, ReactElementWrapper, "React component should use ReactElementWrapper as $type"); + assert.equal(widget.componentType, ReactPropsComponent, "React component should be set as componentType"); + assert.equal(typeof widget.text, "function", "Accessor chain text should be a function"); + assert.equal(widget.text.toString(), "text", "Accessor chain text should resolve to path"); + + // Now verify rendering + let store = new Store({ + data: { + text: "Bound Text", + count: 42, + enabled: true, + tags: ["a", "b", "c"], + }, + }); + + const component = await createTestRenderer(store, widget); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(tree.type, "div"); + + let textSpan = tree.children![0] as any; + assert.deepEqual(textSpan.children, ["Bound Text"]); + + let countSpan = tree.children![1] as any; + assert.deepEqual(countSpan.children, ["42"]); + + let enabledSpan = tree.children![2] as any; + assert.deepEqual(enabledSpan.children, ["yes"]); + + let tagsSpan = tree.children![3] as any; + assert.deepEqual(tagsSpan.children, ["a, b, c"]); + }); + + it("supports visible prop on React components", async () => { + interface StoreModel { + show: boolean; + } + + let $store = createAccessorModelProxy(); + + let store = new Store({ + data: { + show: false, + }, + }); + + const component = await createTestRenderer( + store, + +
    + + Always visible +
    +
    , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + // When visible=false, the React component should not render + assert.equal(tree.children!.length, 1, "Expected only the span to be visible"); + assert.equal((tree.children![0] as any).type, "span"); + + // Update store to make component visible + await act(async () => { + store.set("show", true); + }); + let tree2 = component.toJSON(); + assert(tree2 && !Array.isArray(tree2), "Expected single element"); + assert.equal(tree2.children!.length, 2, "Expected both children to be visible"); + }); + + it("supports controller prop on React components", async () => { + let controllerInitialized = false; + + class TestController extends Controller { + onInit() { + controllerInitialized = true; + } + } + + interface StoreModel { + text: string; + } + + let $store = createAccessorModelProxy(); + + let store = new Store({ + data: { + text: "Controller Test", + }, + }); + + const component = await createTestRenderer( + store, + + + , + ); + + let tree = component.toJSON(); + assert(tree && !Array.isArray(tree), "Expected single element"); + assert.equal(controllerInitialized, true, "Controller should be initialized"); + + let textSpan = tree.children![0] as any; + assert.deepEqual(textSpan.children, ["Controller Test"]); + }); + + it("updates React component when bound store data changes", async () => { + interface StoreModel { + text: string; + count: number; + enabled: boolean; + } + + let $store = createAccessorModelProxy(); + + let store = new Store({ + data: { + text: "Initial", + count: 1, + enabled: false, + }, + }); + + const component = await createTestRenderer( + store, + + + , + ); + + let tree1 = component.toJSON() as any; + assert.deepEqual(tree1.children[0].children, ["Initial"]); + assert.deepEqual(tree1.children[1].children, ["1"]); + assert.deepEqual(tree1.children[2].children, ["no"]); + + // Update store + await act(async () => { + store.set("text", "Updated"); + store.set("count", 99); + store.set("enabled", true); + }); + + let tree2 = component.toJSON() as any; + assert.deepEqual(tree2.children[0].children, ["Updated"]); + assert.deepEqual(tree2.children[1].children, ["99"]); + assert.deepEqual(tree2.children[2].children, ["yes"]); + }); + + describe("React component type inference", () => { + interface RequiredPropsComponentProps { + label: string; + value: number; + onChange: (value: number) => void; + disabled?: boolean; + } + + function RequiredPropsComponent(_props: RequiredPropsComponentProps) { + return null; + } + + it("accepts all required props", () => { + const widget = ( + + console.log(v)} /> + + ); + assert.ok(widget); + }); + + it("accepts required props with optional prop", () => { + const widget = ( + + console.log(v)} disabled={true} /> + + ); + assert.ok(widget); + }); + + it("rejects missing required prop (label)", () => { + const widget = ( + + {/* @ts-expect-error - label is required but missing */} + console.log(v)} /> + + ); + assert.ok(widget); + }); + + it("rejects missing required prop (onChange)", () => { + const widget = ( + + {/* @ts-expect-error - onChange is required but missing */} + + + ); + assert.ok(widget); + }); + + it("rejects missing all required props", () => { + const widget = ( + + {/* @ts-expect-error - label, value, and onChange are required but missing */} + + + ); + assert.ok(widget); + }); + + it("rejects wrong prop type (string for number)", () => { + const widget = ( + + {/* @ts-expect-error - value should be number, not string */} + console.log(v)} /> + + ); + assert.ok(widget); + }); + + it("rejects wrong prop type (number for string)", () => { + const widget = ( + + {/* @ts-expect-error - label should be string, not number */} + console.log(v)} /> + + ); + assert.ok(widget); + }); + + it("provides instance through this", () => { + class TestController extends Controller { + change(_v: number) {} + } + const widget = ( + + + + ); + assert.ok(widget); + }); + }); +}); diff --git a/packages/cx/src/widgets/ReactElementWrapper.tsx b/packages/cx/src/widgets/ReactElementWrapper.tsx new file mode 100644 index 000000000..7de1d87b2 --- /dev/null +++ b/packages/cx/src/widgets/ReactElementWrapper.tsx @@ -0,0 +1,108 @@ +import { ContainerBase, ContainerConfig } from "../ui/Container"; +import { Instance } from "../ui/Instance"; +import { Prop } from "../ui/Prop"; +import type { RenderingContext } from "../ui/RenderingContext"; +import { VDOM, Widget } from "../ui/Widget"; +import { isFunction, isNonEmptyArray } from "../util"; +import { isArray } from "../util/isArray"; + +type ReactElementWrapperConfigBase = Omit; + +// CxJS callback type - can be string (controller method) or callback with Instance as this +type CxCallback = T extends (...args: infer A) => infer R ? (this: Instance, ...args: A) => R : T; + +// Transform a single prop: functions to CxCallback, data to Prop +type TransformProp = K extends keyof ReactElementWrapperConfigBase + ? ReactElementWrapperConfigBase[K] + : NonNullable extends Function + ? CxCallback + : Prop; + +// Transform React component props - functions to CxCallback, data props to Prop +// Preserves required/optional status +type TransformReactElementProps = { + [K in keyof T]: TransformProp; +}; + +/** ReactElementWrapper configuration with component-specific props */ +// componentType is not included here - it's added by the jsx-runtime and declared in the class +export type ReactElementWrapperConfig

    = ReactElementWrapperConfigBase & TransformReactElementProps

    ; + +interface ReactElementWrapperInstance extends Instance { + events?: Record; +} + +export class ReactElementWrapper = React.ComponentType> extends ContainerBase< + ReactElementWrapperConfigBase & { componentType: C; jsxAttributes?: string[] }, + ReactElementWrapperInstance +> { + declare public componentType: React.ComponentType; + declare public jsxAttributes?: string[]; + declare public props?: Record; + [key: string]: unknown; + + init(): void { + // Collect all props to pass to the React component + this.props = {}; + + if (this.jsxAttributes) { + for (const attr of this.jsxAttributes) { + // Skip CxJS reserved attributes and children-related attributes + if ( + attr === "componentType" || + attr === "visible" || + attr === "if" || + attr === "controller" || + attr === "jsxAttributes" || + attr === "layout" || + attr === "outerLayout" || + attr === "putInto" || + attr === "contentFor" || + attr === "children" || + attr === "items" + ) + continue; + + this.props[attr] = this[attr]; + } + } + + super.init(); + } + + public initInstance(_context: RenderingContext, _instance: ReactElementWrapperInstance): void { + let events: Record | undefined; + if (!isNonEmptyArray(this.jsxAttributes)) return; + events = {}; + for (let prop of this.jsxAttributes) { + let f = this[prop]; + if (prop.startsWith("on") && isFunction(f)) events[prop] = (...args: any[]) => f.apply(_instance, args); + } + } + + declareData(...args: Record[]): void { + super.declareData( + { + props: { structured: true }, + }, + ...args, + ); + } + + render(context: RenderingContext, instance: ReactElementWrapperInstance, key: string): React.ReactNode { + const { data } = instance; + + // Render CxJS children to React elements + let children = this.renderChildren(context, instance); + if (children && isArray(children) && children.length === 0) children = undefined; + + const props = { + key, + ...data.props, + ...instance.events, + children, + }; + + return VDOM.createElement(this.componentType, props); + } +} diff --git a/packages/cx/src/widgets/Resizer.d.ts b/packages/cx/src/widgets/Resizer.d.ts deleted file mode 100644 index 2567f0351..000000000 --- a/packages/cx/src/widgets/Resizer.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - Widget, - StyledContainerProps -} from 'cx/src/core'; - -interface ResizerProps extends StyledContainerProps { - - /** Make resizer horizontal. */ - horizontal?: boolean; - - /** Use the element after the the resizer for size measurements. */ - forNextElement?: boolean, - - /** A binding for the new size. */ - size?: Cx.NumberProp, - - /** Default value that will be set when the user double click on the resizer. */ - defaultSize?: Cx.NumberProp, - - /** Minimum size of the element. */ - minSize?: Cx.NumberProp, - - /** Maximum size of the element. */ - maxSize?: Cx.NumberProp, -} - -export class Resizer extends Widget {} diff --git a/packages/cx/src/widgets/Resizer.js b/packages/cx/src/widgets/Resizer.js deleted file mode 100644 index dfa4c81c3..000000000 --- a/packages/cx/src/widgets/Resizer.js +++ /dev/null @@ -1,151 +0,0 @@ -import { Widget, VDOM } from "../ui/Widget"; -import { captureMouseOrTouch, getCursorPos } from "./overlay/captureMouse"; - -export class Resizer extends Widget { - declareData(...args) { - super.declareData(...args, { - size: undefined, - defaultSize: undefined, - minSize: undefined, - maxSize: undefined, - }); - } - - render(context, instance, key) { - let { data } = instance; - - return ; - } -} - -Resizer.prototype.baseClass = "resizer"; -Resizer.prototype.styled = true; -Resizer.prototype.horizontal = false; -Resizer.prototype.forNextElement = false; -Resizer.prototype.defaultSize = null; -Resizer.prototype.minSize = 0; -Resizer.prototype.maxSize = 1e6; - -class ResizerCmp extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - dragged: false, - offset: 0, - }; - } - - shouldComponentUpdate(props, state) { - return state != this.state; - } - - render() { - let { instance, data } = this.props; - let { widget } = instance; - let { baseClass, CSS } = widget; - - return ( -

    { - this.el = el; - }} - className={CSS.expand( - data.classNames, - CSS.state({ - vertical: !widget.horizontal, - horizontal: widget.horizontal, - }) - )} - style={data.style} - onDoubleClick={(e) => { - instance.set("size", data.defaultSize); - }} - onMouseDown={(e) => { - let initialPosition = getCursorPos(e); - this.setState({ dragged: true, initialPosition }); - }} - onMouseUp={(e) => { - this.setState({ dragged: false }); - }} - onMouseMove={this.startCapture.bind(this)} - onMouseLeave={this.startCapture.bind(this)} - > -
    -
    - ); - } - - startCapture(e) { - let { instance } = this.props; - let { widget } = instance; - - if (this.state.dragged && !this.hasCapture) { - this.hasCapture = true; - captureMouseOrTouch( - e, - this.onHandleMove.bind(this), - this.onDragComplete.bind(this), - this.state.initialPosition, - widget.horizontal ? "row-resize" : "col-resize" - ); - } - } - - onHandleMove(e, initialPosition) { - let { instance } = this.props; - let { widget } = instance; - let currentPosition = getCursorPos(e); - const offset = !widget.horizontal - ? currentPosition.clientX - initialPosition.clientX - : currentPosition.clientY - initialPosition.clientY; - - let size = this.getNewSize(0); - let newSize = this.getNewSize(offset); - - let allowedOffset = widget.forNextElement ? size - newSize : newSize - size; - - this.setState({ offset: allowedOffset }); - } - - getNewSize(offset) { - let { instance, data } = this.props; - let { horizontal, forNextElement } = instance.widget; - - if ( - !this.el || - (!forNextElement && !this.el.previousElementSibling) || - (forNextElement && !this.el.nextElementSibling) - ) - return 0; - - let newSize; - - if (horizontal) { - if (forNextElement) newSize = this.el.nextElementSibling.offsetHeight - offset; - else newSize = this.el.previousElementSibling.offsetHeight + offset; - } else { - if (forNextElement) newSize = this.el.nextElementSibling.offsetWidth - offset; - else newSize = this.el.previousElementSibling.offsetWidth + offset; - } - - return Math.max(data.minSize, Math.min(newSize, data.maxSize)); - } - - onDragComplete() { - this.hasCapture = false; - let { instance } = this.props; - - instance.set("size", this.getNewSize(this.state.offset)); - - this.setState({ - dragged: false, - offset: 0, - }); - } -} diff --git a/packages/cx/src/widgets/Resizer.scss b/packages/cx/src/widgets/Resizer.scss index 07a652701..0c2616238 100644 --- a/packages/cx/src/widgets/Resizer.scss +++ b/packages/cx/src/widgets/Resizer.scss @@ -1,13 +1,14 @@ +@use "sass:map"; @mixin cx-resizer( $name: 'resizer', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { position: relative; diff --git a/packages/cx/src/widgets/Resizer.tsx b/packages/cx/src/widgets/Resizer.tsx new file mode 100644 index 000000000..06beddaf4 --- /dev/null +++ b/packages/cx/src/widgets/Resizer.tsx @@ -0,0 +1,200 @@ +/** @jsxImportSource react */ +import { Instance } from "../ui/Instance"; +import { NumberProp } from "../ui/Prop"; +import { RenderingContext } from "../ui/RenderingContext"; +import { VDOM, Widget, WidgetConfig, WidgetStyleConfig } from "../ui/Widget"; +import { captureMouseOrTouch, getCursorPos } from "./overlay/captureMouse"; + +export interface ResizerConfig extends WidgetConfig, WidgetStyleConfig { + /** Make resizer horizontal. */ + horizontal?: boolean; + + /** Use the element after the the resizer for size measurements. */ + forNextElement?: boolean; + + /** A binding for the new size. */ + size?: NumberProp; + + /** Default value that will be set when the user double click on the resizer. */ + defaultSize?: NumberProp; + + /** Minimum size of the element. */ + minSize?: NumberProp; + + /** Maximum size of the element. */ + maxSize?: NumberProp; +} + +export class Resizer extends Widget { + declare baseClass: string; + declare horizontal?: boolean; + declare forNextElement?: boolean; + declare defaultSize?: NumberProp | null; + declare minSize?: NumberProp; + declare maxSize?: NumberProp; + + constructor(config?: ResizerConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData(...args, { + size: undefined, + defaultSize: undefined, + minSize: undefined, + maxSize: undefined, + }); + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + let { data } = instance; + + return ; + } +} + +Resizer.prototype.baseClass = "resizer"; +Resizer.prototype.styled = true; +Resizer.prototype.horizontal = false; +Resizer.prototype.forNextElement = false; +Resizer.prototype.defaultSize = null; +Resizer.prototype.minSize = 0; +Resizer.prototype.maxSize = 1e6; + +interface ResizerCmpProps { + instance: Instance; + data: Record; +} + +interface ResizerCmpState { + dragged: boolean; + offset: number; + initialPosition?: { clientX: number; clientY: number }; +} + +class ResizerCmp extends VDOM.Component { + declare el?: HTMLDivElement | null; + declare hasCapture?: boolean; + + constructor(props: ResizerCmpProps) { + super(props); + this.state = { + dragged: false, + offset: 0, + }; + } + + shouldComponentUpdate(props: ResizerCmpProps, state: ResizerCmpState): boolean { + return state != this.state; + } + + render(): React.ReactNode { + let { instance, data } = this.props; + let { widget } = instance; + let { baseClass, CSS } = widget; + + return ( +
    { + this.el = el; + }} + className={CSS.expand( + data.classNames, + CSS.state({ + vertical: !widget.horizontal, + horizontal: widget.horizontal, + }), + )} + style={data.style} + onDoubleClick={(e) => { + instance.set("size", data.defaultSize); + }} + onMouseDown={(e) => { + let initialPosition = getCursorPos(e); + this.setState({ dragged: true, initialPosition }); + }} + onMouseUp={(e) => { + this.setState({ dragged: false }); + }} + onMouseMove={this.startCapture.bind(this)} + onMouseLeave={this.startCapture.bind(this)} + > +
    +
    + ); + } + + startCapture(e: React.MouseEvent): void { + let { instance } = this.props; + let { widget } = instance; + + if (this.state.dragged && !this.hasCapture) { + this.hasCapture = true; + captureMouseOrTouch( + e, + this.onHandleMove.bind(this), + this.onDragComplete.bind(this), + this.state.initialPosition!, + widget.horizontal ? "row-resize" : "col-resize", + ); + } + } + + onHandleMove(e: any, initialPosition: { clientX: number; clientY: number }): void { + let { instance } = this.props; + let { widget } = instance; + let currentPosition = getCursorPos(e); + const offset = !widget.horizontal + ? currentPosition.clientX - initialPosition.clientX + : currentPosition.clientY - initialPosition.clientY; + + let size = this.getNewSize(0); + let newSize = this.getNewSize(offset); + + let allowedOffset = widget.forNextElement ? size - newSize : newSize - size; + + this.setState({ offset: allowedOffset }); + } + + getNewSize(offset: number): number { + let { instance, data } = this.props; + let { horizontal, forNextElement } = instance.widget; + + if ( + !this.el || + (!forNextElement && !this.el.previousElementSibling) || + (forNextElement && !this.el.nextElementSibling) + ) + return 0; + + let newSize: number; + + if (horizontal) { + if (forNextElement) newSize = (this.el.nextElementSibling as HTMLElement).offsetHeight - offset; + else newSize = (this.el.previousElementSibling as HTMLElement).offsetHeight + offset; + } else { + if (forNextElement) newSize = (this.el.nextElementSibling as HTMLElement).offsetWidth - offset; + else newSize = (this.el.previousElementSibling as HTMLElement).offsetWidth + offset; + } + + return Math.max(data.minSize as number, Math.min(newSize, data.maxSize as number)); + } + + onDragComplete(): void { + this.hasCapture = false; + let { instance } = this.props; + + instance.set("size", this.getNewSize(this.state.offset)); + + this.setState({ + dragged: false, + offset: 0, + }); + } +} diff --git a/packages/cx/src/widgets/Sandbox.d.ts b/packages/cx/src/widgets/Sandbox.d.ts deleted file mode 100644 index e5b6fce41..000000000 --- a/packages/cx/src/widgets/Sandbox.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Cx from "../core"; - -interface SandboxProps extends Cx.PureContainerProps { - storage: Cx.Prop; - - /* Cx.StringProp doesn't work for unknown reason */ - key?: any; - - accessKey?: Cx.StringProp; - - recordName?: Cx.RecordAlias; - recordAlias?: Cx.RecordAlias; - - immutable?: boolean; - sealed?: boolean; -} - -export class Sandbox extends Cx.Widget {} diff --git a/packages/cx/src/widgets/Sandbox.js b/packages/cx/src/widgets/Sandbox.js deleted file mode 100644 index 1fdc1bf10..000000000 --- a/packages/cx/src/widgets/Sandbox.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Widget } from "../ui/Widget"; -import { PureContainer } from "../ui/PureContainer"; -import { Binding } from "../data/Binding"; -import { ExposedValueView } from "../data/ExposedValueView"; - -export class Sandbox extends PureContainer { - init() { - if (this.recordAlias) this.recordName = this.recordAlias; - - if (this.accessKey) this.key = this.accessKey; - - this.storageBinding = Binding.get(this.storage); - - super.init(); - } - - initInstance(context, instance) { - instance.store = new ExposedValueView({ - store: instance.parentStore, - containerBinding: this.storageBinding, - key: null, - recordName: this.recordName, - immutable: this.immutable, - }); - super.initInstance(context, instance); - } - - applyParentStore(instance) { - instance.store.setStore(instance.parentStore); - } - - declareData() { - super.declareData( - { - storage: undefined, - key: undefined, - }, - ...arguments, - ); - } - - prepareData(context, instance) { - var { store, data } = instance; - if (store.getKey() !== data.key) { - //when navigating to a page using the same widget tree as the previous page - //everything needs to be reinstantiated, e.g. user/1 => user/2 - instance.store = new ExposedValueView({ - store: store, - containerBinding: this.storageBinding, - key: data.key, - recordName: this.recordName, - immutable: this.immutable, - sealed: this.sealed, - }); - instance.clearChildrenCache(); - } - super.prepareData(context, instance); - } -} - -Sandbox.prototype.recordName = "$page"; -Sandbox.prototype.immutable = false; -Sandbox.prototype.sealed = false; - -Widget.alias("sandbox", Sandbox); diff --git a/packages/cx/src/widgets/Sandbox.ts b/packages/cx/src/widgets/Sandbox.ts new file mode 100644 index 000000000..3c40d8cf3 --- /dev/null +++ b/packages/cx/src/widgets/Sandbox.ts @@ -0,0 +1,103 @@ +import { Widget } from "../ui/Widget"; +import { PureContainerBase, PureContainerConfig } from "../ui/PureContainer"; +import { Binding, BindingInput } from "../data/Binding"; +import { ExposedValueView, ExposedValueViewConfig } from "../data/ExposedValueView"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Instance } from "../ui/Instance"; +import { StringProp, WritableProp } from "../ui/Prop"; + +export interface SandboxConfig extends PureContainerConfig { + /** Binding to the object that holds sandbox data. */ + storage: WritableProp>; + + /** Key used to identify the sandbox instance within the storage. */ + key?: StringProp; + + /** Alias for `key`. */ + accessKey?: StringProp; + + /** Alias used to expose sandbox data. Default is `$page`. */ + recordName?: string; + + /** Alias for `recordName`. */ + recordAlias?: string; + + /** Indicate that parent store data should not be mutated. */ + immutable?: boolean; + + /** Indicate that sandbox store data should not be mutated. */ + sealed?: boolean; +} + +export interface SandboxInstance extends Instance { + store: ExposedValueView; +} + +export class Sandbox extends PureContainerBase { + declare storage: WritableProp>; + declare key?: StringProp; + declare recordName?: string; + declare recordAlias?: string; + declare accessKey?: StringProp; + declare immutable?: boolean; + declare sealed?: boolean; + declare storageBinding: Binding; + init(): void { + if (this.recordAlias) this.recordName = this.recordAlias; + + if (this.accessKey) this.key = this.accessKey; + + this.storageBinding = Binding.get(this.storage); + + super.init(); + } + + initInstance(context: RenderingContext, instance: SandboxInstance): void { + instance.store = new ExposedValueView({ + store: instance.parentStore, + containerBinding: this.storageBinding, + key: null, + recordName: this.recordName, + immutable: this.immutable, + }); + super.initInstance(context, instance); + } + + applyParentStore(instance: SandboxInstance): void { + instance.store.setStore(instance.parentStore); + } + + declareData(...args: Record[]): void { + super.declareData( + { + storage: undefined, + key: undefined, + }, + ...args, + ); + } + + prepareData(context: RenderingContext, instance: SandboxInstance): void { + var { store, data } = instance; + if (store.getKey() !== data.key) { + //when navigating to a page using the same widget tree as the previous page + //everything needs to be reinstantiated, e.g. user/1 => user/2 + instance.store = new ExposedValueView({ + store: store, + containerBinding: this.storageBinding, + key: data.key, + recordName: this.recordName, + immutable: this.immutable, + sealed: this.sealed, + }); + instance.clearChildrenCache(); + } + super.prepareData(context, instance); + } +} + +Sandbox.prototype.recordName = "$page"; +Sandbox.prototype.immutable = false; +Sandbox.prototype.sealed = false; + +Widget.alias("sandbox", Sandbox); diff --git a/packages/cx/src/widgets/Section.d.ts b/packages/cx/src/widgets/Section.d.ts deleted file mode 100644 index d23aa5a5f..000000000 --- a/packages/cx/src/widgets/Section.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - Widget, - Prop, - StyledContainerProps, - BooleanProp, - StringProp, - StyleProp, - ClassProp, - Config -} from '../core'; - -interface SectionProps extends StyledContainerProps { - - id?: Prop, - - /** Add default padding to the section body. Default is `true`. */ - pad?: BooleanProp, - - /** A custom style which will be applied to the header. */ - headerStyle?: StyleProp, - - /** Additional CSS class to be applied to the header. */ - headerClass?: ClassProp, - - /** A custom style which will be applied to the body. */ - bodyStyle?: StyleProp, - - /** Additional CSS class to be applied to the section body. */ - bodyClass?: ClassProp, - - /** A custom style which will be applied to the footer. */ - footerStyle?: StyleProp, - - /** Additional CSS class to be applied to the footer. */ - footerClass?: ClassProp, - - /** Section's title. */ - title?: StringProp, - - /** Contents that should go in the header. */ - header?: Config, - - /** Contents that should go in the footer. */ - footer?: Config, - - /** Title heading level (1-6) */ - hLevel?: number - -} - -export class Section extends Widget { -} diff --git a/packages/cx/src/widgets/Section.js b/packages/cx/src/widgets/Section.js deleted file mode 100644 index 491937f2d..000000000 --- a/packages/cx/src/widgets/Section.js +++ /dev/null @@ -1,139 +0,0 @@ -import {Widget, VDOM, getContent} from '../ui/Widget'; -import {Container} from '../ui/Container'; -import {Heading} from './Heading'; -import {isString} from '../util/isString'; -import {parseStyle} from '../util/parseStyle'; - -export class Section extends Container { - - init() { - if (isString(this.headerStyle)) - this.headerStyle = parseStyle(this.headerStyle); - - if (isString(this.footerStyle)) - this.footerStyle = parseStyle(this.footerStyle); - - if (isString(this.bodyStyle)) - this.bodyStyle = parseStyle(this.bodyStyle); - - super.init(); - } - - add(item) { - if (item && item.putInto == 'header') - this.header = { - ...item, - putInto: null - }; - else if (item && item.putInto == 'footer') - this.footer = { - ...item, - putInto: null - }; - else - super.add(...arguments); - } - - declareData() { - return super.declareData({ - id: undefined, - headerStyle: {structured: true}, - headerClass: {structured: true}, - bodyStyle: {structured: true}, - bodyClass: {structured: true}, - footerStyle: {structured: true}, - footerClass: {structured: true} - }) - } - - initComponents() { - super.initComponents({ - header: this.getHeader(), - footer: this.getFooter() - }); - } - - getHeader() { - if (this.title) - return Widget.create(Heading, { - text: this.title, - level: this.hLevel - }); - - if (this.header) - return Heading.create(this.header); - - return null; - } - - getFooter() { - if (this.footer) - return Widget.create(this.footer); - - return null; - } - - prepareData(context, instance) { - let {data} = instance; - data.stateMods = { - ...data.stateMods, - pad: this.pad - }; - super.prepareData(context, instance); - } - - initInstance(context, instance) { - instance.eventHandlers = instance.getJsxEventProps(); - super.initInstance(context, instance); - } - - render(context, instance, key) { - let {data, components, eventHandlers} = instance; - let header, footer; - let {CSS, baseClass} = this; - - if (components.header) { - header = ( -
    - {getContent(components.header.render(context))} -
    - ); - } - - if (components.footer) { - footer = ( -
    - {getContent(components.footer.render(context))} -
    - ); - } - - return ( -
    - { header } -
    - {this.renderChildren(context, instance)} -
    - { footer } -
    - ) - } -} - - -Section.prototype.styled = true; -Section.prototype.pad = true; -Section.prototype.baseClass = 'section'; -Section.prototype.hLevel = 3; diff --git a/packages/cx/src/widgets/Section.scss b/packages/cx/src/widgets/Section.scss index fdb7a963b..725f6f62e 100644 --- a/packages/cx/src/widgets/Section.scss +++ b/packages/cx/src/widgets/Section.scss @@ -1,3 +1,4 @@ +@use "sass:map"; @mixin cx-section( $name: 'section', @@ -5,10 +6,10 @@ $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { flex-direction: column; diff --git a/packages/cx/src/widgets/Section.tsx b/packages/cx/src/widgets/Section.tsx new file mode 100644 index 000000000..307819297 --- /dev/null +++ b/packages/cx/src/widgets/Section.tsx @@ -0,0 +1,187 @@ +/** @jsxImportSource react */ +import { Widget, VDOM, getContent } from "../ui/Widget"; +import { ChildNode, ContainerBase, StyledContainerConfig } from "../ui/Container"; +import { Heading } from "./Heading"; +import { isString } from "../util/isString"; +import { parseStyle } from "../util/parseStyle"; +import { RenderingContext } from "../ui/RenderingContext"; +import { Instance } from "../ui/Instance"; +import { BooleanProp, StringProp, StyleProp, ClassProp, Prop } from "../ui/Prop"; +import { Create } from "../util/Component"; + +export interface SectionConfig extends StyledContainerConfig { + id?: Prop; + + /** Add default padding to the section body. Default is `true`. */ + pad?: BooleanProp; + + /** A custom style which will be applied to the header. */ + headerStyle?: StyleProp; + + /** Additional CSS class to be applied to the header. */ + headerClass?: ClassProp; + + /** A custom style which will be applied to the body. */ + bodyStyle?: StyleProp; + + /** Additional CSS class to be applied to the section body. */ + bodyClass?: ClassProp; + + /** A custom style which will be applied to the footer. */ + footerStyle?: StyleProp; + + /** Additional CSS class to be applied to the footer. */ + footerClass?: ClassProp; + + /** Section's title. */ + title?: StringProp; + + /** Contents that should go in the header. */ + header?: Create; + + /** Contents that should go in the footer. */ + footer?: Create; + + /** Title heading level (1-6) */ + hLevel?: number; +} + +export class Section extends ContainerBase { + declare headerStyle?: StyleProp; + declare footerStyle?: StyleProp; + declare bodyStyle?: StyleProp; + declare title?: StringProp; + declare header?: Create; + declare footer?: Create; + declare hLevel?: number; + declare pad?: BooleanProp; + declare baseClass: string; + + init(): void { + if (isString(this.headerStyle)) this.headerStyle = parseStyle(this.headerStyle); + + if (isString(this.footerStyle)) this.footerStyle = parseStyle(this.footerStyle); + + if (isString(this.bodyStyle)) this.bodyStyle = parseStyle(this.bodyStyle); + + super.init(); + } + + add(item: any): void { + if (item && item.putInto == "header") + this.header = { + ...item, + putInto: null, + }; + else if (item && item.putInto == "footer") + this.footer = { + ...item, + putInto: null, + }; + else super.add(item); + } + + declareData(...args: Record[]): void { + super.declareData(...args, { + id: undefined, + headerStyle: { structured: true }, + headerClass: { structured: true }, + bodyStyle: { structured: true }, + bodyClass: { structured: true }, + footerStyle: { structured: true }, + footerClass: { structured: true }, + }); + } + + initComponents(context: RenderingContext, instance: Instance): void { + super.initComponents(context, instance, { + header: this.getHeader(), + footer: this.getFooter(), + }); + } + + getHeader(): Widget | null { + if (this.title) + return Widget.create(Heading, { + text: this.title, + level: this.hLevel, + }) as Widget; + + if (this.header) return Heading.create(this.header); + + return null; + } + + getFooter(): Widget | null { + if (this.footer) return Widget.create(this.footer); + + return null; + } + + prepareData(context: RenderingContext, instance: Instance): void { + let { data } = instance; + data.stateMods = { + ...data.stateMods, + pad: this.pad, + }; + super.prepareData(context, instance); + } + + initInstance(context: RenderingContext, instance: Instance): void { + (instance as any).eventHandlers = instance.getJsxEventProps(); + super.initInstance(context, instance); + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + let { data, components } = instance; + let eventHandlers = (instance as any).eventHandlers; + let header: React.ReactNode, footer: React.ReactNode; + let { CSS, baseClass } = this; + + if (components?.header) { + header = ( +
    + {getContent(components.header.render(context))} +
    + ); + } + + if (components?.footer) { + footer = ( +
    + {getContent(components.footer.render(context))} +
    + ); + } + + return ( +
    + {header} +
    + {this.renderChildren(context, instance)} +
    + {footer} +
    + ); + } +} + +Section.prototype.styled = true; +Section.prototype.pad = true; +Section.prototype.baseClass = "section"; +Section.prototype.hLevel = 3; diff --git a/packages/cx/src/widgets/autoFocus.d.ts b/packages/cx/src/widgets/autoFocus.d.ts deleted file mode 100644 index b60fd7a27..000000000 --- a/packages/cx/src/widgets/autoFocus.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function autoFocus(el: any, component: React.Component); diff --git a/packages/cx/src/widgets/autoFocus.js b/packages/cx/src/widgets/autoFocus.js deleted file mode 100644 index 8a0f3dca1..000000000 --- a/packages/cx/src/widgets/autoFocus.js +++ /dev/null @@ -1,9 +0,0 @@ -import { FocusManager } from "../ui/FocusManager"; -import { isTouchEvent } from "../util/isTouchEvent"; - -export function autoFocus(el, component) { - let data = component.props.data || component.props.instance.data; - let autoFocusValue = el && data.autoFocus; - if (autoFocusValue && autoFocusValue != component.autoFocusValue && !isTouchEvent()) FocusManager.focus(el); - component.autoFocusValue = autoFocusValue; -} diff --git a/packages/cx/src/widgets/autoFocus.ts b/packages/cx/src/widgets/autoFocus.ts new file mode 100644 index 000000000..69b27941a --- /dev/null +++ b/packages/cx/src/widgets/autoFocus.ts @@ -0,0 +1,9 @@ +import { FocusManager } from "../ui/FocusManager"; +import { isTouchEvent } from "../util/isTouchEvent"; + +export function autoFocus(el: HTMLElement | undefined | null, component: any): void { + let data = component.props.data || component.props.instance.data; + let autoFocusValue = el && data.autoFocus; + if (autoFocusValue && autoFocusValue != component.autoFocusValue && !isTouchEvent()) FocusManager.focus(el!); + component.autoFocusValue = autoFocusValue; +} diff --git a/packages/cx/src/widgets/cx.d.ts b/packages/cx/src/widgets/cx.d.ts deleted file mode 100644 index b1d2c9873..000000000 --- a/packages/cx/src/widgets/cx.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export function cx(typeName, props?, ...children); -export function react(config); \ No newline at end of file diff --git a/packages/cx/src/widgets/cx.js b/packages/cx/src/widgets/cx.js deleted file mode 100644 index 88e17f20d..000000000 --- a/packages/cx/src/widgets/cx.js +++ /dev/null @@ -1,72 +0,0 @@ -import {HtmlElement} from './HtmlElement'; -import {VDOM} from '../ui/Widget'; -import {createComponentFactory, isComponentFactory} from '../util/Component'; -import {createFunctionalComponent} from '../ui/createFunctionalComponent' -import {isString} from '../util/isString'; -import {isNumber} from '../util/isNumber'; -import {isFunction} from '../util/isFunction'; -import {isUndefined} from '../util/isUndefined'; -import {isArray} from '../util/isArray'; - -import {flattenProps} from '../ui/flattenProps'; - -let htmlFactoryCache = {}; - -function getHtmlElementFactory(tagName) { - let factory = htmlFactoryCache[tagName]; - if (factory) - return factory; - return htmlFactoryCache[tagName] = createComponentFactory(() => {}, config => HtmlElement.create(HtmlElement, {tag: tagName}, flattenProps(config)), {tag: tagName}); -} - -export function cx(typeName, props, ...children) { - - if (isArray(typeName)) - return typeName; - - if (isFunction(typeName) && isUndefined(props)) - return createFunctionalComponent(config => typeName(flattenProps(config))); - - if (typeName.type || typeName.$type) - return typeName; - - if (children && children.length == 0) - children = null; - - if (children && children.length == 1) - children = children[0]; - - if (typeName == 'cx') - return children; - - if (typeName == 'react') - return react(children); - - if (isString(typeName) && typeName[0] == typeName[0].toLowerCase()) - typeName = getHtmlElementFactory(typeName); - - return { - $type: typeName, - $props: props, - jsxAttributes: props && Object.keys(props), - children - } -} - -export function react(config) { - if (!config || isString(config) || isNumber(config) || VDOM.isValidElement(config)) - return config; - - if (isArray(config)) - return config.map(react); - - let type = config.$type; - - if (isComponentFactory(type) && type.$meta && type.$meta.tag) - type = type.$meta.tag; - - if (isArray(config.children)) - return VDOM.createElement(type, config.$props, ...config.children.map(react)); - - return VDOM.createElement(type, config.$props, react(config.children)); -} \ No newline at end of file diff --git a/packages/cx/src/widgets/cx.ts b/packages/cx/src/widgets/cx.ts new file mode 100644 index 000000000..739aba5ec --- /dev/null +++ b/packages/cx/src/widgets/cx.ts @@ -0,0 +1,63 @@ +import { HtmlElement } from "./HtmlElement"; +import { VDOM } from "../ui/Widget"; +import { createComponentFactory, isComponentFactory } from "../util/Component"; +import { createFunctionalComponent } from "../ui/createFunctionalComponent"; +import { isString } from "../util/isString"; +import { isNumber } from "../util/isNumber"; +import { isFunction } from "../util/isFunction"; +import { isUndefined } from "../util/isUndefined"; +import { isArray } from "../util/isArray"; + +import { flattenProps } from "../ui/flattenProps"; + +let htmlFactoryCache: Record = {}; + +function getHtmlElementFactory(tagName: string): any { + let factory = htmlFactoryCache[tagName]; + if (factory) return factory; + return (htmlFactoryCache[tagName] = createComponentFactory( + () => {}, + (config: any) => HtmlElement.create(HtmlElement, { tag: tagName }, flattenProps(config)), + { tag: tagName }, + )); +} + +export function cx(typeName: any, props?: any, ...children: any[]): any { + if (isArray(typeName)) return typeName; + + if (isFunction(typeName) && isUndefined(props)) + return createFunctionalComponent((config: any) => typeName(flattenProps(config))); + + if (typeName.type || typeName.$type) return typeName; + + if (children && children.length == 0) children = []; + + if (children && children.length == 1) children = children[0]; + + if (typeName == "cx") return children; + + if (typeName == "react") return react(children); + + if (isString(typeName) && typeName[0] == typeName[0].toLowerCase()) typeName = getHtmlElementFactory(typeName); + + return { + $type: typeName, + $props: props, + jsxAttributes: props && Object.keys(props), + children, + }; +} + +export function react(config: any): any { + if (!config || isString(config) || isNumber(config) || VDOM.isValidElement(config)) return config; + + if (isArray(config)) return config.map(react); + + let type = config.$type; + + if (isComponentFactory(type) && type.$meta && type.$meta.tag) type = type.$meta.tag; + + if (isArray(config.children)) return VDOM.createElement(type, config.$props, ...config.children.map(react)); + + return VDOM.createElement(type, config.$props, react(config.children)); +} diff --git a/packages/cx/src/widgets/drag-drop/DragClone.scss b/packages/cx/src/widgets/drag-drop/DragClone.scss index ad3eb69a9..d40bd6a3f 100644 --- a/packages/cx/src/widgets/drag-drop/DragClone.scss +++ b/packages/cx/src/widgets/drag-drop/DragClone.scss @@ -1,12 +1,14 @@ +@use "sass:map"; + @mixin cx-dragclone( $name: 'dragclone', $style: $cx-dragclone-style, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { diff --git a/packages/cx/src/widgets/drag-drop/DragHandle.d.ts b/packages/cx/src/widgets/drag-drop/DragHandle.d.ts deleted file mode 100644 index 4678c2083..000000000 --- a/packages/cx/src/widgets/drag-drop/DragHandle.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Cx from '../../core'; - -interface DragHandleProps extends Cx.StyledContainerProps { - - /** Base CSS class to be applied to the element. Defaults to 'draghandle'. */ - baseClass?: string - -} - -export class DragHandle extends Cx.Widget {} diff --git a/packages/cx/src/widgets/drag-drop/DragHandle.js b/packages/cx/src/widgets/drag-drop/DragHandle.js deleted file mode 100644 index fc731b249..000000000 --- a/packages/cx/src/widgets/drag-drop/DragHandle.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Widget, VDOM } from '../../ui/Widget'; -import { Container } from '../../ui/Container'; -import { ddHandle } from '../drag-drop/ops'; -import {isArray} from '../../util/isArray'; - -export class DragHandle extends Container { - - explore(context, instance) { - if (isArray(context.dragHandles)) - context.dragHandles.push(instance); - super.explore(context, instance); - } - - render(context, instance, key) { - let {data} = instance; - return ( -
    - {this.renderChildren(context, instance)} -
    - ) - } -} - -DragHandle.prototype.styled = true; -DragHandle.prototype.baseClass = 'draghandle'; - -Widget.alias('draghandle', DragHandle); diff --git a/packages/cx/src/widgets/drag-drop/DragHandle.scss b/packages/cx/src/widgets/drag-drop/DragHandle.scss index 29f0a25fd..eb16909da 100644 --- a/packages/cx/src/widgets/drag-drop/DragHandle.scss +++ b/packages/cx/src/widgets/drag-drop/DragHandle.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-draghandle( $name: 'draghandle', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { touch-action: none; diff --git a/packages/cx/src/widgets/drag-drop/DragHandle.tsx b/packages/cx/src/widgets/drag-drop/DragHandle.tsx new file mode 100644 index 000000000..20aad9303 --- /dev/null +++ b/packages/cx/src/widgets/drag-drop/DragHandle.tsx @@ -0,0 +1,47 @@ +/** @jsxImportSource react */ +import { Widget, VDOM } from '../../ui/Widget'; +import { StyledContainerBase, StyledContainerConfig } from '../../ui/Container'; +import { ddHandle } from '../drag-drop/ops'; +import { isArray } from '../../util/isArray'; +import { RenderingContext } from '../../ui/RenderingContext'; +import { Instance } from '../../ui/Instance'; + +export interface DragHandleConfig extends StyledContainerConfig { + /** Base CSS class to be applied to the element. Defaults to 'draghandle'. */ + baseClass?: string; +} + +export class DragHandle extends StyledContainerBase { + constructor(config?: DragHandleConfig) { + super(config); + } + + explore(context: RenderingContext, instance: Instance) { + if (isArray(context.dragHandles)) context.dragHandles.push(instance); + super.explore(context, instance); + } + + render(context: RenderingContext, instance: Instance, key: string) { + const { data } = instance; + return ( +
    + {this.renderChildren(context, instance)} +
    + ); + } +} + +DragHandle.prototype.styled = true; +DragHandle.prototype.baseClass = 'draghandle'; + +Widget.alias('draghandle', DragHandle); diff --git a/packages/cx/src/widgets/drag-drop/DragSource.d.ts b/packages/cx/src/widgets/drag-drop/DragSource.d.ts deleted file mode 100644 index 015914a12..000000000 --- a/packages/cx/src/widgets/drag-drop/DragSource.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as Cx from "../../core"; -import { Instance } from "../../ui/Instance"; - -interface DragSourceProps extends Cx.StyledContainerProps { - /** - * Data about the drag source that can be used by drop zones to test if - * drag source is acceptable and to perform drop operations. - */ - data?: any; - - /** - * Set to true to hide the element while being dragged. - * Use if drop zones are configured to expand to indicate where drop will occur. - */ - hideOnDrag?: boolean; - - /** Set to true to indicate that this drag source can be dragged only by using an inner DragHandle. */ - handled?: boolean; - - /** Base CSS class to be applied to the element. Defaults to 'dragsource'. */ - baseClass?: string; - - onDragStart?: (e, instance: Instance) => any; - - onDragEnd?: (e, instance: Instance) => void; - - id?: Cx.StringProp; - - /** Custom contents to be displayed during drag & drop operation. */ - clone?: Cx.Config; - - /** CSS styles to be applied to the clone of the element being dragged. */ - cloneStyle?: Cx.StyleProp; - - /** CSS styles to be applied to the element being dragged. */ - draggedStyle?: Cx.StyleProp; - - /** Additional CSS class to be applied to the clone of the element being dragged. */ - cloneClass?: Cx.ClassProp; - - /** Additional CSS class to be applied to the element being dragged. */ - draggedClass?: Cx.ClassProp; -} - -export class DragSource extends Cx.Widget {} diff --git a/packages/cx/src/widgets/drag-drop/DragSource.js b/packages/cx/src/widgets/drag-drop/DragSource.js deleted file mode 100644 index 12a958c4b..000000000 --- a/packages/cx/src/widgets/drag-drop/DragSource.js +++ /dev/null @@ -1,160 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Container } from "../../ui/Container"; -import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent } from "./ops"; -import { preventFocus } from "../../ui/FocusManager"; -import { parseStyle } from "../../util/parseStyle"; - -export class DragSource extends Container { - init() { - this.cloneStyle = parseStyle(this.cloneStyle); - this.draggedStyle = parseStyle(this.draggedStyle); - super.init(); - } - - declareData() { - super.declareData(...arguments, { - id: undefined, - data: { structured: true }, - cloneStyle: { structured: true }, - cloneClass: { structured: true }, - draggedClass: { structured: true }, - draggedStyle: { structured: true }, - }); - } - - explore(context, instance) { - context.push("dragHandles", (instance.dragHandles = [])); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("dragHandles"); - } - - render(context, instance, key) { - return ( - 0}> - {this.renderChildren(context, instance)} - - ); - } -} - -DragSource.prototype.styled = true; -DragSource.prototype.baseClass = "dragsource"; -DragSource.prototype.hideOnDrag = false; -DragSource.prototype.handled = false; - -Widget.alias("dragsource", DragSource); - -class DragSourceComponent extends VDOM.Component { - constructor(props) { - super(props); - this.state = { dragged: false }; - this.beginDragDrop = this.beginDragDrop.bind(this); - this.onMouseMove = this.onMouseMove.bind(this); - this.onMouseDown = this.onMouseDown.bind(this); - this.setRef = (el) => { - this.el = el; - }; - } - - render() { - let { instance, children, handled } = this.props; - let { data, widget } = instance; - let { CSS } = widget; - - if (this.state.dragged && widget.hideOnDrag) return null; - - let classes = [ - data.classNames, - CSS.state({ - dragged: this.state.dragged, - draggable: !handled, - }), - ]; - - let style = data.style; - - if (this.state.dragged) { - if (data.draggedClass) classes.push(data.draggedClass); - if (data.draggedStyle) - style = { - ...style, - ...data.draggedStyle, - }; - } - - let eventHandlers = { - ...instance.getJsxEventProps(), - onTouchStart: this.onMouseDown, - onMouseDown: this.onMouseDown, - onTouchMove: this.onMouseMove, - onMouseMove: this.onMouseMove, - onTouchEnd: ddMouseUp, - onMouseUp: ddMouseUp, - }; - - delete eventHandlers.onDragStart; - delete eventHandlers.onDragEnd; - - return ( -
    - {children} -
    - ); - } - - onMouseDown(e) { - ddMouseDown(e); - if (isDragHandleEvent(e) || !this.props.handled) { - preventFocus(e); //disables text selection in Firefox - e.stopPropagation(); - } - } - - onMouseMove(e) { - if (ddDetect(e)) { - if (isDragHandleEvent(e) || !this.props.handled) { - this.beginDragDrop(e); - } - } - } - - beginDragDrop(e) { - let { instance } = this.props; - let { data, widget, store } = instance; - - if (widget.onDragStart && instance.invoke("onDragStart", e, instance) === false) return; - - initiateDragDrop( - e, - { - sourceEl: this.el, - source: { - store: store, - data: data.data, - }, - clone: { - widget: widget.clone || widget, - store, - class: data.cloneClass, - style: data.cloneStyle, - cloneContent: !widget.clone, - matchSize: !widget.clone, - matchCursorOffset: !widget.clone, - }, - }, - (e) => { - this.setState({ - dragged: false, - }); - if (widget.onDragEnd) instance.invoke("onDragEnd", e, instance); - } - ); - - this.setState({ - dragged: true, - }); - } -} diff --git a/packages/cx/src/widgets/drag-drop/DragSource.scss b/packages/cx/src/widgets/drag-drop/DragSource.scss index fd878d966..76c8e9f2b 100644 --- a/packages/cx/src/widgets/drag-drop/DragSource.scss +++ b/packages/cx/src/widgets/drag-drop/DragSource.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-dragsource( $name: 'dragsource', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { box-sizing: border-box; diff --git a/packages/cx/src/widgets/drag-drop/DragSource.tsx b/packages/cx/src/widgets/drag-drop/DragSource.tsx new file mode 100644 index 000000000..6142a7aac --- /dev/null +++ b/packages/cx/src/widgets/drag-drop/DragSource.tsx @@ -0,0 +1,237 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM } from "../../ui/Widget"; +import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container"; +import { ddMouseDown, ddDetect, ddMouseUp, initiateDragDrop, isDragHandleEvent } from "./ops"; +import { preventFocus } from "../../ui/FocusManager"; +import { parseStyle } from "../../util/parseStyle"; +import { Instance } from "../../ui/Instance"; +import { StringProp, StyleProp, ClassProp, Config } from "../../ui/Prop"; +import { RenderingContext } from "../../ui/RenderingContext"; + +export interface DragSourceConfig extends StyledContainerConfig { + /** + * Data about the drag source that can be used by drop zones to test if + * drag source is acceptable and to perform drop operations. + */ + data?: any; + + /** + * Set to true to hide the element while being dragged. + * Use if drop zones are configured to expand to indicate where drop will occur. + */ + hideOnDrag?: boolean; + + /** Set to true to indicate that this drag source can be dragged only by using an inner DragHandle. */ + handled?: boolean; + + /** Base CSS class to be applied to the element. Defaults to 'dragsource'. */ + baseClass?: string; + + onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: Instance) => any; + + onDragEnd?: (e: React.MouseEvent | React.TouchEvent, instance: Instance) => void; + + id?: StringProp; + + /** Custom contents to be displayed during drag & drop operation. */ + clone?: Config; + + /** CSS styles to be applied to the clone of the element being dragged. */ + cloneStyle?: StyleProp; + + /** CSS styles to be applied to the element being dragged. */ + draggedStyle?: StyleProp; + + /** Additional CSS class to be applied to the clone of the element being dragged. */ + cloneClass?: ClassProp; + + /** Additional CSS class to be applied to the element being dragged. */ + draggedClass?: ClassProp; +} + +export interface DragSourceInstance extends Instance { + dragHandles: any[]; +} + +export class DragSource extends StyledContainerBase { + declare styled: boolean; + declare baseClass: string; + declare hideOnDrag: boolean; + declare handled: boolean; + declare data: any; + declare clone?: Config; + declare cloneStyle: any; + declare draggedStyle: any; + declare onDragStart?: (e: React.MouseEvent | React.TouchEvent, instance: DragSourceInstance) => any; + declare onDragEnd?: (e: React.MouseEvent | React.TouchEvent, instance: DragSourceInstance) => void; + + constructor(config?: DragSourceConfig) { + super(config); + } + + init() { + this.cloneStyle = parseStyle(this.cloneStyle); + this.draggedStyle = parseStyle(this.draggedStyle); + super.init(); + } + + declareData() { + super.declareData(...arguments, { + id: undefined, + data: { structured: true }, + cloneStyle: { structured: true }, + cloneClass: { structured: true }, + draggedClass: { structured: true }, + draggedStyle: { structured: true }, + }); + } + + explore(context: RenderingContext, instance: DragSourceInstance) { + context.push("dragHandles", (instance.dragHandles = [])); + super.explore(context, instance); + } + + exploreCleanup(context: RenderingContext, instance: DragSourceInstance) { + context.pop("dragHandles"); + } + + render(context: RenderingContext, instance: DragSourceInstance, key: string) { + return ( + 0}> + {this.renderChildren(context, instance)} + + ); + } +} + +DragSource.prototype.baseClass = "dragsource"; +DragSource.prototype.hideOnDrag = false; +DragSource.prototype.handled = false; + +Widget.alias("dragsource", DragSource); + +interface DragSourceComponentProps { + instance: DragSourceInstance; + children?: any; + handled: boolean; +} + +interface DragSourceComponentState { + dragged: boolean; +} + +class DragSourceComponent extends VDOM.Component { + declare el: HTMLElement | null; + + constructor(props: DragSourceComponentProps) { + super(props); + this.state = { dragged: false }; + this.beginDragDrop = this.beginDragDrop.bind(this); + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + } + + setRef = (el: HTMLElement | null) => { + this.el = el; + }; + + render() { + let { instance, children, handled } = this.props; + let { data, widget } = instance; + let { CSS } = widget; + + if (this.state.dragged && widget.hideOnDrag) return null; + + let classes = [ + data.classNames, + CSS.state({ + dragged: this.state.dragged, + draggable: !handled, + }), + ]; + + let style = data.style; + + if (this.state.dragged) { + if (data.draggedClass) classes.push(data.draggedClass); + if (data.draggedStyle) + style = { + ...style, + ...data.draggedStyle, + }; + } + + let eventHandlers: any = { + ...instance.getJsxEventProps(), + onTouchStart: this.onMouseDown, + onMouseDown: this.onMouseDown, + onTouchMove: this.onMouseMove, + onMouseMove: this.onMouseMove, + onTouchEnd: ddMouseUp, + onMouseUp: ddMouseUp, + }; + + delete eventHandlers.onDragStart; + delete eventHandlers.onDragEnd; + + return ( +
    + {children} +
    + ); + } + + onMouseDown(e: React.MouseEvent) { + ddMouseDown(e); + if (isDragHandleEvent(e) || !this.props.handled) { + preventFocus(e); //disables text selection in Firefox + e.stopPropagation(); + } + } + + onMouseMove(e: React.MouseEvent) { + if (ddDetect(e)) { + if (isDragHandleEvent(e) || !this.props.handled) { + this.beginDragDrop(e); + } + } + } + + beginDragDrop(e: React.MouseEvent) { + let { instance } = this.props; + let { data, widget, store } = instance; + + if (widget.onDragStart && instance.invoke("onDragStart", e, instance) === false) return; + + initiateDragDrop( + e, + { + sourceEl: this.el!, + source: { + store: store, + data: data.data, + }, + clone: { + widget: widget.clone || widget, + store, + class: data.cloneClass, + style: data.cloneStyle, + cloneContent: !widget.clone, + matchSize: !widget.clone, + matchCursorOffset: !widget.clone, + }, + }, + (e: any) => { + this.setState({ + dragged: false, + }); + if (widget.onDragEnd) instance.invoke("onDragEnd", e, instance); + }, + ); + + this.setState({ + dragged: true, + }); + } +} diff --git a/packages/cx/src/widgets/drag-drop/DropZone.d.ts b/packages/cx/src/widgets/drag-drop/DropZone.d.ts deleted file mode 100644 index 91d6d6eda..000000000 --- a/packages/cx/src/widgets/drag-drop/DropZone.d.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as Cx from "../../core"; - -import { Instance } from "../../ui/Instance"; -import { DragEvent } from "./ops"; - -interface DropZoneProps extends Cx.StyledContainerProps { - /** CSS styles to be applied when drag cursor is over the drop zone. */ - overStyle?: Cx.StyleProp; - - /** CSS styles to be applied when drag cursor is near the drop zone. */ - nearStyle?: Cx.StyleProp; - - /** CSS styles to be applied when drag operations begin used to highlight drop zones. */ - farStyle?: Cx.StyleProp; - - /** Additional CSS class to be applied when drag cursor is over the drop zone. */ - overClass?: Cx.ClassProp; - - /** Additional CSS class to be applied when drag cursor is near the drop zone. */ - nearClass?: Cx.ClassProp; - - /** Additional CSS class to be applied when drag operations begin used to highlight drop zones. */ - farClass?: Cx.ClassProp; - - /** Distance in `px` used to determine if cursor is near the dropzone. If not configured, cursor is never considered near. */ - nearDistance?: number; - - /** Bindable data related to the DropZone that might be useful inside onDrop operations. */ - data?: Cx.StructuredProp; - - /** - * Inflate the drop zone's bounding box so it activates on cursor proximity. - * Useful for invisible drop-zones that are only a few pixels tall/wide. - */ - inflate?: number; - - /** - * Inflate the drop zone's bounding box horizontally so it activates on cursor proximity. - * Useful for invisible drop-zones that are only a few pixels tall/wide. - */ - hinflate?: number; - - /** - * Inflate the drop zone's bounding box vertically so it activates on cursor proximity. - * Useful for invisible drop-zones that are only a few pixels tall/wide. - */ - vinflate?: number; - - /** Base CSS class to be applied to the element. Defaults to 'dropzone'. */ - baseClass?: string; - - /** A callback method invoked when dragged item is finally dropped. - The callback takes two arguments: - * dragDropEvent - An object containing information related to the source - * instance - Return value is written into dragDropEvent.result and can be passed - to the source's onDragEnd callback. */ - onDrop?: string | ((event?: DragEvent, instance?: Instance) => any); - - /** A callback method used to test if dragged item (source) is compatible - with the drop zone. */ - onDropTest?: string | ((event?: DragEvent, instance?: Instance) => boolean); - - /** A callback method invoked when the dragged item gets close to the drop zone. - See also `nearDistance`. */ - onDragNear?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when the dragged item is dragged away. */ - onDragAway?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when the dragged item is dragged over the drop zone. - The callback is called for each `mousemove` or `touchmove` event. */ - onDragOver?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when the dragged item is dragged over the drop zone - for the first time. */ - onDragEnter?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when the dragged item leaves the drop zone area. */ - onDragLeave?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when at the beginning of the drag & drop operation. */ - onDragStart?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** A callback method invoked when at the end of the drag & drop operation. */ - onDragEnd?: string | ((event?: DragEvent, instance?: Instance) => void); - - /** Match height of the item being dragged */ - matchHeight?: boolean; - - /** Match width of the item being dragged */ - matchWidth?: boolean; - - /** Match margin of the item being dragged */ - matchMargin?: boolean; -} - -export class DropZone extends Cx.Widget {} diff --git a/packages/cx/src/widgets/drag-drop/DropZone.js b/packages/cx/src/widgets/drag-drop/DropZone.js deleted file mode 100644 index ce353f57d..000000000 --- a/packages/cx/src/widgets/drag-drop/DropZone.js +++ /dev/null @@ -1,214 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Container } from "../../ui/Container"; -import { parseStyle } from "../../util/parseStyle"; -import { registerDropZone, DragDropContext } from "./ops"; -import { findScrollableParent } from "../../util/findScrollableParent"; -import { isNumber } from "../../util/isNumber"; -import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; - -export class DropZone extends Container { - init() { - this.overStyle = parseStyle(this.overStyle); - this.nearStyle = parseStyle(this.nearStyle); - this.farStyle = parseStyle(this.farStyle); - - if (isNumber(this.inflate)) { - this.hinflate = this.inflate; - this.vinflate = this.inflate; - } - - super.init(); - } - - declareData() { - return super.declareData(...arguments, { - overClass: { structured: true }, - nearClass: { structured: true }, - farClass: { structured: true }, - overStyle: { structured: true }, - nearStyle: { structured: true }, - farStyle: { structured: true }, - data: { structured: true }, - }); - } - - render(context, instance, key) { - return ( - - {this.renderChildren(context, instance)} - - ); - } -} - -DropZone.prototype.styled = true; -DropZone.prototype.nearDistance = 0; -DropZone.prototype.hinflate = 0; -DropZone.prototype.vinflate = 0; -DropZone.prototype.baseClass = "dropzone"; - -Widget.alias("dropzone", DropZone); - -class DropZoneComponent extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - state: false, - }; - } - - render() { - let { instance, children } = this.props; - let { data, widget } = instance; - let { CSS } = widget; - - let classes = [data.classNames, CSS.state(this.state.state)]; - - let stateStyle; - - switch (this.state.state) { - case "over": - classes.push(data.overClass); - stateStyle = parseStyle(data.overStyle); - break; - case "near": - classes.push(data.nearClass); - stateStyle = parseStyle(data.nearStyle); - break; - case "far": - classes.push(data.farClass); - stateStyle = parseStyle(data.farStyle); - break; - } - - return ( -
    { - this.el = el; - }} - > - {children} -
    - ); - } - - componentDidMount() { - let dragDropOptions = this.context; - let disabled = dragDropOptions && dragDropOptions.disabled; - if (!disabled) this.unregister = registerDropZone(this); - } - - componentWillUnmount() { - this.unregister && this.unregister(); - } - - onDropTest(e) { - let { instance } = this.props; - let { widget } = instance; - return !widget.onDropTest || instance.invoke("onDropTest", e, instance); - } - - onDragStart(e) { - this.setState({ - state: "far", - }); - } - - onDragNear(e) { - this.setState({ - state: "near", - }); - } - - onDragAway(e) { - this.setState({ - state: "far", - }); - } - - onDragLeave(e) { - let { nearDistance } = this.props.instance.widget; - this.setState({ - state: nearDistance ? "near" : "far", - style: null, - }); - } - - onDragMeasure(e) { - let rect = getTopLevelBoundingClientRect(this.el); - - let { instance } = this.props; - let { widget } = instance; - - let { clientX, clientY } = e.cursor; - let distance = - Math.max(0, rect.left - clientX, clientX - rect.right) + - Math.max(0, rect.top - clientY, clientY - rect.bottom); - - if (widget.hinflate > 0) { - rect.left -= widget.hinflate; - rect.right += widget.hinflate; - } - - if (widget.vinflate > 0) { - rect.top -= widget.vinflate; - rect.bottom += widget.vinflate; - } - - let { nearDistance } = widget; - - let over = rect.left <= clientX && clientX < rect.right && rect.top <= clientY && clientY < rect.bottom; - - return { - over: - over && Math.abs(clientX - (rect.left + rect.right) / 2) + Math.abs(clientY - (rect.top + rect.bottom) / 2), - near: nearDistance && (over || distance < nearDistance), - }; - } - - onDragEnter(e) { - let { instance } = this.props; - let { widget } = instance; - let style = {}; - - if (widget.matchWidth) style.width = `${e.source.width}px`; - - if (widget.matchHeight) style.height = `${e.source.height}px`; - - if (widget.matchMargin) style.margin = e.source.margin.join(" "); - - if (this.state != "over") - this.setState({ - state: "over", - style, - }); - } - - onDragOver(e) {} - - onGetHScrollParent() { - return findScrollableParent(this.el, true); - } - - onGetVScrollParent() { - return findScrollableParent(this.el); - } - - onDrop(e) { - let { instance } = this.props; - let { widget } = instance; - - if (this.state.state == "over" && widget.onDrop) instance.invoke("onDrop", e, instance); - } - - onDragEnd(e) { - this.setState({ - state: false, - style: null, - }); - } -} - -DropZoneComponent.contextType = DragDropContext; diff --git a/packages/cx/src/widgets/drag-drop/DropZone.scss b/packages/cx/src/widgets/drag-drop/DropZone.scss index 401ee63d9..5061799d0 100644 --- a/packages/cx/src/widgets/drag-drop/DropZone.scss +++ b/packages/cx/src/widgets/drag-drop/DropZone.scss @@ -1,11 +1,13 @@ +@use "sass:map"; + @mixin cx-dropzone( $name: 'dropzone', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { box-sizing: border-box; diff --git a/packages/cx/src/widgets/drag-drop/DropZone.tsx b/packages/cx/src/widgets/drag-drop/DropZone.tsx new file mode 100644 index 000000000..8743f2ae3 --- /dev/null +++ b/packages/cx/src/widgets/drag-drop/DropZone.tsx @@ -0,0 +1,353 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM } from "../../ui/Widget"; +import { ContainerBase, ContainerConfig, StyledContainerBase, StyledContainerConfig } from "../../ui/Container"; +import { parseStyle } from "../../util/parseStyle"; +import { registerDropZone, DragDropContext, DragEvent } from "./ops"; +import { findScrollableParent } from "../../util/findScrollableParent"; +import { isNumber } from "../../util/isNumber"; +import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; +import { Instance } from "../../ui/Instance"; +import { StyleProp, ClassProp, StructuredProp } from "../../ui/Prop"; +import { RenderingContext } from "../../ui/RenderingContext"; + +export interface DropZoneConfig extends StyledContainerConfig { + /** CSS styles to be applied when drag cursor is over the drop zone. */ + overStyle?: StyleProp; + + /** CSS styles to be applied when drag cursor is near the drop zone. */ + nearStyle?: StyleProp; + + /** CSS styles to be applied when drag operations begin used to highlight drop zones. */ + farStyle?: StyleProp; + + /** Additional CSS class to be applied when drag cursor is over the drop zone. */ + overClass?: ClassProp; + + /** Additional CSS class to be applied when drag cursor is near the drop zone. */ + nearClass?: ClassProp; + + /** Additional CSS class to be applied when drag operations begin used to highlight drop zones. */ + farClass?: ClassProp; + + /** Distance in `px` used to determine if cursor is near the dropzone. If not configured, cursor is never considered near. */ + nearDistance?: number; + + /** Bindable data related to the DropZone that might be useful inside onDrop operations. */ + data?: StructuredProp; + + /** + * Inflate the drop zone's bounding box so it activates on cursor proximity. + * Useful for invisible drop-zones that are only a few pixels tall/wide. + */ + inflate?: number; + + /** + * Inflate the drop zone's bounding box horizontally so it activates on cursor proximity. + * Useful for invisible drop-zones that are only a few pixels tall/wide. + */ + hinflate?: number; + + /** + * Inflate the drop zone's bounding box vertically so it activates on cursor proximity. + * Useful for invisible drop-zones that are only a few pixels tall/wide. + */ + vinflate?: number; + + /** Base CSS class to be applied to the element. Defaults to 'dropzone'. */ + baseClass?: string; + + /** A callback method invoked when dragged item is finally dropped. + The callback takes two arguments: + * dragDropEvent - An object containing information related to the source + * instance + Return value is written into dragDropEvent.result and can be passed + to the source's onDragEnd callback. */ + onDrop?: string | ((event?: DragEvent, instance?: Instance) => any); + + /** A callback method used to test if dragged item (source) is compatible + with the drop zone. */ + onDropTest?: string | ((event?: DragEvent, instance?: Instance) => boolean); + + /** A callback method invoked when the dragged item gets close to the drop zone. + See also `nearDistance`. */ + onDragNear?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when the dragged item is dragged away. */ + onDragAway?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when the dragged item is dragged over the drop zone. + The callback is called for each `mousemove` or `touchmove` event. */ + onDragOver?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when the dragged item is dragged over the drop zone + for the first time. */ + onDragEnter?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when the dragged item leaves the drop zone area. */ + onDragLeave?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when at the beginning of the drag & drop operation. */ + onDragStart?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** A callback method invoked when at the end of the drag & drop operation. */ + onDragEnd?: string | ((event?: DragEvent, instance?: Instance) => void); + + /** Match height of the item being dragged */ + matchHeight?: boolean; + + /** Match width of the item being dragged */ + matchWidth?: boolean; + + /** Match margin of the item being dragged */ + matchMargin?: boolean; +} + +export interface DropZoneInstance extends Instance {} + +export class DropZone extends StyledContainerBase { + declare styled: boolean; + declare nearDistance: number; + declare hinflate: number; + declare vinflate: number; + declare baseClass: string; + declare overStyle: any; + declare nearStyle: any; + declare farStyle: any; + declare inflate?: number; + declare matchHeight?: boolean; + declare matchWidth?: boolean; + declare matchMargin?: boolean; + declare onDrop?: string | ((event?: DragEvent, instance?: Instance) => any); + declare onDropTest?: string | ((event?: DragEvent, instance?: Instance) => boolean); + declare onDragNear?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragAway?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragOver?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragEnter?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragLeave?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragStart?: string | ((event?: DragEvent, instance?: Instance) => void); + declare onDragEnd?: string | ((event?: DragEvent, instance?: Instance) => void); + + constructor(config?: DropZoneConfig) { + super(config); + } + + init() { + this.overStyle = parseStyle(this.overStyle); + this.nearStyle = parseStyle(this.nearStyle); + this.farStyle = parseStyle(this.farStyle); + + if (isNumber(this.inflate)) { + this.hinflate = this.inflate; + this.vinflate = this.inflate; + } + + super.init(); + } + + declareData() { + return super.declareData(...arguments, { + overClass: { structured: true }, + nearClass: { structured: true }, + farClass: { structured: true }, + overStyle: { structured: true }, + nearStyle: { structured: true }, + farStyle: { structured: true }, + data: { structured: true }, + }); + } + + render(context: RenderingContext, instance: DropZoneInstance, key: string) { + return ( + + {this.renderChildren(context, instance)} + + ); + } +} + +DropZone.prototype.nearDistance = 0; +DropZone.prototype.hinflate = 0; +DropZone.prototype.vinflate = 0; +DropZone.prototype.baseClass = "dropzone"; + +Widget.alias("dropzone", DropZone); + +interface DropZoneComponentProps { + instance: DropZoneInstance; + children?: any; +} + +interface DropZoneComponentState { + state: boolean | string; + style?: any; +} + +class DropZoneComponent extends VDOM.Component { + el: HTMLElement | null = null; + unregister?: () => void; + declare context: any; + + constructor(props: DropZoneComponentProps) { + super(props); + this.state = { + state: false, + }; + } + + render() { + let { instance, children } = this.props; + let { data, widget } = instance; + let { CSS } = widget; + + let classes = [data.classNames, CSS.state(this.state.state)]; + + let stateStyle; + + switch (this.state.state) { + case "over": + classes.push(data.overClass); + stateStyle = parseStyle(data.overStyle); + break; + case "near": + classes.push(data.nearClass); + stateStyle = parseStyle(data.nearStyle); + break; + case "far": + classes.push(data.farClass); + stateStyle = parseStyle(data.farStyle); + break; + } + + return ( +
    { + this.el = el; + }} + > + {children} +
    + ); + } + + componentDidMount() { + let dragDropOptions = this.context; + let disabled = dragDropOptions && dragDropOptions.disabled; + if (!disabled) this.unregister = registerDropZone(this); + } + + componentWillUnmount() { + this.unregister && this.unregister(); + } + + onDropTest(e: DragEvent) { + let { instance } = this.props; + let { widget } = instance; + return !widget.onDropTest || instance.invoke("onDropTest", e, instance); + } + + onDragStart(e: DragEvent) { + this.setState({ + state: "far", + }); + } + + onDragNear(e: DragEvent) { + this.setState({ + state: "near", + }); + } + + onDragAway(e: DragEvent) { + this.setState({ + state: "far", + }); + } + + onDragLeave(e: DragEvent) { + let { nearDistance } = this.props.instance.widget; + this.setState({ + state: nearDistance ? "near" : "far", + style: null, + }); + } + + onDragMeasure(e: DragEvent) { + let rectOrig = getTopLevelBoundingClientRect(this.el!); + let rect = { left: rectOrig.left, right: rectOrig.right, top: rectOrig.top, bottom: rectOrig.bottom }; + + let { instance } = this.props; + let { widget } = instance; + + let { clientX, clientY } = e.cursor; + let distance = + Math.max(0, rect.left - clientX, clientX - rect.right) + + Math.max(0, rect.top - clientY, clientY - rect.bottom); + + if (widget.hinflate > 0) { + rect.left -= widget.hinflate; + rect.right += widget.hinflate; + } + + if (widget.vinflate > 0) { + rect.top -= widget.vinflate; + rect.bottom += widget.vinflate; + } + + let { nearDistance } = widget; + + let over = rect.left <= clientX && clientX < rect.right && rect.top <= clientY && clientY < rect.bottom; + + return { + over: + over && Math.abs(clientX - (rect.left + rect.right) / 2) + Math.abs(clientY - (rect.top + rect.bottom) / 2), + near: nearDistance && (over || distance < nearDistance), + }; + } + + onDragEnter(e: DragEvent) { + let { instance } = this.props; + let { widget } = instance; + let style: any = {}; + + if (widget.matchWidth) style.width = `${e.source.width}px`; + + if (widget.matchHeight) style.height = `${e.source.height}px`; + + if (widget.matchMargin) style.margin = e.source.margin.join(" "); + + if (this.state.state != "over") + this.setState({ + state: "over", + style, + }); + } + + onDragOver(e: DragEvent) {} + + onGetHScrollParent() { + return findScrollableParent(this.el!, true); + } + + onGetVScrollParent() { + return findScrollableParent(this.el!); + } + + onDrop(e: DragEvent) { + let { instance } = this.props; + let { widget } = instance; + + if (this.state.state == "over" && widget.onDrop) instance.invoke("onDrop", e, instance); + } + + onDragEnd(e: DragEvent) { + this.setState({ + state: false, + style: null, + }); + } +} + +DropZoneComponent.contextType = DragDropContext as any; diff --git a/packages/cx/src/widgets/drag-drop/index.js b/packages/cx/src/widgets/drag-drop/index.js deleted file mode 100644 index 122ffd892..000000000 --- a/packages/cx/src/widgets/drag-drop/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./DragSource"; -export * from "./DragHandle"; -export * from "./DropZone"; -export * from "./ops"; diff --git a/packages/cx/src/widgets/drag-drop/index.d.ts b/packages/cx/src/widgets/drag-drop/index.ts similarity index 100% rename from packages/cx/src/widgets/drag-drop/index.d.ts rename to packages/cx/src/widgets/drag-drop/index.ts diff --git a/packages/cx/src/widgets/drag-drop/ops.d.ts b/packages/cx/src/widgets/drag-drop/ops.d.ts deleted file mode 100644 index c0cd5bbbf..000000000 --- a/packages/cx/src/widgets/drag-drop/ops.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as Cx from '../../core'; -import * as React from 'react'; -import { CursorPosition } from '../overlay/captureMouse'; -import { View } from '../../data/View'; - -export interface DragEvent { - type: 'dragstart' | 'dragmove' | 'dragdrop', - event: React.SyntheticEvent, - cursor: CursorPosition, - source: { - width: number, - height: number, - margin: string[], - data?: any, - store: View, - [other: string]: any, - }, - result?: any -} - -interface DragDropOptions { - sourceEl?: Element, - clone?: any, - source?: Cx.Config, -} - -type DragEventHandler = (e: DragEvent) => void; - -export interface IDropZone { - onDropTest?: (e: DragEvent) => boolean; - onDragStart?: DragEventHandler; - onDragAway?: DragEventHandler; - onDragEnd?: DragEventHandler; - onDragMeasure?: (e: DragEvent) => { over: boolean, near: boolean }; - onDragLeave?: DragEventHandler; - onDragOver?: DragEventHandler; - onDragEnter?: DragEventHandler; - onDrop?: DragEventHandler; -} - -type UnregisterFunction = () => void; - -/** Register a drop zone. Return value is a function that can be used to unregister the drop zone. */ -export function registerDropZone(dropZone: IDropZone) : UnregisterFunction; - -/** Initiate a drag-drop operation. */ -export function initiateDragDrop(e: DragEvent, options?: DragDropOptions, onDragEnd?: (e?: DragEvent) => void) : void; - -export function ddMouseDown(e: React.SyntheticEvent) : void; - -export function ddMouseUp() : void; - -export function ddDetect(e: React.SyntheticEvent) : void | true; - -export function ddHandle(e: React.SyntheticEvent) : void; - -export function isDragHandleEvent(e: React.SyntheticEvent) : boolean; \ No newline at end of file diff --git a/packages/cx/src/widgets/drag-drop/ops.js b/packages/cx/src/widgets/drag-drop/ops.js deleted file mode 100644 index 9e16dddd5..000000000 --- a/packages/cx/src/widgets/drag-drop/ops.js +++ /dev/null @@ -1,344 +0,0 @@ -import { SubscriberList } from "../../util/SubscriberList"; -import { getCursorPos, captureMouseOrTouch } from "../overlay/captureMouse"; -import { startAppLoop } from "../../ui/app/startAppLoop"; -import { getScrollerBoundingClientRect } from "../../util/getScrollerBoundingClientRect"; -import { isNumber } from "../../util/isNumber"; -import { isObject } from "../../util/isObject"; -import { isString } from "../../util/isString"; -import { ZIndexManager } from "../../ui/ZIndexManager"; -import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; -import { VDOM } from "../../ui/VDOM"; -import { Container } from "../../ui/Container"; -import { Console } from "../../util"; - -let dropZones = new SubscriberList(), - dragStartedZones, - activeZone, - nearZones, - puppet, - scrollTimer, - vscrollParent, - hscrollParent; - -export function registerDropZone(dropZone) { - return dropZones.subscribe(dropZone); -} - -export function initiateDragDrop(e, options = {}, onDragEnd) { - if (puppet) { - //last operation didn't finish properly - notifyDragDrop(e); - } - - let sourceEl = options.sourceEl || e.currentTarget; - let sourceBounds = getTopLevelBoundingClientRect(sourceEl); - let cursor = getCursorPos(e); - - let clone = { - ...options.clone, - }; - - let cloneEl = document.createElement("div"); - cloneEl.classList.add("cxb-dragclone"); - if (isString(clone["class"])) cloneEl.classList.add(clone["class"]); - if (isObject(clone.style)) Object.assign(cloneEl.style, clone.style); - cloneEl.style.left = `-1000px`; - cloneEl.style.top = `-1000px`; - - if (clone.matchSize || clone.matchWidth) cloneEl.style.width = `${Math.ceil(sourceBounds.width)}px`; - - if (clone.matchSize || clone.matchHeight) cloneEl.style.height = `${Math.ceil(sourceBounds.height)}px`; - - cloneEl.style.zIndex = ZIndexManager.next() + 1000; - - if (clone.cloneContent) { - cloneEl.appendChild(sourceEl.cloneNode(true)); - } - - document.body.appendChild(cloneEl); - - let styles = getComputedStyle(sourceEl); - - let deltaX = clone.matchCursorOffset ? cursor.clientX - sourceBounds.left : -3; - let deltaY = clone.matchCursorOffset ? cursor.clientY - sourceBounds.top : -3; - - let source = { - ...options.source, - width: sourceBounds.width, - height: sourceBounds.height, - deltaX, - deltaY, - margin: [ - styles.getPropertyValue("margin-top"), - styles.getPropertyValue("margin-right"), - styles.getPropertyValue("margin-bottom"), - styles.getPropertyValue("margin-left"), - ], - }; - - puppet = { - deltaX, - deltaY, - el: cloneEl, - clone, - source, - onDragEnd, - }; - - if (clone.widget && clone.store && !clone.cloneContent) { - let content = ( - - {clone.widget} - - ); - puppet.stop = startAppLoop(cloneEl, clone.store, content, { - removeParentDOMElement: true, - }); - } else { - puppet.stop = () => { - document.body.removeChild(cloneEl); - }; - } - - let event = getDragEvent(e, "dragstart"); - - dragStartedZones = new WeakMap(); - - dropZones.execute((zone) => { - if (zone.onDropTest) - try { - if (!zone.onDropTest(event)) return; - } - catch (err) { - Console.warn("Drop zone onDropTest failed. Error: ", err, zone); - return; - } - - if (zone.onDragStart) zone.onDragStart(event); - - dragStartedZones.set(zone, true); - }); - - notifyDragMove(e); - - captureMouseOrTouch(e, notifyDragMove, notifyDragDrop); -} - -function notifyDragMove(e, captureData) { - let event = getDragEvent(e, "dragmove"); - let over = null, - overTest = null, - best = null; - - let near = [], - away = []; - - dropZones.execute((zone) => { - let test; - try { - test = zone.onDropTest && zone.onDropTest(event); - if (!test) return; - } - catch (err) { - //the problem is already reported, so here we just swallow the bug to avoid spammming the console too much - return; - } - - if (zone.onDragMeasure) { - let result = zone.onDragMeasure(event, { test }) || {}; - - if (result.near) near.push(zone); - else away.push(zone); - - if (isNumber(result.over) && (best == null || result.over < best)) { - over = zone; - overTest = test; - best = result.over; - } - } - }); - - let newNear = new WeakMap(); - - if (nearZones != null) { - away.forEach((z) => { - if (z.onDragAway && nearZones.has(z)) z.onDragAway(z); - }); - } - - near.forEach((z) => { - if (z.onDragNear && z != over && (nearZones == null || !nearZones.has(z))) { - z.onDragNear(z); - newNear.set(z, true); - } - }); - - nearZones = newNear; - - if (over != activeZone) { - vscrollParent = null; - hscrollParent = null; - } - - if (over != activeZone && activeZone && activeZone.onDragLeave) activeZone.onDragLeave(event); - - if (over != activeZone && over) { - if (over.onDragEnter) over.onDragEnter(event); - - vscrollParent = over.onGetVScrollParent && over.onGetVScrollParent({ test: overTest }); - hscrollParent = over.onGetHScrollParent && over.onGetHScrollParent({ test: overTest }); - } - - activeZone = over; - - if (over && over.onDragOver) { - over.onDragOver(event, { test: overTest }); - } - - //do it last to avoid forced redraw if nothing changed - let cursor = getCursorPos(e); - puppet.el.style.left = `${cursor.clientX - puppet.deltaX}px`; - puppet.el.style.top = `${cursor.clientY - puppet.deltaY}px`; - - if (vscrollParent || hscrollParent) { - let scrollX = 0, - scrollY = 0; - let vscrollBounds = vscrollParent && getScrollerBoundingClientRect(vscrollParent, true); - let hscrollBounds = - hscrollParent == vscrollParent - ? vscrollBounds - : hscrollParent && getScrollerBoundingClientRect(hscrollParent, true); - - if (vscrollBounds) { - if (cursor.clientY < vscrollBounds.top + 20) scrollY = -1; - else if (cursor.clientY >= vscrollBounds.bottom - 20) scrollY = 1; - } - - if (hscrollBounds) { - if (cursor.clientX < hscrollBounds.left + 20) scrollX = -1; - else if (cursor.clientX >= hscrollBounds.right - 20) scrollX = 1; - } - - if (scrollY || scrollX) { - if (!scrollTimer) { - let cb = () => { - if (scrollY) { - let current = vscrollParent.scrollTop; - let next = Math.min( - vscrollParent.scrollHeight, - Math.max(0, current + (scrollY * 5 * Math.min(200, Math.max(50, event.source.height))) / 60) - ); //60 FPS - vscrollParent.scrollTop = next; - } - if (scrollX) { - let current = hscrollParent.scrollLeft; - let next = Math.min( - hscrollParent.scrollWidth, - Math.max(0, current + (scrollX * 5 * Math.min(200, Math.max(50, event.source.width))) / 60) - ); //60 FPS - hscrollParent.scrollLeft = next; - } - scrollTimer = requestAnimationFrame(cb); - }; - scrollTimer = requestAnimationFrame(cb); - } - } else { - clearScrollTimer(); - } - } else clearScrollTimer(); -} - -function clearScrollTimer() { - if (scrollTimer) { - cancelAnimationFrame(scrollTimer); - scrollTimer = null; - } -} - -function notifyDragDrop(e) { - clearScrollTimer(); - - let event = getDragEvent(e, "dragdrop"); - - if (puppet.stop) puppet.stop(); - - if (activeZone && activeZone.onDrop) event.result = activeZone.onDrop(event); - - dropZones.execute((zone) => { - if (nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(e); - - if (!dragStartedZones.has(zone)) return; - - if (zone.onDragEnd) zone.onDragEnd(event); - }); - - if (puppet.onDragEnd) puppet.onDragEnd(event); - - nearZones = null; - activeZone = null; - puppet = null; - dragStartedZones = null; -} - -function getDragEvent(e, type) { - return { - type: type, - event: e, - cursor: getCursorPos(e), - source: puppet.source, - }; -} - -let dragCandidate = {}; - -export function ddMouseDown(e) { - //do not allow that the same event is processed by multiple drag sources - //the first (top-level) source should be a drag-candidate - if (e.timeStamp <= dragCandidate.timeStamp) return; - - dragCandidate = { - el: e.currentTarget, - start: { ...getCursorPos(e) }, - timeStamp: e.timeStamp - }; -} - -export function ddMouseUp() { - dragCandidate = {}; -} - -export function ddDetect(e) { - let cursor = getCursorPos(e); - if ( - e.currentTarget == dragCandidate.el && - Math.abs(cursor.clientX - dragCandidate.start.clientX) + Math.abs(cursor.clientY - dragCandidate.start.clientY) >= - 2 - ) { - dragCandidate = {}; - return true; - } -} - -let lastDragHandle; - -export function ddHandle(e) { - lastDragHandle = e.currentTarget; -} - -export function isDragHandleEvent(e) { - return lastDragHandle && (e.target == lastDragHandle || lastDragHandle.contains(e.target)); -} - -export const DragDropContext = VDOM.createContext - ? VDOM.createContext({ disabled: false }) - : ({ children }) => children; - -class ContextWrap extends Container { - render(context, instance, key) { - return ( - - {this.renderChildren(context, instance)} - - ); - } -} diff --git a/packages/cx/src/widgets/drag-drop/ops.tsx b/packages/cx/src/widgets/drag-drop/ops.tsx new file mode 100644 index 000000000..9c8c1a4fe --- /dev/null +++ b/packages/cx/src/widgets/drag-drop/ops.tsx @@ -0,0 +1,422 @@ +/** @jsxImportSource react */ +import { getCursorPos, captureMouseOrTouch, CursorPosition } from "../overlay/captureMouse"; +import { startAppLoop } from "../../ui/app/startAppLoop"; +import { View } from "../../data/View"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; + +export interface DragEvent { + type: "dragstart" | "dragmove" | "dragdrop"; + event: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent; + cursor: CursorPosition; + source: { + width: number; + height: number; + margin: string[]; + data?: any; + store: View; + [other: string]: any; + }; + dataTransfer?: DataTransfer; + result?: any; +} + +export interface DragDropOptions { + sourceEl?: Element | null; + clone?: any; + source?: any; +} + +export interface DragDropOperationContext { + test: any; +} + +export type DragEventHandler = (e: DragEvent) => void; + +export interface IDropZone { + onDropTest?: (e: DragEvent) => boolean; + onDragStart?: DragEventHandler; + onDragAway?: DragEventHandler; + onDragNear?: DragEventHandler; + onDragEnd?: DragEventHandler; + onDragMeasure?: ( + e: DragEvent, + operation: DragDropOperationContext, + ) => { over: number | false; near: number | boolean } | false | undefined; + onDragLeave?: DragEventHandler; + onDragOver?: (e: DragEvent, operation: DragDropOperationContext) => void; + onDragEnter?: DragEventHandler; + onDrop?: DragEventHandler; + onGetVScrollParent?: (operation: DragDropOperationContext) => Element | null; + onGetHScrollParent?: (operation: DragDropOperationContext) => Element | null; +} + +export type UnregisterFunction = () => void; + +import { getScrollerBoundingClientRect } from "../../util/getScrollerBoundingClientRect"; +import { isNumber } from "../../util/isNumber"; +import { isObject } from "../../util/isObject"; +import { isString } from "../../util/isString"; +import { ZIndexManager } from "../../ui/ZIndexManager"; +import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; +import { VDOM } from "../../ui/VDOM"; +import { Container } from "../../ui/Container"; +import { Console } from "../../util"; +import { WidgetConfig } from "../../ui"; + +interface Puppet { + deltaX: number; + deltaY: number; + el: HTMLDivElement; + clone: any; + source: any; + onDragEnd?: (e?: DragEvent) => void; + stop?: () => void; +} + +let dropZones: IDropZone[] = []; +let dragStartedZones: WeakMap | null = null; +let activeZone: IDropZone | null = null; +let nearZones: WeakMap | null = null; +let puppet: Puppet | null = null; +let scrollTimer: number | null = null; +let vscrollParent: Element | null = null; +let hscrollParent: Element | null = null; + +export function registerDropZone(dropZone: IDropZone): UnregisterFunction { + dropZones.push(dropZone); + return () => { + let index = dropZones.indexOf(dropZone); + if (index !== -1) dropZones.splice(index, 1); + }; +} + +export function initiateDragDrop( + e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent, + options: DragDropOptions = {}, + onDragEnd?: (e?: DragEvent) => void, +): void { + if (puppet) { + //last operation didn't finish properly + notifyDragDrop(e); + } + + let sourceEl = options.sourceEl || (e.currentTarget as Element); + let sourceBounds = getTopLevelBoundingClientRect(sourceEl); + let cursor = getCursorPos(e); + + let clone = { + ...options.clone, + }; + + let cloneEl = document.createElement("div"); + cloneEl.classList.add("cxb-dragclone"); + if (isString(clone["class"])) cloneEl.classList.add(clone["class"]); + if (isObject(clone.style)) Object.assign(cloneEl.style, clone.style); + cloneEl.style.left = `-1000px`; + cloneEl.style.top = `-1000px`; + + if (clone.matchSize || clone.matchWidth) cloneEl.style.width = `${Math.ceil(sourceBounds.width)}px`; + + if (clone.matchSize || clone.matchHeight) cloneEl.style.height = `${Math.ceil(sourceBounds.height)}px`; + + cloneEl.style.zIndex = String(ZIndexManager.next() + 1000); + + if (clone.cloneContent) { + cloneEl.appendChild(sourceEl.cloneNode(true)); + } + + document.body.appendChild(cloneEl); + + let styles = getComputedStyle(sourceEl); + + let deltaX = clone.matchCursorOffset ? cursor.clientX - sourceBounds.left : -3; + let deltaY = clone.matchCursorOffset ? cursor.clientY - sourceBounds.top : -3; + + let source = { + ...options.source, + width: sourceBounds.width, + height: sourceBounds.height, + deltaX, + deltaY, + margin: [ + styles.getPropertyValue("margin-top"), + styles.getPropertyValue("margin-right"), + styles.getPropertyValue("margin-bottom"), + styles.getPropertyValue("margin-left"), + ], + }; + + puppet = { + deltaX, + deltaY, + el: cloneEl, + clone, + source, + onDragEnd, + }; + + if (clone.widget && clone.store && !clone.cloneContent) { + let content = { $type: ContextWrap, value: { disabled: true }, children: clone.widget }; + puppet.stop = startAppLoop(cloneEl, clone.store, content, { + removeParentDOMElement: true, + }); + } else { + puppet.stop = () => { + document.body.removeChild(cloneEl); + }; + } + + let event = getDragEvent(e, "dragstart"); + + dragStartedZones = new WeakMap(); + + dropZones.forEach((zone) => { + if (zone.onDropTest) + try { + if (!zone.onDropTest(event)) return; + } catch (err) { + Console.warn("Drop zone onDropTest failed. Error: ", err, zone); + return; + } + + if (zone.onDragStart) zone.onDragStart(event); + + dragStartedZones!.set(zone, true); + }); + + notifyDragMove(e, null); + + captureMouseOrTouch(e, notifyDragMove, notifyDragDrop); +} + +function notifyDragMove(e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, _captureData: any): void { + let event = getDragEvent(e, "dragmove"); + let over: IDropZone | null = null, + overTest: any = null, + best: number | null = null; + + let near: IDropZone[] = [], + away: IDropZone[] = []; + + for (let zone of dropZones) { + let test; + try { + test = zone.onDropTest && zone.onDropTest(event); + if (!test) continue; + } catch (err) { + //the problem is already reported, so here we just swallow the bug to avoid spammming the console too much + continue; + } + + if (zone.onDragMeasure) { + let result = zone.onDragMeasure(event, { test }); + if (!result) continue; + + if (result.near) near.push(zone); + else away.push(zone); + + if (isNumber(result.over) && (best == null || result.over < best)) { + over = zone; + overTest = test; + best = result.over; + } + } + } + + let newNear = new WeakMap(); + + if (nearZones != null) { + away.forEach((z) => { + if (z.onDragAway && nearZones!.has(z)) z.onDragAway(event); + }); + } + + near.forEach((z) => { + if (z.onDragNear && z != over && (nearZones == null || !nearZones.has(z))) { + z.onDragNear(event); + newNear.set(z, true); + } + }); + + nearZones = newNear; + + if (over != activeZone) { + vscrollParent = null; + hscrollParent = null; + } + + if (over != activeZone && activeZone && activeZone.onDragLeave) activeZone.onDragLeave(event); + + if (over != activeZone && over) { + if (over.onDragEnter) over.onDragEnter(event); + + vscrollParent = (over.onGetVScrollParent && over.onGetVScrollParent({ test: overTest })) || null; + hscrollParent = (over.onGetHScrollParent && over.onGetHScrollParent({ test: overTest })) || null; + } + + activeZone = over; + + if (over && over.onDragOver) { + over.onDragOver(event, { test: overTest }); + } + + //do it last to avoid forced redraw if nothing changed + let cursor = getCursorPos(e); + puppet!.el.style.left = `${cursor.clientX - puppet!.deltaX}px`; + puppet!.el.style.top = `${cursor.clientY - puppet!.deltaY}px`; + + if (vscrollParent || hscrollParent) { + let scrollX = 0, + scrollY = 0; + let vscrollBounds = vscrollParent && getScrollerBoundingClientRect(vscrollParent, true); + let hscrollBounds = + hscrollParent == vscrollParent + ? vscrollBounds + : hscrollParent && getScrollerBoundingClientRect(hscrollParent, true); + + if (vscrollBounds) { + if (cursor.clientY < vscrollBounds.top + 20) scrollY = -1; + else if (cursor.clientY >= vscrollBounds.bottom - 20) scrollY = 1; + } + + if (hscrollBounds) { + if (cursor.clientX < hscrollBounds.left + 20) scrollX = -1; + else if (cursor.clientX >= hscrollBounds.right - 20) scrollX = 1; + } + + if (scrollY || scrollX) { + if (!scrollTimer) { + let cb = () => { + if (scrollY) { + let current = vscrollParent!.scrollTop; + let next = Math.min( + vscrollParent!.scrollHeight, + Math.max(0, current + (scrollY * 5 * Math.min(200, Math.max(50, event.source.height))) / 60), + ); //60 FPS + vscrollParent!.scrollTop = next; + } + if (scrollX) { + let current = hscrollParent!.scrollLeft; + let next = Math.min( + hscrollParent!.scrollWidth, + Math.max(0, current + (scrollX * 5 * Math.min(200, Math.max(50, event.source.width))) / 60), + ); //60 FPS + hscrollParent!.scrollLeft = next; + } + scrollTimer = requestAnimationFrame(cb); + }; + scrollTimer = requestAnimationFrame(cb); + } + } else { + clearScrollTimer(); + } + } else clearScrollTimer(); +} + +function clearScrollTimer() { + if (scrollTimer) { + cancelAnimationFrame(scrollTimer); + scrollTimer = null; + } +} + +function notifyDragDrop(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent): void { + clearScrollTimer(); + + let event = getDragEvent(e, "dragdrop"); + + if (puppet!.stop) puppet!.stop(); + + if (activeZone && activeZone.onDrop) event.result = activeZone.onDrop(event); + + dropZones.forEach((zone) => { + if (nearZones != null && zone.onDragAway && nearZones.has(zone)) zone.onDragAway(event); + + if (!dragStartedZones!.has(zone)) return; + + if (zone.onDragEnd) zone.onDragEnd(event); + }); + + if (puppet!.onDragEnd) puppet!.onDragEnd(event); + + nearZones = null; + activeZone = null; + puppet = null; + dragStartedZones = null; +} + +function getDragEvent( + event: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent, + type: DragEvent["type"], +): DragEvent { + return { + type: type, + event, + cursor: getCursorPos(event), + source: puppet!.source, + }; +} + +interface DragCandidate { + el?: EventTarget | null; + start?: CursorPosition; + timeStamp?: number; +} + +let dragCandidate: DragCandidate = {}; + +export function ddMouseDown(e: React.MouseEvent | React.TouchEvent): void { + //do not allow that the same event is processed by multiple drag sources + //the first (top-level) source should be a drag-candidate + if (dragCandidate.timeStamp != null && e.timeStamp <= dragCandidate.timeStamp) return; + + dragCandidate = { + el: e.currentTarget, + start: { ...getCursorPos(e) }, + timeStamp: e.timeStamp, + }; +} + +export function ddMouseUp(): void { + dragCandidate = {}; +} + +export function ddDetect(e: MouseEvent | TouchEvent | React.TouchEvent | React.MouseEvent): void | true { + let cursor = getCursorPos(e); + if ( + dragCandidate.start && + e.currentTarget == dragCandidate.el && + Math.abs(cursor.clientX - dragCandidate.start.clientX) + Math.abs(cursor.clientY - dragCandidate.start.clientY) >= + 2 + ) { + dragCandidate = {}; + return true; + } +} + +let lastDragHandle: any; + +export function ddHandle(e: React.SyntheticEvent): void { + lastDragHandle = e.currentTarget; +} + +export function isDragHandleEvent(e: React.SyntheticEvent): boolean { + return lastDragHandle && (e.target == lastDragHandle || lastDragHandle.contains(e.target)); +} + +export const DragDropContext = ( + VDOM.createContext ? VDOM.createContext({ disabled: false }) : ({ children }: { children: any }) => children +) as React.Context<{ disabled: boolean }>; + +class ContextWrap extends Container { + declare value: any; + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + return ( + + {this.renderChildren(context, instance)} + + ); + } +} diff --git a/packages/cx/src/widgets/drag-drop/variables.scss b/packages/cx/src/widgets/drag-drop/variables.scss index 3036d3196..d6a6884c5 100644 --- a/packages/cx/src/widgets/drag-drop/variables.scss +++ b/packages/cx/src/widgets/drag-drop/variables.scss @@ -1,3 +1,6 @@ +@use "sass:map"; + + $cx-default-dragclone-background-color: rgba(white, 0.9) !default; $cx-default-dragclone-box-shadow: 0 0 3px rgba(128, 128, 128, 0.3) !default; @@ -7,6 +10,6 @@ $cx-dragclone-style: ( opacity: 0.9 ) !default; -$cx-dependencies: map-merge($cx-dependencies, ( +$cx-dependencies: map.merge($cx-dependencies, ( 'cx/widgets/DragSource': 'cx/widgets/DragClone' )) \ No newline at end of file diff --git a/packages/cx/src/widgets/enableAllInternalDependencies.d.ts b/packages/cx/src/widgets/enableAllInternalDependencies.d.ts deleted file mode 100644 index 78359972b..000000000 --- a/packages/cx/src/widgets/enableAllInternalDependencies.d.ts +++ /dev/null @@ -1 +0,0 @@ -export function enableAllInternalDependencies(); diff --git a/packages/cx/src/widgets/enableAllInternalDependencies.js b/packages/cx/src/widgets/enableAllInternalDependencies.js deleted file mode 100644 index ac70c047d..000000000 --- a/packages/cx/src/widgets/enableAllInternalDependencies.js +++ /dev/null @@ -1,11 +0,0 @@ -import {enableTooltips} from './overlay/Tooltip'; -import {enableMsgBoxAlerts} from './overlay/MsgBox'; -import {enableCultureSensitiveFormatting} from '../ui/Format'; -import {enableFatArrowExpansion} from '../data/enableFatArrowExpansion'; - -export function enableAllInternalDependencies() { - enableTooltips(); - enableMsgBoxAlerts(); - enableCultureSensitiveFormatting(); - enableFatArrowExpansion(); -} diff --git a/packages/cx/src/widgets/enableAllInternalDependencies.ts b/packages/cx/src/widgets/enableAllInternalDependencies.ts new file mode 100644 index 000000000..1d9cf51da --- /dev/null +++ b/packages/cx/src/widgets/enableAllInternalDependencies.ts @@ -0,0 +1,11 @@ +import {enableTooltips} from './overlay/Tooltip'; +import {enableMsgBoxAlerts} from './overlay/MsgBox'; +import {enableCultureSensitiveFormatting} from '../ui/Format'; +import {enableFatArrowExpansion} from '../data/enableFatArrowExpansion'; + +export function enableAllInternalDependencies(): void { + enableTooltips(); + enableMsgBoxAlerts(); + enableCultureSensitiveFormatting(); + enableFatArrowExpansion(); +} diff --git a/packages/cx/src/widgets/form/Calendar.d.ts b/packages/cx/src/widgets/form/Calendar.d.ts deleted file mode 100644 index 7dfe3cfbc..000000000 --- a/packages/cx/src/widgets/form/Calendar.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface DayInfo { - mod?: string; - className?: string; - style?: Cx.Record; - unselectable?: boolean; - disabled?: boolean; -} - -interface DayData { - [day: string]: DayInfo; -} - -interface CalendarProps extends FieldProps { - /** Selected date. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ - value?: Cx.Prop; - - /** View reference date. If no date is selected, this date is used to determine which month to show in the calendar. */ - refDate?: Cx.Prop; - - /** Minimum date value. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ - minValue?: Cx.Prop; - - /** Set to `true` to disallow the `minValue`. Default value is `false`. */ - minExclusive?: Cx.BooleanProp; - - /** Maximum date value. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ - maxValue?: Cx.Prop; - - /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ - maxExclusive?: Cx.BooleanProp; - - /** Base CSS class to be applied to the calendar. Defaults to `calendar`. */ - baseClass?: string; - - /** Highlight today's date. Default is true. */ - highlightToday?: boolean; - - /** Maximum value error text. */ - maxValueErrorText?: string; - - /** Maximum exclusive value error text. */ - maxExclusiveErrorText?: string; - - /** Minimum value error text. */ - minValueErrorText?: string; - - /** Minimum exclusive value error text. */ - minExclusiveErrorText?: string; - - /** The function that will be used to convert Date objects before writing data to the store. - * Default implementation is Date.toISOString. - * See also Culture.setDefaultDateEncoding. - */ - encoding?: (date: Date) => any; - - /** Set to true to show the button for quickly selecting today's date. */ - showTodayButton?: boolean; - - /** Localizable text for the todayButton. Defaults to `"Today"`. */ - todayButtonText?: string; - - /** Defines which days of week should be displayed as disabled, i.e. `[0, 6]` will make Sunday and Saturday unselectable. */ - disabledDaysOfWeek?: number[]; - - /** Set to true to show weeks starting from Monday. */ - startWithMonday?: boolean; - - /** Map of days to additional day information such as style, className, mod, unselectable and disabled. - * Keys for the map should be created with date.toDateString(). - * Example. { - * [new Date().toDateString()]: { - * style: "color: red", - * className: 'test-class', - * mod: 'holiday', - * unselectable: false, - * disabled: false - * } - * } - */ - dayData?: Cx.Prop; -} - -export class Calendar extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Calendar.js b/packages/cx/src/widgets/form/Calendar.js deleted file mode 100644 index db7f506d4..000000000 --- a/packages/cx/src/widgets/form/Calendar.js +++ /dev/null @@ -1,618 +0,0 @@ -import { StringTemplate } from "../../data/StringTemplate"; -import { Culture } from "../../ui/Culture"; -import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager"; -import "../../ui/Format"; -import { Localization } from "../../ui/Localization"; -import { VDOM, Widget } from "../../ui/Widget"; -import { parseDateInvariant } from "../../util"; -import { KeyCode } from "../../util/KeyCode"; -import { dateDiff } from "../../util/date/dateDiff"; -import { lowerBoundCheck } from "../../util/date/lowerBoundCheck"; -import { monthStart } from "../../util/date/monthStart"; -import { sameDate } from "../../util/date/sameDate"; -import { upperBoundCheck } from "../../util/date/upperBoundCheck"; -import { zeroTime } from "../../util/date/zeroTime"; -import DropdownIcon from "../icons/drop-down"; -import ForwardIcon from "../icons/forward"; -import { - tooltipMouseLeave, - tooltipMouseMove, - tooltipParentDidMount, - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, -} from "../overlay/tooltip-ops"; -import { Field, getFieldTooltip } from "./Field"; - -export class Calendar extends Field { - declareData() { - super.declareData( - { - value: undefined, - refDate: undefined, - disabled: undefined, - enabled: undefined, - minValue: undefined, - minExclusive: undefined, - maxValue: undefined, - maxExclusive: undefined, - focusable: undefined, - dayData: undefined, - }, - ...arguments, - ); - } - - init() { - if (this.unfocusable) this.focusable = false; - - super.init(); - } - - prepareData(context, { data }) { - data.stateMods = { - disabled: data.disabled, - }; - - if (data.value) { - let d = parseDateInvariant(data.value); - if (!isNaN(d.getTime())) { - data.date = zeroTime(d); - } - } - - if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); - - if (data.maxValue) data.maxValue = zeroTime(parseDateInvariant(data.maxValue)); - - if (data.minValue) data.minValue = zeroTime(parseDateInvariant(data.minValue)); - - super.prepareData(...arguments); - } - - validate(context, instance) { - super.validate(context, instance); - let { data, widget } = instance; - if (!data.error && data.date) { - let d; - if (data.maxValue) { - d = dateDiff(data.date, data.maxValue); - if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); - else if (d == 0 && data.maxExclusive) - data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); - } - - if (data.minValue) { - d = dateDiff(data.date, data.minValue); - if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); - else if (d == 0 && data.minExclusive) - data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); - } - - if (widget.disabledDaysOfWeek) { - if (widget.disabledDaysOfWeek.includes(data.date.getDay())) data.error = this.disabledDaysOfWeekErrorText; - } - - if (data.dayData) { - let date = parseDateInvariant(data.value); - let info = data.dayData[date.toDateString()]; - if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText; - } - } - } - - renderInput(context, instance, key) { - return ( - this.handleSelect(e, instance, date)} /> - ); - } - - handleSelect(e, instance, date) { - let { store, data, widget } = instance; - - e.stopPropagation(); - - if (data.disabled) return; - - if (!validationCheck(date, data)) return; - - if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return; - - if (widget.partial) { - let mixed = parseDateInvariant(data.value); - if (data.value && !isNaN(mixed)) { - mixed.setFullYear(date.getFullYear()); - mixed.setMonth(date.getMonth()); - mixed.setDate(date.getDate()); - date = mixed; - } - } - - let encode = widget.encoding || Culture.getDefaultDateEncoding(); - instance.set("value", encode(date)); - - if (this.onSelect) instance.invoke("onSelect", e, instance, date); - } -} - -Calendar.prototype.baseClass = "calendar"; -Calendar.prototype.highlightToday = true; -Calendar.prototype.maxValueErrorText = "Select a date not after {0:d}."; -Calendar.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; -Calendar.prototype.minValueErrorText = "Select a date not before {0:d}."; -Calendar.prototype.minExclusiveErrorText = "Select a date after {0:d}."; -Calendar.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed."; -Calendar.prototype.suppressErrorsUntilVisited = false; -Calendar.prototype.showTodayButton = false; -Calendar.prototype.todayButtonText = "Today"; -Calendar.prototype.startWithMonday = false; -Calendar.prototype.focusable = true; - -Localization.registerPrototype("cx/widgets/Calendar", Calendar); - -const validationCheck = (date, data, disabledDaysOfWeek) => { - if (data.maxValue && !upperBoundCheck(date, data.maxValue, data.maxExclusive)) return false; - - if (data.minValue && !lowerBoundCheck(date, data.minValue, data.minExclusive)) return false; - - if (disabledDaysOfWeek && disabledDaysOfWeek.includes(date.getDay())) return false; - - if (data.dayData) { - let day = data.dayData[date.toDateString()]; - if (day && (day.disabled || day.unselectable)) return false; - } - - return true; -}; - -export class CalendarCmp extends VDOM.Component { - constructor(props) { - super(props); - let { data } = props.instance; - - let refDate = data.refDate ? data.refDate : data.date || zeroTime(new Date()); - - this.state = Object.assign( - { - hover: false, - focus: false, - cursor: zeroTime(data.date || refDate), - activeView: "calendar", - }, - this.getPage(refDate), - ); - - this.handleMouseMove = this.handleMouseMove.bind(this); - this.handleMouseDown = this.handleMouseDown.bind(this); - } - - getPage(refDate) { - refDate = monthStart(refDate); //make a copy - - let startWithMonday = this.props.instance.widget.startWithMonday; - - let startDay = startWithMonday ? 1 : 0; - let startDate = new Date(refDate); - while (startDate.getDay() != startDay) startDate.setDate(startDate.getDate() - 1); - - let endDate = new Date(refDate); - endDate.setMonth(refDate.getMonth() + 1); - endDate.setDate(endDate.getDate() - 1); - - let endDay = startWithMonday ? 0 : 6; - while (endDate.getDay() != endDay) endDate.setDate(endDate.getDate() + 1); - - return { - refDate, - startDate, - endDate, - }; - } - - moveCursor(e, date, options = {}) { - e.preventDefault(); - e.stopPropagation(); - - date = zeroTime(date); - if (date.getTime() == this.state.cursor.getTime()) return; - - let refDate = this.state.refDate; - - if (options.movePage || date < this.state.startDate || date > this.state.endDate) refDate = date; - - this.setState({ - ...this.getPage(refDate), - cursor: date, - }); - } - - move(e, period, delta) { - e.preventDefault(); - e.stopPropagation(); - - let refDate = this.state.refDate; - - switch (period) { - case "y": - refDate.setFullYear(refDate.getFullYear() + delta); - break; - - case "m": - refDate.setMonth(refDate.getMonth() + delta); - break; - } - - let page = this.getPage(refDate); - if (this.state.cursor < page.startDate) page.cursor = page.startDate; - else if (this.state.cursor > page.endDate) page.cursor = page.endDate; - - this.setState(page); - } - - handleKeyPress(e) { - let cursor = new Date(this.state.cursor); - - switch (e.keyCode) { - case KeyCode.enter: - this.props.handleSelect(e, this.state.cursor); - break; - - case KeyCode.left: - cursor.setDate(cursor.getDate() - 1); - this.moveCursor(e, cursor); - break; - - case KeyCode.right: - cursor.setDate(cursor.getDate() + 1); - this.moveCursor(e, cursor); - break; - - case KeyCode.up: - cursor.setDate(cursor.getDate() - 7); - this.moveCursor(e, cursor); - break; - - case KeyCode.down: - cursor.setDate(cursor.getDate() + 7); - this.moveCursor(e, cursor); - break; - - case KeyCode.pageUp: - cursor.setMonth(cursor.getMonth() - 1); - this.moveCursor(e, cursor, { movePage: true }); - break; - - case KeyCode.pageDown: - cursor.setMonth(cursor.getMonth() + 1); - this.moveCursor(e, cursor, { movePage: true }); - break; - - case KeyCode.home: - cursor.setDate(1); - this.moveCursor(e, cursor, { movePage: true }); - break; - - case KeyCode.end: - cursor.setMonth(cursor.getMonth() + 1); - cursor.setDate(0); - this.moveCursor(e, cursor, { movePage: true }); - break; - - default: - let { instance } = this.props; - let { widget } = instance; - if (widget.onKeyDown) instance.invoke("onKeyDown", e, instance); - break; - } - } - - handleWheel(e) { - e.preventDefault(); - e.stopPropagation(); - - let cursor = new Date(this.state.cursor); - - if (e.deltaY < 0) { - cursor.setMonth(cursor.getMonth() - 1); - this.moveCursor(e, cursor, { movePage: true }); - } else if (e.deltaY > 0) { - cursor.setMonth(cursor.getMonth() + 1); - this.moveCursor(e, cursor, { movePage: true }); - } - } - - handleBlur(e) { - FocusManager.nudge(); - let { instance } = this.props; - let { widget } = instance; - if (widget.onBlur) instance.invoke("onBlur", e, instance); - this.setState({ - focus: false, - }); - } - - handleFocus(e) { - oneFocusOut(this, this.el, this.handleFocusOut.bind(this)); - this.setState({ - focus: true, - }); - } - - handleFocusOut() { - let { instance } = this.props; - let { widget } = instance; - if (widget.onFocusOut) instance.invoke("onFocusOut", null, instance); - } - - handleMouseLeave(e) { - tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance)); - this.setState({ - hover: false, - }); - } - - handleMouseEnter(e) { - this.setState({ - hover: true, - }); - } - - handleMouseMove(e) { - this.moveCursor(e, readDate(e.target.dataset)); - } - - handleMouseDown(e) { - this.props.handleSelect(e, readDate(e.target.dataset)); - } - - componentDidMount() { - //calendar doesn't bring up keyboard so it's ok to focus it even on mobile - if (this.props.instance.widget.autoFocus) this.el.focus(); - - tooltipParentDidMount(this.el, ...getFieldTooltip(this.props.instance)); - this.el.addEventListener("wheel", (e) => this.handleWheel(e)); - } - - UNSAFE_componentWillReceiveProps(props) { - let { data } = props.instance; - if (data.date) - this.setState({ - ...this.getPage(data.date), - value: data.date, - }); - - tooltipParentWillReceiveProps(this.el, ...getFieldTooltip(props.instance)); - } - - componentWillUnmount() { - offFocusOut(this); - tooltipParentWillUnmount(this.props.instance); - } - - showYearDropdown() { - this.setState({ - activeView: "year-picker", - yearPickerHeight: this.el.firstChild.offsetHeight, - }); - } - - handleYearSelect(e, year) { - e.preventDefault(); - e.stopPropagation(); - let refDate = new Date(this.state.refDate); - refDate.setFullYear(year); - this.setState({ - ...this.getPage(refDate), - refDate, - activeView: "calendar", - }); - } - - renderYearPicker() { - let { data, widget } = this.props.instance; - let minYear = data.minValue?.getFullYear(); - let maxYear = data.maxValue?.getFullYear(); - let { CSS } = this.props.instance.widget; - - let years = []; - let currentYear = new Date().getFullYear(); - let midYear = currentYear - (currentYear % 5); - let refYear = new Date(this.state.refDate).getFullYear(); - for (let i = midYear - 100; i <= midYear + 100; i++) { - years.push(i); - } - - let rows = []; - for (let i = 0; i < years.length; i += 5) { - rows.push(years.slice(i, i + 5)); - } - return ( -
    { - if (el) { - el.addEventListener("wheel", (e) => { - e.stopPropagation(); - }); - - let activeYear = el.querySelector("." + CSS.state("selected")); - if (activeYear) activeYear.scrollIntoView({ block: "center", behavior: "instant" }); - } - }} - > - - - {rows.map((row, rowIndex) => ( - - {row.map((year) => ( - - ))} - - ))} - -
    maxYear), - selected: year === refYear, - active: year === currentYear, - })} - onClick={(e) => this.handleYearSelect(e, year)} - > - {year} -
    -
    - ); - } - - render() { - let { data, widget } = this.props.instance; - let { CSS, baseClass, disabledDaysOfWeek, startWithMonday } = widget; - - let { refDate, startDate, endDate } = this.getPage(this.state.refDate); - - let month = refDate.getMonth(); - let year = refDate.getFullYear(); - let weeks = []; - let date = startDate; - - let empty = {}; - - let today = zeroTime(new Date()); - while (date >= startDate && date <= endDate) { - let days = []; - for (let i = 0; i < 7; i++) { - let dayInfo = (data.dayData && data.dayData[date.toDateString()]) || empty; - let unselectable = !validationCheck(date, data, disabledDaysOfWeek); - let classNames = CSS.expand( - CSS.element(baseClass, "day", { - outside: month != date.getMonth(), - unselectable: unselectable, - selected: data.date && sameDate(data.date, date), - cursor: - (this.state.hover || this.state.focus) && this.state.cursor && sameDate(this.state.cursor, date), - today: widget.highlightToday && sameDate(date, today), - }), - dayInfo.className, - CSS.mod(dayInfo.mod), - ); - let dateInst = new Date(date); - days.push( - - {date.getDate()} - , - ); - date.setDate(date.getDate() + 1); - } - weeks.push( - - - {days} - - , - ); - } - - let culture = Culture.getDateTimeCulture(); - let monthNames = culture.getMonthNames("long"); - let dayNames = culture.getWeekdayNames("short").map((x) => x.substr(0, 2)); - if (startWithMonday) dayNames = [...dayNames.slice(1), dayNames[0]]; - - return ( -
    this.handleKeyPress(e)} - onMouseDown={(e) => { - // prevent losing focus from the input field - if (!data.focusable) { - e.preventDefault(); - } - e.stopPropagation(); - }} - ref={(el) => { - this.el = el; - }} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={(e) => this.handleMouseLeave(e)} - onMouseEnter={(e) => this.handleMouseEnter(e)} - // onWheel={(e) => this.handleWheel(e)} - onFocus={(e) => this.handleFocus(e)} - onBlur={(e) => this.handleBlur(e)} - > - {this.state.activeView == "calendar" && ( - - - - - - - - - - - - ))} - - - {weeks} -
    - this.move(e, "y", -1)}> - - this.move(e, "m", -1)}> - - - {monthNames[month]} -
    - this.showYearDropdown()} - className={CSS.element(baseClass, "year-name")} - > - {year} - -
    this.move(e, "m", +1)}> - - this.move(e, "y", +1)}> - - -
    - {dayNames.map((name, i) => ( - {name} -
    - )} - {this.state.activeView == "calendar" && widget.showTodayButton && ( -
    - -
    - )} - - {this.state.activeView == "year-picker" && this.renderYearPicker()} -
    - ); - } -} - -const readDate = (ds) => new Date(Number(ds.year), Number(ds.month) - 1, Number(ds.date)); - -Widget.alias("calendar", Calendar); diff --git a/packages/cx/src/widgets/form/Calendar.scss b/packages/cx/src/widgets/form/Calendar.scss index a7cb50bed..b1a07623c 100644 --- a/packages/cx/src/widgets/form/Calendar.scss +++ b/packages/cx/src/widgets/form/Calendar.scss @@ -1,3 +1,6 @@ +@use "sass:map"; +@use "sass:color"; + @mixin cx-calendar( $name: "calendar", $state-style-map: $cx-calendar-state-style-map, @@ -5,10 +8,10 @@ $icon-size: $cx-default-icon-size, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}calendar { display: inline-block; @@ -116,7 +119,7 @@ &.#{$state}disabled { background-color: transparent; border-color: transparent; - color: darken(#fff, 18); + color: color.adjust(#fff, $lightness: -18%); pointer-events: none; opacity: 0.9; } diff --git a/packages/cx/src/widgets/form/Calendar.tsx b/packages/cx/src/widgets/form/Calendar.tsx new file mode 100644 index 000000000..4b96a6500 --- /dev/null +++ b/packages/cx/src/widgets/form/Calendar.tsx @@ -0,0 +1,760 @@ +/** @jsxImportSource react */ + +import { StringTemplate } from "../../data/StringTemplate"; +import { Culture } from "../../ui/Culture"; +import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager"; +import "../../ui/Format"; +import { Localization } from "../../ui/Localization"; +import { VDOM, Widget } from "../../ui/Widget"; +import { parseDateInvariant } from "../../util"; +import { KeyCode } from "../../util/KeyCode"; +import { dateDiff } from "../../util/date/dateDiff"; +import { lowerBoundCheck } from "../../util/date/lowerBoundCheck"; +import { monthStart } from "../../util/date/monthStart"; +import { sameDate } from "../../util/date/sameDate"; +import { upperBoundCheck } from "../../util/date/upperBoundCheck"; +import { zeroTime } from "../../util/date/zeroTime"; +import DropdownIcon from "../icons/drop-down"; +import ForwardIcon from "../icons/forward"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, +} from "../overlay/tooltip-ops"; +import { Field, FieldConfig, getFieldTooltip, FieldInstance } from "./Field"; +import type { Instance } from "../../ui/Instance"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { BooleanProp, DataRecord, Prop } from "../../ui/Prop"; + +interface DayInfo { + mod?: string; + className?: string; + style?: DataRecord | string; + unselectable?: boolean; + disabled?: boolean; +} + +interface DayData { + [day: string]: DayInfo; +} + +export interface CalendarConfig extends FieldConfig { + /** Selected date. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ + value?: Prop; + + /** View reference date. If no date is selected, this date is used to determine which month to show in the calendar. */ + refDate?: Prop; + + /** Minimum date value. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ + minValue?: Prop; + + /** Set to `true` to disallow the `minValue`. Default value is `false`. */ + minExclusive?: BooleanProp; + + /** Maximum date value. This should be a `Date` object or a valid date string consumable by `Date.parse` function. */ + maxValue?: Prop; + + /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ + maxExclusive?: BooleanProp; + + /** Base CSS class to be applied to the calendar. Defaults to `calendar`. */ + baseClass?: string; + + /** Highlight today's date. Default is true. */ + highlightToday?: boolean; + + /** Maximum value error text. */ + maxValueErrorText?: string; + + /** Maximum exclusive value error text. */ + maxExclusiveErrorText?: string; + + /** Minimum value error text. */ + minValueErrorText?: string; + + /** Minimum exclusive value error text. */ + minExclusiveErrorText?: string; + + /** The function that will be used to convert Date objects before writing data to the store. + * Default implementation is Date.toISOString. + * See also Culture.setDefaultDateEncoding. + */ + encoding?: (date: Date) => any; + + /** Set to true to show the button for quickly selecting today's date. */ + showTodayButton?: boolean; + + /** Localizable text for the todayButton. Defaults to `"Today"`. */ + todayButtonText?: string; + + /** Defines which days of week should be displayed as disabled, i.e. `[0, 6]` will make Sunday and Saturday unselectable. */ + disabledDaysOfWeek?: number[]; + + /** Set to true to show weeks starting from Monday. */ + startWithMonday?: boolean; + + /** Map of days to additional day information such as style, className, mod, unselectable and disabled. */ + dayData?: Prop; +} + +export class Calendar extends Field { + declare public baseClass: string; + declare public unfocusable?: boolean; + declare public focusable?: boolean; + declare public highlightToday?: boolean; + declare public maxValueErrorText?: string; + declare public maxExclusiveErrorText?: string; + declare public minValueErrorText?: string; + declare public minExclusiveErrorText?: string; + declare public disabledDaysOfWeekErrorText?: string; + declare public showTodayButton?: boolean; + declare public todayButtonText?: string; + declare public startWithMonday?: boolean; + declare public onBeforeSelect?: string | ((e: React.MouseEvent, instance: Instance, date: Date) => boolean | void); + declare public onSelect?: string | ((e: React.MouseEvent, instance: Instance, date: Date) => void); + declare public onBlur?: string | ((e: React.FocusEvent, instance: Instance) => void); + declare public onFocusOut?: string | ((instance: Instance) => void); + declare public disabledDaysOfWeek?: number[]; + declare public partial?: boolean; + declare public encoding?: (date: Date) => string; + + constructor(config?: CalendarConfig) { + super(config); + } + + declareData(...args: Record[]) { + super.declareData( + { + value: undefined, + refDate: undefined, + disabled: undefined, + enabled: undefined, + minValue: undefined, + minExclusive: undefined, + maxValue: undefined, + maxExclusive: undefined, + focusable: undefined, + dayData: undefined, + }, + ...args, + ); + } + + init() { + if (this.unfocusable) this.focusable = false; + + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance, ...args: any[]) { + const { data } = instance; + data.stateMods = { + disabled: data.disabled, + }; + + if (data.value) { + let d = parseDateInvariant(data.value); + if (!isNaN(d.getTime())) { + data.date = zeroTime(d); + } + } + + if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); + + if (data.maxValue) data.maxValue = zeroTime(parseDateInvariant(data.maxValue)); + + if (data.minValue) data.minValue = zeroTime(parseDateInvariant(data.minValue)); + + super.prepareData(context, instance, ...args); + } + + validate(context: RenderingContext, instance: FieldInstance) { + super.validate(context, instance); + let { data, widget } = instance; + let calendarWidget = widget as Calendar; + + if (!data.error && data.date) { + let d; + if (data.maxValue) { + d = dateDiff(data.date, data.maxValue); + if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText!, data.maxValue); + else if (d == 0 && data.maxExclusive) + data.error = StringTemplate.format(this.maxExclusiveErrorText!, data.maxValue); + } + + if (data.minValue) { + d = dateDiff(data.date, data.minValue); + if (d < 0) data.error = StringTemplate.format(this.minValueErrorText!, data.minValue); + else if (d == 0 && data.minExclusive) + data.error = StringTemplate.format(this.minExclusiveErrorText!, data.minValue); + } + + if (calendarWidget.disabledDaysOfWeek) { + if (calendarWidget.disabledDaysOfWeek.includes(data.date.getDay())) + data.error = this.disabledDaysOfWeekErrorText; + } + + if (data.dayData) { + let date = parseDateInvariant(data.value); + let info = data.dayData[date.toDateString()]; + if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText; + } + } + } + + renderInput(context: RenderingContext, instance: any, key: string): React.ReactElement { + return ( + this.handleSelect(e, instance, date)} + /> + ); + } + + handleSelect(e: React.MouseEvent, instance: any, date: Date): void { + let { store, data, widget } = instance; + let calendarWidget = widget as Calendar; + + e.stopPropagation(); + + if (data.disabled) return; + + if (!validationCheck(date, data, calendarWidget.disabledDaysOfWeek)) return; + + if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return; + + if (calendarWidget.partial) { + let mixed = parseDateInvariant(data.value); + if (data.value && !isNaN(mixed.getTime())) { + mixed.setFullYear(date.getFullYear()); + mixed.setMonth(date.getMonth()); + mixed.setDate(date.getDate()); + date = mixed; + } + } + + let encode = calendarWidget.encoding || Culture.getDefaultDateEncoding()!; + instance.set("value", encode(date)); + + if (this.onSelect) instance.invoke("onSelect", e, instance, date); + } +} + +Calendar.prototype.baseClass = "calendar"; +Calendar.prototype.highlightToday = true; +Calendar.prototype.maxValueErrorText = "Select a date not after {0:d}."; +Calendar.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; +Calendar.prototype.minValueErrorText = "Select a date not before {0:d}."; +Calendar.prototype.minExclusiveErrorText = "Select a date after {0:d}."; +Calendar.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed."; +Calendar.prototype.suppressErrorsUntilVisited = false; +Calendar.prototype.showTodayButton = false; +Calendar.prototype.todayButtonText = "Today"; +Calendar.prototype.startWithMonday = false; +Calendar.prototype.focusable = true; + +Localization.registerPrototype("cx/widgets/Calendar", Calendar); + +interface CalendarData { + maxValue?: Date; + maxExclusive?: boolean; + minValue?: Date; + minExclusive?: boolean; + dayData?: Record; +} + +const validationCheck = (date: Date, data: CalendarData, disabledDaysOfWeek?: number[]): boolean => { + if (data.maxValue && !upperBoundCheck(date, data.maxValue, data.maxExclusive)) return false; + + if (data.minValue && !lowerBoundCheck(date, data.minValue, data.minExclusive)) return false; + + if (disabledDaysOfWeek && disabledDaysOfWeek.includes(date.getDay())) return false; + + if (data.dayData) { + let day = data.dayData[date.toDateString()]; + if (day && (day.disabled || day.unselectable)) return false; + } + + return true; +}; + +interface CalendarCmpProps { + instance: FieldInstance; + handleSelect: (e: React.MouseEvent, date: Date) => void; +} + +interface CalendarState { + hover: boolean; + focus: boolean; + cursor: Date; + activeView: string; + refDate: Date; + startDate: Date; + endDate: Date; + yearPickerHeight?: number; +} + +export class CalendarCmp extends VDOM.Component { + el: HTMLElement | null = null; + + constructor(props: CalendarCmpProps) { + super(props); + let { data } = props.instance; + + let refDate = data.refDate ? data.refDate : data.date || zeroTime(new Date()); + + this.state = { + hover: false, + focus: false, + cursor: zeroTime(data.date || refDate), + activeView: "calendar", + ...this.getPage(refDate), + }; + + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleMouseDown = this.handleMouseDown.bind(this); + } + + getPage(refDate: Date): { refDate: Date; startDate: Date; endDate: Date } { + refDate = monthStart(refDate); //make a copy + + let calendarWidget = this.props.instance.widget as Calendar; + let startWithMonday = calendarWidget.startWithMonday; + + let startDay = startWithMonday ? 1 : 0; + let startDate = new Date(refDate); + while (startDate.getDay() != startDay) startDate.setDate(startDate.getDate() - 1); + + let endDate = new Date(refDate); + endDate.setMonth(refDate.getMonth() + 1); + endDate.setDate(endDate.getDate() - 1); + + let endDay = startWithMonday ? 0 : 6; + while (endDate.getDay() != endDay) endDate.setDate(endDate.getDate() + 1); + + return { + refDate, + startDate, + endDate, + }; + } + + moveCursor(e: React.SyntheticEvent | React.KeyboardEvent, date: Date, options: { movePage?: boolean } = {}): void { + e.preventDefault(); + e.stopPropagation(); + + date = zeroTime(date); + if (date.getTime() == this.state.cursor.getTime()) return; + + let refDate = this.state.refDate; + + if (options.movePage || date < this.state.startDate || date > this.state.endDate) refDate = date; + + this.setState({ + ...this.getPage(refDate), + cursor: date, + }); + } + + move(e: React.MouseEvent, period: string, delta: number): void { + e.preventDefault(); + e.stopPropagation(); + + let refDate = new Date(this.state.refDate); + + switch (period) { + case "y": + refDate.setFullYear(refDate.getFullYear() + delta); + break; + + case "m": + refDate.setMonth(refDate.getMonth() + delta); + break; + } + + let page = this.getPage(refDate); + let cursor = this.state.cursor; + if (cursor < page.startDate) cursor = page.startDate; + else if (cursor > page.endDate) cursor = page.endDate; + + this.setState({ ...page, cursor }); + } + + handleKeyPress(e: React.KeyboardEvent): void { + let cursor = new Date(this.state.cursor); + + switch (e.keyCode) { + case KeyCode.enter: + this.props.handleSelect(e as unknown as React.MouseEvent, this.state.cursor); + break; + + case KeyCode.left: + cursor.setDate(cursor.getDate() - 1); + this.moveCursor(e, cursor); + break; + + case KeyCode.right: + cursor.setDate(cursor.getDate() + 1); + this.moveCursor(e, cursor); + break; + + case KeyCode.up: + cursor.setDate(cursor.getDate() - 7); + this.moveCursor(e, cursor); + break; + + case KeyCode.down: + cursor.setDate(cursor.getDate() + 7); + this.moveCursor(e, cursor); + break; + + case KeyCode.pageUp: + cursor.setMonth(cursor.getMonth() - 1); + this.moveCursor(e, cursor, { movePage: true }); + break; + + case KeyCode.pageDown: + cursor.setMonth(cursor.getMonth() + 1); + this.moveCursor(e, cursor, { movePage: true }); + break; + + case KeyCode.home: + cursor.setDate(1); + this.moveCursor(e, cursor, { movePage: true }); + break; + + case KeyCode.end: + cursor.setMonth(cursor.getMonth() + 1); + cursor.setDate(0); + this.moveCursor(e, cursor, { movePage: true }); + break; + + default: + let { instance } = this.props; + let calendarWidget = instance.widget as Calendar; + if (calendarWidget.onKeyDown) instance.invoke("onKeyDown", e, instance); + break; + } + } + + handleWheel(e: WheelEvent): void { + e.preventDefault(); + e.stopPropagation(); + + let cursor = new Date(this.state.cursor); + + if (e.deltaY < 0) { + cursor.setMonth(cursor.getMonth() - 1); + this.moveCursor(e as unknown as React.SyntheticEvent, cursor, { movePage: true }); + } else if (e.deltaY > 0) { + cursor.setMonth(cursor.getMonth() + 1); + this.moveCursor(e as unknown as React.SyntheticEvent, cursor, { movePage: true }); + } + } + + handleBlur(e: React.FocusEvent): void { + FocusManager.nudge(); + let { instance } = this.props; + let calendarWidget = instance.widget as Calendar; + if (calendarWidget.onBlur) instance.invoke("onBlur", e, instance); + this.setState({ + focus: false, + }); + } + + handleFocus(e: React.FocusEvent): void { + oneFocusOut(this, this.el!, this.handleFocusOut.bind(this)); + this.setState({ + focus: true, + }); + } + + handleFocusOut(): void { + let { instance } = this.props; + let calendarWidget = instance.widget as Calendar; + if (calendarWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); + } + + handleMouseLeave(e: React.MouseEvent): void { + tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance)); + this.setState({ + hover: false, + }); + } + + handleMouseEnter(e: React.MouseEvent): void { + this.setState({ + hover: true, + }); + } + + handleMouseMove(e: React.MouseEvent): void { + this.moveCursor(e, readDate((e.target as HTMLElement).dataset)); + } + + handleMouseDown(e: React.MouseEvent): void { + this.props.handleSelect(e, readDate((e.target as HTMLElement).dataset)); + } + + componentDidMount(): void { + //calendar doesn't bring up keyboard so it's ok to focus it even on mobile + let calendarWidget = this.props.instance.widget as Calendar; + if (calendarWidget.autoFocus && this.el) this.el.focus(); + + if (this.el) { + tooltipParentDidMount(this.el, ...getFieldTooltip(this.props.instance)); + this.el.addEventListener("wheel", (e) => this.handleWheel(e)); + } + } + + UNSAFE_componentWillReceiveProps(props: CalendarCmpProps): void { + let { data } = props.instance; + if (data.date) + this.setState({ + ...this.getPage(data.date), + }); + + if (this.el) { + tooltipParentWillReceiveProps(this.el, ...getFieldTooltip(props.instance)); + } + } + + componentWillUnmount(): void { + offFocusOut(this); + tooltipParentWillUnmount(this.props.instance); + } + + showYearDropdown(): void { + if (this.el && this.el.firstChild) { + this.setState({ + activeView: "year-picker", + yearPickerHeight: (this.el.firstChild as HTMLElement).offsetHeight, + }); + } + } + + handleYearSelect(e: React.MouseEvent, year: number): void { + e.preventDefault(); + e.stopPropagation(); + let refDate = new Date(this.state.refDate); + refDate.setFullYear(year); + this.setState({ + ...this.getPage(refDate), + activeView: "calendar", + }); + } + + renderYearPicker(): React.ReactElement { + let { data, widget } = this.props.instance; + let calendarWidget = widget as Calendar; + let minYear: number | undefined = data.minValue?.getFullYear(); + let maxYear: number | undefined = data.maxValue?.getFullYear(); + let { CSS } = widget; + + let years: number[] = []; + let currentYear = new Date().getFullYear(); + let midYear = currentYear - (currentYear % 5); + let refYear = new Date(this.state.refDate).getFullYear(); + for (let i = midYear - 100; i <= midYear + 100; i++) { + years.push(i); + } + + let rows: number[][] = []; + for (let i = 0; i < years.length; i += 5) { + rows.push(years.slice(i, i + 5)); + } + return ( +
    { + if (el) { + el.addEventListener("wheel", (e) => { + e.stopPropagation(); + }); + + let activeYear = el.querySelector("." + CSS.state("selected")); + if (activeYear) activeYear.scrollIntoView({ block: "center", behavior: "instant" }); + } + }} + > + + + {rows.map((row: number[], rowIndex: number) => ( + + {row.map((year: number) => ( + + ))} + + ))} + +
    maxYear), + selected: year === refYear, + active: year === currentYear, + })} + onClick={(e) => this.handleYearSelect(e, year)} + > + {year} +
    +
    + ); + } + + render(): React.ReactElement { + let { data, widget } = this.props.instance; + let calendarWidget = widget as Calendar; + let { CSS, baseClass, disabledDaysOfWeek, startWithMonday } = calendarWidget; + + let { refDate, startDate, endDate } = this.getPage(this.state.refDate); + + let month = refDate.getMonth(); + let year = refDate.getFullYear(); + let weeks: React.ReactNode[] = []; + let date = startDate; + + let empty: Record = {}; + + let today = zeroTime(new Date()); + while (date >= startDate && date <= endDate) { + let days: React.ReactNode[] = []; + for (let i = 0; i < 7; i++) { + let dayInfo = (data.dayData && data.dayData[date.toDateString()]) || empty; + let unselectable = !validationCheck(date, data, disabledDaysOfWeek); + let classNames = CSS.expand( + CSS.element(baseClass, "day", { + outside: month != date.getMonth(), + unselectable: unselectable, + selected: data.date && sameDate(data.date, date), + cursor: + (this.state.hover || this.state.focus) && this.state.cursor && sameDate(this.state.cursor, date), + today: calendarWidget.highlightToday && sameDate(date, today), + }), + dayInfo.className, + CSS.mod(dayInfo.mod), + ); + let dateInst = new Date(date); + days.push( + + {date.getDate()} + , + ); + date.setDate(date.getDate() + 1); + } + weeks.push( + + + {days} + + , + ); + } + + let culture = Culture.getDateTimeCulture(); + let monthNames = culture.getMonthNames("long"); + let dayNames = culture.getWeekdayNames("short").map((x: string) => x.substr(0, 2)); + if (startWithMonday) dayNames = [...dayNames.slice(1), dayNames[0]]; + + return ( +
    this.handleKeyPress(e)} + onMouseDown={(e) => { + // prevent losing focus from the input field + if (!data.focusable) { + e.preventDefault(); + } + e.stopPropagation(); + }} + ref={(el) => { + this.el = el; + }} + onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} + onMouseLeave={(e) => this.handleMouseLeave(e)} + onMouseEnter={(e) => this.handleMouseEnter(e)} + // onWheel={(e) => this.handleWheel(e)} + onFocus={(e) => this.handleFocus(e)} + onBlur={(e) => this.handleBlur(e)} + > + {this.state.activeView == "calendar" && ( + + + + + + + + + + + + ))} + + + {weeks} +
    + this.move(e, "y", -1)}> + + this.move(e, "m", -1)}> + + + {monthNames[month]} +
    + this.showYearDropdown()} + className={CSS.element(baseClass, "year-name")} + > + {year} + +
    this.move(e, "m", +1)}> + + this.move(e, "y", +1)}> + + +
    + {dayNames.map((name: string, i: number) => ( + {name} +
    + )} + {this.state.activeView == "calendar" && calendarWidget.showTodayButton && ( +
    + +
    + )} + + {this.state.activeView == "year-picker" && this.renderYearPicker()} +
    + ); + } +} + +const readDate = (ds: DOMStringMap): Date => new Date(Number(ds.year), Number(ds.month) - 1, Number(ds.date)); + +Widget.alias("calendar", Calendar); diff --git a/packages/cx/src/widgets/form/Checkbox.d.ts b/packages/cx/src/widgets/form/Checkbox.d.ts deleted file mode 100644 index c72cd738f..000000000 --- a/packages/cx/src/widgets/form/Checkbox.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface CheckboxProps extends FieldProps { - /** Value of the checkbox. `true` makes the checkbox checked. */ - value?: Cx.BooleanProp; - - /** efaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** Base CSS class to be applied to the field. Defaults to `checkbox`. */ - baseClass?: string; - - /** - * Use native checkbox HTML element (``). Default is `false`. - * Native checkboxes are difficult to style. - */ - native?: boolean; - - /** - * Set to true to instruct the widget to indicate indeterminate state - * (null or undefined value) with a square icon instead of appearing unchecked. - */ - indeterminate?: boolean; - - /** Value of the checkbox. `true` makes the checkbox checked. */ - checked?: Cx.BooleanProp; - - /** Text property. */ - text?: Cx.StringProp; - - /** Prevent moving focus on the checkbox. This is useful when checkboxes are found - inside other focusable elements, such as grids or lists. */ - unfocusable?: boolean; - - /** - * Text to be displayed when checkobx is in the view mode. - * Useful to describe state of the checkbox in `viewMode` in a form of text, i.e. `Active/Inactive`. - */ - viewText?: Cx.StringProp; -} - -export class Checkbox extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Checkbox.js b/packages/cx/src/widgets/form/Checkbox.js deleted file mode 100644 index 9f737e80c..000000000 --- a/packages/cx/src/widgets/form/Checkbox.js +++ /dev/null @@ -1,203 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { tooltipMouseMove, tooltipMouseLeave } from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { KeyCode } from "../../util/KeyCode"; -import CheckIcon from "../icons/check"; -import SquareIcon from "../icons/square"; - -export class Checkbox extends Field { - init() { - if (this.checked) this.value = this.checked; - - super.init(); - } - - declareData() { - super.declareData( - { - value: !this.indeterminate ? false : undefined, - text: undefined, - readOnly: undefined, - disabled: undefined, - enabled: undefined, - required: undefined, - viewText: undefined, - }, - ...arguments, - ); - } - - renderWrap(context, instance, key, content) { - let { data } = instance; - return ( - - ); - } - - validateRequired(context, instance) { - let { data } = instance; - if (!data.value) return this.requiredText; - } - - renderNativeCheck(context, instance) { - let { CSS, baseClass } = this; - let { data } = instance; - return ( - { - this.handleChange(e, instance); - }} - /> - ); - } - - renderCheck(context, instance) { - return ; - } - - renderInput(context, instance, key) { - let { data } = instance; - let text = data.text || this.renderChildren(context, instance); - let { CSS, baseClass } = this; - return this.renderWrap(context, instance, key, [ - this.native ? this.renderNativeCheck(context, instance) : this.renderCheck(context, instance), - text ? ( -
    - {text} -
    - ) : ( - -   - - ), - ]); - } - - renderValue(context, { data }) { - if (!data.viewText) return super.renderValue(...arguments); - return {data.viewText}; - } - - formatValue(context, instance) { - let { data } = instance; - return data.value && (data.text || this.renderChildren(context, instance)); - } - - handleClick(e, instance) { - if (this.native) e.stopPropagation(); - else { - var el = document.getElementById(instance.data.id); - if (el) el.focus(); - if (!instance.data.viewMode) { - e.preventDefault(); - e.stopPropagation(); - this.handleChange(e, instance, !instance.data.value); - } - } - } - - handleChange(e, instance, checked) { - let { data } = instance; - - if (data.readOnly || data.disabled || data.viewMode) return; - - instance.set("value", checked != null ? checked : e.target.checked); - } -} - -Checkbox.prototype.baseClass = "checkbox"; -Checkbox.prototype.native = false; -Checkbox.prototype.indeterminate = false; -Checkbox.prototype.unfocusable = false; - -Widget.alias("checkbox", Checkbox); - -class CheckboxCmp extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - value: props.data.value, - }; - } - - UNSAFE_componentWillReceiveProps(props) { - this.setState({ - value: props.data.value, - }); - } - - render() { - let { instance, data } = this.props; - let { widget } = instance; - let { baseClass, CSS } = widget; - - let check = false; - - if (this.state.value == null && widget.indeterminate) check = "indeterminate"; - else if (this.state.value) check = "check"; - - return ( - - {check == "check" && } - {check == "indeterminate" && } - - ); - } - - onClick(e) { - let { instance, data } = this.props; - let { widget } = instance; - if (!data.disabled && !data.readOnly) { - e.stopPropagation(); - e.preventDefault(); - this.setState({ value: !this.state.value }); - widget.handleChange(e, instance, !this.state.value); - } - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.space: - this.onClick(e); - break; - } - } -} diff --git a/packages/cx/src/widgets/form/Checkbox.scss b/packages/cx/src/widgets/form/Checkbox.scss index b7a6640fb..33bc80ff2 100644 --- a/packages/cx/src/widgets/form/Checkbox.scss +++ b/packages/cx/src/widgets/form/Checkbox.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-checkbox( $name: "checkbox", $state-style-map: $cx-checkbox-state-style-map, @@ -6,9 +8,9 @@ $besm: $cx-besm, $size: $cx-default-checkbox-size ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $padding: cx-get-state-rule($state-style-map, default, padding); $border-width: cx-get-state-rule($state-style-map, default, border-width); diff --git a/packages/cx/src/widgets/form/Checkbox.tsx b/packages/cx/src/widgets/form/Checkbox.tsx new file mode 100644 index 000000000..04650eed3 --- /dev/null +++ b/packages/cx/src/widgets/form/Checkbox.tsx @@ -0,0 +1,287 @@ +/** @jsxImportSource react */ + +import { VDOM } from "../../ui/VDOM"; +import type { Instance } from "../../ui/Instance"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { Widget, getContent } from "../../ui/Widget"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { KeyCode } from "../../util/KeyCode"; +import CheckIcon from "../icons/check"; +import SquareIcon from "../icons/square"; +import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops"; +import { Field, FieldConfig, getFieldTooltip, FieldInstance } from "./Field"; +import { BooleanProp, StringProp } from "../../ui/Prop"; + +export interface CheckboxConfig extends FieldConfig { + /** Value of the checkbox. */ + value?: BooleanProp; + + /** Set to `true` to make the checkbox read-only. */ + readOnly?: BooleanProp; + + /** Base CSS class to be applied to the element. Defaults to `checkbox`. */ + baseClass?: string; + + /** Use native checkbox HTML element. */ + native?: boolean; + + /** Set to `true` to display a square icon to indicate `null` or `undefined` value. */ + indeterminate?: boolean; + + /** Checked value alias for `value`. */ + checked?: BooleanProp; + + /** Text description. */ + text?: StringProp; + + /** Set to true to disable focusing on the checkbox. Used in grids to avoid conflicts. */ + unfocusable?: boolean; + + /** View mode text. */ + viewText?: StringProp; + + /** Custom validation function. */ + onValidate?: string | ((value: boolean, instance: Instance, validationParams: Record) => unknown); +} + +export class Checkbox extends Field { + declare public baseClass: string; + declare public checked?: unknown; + declare public value?: unknown; + declare public indeterminate?: boolean; + declare public unfocusable?: boolean; + declare public native?: boolean; + + constructor(config?: CheckboxConfig) { + super(config); + } + + init(): void { + if (this.checked) this.value = this.checked; + + super.init(); + } + + declareData(...args: Record[]): void { + super.declareData( + { + value: !this.indeterminate ? false : undefined, + text: undefined, + readOnly: undefined, + disabled: undefined, + enabled: undefined, + required: undefined, + viewText: undefined, + }, + ...args, + ); + } + + renderWrap( + context: RenderingContext, + instance: FieldInstance, + key: string, + content: React.ReactNode, + ): React.ReactElement { + let { data } = instance; + return ( + + ); + } + + validateRequired(context: RenderingContext, instance: FieldInstance): string | undefined { + let { data } = instance; + if (!data.value) return this.requiredText; + } + + renderNativeCheck(context: RenderingContext, instance: FieldInstance): React.ReactElement { + let { CSS, baseClass } = this; + let { data } = instance; + return ( + { + this.handleChange(e, instance, (e.target as HTMLInputElement).checked); + }} + /> + ); + } + + renderCheck(context: RenderingContext, instance: FieldInstance): React.ReactElement { + return ; + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactElement { + let { data } = instance; + let text = data.text || this.renderChildren?.(context, instance); + let { CSS, baseClass } = this; + return this.renderWrap(context, instance, key, [ + this.native ? this.renderNativeCheck(context, instance) : this.renderCheck(context, instance), + text ? ( +
    + {text} +
    + ) : ( + +   + + ), + ]); + } + + renderValue(context: RenderingContext, instance: FieldInstance): React.ReactNode { + let { data } = instance; + if (!data.viewText) return super.renderValue(context, instance, undefined); + return {data.viewText}; + } + + formatValue(context: RenderingContext, instance: Instance): React.ReactNode | string { + let { data } = instance; + return data.value && (data.text || this.renderChildren?.(context, instance)); + } + + handleClick(e: React.MouseEvent, instance: Instance): void { + if (this.native) e.stopPropagation(); + else { + var el = document.getElementById(instance.data.id); + if (el) el.focus(); + if (!instance.data.viewMode) { + e.preventDefault(); + e.stopPropagation(); + this.handleChange(e, instance, !instance.data.value); + } + } + } + + handleChange( + e: React.ChangeEvent | React.MouseEvent, + instance: Instance, + checked?: boolean, + ): void { + let { data } = instance; + + if (data.readOnly || data.disabled || data.viewMode) return; + + instance.set("value", checked != null ? checked : (e.target as HTMLInputElement).checked); + } +} + +Checkbox.prototype.baseClass = "checkbox"; +Checkbox.prototype.native = false; +Checkbox.prototype.indeterminate = false; +Checkbox.prototype.unfocusable = false; + +Widget.alias("checkbox", Checkbox); + +interface CheckboxCmpProps { + key?: string; + instance: FieldInstance; + data: Record; +} + +interface CheckboxCmpState { + value: unknown; +} + +class CheckboxCmp extends VDOM.Component { + constructor(props: CheckboxCmpProps) { + super(props); + this.state = { + value: props.data.value, + }; + } + + UNSAFE_componentWillReceiveProps(props: CheckboxCmpProps) { + this.setState({ + value: props.data.value, + }); + } + + render(): React.ReactElement { + let { instance, data } = this.props; + let { widget } = instance; + let { baseClass, CSS } = widget; + + let check: string | boolean = false; + + if (this.state.value == null && widget.indeterminate) check = "indeterminate"; + else if (this.state.value) check = "check"; + + return ( + + {check == "check" && } + {check == "indeterminate" && } + + ); + } + + onClick(e: React.MouseEvent): void { + let { instance, data } = this.props; + let { widget } = instance; + if (!data.disabled && !data.readOnly) { + e.stopPropagation(); + e.preventDefault(); + this.setState({ value: !this.state.value }); + widget.handleChange(e, instance, !this.state.value); + } + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + const { widget } = instance; + if ( + widget.handleKeyDown && + widget.handleKeyDown(e as unknown as React.KeyboardEvent, instance) === false + ) + return; + + switch (e.keyCode) { + case KeyCode.space: + this.onClick(e as unknown as React.MouseEvent); + break; + } + } +} diff --git a/packages/cx/src/widgets/form/ColorField.d.ts b/packages/cx/src/widgets/form/ColorField.d.ts deleted file mode 100644 index 2a689aa9c..000000000 --- a/packages/cx/src/widgets/form/ColorField.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface ColorFieldProps extends FieldProps { - /** Either `rgba`, `hsla` or `hex` value of the selected color. */ - value?: Cx.StringProp; - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** - * Set to `true` to hide the clear button. - * It can be used interchangeably with the `showClear` property. Default value is `false`. - */ - hideClear?: boolean; - - /** - * Set to `false` to hide the clear button. - * It can be used interchangeably with the `hideClear` property. Default value is `true`. - */ - showClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** Base CSS class to be applied to the element. Defaults to `colorfield`. */ - baseClass?: boolean; - - /** Format of the color representation. Either `rgba`, `hsla` or `hex`. */ - format?: "rgba" | "hsla" | "hex"; - - /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ - dropdownOptions?: Cx.Config; -} - -export class ColorField extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/ColorField.js b/packages/cx/src/widgets/form/ColorField.js deleted file mode 100644 index 4b27445b1..000000000 --- a/packages/cx/src/widgets/form/ColorField.js +++ /dev/null @@ -1,397 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Cx } from "../../ui/Cx"; -import { Field, getFieldTooltip } from "./Field"; -import { Dropdown } from "../overlay/Dropdown"; -import { ColorPicker } from "./ColorPicker"; -import { parseColor } from "../../util/color/parseColor"; -import { isTouchDevice } from "../../util/isTouchDevice"; -import { isTouchEvent } from "../../util/isTouchEvent"; - -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { KeyCode } from "../../util/KeyCode"; - -import DropdownIcon from "../icons/drop-down"; -import ClearIcon from "../icons/clear"; -import { Localization } from "../../ui/Localization"; -import { isDefined } from "../../util/isDefined"; -import { getActiveElement } from "../../util/getActiveElement"; - -export class ColorField extends Field { - declareData() { - super.declareData( - { - value: this.emptyValue, - disabled: undefined, - readOnly: undefined, - enabled: undefined, - placeholder: undefined, - required: undefined, - format: undefined, - }, - ...arguments, - ); - } - - init() { - if (isDefined(this.hideClear)) this.showClear = !this.hideClear; - - if (this.alwaysShowClear) this.showClear = true; - - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - data.stateMods = [ - data.stateMods, - { - empty: !data.value, - }, - ]; - instance.lastDropdown = context.lastDropdown; - super.prepareData(context, instance); - } - - renderInput(context, instance, key) { - return ( - - ); - } -} - -ColorField.prototype.baseClass = "colorfield"; -ColorField.prototype.format = "rgba"; -ColorField.prototype.suppressErrorsUntilVisited = true; -ColorField.prototype.showClear = true; -ColorField.prototype.alwaysShowClear = false; - -Widget.alias("color-field", ColorField); -Localization.registerPrototype("cx/widgets/ColorField", ColorField); - -class ColorInput extends VDOM.Component { - constructor(props) { - super(props); - let { data } = this.props; - this.data = data; - this.state = { - dropdownOpen: false, - focus: false, - }; - } - - getDropdown() { - if (this.dropdown) return this.dropdown; - - let { widget, lastDropdown } = this.props.instance; - - let dropdown = { - scrollTracking: true, - autoFocus: true, //put focus on the dropdown to prevent opening the keyboard - focusable: true, - inline: !isTouchDevice() || !!lastDropdown, - touchFriendly: true, - placementOrder: - " down down-left down-right up up-left up-right right right-up right-down left left-up left-down", - ...widget.dropdownOptions, - type: Dropdown, - relatedElement: this.input, - items: { - type: ColorPicker, - ...this.props.picker, - onColorClick: (e) => { - e.stopPropagation(); - e.preventDefault(); - let touch = isTouchEvent(e); - this.closeDropdown(e, () => { - if (!touch) this.input.focus(); - }); - }, - }, - onFocusOut: () => { - this.closeDropdown(); - }, - dismissAfterScroll: () => { - this.closeDropdown(); - }, - firstChildDefinesHeight: true, - firstChildDefinesWidth: true, - }; - - return (this.dropdown = Widget.create(dropdown)); - } - - render() { - let { instance, label, help, data } = this.props; - let { widget, state } = instance; - let { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let insideButton; - if (!data.readOnly && !data.disabled) { - if ( - widget.showClear && - (((!data.required || widget.alwaysShowClear) && !data.empty) || instance.state.inputError) - ) - insideButton = ( -
    { - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => { - this.onClearClick(e); - }} - > - -
    - ); - else - insideButton = ( -
    - -
    - ); - } - - let well = ( -
    -
    -
    - ); - - let dropdown = false; - if (this.state.dropdownOpen) - dropdown = ( - - ); - - let empty = this.input ? !this.input.value : data.empty; - - return ( -
    - { - this.input = el; - }} - type="text" - className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} - style={data.inputStyle} - defaultValue={this.trim(data.value || "")} - disabled={data.disabled} - readOnly={data.readOnly} - tabIndex={data.tabIndex} - placeholder={data.placeholder} - {...data.inputAttrs} - onInput={(e) => this.onChange(e.target.value, "input")} - onChange={(e) => this.onChange(e.target.value, "change")} - onKeyDown={(e) => this.onKeyDown(e)} - onBlur={(e) => { - this.onBlur(e); - }} - onFocus={(e) => { - this.onFocus(e); - }} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(instance))} - /> - {well} - {insideButton} - {dropdown} - {label} - {help} -
    - ); - } - - onMouseDown(e) { - e.stopPropagation(); - if (this.state.dropdownOpen) this.closeDropdown(e); - else { - this.openDropdownOnFocus = true; - } - - //icon click - if (e.target != this.input) { - e.preventDefault(); - if (!this.state.dropdownOpen) this.openDropdown(e); - else this.input.focus(); - } - } - - onFocus(e) { - if (this.openDropdownOnFocus) this.openDropdown(e); - - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.enter: - e.stopPropagation(); - this.onChange(e.target.value, "enter"); - break; - - case KeyCode.esc: - if (this.state.dropdownOpen) { - e.stopPropagation(); - this.closeDropdown(e, () => { - this.input.focus(); - }); - } - break; - - case KeyCode.left: - case KeyCode.right: - e.stopPropagation(); - break; - - case KeyCode.down: - this.openDropdown(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - } - - onBlur(e) { - if (this.state.focus) - this.setState({ - focus: false, - }); - this.onChange(e.target.value, "blur"); - } - - closeDropdown(e, callback) { - if (this.state.dropdownOpen) { - if (this.scrollableParents) - this.scrollableParents.forEach((el) => { - el.removeEventListener("scroll", this.updateDropdownPosition); - }); - - this.setState({ dropdownOpen: false }, callback); - } else if (callback) callback(); - } - - openDropdown(e) { - let { data } = this.props; - this.openDropdownOnFocus = false; - - if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { - this.setState({ dropdownOpen: true }); - } - } - - trim(value) { - return value.replace(/\s/g, ""); - } - - UNSAFE_componentWillReceiveProps(props) { - let { data, instance } = props; - let { state } = instance; - let nv = this.trim(data.value || ""); - if (nv != this.input.value && (this.data.value != data.value || !state.inputError)) { - this.input.value = nv; - instance.setState({ - inputError: false, - }); - } - this.data = data; - - tooltipParentWillReceiveProps(this.input, ...getFieldTooltip(instance)); - } - - componentDidMount() { - tooltipParentDidMount(this.input, ...getFieldTooltip(this.props.instance)); - if (this.props.instance.widget.autoFocus && !isTouchDevice()) this.input.focus(); - } - - componentWillUnmount() { - if (this.input == getActiveElement() && this.input.value != this.props.data.value) { - this.onChange(this.input.value, "blur"); - } - tooltipParentWillUnmount(this.props.instance); - } - - onClearClick(e) { - let { instance } = this.props; - instance.set("value", instance.widget.emptyValue); - instance.setState({ - inputError: false, - }); - e.stopPropagation(); - e.preventDefault(); - } - - onChange(inputValue, eventType) { - let { instance, data } = this.props; - let { widget } = instance; - - if (eventType == "blur") { - instance.setState({ visited: true }); - } - - let isValid; - try { - parseColor(inputValue); - isValid = true; - } catch (e) { - isValid = false; - } - - if (eventType == "blur" || eventType == "enter") { - let value = inputValue || widget.emptyValue; - if (isValid && value !== data.value) instance.set("value", value); - - instance.setState({ - inputError: !isValid && "Invalid color entered.", - }); - } - } -} diff --git a/packages/cx/src/widgets/form/ColorField.scss b/packages/cx/src/widgets/form/ColorField.scss index 39c342009..564cde936 100644 --- a/packages/cx/src/widgets/form/ColorField.scss +++ b/packages/cx/src/widgets/form/ColorField.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-colorfield( $name: "colorfield", $state-style-map: $cx-std-field-state-style-map, @@ -10,9 +12,9 @@ $icon-size: $cx-default-icon-size, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-field($besm, $state-style-map, $width: $width, $input: true); diff --git a/packages/cx/src/widgets/form/ColorField.tsx b/packages/cx/src/widgets/form/ColorField.tsx new file mode 100644 index 000000000..1ce0ec51f --- /dev/null +++ b/packages/cx/src/widgets/form/ColorField.tsx @@ -0,0 +1,497 @@ +/** @jsxImportSource react */ + +import { Cx } from "../../ui/Cx"; +import { DropdownInstance, DropdownWidgetProps, Instance } from "../../ui/Instance"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { VDOM, Widget, getContent } from "../../ui/Widget"; +import { parseColor } from "../../util/color/parseColor"; +import { isTouchDevice } from "../../util/isTouchDevice"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { Dropdown, DropdownConfig } from "../overlay/Dropdown"; +import { ColorPicker } from "./ColorPicker"; +import { Field, FieldConfig, getFieldTooltip, FieldInstance } from "./Field"; +import { BooleanProp, StringProp } from "../../ui/Prop"; +import { Config } from "../../ui/Prop"; + +export interface ColorFieldConfig extends FieldConfig { + /** Either `rgba`, `hsla` or `hex` value of the selected color. */ + value?: StringProp; + + /** Defaults to `false`. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** The opposite of `disabled`. */ + enabled?: BooleanProp; + + /** Default text displayed when the field is empty. */ + placeholder?: StringProp; + + /** Set to `true` to hide the clear button. Default value is `false`. */ + hideClear?: boolean; + + /** Set to `false` to hide the clear button. Default value is `true`. */ + showClear?: boolean; + + /** Set to `true` to display the clear button even if `required` is set. Default is `false`. */ + alwaysShowClear?: boolean; + + /** Base CSS class to be applied to the element. Defaults to `colorfield`. */ + baseClass?: string; + + /** Format of the color representation. Either `rgba`, `hsla` or `hex`. */ + format?: "rgba" | "hsla" | "hex"; + + /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ + dropdownOptions?: Partial; + + /** Custom validation function. */ + onValidate?: string | ((value: string, instance: Instance, validationParams: Record) => unknown); +} + +export class ColorFieldInstance + extends FieldInstance + implements DropdownWidgetProps +{ + lastDropdown?: Instance; + dropdownOpen?: boolean; + selectedIndex?: number; +} + +import { stopPropagation } from "../../util/eventCallbacks"; +import { KeyCode } from "../../util/KeyCode"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, +} from "../overlay/tooltip-ops"; + +import { Localization } from "../../ui/Localization"; +import { getActiveElement } from "../../util/getActiveElement"; +import { isDefined } from "../../util/isDefined"; +import ClearIcon from "../icons/clear"; +import DropdownIcon from "../icons/drop-down"; + +interface ColorInputProps { + key?: string; + instance: ColorFieldInstance; + data: Record; + picker: { + value: unknown; + format: string; + }; + label?: React.ReactNode; + help?: React.ReactNode; +} + +interface ColorInputState { + dropdownOpen: boolean; + focus: boolean; +} + +export class ColorField extends Field { + declare public baseClass: string; + declare public showClear?: boolean; + declare public alwaysShowClear?: boolean; + declare public hideClear?: boolean; + declare public format: string; + declare public lastDropdown?: string; + declare public value?: string; + declare public dropdownOptions?: Partial; + + constructor(config?: ColorFieldConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + value: this.emptyValue, + disabled: undefined, + readOnly: undefined, + enabled: undefined, + placeholder: undefined, + required: undefined, + format: undefined, + }, + ...args, + ); + } + + init(): void { + if (isDefined(this.hideClear)) this.showClear = !this.hideClear; + + if (this.alwaysShowClear) this.showClear = true; + + super.init(); + } + + prepareData(context: RenderingContext, instance: ColorFieldInstance): void { + let { data } = instance; + data.stateMods = [ + data.stateMods, + { + empty: !data.value, + }, + ]; + instance.lastDropdown = context.lastDropdown; + super.prepareData(context, instance); + } + + renderInput(context: RenderingContext, instance: ColorFieldInstance, key: string): React.ReactNode { + return ( + + ); + } +} + +ColorField.prototype.baseClass = "colorfield"; +ColorField.prototype.format = "rgba"; +ColorField.prototype.suppressErrorsUntilVisited = true; +ColorField.prototype.showClear = true; +ColorField.prototype.alwaysShowClear = false; + +Widget.alias("color-field", ColorField); +Localization.registerPrototype("cx/widgets/ColorField", ColorField); + +class ColorInput extends VDOM.Component { + data: Record; + dropdown?: Widget; + input!: HTMLInputElement; + openDropdownOnFocus: boolean = false; + scrollableParents?: Element[]; + updateDropdownPosition: () => void; + + constructor(props: ColorInputProps) { + super(props); + let { data } = this.props; + this.data = data; + this.state = { + dropdownOpen: false, + focus: false, + }; + this.updateDropdownPosition = () => {}; + } + + getDropdown(): Widget { + if (this.dropdown) return this.dropdown; + + let { widget, lastDropdown } = this.props.instance as DropdownInstance; + const colorFieldWidget = widget as ColorField; + + let dropdown = { + scrollTracking: true, + autoFocus: true, //put focus on the dropdown to prevent opening the keyboard + focusable: true, + inline: !isTouchDevice() || !!lastDropdown, + touchFriendly: true, + placementOrder: + " down down-left down-right up up-left up-right right right-up right-down left left-up left-down", + ...colorFieldWidget.dropdownOptions, + type: Dropdown, + relatedElement: this.input, + items: { + type: ColorPicker, + ...this.props.picker, + onColorClick: (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + let touch = isTouchEvent(); + this.closeDropdown(e, () => { + if (!touch) this.input.focus(); + }); + }, + }, + onFocusOut: () => { + this.closeDropdown(); + }, + dismissAfterScroll: () => { + this.closeDropdown(); + }, + firstChildDefinesHeight: true, + firstChildDefinesWidth: true, + }; + + return (this.dropdown = Widget.create(dropdown)); + } + + render(): React.ReactNode { + let { instance, label, help, data } = this.props; + let { widget, state } = instance; + let { CSS, baseClass, suppressErrorsUntilVisited, showClear, alwaysShowClear } = widget as ColorField; + + let insideButton; + if (!data.readOnly && !data.disabled) { + if (showClear && (((!data.required || alwaysShowClear) && !data.empty) || instance.state?.inputError)) + insideButton = ( +
    { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={(e) => { + this.onClearClick(e); + }} + > + +
    + ); + else + insideButton = ( +
    + +
    + ); + } + + let well = ( +
    +
    +
    + ); + + let dropdown: React.ReactNode | false = false; + if (this.state.dropdownOpen) + dropdown = ( + + ); + + let empty = this.input ? !this.input.value : data.empty; + + return ( +
    + { + this.input = el!; + }} + type="text" + className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} + style={data.inputStyle as React.CSSProperties} + defaultValue={this.trim((data.value as string) || "")} + disabled={data.disabled as boolean} + readOnly={data.readOnly as boolean} + tabIndex={data.tabIndex as number} + placeholder={data.placeholder as string} + {...(data.inputAttrs as Record)} + onInput={(e: React.ChangeEvent) => + this.onChange((e.target as HTMLInputElement).value, "input") + } + onChange={(e: React.ChangeEvent) => + this.onChange((e.target as HTMLInputElement).value, "change") + } + onKeyDown={(e: React.KeyboardEvent) => this.onKeyDown(e)} + onBlur={(e: React.FocusEvent) => { + this.onBlur(e); + }} + onFocus={(e: React.FocusEvent) => { + this.onFocus(e); + }} + onMouseMove={(e: React.MouseEvent) => { + const tooltip = getFieldTooltip(instance); + tooltipMouseMove(e, tooltip[0], tooltip[1]); + }} + onMouseLeave={(e: React.MouseEvent) => { + const tooltip = getFieldTooltip(instance); + tooltipMouseLeave(e, tooltip[0], tooltip[1]); + }} + /> + {well} + {insideButton} + {dropdown} + {label} + {help} +
    + ); + } + + onMouseDown(e: React.MouseEvent): void { + e.stopPropagation(); + if (this.state.dropdownOpen) this.closeDropdown(e); + else { + this.openDropdownOnFocus = true; + } + + //icon click + if (e.target != this.input) { + e.preventDefault(); + if (!this.state.dropdownOpen) this.openDropdown(e); + else this.input.focus(); + } + } + + onFocus(e: React.FocusEvent): void { + if (this.openDropdownOnFocus) this.openDropdown(e); + + let { instance } = this.props; + let { widget } = instance; + const colorFieldWidget = widget as ColorField; + + if (colorFieldWidget.trackFocus) { + this.setState({ + focus: true, + }); + } + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + if ((instance.widget as ColorField).handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.enter: + e.stopPropagation(); + this.onChange((e.target as HTMLInputElement).value, "enter"); + break; + + case KeyCode.esc: + if (this.state.dropdownOpen) { + e.stopPropagation(); + this.closeDropdown(e, () => { + this.input.focus(); + }); + } + break; + + case KeyCode.left: + case KeyCode.right: + e.stopPropagation(); + break; + + case KeyCode.down: + this.openDropdown(e); + e.stopPropagation(); + e.preventDefault(); + break; + } + } + + onBlur(e: React.FocusEvent): void { + if (this.state.focus) + this.setState({ + focus: false, + }); + this.onChange((e.target as HTMLInputElement).value, "blur"); + } + + closeDropdown(e?: React.KeyboardEvent | React.MouseEvent, callback?: () => void): void { + if (this.state.dropdownOpen) { + if (this.scrollableParents) + this.scrollableParents.forEach((el) => { + el.removeEventListener("scroll", this.updateDropdownPosition); + }); + + this.setState({ dropdownOpen: false }, callback); + } else if (callback) callback(); + } + + openDropdown(e: React.KeyboardEvent | React.FocusEvent | React.MouseEvent): void { + let { data } = this.props; + this.openDropdownOnFocus = false; + + if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { + this.setState({ dropdownOpen: true }); + } + } + + trim(value: string): string { + return value.replace(/\s/g, ""); + } + + UNSAFE_componentWillReceiveProps(props: ColorInputProps): void { + let { data, instance } = props; + let { state } = instance; + let nv = this.trim((data.value as string) || ""); + if (nv != this.input.value && (this.data.value != data.value || !state?.inputError)) { + this.input.value = nv; + instance.setState({ + inputError: false, + }); + } + this.data = data; + + const tooltip1 = getFieldTooltip(instance); + tooltipParentWillReceiveProps(this.input, tooltip1[0], tooltip1[1]); + } + + componentDidMount(): void { + const tooltip2 = getFieldTooltip(this.props.instance); + tooltipParentDidMount(this.input, tooltip2[0], tooltip2[1]); + if ((this.props.instance.widget as ColorField).autoFocus && !isTouchDevice()) this.input.focus(); + } + + componentWillUnmount(): void { + if (this.input == getActiveElement() && this.input.value != this.props.data.value) { + this.onChange(this.input.value, "blur"); + } + tooltipParentWillUnmount(this.props.instance); + } + + onClearClick(e: React.MouseEvent): void { + let { instance } = this.props; + instance.set("value", (instance.widget as ColorField).emptyValue); + instance.setState({ + inputError: false, + }); + e.stopPropagation(); + e.preventDefault(); + } + + onChange(inputValue: string, eventType: string): void { + let { instance, data } = this.props; + let { widget } = instance; + + if (eventType == "blur") { + instance.setState({ visited: true }); + } + + let isValid; + try { + parseColor(inputValue); + isValid = true; + } catch (e) { + isValid = false; + } + + if (eventType == "blur" || eventType == "enter") { + let value = inputValue || (widget as ColorField).emptyValue; + if (isValid && value !== data.value) instance.set("value", value); + + instance.setState({ + inputError: !isValid && "Invalid color entered.", + }); + } + } +} diff --git a/packages/cx/src/widgets/form/ColorPicker.d.ts b/packages/cx/src/widgets/form/ColorPicker.d.ts deleted file mode 100644 index 459e6e6d6..000000000 --- a/packages/cx/src/widgets/form/ColorPicker.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Cx from '../../core'; -import { FieldProps } from './Field'; - -interface ColorPickerProps extends FieldProps { - - /** Either `rgba`, `hsla` or `hex` value of the selected color. */ - value?: Cx.StringProp, - - /** Base CSS class to be applied to the element. Defaults to `colorpicker`. */ - baseClass?: string, - - /** - * A string containing the list of all events that cause that selected value is written to the store. - * Default value is `blur change` which means that changes are propagated immediately. - */ - reportOn?: string, - - /** Format of the color representation. Either `rgba`, `hsla` or `hex`. */ - format?: "rgba" | "hsla" | "hex" - -} - -export class ColorPicker extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/ColorPicker.js b/packages/cx/src/widgets/form/ColorPicker.js deleted file mode 100644 index a534cb035..000000000 --- a/packages/cx/src/widgets/form/ColorPicker.js +++ /dev/null @@ -1,485 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Field } from "./Field"; -import { captureMouseOrTouch, getCursorPos } from "../overlay/captureMouse"; -import { hslToRgb } from "../../util/color/hslToRgb"; -import { rgbToHsl } from "../../util/color/rgbToHsl"; -import { rgbToHex } from "../../util/color/rgbToHex"; -import { parseColor } from "../../util/color/parseColor"; -import { getVendorPrefix } from "../../util/getVendorPrefix"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { isString } from "../../util/isString"; -import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; -import PixelPickerIcon from "../icons/pixel-picker"; - -//TODO: Increase HSL precision in calculations, round only RGB values -//TODO: Resolve alpha input problems - -export class ColorPicker extends Field { - declareData() { - super.declareData( - { - value: this.emptyValue, - format: undefined, - }, - ...arguments, - ); - } - - renderInput(context, instance, key) { - return ; - } - - handleEvent(eventType, instance, color) { - let { data } = instance; - if (this.reportOn.indexOf(eventType) != -1) { - let value; - switch (data.format) { - default: - case "rgba": - value = `rgba(${color.r.toFixed(0)},${color.g.toFixed(0)},${color.b.toFixed(0)},${ - Math.round(color.a * 100) / 100 - })`; - break; - - case "hsla": - value = `hsla(${color.h.toFixed(0)},${color.s.toFixed(0)}%,${color.l.toFixed(0)}%,${ - Math.round(color.a * 100) / 100 - })`; - break; - - case "hex": - value = rgbToHex(color.r, color.g, color.b); - break; - } - instance.set("value", value); - } - } -} - -ColorPicker.prototype.baseClass = "colorpicker"; -ColorPicker.prototype.reportOn = "blur change"; -ColorPicker.prototype.format = "rgba"; - -Widget.alias("color-picker", ColorPicker); - -class ColorPickerComponent extends VDOM.Component { - constructor(props) { - super(props); - this.data = props.instance.data; - try { - this.state = this.parse(props.instance.data.value); - } catch (e) { - //if web colors are used (e.g. red), fallback to the default color - this.state = this.parse(null); - } - } - - UNSAFE_componentWillReceiveProps(props) { - let { data } = props.instance; - let color; - try { - color = this.parse(data.value); - } catch { - color = this.parse(null); - } - if (color.r != this.state.r || color.g != this.state.g || color.b != this.state.b || color.a != this.state.a) - this.setState(color); - } - - parse(color) { - let c = parseColor(color); - if (c == null) { - c = { - type: "rgba", - r: 128, - g: 128, - b: 128, - a: 0, - }; - } - - c.a = Math.round(c.a * 100) / 100; - - if (c.type == "rgba") { - let [h, s, l] = rgbToHsl(c.r, c.g, c.b); - return { r: c.r, g: c.g, b: c.b, h, s, l, a: c.a }; - } - - if (c.type == "hsla") { - let [r, g, b] = hslToRgb(c.h, c.s, c.l); - r = this.fix255(r); - g = this.fix255(g); - b = this.fix255(b); - return { r: r, g, b, h: c.h, s: c.s, l: c.l, a: c.a }; - } - - throw new Error(`Color ${color} parsing failed.`); - } - - render() { - let { h, s, l, a, r, g, b } = this.state; - let { instance } = this.props; - let { widget, data } = instance; - let { CSS, baseClass } = widget; - let hcolor = `hsl(${h},100%,50%)`; - let hsla = `hsla(${h.toFixed(0)},${s.toFixed(0)}%,${l.toFixed(0)}%,${a})`; - let rgba = `rgba(${r.toFixed(0)},${g.toFixed(0)},${b.toFixed(0)},${a})`; - let hex = rgbToHex(r, g, b); - let pixelPicker; - - let alphaGradient = `${getVendorPrefix( - "css", - )}linear-gradient(left, hsla(${h},${s}%,${l}%,0) 0%, hsla(${h},${s}%,${l}%,1) 100%)`; - - if (window.EyeDropper) { - pixelPicker = ( -
    { - const eyeDropper = new EyeDropper(); - eyeDropper - .open() - .then((result) => { - instance.set("value", result.sRGBHex); - }) - .catch((e) => {}); - }} - > - -
    - ); - } - - return ( -
    -
    -
    -
    -
    -
    { - this.onWheel(e, "h", 10); - }} - > -
    -
    -
    - - - - -
    -
    { - this.onWheel(e, "a", 0.1); - }} - > -
    -
    -
    -
    - - - - -
    -
    -
    -
    { - this.onColorClick(e); - }} - > -
    -
    -
    -
    - - - -
    - {pixelPicker} -
    -
    -
    - ); - } - - onColorClick(e) { - let { instance } = this.props; - let { widget } = instance; - - if (widget.onColorClick) instance.invoke("onColorClick", e, instance); - } - - onHueSelect(e) { - e.preventDefault(); - e.stopPropagation(); - - let el = e.currentTarget; - let bounds = el.getBoundingClientRect(); - - let move = (e) => { - let pos = getCursorPos(e); - let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / el.offsetWidth)); - this.setColorProp({ - h: x * 360, - }); - }; - - captureMouseOrTouch(e, move); - move(e); - } - - onAlphaSelect(e) { - e.preventDefault(); - e.stopPropagation(); - - let el = e.currentTarget; - let bounds = getTopLevelBoundingClientRect(el); - - let move = (e) => { - let pos = getCursorPos(e); - let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / el.offsetWidth)); - this.setColorProp({ - a: x, - }); - }; - - captureMouseOrTouch(e, move); - move(e); - } - - onSLSelect(e) { - e.preventDefault(); - e.stopPropagation(); - - let el = e.currentTarget; - let bounds = getTopLevelBoundingClientRect(el); - - let move = (e) => { - let pos = getCursorPos(e); - let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / el.offsetWidth)); - let y = Math.max(0, Math.min(1, (pos.clientY + 1 - bounds.top) / el.offsetWidth)); - let s = x; - let l = 1 - y; - this.setColorProp({ - s: s * 100, - l: l * 100, - }); - }; - - captureMouseOrTouch(e, move); - move(e); - } - - fix255(v) { - return Math.max(0, Math.min(255, Math.round(v))); - } - - setColorProp(props, value) { - if (isString(props)) { - props = { - [props]: value, - }; - } - - let state = { ...this.state }; - let fixAlpha = false; - - for (let prop in props) { - value = props[prop]; - - switch (prop) { - case "h": - state.h = Math.min(360, Math.max(0, value)); - [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); - fixAlpha = true; - break; - - case "s": - state.s = Math.min(100, Math.max(0, value)); - [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); - fixAlpha = true; - break; - - case "l": - state.l = Math.min(100, Math.max(0, value)); - [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); - fixAlpha = true; - break; - - case "r": - case "g": - case "b": - state[prop] = Math.round(Math.min(255, Math.max(0, value))); - let [h, s, l] = rgbToHsl(state.r, state.g, state.b); - state.h = h; - state.s = s; - state.l = l; - fixAlpha = true; - break; - - case "a": - state.a = Math.round(100 * Math.min(1, Math.max(0, value))) / 100; - break; - } - } - - state.r = this.fix255(state.r); - state.g = this.fix255(state.g); - state.b = this.fix255(state.b); - - if (fixAlpha && state.a === 0) state.a = 1; - - this.setState(state, () => { - this.props.instance.widget.handleEvent("change", this.props.instance, this.state); - }); - } - - onNumberChange(e, prop) { - e.preventDefault(); - e.stopPropagation(); - let number = parseFloat(e.target.value || "0"); - this.setColorProp(prop, number); - } - - onWheel(e, prop, delta) { - e.preventDefault(); - e.stopPropagation(); - let factor = e.deltaY < 0 ? 1 : -1; - this.setColorProp(prop, this.state[prop] + delta * factor); - } - - onBlur() { - this.props.instance.widget.handleEvent("blur", this.props.instance, this.state); - } -} diff --git a/packages/cx/src/widgets/form/ColorPicker.scss b/packages/cx/src/widgets/form/ColorPicker.scss index 54b55efda..56e59ae4f 100644 --- a/packages/cx/src/widgets/form/ColorPicker.scss +++ b/packages/cx/src/widgets/form/ColorPicker.scss @@ -5,6 +5,8 @@ // } //} +@use "sass:map"; + @mixin cx-checker-background($tile-size: 4px, $color: rgba(gray, 0.5)) { background-image: linear-gradient(45deg, $color 25%, transparent 25%), linear-gradient(-45deg, $color 25%, transparent 25%), linear-gradient(45deg, transparent 75%, $color 75%), @@ -23,9 +25,9 @@ $placeholder: $cx-input-placeholder, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $size: 220px; diff --git a/packages/cx/src/widgets/form/ColorPicker.tsx b/packages/cx/src/widgets/form/ColorPicker.tsx new file mode 100644 index 000000000..8f8cc1422 --- /dev/null +++ b/packages/cx/src/widgets/form/ColorPicker.tsx @@ -0,0 +1,544 @@ +/** @jsxImportSource react */ +import { Widget, VDOM } from "../../ui/Widget"; +import { Field, FieldConfig, FieldInstance } from "./Field"; +import { captureMouseOrTouch, getCursorPos } from "../overlay/captureMouse"; +import { hslToRgb } from "../../util/color/hslToRgb"; +import { rgbToHsl } from "../../util/color/rgbToHsl"; +import { rgbToHex } from "../../util/color/rgbToHex"; +import { parseColor } from "../../util/color/parseColor"; +import { getVendorPrefix } from "../../util/getVendorPrefix"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { isString } from "../../util/isString"; +import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; +import PixelPickerIcon from "../icons/pixel-picker"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import { StringProp } from "../../ui/Prop"; + +// Type declaration for EyeDropper API +declare global { + class EyeDropper { + open(): Promise<{ sRGBHex: string }>; + } + interface Window { + EyeDropper: typeof EyeDropper; + } +} + +//TODO: Increase HSL precision in calculations, round only RGB values +//TODO: Resolve alpha input problems + +interface ColorState { + r: number; + g: number; + b: number; + h: number; + s: number; + l: number; + a: number; +} + +export interface ColorPickerConfig extends FieldConfig { + /** Either `rgba`, `hsla` or `hex` value of the selected color. */ + value?: StringProp; + + /** Format of the color representation. Either `rgba`, `hsla` or `hex`. */ + format?: "rgba" | "hsla" | "hex"; + + /** + * A string containing the list of all events that cause that selected value is written to the store. + * Default value is `blur change` which means that changes are propagated immediately. + */ + reportOn?: string; + + /** Callback function invoked when the color preview is clicked. */ + onColorClick?: (e: React.MouseEvent, instance: Instance) => void; +} + +export class ColorPicker extends Field> { + declare format: string; + declare reportOn: string; + declare onColorClick?: (e: React.MouseEvent, instance: FieldInstance) => void; + + constructor(config?: ColorPickerConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + value: this.emptyValue, + format: undefined, + }, + ...args, + ); + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + return ; + } + + handleEvent(eventType: string, instance: FieldInstance, color: ColorState): void { + let { data } = instance; + if (this.reportOn.indexOf(eventType) != -1) { + let value; + switch (data.format) { + default: + case "rgba": + value = `rgba(${color.r.toFixed(0)},${color.g.toFixed(0)},${color.b.toFixed(0)},${ + Math.round(color.a * 100) / 100 + })`; + break; + + case "hsla": + value = `hsla(${color.h.toFixed(0)},${color.s.toFixed(0)}%,${color.l.toFixed(0)}%,${ + Math.round(color.a * 100) / 100 + })`; + break; + + case "hex": + value = rgbToHex(color.r, color.g, color.b); + break; + } + instance.set("value", value); + } + } +} + +ColorPicker.prototype.baseClass = "colorpicker"; +ColorPicker.prototype.reportOn = "blur change"; +ColorPicker.prototype.format = "rgba"; + +Widget.alias("color-picker", ColorPicker); + +interface ColorPickerComponentProps { + instance: FieldInstance; +} + +class ColorPickerComponent extends VDOM.Component { + data: Record; + + constructor(props: ColorPickerComponentProps) { + super(props); + this.data = props.instance.data; + try { + this.state = this.parse(props.instance.data.value); + } catch (e) { + //if web colors are used (e.g. red), fallback to the default color + this.state = this.parse(null); + } + } + + UNSAFE_componentWillReceiveProps(props: ColorPickerComponentProps): void { + let { data } = props.instance; + let color; + try { + color = this.parse(data.value); + } catch { + color = this.parse(null); + } + if (color.r != this.state.r || color.g != this.state.g || color.b != this.state.b || color.a != this.state.a) + this.setState(color); + } + + parse(color: string | null): ColorState { + let c = parseColor(color); + if (c == null) { + c = { + type: "rgba", + r: 128, + g: 128, + b: 128, + a: 0, + }; + } + + c.a = Math.round(c.a * 100) / 100; + + if (c.type == "rgba") { + let [h, s, l] = rgbToHsl(c.r, c.g, c.b); + return { r: c.r, g: c.g, b: c.b, h, s, l, a: c.a }; + } + + if (c.type == "hsla") { + let [r, g, b] = hslToRgb(c.h, c.s, c.l); + r = this.fix255(r); + g = this.fix255(g); + b = this.fix255(b); + return { r: r, g, b, h: c.h, s: c.s, l: c.l, a: c.a }; + } + + throw new Error(`Color ${color} parsing failed.`); + } + + render(): React.ReactNode { + let { h, s, l, a, r, g, b } = this.state; + let { instance } = this.props; + let { widget, data } = instance; + let { CSS, baseClass } = widget; + let hcolor = `hsl(${h},100%,50%)`; + let hsla = `hsla(${h.toFixed(0)},${s.toFixed(0)}%,${l.toFixed(0)}%,${a})`; + let rgba = `rgba(${r.toFixed(0)},${g.toFixed(0)},${b.toFixed(0)},${a})`; + let hex = rgbToHex(r, g, b); + let pixelPicker; + + let alphaGradient = `${getVendorPrefix( + "css", + )}linear-gradient(left, hsla(${h},${s}%,${l}%,0) 0%, hsla(${h},${s}%,${l}%,1) 100%)`; + + if ("EyeDropper" in window && window.EyeDropper) { + pixelPicker = ( +
    { + const eyeDropper = new EyeDropper(); + eyeDropper + .open() + .then((result: { sRGBHex: string }) => { + instance.set("value", result.sRGBHex); + }) + .catch((e: any) => {}); + }} + > + +
    + ); + } + + return ( +
    +
    +
    +
    +
    +
    { + this.onWheel(e, "h", 10); + }} + > +
    +
    +
    + + + + +
    +
    { + this.onWheel(e, "a", 0.1); + }} + > +
    +
    +
    +
    + + + + +
    +
    +
    +
    { + this.onColorClick(e); + }} + > +
    +
    +
    +
    + + + +
    + {pixelPicker} +
    +
    +
    + ); + } + + onColorClick(e: React.MouseEvent): void { + let { instance } = this.props; + let { widget } = instance; + + if ((widget as ColorPicker).onColorClick) (widget as ColorPicker).onColorClick!(e, instance); + } + + onHueSelect(e: React.MouseEvent | React.TouchEvent): void { + e.preventDefault(); + e.stopPropagation(); + + let el = e.currentTarget; + let bounds = el.getBoundingClientRect(); + + let move = (e: MouseEvent) => { + let pos = getCursorPos(e); + let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / (el as HTMLElement).offsetWidth)); + this.setColorProp({ + h: x * 360, + }); + }; + + captureMouseOrTouch(e, move); + move(e as any); + } + + onAlphaSelect(e: React.MouseEvent | React.TouchEvent): void { + e.preventDefault(); + e.stopPropagation(); + + let el = e.currentTarget; + let bounds = getTopLevelBoundingClientRect(el); + + let move = (e: MouseEvent | React.MouseEvent) => { + let pos = getCursorPos(e); + let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / (el as HTMLElement).offsetWidth)); + this.setColorProp({ + a: x, + }); + }; + + captureMouseOrTouch(e, move); + move(e as any); + } + + onSLSelect(e: React.MouseEvent | React.TouchEvent): void { + e.preventDefault(); + e.stopPropagation(); + + let el = e.currentTarget; + let bounds = getTopLevelBoundingClientRect(el); + + let move = (e: MouseEvent) => { + let pos = getCursorPos(e); + let x = Math.max(0, Math.min(1, (pos.clientX + 1 - bounds.left) / (el as HTMLElement).offsetWidth)); + let y = Math.max(0, Math.min(1, (pos.clientY + 1 - bounds.top) / (el as HTMLElement).offsetWidth)); + let s = x; + let l = 1 - y; + this.setColorProp({ + s: s * 100, + l: l * 100, + }); + }; + + captureMouseOrTouch(e, move); + move(e as any); + } + + fix255(v: number): number { + return Math.max(0, Math.min(255, Math.round(v))); + } + + setColorProp(props: string | Partial, value?: number): void { + let propsObj: Partial; + if (isString(props)) { + propsObj = { + [props]: value, + }; + } else { + propsObj = props; + } + + let state = { ...this.state }; + let fixAlpha = false; + + for (let prop in propsObj) { + let propValue = propsObj[prop as keyof ColorState]; + if (propValue === undefined) continue; + + switch (prop) { + case "h": + state.h = Math.min(360, Math.max(0, propValue)); + [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); + fixAlpha = true; + break; + + case "s": + state.s = Math.min(100, Math.max(0, propValue)); + [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); + fixAlpha = true; + break; + + case "l": + state.l = Math.min(100, Math.max(0, propValue)); + [state.r, state.g, state.b] = hslToRgb(state.h, state.s, state.l); + fixAlpha = true; + break; + + case "r": + case "g": + case "b": + state[prop] = Math.round(Math.min(255, Math.max(0, propValue))); + let [h, s, l] = rgbToHsl(state.r, state.g, state.b); + state.h = h; + state.s = s; + state.l = l; + fixAlpha = true; + break; + + case "a": + state.a = Math.round(100 * Math.min(1, Math.max(0, propValue))) / 100; + break; + } + } + + state.r = this.fix255(state.r); + state.g = this.fix255(state.g); + state.b = this.fix255(state.b); + + if (fixAlpha && state.a === 0) state.a = 1; + + this.setState(state, () => { + (this.props.instance.widget as ColorPicker).handleEvent("change", this.props.instance, this.state); + }); + } + + onNumberChange(e: React.ChangeEvent, prop: keyof ColorState): void { + e.preventDefault(); + e.stopPropagation(); + let number = parseFloat(e.target.value || "0"); + this.setColorProp(prop, number); + } + + onWheel(e: React.WheelEvent, prop: keyof ColorState, delta: number): void { + e.preventDefault(); + e.stopPropagation(); + let factor = e.deltaY < 0 ? 1 : -1; + this.setColorProp(prop, this.state[prop] + delta * factor); + } + + onBlur(): void { + (this.props.instance.widget as ColorPicker).handleEvent("blur", this.props.instance, this.state); + } +} diff --git a/packages/cx/src/widgets/form/DateField.d.ts b/packages/cx/src/widgets/form/DateField.d.ts deleted file mode 100644 index bb6743dc0..000000000 --- a/packages/cx/src/widgets/form/DateField.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Cx from '../../core'; -import { DateTimeFieldProps } from './DateTimeField'; - -interface DateFieldProps extends DateTimeFieldProps {} - -export class DateField extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/DateField.js b/packages/cx/src/widgets/form/DateField.js deleted file mode 100644 index 831ab43df..000000000 --- a/packages/cx/src/widgets/form/DateField.js +++ /dev/null @@ -1,12 +0,0 @@ -import {Widget} from '../../ui/Widget'; -import {Localization} from '../../ui/Localization'; -import {DateTimeField} from './DateTimeField'; - -export class DateField extends DateTimeField {} - -DateField.prototype.picker = "calendar"; -DateField.prototype.segment = "date"; - -Widget.alias('datefield', DateField); -Localization.registerPrototype('cx/widgets/DateField', DateField); - diff --git a/packages/cx/src/widgets/form/DateField.ts b/packages/cx/src/widgets/form/DateField.ts new file mode 100644 index 000000000..dbe3b915f --- /dev/null +++ b/packages/cx/src/widgets/form/DateField.ts @@ -0,0 +1,21 @@ +import { Widget } from "../../ui/Widget"; +import { Localization } from "../../ui/Localization"; +import { DateTimeField, DateTimeFieldConfig } from "./DateTimeField"; + +export interface DateFieldConfig extends DateTimeFieldConfig {} + +export class DateField extends DateTimeField { + declare public picker: string; + declare public segment: string; + + constructor(config?: DateFieldConfig) { + super(config); + } +} + +DateField.prototype.picker = "calendar"; +DateField.prototype.segment = "date"; + +Widget.alias("datefield", DateField); +Localization.registerPrototype("cx/widgets/DateField", DateField); + diff --git a/packages/cx/src/widgets/form/DateTimeField.d.ts b/packages/cx/src/widgets/form/DateTimeField.d.ts deleted file mode 100644 index ff47f4ded..000000000 --- a/packages/cx/src/widgets/form/DateTimeField.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -export interface DateTimeFieldProps extends FieldProps { - /** Selected date. This should be a Date object or a valid date string consumable by Date.parse function. */ - value?: Cx.Prop; - - /** Defaults to false. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** Minimum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - minValue?: Cx.Prop; - - /** Set to `true` to disallow the `minValue`. Default value is `false`. */ - minExclusive?: Cx.BooleanProp; - - /** Maximum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - maxValue?: Cx.Prop; - - /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ - maxExclusive?: Cx.BooleanProp; - - /** Date format used to display the selected date. See Formatting for more details. */ - format?: Cx.StringProp; - - /** Base CSS class to be applied to the field. Defaults to `datefield`. */ - baseClass?: string; - - /** Maximum value error text. */ - maxValueErrorText?: string; - - /** Maximum exclusive value error text. */ - maxExclusiveErrorText?: string; - - /** Minimum value error text. */ - minValueErrorText?: string; - - /** Minimum exclusive value error text. */ - minExclusiveErrorText?: string; - - /** Error message used to indicate wrong user input, e.g. invalid date entered. */ - inputErrorText?: string; - - /** Name or configuration of the icon to be put on the left side of the input. */ - icon?: Cx.StringProp | Cx.Record; - - /** Set to false to hide the clear button. It can be used interchangeably with the hideClear property. Default value is true. */ - showClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** Set to true to hide the clear button. It can be used interchangeably with the showClear property. Default value is false. */ - hideClear?: boolean; - - /** Determines which segment of date/time is used. Default value is `datetime`. */ - segment?: "date" | "time" | "datetime"; - - /** Set to `true` to indicate that only one segment of the selected date is affected. */ - partial?: boolean; - - /** The function that will be used to convert Date objects before writing data to the store. - * Default implementation is Date.toISOString. - * See also Culture.setDefaultDateEncoding. - */ - encoding?: (date: Date) => any; - - /** Defines which days of week should be displayed as disabled, i.e. `[0, 6]` will make Sunday and Saturday unselectable. */ - disabledDaysOfWeek?: number[]; - - /** Set to true to focus the input field instead of the picker first. */ - focusInputFirst?: boolean; - - /** Set to true to enable seconds segment in the picker. */ - showSeconds?: boolean; - - /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ - dropdownOptions?: Cx.Config; -} - -export class DateTimeField extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/DateTimeField.js b/packages/cx/src/widgets/form/DateTimeField.js deleted file mode 100644 index b090b2374..000000000 --- a/packages/cx/src/widgets/form/DateTimeField.js +++ /dev/null @@ -1,576 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Cx } from "../../ui/Cx"; -import { Field, getFieldTooltip } from "./Field"; -import { DateTimePicker } from "./DateTimePicker"; -import { Calendar } from "./Calendar"; -import { Culture } from "../../ui/Culture"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { isTouchDevice } from "../../util/isTouchDevice"; -import { Dropdown } from "../overlay/Dropdown"; -import { StringTemplate } from "../../data/StringTemplate"; -import { zeroTime } from "../../util/date/zeroTime"; -import { dateDiff } from "../../util/date/dateDiff"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { KeyCode } from "../../util/KeyCode"; -import { Localization } from "../../ui/Localization"; -import DropdownIcon from "../icons/drop-down"; -import { Icon } from "../Icon"; -import ClearIcon from "../icons/clear"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { Format } from "../../util/Format"; -import { TimeList } from "./TimeList"; -import { autoFocus } from "../autoFocus"; -import { getActiveElement } from "../../util"; -import { parseDateInvariant } from "../../util"; - -export class DateTimeField extends Field { - declareData() { - super.declareData( - { - value: this.emptyValue, - disabled: undefined, - readOnly: undefined, - enabled: undefined, - placeholder: undefined, - required: undefined, - minValue: undefined, - minExclusive: undefined, - maxValue: undefined, - maxExclusive: undefined, - format: undefined, - icon: undefined, - autoOpen: undefined, - }, - ...arguments, - ); - } - - init() { - if (typeof this.hideClear !== "undefined") this.showClear = !this.hideClear; - - if (this.alwaysShowClear) this.showClear = true; - - if (!this.format) { - switch (this.segment) { - case "datetime": - this.format = "datetime;YYYYMMddhhmm"; - break; - - case "time": - this.format = "time;hhmm"; - break; - - case "date": - this.format = "date;yyyyMMMdd"; - break; - } - } - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - - if (data.value) { - let date = parseDateInvariant(data.value); - // let date = new Date(data.value); - - if (isNaN(date.getTime())) data.formatted = String(data.value); - else { - // handle utc edge cases - if (this.segment == "date") date = zeroTime(date); - data.formatted = Format.value(date, data.format); - } - data.date = date; - } else data.formatted = ""; - - if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); - - if (data.maxValue) data.maxValue = parseDateInvariant(data.maxValue); - - if (data.minValue) data.minValue = parseDateInvariant(data.minValue); - - if (this.segment == "date") { - if (data.minValue) data.minValue = zeroTime(data.minValue); - - if (data.maxValue) data.maxValue = zeroTime(data.maxValue); - } - - instance.lastDropdown = context.lastDropdown; - - super.prepareData(context, instance); - } - - validate(context, instance) { - super.validate(context, instance); - var { data, widget } = instance; - if (!data.error && data.date) { - if (isNaN(data.date)) data.error = this.inputErrorText; - else { - let d; - if (data.maxValue) { - d = dateDiff(data.date, data.maxValue); - if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); - else if (d == 0 && data.maxExclusive) - data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); - } - if (data.minValue) { - d = dateDiff(data.date, data.minValue); - if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); - else if (d == 0 && data.minExclusive) - data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); - } - if (widget.disabledDaysOfWeek) { - if (widget.disabledDaysOfWeek.includes(data.date.getDay())) - data.error = this.disabledDaysOfWeekErrorText; - } - } - } - } - - renderInput(context, instance, key) { - return ( - - ); - } - - formatValue(context, { data }) { - return data.value ? data.formatted : null; - } - - parseDate(date, instance) { - if (!date) return null; - if (date instanceof Date) return date; - if (this.onParseInput) { - let result = instance.invoke("onParseInput", date, instance); - if (result !== undefined) return result; - } - date = Culture.getDateTimeCulture().parse(date, { useCurrentDateForDefaults: true }); - return date; - } -} - -DateTimeField.prototype.baseClass = "datetimefield"; -DateTimeField.prototype.maxValueErrorText = "Select {0:d} or before."; -DateTimeField.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; -DateTimeField.prototype.minValueErrorText = "Select {0:d} or later."; -DateTimeField.prototype.minExclusiveErrorText = "Select a date after {0:d}."; -DateTimeField.prototype.inputErrorText = "Invalid date entered."; -DateTimeField.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed."; - -DateTimeField.prototype.suppressErrorsUntilVisited = true; -DateTimeField.prototype.icon = "calendar"; -DateTimeField.prototype.showClear = true; -DateTimeField.prototype.alwaysShowClear = false; -DateTimeField.prototype.reactOn = "enter blur"; -DateTimeField.prototype.segment = "datetime"; -DateTimeField.prototype.picker = "auto"; -DateTimeField.prototype.disabledDaysOfWeek = null; -DateTimeField.prototype.focusInputFirst = false; - -Widget.alias("datetimefield", DateTimeField); -Localization.registerPrototype("cx/widgets/DateTimeField", DateTimeField); - -class DateTimeInput extends VDOM.Component { - constructor(props) { - super(props); - props.instance.component = this; - this.state = { - dropdownOpen: false, - focus: false, - }; - } - - getDropdown() { - if (this.dropdown) return this.dropdown; - - let { widget, lastDropdown } = this.props.instance; - - let pickerConfig; - - switch (widget.picker) { - case "calendar": - pickerConfig = { - type: Calendar, - partial: widget.partial, - encoding: widget.encoding, - disabledDaysOfWeek: widget.disabledDaysOfWeek, - focusable: !widget.focusInputFirst, - }; - break; - - case "list": - pickerConfig = { - type: TimeList, - style: "height: 300px", - encoding: widget.encoding, - step: widget.step, - format: widget.format, - scrollSelectionIntoView: true, - }; - break; - - default: - pickerConfig = { - type: DateTimePicker, - segment: widget.segment, - encoding: widget.encoding, - showSeconds: widget.showSeconds, - }; - break; - } - - let dropdown = { - scrollTracking: true, - inline: !isTouchDevice() || !!lastDropdown, - matchWidth: false, - placementOrder: "down down-right down-left up up-right up-left", - touchFriendly: true, - firstChildDefinesHeight: true, - firstChildDefinesWidth: true, - ...widget.dropdownOptions, - type: Dropdown, - relatedElement: this.input, - onFocusOut: (e) => { - this.closeDropdown(e); - }, - onMouseDown: stopPropagation, - items: { - ...pickerConfig, - ...this.props.picker, - autoFocus: !widget.focusInputFirst, - tabIndex: widget.focusInputFirst ? -1 : 0, - onKeyDown: (e) => this.onKeyDown(e), - onSelect: (e, calendar, date) => { - e.stopPropagation(); - e.preventDefault(); - let touch = isTouchEvent(e); - this.closeDropdown(e, () => { - if (date) { - // If a blur event occurs before we re-render the input, - // the old input value is parsed and written to the store. - // We want to prevent that by eagerly updating the input value. - // This can happen if the date field is within a menu. - let newFormattedValue = Format.value(date, this.props.data.format); - this.input.value = newFormattedValue; - } - if (!touch) this.input.focus(); - }); - }, - }, - }; - - return (this.dropdown = Widget.create(dropdown)); - } - - render() { - let { instance, label, help, icon: iconVDOM } = this.props; - let { data, widget, state } = instance; - let { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let insideButton, icon; - - if (!data.readOnly && !data.disabled) { - if ( - widget.showClear && - (((widget.alwaysShowClear || !data.required) && !data.empty) || instance.state.inputError) - ) - insideButton = ( -
    { - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => this.onClearClick(e)} - > - -
    - ); - else - insideButton = ( -
    - -
    - ); - } - - if (iconVDOM) { - icon =
    {iconVDOM}
    ; - } - - let dropdown = false; - if (this.state.dropdownOpen) - dropdown = ( - - ); - - let empty = this.input ? !this.input.value : data.empty; - - return ( -
    - { - this.input = el; - }} - type="text" - className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} - style={data.inputStyle} - defaultValue={data.formatted} - disabled={data.disabled} - readOnly={data.readOnly} - tabIndex={data.tabIndex} - placeholder={data.placeholder} - {...data.inputAttrs} - onInput={(e) => this.onChange(e.target.value, "input")} - onChange={(e) => this.onChange(e.target.value, "change")} - onKeyDown={(e) => this.onKeyDown(e)} - onBlur={(e) => { - this.onBlur(e); - }} - onFocus={(e) => { - this.onFocus(e); - }} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} - /> - {icon} - {insideButton} - {dropdown} - {label} - {help} -
    - ); - } - - onMouseDown(e) { - e.stopPropagation(); - - if (this.state.dropdownOpen) { - this.closeDropdown(e); - } else { - this.openDropdownOnFocus = true; - } - - //icon click - if (e.target !== this.input) { - e.preventDefault(); - - //the field should not focus only in case when dropdown will open and autofocus - if (this.props.instance.widget.focusInputFirst || this.state.dropdownOpen) this.input.focus(); - - if (this.state.dropdownOpen) this.closeDropdown(e); - else this.openDropdown(e); - } - } - - onFocus(e) { - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - if (this.openDropdownOnFocus || widget.focusInputFirst) this.openDropdown(e); - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.enter: - this.onChange(e.target.value, "enter"); - break; - - case KeyCode.esc: - if (this.state.dropdownOpen) { - e.stopPropagation(); - this.closeDropdown(e, () => { - this.input.focus(); - }); - } - break; - - case KeyCode.left: - case KeyCode.right: - e.stopPropagation(); - break; - - case KeyCode.down: - this.openDropdown(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - } - - onBlur(e) { - if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); - else if (this.props.instance.widget.focusInputFirst) this.closeDropdown(e); - if (this.state.focus) - this.setState({ - focus: false, - }); - this.onChange(e.target.value, "blur"); - } - - closeDropdown(e, callback) { - if (this.state.dropdownOpen) { - if (this.scrollableParents) - this.scrollableParents.forEach((el) => { - el.removeEventListener("scroll", this.updateDropdownPosition); - }); - - this.setState({ dropdownOpen: false }, callback); - this.props.instance.setState({ visited: true }); - } else if (callback) callback(); - } - - openDropdown() { - let { data } = this.props.instance; - this.openDropdownOnFocus = false; - - if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { - this.setState({ - dropdownOpen: true, - dropdownOpenTime: Date.now(), - }); - } - } - - onClearClick(e) { - this.setValue(null); - e.stopPropagation(); - e.preventDefault(); - } - - UNSAFE_componentWillReceiveProps(props) { - let { data, state } = props.instance; - if (data.formatted !== this.input.value && (data.formatted !== this.props.data.formatted || !state.inputError)) { - this.input.value = data.formatted || ""; - props.instance.setState({ - inputError: false, - }); - } - - tooltipParentWillReceiveProps(this.input, ...getFieldTooltip(this.props.instance)); - } - - componentDidMount() { - tooltipParentDidMount(this.input, ...getFieldTooltip(this.props.instance)); - autoFocus(this.input, this); - if (this.props.data.autoOpen) this.openDropdown(); - } - - componentDidUpdate() { - autoFocus(this.input, this); - } - - componentWillUnmount() { - if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { - this.onChange(this.input.value, "blur"); - } - tooltipParentWillUnmount(this.props.instance); - } - - onChange(inputValue, eventType) { - let { instance, data } = this.props; - let { widget } = instance; - - if (data.disabled || data.readOnly) return; - - if (widget.reactOn.indexOf(eventType) === -1) return; - - if (eventType == "enter") instance.setState({ visited: true }); - - this.setValue(inputValue, data.value); - } - - setValue(text, baseValue) { - let { instance, data } = this.props; - let { widget } = instance; - - let date = widget.parseDate(text, instance); - - instance.setState({ - inputError: isNaN(date) && widget.inputErrorText, - }); - - if (!isNaN(date)) { - let mixed = parseDateInvariant(baseValue); - if (date && baseValue && !isNaN(mixed) && widget.partial) { - switch (widget.segment) { - case "date": - mixed.setFullYear(date.getFullYear()); - mixed.setMonth(date.getMonth()); - mixed.setDate(date.getDate()); - break; - - case "time": - mixed.setHours(date.getHours()); - mixed.setMinutes(date.getMinutes()); - mixed.setSeconds(date.getSeconds()); - break; - - default: - mixed = date; - break; - } - - date = mixed; - } - - let encode = widget.encoding || Culture.getDefaultDateEncoding(); - - let value = date ? encode(date) : widget.emptyValue; - - if (!instance.set("value", value)) this.input.value = value ? Format.value(date, data.format) : ""; - } - } -} diff --git a/packages/cx/src/widgets/form/DateTimeField.scss b/packages/cx/src/widgets/form/DateTimeField.scss index 9b67c2046..ff8627dbd 100644 --- a/packages/cx/src/widgets/form/DateTimeField.scss +++ b/packages/cx/src/widgets/form/DateTimeField.scss @@ -1,4 +1,6 @@ +@use "sass:map"; + @mixin cx-datetimefield( $name: 'datetimefield', $input-state-style-map: $cx-std-field-state-style-map, @@ -11,9 +13,9 @@ $width: $cx-default-input-width, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-field($besm, $input-state-style-map, $width: $width, $input: true); diff --git a/packages/cx/src/widgets/form/DateTimeField.tsx b/packages/cx/src/widgets/form/DateTimeField.tsx new file mode 100644 index 000000000..2b67cf443 --- /dev/null +++ b/packages/cx/src/widgets/form/DateTimeField.tsx @@ -0,0 +1,726 @@ +/** @jsxImportSource react */ +import { StringTemplate } from "../../data/StringTemplate"; +import { Culture } from "../../ui/Culture"; +import { Cx } from "../../ui/Cx"; +import type { DropdownInstance, Instance } from "../../ui/Instance"; +import { FieldInstance } from "./Field"; +import { Localization } from "../../ui/Localization"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { VDOM, Widget, getContent } from "../../ui/Widget"; +import { getActiveElement, parseDateInvariant } from "../../util"; +import { dateDiff } from "../../util/date/dateDiff"; +import { zeroTime } from "../../util/date/zeroTime"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { Format } from "../../util/Format"; +import { isTouchDevice } from "../../util/isTouchDevice"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { KeyCode } from "../../util/KeyCode"; +import { autoFocus } from "../autoFocus"; +import ClearIcon from "../icons/clear"; +import DropdownIcon from "../icons/drop-down"; +import { Dropdown, DropdownConfig } from "../overlay/Dropdown"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, +} from "../overlay/tooltip-ops"; +import { Calendar } from "./Calendar"; +import { DateTimePicker } from "./DateTimePicker"; +import { Field, FieldConfig, getFieldTooltip } from "./Field"; +import { TimeList } from "./TimeList"; +import { BooleanProp, Config, Prop, StringProp } from "../../ui/Prop"; + +export interface DateTimeFieldConfig extends FieldConfig { + /** Selected date. This should be a Date object or a valid date string consumable by Date.parse function. */ + value?: Prop; + + /** Defaults to false. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** The opposite of `disabled`. */ + enabled?: BooleanProp; + + /** Default text displayed when the field is empty. */ + placeholder?: StringProp; + + /** Minimum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ + minValue?: Prop; + + /** Set to `true` to disallow the `minValue`. Default value is `false`. */ + minExclusive?: BooleanProp; + + /** Maximum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ + maxValue?: Prop; + + /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ + maxExclusive?: BooleanProp; + + /** Date format used to display the selected date. */ + format?: StringProp; + + /** Base CSS class to be applied to the field. Defaults to `datefield`. */ + baseClass?: string; + + /** Maximum value error text. */ + maxValueErrorText?: string; + + /** Maximum exclusive value error text. */ + maxExclusiveErrorText?: string; + + /** Minimum value error text. */ + minValueErrorText?: string; + + /** Minimum exclusive value error text. */ + minExclusiveErrorText?: string; + + /** Error message used to indicate wrong user input. */ + inputErrorText?: string; + + /** Name or configuration of the icon to be put on the left side of the input. */ + icon?: StringProp | Config; + + /** Set to false to hide the clear button. Default value is true. */ + showClear?: boolean; + + /** Set to `true` to display the clear button even if `required` is set. Default is `false`. */ + alwaysShowClear?: boolean; + + /** Set to true to hide the clear button. Default value is false. */ + hideClear?: boolean; + + /** Determines which segment of date/time is used. Default value is `datetime`. */ + segment?: "date" | "time" | "datetime"; + + /** Set to `true` to indicate that only one segment of the selected date is affected. */ + partial?: boolean; + + /** The function that will be used to convert Date objects before writing data to the store. */ + encoding?: (date: Date) => any; + + /** Defines which days of week should be displayed as disabled. */ + disabledDaysOfWeek?: number[]; + + /** Set to true to focus the input field instead of the picker first. */ + focusInputFirst?: boolean; + + /** Set to true to enable seconds segment in the picker. */ + showSeconds?: boolean; + + /** Additional configuration to be passed to the dropdown. */ + dropdownOptions?: Partial; + + /** Custom validation function. */ + onValidate?: + | string + | ((value: string | Date, instance: Instance, validationParams: Record) => unknown); +} + +export class DateTimeField extends Field { + declare public showClear?: boolean; + declare public alwaysShowClear?: boolean; + declare public hideClear?: boolean; + declare public format?: string; + declare public segment?: string; + declare public maxValueErrorText?: string; + declare public maxExclusiveErrorText?: string; + declare public minValueErrorText?: string; + declare public minExclusiveErrorText?: string; + declare public inputErrorText?: string; + declare public disabledDaysOfWeekErrorText?: string; + declare public value?: unknown; + declare public minValue?: unknown; + declare public maxValue?: unknown; + declare public minExclusive?: boolean; + declare public maxExclusive?: boolean; + declare public picker?: string; + declare public partial?: boolean; + declare public encoding?: (date: Date) => string; + declare public disabledDaysOfWeek?: number[] | null; + declare public reactOn?: string; + declare public focusInputFirst?: boolean; + declare public dropdownOptions?: Partial; + declare public onParseInput?: string | ((date: unknown, instance: Instance) => Date | undefined); + declare public showSeconds?: boolean; + declare public step?: number; + + constructor(config?: DateTimeFieldConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + value: this.emptyValue, + disabled: undefined, + readOnly: undefined, + enabled: undefined, + placeholder: undefined, + required: undefined, + minValue: undefined, + minExclusive: undefined, + maxValue: undefined, + maxExclusive: undefined, + format: undefined, + icon: undefined, + autoOpen: undefined, + }, + ...args, + ); + } + + init(): void { + if (typeof this.hideClear !== "undefined") this.showClear = !this.hideClear; + + if (this.alwaysShowClear) this.showClear = true; + + if (!this.format) { + switch (this.segment) { + case "datetime": + this.format = "datetime;YYYYMMddhhmm"; + break; + + case "time": + this.format = "time;hhmm"; + break; + + case "date": + this.format = "date;yyyyMMMdd"; + break; + } + } + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + let { data } = instance; + let dropdownInstance = instance as DropdownInstance; + + if (data.value) { + let date = parseDateInvariant(data.value); + // let date = new Date(data.value); + + if (isNaN(date.getTime())) data.formatted = String(data.value); + else { + // handle utc edge cases + if (this.segment == "date") date = zeroTime(date); + data.formatted = Format.value(date, data.format); + } + data.date = date; + } else data.formatted = ""; + + if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); + + if (data.maxValue) data.maxValue = parseDateInvariant(data.maxValue); + + if (data.minValue) data.minValue = parseDateInvariant(data.minValue); + + if (this.segment == "date") { + if (data.minValue) data.minValue = zeroTime(data.minValue); + + if (data.maxValue) data.maxValue = zeroTime(data.maxValue); + } + + dropdownInstance.lastDropdown = context.lastDropdown; + + super.prepareData(context, instance); + } + + validate(context: RenderingContext, instance: FieldInstance): void { + super.validate(context, instance); + let { data, widget } = instance; + let dateTimeWidget = widget as DateTimeField; + + if (!data.error && data.date) { + if (isNaN(data.date)) data.error = this.inputErrorText; + else { + let d; + if (data.maxValue) { + d = dateDiff(data.date, data.maxValue); + if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText!, data.maxValue); + else if (d == 0 && data.maxExclusive) + data.error = StringTemplate.format(this.maxExclusiveErrorText!, data.maxValue); + } + if (data.minValue) { + d = dateDiff(data.date, data.minValue); + if (d < 0) data.error = StringTemplate.format(this.minValueErrorText!, data.minValue); + else if (d == 0 && data.minExclusive) + data.error = StringTemplate.format(this.minExclusiveErrorText!, data.minValue); + } + if (dateTimeWidget.disabledDaysOfWeek) { + if (dateTimeWidget.disabledDaysOfWeek.includes(data.date.getDay())) + data.error = this.disabledDaysOfWeekErrorText; + } + } + } + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + return ( + + ); + } + + formatValue(context: RenderingContext, { data }: Instance): React.ReactNode { + return data.value ? data.formatted : null; + } + + parseDate(date: unknown, instance: Instance): Date | null { + if (!date) return null; + if (date instanceof Date) return date; + if (this.onParseInput) { + let result = instance.invoke("onParseInput", date, instance); + if (result !== undefined) return result; + } + date = Culture.getDateTimeCulture().parse(date, { useCurrentDateForDefaults: true }) as Date; + return date as Date | null; + } +} + +DateTimeField.prototype.baseClass = "datetimefield"; +DateTimeField.prototype.maxValueErrorText = "Select {0:d} or before."; +DateTimeField.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; +DateTimeField.prototype.minValueErrorText = "Select {0:d} or later."; +DateTimeField.prototype.minExclusiveErrorText = "Select a date after {0:d}."; +DateTimeField.prototype.inputErrorText = "Invalid date entered."; +DateTimeField.prototype.disabledDaysOfWeekErrorText = "Selected day of week is not allowed."; + +DateTimeField.prototype.suppressErrorsUntilVisited = true; +DateTimeField.prototype.icon = "calendar"; +DateTimeField.prototype.showClear = true; +DateTimeField.prototype.alwaysShowClear = false; +DateTimeField.prototype.reactOn = "enter blur"; +DateTimeField.prototype.segment = "datetime"; +DateTimeField.prototype.picker = "auto"; +DateTimeField.prototype.disabledDaysOfWeek = null; +DateTimeField.prototype.focusInputFirst = false; + +Widget.alias("datetimefield", DateTimeField); +Localization.registerPrototype("cx/widgets/DateTimeField", DateTimeField); + +interface DateTimeInputProps { + instance: FieldInstance; + data: Record; + picker: Record; + label?: React.ReactNode; + help?: React.ReactNode; + icon?: React.ReactNode; +} + +interface DateTimeInputState { + dropdownOpen: boolean; + focus: boolean; + dropdownOpenTime?: number; +} + +class DateTimeInput extends VDOM.Component { + input!: HTMLInputElement; + dropdown?: Widget; + scrollableParents?: Element[]; + openDropdownOnFocus: boolean = false; + updateDropdownPosition: () => void; + scrolling?: boolean; + + constructor(props: DateTimeInputProps) { + super(props); + (props.instance as any).component = this; + this.state = { + dropdownOpen: false, + focus: false, + }; + this.updateDropdownPosition = () => {}; + } + + getDropdown(): Widget { + if (this.dropdown) return this.dropdown; + + let { widget, lastDropdown } = this.props.instance as DropdownInstance; + let dateTimeWidget = widget as DateTimeField; + + let pickerConfig; + + switch (dateTimeWidget.picker) { + case "calendar": + pickerConfig = { + type: Calendar, + partial: dateTimeWidget.partial, + encoding: dateTimeWidget.encoding, + disabledDaysOfWeek: dateTimeWidget.disabledDaysOfWeek, + focusable: !dateTimeWidget.focusInputFirst, + }; + break; + + case "list": + pickerConfig = { + type: TimeList, + style: "height: 300px", + encoding: dateTimeWidget.encoding, + step: dateTimeWidget.step, + format: dateTimeWidget.format, + scrollSelectionIntoView: true, + }; + break; + + default: + pickerConfig = { + type: DateTimePicker, + segment: dateTimeWidget.segment, + encoding: dateTimeWidget.encoding, + showSeconds: dateTimeWidget.showSeconds, + }; + break; + } + + let dropdown = { + scrollTracking: true, + inline: !isTouchDevice() || !!lastDropdown, + matchWidth: false, + placementOrder: "down down-right down-left up up-right up-left", + touchFriendly: true, + firstChildDefinesHeight: true, + firstChildDefinesWidth: true, + ...dateTimeWidget.dropdownOptions, + type: Dropdown, + relatedElement: this.input, + onFocusOut: (e: unknown) => { + this.closeDropdown(e); + }, + onMouseDown: stopPropagation, + items: { + ...pickerConfig, + ...this.props.picker, + autoFocus: !dateTimeWidget.focusInputFirst, + tabIndex: dateTimeWidget.focusInputFirst ? -1 : 0, + onKeyDown: (e: React.KeyboardEvent) => this.onKeyDown(e), + onSelect: (e: React.MouseEvent, calendar: any, date: Date) => { + e.stopPropagation(); + e.preventDefault(); + let touch = isTouchEvent(); + this.closeDropdown(e, () => { + if (date) { + // If a blur event occurs before we re-render the input, + // the old input value is parsed and written to the store. + // We want to prevent that by eagerly updating the input value. + // This can happen if the date field is within a menu. + let newFormattedValue = Format.value(date, this.props.data.format as string); + this.input.value = newFormattedValue; + } + if (!touch) this.input.focus(); + }); + }, + }, + }; + + return (this.dropdown = Widget.create(dropdown)); + } + + render(): React.ReactNode { + let { instance, label, help, icon: iconVDOM } = this.props; + let { data, widget, state } = instance; + let { CSS, baseClass, suppressErrorsUntilVisited, showClear, alwaysShowClear } = widget; + + let insideButton, icon; + + if (!data.readOnly && !data.disabled) { + if (showClear && (((alwaysShowClear || !data.required) && !data.empty) || instance.state?.inputError)) + insideButton = ( +
    { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={(e) => this.onClearClick(e)} + > + +
    + ); + else + insideButton = ( +
    + +
    + ); + } + + if (iconVDOM) { + icon =
    {iconVDOM}
    ; + } + + let dropdown: React.ReactNode | undefined; + if (this.state.dropdownOpen) + dropdown = ( + + ); + + let empty = this.input ? !this.input.value : data.empty; + + return ( +
    + { + this.input = el!; + }} + type="text" + className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} + style={data.inputStyle as React.CSSProperties} + defaultValue={data.formatted as string} + disabled={data.disabled as boolean} + readOnly={data.readOnly as boolean} + tabIndex={data.tabIndex as number} + placeholder={data.placeholder as string} + {...(data.inputAttrs as Record)} + onInput={(e) => this.onChange((e.target as HTMLInputElement).value, "input")} + onChange={(e) => this.onChange(e.target.value, "change")} + onKeyDown={(e) => this.onKeyDown(e)} + onBlur={(e) => { + this.onBlur(e); + }} + onFocus={(e) => { + this.onFocus(e); + }} + onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} + onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} + /> + {icon} + {insideButton} + {dropdown} + {label} + {help} +
    + ); + } + + onMouseDown(e: React.MouseEvent): void { + e.stopPropagation(); + let { widget } = this.props.instance; + + if (this.state.dropdownOpen) { + this.closeDropdown(e); + } else { + this.openDropdownOnFocus = true; + } + + //icon click + if (e.target !== this.input) { + e.preventDefault(); + + //the field should not focus only in case when dropdown will open and autofocus + if (widget.focusInputFirst || this.state.dropdownOpen) this.input.focus(); + + if (this.state.dropdownOpen) this.closeDropdown(e); + else this.openDropdown(); + } + } + + onFocus(e: React.FocusEvent): void { + let { instance } = this.props; + let { widget } = instance; + + if (widget.trackFocus) { + this.setState({ + focus: true, + }); + } + if (this.openDropdownOnFocus || widget.focusInputFirst) this.openDropdown(); + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + if (instance.widget.handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.enter: + this.onChange((e.target as HTMLInputElement).value, "enter"); + break; + + case KeyCode.esc: + if (this.state.dropdownOpen) { + e.stopPropagation(); + this.closeDropdown(e, () => { + this.input.focus(); + }); + } + break; + + case KeyCode.left: + case KeyCode.right: + e.stopPropagation(); + break; + + case KeyCode.down: + this.openDropdown(); + e.stopPropagation(); + e.preventDefault(); + break; + } + } + + onBlur(e: React.FocusEvent): void { + let { widget } = this.props.instance; + let dateTimeWidget = widget as DateTimeField; + + if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); + else if (dateTimeWidget.focusInputFirst) this.closeDropdown(e); + if (this.state.focus) + this.setState({ + focus: false, + }); + this.onChange(e.target.value, "blur"); + } + + closeDropdown(e: unknown, callback?: () => void): void { + if (this.state.dropdownOpen) { + if (this.scrollableParents) + this.scrollableParents.forEach((el) => { + el.removeEventListener("scroll", this.updateDropdownPosition); + }); + + this.setState({ dropdownOpen: false }, callback); + this.props.instance.setState({ visited: true }); + } else if (callback) callback(); + } + + openDropdown(): void { + let { data } = this.props.instance; + this.openDropdownOnFocus = false; + + if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { + this.setState({ + dropdownOpen: true, + dropdownOpenTime: Date.now(), + }); + } + } + + onClearClick(e: React.MouseEvent): void { + this.setValue(null); + e.stopPropagation(); + e.preventDefault(); + } + + UNSAFE_componentWillReceiveProps(props: DateTimeInputProps): void { + let { data, state } = props.instance; + if (data.formatted !== this.input.value && (data.formatted !== this.props.data.formatted || !state?.inputError)) { + this.input.value = data.formatted || ""; + props.instance.setState({ + inputError: false, + }); + } + + tooltipParentWillReceiveProps(this.input, ...getFieldTooltip(this.props.instance)); + } + + componentDidMount(): void { + tooltipParentDidMount(this.input, ...getFieldTooltip(this.props.instance)); + autoFocus(this.input, this); + if (this.props.data.autoOpen) this.openDropdown(); + } + + componentDidUpdate(): void { + autoFocus(this.input, this); + } + + componentWillUnmount(): void { + if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { + this.onChange(this.input.value, "blur"); + } + tooltipParentWillUnmount(this.props.instance); + } + + onChange(inputValue: string, eventType: string): void { + let { instance, data } = this.props; + let { widget } = instance; + let dateTimeWidget = widget as DateTimeField; + + if (data.disabled || data.readOnly) return; + + if (dateTimeWidget.reactOn!.indexOf(eventType) === -1) return; + + if (eventType == "enter") instance.setState({ visited: true }); + + this.setValue(inputValue, data.value); + } + + setValue(text: string | null, baseValue?: unknown): void { + let { instance, data } = this.props; + let { widget } = instance; + let dateTimeWidget = widget as DateTimeField; + + let date = dateTimeWidget.parseDate(text, instance); + + instance.setState({ + inputError: isNaN(date as any) && dateTimeWidget.inputErrorText, + }); + + if (!isNaN(date as any)) { + let mixed = parseDateInvariant(baseValue as string); + if (date && baseValue && !isNaN(mixed as any) && dateTimeWidget.partial) { + switch (dateTimeWidget.segment) { + case "date": + mixed.setFullYear(date!.getFullYear()); + mixed.setMonth(date!.getMonth()); + mixed.setDate(date!.getDate()); + break; + + case "time": + mixed.setHours(date!.getHours()); + mixed.setMinutes(date!.getMinutes()); + mixed.setSeconds(date!.getSeconds()); + break; + + default: + mixed = date; + break; + } + + date = mixed; + } + + let encode = dateTimeWidget.encoding || Culture.getDefaultDateEncoding(); + + let value = date ? encode!(date!) : dateTimeWidget.emptyValue; + + if (!instance.set("value", value)) this.input.value = value ? Format.value(date!, data.format as string) : ""; + } + } +} diff --git a/packages/cx/src/widgets/form/DateTimePicker.js b/packages/cx/src/widgets/form/DateTimePicker.js deleted file mode 100644 index cdc0cadb0..000000000 --- a/packages/cx/src/widgets/form/DateTimePicker.js +++ /dev/null @@ -1,392 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Culture } from "../../ui/Culture"; -import { KeyCode } from "../../util/KeyCode"; -import { WheelComponent } from "./Wheel"; -import { oneFocusOut, offFocusOut } from "../../ui/FocusManager"; - -import { enableCultureSensitiveFormatting } from "../../ui/Format"; -import { parseDateInvariant } from "../../util"; -enableCultureSensitiveFormatting(); - -export class DateTimePicker extends Widget { - declareData() { - return super.declareData(...arguments, { - value: undefined, - }); - } - - render(context, instance, key) { - return ( - - ); - } -} - -DateTimePicker.prototype.baseClass = "datetimepicker"; -DateTimePicker.prototype.styled = true; -DateTimePicker.prototype.size = 3; -DateTimePicker.prototype.autoFocus = false; -DateTimePicker.prototype.segment = "datetime"; -DateTimePicker.prototype.showSeconds = false; - -class DateTimePickerComponent extends VDOM.Component { - constructor(props) { - super(props); - let date = props.data.value ? parseDateInvariant(props.data.value) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.state = { - date: date, - activeWheel: null, - }; - - let { widget } = props.instance; - - this.handleChange = this.handleChange.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - - let showDate = props.segment.indexOf("date") !== -1; - let showTime = props.segment.indexOf("time") !== -1; - - this.wheels = { - year: showDate, - month: showDate, - date: showDate, - hours: showTime, - minutes: showTime, - seconds: showTime && widget.showSeconds, - }; - - this.keyDownPipes = {}; - } - - UNSAFE_componentWillReceiveProps(props) { - let date = props.data.value ? parseDateInvariant(props.data.value) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.setState({ date }); - } - - setDateComponent(date, component, value) { - let v = new Date(date); - switch (component) { - case "year": - v.setFullYear(value); - break; - - case "month": - v.setMonth(value); - break; - - case "date": - v.setDate(value); - break; - - case "hours": - v.setHours(value); - break; - - case "minutes": - v.setMinutes(value); - break; - - case "seconds": - v.setSeconds(value); - break; - } - return v; - } - - handleChange() { - let encode = this.props.instance.widget.encoding || Culture.getDefaultDateEncoding(); - this.props.instance.set("value", encode(this.state.date)); - } - - render() { - let { instance, data, size } = this.props; - let { widget } = instance; - let { CSS, baseClass } = widget; - let date = this.state.date; - - let culture = Culture.getDateTimeCulture(); - let monthNames = culture.getMonthNames("short"); - - let years = []; - for (let y = 1970; y <= 2050; y++) years.push({y}); - - let days = []; - let start = new Date(date.getFullYear(), date.getMonth(), 1); - while (start.getMonth() === date.getMonth()) { - let day = start.getDate(); - days.push({day < 10 ? "0" + day : day}); - start.setDate(start.getDate() + 1); - } - - let hours = []; - for (let h = 0; h < 24; h++) { - hours.push({h < 10 ? "0" + h : h}); - } - - let minutes = []; - for (let m = 0; m < 60; m++) { - minutes.push({m < 10 ? "0" + m : m}); - } - - return ( -
    { - this.el = el; - }} - className={data.classNames} - onFocus={this.onFocus} - onBlur={this.onBlur} - onKeyDown={this.onKeyDown} - > - {this.wheels.year && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "year", newIndex + 1970), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["year"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "year" }); - }} - > - {years} - - )} - {this.wheels.year && this.wheels.month && -} - {this.wheels.month && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "month", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["month"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "month" }); - }} - > - {monthNames.map((m, i) => ( - {m} - ))} - - )} - {this.wheels.month && this.wheels.date && -} - {this.wheels.date && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "date", newIndex + 1), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["date"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "date" }); - }} - > - {days} - - )} - {this.wheels.hours && this.wheels.year && } - {this.wheels.hours && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "hours", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["hours"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "hours" }); - }} - > - {hours} - - )} - {this.wheels.hours && this.wheels.minutes && :} - {this.wheels.minutes && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "minutes", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["minutes"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "minutes" }); - }} - > - {minutes} - - )} - {this.wheels.minutes && this.wheels.seconds && :} - {this.wheels.seconds && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "seconds", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["seconds"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "seconds" }); - }} - > - {minutes} - - )} -
    - ); - } - - componentDidMount() { - if (this.props.instance.widget.autoFocus) this.el.focus(); - } - - componentWillUnmount() { - offFocusOut(this); - } - - onFocus() { - oneFocusOut(this, this.el, this.onFocusOut.bind(this)); - - if (!this.state.activeWheel) { - let firstWheel = null; - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - firstWheel = wheel; - break; - } - } - - this.setState({ - activeWheel: firstWheel, - }); - } - } - - onFocusOut() { - let { instance } = this.props; - let { widget } = instance; - if (widget.onFocusOut) instance.invoke("onFocusOut", null, instance); - } - - onBlur() { - this.setState({ - activeWheel: null, - }); - } - - onKeyDown(e) { - let tmp = null; - let { instance } = this.props; - switch (e.keyCode) { - case KeyCode.right: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (tmp === this.state.activeWheel) { - this.setState({ activeWheel: wheel }); - break; - } - tmp = wheel; - } - } - break; - - case KeyCode.left: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (wheel === this.state.activeWheel && tmp) { - this.setState({ activeWheel: tmp }); - break; - } - tmp = wheel; - } - } - break; - - case KeyCode.enter: - e.preventDefault(); - if (instance.widget.onSelect) instance.invoke("onSelect", e, instance, this.state.date); - break; - - default: - let kdp = this.keyDownPipes[this.state.activeWheel]; - if (kdp) kdp(e); - break; - } - } -} diff --git a/packages/cx/src/widgets/form/DateTimePicker.scss b/packages/cx/src/widgets/form/DateTimePicker.scss index 168375bd8..633938feb 100644 --- a/packages/cx/src/widgets/form/DateTimePicker.scss +++ b/packages/cx/src/widgets/form/DateTimePicker.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-datetimepicker( $name: 'datetimepicker', $dtp-font-size: $cx-default-font-size, @@ -10,10 +12,10 @@ $icon-size: $cx-default-input-icon-size, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { background: $dtp-background-color; diff --git a/packages/cx/src/widgets/form/DateTimePicker.tsx b/packages/cx/src/widgets/form/DateTimePicker.tsx new file mode 100644 index 000000000..c0eacc8ab --- /dev/null +++ b/packages/cx/src/widgets/form/DateTimePicker.tsx @@ -0,0 +1,429 @@ +/** @jsxImportSource react */ +import { Culture } from "../../ui/Culture"; +import { offFocusOut, oneFocusOut } from "../../ui/FocusManager"; +import { enableCultureSensitiveFormatting } from "../../ui/Format"; +import type { Instance } from "../../ui/Instance"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { VDOM, Widget } from "../../ui/Widget"; +import { parseDateInvariant } from "../../util"; +import { KeyCode } from "../../util/KeyCode"; +import { WheelComponent } from "./Wheel"; + +enableCultureSensitiveFormatting(); + +export class DateTimePicker extends Widget { + declare public size: number; + declare public segment: string; + declare public autoFocus?: boolean; + declare public showSeconds?: boolean; + declare public encoding?: (date: Date) => string; + declare public onFocusOut?: string | ((instance: Instance) => void); + declare public onSelect?: string | ((e: React.KeyboardEvent, instance: Instance, date: Date) => void); + declare baseClass: string; + + declareData(...args: Record[]): void { + return super.declareData(...args, { + value: undefined, + }); + } + + render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + return ( + + ); + } +} + +DateTimePicker.prototype.baseClass = "datetimepicker"; +DateTimePicker.prototype.styled = true; +DateTimePicker.prototype.size = 3; +DateTimePicker.prototype.autoFocus = false; +DateTimePicker.prototype.segment = "datetime"; +DateTimePicker.prototype.showSeconds = false; + +interface DateTimePickerComponentProps { + instance: Instance; + data: Record; + size: number; + segment: string; +} + +interface DateTimePickerComponentState { + date: Date; + activeWheel: string | null; +} + +class DateTimePickerComponent extends VDOM.Component { + el!: HTMLDivElement; + declare wheels: Record; + keyDownPipes: Record void>; + + constructor(props: DateTimePickerComponentProps) { + super(props); + let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.state = { + date: date, + activeWheel: null, + }; + + let { widget } = props.instance; + let pickerWidget = widget as DateTimePicker; + + this.handleChange = this.handleChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + + let showDate = props.segment.indexOf("date") !== -1; + let showTime = props.segment.indexOf("time") !== -1; + + this.wheels = { + year: showDate, + month: showDate, + date: showDate, + hours: showTime, + minutes: showTime, + seconds: showTime && !!pickerWidget.showSeconds, + }; + + this.keyDownPipes = {}; + } + + UNSAFE_componentWillReceiveProps(props: DateTimePickerComponentProps): void { + let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.setState({ date }); + } + + setDateComponent(date: Date, component: string, value: number): Date { + let v = new Date(date); + switch (component) { + case "year": + v.setFullYear(value); + break; + + case "month": + v.setMonth(value); + break; + + case "date": + v.setDate(value); + break; + + case "hours": + v.setHours(value); + break; + + case "minutes": + v.setMinutes(value); + break; + + case "seconds": + v.setSeconds(value); + break; + } + return v; + } + + handleChange(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + let encode = pickerWidget.encoding || Culture.getDefaultDateEncoding(); + this.props.instance.set("value", encode!(this.state.date)); + } + + render(): React.ReactNode { + let { instance, data, size } = this.props; + let { widget } = instance; + let { CSS, baseClass } = widget; + let pickerWidget = widget as DateTimePicker; + let date = this.state.date; + + let culture = Culture.getDateTimeCulture(); + let monthNames = culture.getMonthNames("short"); + + let years = []; + for (let y = 1970; y <= 2050; y++) years.push({y}); + + let days = []; + let start = new Date(date.getFullYear(), date.getMonth(), 1); + while (start.getMonth() === date.getMonth()) { + let day = start.getDate(); + days.push({day < 10 ? "0" + day : day}); + start.setDate(start.getDate() + 1); + } + + let hours = []; + for (let h = 0; h < 24; h++) { + hours.push({h < 10 ? "0" + h : h}); + } + + let minutes = []; + for (let m = 0; m < 60; m++) { + minutes.push({m < 10 ? "0" + m : m}); + } + + return ( +
    { + this.el = el!; + }} + className={data.classNames as string} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + > + {this.wheels.year && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "year", newIndex + 1970), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["year"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "year" }); + }} + > + {years} + + )} + {this.wheels.year && this.wheels.month && -} + {this.wheels.month && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "month", newIndex), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["month"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "month" }); + }} + > + {monthNames.map((m: string, i: number) => ( + {m} + ))} + + )} + {this.wheels.month && this.wheels.date && -} + {this.wheels.date && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "date", newIndex + 1), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["date"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "date" }); + }} + > + {days} + + )} + {this.wheels.hours && this.wheels.year && } + {this.wheels.hours && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "hours", newIndex), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["hours"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "hours" }); + }} + > + {hours} + + )} + {this.wheels.hours && this.wheels.minutes && :} + {this.wheels.minutes && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "minutes", newIndex), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["minutes"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "minutes" }); + }} + > + {minutes} + + )} + {this.wheels.minutes && this.wheels.seconds && :} + {this.wheels.seconds && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent(this.state.date, "seconds", newIndex), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["seconds"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "seconds" }); + }} + > + {minutes} + + )} +
    + ); + } + + componentDidMount(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.autoFocus) this.el.focus(); + } + + componentWillUnmount(): void { + offFocusOut(this); + } + + onFocus(): void { + oneFocusOut(this, this.el, this.onFocusOut.bind(this)); + + if (!this.state.activeWheel) { + let firstWheel: string | null = null; + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + firstWheel = wheel; + break; + } + } + + this.setState({ + activeWheel: firstWheel, + }); + } + } + + onFocusOut(): void { + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); + } + + onBlur(): void { + this.setState({ + activeWheel: null, + }); + } + + onKeyDown(e: React.KeyboardEvent): void { + let tmp: string | null = null; + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + + switch (e.keyCode) { + case KeyCode.right: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (tmp === this.state.activeWheel) { + this.setState({ activeWheel: wheel }); + break; + } + tmp = wheel; + } + } + break; + + case KeyCode.left: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (wheel === this.state.activeWheel && tmp) { + this.setState({ activeWheel: tmp }); + break; + } + tmp = wheel; + } + } + break; + + case KeyCode.enter: + e.preventDefault(); + if (pickerWidget.onSelect) instance.invoke("onSelect", e, instance, this.state.date); + break; + + default: let kdp = this.keyDownPipes[this.state.activeWheel!]; + if (kdp) kdp(e); + break; + } + } +} diff --git a/packages/cx/src/widgets/form/Field.d.ts b/packages/cx/src/widgets/form/Field.d.ts deleted file mode 100644 index 62b1c79ef..000000000 --- a/packages/cx/src/widgets/form/Field.d.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as Cx from "../../core"; -import { Instance } from "../../ui"; - -export interface FieldProps extends Cx.StyledContainerProps { - /** Field label. For advanced use cases. */ - label?: Cx.StringProp | Cx.Config; - - /** Set to `material` to use custom label placement instruction. Used in Material theme to implement animated labels. */ - labelPlacement?: "material"; - - /** Set to `material` to use custom help placement instruction. Used in Material theme to implement absolutely positioned validation messages. */ - helpPlacement?: "material"; - - /* Deprecated */ - labelWidth?: any; - - /** Either `view` or `edit` (default). In `view` mode, the field is displayed as plain text. */ - mode?: Cx.Prop<"view" | "edit">; - - /** Set to `true` to switch to widget view mode. Same as `mode="view". Default is false. */ - viewMode?: Cx.BooleanProp; - - id?: Cx.Prop; - - /** Used for validation. If error evaluates to non-null, the field is marked in red. */ - error?: Cx.StringProp; - - /** Style object applied to the input element. Used for setting visual elements, such as borders and backgrounds. */ - inputStyle?: Cx.StyleProp; - - /** Additional CSS class applied to the input element. Used for setting visual elements, such as borders and backgrounds. */ - inputClass?: Cx.ClassProp; - - /** Additional attributes that should be rendered on the input element. E.g. inputAttrs={{ autoComplete: "off" }}. */ - inputAttrs?: Cx.Config; - - /** Text to be rendered in view mode when the field is empty. */ - emptyText?: Cx.StringProp; - - /** Set to `true` to make error indicators visible in pristine state. By default, validation errors are not shown until the user visits the field. */ - visited?: Cx.BooleanProp; - - /** Set to `true` to automatically focus the field, after it renders for the first time. */ - autoFocus?: Cx.BooleanProp; - - /** Defines how to present validation errors. Default mode is `tooltip`. Other options are `help` and `help-block`. */ - validationMode?: "tooltip" | "help" | "help-block"; - - /** Defaults to `false`. Set to `true` to disable the field. */ - disabled?: Cx.BooleanProp; - - /** Defaults to `false`. Used to make the field required. */ - required?: Cx.BooleanProp; - - /** Renamed to suppressErrorsUntilVisited. Used to indicate that required fields should not be marked as invalid before the user visits them. */ - suppressErrorTooltipsUntilVisited?: boolean; - - /** Error message used to indicate that field is required. */ - requiredText?: string; - - /** Append asterisk to the label to indicate a required field. */ - asterisk?: Cx.BooleanProp; - - /** Text displayed to the user to indicate that server-side validation is in progress. */ - validatingText?: string; - - /** Text displayed to the user to indicate that server-side validation has thrown an exception. */ - validationExceptionText?: string; - - /** Configuration of the toolitp used to indicate validation errors. */ - errorTooltip?: Cx.Config; - - /** Tooltip configuration. */ - tooltip?: Cx.StringProp | Cx.StructuredProp; - - /** Indicates that `help` should be separated from the input with a whitespace. Default is `true`. */ - helpSpacer?: boolean; - - /** - * If set to `true` top level element will get additional CSS class indicating that input is focused. - * Used for adding special effects on focus. Default is `false`. - */ - trackFocus?: boolean; - - /** Custom tab index */ - tabIndex?: Cx.StringProp; - - /** - * Additional content to be displayed next to the field. - * This is commonly used for presenting additional information or validation errors. - */ - help?: string | Cx.Config; - - /** Custom validation function. */ - onValidate?: string | ((value, instance: Instance, validationParams) => any); - - /** Validation parameters to be passed to the validation function. */ - validationParams?: Cx.Config; - - onValidationException?: string | ((error: any, instance: Instance) => void); - - /** Value to be set in the store if the field is empty. Default value is null. */ - emptyValue?: any; - - /** Additional CSS style to be passed to the label object. */ - labelStyle?: Cx.StyleProp; - - /** Additional CSS class to be passed to the label object. */ - labelClass?: Cx.ClassProp; -} - -export class Field extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Field.js b/packages/cx/src/widgets/form/Field.js deleted file mode 100644 index 09d74f371..000000000 --- a/packages/cx/src/widgets/form/Field.js +++ /dev/null @@ -1,446 +0,0 @@ -import { VDOM, getContent } from "../../ui/Widget"; -import { PureContainer } from "../../ui/PureContainer"; -import { ValidationError } from "./ValidationError"; -import { HelpText } from "./HelpText"; -import { Label } from "./Label"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { isSelector } from "../../data/isSelector"; -import { Localization } from "../../ui/Localization"; -import { isPromise } from "../../util/isPromise"; -import { Console } from "../../util/Console"; -import { parseStyle } from "../../util/parseStyle"; -import { FocusManager } from "../../ui/FocusManager"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops"; -import { coalesce } from "../../util/coalesce"; -import { isUndefined } from "../../util/isUndefined"; -import { shallowEquals } from "../../util/shallowEquals"; -import { FieldIcon } from "./FieldIcon"; - -export class Field extends PureContainer { - declareData() { - super.declareData( - { - label: undefined, - labelWidth: undefined, - mode: undefined, - viewMode: undefined, - id: undefined, - error: undefined, - inputStyle: { structured: true }, - inputClass: { structured: true }, - inputAttrs: { structured: true }, - emptyText: undefined, - visited: undefined, - autoFocus: undefined, - tabOnEnterKey: undefined, - tabIndex: undefined, - validationParams: { structured: true }, - }, - ...arguments, - ); - } - - init() { - this.inputStyle = parseStyle(this.inputStyle); - super.init(); - } - - initComponents(context, instance) { - if (this.validationMode == "tooltip" && isUndefined(this.errorTooltip)) { - this.errorTooltip = { - text: { bind: "$error" }, - mod: "error", - ...this.errorTooltip, - }; - } - - if (isUndefined(this.help)) { - switch (this.validationMode) { - case "help": - case "help-inline": - this.help = ValidationError; - break; - - case "help-block": - this.help = { - type: ValidationError, - mod: "block", - }; - break; - } - } - - if (this.help != null) { - let helpConfig = {}; - - if (this.help.isComponentType) helpConfig = this.help; - else if (isSelector(this.help)) helpConfig.text = this.help; - else Object.assign(helpConfig, this.help); - - this.help = HelpText.create(helpConfig); - } - - if (this.label != null) { - let labelConfig = { - mod: this.mod, - disabled: this.disabled, - required: this.required, - asterisk: this.asterisk, - style: this.labelStyle, - class: this.labelClass, - }; - - if (this.label.isComponentType) labelConfig = this.label; - else if (isSelector(this.label)) labelConfig.text = this.label; - else Object.assign(labelConfig, this.label); - - this.label = Label.create(labelConfig); - } - - if (this.icon != null) { - let iconConfig = { - className: this.CSS.element(this.baseClass, "icon"), - }; - if (isSelector(this.icon)) iconConfig.name = this.icon; - else Object.assign(iconConfig, this.icon); - - this.icon = FieldIcon.create(iconConfig); - } - - return super.initComponents(...arguments, { - label: this.label, - help: this.help, - icon: this.icon, - }); - } - - initState(context, instance) { - instance.state = { - inputError: false, - visited: this.visited === true, - }; - } - - prepareData(context, instance) { - let { data, state } = instance; - if (!data.id) data.id = "fld-" + instance.id; - - data._disabled = data.disabled; - data._readOnly = data.readOnly; - data._viewMode = data.mode === "view" || data.viewMode; - data._tabOnEnterKey = data.tabOnEnterKey; - data.validationValue = this.getValidationValue(data); - instance.parentDisabled = context.parentDisabled; - instance.parentReadOnly = context.parentReadOnly; - instance.parentViewMode = context.parentViewMode; - instance.parentTabOnEnterKey = context.parentTabOnEnterKey; - instance.parentVisited = context.parentVisited; - - if (typeof data.enabled !== "undefined") data._disabled = !data.enabled; - - this.disableOrValidate(context, instance); - - data.inputStyle = parseStyle(data.inputStyle); - - if (this.labelPlacement && this.label) data.mod = [data.mod, "label-placement-" + this.labelPlacement]; - - if (this.helpPlacement && this.help) data.mod = [data.mod, "help-placement-" + this.helpPlacement]; - - data.empty = this.isEmpty(data); - - super.prepareData(...arguments); - } - - disableOrValidate(context, instance) { - let { data, state } = instance; - - //if the parent is strict and sets some flag to true, it is not allowed to overrule that flag by field settings - - data.disabled = coalesce( - context.parentStrict ? context.parentDisabled : null, - data._disabled, - context.parentDisabled, - ); - data.readOnly = coalesce( - context.parentStrict ? context.parentReadOnly : null, - data._readOnly, - context.parentReadOnly, - ); - data.viewMode = coalesce( - context.parentStrict ? context.parentViewMode : null, - data._viewMode, - context.parentViewMode, - ); - data.tabOnEnterKey = coalesce( - context.parentStrict ? context.parentTabOnEnterKey : null, - data._tabOnEnterKey, - context.parentTabOnEnterKey, - ); - data.visited = coalesce(context.parentStrict ? context.parentVisited : null, data.visited, context.parentVisited); - - if (!data.error && !data.disabled && !data.viewMode) this.validate(context, instance); - - if (data.visited && !state.visited) { - //feels hacky but it should be ok since we're in the middle of a new render cycle - state.visited = true; - } - - data.stateMods = { - ...data.stateMods, - disabled: data.disabled, - "edit-mode": !data.viewMode, - "view-mode": data.viewMode, - }; - } - - explore(context, instance) { - let { data, state } = instance; - - instance.parentDisabled = context.parentDisabled; - instance.parentReadOnly = context.parentReadOnly; - instance.parentViewMode = context.parentViewMode; - instance.parentTabOnEnterKey = context.parentTabOnEnterKey; - instance.parentVisited = context.parentVisited; - - if ( - instance.cache("parentDisabled", context.parentDisabled) || - instance.cache("parentReadOnly", context.parentReadOnly) || - instance.cache("parentViewMode", context.parentViewMode) || - instance.cache("parentTabOnEnterKey", context.parentTabOnEnterKey) || - instance.cache("parentVisited", context.parentVisited) - ) { - instance.markShouldUpdate(context); - this.disableOrValidate(context, instance); - this.prepareCSS(context, instance); - } - - if (!context.validation) - context.validation = { - errors: [], - }; - - if (data.error) { - context.validation.errors.push({ - fieldId: data.id, - message: data.error, - visited: state.visited, - type: "error", - }); - } - - context.push("lastFieldId", data.id); - super.explore(context, instance); - } - - exploreCleanup(context, instance) { - context.pop("lastFieldId"); - } - - isEmpty(data) { - return data.value == null || data.value === this.emptyValue; - } - - validateRequired(context, instance) { - let { data } = instance; - if (this.isEmpty(data)) return this.requiredText; - } - - getValidationValue(data) { - return data.value; - } - - validate(context, instance) { - let { data, state } = instance; - state = state || {}; - - let empty = this.isEmpty(data); - - if (!data.error) { - if (state.inputError) data.error = state.inputError; - else if (state.validating && !empty) data.error = this.validatingText; - else if ( - state.validationError && - data.validationValue === state.lastValidatedValue && - shallowEquals(data.validationParams, state.lastValidationParams) - ) - data.error = state.validationError; - else if (data.required) data.error = this.validateRequired(context, instance); - } - - if ( - !empty && - !state.validating && - !data.error && - this.onValidate && - (!state.previouslyValidated || - data.validationValue != state.lastValidatedValue || - data.validationParams != state.lastValidationParams) - ) { - let result = instance.invoke("onValidate", data.validationValue, instance, data.validationParams); - if (isPromise(result)) { - data.error = this.validatingText; - instance.setState({ - validating: true, - lastValidatedValue: data.validationValue, - previouslyValidated: true, - lastValidationParams: data.validationParams, - }); - result - .then((r) => { - let { data, state } = instance; - let error = - data.validationValue == state.lastValidatedValue && - shallowEquals(data.validationParams, state.lastValidationParams) - ? r - : this.validatingText; //parameters changed, this will be revalidated - - instance.setState({ - validating: false, - validationError: error, - }); - }) - .catch((e) => { - instance.setState({ - validating: false, - validationError: this.validationExceptionText, - }); - if (this.onValidationException) instance.invoke("onValidationException", e, instance); - else { - Console.warn("Unhandled validation exception:", e); - } - }); - } else { - data.error = result; - } - } - } - - renderLabel(context, instance, key) { - if (instance.components.label) return getContent(instance.components.label.vdom); - } - - renderInput(context, instance, key) { - throw new Error("Not implemented."); - } - - renderHelp(context, instance, key) { - if (instance.components.help) return getContent(instance.components.help.render(context, key)); - } - - renderIcon(context, instance, key) { - if (instance.components.icon) return getContent(instance.components.icon.render(context, key)); - } - - formatValue(context, { data }) { - return data.text || data.value; - } - - renderValue(context, instance, key) { - let text = this.formatValue(context, instance); - if (text) { - return ( - tooltipMouseMove(e, ...getFieldTooltip(instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(instance))} - > - {text} - - ); - } - } - - renderContent(context, instance, key) { - let content = this.renderValue(...arguments) || this.renderEmptyText(...arguments); - return this.renderWrap(context, instance, key, content); - } - - renderWrap(context, instance, key, content) { - let { data } = instance; - let interactive = !data.viewMode && !data.disabled; - return ( -
    - {content} - {this.labelPlacement && this.renderLabel(context, instance, "label")} -
    - ); - } - - renderEmptyText(context, { data }, key) { - return ( - - {data.emptyText ||  } - - ); - } - - render(context, instance, key) { - let { data } = instance; - let content = !data.viewMode - ? this.renderInput(context, instance, key) - : this.renderContent(context, instance, key); - - return { - label: !this.labelPlacement && this.renderLabel(context, instance, key), - content: content, - helpSpacer: this.helpSpacer && instance.components.help ? " " : null, - help: !this.helpPlacement && this.renderHelp(context, instance, key), - }; - } - - handleKeyDown(e, instance) { - if (this.onKeyDown && instance.invoke("onKeyDown", e, instance) === false) return false; - - if (instance.data.tabOnEnterKey && e.keyCode === 13) { - let target = e.target; - setTimeout(() => { - if (!instance.state.inputError) FocusManager.focusNext(target); - }, 10); - } - } -} - -Field.prototype.validationMode = "tooltip"; -Field.prototype.suppressErrorsUntilVisited = false; -Field.prototype.requiredText = "This field is required."; -Field.prototype.autoFocus = false; -Field.prototype.asterisk = false; -Field.prototype.validatingText = "Validation is in progress..."; -Field.prototype.validationExceptionText = "Something went wrong during input validation. Check log for more details."; -Field.prototype.helpSpacer = true; -Field.prototype.trackFocus = false; //add cxs-focus on parent element -Field.prototype.labelPlacement = false; -Field.prototype.helpPlacement = false; -Field.prototype.emptyValue = null; -Field.prototype.styled = true; - -//These flags are inheritable and should not be set to false -//Field.prototype.visited = null; -//Field.prototype.disabled = null; -//Field.prototype.readOnly = null; -//Field.prototype.viewMode = null; - -Localization.registerPrototype("cx/widgets/Field", Field); - -export function getFieldTooltip(instance) { - let { widget, data, state } = instance; - - if (widget.errorTooltip && data.error && (!state || state.visited || !widget.suppressErrorsUntilVisited)) - return [ - instance, - widget.errorTooltip, - { - data: { - $error: data.error, - }, - }, - ]; - return [instance, widget.tooltip]; -} diff --git a/packages/cx/src/widgets/form/Field.scss b/packages/cx/src/widgets/form/Field.scss index aca29ab28..10e7d8667 100644 --- a/packages/cx/src/widgets/form/Field.scss +++ b/packages/cx/src/widgets/form/Field.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cxb-field($besm, $state-style-map, $input: true, $box: false, $width: null) { position: relative; display: inline-block; @@ -37,7 +39,7 @@ height: cx-get-state-rule($state-style-map, default, "height"); } - $state: map-get($besm, state); + $state: map.get($besm, state); &.#{$state}view-mode { line-height: $line-height; @@ -49,7 +51,7 @@ } @mixin cxe-field-input($besm, $state-style-map, $overrides: null, $placeholder: null, $width: 100%, $input: true) { - $state: map-get($besm, state); + $state: map.get($besm, state); $styles: cx-deep-map-merge($state-style-map, $overrides); diff --git a/packages/cx/src/widgets/form/Field.tsx b/packages/cx/src/widgets/form/Field.tsx new file mode 100644 index 000000000..6f8a98b3a --- /dev/null +++ b/packages/cx/src/widgets/form/Field.tsx @@ -0,0 +1,608 @@ +/** @jsxImportSource react */ +import { TooltipConfig, TooltipOptions, TooltipParentInstance } from "../overlay/tooltip-ops"; +import { isSelector } from "../../data/isSelector"; +import { FocusManager } from "../../ui/FocusManager"; +import { Instance, PartialInstance } from "../../ui/Instance"; +import { Localization } from "../../ui/Localization"; +import { PureContainerBase, PureContainerConfig } from "../../ui/PureContainer"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { getContent, WidgetStyleConfig } from "../../ui/Widget"; +import { coalesce } from "../../util/coalesce"; +import { Console } from "../../util/Console"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { isPromise } from "../../util/isPromise"; +import { isUndefined } from "../../util/isUndefined"; +import { parseStyle } from "../../util/parseStyle"; +import { shallowEquals } from "../../util/shallowEquals"; +import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops"; +import { FieldIcon } from "./FieldIcon"; +import { HelpText } from "./HelpText"; +import { Label } from "./Label"; +import { ValidationError } from "./ValidationError"; +import { BooleanProp, ClassProp, Config, Prop, StringProp, StructuredProp, StyleProp } from "../../ui/Prop"; +import type { TooltipInstance } from "../overlay"; +import type { FormRenderingContext } from "./ValidationGroup"; + +export interface FieldConfig extends PureContainerConfig, WidgetStyleConfig { + /** Field label. For advanced use cases. */ + label?: StringProp | Config; + + /** Set to `material` to use custom label placement instruction. Used in Material theme to implement animated labels. */ + labelPlacement?: "material"; + + /** Set to `material` to use custom help placement instruction. Used in Material theme to implement absolutely positioned validation messages. */ + helpPlacement?: "material"; + + /** Either `view` or `edit` (default). In `view` mode, the field is displayed as plain text. */ + mode?: Prop<"view" | "edit">; + + /** Set to `true` to switch to widget view mode. Same as `mode="view"`. Default is false. */ + viewMode?: BooleanProp; + + id?: Prop; + + /** Used for validation. If error evaluates to non-null, the field is marked in red. */ + error?: StringProp; + + /** Style object applied to the input element. Used for setting visual elements, such as borders and backgrounds. */ + inputStyle?: StyleProp; + + /** Additional CSS class applied to the input element. Used for setting visual elements, such as borders and backgrounds. */ + inputClass?: ClassProp; + + /** Additional attributes that should be rendered on the input element. E.g. inputAttrs={{ autoComplete: "off" }}. */ + inputAttrs?: Config; + + /** Text to be rendered in view mode when the field is empty. */ + emptyText?: StringProp; + + /** Set to `true` to make error indicators visible in pristine state. By default, validation errors are not shown until the user visits the field. */ + visited?: BooleanProp; + + /** Set to `true` to automatically focus the field, after it renders for the first time. */ + autoFocus?: BooleanProp; + + /** Defines how to present validation errors. Default mode is `tooltip`. Other options are `help` and `help-block`. */ + validationMode?: "tooltip" | "help" | "help-block"; + + /** Defaults to `false`. Set to `true` to disable the field. */ + disabled?: BooleanProp; + + /** Defaults to `false`. Used to make the field required. */ + required?: BooleanProp; + + /** Used to indicate that required fields should not be marked as invalid before the user visits them. */ + suppressErrorsUntilVisited?: boolean; + + /** Error message used to indicate that field is required. */ + requiredText?: string; + + /** Append asterisk to the label to indicate a required field. */ + asterisk?: BooleanProp; + + /** Text displayed to the user to indicate that server-side validation is in progress. */ + validatingText?: string; + + /** Text displayed to the user to indicate that server-side validation has thrown an exception. */ + validationExceptionText?: string; + + /** Configuration of the tooltip used to indicate validation errors. */ + errorTooltip?: Config; + + /** Tooltip configuration. */ + tooltip?: StringProp | StructuredProp; + + /** Indicates that `help` should be separated from the input with a whitespace. Default is `true`. */ + helpSpacer?: boolean; + + /** If set to `true` top level element will get additional CSS class indicating that input is focused. Default is `false`. */ + trackFocus?: boolean; + + /** Custom tab index */ + tabIndex?: StringProp; + + /** Additional content to be displayed next to the field. */ + help?: string | Config; + + /** Validation parameters to be passed to the validation function. */ + validationParams?: Config; + + onValidationException?: string | ((error: unknown, instance: FieldInstance) => void); + + /** Value to be set in the store if the field is empty. Default value is null. */ + emptyValue?: unknown; + + /** Additional CSS style to be passed to the label object. */ + labelStyle?: StyleProp; + + /** Additional CSS class to be passed to the label object. */ + labelClass?: ClassProp; +} + +export class FieldInstance = Field> + extends Instance + implements TooltipParentInstance +{ + declare state: Record; + declare parentDisabled?: boolean; + declare parentReadOnly?: boolean; + declare parentViewMode?: boolean | string; + declare parentTabOnEnterKey?: boolean; + declare parentVisited?: boolean; + declare tooltips: { [key: string]: TooltipInstance }; +} + +export class Field< + Config extends FieldConfig = FieldConfig, + InstanceType extends FieldInstance = FieldInstance, +> extends PureContainerBase { + declare public inputStyle?: Record | string; + declare public validationMode?: string; + declare public errorTooltip?: Record; + declare public tooltip?: TooltipConfig; + declare public help?: Record | string; + declare public label?: Record | string; + declare public mod?: Record; + declare public disabled?: boolean; + declare public required?: boolean; + declare public asterisk?: boolean; + declare public labelStyle?: Record | string; + declare public labelClass?: string; + declare public icon?: null | string; + declare public visited?: boolean; + declare public labelPlacement?: string | boolean; + declare public helpPlacement?: string | boolean; + declare public emptyValue?: unknown; + declare public requiredText?: string; + declare public validatingText?: string; + declare public onValidate?: + | string + | ((value: unknown, instance: Instance, validationParams: Record) => unknown); + declare public validationExceptionText?: string; + declare public onValidationException?: string | ((error: unknown, instance: Instance) => void); + declare public onKeyDown?: string | ((e: React.KeyboardEvent, instance: Instance) => boolean | void); + declare public suppressErrorsUntilVisited?: boolean; + declare public autoFocus?: boolean; + declare public helpSpacer?: boolean; + declare public trackFocus?: boolean; + declare public baseClass: string; + + public declareData(...args: Record[]): void { + super.declareData( + { + label: undefined, + labelWidth: undefined, + mode: undefined, + viewMode: undefined, + id: undefined, + error: undefined, + inputStyle: { structured: true }, + inputClass: { structured: true }, + inputAttrs: { structured: true }, + emptyText: undefined, + visited: undefined, + autoFocus: undefined, + tabOnEnterKey: undefined, + tabIndex: undefined, + validationParams: { structured: true }, + }, + ...args, + ); + } + + public init(): void { + this.inputStyle = parseStyle(this.inputStyle); + super.init(); + } + + public initComponents(_context: RenderingContext, instance: Instance): void { + if (this.validationMode == "tooltip" && isUndefined(this.errorTooltip)) { + this.errorTooltip = { + text: { bind: "$error" }, + mod: "error", + ...(this.errorTooltip || {}), + }; + } + + if (isUndefined(this.help)) { + switch (this.validationMode) { + case "help": + case "help-inline": + this.help = ValidationError as any; + break; + + case "help-block": + this.help = { + type: ValidationError as any, + mod: "block", + }; + break; + } + } + + if (this.help != null) { + let helpConfig: any = {}; + + if ((this.help as any).isComponentType) helpConfig = this.help; + else if (isSelector(this.help)) helpConfig.text = this.help; + else Object.assign(helpConfig, this.help); + + this.help = HelpText.create(helpConfig) as any; + } + + if (this.label != null) { + let labelConfig: any = { + mod: this.mod, + disabled: this.disabled, + required: this.required, + asterisk: this.asterisk, + style: this.labelStyle, + class: this.labelClass, + }; + + if ((this.label as any).isComponentType) labelConfig = this.label; + else if (isSelector(this.label)) labelConfig.text = this.label; + else Object.assign(labelConfig, this.label); + + this.label = Label.create(labelConfig) as any; + } + + if (this.icon != null) { + let iconConfig: any = { + className: this.CSS.element(this.baseClass, "icon"), + }; + if (isSelector(this.icon)) iconConfig.name = this.icon; + else Object.assign(iconConfig, this.icon); + + this.icon = FieldIcon.create(iconConfig) as any; + } + + super.initComponents({ + label: this.label, + help: this.help, + icon: this.icon, + }); + } + + public initState(_context: RenderingContext, instance: InstanceType): void { + instance.state = { + inputError: false, + visited: this.visited === true, + }; + } + + public prepareData(context: FormRenderingContext, instance: InstanceType, ...args: Record[]): void { + let { data, state } = instance; + if (!data.id) data.id = "fld-" + instance.id; + + data._disabled = data.disabled; + data._readOnly = data.readOnly; + data._viewMode = data.mode === "view" || data.viewMode; + data._tabOnEnterKey = data.tabOnEnterKey; + data.validationValue = this.getValidationValue(data); + instance.parentDisabled = context.parentDisabled; + instance.parentReadOnly = context.parentReadOnly; + instance.parentViewMode = context.parentViewMode; + instance.parentTabOnEnterKey = context.parentTabOnEnterKey; + instance.parentVisited = context.parentVisited; + + if (typeof data.enabled !== "undefined") data._disabled = !data.enabled; + + this.disableOrValidate(context, instance); + + data.inputStyle = parseStyle(data.inputStyle); + + if (this.labelPlacement && this.label) data.mod = [data.mod, "label-placement-" + this.labelPlacement]; + + if (this.helpPlacement && this.help) data.mod = [data.mod, "help-placement-" + this.helpPlacement]; + + data.empty = this.isEmpty(data); + + super.prepareData(context, instance); + } + + protected disableOrValidate(context: FormRenderingContext, instance: Instance): void { + let { data, state } = instance; + + //if the parent is strict and sets some flag to true, it is not allowed to overrule that flag by field settings + + data.disabled = coalesce( + context.parentStrict ? context.parentDisabled : null, + data._disabled, + context.parentDisabled, + ); + data.readOnly = coalesce( + context.parentStrict ? context.parentReadOnly : null, + data._readOnly, + context.parentReadOnly, + ); + data.viewMode = coalesce( + context.parentStrict ? context.parentViewMode : null, + data._viewMode, + context.parentViewMode, + ); + data.tabOnEnterKey = coalesce( + context.parentStrict ? context.parentTabOnEnterKey : null, + data._tabOnEnterKey, + context.parentTabOnEnterKey, + ); + data.visited = coalesce(context.parentStrict ? context.parentVisited : null, data.visited, context.parentVisited); + + if (!data.error && !data.disabled && !data.viewMode) this.validate(context, instance); + + if (data.visited && !state?.visited) { + //feels hacky but it should be ok since we're in the middle of a new render cycle + state!.visited = true; + } + + data.stateMods = { + ...data.stateMods, + disabled: data.disabled, + "edit-mode": !data.viewMode, + "view-mode": data.viewMode, + }; + } + + explore(context: FormRenderingContext, instance: InstanceType): void { + let { data, state } = instance; + + instance.parentDisabled = context.parentDisabled; + instance.parentReadOnly = context.parentReadOnly; + instance.parentViewMode = context.parentViewMode; + instance.parentTabOnEnterKey = context.parentTabOnEnterKey; + instance.parentVisited = context.parentVisited; + + if ( + instance.cache("parentDisabled", context.parentDisabled) || + instance.cache("parentReadOnly", context.parentReadOnly) || + instance.cache("parentViewMode", context.parentViewMode) || + instance.cache("parentTabOnEnterKey", context.parentTabOnEnterKey) || + instance.cache("parentVisited", context.parentVisited) + ) { + instance.markShouldUpdate(context); + this.disableOrValidate(context, instance); + this.prepareCSS(context, instance); + } + + if (!context.validation) + context.validation = { + errors: [], + }; + + if (data.error) { + context.validation.errors.push({ + fieldId: data.id, + message: data.error, + visited: state?.visited, + type: "error", + }); + } + + context.push("lastFieldId", data.id); + super.explore(context, instance); + } + + exploreCleanup(context: FormRenderingContext, instance: Instance): void { + context.pop("lastFieldId"); + } + + isEmpty(data: Record): boolean { + return data.value == null || data.value === this.emptyValue; + } + + validateRequired(context: FormRenderingContext, instance: Instance): string | undefined { + let { data } = instance; + if (this.isEmpty(data)) return this.requiredText; + } + + getValidationValue(data: Record): unknown { + return data.value; + } + + validate(context: FormRenderingContext, instance: Instance): void { + let { data } = instance; + let state = instance.state || {}; + + let empty = this.isEmpty(data); + + if (!data.error) { + if (state.inputError) data.error = state.inputError; + else if (state.validating && !empty) data.error = this.validatingText; + else if ( + state.validationError && + data.validationValue === state.lastValidatedValue && + shallowEquals(data.validationParams, state.lastValidationParams) + ) + data.error = state.validationError; + else if (data.required) data.error = this.validateRequired(context, instance); + } + + if ( + !empty && + !state.validating && + !data.error && + this.onValidate && + (!state.previouslyValidated || + data.validationValue != state.lastValidatedValue || + data.validationParams != state.lastValidationParams) + ) { + let result = instance.invoke("onValidate", data.validationValue, instance, data.validationParams); + if (isPromise(result)) { + data.error = this.validatingText; + instance.setState({ + validating: true, + lastValidatedValue: data.validationValue, + previouslyValidated: true, + lastValidationParams: data.validationParams, + }); + result + .then((r) => { + let { data, state } = instance; + let error = + data.validationValue == state?.lastValidatedValue && + shallowEquals(data.validationParams, state?.lastValidationParams) + ? r + : this.validatingText; //parameters changed, this will be revalidated + + instance.setState({ + validating: false, + validationError: error, + }); + }) + .catch((e) => { + instance.setState({ + validating: false, + validationError: this.validationExceptionText, + }); + if (this.onValidationException) instance.invoke("onValidationException", e, instance); + else { + Console.warn("Unhandled validation exception:", e); + } + }); + } else { + data.error = result; + } + } + } + + renderLabel(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + if (instance.components?.label) return getContent(instance.components.label.vdom); + } + + renderInput(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + throw new Error("Not implemented."); + } + + renderHelp(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + if (instance.components?.help) return getContent(instance.components.help.render(context)); + } + + renderIcon(context: RenderingContext, instance: Instance, key: string): React.ReactNode { + if (instance.components?.icon) return getContent(instance.components.icon.render(context)); + } + + formatValue(context: RenderingContext, { data }: Instance): string | React.ReactNode { + return data.text || data.value; + } + + renderValue(context: RenderingContext, instance: FieldInstance, key?: string | number): React.ReactNode { + let text = this.formatValue(context, instance as Instance); + if (text) { + return ( + { + const tooltip = getFieldTooltip(instance); + if (tooltip) (tooltipMouseMove as any)(e, tooltip, null, null); + }} + onMouseLeave={(e: any) => { + const tooltip = getFieldTooltip(instance); + if (tooltip) (tooltipMouseLeave as any)(e, tooltip, null, null); + }} + > + {text} + + ); + } + } + + protected renderContent(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + let content = this.renderValue(context, instance, key) || this.renderEmptyText(context, instance, key); + return this.renderWrap(context, instance, key, content); + } + + protected renderWrap( + context: RenderingContext, + instance: Instance, + key: string, + content: React.ReactNode, + ): React.ReactNode { + let { data } = instance; + let interactive = !data.viewMode && !data.disabled; + return ( +
    + {content} + {this.labelPlacement && this.renderLabel(context, instance, "label")} +
    + ); + } + + protected renderEmptyText(_context: RenderingContext, { data }: Instance, key: string): React.ReactNode { + return ( + + {data.emptyText ||  } + + ); + } + + public render(context: RenderingContext, instance: InstanceType, key: string): Record { + let { data } = instance; + let content = !data.viewMode + ? this.renderInput(context, instance, key) + : this.renderContent(context, instance, key); + + return { + label: !this.labelPlacement && this.renderLabel(context, instance, key), + content: content, + helpSpacer: this.helpSpacer && instance.components?.help ? " " : undefined, + help: !this.helpPlacement && this.renderHelp(context, instance, key), + }; + } + + public handleKeyDown(e: React.KeyboardEvent, instance: Instance): boolean | void { + if (this.onKeyDown && instance.invoke("onKeyDown", e, instance) === false) return false; + + if (instance.data.tabOnEnterKey && e.keyCode === 13) { + let target = e.target; + setTimeout(() => { + if (!instance.state?.inputError) (FocusManager as any).focusNext(target); + }, 10); + } + } +} + +Field.prototype.validationMode = "tooltip"; +Field.prototype.suppressErrorsUntilVisited = false; +Field.prototype.requiredText = "This field is required."; +Field.prototype.autoFocus = false; +Field.prototype.asterisk = false; +Field.prototype.validatingText = "Validation is in progress..."; +Field.prototype.validationExceptionText = "Something went wrong during input validation. Check log for more details."; +Field.prototype.helpSpacer = true; +Field.prototype.trackFocus = false; //add cxs-focus on parent element +Field.prototype.labelPlacement = false; +Field.prototype.helpPlacement = false; +Field.prototype.emptyValue = null; +Field.prototype.styled = true; + +//These flags are inheritable and should not be set to false +//Field.prototype.visited = null; +//Field.prototype.disabled = null; +//Field.prototype.readOnly = null; +//Field.prototype.viewMode = null; + +Localization.registerPrototype("cx/widgets/Field", Field); + +export function getFieldTooltip( + instance: FieldInstance, +): [FieldInstance, TooltipConfig, TooltipOptions | undefined] { + let { widget, data, state } = instance; + + if (widget.errorTooltip && data.error && (!state || state.visited || !widget.suppressErrorsUntilVisited)) + return [ + instance, + widget.errorTooltip, + { + data: { + $error: data.error, + }, + }, + ]; + return [instance, widget.tooltip!, undefined]; +} diff --git a/packages/cx/src/widgets/form/FieldGroup.d.ts b/packages/cx/src/widgets/form/FieldGroup.d.ts deleted file mode 100644 index 8e43bb4d1..000000000 --- a/packages/cx/src/widgets/form/FieldGroup.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Cx from '../../core'; -import { ValidationGroupProps } from './ValidationGroup'; - -export interface FieldGroupProps extends ValidationGroupProps {} - -export class FieldGroup extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/FieldGroup.js b/packages/cx/src/widgets/form/FieldGroup.js deleted file mode 100644 index 54217676a..000000000 --- a/packages/cx/src/widgets/form/FieldGroup.js +++ /dev/null @@ -1,6 +0,0 @@ -import {Widget} from '../../ui/Widget'; -import {ValidationGroup} from './ValidationGroup'; - -export class FieldGroup extends ValidationGroup {} - -Widget.alias('field-group', FieldGroup); \ No newline at end of file diff --git a/packages/cx/src/widgets/form/FieldGroup.ts b/packages/cx/src/widgets/form/FieldGroup.ts new file mode 100644 index 000000000..2dfd76244 --- /dev/null +++ b/packages/cx/src/widgets/form/FieldGroup.ts @@ -0,0 +1,10 @@ +import { Widget } from "../../ui/Widget"; +import { ValidationGroup, ValidationGroupConfig } from "./ValidationGroup"; + +export interface FieldGroupConfig extends ValidationGroupConfig {} + +export class FieldGroup< + TConfig extends FieldGroupConfig = FieldGroupConfig +> extends ValidationGroup {} + +Widget.alias("field-group", FieldGroup); diff --git a/packages/cx/src/widgets/form/FieldIcon.js b/packages/cx/src/widgets/form/FieldIcon.js deleted file mode 100644 index 50f3d5f5f..000000000 --- a/packages/cx/src/widgets/form/FieldIcon.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Widget } from "../../ui/Widget"; -import { Icon } from "../Icon"; -import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops"; - -export class FieldIcon extends Widget { - declareData(...args) { - super.declareData(...args, { - name: undefined, - }); - } - - render(context, instance, key) { - let { data } = instance; - if (!data.name) return null; - - let onClick, onMouseMove, onMouseLeave; - - if (this.onClick) - onClick = (e) => { - instance.invoke("onClick", e, instance); - }; - - if (this.tooltip) { - onMouseLeave = (e) => { - tooltipMouseLeave(e, instance, this.tooltip); - }; - onMouseMove = (e) => { - tooltipMouseMove(e, instance, this.tooltip); - }; - } - - return Icon.render(data.name, { - className: data.classNames, - style: data.style, - onClick, - onMouseMove, - onMouseLeave, - }); - } -} - -FieldIcon.prototype.styled = true; diff --git a/packages/cx/src/widgets/form/FieldIcon.ts b/packages/cx/src/widgets/form/FieldIcon.ts new file mode 100644 index 000000000..0aef406f4 --- /dev/null +++ b/packages/cx/src/widgets/form/FieldIcon.ts @@ -0,0 +1,61 @@ +import { Widget, WidgetConfig } from "../../ui/Widget"; +import { Icon } from "../Icon"; +import { tooltipMouseLeave, tooltipMouseMove, TooltipConfig, TooltipParentInstance } from "../overlay/tooltip-ops"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { Instance } from "../../ui/Instance"; +import type { TooltipInstance } from "../overlay/Tooltip"; +import { StringProp } from "../../ui/Prop"; + +export interface FieldIconConfig extends WidgetConfig { + onClick?: (e: MouseEvent, instance: FieldIconInstance) => void; + tooltip?: TooltipConfig; + name: StringProp; +} + +export class FieldIconInstance extends Instance implements TooltipParentInstance { + declare tooltips: { [key: string]: TooltipInstance }; +} + +export class FieldIcon extends Widget { + declare onClick?: (e: MouseEvent, instance: FieldIconInstance) => void; + declare tooltip?: TooltipConfig; + + declareData(...args: Record[]): void { + super.declareData(...args, { + name: undefined, + }); + } + + render(context: RenderingContext, instance: FieldIconInstance, key: string): React.ReactNode { + let { data } = instance; + if (!data.name) return null; + + let onClick: ((e: React.MouseEvent) => void) | undefined; + let onMouseMove: ((e: React.MouseEvent) => void) | undefined; + let onMouseLeave: ((e: React.MouseEvent) => void) | undefined; + + if (this.onClick) + onClick = (e: React.MouseEvent) => { + instance.invoke("onClick", e, instance); + }; + + if (this.tooltip) { + onMouseLeave = (e: React.MouseEvent) => { + tooltipMouseLeave(e, instance, this.tooltip!, {}); + }; + onMouseMove = (e: React.MouseEvent) => { + tooltipMouseMove(e, instance, this.tooltip!, {}); + }; + } + + return Icon.render(data.name, { + className: data.classNames, + style: data.style, + onClick, + onMouseMove, + onMouseLeave, + }); + } +} + +FieldIcon.prototype.styled = true; diff --git a/packages/cx/src/widgets/form/HelpText.d.ts b/packages/cx/src/widgets/form/HelpText.d.ts deleted file mode 100644 index c73130f80..000000000 --- a/packages/cx/src/widgets/form/HelpText.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Cx from '../../core'; -import { ValidationGroupProps } from './ValidationGroup'; - -export interface HelpTextProps extends Cx.HtmlElementProps { - - /** Base CSS class to be applied to the field. Defaults to `helptext`. */ - baseClass?: string -} - -export class HelpText extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/HelpText.js b/packages/cx/src/widgets/form/HelpText.js deleted file mode 100644 index 7d7ae8cab..000000000 --- a/packages/cx/src/widgets/form/HelpText.js +++ /dev/null @@ -1,9 +0,0 @@ -import {HtmlElement} from '../HtmlElement'; -import {Widget} from '../../ui/Widget'; - -export class HelpText extends HtmlElement {} - -HelpText.prototype.tag = 'span'; -HelpText.prototype.baseClass = 'helptext'; - -Widget.alias('help-text', HelpText); diff --git a/packages/cx/src/widgets/form/HelpText.scss b/packages/cx/src/widgets/form/HelpText.scss index e9bf5f26b..e067ad552 100644 --- a/packages/cx/src/widgets/form/HelpText.scss +++ b/packages/cx/src/widgets/form/HelpText.scss @@ -1,12 +1,13 @@ +@use "sass:map"; @mixin cx-helptext( $name: 'helptext', $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { font-size: smaller; diff --git a/packages/cx/src/widgets/form/HelpText.ts b/packages/cx/src/widgets/form/HelpText.ts new file mode 100644 index 000000000..0b3d4f425 --- /dev/null +++ b/packages/cx/src/widgets/form/HelpText.ts @@ -0,0 +1,15 @@ +import { Widget } from "../../ui/Widget"; +import { HtmlElement, HtmlElementConfig } from "../HtmlElement"; + +export interface HelpTextConfig extends HtmlElementConfig {} + +export class HelpText extends HtmlElement { + constructor(config?: HelpTextConfig) { + super(config); + } +} + +HelpText.prototype.tag = "span"; +HelpText.prototype.baseClass = "helptext"; + +Widget.alias("help-text", HelpText); diff --git a/packages/cx/src/widgets/form/Label.d.ts b/packages/cx/src/widgets/form/Label.d.ts deleted file mode 100644 index f6cd6e5f8..000000000 --- a/packages/cx/src/widgets/form/Label.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Cx from "../../core"; - -interface LabelProps extends Cx.HtmlElementProps { - /** Used in combination with `asterisk` to indicate required fields. */ - required?: Cx.BooleanProp; - - /** Set to true to disable the label. */ - disabled?: Cx.BooleanProp; - - /** Id of the field. */ - htmlFor?: Cx.StringProp; - - /** Base CSS class to be applied to the element. No class is applied by default. */ - baseClass?: string; - - /** Set to `true` to add red asterisk for required fields. */ - asterisk?: Cx.BooleanProp; - - /** Name of the HTML element to be rendered. Default is `div`. */ - tag?: string; -} - -export class Label extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Label.js b/packages/cx/src/widgets/form/Label.js deleted file mode 100644 index de36984e5..000000000 --- a/packages/cx/src/widgets/form/Label.js +++ /dev/null @@ -1,89 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { HtmlElement } from "../HtmlElement"; -import { FocusManager } from "../../ui/FocusManager"; -import { isArray } from "../../util/isArray"; -import { coalesce } from "../../util/coalesce"; - -export class Label extends HtmlElement { - declareData() { - super.declareData(...arguments, { - required: undefined, - disabled: undefined, - htmlFor: undefined, - asterisk: undefined, - }); - } - - prepareData(context, instance) { - let { data } = instance; - data.stateMods = { - ...data.stateMods, - disabled: data.disabled, - }; - data._disabled = data.disabled; - super.prepareData(context, instance); - } - - explore(context, instance) { - let { data } = instance; - - if (!data.htmlFor) data.htmlFor = context.lastFieldId; - - data.disabled = data.stateMods.disabled = coalesce( - context.parentStrict ? context.parentDisabled : null, - data._disabled, - context.parentDisabled - ); - - data.asterisk = context.parentAsterisk || data.asterisk; - - if (instance.cache("disabled", data.disabled) || instance.cache("asterisk", data.asterisk)) { - instance.markShouldUpdate(context); - this.prepareCSS(context, instance); - } - - super.explore(context, instance); - } - - isValidHtmlAttribute(attrName) { - switch (attrName) { - case "asterisk": - case "required": - return false; - } - return super.isValidHtmlAttribute(attrName); - } - - attachProps(context, instance, props) { - super.attachProps(context, instance, props); - - let { data } = instance; - - if (data.htmlFor) { - props.htmlFor = data.htmlFor; - - if (!props.onClick) - props.onClick = () => { - //additional focus for LookupFields which are not input based - let el = document.getElementById(instance.data.htmlFor); - if (el) FocusManager.focusFirst(el); - }; - } - - if (!props.id && data.htmlFor) props.id = `${data.htmlFor}-label`; - - if (data.required && data.asterisk) { - if (!isArray(props.children)) props.children = [props.children]; - props.children.push(" "); - props.children.push( - - * - - ); - } - } -} - -Label.prototype.baseClass = "label"; -Label.prototype.tag = "label"; -Label.prototype.asterisk = false; diff --git a/packages/cx/src/widgets/form/Label.scss b/packages/cx/src/widgets/form/Label.scss index dc4a26c8a..80d28d7c4 100644 --- a/packages/cx/src/widgets/form/Label.scss +++ b/packages/cx/src/widgets/form/Label.scss @@ -1,13 +1,14 @@ +@use "sass:map"; @mixin cx-label( $name: 'label', $state-style-map: $cx-label-state-style-map, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}label { display: inline-block; diff --git a/packages/cx/src/widgets/form/Label.tsx b/packages/cx/src/widgets/form/Label.tsx new file mode 100644 index 000000000..7a71cee1b --- /dev/null +++ b/packages/cx/src/widgets/form/Label.tsx @@ -0,0 +1,117 @@ +/** @jsxImportSource react */ +import { FocusManager } from "../../ui/FocusManager"; +import type { Instance, RenderProps } from "../../ui/Instance"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { BooleanProp, StringProp } from "../../ui/Prop"; +import { coalesce } from "../../util/coalesce"; +import { isArray } from "../../util/isArray"; +import { HtmlElement, HtmlElementConfig, HtmlElementInstance } from "../HtmlElement"; +import type { FormRenderingContext } from "./ValidationGroup"; + +export interface LabelConfig extends HtmlElementConfig { + /** Used in combination with `asterisk` to indicate required fields. */ + required?: BooleanProp; + + /** Set to true to disable the label. */ + disabled?: BooleanProp; + + /** Id of the field. */ + htmlFor?: StringProp; + + /** Set to `true` to add red asterisk for required fields. */ + asterisk?: BooleanProp; +} + +export class Label extends HtmlElement { + declare required?: BooleanProp; + declare disabled?: BooleanProp; + declare htmlFor?: StringProp; + declare asterisk?: BooleanProp; + + constructor(config?: LabelConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData(...args, { + required: undefined, + disabled: undefined, + htmlFor: undefined, + asterisk: undefined, + }); + } + + prepareData(context: RenderingContext, instance: HtmlElementInstance): void { + let { data } = instance; + data.stateMods = { + ...data.stateMods, + disabled: data.disabled, + }; + data._disabled = data.disabled; + super.prepareData(context, instance); + } + + explore(context: FormRenderingContext, instance: Instance): void { + let { data } = instance; + + if (!data.htmlFor) data.htmlFor = context.lastFieldId; + + data.disabled = data.stateMods.disabled = coalesce( + context.parentStrict ? context.parentDisabled : null, + data._disabled, + context.parentDisabled, + ); + + data.asterisk = context.parentAsterisk || data.asterisk; + + if (instance.cache("disabled", data.disabled) || instance.cache("asterisk", data.asterisk)) { + instance.markShouldUpdate(context); + this.prepareCSS(context, instance); + } + + super.explore(context, instance); + } + + isValidHtmlAttribute(attrName: string): string | false { + switch (attrName) { + case "asterisk": + case "required": + return false; + } + return super.isValidHtmlAttribute(attrName); + } + + attachProps(context: RenderingContext, instance: HtmlElementInstance, props: RenderProps): void { + super.attachProps(context, instance, props); + + let { data } = instance; + + if (data.htmlFor) { + props.htmlFor = data.htmlFor; + + if (!props.onClick) + props.onClick = () => { + //additional focus for LookupFields which are not input based + let el = document.getElementById(instance.data.htmlFor); + if (el) FocusManager.focusFirst(el); + }; + } + + if (!props.id && data.htmlFor) props.id = `${data.htmlFor}-label`; + + if (data.required && data.asterisk) { + if (!isArray(props.children)) props.children = [props.children]; + const children = props.children as React.ReactNode[]; + children.push(" "); + children.push( + + * + , + ); + } + } +} + +Label.prototype.baseClass = "label"; +Label.prototype.tag = "label"; +Label.prototype.asterisk = false; diff --git a/packages/cx/src/widgets/form/LabeledContainer.d.ts b/packages/cx/src/widgets/form/LabeledContainer.d.ts deleted file mode 100644 index ecce9f374..000000000 --- a/packages/cx/src/widgets/form/LabeledContainer.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as Cx from "../../core"; -import { FieldGroupProps } from "./FieldGroup"; - -interface LabeledContainerProps extends FieldGroupProps { - /** The label. */ - label?: Cx.StringProp | Cx.Config; -} - -export class LabeledContainer extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/LabeledContainer.js b/packages/cx/src/widgets/form/LabeledContainer.js deleted file mode 100644 index ee1f9e663..000000000 --- a/packages/cx/src/widgets/form/LabeledContainer.js +++ /dev/null @@ -1,59 +0,0 @@ -import {Widget} from '../../ui/Widget'; -import {FieldGroup} from './FieldGroup'; -import {Label} from './Label'; -import {isSelector} from '../../data/isSelector'; - -export class LabeledContainer extends FieldGroup -{ - declareData() { - super.declareData({ - label: undefined - }, ...arguments); - } - - init() { - - if (this.label != null) { - let labelConfig = { - type: Label, - disabled: this.disabled, - mod: this.mod, - asterisk: this.asterisk, - required: true - }; - - if (this.label.isComponentType) - labelConfig = this.label; - else if (isSelector(this.label)) - labelConfig.text = this.label; - else - Object.assign(labelConfig, this.label); - - this.label = Widget.create(labelConfig); - } - - super.init(); - } - - initComponents(context, instance) { - return super.initComponents(...arguments, { - label: this.label - }); - } - - renderLabel(context, instance, key) { - if (instance.components.label) - return instance.components.label.render(context, key); - } - - render(context, instance, key) { - return { - label: this.renderLabel(context, instance), - content: this.renderChildren(context, instance) - } - } -} - -LabeledContainer.prototype.styled = true; - -Widget.alias('labeled-container', LabeledContainer); diff --git a/packages/cx/src/widgets/form/LabeledContainer.ts b/packages/cx/src/widgets/form/LabeledContainer.ts new file mode 100644 index 000000000..1a5b45147 --- /dev/null +++ b/packages/cx/src/widgets/form/LabeledContainer.ts @@ -0,0 +1,77 @@ +import { isSelector } from "../../data/isSelector"; +import type { Instance } from "../../ui/Instance"; +import type { CxChild, RenderingContext } from "../../ui/RenderingContext"; +import { Widget } from "../../ui/Widget"; +import { FieldGroup, FieldGroupConfig } from "./FieldGroup"; +import { Label, LabelConfig } from "./Label"; +import { StringProp, Config, BooleanProp, ModProp } from "../../ui/Prop"; +import { Create } from "../../util/Component"; + +export interface LabeledContainerConfig extends FieldGroupConfig { + /** The label. */ + label?: StringProp | Create | LabelConfig; + + /** Set to true to disable the field. */ + disabled?: BooleanProp; + + /** CSS modifier classes. */ + mod?: ModProp; + + /** Set to true to display an asterisk next to the label. */ + asterisk?: BooleanProp; +} + +export class LabeledContainer extends FieldGroup { + declare label?: string | Record | Label | Widget; // Can be string, selector, Label widget config, or Widget + declare disabled?: boolean; + declare mod?: Record; + declare asterisk?: boolean; + + declareData(...args: Record[]): void { + super.declareData(...args, { + label: undefined, + }); + } + + init(): void { + if (this.label != null) { + let labelConfig: any = { + type: Label, + disabled: this.disabled, + mod: this.mod, + asterisk: this.asterisk, + required: true, + }; + + if ((this.label as any).isComponentType) labelConfig = this.label as Record; + else if (isSelector(this.label)) labelConfig.text = this.label; + else Object.assign(labelConfig, this.label); + + this.label = Widget.create(labelConfig); + } + + super.init(); + } + + initComponents(context: RenderingContext, instance: Instance, ...args: Record[]): void { + return super.initComponents(context, instance, ...args, { + label: this.label, + }); + } + + renderLabel(context: RenderingContext, instance: Instance, key?: string): CxChild { + if (instance.components && instance.components.label) return instance.components.label.render(context); + return null; + } + + render(context: RenderingContext, instance: Instance, key: string): { label: CxChild; content: CxChild } { + return { + label: this.renderLabel(context, instance), + content: this.renderChildren(context, instance), + }; + } +} + +LabeledContainer.prototype.styled = true; + +Widget.alias("labeled-container", LabeledContainer); diff --git a/packages/cx/src/widgets/form/LookupField.d.ts b/packages/cx/src/widgets/form/LookupField.d.ts deleted file mode 100644 index b4b2e2ad3..000000000 --- a/packages/cx/src/widgets/form/LookupField.d.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Instance } from "./../../ui/Instance"; -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -export interface LookupBinding { - local: string; - remote: string; - key?: boolean; -} - -interface LookupFieldProps extends FieldProps { - /** Defaults to `false`. Set to `true` to enable multiple selection. */ - multiple?: Cx.BooleanProp; - - /** Selected value. Used only if `multiple` is set to `false`. */ - value?: Cx.Prop; - - /** A list of selected ids. Used only if `multiple` is set to `true`. */ - values?: Cx.Prop<(number | string)[]>; - - /** A list of selected records. Used only if `multiple` is set to `true`. */ - records?: Cx.Prop; - - /** Text associated with the selection. Used only if `multiple` is set to `false`. */ - text?: Cx.StringProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** A list of available options. */ - options?: Cx.Prop; - - /** - * Set to `true` to hide the clear button. It can be used interchangeably with the `showClear` property. - * No effect if `multiple` is used. Default value is `false`. - */ - hideClear?: boolean; - - /** - * Set to `false` to hide the clear button. It can be used interchangeably with the `hideClear` property. - * No effect if `multiple` is used. Default value is `true`. - */ - showClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** Base CSS class to be applied to the field. Defaults to `lookupfield`. */ - baseClass?: string; - - /* TODO: Check type */ - - /** Additional config to be applied to all items */ - itemsConfig?: any; - - /** - * An array of objects describing the mapping of option data to store data. - * Each entry must define `local`, `remote` bindings. `key: true` is used to indicate fields that are used in the primary key. - */ - bindings?: LookupBinding[]; - - /** A delay in milliseconds between the moment the user stops typing and when tha query is made. Default value is `150`. */ - queryDelay?: number; - - /** Minimal number of characters required before query is made. */ - minQueryLength?: number; - - /** Set to `true` to hide the search field. */ - hideSearchField?: boolean; - - /** - * Number of options required to show the search field. - * If there are only a few options, there is no need for search. Defaults to `7`. - */ - minOptionsForSearchField?: number; - - /** Text to display while data is being loaded. */ - loadingText?: string; - - /** Error message displayed to the user if server query throws an exception. */ - queryErrorText?: string; - - /** Message to be displayed if no entries match the user query. */ - noResultsText?: string; - - /** Name of the field which holds the id of the option. Default value is `id`. */ - optionIdField?: string; - - /** Name of the field which holds the display text of the option. Default value is `text`. */ - optionTextField?: string; - - /** - * Available only in `multiple` selection mode and without custom `bindings`. - * Name of the field to store id of the selected value. Default value is `id`. - */ - valueIdField?: string; - - /** - * Available only in `multiple` selection mode. - * Name of the field to store display text of the selected value. Default value is `text`. - */ - valueTextField?: string; - - /** - * If `true` `onQuery` will be called only once to fetch all options. - * After that options are filtered client-side. - */ - fetchAll?: boolean; - - /** - * If this flag is set along with `fetchAll`, fetched options are cached for the lifetime of the widget. - * Otherwise, data is fetched whenever the dropdown is shown. - */ - cacheAll?: boolean; - - /** Close the dropdown after selection. Default is `true`. */ - closeOnSelect?: boolean; - - /** Message to be displayed to the user if the entered search query is too short. */ - minQueryLengthMessageText?: string; - - /** Name or configuration of the icon to be put on the left side of the input. */ - icon?: Cx.StringProp | Cx.Record; - - /** Query function. */ - onQuery?: - | string - | (( - query: string | { query: string; page: number; pageSize: number }, - instance: Instance, - ) => TOption[] | Promise); - - /** Set to true to sort dropdown options. */ - sort?: boolean; - - /** Additional list options, such as grouping configuration, custom sorting, etc. */ - listOptions?: Cx.Config; - - /** Set to true to show the dropdown immediately after the component has mounted. - * This is commonly used for cell editing in grids. */ - autoOpen?: Cx.BooleanProp; - - /** Set to true to allow enter key events to be propagated. This is useful for submitting forms or closing grid cell editors. */ - submitOnEnterKey?: Cx.BooleanProp; - - /** Set to true to allow dropdown enter key events to be propagated. This is useful for submitting forms on dropdown enter key selection. */ - submitOnDropdownEnterKey?: Cx.BooleanProp; - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** Set to `true` to enable loading of additional lookup options when the scroll is reaching the end. */ - infinite?: boolean; - - /** Number of additional items to be loaded in `infinite` mode. Default is 100. */ - pageSize?: number; - - /** Set to `true` to allow quick selection of all displayed lookup items on `ctrl + a` keys combination. */ - quickSelectAll?: boolean; - - /** Parameters that affect filtering. */ - filterParams?: Cx.StructuredProp; - - /** Callback to create a filter function for given filter params. */ - onCreateVisibleOptionsFilter?: (filterParams: any, instance?: Instance) => (option: TOption) => boolean; - - /** Used in multiple selection lookups in combination with records, to construct the display text out of multiple fields or when additional formatting is needed. */ - onGetRecordDisplayText?: (record: TRecord, instance?: Instance) => string; - - /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ - dropdownOptions?: Cx.Config; -} - -export class LookupField extends Cx.Widget> {} diff --git a/packages/cx/src/widgets/form/LookupField.js b/packages/cx/src/widgets/form/LookupField.js deleted file mode 100644 index de15ebc9d..000000000 --- a/packages/cx/src/widgets/form/LookupField.js +++ /dev/null @@ -1,1135 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Cx } from "../../ui/Cx"; -import { Field, getFieldTooltip } from "./Field"; -import { ReadOnlyDataView } from "../../data/ReadOnlyDataView"; -import { HtmlElement } from "../HtmlElement"; -import { Binding } from "../../data/Binding"; -import { debug } from "../../util/Debug"; -import { Dropdown } from "../overlay/Dropdown"; -import { FocusManager } from "../../ui/FocusManager"; -import { isFocused } from "../../util/DOM"; -import { isTouchDevice } from "../../util/isTouchDevice"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { stopPropagation, preventDefault } from "../../util/eventCallbacks"; -import ClearIcon from "../icons/clear"; -import DropdownIcon from "../icons/drop-down"; -import { getSearchQueryPredicate } from "../../util/getSearchQueryPredicate"; -import { KeyCode } from "../../util/KeyCode"; -import { Localization } from "../../ui/Localization"; -import { StringTemplate } from "../../data/StringTemplate"; -import { Icon } from "../Icon"; -import { isString } from "../../util/isString"; -import { isDefined } from "../../util/isDefined"; -import { isArray } from "../../util/isArray"; -import { isNonEmptyArray } from "../../util/isNonEmptyArray"; -import { addEventListenerWithOptions } from "../../util/addEventListenerWithOptions"; -import { List } from "../List"; -import { Selection } from "../../ui/selection/Selection"; -import { HighlightedSearchText } from "../HighlightedSearchText"; -import { autoFocus } from "../autoFocus"; -import { bind } from "../../ui"; -import { isAccessorChain } from "../../data/createAccessorModelProxy"; - -export class LookupField extends Field { - declareData() { - let additionalAttributes = this.multiple - ? { values: undefined, records: undefined } - : { value: undefined, text: undefined }; - - super.declareData( - { - disabled: undefined, - enabled: undefined, - placeholder: undefined, - required: undefined, - options: undefined, - icon: undefined, - autoOpen: undefined, - readOnly: undefined, - filterParams: { structured: true }, - }, - additionalAttributes, - ...arguments, - ); - } - - init() { - if (isDefined(this.hideClear)) this.showClear = !this.hideClear; - - if (this.alwaysShowClear) this.showClear = true; - - if (!this.bindings) { - let b = []; - if (this.value) { - if (isAccessorChain(this.value)) this.value = bind(this.value); - if (this.value.bind) - b.push({ - key: true, - local: this.value.bind, - remote: `$option.${this.optionIdField}`, - set: this.value.set, - }); - } - - if (this.text) { - if (isAccessorChain(this.text)) this.text = bind(this.text); - if (this.text.bind) - b.push({ - local: this.text.bind, - remote: `$option.${this.optionTextField}`, - set: this.text.set, - }); - } - - this.bindings = b; - } - - if (this.bindings.length == 0 && this.multiple) - this.bindings = [ - { - key: true, - local: `$value.${this.valueIdField}`, - remote: `$option.${this.optionIdField}`, - }, - { - local: `$value.${this.valueTextField}`, - remote: `$option.${this.optionTextField}`, - }, - ]; - - this.keyBindings = this.bindings.filter((b) => b.key); - - if (!this.items && !this.children) - this.items = { - $type: HighlightedSearchText, - text: { bind: `$option.${this.optionTextField}` }, - query: { bind: "$query" }, - }; - - this.itemConfig = this.children || this.items; - - delete this.items; - delete this.children; - - super.init(); - } - - prepareData(context, instance) { - let { data, store } = instance; - - data.stateMods = { - multiple: this.multiple, - single: !this.multiple, - disabled: data.disabled, - readonly: data.readOnly, - }; - - data.visibleOptions = data.options; - if (this.onCreateVisibleOptionsFilter && isArray(data.options)) { - let filterPredicate = instance.invoke("onCreateVisibleOptionsFilter", data.filterParams, instance); - data.visibleOptions = data.options.filter(filterPredicate); - } - - data.selectedKeys = []; - - if (this.multiple) { - if (isArray(data.values) && isArray(data.options)) { - data.selectedKeys = data.values.map((v) => (this.keyBindings.length == 1 ? [v] : v)); - let map = {}; - data.options.filter(($option) => { - let optionKey = getOptionKey(this.keyBindings, { $option }); - for (let i = 0; i < data.selectedKeys.length; i++) - if (areKeysEqual(optionKey, data.selectedKeys[i])) { - map[i] = convertOption(this.bindings, { $option }); - break; - } - }); - data.records = []; - for (let i = 0; i < data.selectedKeys.length; i++) if (map[i]) data.records.push(map[i]); - } else if (isArray(data.records)) - data.selectedKeys.push( - ...data.records.map(($value) => this.keyBindings.map((b) => Binding.get(b.local).value({ $value }))), - ); - } else { - let dataViewData = store.getData(); - data.selectedKeys.push(this.keyBindings.map((b) => Binding.get(b.local).value(dataViewData))); - if (!this.text && isArray(data.options)) { - let option = data.options.find(($option) => - areKeysEqual(getOptionKey(this.keyBindings, { $option }), data.selectedKeys[0]), - ); - data.text = (option && option[this.optionTextField]) || ""; - } - } - - instance.lastDropdown = context.lastDropdown; - - super.prepareData(context, instance); - } - - renderInput(context, instance, key) { - return ( - - ); - } - - filterOptions(instance, options, query) { - if (!query) return options; - let textPredicate = getSearchQueryPredicate(query); - return options.filter((o) => isString(o[this.optionTextField]) && textPredicate(o[this.optionTextField])); - } - - isEmpty(data) { - if (this.multiple) return !isNonEmptyArray(data.values) && !isNonEmptyArray(data.records); - return super.isEmpty(data); - } - - getValidationValue(data) { - if (this.multiple) return data.records ?? data.values; - return super.getValidationValue(data); - } - - formatValue(context, instance) { - if (!this.multiple) return super.formatValue(context, instance); - - let { records, values, options } = instance.data; - if (isArray(records)) { - let valueTextFormatter = - this.onGetRecordDisplayText ?? ((record) => record[this.valueTextField] || record[this.valueIdField]); - return records.map((record) => valueTextFormatter(record, instance)); - } - - if (isArray(values)) { - if (isArray(options)) - return values - .map((id) => { - let option = options.find((o) => o[this.optionIdField] == id); - return option ? option[this.valueTextField] : id; - }) - .filter(Boolean) - .join(", "); - - return values.join(", "); - } - - return null; - } -} - -LookupField.prototype.baseClass = "lookupfield"; -//LookupField.prototype.memoize = false; -LookupField.prototype.multiple = false; -LookupField.prototype.queryDelay = 150; -LookupField.prototype.minQueryLength = 0; -LookupField.prototype.hideSearchField = false; -LookupField.prototype.minOptionsForSearchField = 7; -LookupField.prototype.loadingText = "Loading..."; -LookupField.prototype.queryErrorText = "Error occurred while querying for lookup data."; -LookupField.prototype.noResultsText = "No results found."; -LookupField.prototype.optionIdField = "id"; -LookupField.prototype.optionTextField = "text"; -LookupField.prototype.valueIdField = "id"; -LookupField.prototype.valueTextField = "text"; -LookupField.prototype.suppressErrorsUntilVisited = true; -LookupField.prototype.fetchAll = false; -LookupField.prototype.cacheAll = false; -LookupField.prototype.showClear = true; -LookupField.prototype.alwaysShowClear = false; -LookupField.prototype.closeOnSelect = true; -LookupField.prototype.minQueryLengthMessageText = "Type in at least {0} character(s)."; -LookupField.prototype.icon = null; -LookupField.prototype.sort = false; -LookupField.prototype.listOptions = null; -LookupField.prototype.autoOpen = false; -LookupField.prototype.submitOnEnterKey = false; -LookupField.prototype.submitOnDropdownEnterKey = false; -LookupField.prototype.pageSize = 100; -LookupField.prototype.infinite = false; -LookupField.prototype.quickSelectAll = false; -LookupField.prototype.onGetRecordDisplayText = null; - -Localization.registerPrototype("cx/widgets/LookupField", LookupField); - -Widget.alias("lookupfield", LookupField); - -function getOptionKey(bindings, data) { - return bindings.filter((a) => a.key).map((b) => Binding.get(b.remote).value(data)); -} - -function areKeysEqual(key1, key2) { - if (!key1 || !key2 || key1.length != key2.length) return false; - - for (let i = 0; i < key1.length; i++) if (key1[i] !== key2[i]) return false; - - return true; -} - -function convertOption(bindings, data) { - let result = { $value: {} }; - bindings.forEach((b) => { - let value = Binding.get(b.remote).value(data); - result = Binding.get(b.local).set(result, value); - }); - return result.$value; -} - -class SelectionDelegate extends Selection { - constructor({ delegate }) { - super(); - this.delegate = delegate; - } - - getIsSelectedDelegate(store) { - return (record, index) => this.delegate(record, index); - } - - select() { - return false; - } -} - -class LookupComponent extends VDOM.Component { - constructor(props) { - super(props); - let { data, store } = this.props.instance; - this.dom = {}; - this.state = { - options: [], - formatted: data.formatted, - value: data.formatted, - dropdownOpen: false, - focus: false, - }; - - this.itemStore = new ReadOnlyDataView({ - store: store, - }); - } - - getOptionKey(data) { - return this.props.bindings.filter((a) => a.key).map((b) => Binding.get(b.remote).value(data)); - } - - getLocalKey(data) { - return this.props.bindings.filter((a) => a.key).map((b) => Binding.get(b.local).value(data)); - } - - findOption(options, key) { - if (!key) return -1; - for (let i = 0; i < options.length; i++) { - let optionKey = this.getOptionKey({ $option: options[i] }); - if (areKeysEqual(key, optionKey)) return i; - } - return -1; - } - - getDropdown() { - if (this.dropdown) return this.dropdown; - - let { widget, lastDropdown } = this.props.instance; - - this.list = Widget.create( - - this.onItemClick(e, inst)} - pipeKeyDown={(kd) => { - this.listKeyDown = kd; - }} - selectOnTab - focusable={false} - selection={{ - type: SelectionDelegate, - delegate: (data) => - this.props.instance.data.selectedKeys.find((x) => - areKeysEqual(x, this.getOptionKey({ $option: data })), - ) != null, - }} - > - {this.props.itemConfig} - - , - ); - - let dropdown = { - constrain: true, - scrollTracking: true, - inline: !isTouchDevice() || !!lastDropdown, - placementOrder: "down-right down-left up-right up-left", - ...widget.dropdownOptions, - type: Dropdown, - relatedElement: this.dom.input, - renderChildren: () => this.renderDropdownContents(), - onFocusOut: (e) => this.closeDropdown(e), - memoize: false, - touchFriendly: isTouchDevice(), - onMeasureNaturalContentSize: () => { - if (this.dom.dropdown && this.dom.list) { - return { - height: - this.dom.dropdown.offsetHeight - - this.dom.list.offsetHeight + - (this.dom.list.firstElementChild?.offsetHeight || 0), - }; - } - }, - onDismissAfterScroll: () => { - this.closeDropdown(null, true); - return false; - }, - }; - - return (this.dropdown = Widget.create(dropdown)); - } - - renderDropdownContents() { - let content; - let { instance } = this.props; - let { data, widget } = instance; - let { CSS, baseClass } = widget; - - let searchVisible = - !widget.hideSearchField && - (!isArray(data.visibleOptions) || - (widget.minOptionsForSearchField && data.visibleOptions.length >= widget.minOptionsForSearchField)); - - if (this.state.status == "loading") { - content = ( -
    - {widget.loadingText} -
    - ); - } else if (this.state.status == "error") { - content = ( -
    - {widget.queryErrorText} -
    - ); - } else if (this.state.status == "info") { - content = ( -
    - {this.state.message} -
    - ); - } else if (this.state.options.length == 0) { - content = ( -
    - {widget.noResultsText} -
    - ); - } else { - content = ( -
    { - this.dom.list = el; - this.subscribeListOnWheel(el); - this.subscribeListOnScroll(el); - }} - className={CSS.element(baseClass, "scroll-container")} - > - -
    - ); - } - - return ( -
    { - this.dom.dropdown = el; - }} - className={CSS.element(baseClass, "dropdown")} - tabIndex={0} - onFocus={(e) => this.onDropdownFocus(e)} - onKeyDown={(e) => this.onDropdownKeyPress(e)} - > - {searchVisible && ( - { - this.dom.query = el; - }} - type="text" - className={CSS.element(baseClass, "query")} - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - onChange={(e) => this.query(e.target.value)} - onBlur={(e) => this.onQueryBlur(e)} - /> - )} - {content} -
    - ); - } - - onListWheel(e) { - let { list } = this.dom; - if ( - (list.scrollTop + list.offsetHeight == list.scrollHeight && e.deltaY > 0) || - (list.scrollTop == 0 && e.deltaY < 0) - ) { - e.preventDefault(); - e.stopPropagation(); - } - } - - onListScroll() { - if (!this.dom.list) return; - var el = this.dom.list; - if (el.scrollTop > el.scrollHeight - 2 * el.offsetHeight) { - this.loadAdditionalOptionPages(); - } - } - - onDropdownFocus(e) { - if (this.dom.query && !isFocused(this.dom.query) && !isTouchDevice()) FocusManager.focus(this.dom.query); - } - - getPlaceholder(text) { - let { CSS, baseClass } = this.props.instance.widget; - - if (text) return {text}; - - return  ; - } - - render() { - let { instance, label, help, icon: iconVDOM } = this.props; - let { data, widget, state } = instance; - let { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let icon = iconVDOM && ( -
    { - this.openDropdown(e); - e.stopPropagation(); - e.preventDefault(); - }} - > - {iconVDOM} -
    - ); - - let dropdown; - if (this.state.dropdownOpen) { - this.itemStore.setData({ - $options: this.state.options, - $query: this.lastQuery, - }); - dropdown = ( - - ); - } - - let insideButton = null; - let multipleEntries = this.props.multiple && isArray(data.records) && data.records.length > 1; - - if (!data.readOnly) { - if ( - widget.showClear && - !data.disabled && - !data.empty && - (widget.alwaysShowClear || (!data.required && !this.props.multiple) || multipleEntries) - ) { - insideButton = ( -
    (!this.props.multiple ? this.onClearClick(e) : this.onClearMultipleClick(e))} - className={CSS.element(baseClass, "clear")} - > - -
    - ); - } else { - insideButton = ( -
    { - this.toggleDropdown(e, true); - e.stopPropagation(); - e.preventDefault(); - }} - > - -
    - ); - } - } - - let text; - - if (this.props.multiple) { - let readOnly = data.disabled || data.readOnly; - if (isNonEmptyArray(data.records)) { - let valueTextFormatter = widget.onGetRecordDisplayText ?? ((record) => record[widget.valueTextField]); - text = data.records.map((v, i) => ( -
    - {valueTextFormatter(v, instance)} - {!readOnly && ( -
    { - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => this.onClearClick(e, v)} - > - -
    - )} -
    - )); - } else { - text = this.getPlaceholder(data.placeholder); - } - } else { - text = !data.empty ? data.text || this.getPlaceholder() : this.getPlaceholder(data.placeholder); - } - - let states = { - visited: state.visited, - focus: this.state.focus || this.state.dropdownOpen, - icon: !!iconVDOM, - empty: !data.placeholder && data.empty, - error: data.error && (state.visited || !suppressErrorsUntilVisited || !data.empty), - }; - - return ( -
    this.onKeyDown(e)} - > -
    { - this.dom.input = el; - }} - aria-labelledby={data.id + "-label"} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} - onClick={(e) => this.onClick(e)} - onInput={(e) => this.onChange(e, "input")} - onChange={(e) => this.onChange(e, "change")} - onKeyDown={(e) => this.onInputKeyDown(e)} - onMouseDown={(e) => this.onMouseDown(e)} - onBlur={(e) => this.onBlur(e)} - onFocus={(e) => this.onFocus(e)} - > - {text} -
    - {insideButton} - {icon} - {dropdown} - {label} - {help} -
    - ); - } - - onMouseDown(e) { - //skip touch start to allow touch scrolling - if (isTouchEvent()) return; - e.preventDefault(); - e.stopPropagation(); - this.toggleDropdown(e, true); - } - - onClick(e) { - //mouse down will handle it for non-touch events - if (!isTouchEvent()) return; - e.preventDefault(); - e.stopPropagation(); - this.toggleDropdown(e, true); - } - - onItemClick(e, { store }) { - this.select(e, [store.getData()]); - if (!this.props.instance.widget.submitOnEnterKey || e.type != "keydown") e.stopPropagation(); - if (e.keyCode != KeyCode.tab) e.preventDefault(); - } - - onClearClick(e, value) { - let { instance } = this.props; - let { data, store, widget } = instance; - let { keyBindings } = widget; - e.stopPropagation(); - e.preventDefault(); - if (widget.multiple) { - if (isArray(data.records)) { - let itemKey = this.getLocalKey({ $value: value }); - let newRecords = data.records.filter((v) => !areKeysEqual(this.getLocalKey({ $value: v }), itemKey)); - - instance.set("records", newRecords); - - let newValues = newRecords - .map((rec) => this.getLocalKey({ $value: rec })) - .map((k) => (keyBindings.length == 1 ? k[0] : k)); - - instance.set("values", newValues); - } - } else { - this.props.bindings.forEach((b) => { - store.set(b.local, widget.emptyValue); - }); - } - - if (!isTouchEvent(e)) this.dom.input.focus(); - } - - onClearMultipleClick(e) { - let { instance } = this.props; - instance.set("records", []); - instance.set("values", []); - } - - select(e, itemsData, reset) { - let { instance } = this.props; - let { store, data, widget } = instance; - let { bindings, keyBindings } = widget; - - if (widget.multiple) { - let { selectedKeys, records } = data; - - let newRecords = reset ? [] : [...(records || [])]; - let singleSelect = itemsData.length == 1; - let optionKey = null; - if (singleSelect) optionKey = this.getOptionKey(itemsData[0]); - - // deselect - if (singleSelect && selectedKeys.find((k) => areKeysEqual(optionKey, k))) { - newRecords = records.filter((v) => !areKeysEqual(optionKey, this.getLocalKey({ $value: v }))); - } else { - itemsData.forEach((itemData) => { - let valueData = { - $value: {}, - }; - bindings.forEach((b) => { - valueData = Binding.get(b.local).set(valueData, Binding.get(b.remote).value(itemData)); - }); - newRecords.push(valueData.$value); - }); - } - - instance.set("records", newRecords); - - let newValues = newRecords - .map((rec) => this.getLocalKey({ $value: rec })) - .map((k) => (keyBindings.length == 1 ? k[0] : k)); - - instance.set("values", newValues); - } else { - bindings.forEach((b) => { - let v = Binding.get(b.remote).value(itemsData[0]); - if (b.set) b.set(v, instance); - else store.set(b.local, v); - }); - } - - if (widget.closeOnSelect) { - //Pressing Tab should work it's own thing. Focus will move elsewhere and the dropdown will close. - if (e.keyCode != KeyCode.tab) { - if (!isTouchEvent(e)) this.dom.input.focus(); - this.closeDropdown(e); - } - } - - if (e.keyCode == KeyCode.enter && widget.submitOnDropdownEnterKey) { - this.submitOnEnter(e); - } - } - - onDropdownKeyPress(e) { - switch (e.keyCode) { - case KeyCode.esc: - this.closeDropdown(e); - this.dom.input.focus(); - break; - - case KeyCode.tab: - // if tab needs to do a list selection, we have to first call List's handleKeyDown - if (this.listKeyDown) this.listKeyDown(e); - // if next focusable element is disabled, recalculate and update the dom before switching focus - this.props.forceUpdate(); - break; - - case KeyCode.a: - if (!e.ctrlKey) return; - - let { quickSelectAll, multiple } = this.props.instance.widget; - if (!quickSelectAll || !multiple) return; - - let optionsToSelect = this.state.options.map((o) => ({ - $option: o, - })); - this.select(e, optionsToSelect, true); - e.stopPropagation(); - e.preventDefault(); - break; - - default: - if (this.listKeyDown) this.listKeyDown(e); - break; - } - } - - onKeyDown(e) { - switch (e.keyCode) { - case KeyCode.pageDown: - case KeyCode.pageUp: - if (this.state.dropdownOpen) e.preventDefault(); - break; - } - } - - onInputKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.delete: - this.onClearClick(e); - return; - - case KeyCode.shift: - case KeyCode.ctrl: - case KeyCode.tab: - case KeyCode.left: - case KeyCode.right: - case KeyCode.pageUp: - case KeyCode.pageDown: - case KeyCode.insert: - case KeyCode.esc: - break; - - case KeyCode.down: - this.openDropdown(e); - e.stopPropagation(); - break; - - case KeyCode.enter: - if (this.props.instance.widget.submitOnEnterKey) { - this.submitOnEnter(e); - } else { - this.openDropdown(e); - } - break; - - default: - this.openDropdown(e); - break; - } - } - - onQueryBlur(e) { - FocusManager.nudge(); - } - - onFocus(e) { - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - - if (this.props.instance.data.autoOpen) this.openDropdown(null); - } - - onBlur(e) { - if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); - - if (this.state.focus) - this.setState({ - focus: false, - }); - } - - toggleDropdown(e, keepFocus) { - if (this.state.dropdownOpen) this.closeDropdown(e, keepFocus); - else this.openDropdown(e); - } - - closeDropdown(e, keepFocus) { - if (this.state.dropdownOpen) { - this.setState( - { - dropdownOpen: false, - }, - () => keepFocus && this.dom.input.focus(), - ); - - this.props.instance.setState({ - visited: true, - }); - } - - //delete results valid only while the dropdown is open - delete this.tmpCachedResult; - } - - openDropdown(e) { - let { instance } = this.props; - let { data } = instance; - if (!this.state.dropdownOpen && !data.disabled && !data.readOnly) { - this.query(""); - this.setState( - { - dropdownOpen: true, - }, - () => { - if (this.dom.dropdown) this.dom.dropdown.focus(); - }, - ); - } - } - - query(q) { - /* - In fetchAll mode onQuery should fetch all data and after - that everything is done filtering is done client-side. - If cacheAll is set results are cached for the lifetime of the - widget, otherwise cache is invalidated when dropdown closes. - */ - - let { instance } = this.props; - let { widget, data } = instance; - - this.lastQuery = q; - - //do not make duplicate queries if fetchAll is enabled - if (widget.fetchAll && this.state.status == "loading") return; - - if (this.queryTimeoutId) clearTimeout(this.queryTimeoutId); - - if (q.length < widget.minQueryLength) { - this.setState({ - status: "info", - message: StringTemplate.format(widget.minQueryLengthMessageText, widget.minQueryLength), - }); - return; - } - - if (isArray(data.visibleOptions)) { - let results = widget.filterOptions(this.props.instance, data.visibleOptions, q); - this.setState({ - options: results, - status: "loaded", - }); - } - - if (widget.onQuery) { - let { queryDelay, fetchAll, cacheAll, pageSize } = widget; - - if (fetchAll) queryDelay = 0; - - if (!this.cachedResult) { - this.setState({ - status: "loading", - }); - } - - this.queryTimeoutId = setTimeout(() => { - delete this.queryTimeoutId; - - let result = this.tmpCachedResult || this.cachedResult; - let query = fetchAll ? "" : q; - let params = !widget.infinite - ? query - : { - query, - page: 1, - pageSize, - }; - - if (!result) result = instance.invoke("onQuery", params, instance); - - let queryId = (this.lastQueryId = Date.now()); - - Promise.resolve(result) - .then((results) => { - //discard results which do not belong to the last query - if (queryId !== this.lastQueryId) return; - - if (!isArray(results)) results = []; - - if (fetchAll) { - if (cacheAll) this.cachedResult = results; - else this.tmpCachedResult = results; - - results = widget.filterOptions(this.props.instance, results, this.lastQuery); - } - - this.setState( - { - page: 1, - query, - options: results, - status: "loaded", - }, - () => { - if (widget.infinite) this.onListScroll(); - }, - ); - }) - .catch((err) => { - this.setState({ status: "error" }); - debug("Lookup query error:", err); - }); - }, queryDelay); - } - } - - loadAdditionalOptionPages() { - let { instance } = this.props; - let { widget } = instance; - if (!widget.infinite) return; - - let { query, page, status, options } = this.state; - - let blockerKey = query; - - if (status != "loaded") return; - - if (options.length < page * widget.pageSize) return; //some pages were not full which means we reached the end - - if (this.extraPageLoadingBlocker === blockerKey) return; - - this.extraPageLoadingBlocker = blockerKey; - - let params = { - page: page + 1, - query, - pageSize: widget.pageSize, - }; - - var result = instance.invoke("onQuery", params, instance); - - Promise.resolve(result) - .then((results) => { - //discard results which do not belong to the last query - if (this.extraPageLoadingBlocker !== blockerKey) return; - - this.extraPageLoadingBlocker = false; - - if (!isArray(results)) return; - - this.setState( - { - page: params.page, - query, - options: [...options, ...results], - }, - () => { - this.onListScroll(); - }, - ); - }) - .catch((err) => { - if (this.extraPageLoadingBlocker !== blockerKey) return; - this.extraPageLoadingBlocker = false; - this.setState({ status: "error" }); - debug("Lookup query error:", err); - console.error(err); - }); - } - - UNSAFE_componentWillReceiveProps(props) { - tooltipParentWillReceiveProps(this.dom.input, ...getFieldTooltip(props.instance)); - } - - componentDidMount() { - tooltipParentDidMount(this.dom.input, ...getFieldTooltip(this.props.instance)); - autoFocus(this.dom.input, this); - } - - componentDidUpdate() { - autoFocus(this.dom.input, this); - } - - componentWillUnmount() { - if (this.queryTimeoutId) clearTimeout(this.queryTimeoutId); - tooltipParentWillUnmount(this.props.instance); - this.subscribeListOnWheel(null); - } - - subscribeListOnWheel(list) { - if (this.unsubscribeListOnWheel) { - this.unsubscribeListOnWheel(); - this.unsubscribeListOnWheel = null; - } - if (list) { - this.unsubscribeListOnWheel = addEventListenerWithOptions(list, "wheel", (e) => this.onListWheel(e), { - passive: false, - }); - } - } - - subscribeListOnScroll(list) { - if (this.unsubscribeListOnScroll) { - this.unsubscribeListOnScroll(); - this.unsubscribeListOnScroll = null; - } - if (list) { - this.unsubscribeListOnScroll = addEventListenerWithOptions(list, "scroll", (e) => this.onListScroll(e), { - passive: false, - }); - } - } - - submitOnEnter(e) { - let instance = this.props.instance.parent; - while (instance) { - if (instance.events && instance.events.onSubmit) { - instance.events.onSubmit(e, instance); - break; - } else { - instance = instance.parent; - } - } - } -} diff --git a/packages/cx/src/widgets/form/LookupField.scss b/packages/cx/src/widgets/form/LookupField.scss index 3e47245d2..58cb10eda 100644 --- a/packages/cx/src/widgets/form/LookupField.scss +++ b/packages/cx/src/widgets/form/LookupField.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-lookupfield( $name: "lookupfield", $state-style-map: $cx-std-field-state-style-map, @@ -12,9 +14,9 @@ $tag-clear-state-style-map: $cx-input-tag-clear-state-style-map, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $padding: cx-get-state-rule($state-style-map, default, padding); $border-radius-offset: round(cx-get-state-rule($tag-state-style-map, default, border-radius) * 0.25); diff --git a/packages/cx/src/widgets/form/LookupField.spec.tsx b/packages/cx/src/widgets/form/LookupField.spec.tsx new file mode 100644 index 000000000..ddd481880 --- /dev/null +++ b/packages/cx/src/widgets/form/LookupField.spec.tsx @@ -0,0 +1,93 @@ +import { createAccessorModelProxy } from "../../data/createAccessorModelProxy"; +import { LookupField } from "./LookupField"; + +interface User { + id: number; + name: string; + email: string; +} + +interface SelectedUser { + id: number; + name: string; +} + +interface Model { + users: User[]; + selectedUsers: SelectedUser[]; + selectedUserId: number; +} + +describe("LookupField", () => { + it("infers TOption from AccessorChain in options prop", () => { + const model = createAccessorModelProxy(); + + // TOption should be inferred as User from model.users (AccessorChain) + let widget = ( + + { + // Should return User[] + return [] as User[]; + }} + onCreateVisibleOptionsFilter={(params, instance) => (option) => { + // option should be typed as User + const id: number = option.id; + const name: string = option.name; + const email: string = option.email; + return true; + }} + /> + + ); + }); + + it("infers TRecord from AccessorChain in records prop", () => { + const model = createAccessorModelProxy(); + + // TRecord should be inferred as SelectedUser from model.selectedUsers + let widget = ( + + { + // record should be typed as SelectedUser + const id: number = record.id; + const name: string = record.name; + return record.name; + }} + /> + + ); + }); + + it("infers both TOption and TRecord from accessor chains", () => { + const model = createAccessorModelProxy(); + + let widget = ( + + { + // Should return User[] + return [] as User[]; + }} + onCreateVisibleOptionsFilter={(params) => (option) => { + // option should be User + const email: string = option.email; + return true; + }} + onGetRecordDisplayText={(record) => { + // record should be SelectedUser + const name: string = record.name; + return name; + }} + /> + + ); + }); +}); diff --git a/packages/cx/src/widgets/form/LookupField.tsx b/packages/cx/src/widgets/form/LookupField.tsx new file mode 100644 index 000000000..3bcc47e0d --- /dev/null +++ b/packages/cx/src/widgets/form/LookupField.tsx @@ -0,0 +1,1421 @@ +/**@jsxImportSource react */ +import { Widget, VDOM, getContent } from "../../ui/Widget"; +import { Cx } from "../../ui/Cx"; +import { Field, getFieldTooltip, FieldInstance } from "./Field"; +import { ReadOnlyDataView } from "../../data/ReadOnlyDataView"; +import { HtmlElement, HtmlElementInstance } from "../HtmlElement"; +import { Binding, BindingInput } from "../../data/Binding"; +import { debug } from "../../util/Debug"; +import { Dropdown, DropdownConfig } from "../overlay/Dropdown"; +import { FocusManager } from "../../ui/FocusManager"; +import { isFocused } from "../../util/DOM"; +import { isTouchDevice } from "../../util/isTouchDevice"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, + tooltipMouseMove, + tooltipMouseLeave, + tooltipParentDidMount, +} from "../overlay/tooltip-ops"; +import { stopPropagation, preventDefault } from "../../util/eventCallbacks"; +import ClearIcon from "../icons/clear"; +import DropdownIcon from "../icons/drop-down"; +import { getSearchQueryPredicate } from "../../util/getSearchQueryPredicate"; +import { KeyCode } from "../../util/KeyCode"; +import { Localization } from "../../ui/Localization"; +import { StringTemplate } from "../../data/StringTemplate"; +import { Icon } from "../Icon"; +import { isString } from "../../util/isString"; +import { isDefined } from "../../util/isDefined"; +import { isArray } from "../../util/isArray"; +import { isNonEmptyArray } from "../../util/isNonEmptyArray"; +import { addEventListenerWithOptions } from "../../util/addEventListenerWithOptions"; +import { List } from "../List"; +import { Selection } from "../../ui/selection/Selection"; +import { HighlightedSearchText } from "../HighlightedSearchText"; +import { autoFocus } from "../autoFocus"; +import { bind } from "../../ui"; +import { AccessorChain, isAccessorChain } from "../../data/createAccessorModelProxy"; +import type { CxChild, RenderingContext } from "../../ui/RenderingContext"; +import type { DropdownInstance, Instance } from "../../ui/Instance"; +import { FieldConfig } from "./Field"; +import { Prop, BooleanProp, StringProp, StructuredProp, DataRecord } from "../../ui/Prop"; + +export interface LookupBinding { + local: string; + remote: string; + key?: boolean; +} + +export interface LookupFieldConfig extends FieldConfig { + /** Defaults to `false`. Set to `true` to enable multiple selection. */ + multiple?: BooleanProp; + + /** Selected value. Used only if `multiple` is set to `false`. */ + value?: Prop; + + /** A list of selected ids. Used only if `multiple` is set to `true`. */ + values?: Prop<(number | string)[]>; + + /** A list of selected records. Used only if `multiple` is set to `true`. */ + records?: Prop; + + /** Text associated with the selection. Used only if `multiple` is set to `false`. */ + text?: StringProp; + + /** The opposite of `disabled`. */ + enabled?: BooleanProp; + + /** Defaults to `false`. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** Default text displayed when the field is empty. */ + placeholder?: StringProp; + + /** A list of available options. */ + options?: Prop; + + /** Set to `true` to hide the clear button. Default value is `false`. */ + hideClear?: boolean; + + /** Set to `false` to hide the clear button. Default value is `true`. */ + showClear?: boolean; + + /** Set to `true` to display the clear button even if `required` is set. Default is `false`. */ + alwaysShowClear?: boolean; + + /** Base CSS class to be applied to the field. Defaults to `lookupfield`. */ + baseClass?: string; + + /** Name or configuration of the icon to be put on the left side of the input. */ + icon?: StringProp | Record; + + /** Additional config to be applied to all items. */ + itemConfig?: any; + + /** An array of objects describing the mapping of option data to store data. */ + bindings?: LookupBinding[]; + + /** A delay in milliseconds between typing stop and query. Default is `150`. */ + queryDelay?: number; + + /** Minimal number of characters required before query is made. */ + minQueryLength?: number; + + /** Set to `true` to hide the search field. */ + hideSearchField?: boolean; + + /** Number of options required to show search field. Defaults to `7`. */ + minOptionsForSearchField?: number; + + /** Text to display while data is being loaded. */ + loadingText?: string; + + /** Error message displayed if server query throws an exception. */ + queryErrorText?: string; + + /** Message to be displayed if no entries match the user query. */ + noResultsText?: string; + + /** Name of the field which holds the id of the option. Default is `id`. */ + optionIdField?: string; + + /** Name of the field which holds the display text of the option. Default is `text`. */ + optionTextField?: string; + + /** Name of the field to store id of selected value in multiple mode. Default is `id`. */ + valueIdField?: string; + + /** Name of the field to store display text of selected value. Default is `text`. */ + valueTextField?: string; + + /** `onQuery` will be called once to fetch all options; filtering occurs client-side. */ + fetchAll?: boolean; + + /** When set with `fetchAll`, fetched options are cached for widget lifetime. */ + cacheAll?: boolean; + + /** Close the dropdown after selection. Default is `true`. */ + closeOnSelect?: boolean; + + /** Message displayed if the entered search query is too short. */ + minQueryLengthMessageText?: string; + + /** Query function called to fetch options. */ + onQuery?: + | string + | (( + query: string | { query: string; page: number; pageSize: number }, + instance: Instance, + ) => TOption[] | Promise); + + /** Set to `true` to sort dropdown options. */ + sort?: boolean; + + /** Additional list options, such as grouping configuration, custom sorting, etc. */ + listOptions?: Record; + + /** Show dropdown immediately after component mount; useful for cell editing. */ + autoOpen?: BooleanProp; + + /** Allow enter key events to propagate; useful for forms or grid cell editors. */ + submitOnEnterKey?: BooleanProp; + + /** Allow dropdown enter key events to propagate for form submission. */ + submitOnDropdownEnterKey?: BooleanProp; + + /** Number of additional items loaded in `infinite` mode. Default is `100`. */ + pageSize?: number; + + /** Set to `true` to enable loading additional options when scroll reaches end. */ + infinite?: boolean; + + /** Allow quick selection of all displayed items on `Ctrl + A` key combination. */ + quickSelectAll?: boolean; + + /** Parameters that affect filtering. */ + filterParams?: StructuredProp; + + /** Used in multiple selection lookups to construct display text from multiple fields. */ + onGetRecordDisplayText?: ((record: TRecord, instance: Instance) => string) | null; + + /** Callback to create a filter function for given filter params. */ + onCreateVisibleOptionsFilter?: + | string + | ((filterParams: unknown, instance: Instance) => (option: TOption) => boolean); + + /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ + dropdownOptions?: Partial; + + /** Custom validation function. */ + onValidate?: + | string + | ((value: number | string, instance: Instance, validationParams: Record) => unknown); +} + +export class LookupField extends Field< + LookupFieldConfig +> { + declare public baseClass: string; + declare public multiple: boolean; + declare public hideClear?: boolean; + declare public showClear: boolean; + declare public alwaysShowClear: boolean; + declare public hideSearchField: boolean; + declare public minOptionsForSearchField: number; + declare public loadingText: string; + declare public queryErrorText: string; + declare public noResultsText: string; + declare public optionIdField: string; + declare public optionTextField: string; + declare public valueIdField: string; + declare public valueTextField: string; + declare public fetchAll: boolean; + declare public cacheAll: boolean; + declare public closeOnSelect: boolean; + declare public minQueryLengthMessageText: string; + declare public sort?: boolean; + declare public listOptions?: Record | null; + declare public autoOpen?: boolean; + declare public submitOnEnterKey?: boolean; + declare public submitOnDropdownEnterKey?: boolean; + declare public pageSize: number; + declare public infinite?: boolean; + declare public quickSelectAll?: boolean; + declare public queryDelay: number; + declare public minQueryLength: number; + declare public onGetRecordDisplayText?: ((record: Record, instance: Instance) => string) | null; + declare public onQuery?: + | string + | (( + params: string | { query: string; page: number; pageSize: number }, + instance: Instance, + ) => Promise[]> | Record[]); + declare public onCreateVisibleOptionsFilter?: + | string + | ((filterParams: unknown, instance: Instance) => (option: Record) => boolean); + declare public value?: BindingInput; + declare public text?: BindingInput; + declare public records?: Record[]; + declare public values?: unknown[]; + declare public options?: Record[]; + + declare public enabled?: boolean; + declare public placeholder?: string; + declare public readOnly?: boolean; + declare public dropdownOptions?: Partial; + declare public bindings?: BindingConfig[]; + declare public keyBindings?: BindingConfig[]; + declare public itemConfig?: CxChild; + + declareData(...args: Record[]): void { + let additionalAttributes = this.multiple + ? { values: undefined, records: undefined } + : { value: undefined, text: undefined }; + + super.declareData( + { + disabled: undefined, + enabled: undefined, + placeholder: undefined, + required: undefined, + options: undefined, + icon: undefined, + autoOpen: undefined, + readOnly: undefined, + filterParams: { structured: true }, + }, + additionalAttributes, + ...args, + ); + } + + init(): void { + if (isDefined(this.hideClear)) this.showClear = !this.hideClear; + + if (this.alwaysShowClear) this.showClear = true; + + if (!this.bindings) { + let b: BindingConfig[] = []; + if (this.value) { + if (isAccessorChain(this.value)) this.value = bind(this.value); + if ((this.value as any).bind) + b.push({ + key: true, + local: (this.value as any).bind, + remote: `$option.${this.optionIdField}`, + set: (this.value as any).set, + }); + } + + if (this.text as string | AccessorChain) { + if (isAccessorChain(this.text)) this.text = bind(this.text); + if ((this.text as any).bind) + b.push({ + local: (this.text as any).bind, + remote: `$option.${this.optionTextField}`, + set: (this.text as any).set, + }); + } + + this.bindings = b; + } + + if (this.bindings.length == 0 && this.multiple) + this.bindings = [ + { + key: true, + local: `$value.${this.valueIdField}`, + remote: `$option.${this.optionIdField}`, + }, + { + local: `$value.${this.valueTextField}`, + remote: `$option.${this.optionTextField}`, + }, + ]; + + this.keyBindings = this.bindings.filter((b) => b.key); + + if (!this.items && !this.children) + this.items = { + type: HighlightedSearchText, + text: { bind: `$option.${this.optionTextField}` }, + query: { bind: "$query" }, + } as any; + + this.itemConfig = this.children || this.items; + + this.items = []; + delete this.children; + + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + let { data, store } = instance; + + data.stateMods = { + multiple: this.multiple, + single: !this.multiple, + disabled: data.disabled, + readonly: data.readOnly, + }; + + data.visibleOptions = data.options; + if (this.onCreateVisibleOptionsFilter && isArray(data.options)) { + let filterPredicate = instance.invoke("onCreateVisibleOptionsFilter", data.filterParams, instance); + data.visibleOptions = data.options.filter(filterPredicate); + } + + data.selectedKeys = []; + + if (this.multiple) { + if (isArray(data.values) && isArray(data.options)) { + data.selectedKeys = data.values.map((v) => (this.keyBindings!.length == 1 ? [v] : v)); + let map: Record> = {}; + data.options.filter(($option) => { + let optionKey = getOptionKey(this.keyBindings!, { $option }); + for (let i = 0; i < data.selectedKeys.length; i++) + if (areKeysEqual(optionKey, data.selectedKeys[i])) { + map[i] = convertOption(this.bindings!, { $option }); + break; + } + }); + data.records = []; + for (let i = 0; i < data.selectedKeys.length; i++) if (map[i]) data.records.push(map[i]); + } else if (isArray(data.records)) + data.selectedKeys.push( + ...data.records.map(($value) => this.keyBindings!.map((b) => Binding.get(b.local).value({ $value }))), + ); + } else { + let dataViewData = store.getData(); + data.selectedKeys.push(this.keyBindings!.map((b) => Binding.get(b.local).value(dataViewData))); + if (!this.text && isArray(data.options)) { + let option = data.options.find(($option) => + areKeysEqual(getOptionKey(this.keyBindings!, { $option }), data.selectedKeys[0]), + ); + data.text = (option && (option as any)[this.optionTextField!]) || ""; + } + } + + (instance as DropdownInstance).lastDropdown = context.lastDropdown; + + super.prepareData(context, instance); + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + return ( + + ); + } + + filterOptions(instance: Instance, options: DataRecord[], query?: string): DataRecord[] { + if (!query) return options; + let textPredicate = getSearchQueryPredicate(query); + return options.filter( + (o) => isString(o[this.optionTextField!]) && textPredicate((o as any)[this.optionTextField!] as string), + ); + } + + isEmpty(data: Record): boolean { + if (this.multiple) return !isNonEmptyArray(data.values) && !isNonEmptyArray(data.records); + return super.isEmpty(data); + } + + getValidationValue(data: Record): unknown { + if (this.multiple) return data.records ?? data.values; + return super.getValidationValue(data); + } + + formatValue(context: RenderingContext, instance: Instance): string | React.ReactNode { + if (!this.multiple) return super.formatValue(context, instance); + + let { records, values, options } = instance.data; + if (isArray(records)) { + let valueTextFormatter = + typeof this.onGetRecordDisplayText === "function" + ? this.onGetRecordDisplayText + : (record: Record) => + (record as any)[this.valueTextField!] || (record as any)[this.valueIdField!]; + return records.map((record) => valueTextFormatter(record as any, instance)); + } + + if (isArray(values)) { + if (isArray(options)) + return values + .map((id) => { + let option = options.find((o) => (o as any)[this.optionIdField!] == id); + return option ? (option as any)[this.valueTextField!] : id; + }) + .filter(Boolean) + .join(", "); + + return values.join(", "); + } + + return null; + } +} + +LookupField.prototype.baseClass = "lookupfield"; +//LookupField.prototype.memoize = false; +LookupField.prototype.multiple = false; +LookupField.prototype.queryDelay = 150; +LookupField.prototype.minQueryLength = 0; +LookupField.prototype.hideSearchField = false; +LookupField.prototype.minOptionsForSearchField = 7; +LookupField.prototype.loadingText = "Loading..."; +LookupField.prototype.queryErrorText = "Error occurred while querying for lookup data."; +LookupField.prototype.noResultsText = "No results found."; +LookupField.prototype.optionIdField = "id"; +LookupField.prototype.optionTextField = "text"; +LookupField.prototype.valueIdField = "id"; +LookupField.prototype.valueTextField = "text"; +LookupField.prototype.suppressErrorsUntilVisited = true; +LookupField.prototype.fetchAll = false; +LookupField.prototype.cacheAll = false; +LookupField.prototype.showClear = true; +LookupField.prototype.alwaysShowClear = false; +LookupField.prototype.closeOnSelect = true; +LookupField.prototype.minQueryLengthMessageText = "Type in at least {0} character(s)."; +LookupField.prototype.icon = null; +LookupField.prototype.sort = false; +LookupField.prototype.listOptions = null; +LookupField.prototype.autoOpen = false; +LookupField.prototype.submitOnEnterKey = false; +LookupField.prototype.submitOnDropdownEnterKey = false; +LookupField.prototype.pageSize = 100; +LookupField.prototype.infinite = false; +LookupField.prototype.quickSelectAll = false; +LookupField.prototype.onGetRecordDisplayText = null; + +Localization.registerPrototype("cx/widgets/LookupField", LookupField); + +Widget.alias("lookupfield", LookupField); + +interface BindingConfig { + local: string; + remote: string; + key?: boolean; + set?: (value: unknown, instance: Instance) => void; +} + +function getOptionKey(bindings: BindingConfig[], data: Record): unknown[] { + return bindings.filter((a) => a.key).map((b) => Binding.get(b.remote).value(data)); +} + +function areKeysEqual(key1: unknown[], key2: unknown[]): boolean { + if (!key1 || !key2 || key1.length != key2.length) return false; + + for (let i = 0; i < key1.length; i++) if (key1[i] !== key2[i]) return false; + + return true; +} + +function convertOption(bindings: BindingConfig[], data: Record): Record { + let result: Record = { $value: {} }; + bindings.forEach((b) => { + let value = Binding.get(b.remote).value(data); + result = Binding.get(b.local).set(result, value); + }); + return result.$value as Record; +} + +class SelectionDelegate extends Selection { + delegate: (record: Record, index: number) => boolean; + + constructor({ delegate }: { delegate: (record: Record, index: number) => boolean }) { + super(); + this.delegate = delegate; + } + + getIsSelectedDelegate(store: unknown): (record: Record, index: number) => boolean { + return (record: Record, index: number) => this.delegate(record, index); + } + + select(): boolean { + return false; + } +} + +interface LookupComponentProps { + instance: FieldInstance; + multiple: boolean; + itemConfig: unknown; + bindings: BindingConfig[]; + baseClass: string; + label?: React.ReactNode; + help?: React.ReactNode; + forceUpdate: () => void; + icon?: React.ReactNode; +} + +interface LookupComponentState { + options: unknown[]; + formatted?: string; + value?: string; + dropdownOpen: boolean; + focus: boolean; + status?: string; + message?: string; + query?: string; + page?: number; + hover?: boolean; +} + +class LookupComponent extends VDOM.Component { + dom: { + input?: HTMLDivElement | null; + dropdown?: HTMLDivElement | null; + list?: HTMLDivElement | null; + query?: HTMLInputElement | null; + } = {}; + itemStore: ReadOnlyDataView; + dropdown?: Widget; + list?: Widget; + listKeyDown?: (e: React.KeyboardEvent) => void; + queryTimeoutId?: ReturnType; + cachedResult?: Record[]; + tmpCachedResult?: Record[]; + lastQueryId?: number; + lastQuery?: string; + extraPageLoadingBlocker?: string | false; + unsubscribeListOnWheel?: (() => void) | null; + unsubscribeListOnScroll?: (() => void) | null; + + constructor(props: LookupComponentProps) { + super(props); + let { data, store } = this.props.instance; + this.dom = {}; + this.state = { + options: [], + formatted: data.formatted, + value: data.formatted, + dropdownOpen: false, + focus: false, + }; + + this.itemStore = new ReadOnlyDataView({ + store: store, + }); + } + + getOptionKey(data: Record): unknown[] { + return this.props.bindings.filter((a) => a.key).map((b) => Binding.get(b.remote).value(data)); + } + + getLocalKey(data: Record): unknown[] { + return this.props.bindings.filter((a) => a.key).map((b) => Binding.get(b.local).value(data)); + } + + findOption(options: Record[], key: unknown[]): number { + if (!key) return -1; + for (let i = 0; i < options.length; i++) { + let optionKey = this.getOptionKey({ $option: options[i] }); + if (areKeysEqual(key, optionKey)) return i; + } + return -1; + } + + getDropdown(): Widget { + if (this.dropdown) return this.dropdown; + + let { widget }: { widget: LookupField } = this.props.instance as unknown as { widget: LookupField }; + let { lastDropdown } = this.props.instance as DropdownInstance; + + this.list = Widget.create({ + type: List, + sortField: widget.sort && widget.optionTextField, + sortDirection: "ASC", + mod: "dropdown", + scrollSelectionIntoView: true, + cached: widget.infinite, + ...widget.listOptions, + records: bind("$options"), + recordName: "$option", + onItemClick: (e: React.MouseEvent, inst: Instance) => this.onItemClick(e, inst), + pipeKeyDown: (kd: (e: React.KeyboardEvent) => void) => { + this.listKeyDown = kd; + }, + selectOnTab: true, + focusable: false, + selection: { + type: SelectionDelegate, + delegate: (data: any) => + this.props.instance.data.selectedKeys.find((x: any) => + areKeysEqual(x, this.getOptionKey({ $option: data })), + ) != null, + }, + children: this.props.itemConfig, + }); + + let dropdown = { + constrain: true, + scrollTracking: true, + inline: !isTouchDevice() || !!lastDropdown, + placementOrder: "down-right down-left up-right up-left", + ...widget.dropdownOptions, + type: Dropdown, + relatedElement: this.dom.input, + renderChildren: () => this.renderDropdownContents(), + onFocusOut: (e: React.MouseEvent) => this.closeDropdown(e), + memoize: false, + touchFriendly: isTouchDevice(), + onMeasureNaturalContentSize: () => { + if (this.dom.dropdown && this.dom.list) { + return { + height: + this.dom.dropdown.offsetHeight - + this.dom.list.offsetHeight + + ((this.dom.list.firstElementChild as HTMLElement)?.offsetHeight || 0), + }; + } + }, + onDismissAfterScroll: () => { + this.closeDropdown(null, true); + return false; + }, + }; + + return (this.dropdown = Widget.create(dropdown)); + } + + renderDropdownContents(): React.ReactNode { + let content; + let { instance } = this.props; + let { data, widget }: { data: Record; widget: LookupField } = instance as unknown as { + data: Record; + widget: LookupField; + }; + let { CSS, baseClass } = widget; + + let searchVisible = + !widget.hideSearchField && + (!isArray(data.visibleOptions) || + (widget.minOptionsForSearchField && data.visibleOptions.length >= widget.minOptionsForSearchField)); + + if (this.state.status == "loading") { + content = ( +
    + {widget.loadingText} +
    + ); + } else if (this.state.status == "error") { + content = ( +
    + {widget.queryErrorText} +
    + ); + } else if (this.state.status == "info") { + content = ( +
    + {this.state.message} +
    + ); + } else if (this.state.options.length == 0) { + content = ( +
    + {widget.noResultsText} +
    + ); + } else { + content = ( +
    { + this.dom.list = el; + this.subscribeListOnWheel(el); + this.subscribeListOnScroll(el); + }} + className={CSS.element(baseClass, "scroll-container")} + > + +
    + ); + } + + return ( +
    { + this.dom.dropdown = el as HTMLDivElement; + }} + className={CSS.element(baseClass, "dropdown")} + tabIndex={0} + onFocus={(e) => this.onDropdownFocus(e)} + onKeyDown={(e) => this.onDropdownKeyPress(e)} + > + {searchVisible && ( + { + this.dom.query = el as HTMLInputElement; + }} + type="text" + className={CSS.element(baseClass, "query")} + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + onChange={(e) => this.query(e.target.value)} + onBlur={(e) => this.onQueryBlur(e)} + /> + )} + {content} +
    + ); + } + + onListWheel(e: WheelEvent): void { + let { list } = this.dom; + if ( + list && + ((list.scrollTop + list.offsetHeight == list.scrollHeight && e.deltaY > 0) || + (list.scrollTop == 0 && e.deltaY < 0)) + ) { + e.preventDefault(); + e.stopPropagation(); + } + } + + onListScroll(): void { + if (!this.dom.list) return; + var el = this.dom.list; + if (el.scrollTop > el.scrollHeight - 2 * el.offsetHeight) { + this.loadAdditionalOptionPages(); + } + } + + onDropdownFocus(e: React.FocusEvent): void { + if (this.dom.query && !isFocused(this.dom.query) && !isTouchDevice()) FocusManager.focus(this.dom.query); + } + + getPlaceholder(text?: string): React.ReactNode { + let { CSS, baseClass } = this.props.instance.widget; + + if (text) return {text}; + + return  ; + } + + render(): React.ReactNode { + let { instance, label, help, icon: iconVDOM } = this.props; + let { data, widget, state } = instance; + let { CSS, baseClass, suppressErrorsUntilVisited } = widget as LookupField; + + let icon = iconVDOM && ( +
    { + this.openDropdown(e); + e.stopPropagation(); + e.preventDefault(); + }} + > + {iconVDOM} +
    + ); + + let dropdown; + if (this.state.dropdownOpen) { + this.itemStore.setData({ + $options: this.state.options, + $query: this.lastQuery, + }); + dropdown = ( + + ); + } + + let insideButton = null; + let multipleEntries = this.props.multiple && isArray(data.records) && data.records.length > 1; + + if (!data.readOnly) { + if ( + widget.showClear && + !data.disabled && + !data.empty && + (widget.alwaysShowClear || (!data.required && !this.props.multiple) || multipleEntries) + ) { + insideButton = ( +
    (!this.props.multiple ? this.onClearClick(e) : this.onClearMultipleClick(e))} + className={CSS.element(baseClass, "clear")} + > + +
    + ); + } else { + insideButton = ( +
    { + this.toggleDropdown(e, true); + e.stopPropagation(); + e.preventDefault(); + }} + > + +
    + ); + } + } + + let text; + + if (this.props.multiple) { + let readOnly = data.disabled || data.readOnly; + if (isNonEmptyArray(data.records)) { + let valueTextFormatter = + widget.onGetRecordDisplayText ?? + ((record: Record) => record[widget.valueTextField] as string); + text = data.records.map((v, i) => ( +
    + {valueTextFormatter(v, instance)} + {!readOnly && ( +
    { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={(e) => this.onClearClick(e, v)} + > + +
    + )} +
    + )); + } else { + text = this.getPlaceholder(data.placeholder); + } + } else { + text = !data.empty ? data.text || this.getPlaceholder() : this.getPlaceholder(data.placeholder); + } + + let states = { + visited: state.visited, + focus: this.state.focus || this.state.dropdownOpen, + icon: !!iconVDOM, + empty: !data.placeholder && data.empty, + error: data.error && (state.visited || !suppressErrorsUntilVisited || !data.empty), + }; + + return ( +
    this.onKeyDown(e)} + > +
    { + this.dom.input = el; + }} + aria-labelledby={data.id + "-label"} + onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} + onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} + onClick={(e) => this.onClick(e)} + onKeyDown={(e) => this.onInputKeyDown(e)} + onMouseDown={(e) => this.onMouseDown(e)} + onBlur={(e) => this.onBlur(e)} + onFocus={(e) => this.onFocus(e)} + > + {text} +
    + {insideButton} + {icon} + {dropdown} + {label} + {help} +
    + ); + } + + onMouseDown(e: React.MouseEvent): void { + //skip touch start to allow touch scrolling + if (isTouchEvent()) return; + e.preventDefault(); + e.stopPropagation(); + this.toggleDropdown(e, true); + } + + onClick(e: React.MouseEvent): void { + //mouse down will handle it for non-touch events + if (!isTouchEvent()) return; + e.preventDefault(); + e.stopPropagation(); + this.toggleDropdown(e, true); + } + + onItemClick( + e: React.KeyboardEvent | React.MouseEvent, + { store }: { store: { getData: () => Record } }, + ): void { + this.select(e, [store.getData()]); + if (!this.props.instance.widget.submitOnEnterKey || e.type != "keydown") e.stopPropagation(); + if ((e as React.KeyboardEvent).keyCode != KeyCode.tab) e.preventDefault(); + } + + onClearClick(e: React.MouseEvent | React.KeyboardEvent, value?: Record): void { + let { instance } = this.props; + let { data, store, widget } = instance; + let { keyBindings } = widget; + e.stopPropagation(); + e.preventDefault(); + if (widget.multiple) { + if (isArray(data.records)) { + let itemKey = this.getLocalKey({ $value: value }); + let newRecords = data.records.filter((v) => !areKeysEqual(this.getLocalKey({ $value: v }), itemKey)); + + instance.set("records", newRecords); + + let newValues = newRecords + .map((rec) => this.getLocalKey({ $value: rec })) + .map((k) => (keyBindings!.length == 1 ? k[0] : k)); + + instance.set("values", newValues); + } + } else { + this.props.bindings.forEach((b) => { + store.set(b.local, widget.emptyValue); + }); + } + + if (!isTouchEvent()) this.dom.input!.focus(); + } + + onClearMultipleClick(e: React.MouseEvent): void { + let { instance } = this.props; + instance.set("records", []); + instance.set("values", []); + } + + select(e: React.KeyboardEvent | React.MouseEvent, itemsData: Record[], reset?: boolean): void { + let { instance } = this.props; + let { store, data, widget } = instance; + let { bindings, keyBindings } = widget; + + if (widget.multiple) { + let { selectedKeys, records } = data; + + let newRecords = reset ? [] : [...(records || [])]; + let singleSelect = itemsData.length == 1; + let optionKey: unknown[] | null = null; + if (singleSelect) optionKey = this.getOptionKey(itemsData[0]); + + // deselect + if (singleSelect && selectedKeys.find((k: any) => areKeysEqual(optionKey!, k))) { + newRecords = records.filter((v: any) => !areKeysEqual(optionKey!, this.getLocalKey({ $value: v }))); + } else { + itemsData.forEach((itemData) => { + let valueData: Record = { + $value: {}, + }; + bindings!.forEach((b) => { + valueData = Binding.get(b.local).set(valueData, Binding.get(b.remote).value(itemData)); + }); + newRecords.push(valueData.$value as Record); + }); + } + + instance.set("records", newRecords); + + let newValues = newRecords + .map((rec) => this.getLocalKey({ $value: rec })) + .map((k) => (keyBindings!.length == 1 ? k[0] : k)); + + instance.set("values", newValues); + } else { + bindings!.forEach((b) => { + let v = Binding.get(b.remote).value(itemsData[0]); + if (b.set) b.set(v, instance); + else store.set(b.local, v); + }); + } + + if (widget.closeOnSelect) { + //Pressing Tab should work it's own thing. Focus will move elsewhere and the dropdown will close. + if ((e as React.KeyboardEvent).keyCode != KeyCode.tab) { + if (!isTouchEvent()) this.dom.input!.focus(); + this.closeDropdown(e); + } + } + + if ((e as React.KeyboardEvent).keyCode == KeyCode.enter && widget.submitOnDropdownEnterKey) { + this.submitOnEnter(e as React.KeyboardEvent); + } + } + + onDropdownKeyPress(e: React.KeyboardEvent): void { + switch (e.keyCode) { + case KeyCode.esc: + this.closeDropdown(e); + this.dom.input!.focus(); + break; + + case KeyCode.tab: + // if tab needs to do a list selection, we have to first call List's handleKeyDown + if (this.listKeyDown) this.listKeyDown(e); + // if next focusable element is disabled, recalculate and update the dom before switching focus + this.props.forceUpdate(); + break; + + case KeyCode.a: + if (!e.ctrlKey) return; + + let { quickSelectAll, multiple } = this.props.instance.widget; + if (!quickSelectAll || !multiple) return; + + let optionsToSelect = this.state.options.map((o) => ({ + $option: o, + })); + this.select(e, optionsToSelect, true); + e.stopPropagation(); + e.preventDefault(); + break; + + default: + if (this.listKeyDown) this.listKeyDown(e); + break; + } + } + + onKeyDown(e: React.KeyboardEvent): void { + switch (e.keyCode) { + case KeyCode.pageDown: + case KeyCode.pageUp: + if (this.state.dropdownOpen) e.preventDefault(); + break; + } + } + + onInputKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + if (instance.widget.handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.delete: + this.onClearClick(e); + return; + + case KeyCode.shift: + case KeyCode.ctrl: + case KeyCode.tab: + case KeyCode.left: + case KeyCode.right: + case KeyCode.pageUp: + case KeyCode.pageDown: + case KeyCode.insert: + case KeyCode.esc: + break; + + case KeyCode.down: + this.openDropdown(e); + e.stopPropagation(); + break; + + case KeyCode.enter: + if (this.props.instance.widget.submitOnEnterKey) { + this.submitOnEnter(e); + } else { + this.openDropdown(e); + } + break; + + default: + this.openDropdown(e); + break; + } + } + + onQueryBlur(e: React.FocusEvent): void { + FocusManager.nudge(); + } + + onFocus(e: React.FocusEvent): void { + let { instance } = this.props; + let { widget } = instance; + if (widget.trackFocus) { + this.setState({ + focus: true, + }); + } + + if (this.props.instance.data.autoOpen) this.openDropdown(null); + } + + onBlur(e: React.FocusEvent): void { + if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); + + if (this.state.focus) + this.setState({ + focus: false, + }); + } + + toggleDropdown(e: React.KeyboardEvent | React.MouseEvent, keepFocus?: boolean): void { + if (this.state.dropdownOpen) this.closeDropdown(e, keepFocus); + else this.openDropdown(e); + } + + closeDropdown(e?: React.KeyboardEvent | React.MouseEvent | null, keepFocus?: boolean): void { + if (this.state.dropdownOpen) { + this.setState( + { + dropdownOpen: false, + }, + () => keepFocus && this.dom.input?.focus(), + ); + + this.props.instance.setState({ + visited: true, + }); + } + + //delete results valid only while the dropdown is open + delete this.tmpCachedResult; + } + + openDropdown(e: React.KeyboardEvent | React.MouseEvent | null): void { + let { instance } = this.props; + let { data } = instance; + if (!this.state.dropdownOpen && !data.disabled && !data.readOnly) { + this.query(""); + this.setState( + { + dropdownOpen: true, + }, + () => { + if (this.dom.dropdown) this.dom.dropdown.focus(); + }, + ); + } + } + + query(q: string): void { + /* + In fetchAll mode onQuery should fetch all data and after + that everything is done filtering is done client-side. + If cacheAll is set results are cached for the lifetime of the + widget, otherwise cache is invalidated when dropdown closes. + */ + + let { instance } = this.props; + let { widget, data } = instance; + + this.lastQuery = q; + + //do not make duplicate queries if fetchAll is enabled + if (widget.fetchAll && this.state.status == "loading") return; + + if (this.queryTimeoutId) clearTimeout(this.queryTimeoutId); + + if (q.length < widget.minQueryLength) { + this.setState({ + status: "info", + message: StringTemplate.format(widget.minQueryLengthMessageText, widget.minQueryLength), + }); + return; + } + + if (isArray(data.visibleOptions)) { + let results = widget.filterOptions(this.props.instance, data.visibleOptions as DataRecord[], q); + this.setState({ + options: results, + status: "loaded", + }); + } + + if (widget.onQuery) { + let { queryDelay, fetchAll, cacheAll, pageSize } = widget; + + if (fetchAll) queryDelay = 0; + + if (!this.cachedResult) { + this.setState({ + status: "loading", + }); + } + + this.queryTimeoutId = setTimeout(() => { + delete this.queryTimeoutId; + + let result = this.tmpCachedResult || this.cachedResult; + let query = fetchAll ? "" : q; + let params = !widget.infinite + ? query + : { + query, + page: 1, + pageSize, + }; + + if (!result) result = instance.invoke("onQuery", params, instance); + + let queryId = (this.lastQueryId = Date.now()); + + Promise.resolve(result) + .then((results) => { + //discard results which do not belong to the last query + if (queryId !== this.lastQueryId) return; + + if (!isArray(results)) results = []; + + if (fetchAll) { + if (cacheAll) this.cachedResult = results; + else this.tmpCachedResult = results; + + results = widget.filterOptions(this.props.instance, results, this.lastQuery); + } + + this.setState( + { + page: 1, + query, + options: results, + status: "loaded", + }, + () => { + if (widget.infinite) this.onListScroll(); + }, + ); + }) + .catch((err) => { + this.setState({ status: "error" }); + debug("Lookup query error:", err); + }); + }, queryDelay); + } + } + + loadAdditionalOptionPages(): void { + let { instance } = this.props; + let { widget } = instance; + if (!widget.infinite) return; + + let { query, page, status, options } = this.state; + if (!page) page = 1; + + let blockerKey = query; + + if (status != "loaded") return; + + if (options.length < page * widget.pageSize) return; //some pages were not full which means we reached the end + + if (this.extraPageLoadingBlocker === blockerKey) return; + + this.extraPageLoadingBlocker = blockerKey; + + let params = { + page: page + 1, + query, + pageSize: widget.pageSize, + }; + + var result = instance.invoke("onQuery", params, instance); + + Promise.resolve(result) + .then((results) => { + //discard results which do not belong to the last query + if (this.extraPageLoadingBlocker !== blockerKey) return; + + this.extraPageLoadingBlocker = false; + + if (!isArray(results)) return; + + this.setState( + { + page: params.page, + query, + options: [...options, ...results], + }, + () => { + this.onListScroll(); + }, + ); + }) + .catch((err) => { + if (this.extraPageLoadingBlocker !== blockerKey) return; + this.extraPageLoadingBlocker = false; + this.setState({ status: "error" }); + debug("Lookup query error:", err); + console.error(err); + }); + } + + UNSAFE_componentWillReceiveProps(props: LookupComponentProps): void { + if (this.dom.input) { + tooltipParentWillReceiveProps(this.dom.input, ...getFieldTooltip(props.instance)); + } + } + + componentDidMount(): void { + if (this.dom.input) { + tooltipParentDidMount(this.dom.input, ...getFieldTooltip(this.props.instance)); + autoFocus(this.dom.input, this); + } + } + + componentDidUpdate(): void { + if (this.dom.input) { + autoFocus(this.dom.input, this); + } + } + + componentWillUnmount(): void { + if (this.queryTimeoutId) clearTimeout(this.queryTimeoutId); + tooltipParentWillUnmount(this.props.instance); + this.subscribeListOnWheel(null); + } + + subscribeListOnWheel(list: HTMLDivElement | null): void { + if (this.unsubscribeListOnWheel) { + this.unsubscribeListOnWheel(); + this.unsubscribeListOnWheel = null; + } + if (list) { + this.unsubscribeListOnWheel = addEventListenerWithOptions( + list, + "wheel", + (e) => this.onListWheel(e as WheelEvent), + { + passive: false, + }, + ); + } + } + + subscribeListOnScroll(list: HTMLDivElement | null): void { + if (this.unsubscribeListOnScroll) { + this.unsubscribeListOnScroll(); + this.unsubscribeListOnScroll = null; + } + if (list) { + this.unsubscribeListOnScroll = addEventListenerWithOptions(list, "scroll", () => this.onListScroll(), { + passive: false, + }); + } + } + + submitOnEnter(e: React.KeyboardEvent): void { + let instance = this.props.instance.parent; + while (instance) { + let htmlInstance = instance as HtmlElementInstance; + if (htmlInstance.events && htmlInstance.events.onSubmit) { + htmlInstance.events.onSubmit(e, instance); + break; + } else { + instance = instance.parent; + } + } + } +} diff --git a/packages/cx/src/widgets/form/MonthField.d.ts b/packages/cx/src/widgets/form/MonthField.d.ts deleted file mode 100644 index 7c9f7822b..000000000 --- a/packages/cx/src/widgets/form/MonthField.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface MonthFieldProps extends FieldProps { - /** Selected month. This should be a Date object or a valid date string consumable by Date.parse function. */ - value?: Cx.Prop; - - /** Set to `true` to allow range select. */ - range?: Cx.BooleanProp; - - /** - * Start of the selected month range. This should be a Date object or a valid date string consumable by Date.parse function. - * Used only if `range` is set to `true`. - */ - from?: Cx.Prop; - - /** - * End of the selected month range. This should be a Date object or a valid date string consumable by Date.parse function. - * Used only if `range` is set to `true`. - */ - to?: Cx.Prop; - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** Minimum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - minValue?: Cx.Prop; - - /** Set to `true` to disallow the `minValue`. Default value is `false`. */ - minExclusive?: Cx.BooleanProp; - - /** Maximum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - maxValue?: Cx.Prop; - - /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ - maxExclusive?: Cx.BooleanProp; - - /** String representing culture. Default is `en` */ - culture?: string; - - /** - * Set to `true` to hide the clear button. It can be used interchangeably with the `showClear` property. - * Default value is `false`. - */ - hideClear?: boolean; - - /** Base CSS class to be applied on the field. Defaults to `monthfield`. */ - baseClass?: string; - - /** Maximum value error text. */ - maxValueErrorText?: string; - - /** Maximum exclusive value error text. */ - maxExclusiveErrorText?: string; - - /** Minimum value error text. */ - minValueErrorText?: string; - - /** Minimum exclusive value error text. */ - minExclusiveErrorText?: string; - - /** Invalid input error text. */ - inputErrorText?: string; - - /** Name or configuration of the icon to be put on the left side of the input. */ - icon?: Cx.StringProp | Cx.Record; - - /** - * Set to `false` to hide the clear button. It can be used interchangeably with the `hideClear` property. - * Default value is `true`. - */ - showClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** The function that will be used to convert Date objects before writing data to the store. - * Default implementation is Date.toISOString. - * See also Culture.setDefaultDateEncoding. - */ - encoding?: (date: Date) => any; - - /** Additional configuration to be passed to the dropdown, such as `style`, `positioning`, etc. */ - dropdownOptions?: Cx.Config; - - /** A boolean flag that determines whether the `to` date is included in the range. - * When set to true the value stored in the to field would be the last day of the month, i.e. `2024-12-31`. */ - inclusiveTo?: boolean; - - /** Optional configuration options for the MonthPicker component rendered within the dropdown. - * You can pass any valid additional MonthPicker props here, such as `startYear`, `endYear`, etc. - * Refer to the MonthPicker component documentation for a full list of supported options. */ - monthPickerOptions?: Cx.Config; -} - -export class MonthField extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/MonthField.js b/packages/cx/src/widgets/form/MonthField.js deleted file mode 100644 index 9f7514cea..000000000 --- a/packages/cx/src/widgets/form/MonthField.js +++ /dev/null @@ -1,524 +0,0 @@ -import { DateTimeCulture } from "intl-io"; -import { StringTemplate } from "../../data/StringTemplate"; -import { Culture } from "../../ui"; -import { Cx } from "../../ui/Cx"; -import { Localization } from "../../ui/Localization"; -import { VDOM, Widget, getContent } from "../../ui/Widget"; -import { Console } from "../../util/Console"; -import { Format } from "../../util/Format"; -import { KeyCode } from "../../util/KeyCode"; -import { dateDiff } from "../../util/date/dateDiff"; -import { parseDateInvariant } from "../../util"; -import { monthStart } from "../../util/date/monthStart"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { isDefined } from "../../util/isDefined"; -import { isTouchDevice } from "../../util/isTouchDevice"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { autoFocus } from "../autoFocus"; -import ClearIcon from "../icons/clear"; -import DropdownIcon from "../icons/drop-down"; -import { Dropdown } from "../overlay/Dropdown"; -import { - tooltipMouseLeave, - tooltipMouseMove, - tooltipParentDidMount, - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, -} from "../overlay/tooltip-ops"; -import { Field, getFieldTooltip } from "./Field"; -import { MonthPicker } from "./MonthPicker"; -import { getActiveElement } from "../../util/getActiveElement"; - -export class MonthField extends Field { - declareData() { - if (this.mode == "range") { - this.range = true; - this.mode = "edit"; - Console.warn('Please use the range flag on MonthFields. Syntax mode="range" is deprecated.', this); - } - - let values = {}; - - if (this.range) { - values = { - from: null, - to: null, - }; - } else { - values = { - value: this.emptyValue, - }; - } - - super.declareData( - values, - { - disabled: undefined, - readOnly: undefined, - enabled: undefined, - placeholder: undefined, - required: undefined, - minValue: undefined, - minExclusive: undefined, - maxValue: undefined, - maxExclusive: undefined, - icon: undefined, - }, - ...arguments, - ); - } - - isEmpty(data) { - return this.range ? data.from == null : data.value == null; - } - - init() { - if (!this.culture) this.culture = new DateTimeCulture(Format.culture); - - if (isDefined(this.hideClear)) this.showClear = !this.hideClear; - - if (this.alwaysShowClear) this.showClear = true; - - super.init(); - } - - prepareData(context, instance) { - super.prepareData(context, instance); - - let { data } = instance; - - let formatOptions = { - year: "numeric", - month: "short", - }; - - if (!this.range && data.value) { - data.date = parseDateInvariant(data.value); - data.formatted = this.culture.format(data.date, formatOptions); - } else if (this.range && data.from && data.to) { - data.from = parseDateInvariant(data.from); - data.to = parseDateInvariant(data.to); - if (!this.inclusiveTo) data.to.setDate(data.to.getDate() - 1); - let fromStr = this.culture.format(data.from, formatOptions); - let toStr = this.culture.format(data.to, formatOptions); - if (fromStr != toStr) data.formatted = fromStr + " - " + toStr; - else data.formatted = fromStr; - } - - if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); - - if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); - - if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); - - instance.lastDropdown = context.lastDropdown; - } - - validateRequired(context, instance) { - var { data } = instance; - if (this.range) { - if (!data.from || !data.to) return this.requiredText; - } else return super.validateRequired(context, instance); - } - - validate(context, instance) { - super.validate(context, instance); - var { data } = instance; - if (!data.error && data.date) { - var d; - if (data.maxValue) { - d = dateDiff(data.date, data.maxValue); - if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); - else if (d == 0 && data.maxExclusive) - data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); - } - - if (data.minValue) { - d = dateDiff(data.date, data.minValue); - if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); - else if (d == 0 && data.minExclusive) - data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); - } - } - } - - renderInput(context, instance, key) { - return ( - - ); - } - - formatValue(context, { data }) { - return data.formatted || ""; - } - - parseDate(date) { - if (!date) return null; - if (date instanceof Date) return date; - date = this.culture.parse(date, { useCurrentDateForDefaults: true }); - return date; - } - - handleSelect(instance, date1, date2) { - let { widget } = instance; - let encode = widget.encoding || Culture.getDefaultDateEncoding(); - instance.setState({ - inputError: false, - }); - if (this.range) { - let d1 = date1 ? encode(date1) : this.emptyValue; - let toDate = date2; - if (date2 && this.inclusiveTo) { - toDate = new Date(date2); - toDate.setDate(toDate.getDate() - 1); - } - let d2 = toDate ? encode(toDate) : this.emptyValue; - instance.set("from", d1); - instance.set("to", d2); - } else { - let value = date1 ? encode(date1) : this.emptyValue; - instance.set("value", value); - } - } -} - -MonthField.prototype.baseClass = "monthfield"; -MonthField.prototype.maxValueErrorText = "Select {0:d} or before."; -MonthField.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; -MonthField.prototype.minValueErrorText = "Select {0:d} or later."; -MonthField.prototype.minExclusiveErrorText = "Select a date after {0:d}."; -MonthField.prototype.inputErrorText = "Invalid date entered"; -MonthField.prototype.suppressErrorsUntilVisited = true; -MonthField.prototype.icon = "calendar"; -MonthField.prototype.showClear = true; -MonthField.prototype.alwaysShowClear = false; -MonthField.prototype.range = false; -MonthField.prototype.reactOn = "enter blur"; -MonthField.prototype.inclusiveTo = false; - -Localization.registerPrototype("cx/widgets/MonthField", MonthField); - -Widget.alias("monthfield", MonthField); - -class MonthInput extends VDOM.Component { - constructor(props) { - super(props); - this.props.instance.component = this; - this.state = { - dropdownOpen: false, - focus: false, - }; - } - - getDropdown() { - if (this.dropdown) return this.dropdown; - - let { widget, lastDropdown } = this.props.instance; - - var dropdown = { - scrollTracking: true, - inline: !isTouchDevice() || !!lastDropdown, - placementOrder: - "down down-left down-right up up-left up-right right right-up right-down left left-up left-down", - touchFriendly: true, - ...widget.dropdownOptions, - type: Dropdown, - relatedElement: this.input, - items: { - type: MonthPicker, - ...this.props.monthPicker, - encoding: widget.encoding, - inclusiveTo: widget.inclusiveTo, - autoFocus: true, - onFocusOut: (e) => { - this.closeDropdown(e); - }, - onKeyDown: (e) => this.onKeyDown(e), - onSelect: (e) => { - let touch = isTouchEvent(e); - this.closeDropdown(e, () => { - if (!touch) this.input.focus(); - }); - }, - }, - constrain: true, - firstChildDefinesWidth: true, - }; - - return (this.dropdown = Widget.create(dropdown)); - } - - render() { - var { instance, label, help, data, icon: iconVDOM } = this.props; - var { widget, state } = instance; - var { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let insideButton, icon; - - if (!data.readOnly && !data.disabled) { - if ( - widget.showClear && - (((widget.alwaysShowClear || !data.required) && !data.empty) || instance.state.inputError) - ) - insideButton = ( -
    { - e.preventDefault(); - e.stopPropagation(); - }} - onClick={(e) => { - this.onClearClick(e); - }} - > - -
    - ); - else - insideButton = ( -
    - -
    - ); - } - - if (iconVDOM) { - icon =
    {iconVDOM}
    ; - } - - var dropdown = false; - if (this.state.dropdownOpen) - dropdown = ( - - ); - - let empty = this.input ? !this.input.value : data.empty; - - return ( -
    - { - this.input = el; - }} - type="text" - className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} - style={data.inputStyle} - defaultValue={data.formatted} - disabled={data.disabled} - readOnly={data.readOnly} - tabIndex={data.tabIndex} - placeholder={data.placeholder} - onInput={(e) => this.onChange(e.target.value, "input")} - onChange={(e) => this.onChange(e.target.value, "change")} - onKeyDown={(e) => this.onKeyDown(e)} - onBlur={(e) => { - this.onBlur(e); - }} - onFocus={(e) => { - this.onFocus(e); - }} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} - /> - {icon} - {insideButton} - {dropdown} - {label} - {help} -
    - ); - } - - onMouseDown(e) { - e.stopPropagation(); - - if (this.state.dropdownOpen) this.closeDropdown(e); - else { - this.openDropdownOnFocus = true; - } - - //icon click - if (e.target != this.input) { - e.preventDefault(); - if (!this.state.dropdownOpen) this.openDropdown(e); - else this.input.focus(); - } - } - - onFocus(e) { - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - if (this.openDropdownOnFocus) this.openDropdown(e); - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.enter: - e.stopPropagation(); - this.onChange(e.target.value, "enter"); - break; - - case KeyCode.esc: - if (this.state.dropdownOpen) { - e.stopPropagation(); - this.closeDropdown(e, () => { - this.input.focus(); - }); - } - break; - - case KeyCode.left: - case KeyCode.right: - e.stopPropagation(); - break; - - case KeyCode.down: - this.openDropdown(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - } - - onBlur(e) { - if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); - - if (this.state.focus) - this.setState({ - focus: false, - }); - this.onChange(e.target.value, "blur"); - } - - closeDropdown(e, callback) { - if (this.state.dropdownOpen) { - if (this.scrollableParents) - this.scrollableParents.forEach((el) => { - el.removeEventListener("scroll", this.updateDropdownPosition); - }); - - this.props.instance.setState({ visited: true }); - this.setState({ dropdownOpen: false }, callback); - } else if (callback) callback(); - } - - openDropdown(e) { - var { data } = this.props.instance; - this.openDropdownOnFocus = false; - - if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { - this.setState({ dropdownOpen: true }); - } - } - - onClearClick(e) { - e.stopPropagation(); - e.preventDefault(); - - var { instance } = this.props; - var { widget } = instance; - - widget.handleSelect(instance, null, null); - } - - UNSAFE_componentWillReceiveProps(props) { - var { data, state } = props.instance; - if (data.formatted != this.input.value && (data.formatted != this.props.data.formatted || !state.inputError)) { - this.input.value = data.formatted || ""; - props.instance.setState({ - inputError: false, - }); - } - tooltipParentWillReceiveProps(this.input, ...getFieldTooltip(this.props.instance)); - } - - componentDidMount() { - tooltipParentDidMount(this.input, ...getFieldTooltip(this.props.instance)); - autoFocus(this.input, this); - } - - componentDidUpdate() { - autoFocus(this.input, this); - } - - componentWillUnmount() { - if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { - this.onChange(this.input.value, "blur"); - } - tooltipParentWillUnmount(this.props.instance); - } - - onChange(inputValue, eventType) { - var { instance } = this.props; - var { widget } = instance; - - if (widget.reactOn.indexOf(eventType) == -1) return; - - var parts = inputValue.split("-"); - var date1 = widget.parseDate(parts[0]); - var date2 = widget.parseDate(parts[1]) || date1; - - if ((date1 != null && isNaN(date1)) || (date2 != null && isNaN(date2))) { - instance.setState({ - inputError: widget.inputErrorText, - }); - } else if (eventType == "blur" || eventType == "enter") { - if (date2) date2 = new Date(date2.getFullYear(), date2.getMonth() + 1, 1); - instance.setState({ - visited: true, - }); - widget.handleSelect(instance, date1, date2); - } - } -} diff --git a/packages/cx/src/widgets/form/MonthField.scss b/packages/cx/src/widgets/form/MonthField.scss index 1f24b4b51..ea7cb99d5 100644 --- a/packages/cx/src/widgets/form/MonthField.scss +++ b/packages/cx/src/widgets/form/MonthField.scss @@ -1,3 +1,4 @@ +@use "sass:map"; @mixin cx-monthfield( $name: 'monthfield', @@ -11,9 +12,9 @@ $width: $cx-default-input-width, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { @include cxb-field( diff --git a/packages/cx/src/widgets/form/MonthField.tsx b/packages/cx/src/widgets/form/MonthField.tsx new file mode 100644 index 000000000..d21a1617a --- /dev/null +++ b/packages/cx/src/widgets/form/MonthField.tsx @@ -0,0 +1,670 @@ +/**@jsxImportSource react */ + +// @ts-expect-error +import { DateTimeCulture } from "intl-io"; +import { StringTemplate } from "../../data/StringTemplate"; +import { Culture } from "../../ui"; +import { Cx } from "../../ui/Cx"; +import { Localization } from "../../ui/Localization"; +import { VDOM, Widget, getContent } from "../../ui/Widget"; +import { Console } from "../../util/Console"; +import { KeyCode } from "../../util/KeyCode"; +import { dateDiff } from "../../util/date/dateDiff"; +import { parseDateInvariant } from "../../util"; +import { monthStart } from "../../util/date/monthStart"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { isDefined } from "../../util/isDefined"; +import { isTouchDevice } from "../../util/isTouchDevice"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { autoFocus } from "../autoFocus"; +import ClearIcon from "../icons/clear"; +import DropdownIcon from "../icons/drop-down"; +import { Dropdown, DropdownConfig } from "../overlay/Dropdown"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, +} from "../overlay/tooltip-ops"; +import { Field, getFieldTooltip, FieldInstance, FieldConfig } from "./Field"; +import { MonthPicker } from "./MonthPicker"; +import { getActiveElement } from "../../util/getActiveElement"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance, DropdownWidgetProps } from "../../ui/Instance"; +import type { Config, Prop, BooleanProp, StringProp } from "../../ui/Prop"; + +export class MonthFieldInstance + extends FieldInstance + implements DropdownWidgetProps +{ + lastDropdown?: Instance; + dropdownOpen?: boolean; + selectedIndex?: number; + component?: any; +} + +export interface MonthFieldConfig extends FieldConfig { + /** Selected month. This should be a Date object or a valid date string consumable by Date.parse function. */ + value?: Prop; + + /** Set to `true` to allow range select. */ + range?: BooleanProp; + + /** Start of the selected month range. Used only if `range` is set to `true`. */ + from?: Prop; + + /** End of the selected month range. Used only if `range` is set to `true`. */ + to?: Prop; + + /** Defaults to `false`. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** The opposite of `disabled`. */ + enabled?: BooleanProp; + + /** Default text displayed when the field is empty. */ + placeholder?: StringProp; + + /** Minimum date value. */ + minValue?: Prop; + + /** Set to `true` to disallow the `minValue`. Default value is `false`. */ + minExclusive?: BooleanProp; + + /** Maximum date value. */ + maxValue?: Prop; + + /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ + maxExclusive?: BooleanProp; + + /** String representing culture. Default is `en`. */ + culture?: string; + + /** Set to `true` to hide the clear button. Default value is `false`. */ + hideClear?: boolean; + + /** Base CSS class to be applied on the field. Defaults to `monthfield`. */ + baseClass?: string; + + /** Maximum value error text. */ + maxValueErrorText?: string; + + /** Maximum exclusive value error text. */ + maxExclusiveErrorText?: string; + + /** Minimum value error text. */ + minValueErrorText?: string; + + /** Minimum exclusive value error text. */ + minExclusiveErrorText?: string; + + /** Invalid input error text. */ + inputErrorText?: string; + + /** Name or configuration of the icon to be put on the left side of the input. */ + icon?: StringProp | Config; + + /** Set to `false` to hide the clear button. Default value is `true`. */ + showClear?: boolean; + + /** Set to `true` to display the clear button even if `required` is set. Default is `false`. */ + alwaysShowClear?: boolean; + + /** The function that will be used to convert Date objects before writing data to the store. */ + encoding?: (date: Date) => any; + + /** Additional configuration to be passed to the dropdown. */ + dropdownOptions?: Partial; + + /** A boolean flag that determines whether the `to` date is included in the range. */ + inclusiveTo?: boolean; + + /** Optional configuration options for the MonthPicker component rendered within the dropdown. */ + monthPickerOptions?: Config; + + /** Custom validation function. */ + onValidate?: + | string + | ((value: string | Date, instance: Instance, validationParams: Record) => unknown); +} + +export class MonthField extends Field { + declare public baseClass: string; + declare public mode?: string; + declare public range?: BooleanProp; + declare public from?: Prop; + declare public to?: Prop; + declare public value?: Prop; + declare public culture: DateTimeCulture; + declare public hideClear?: boolean; + declare public showClear?: boolean; + declare public alwaysShowClear?: boolean; + declare public encoding?: (date: Date) => string; + declare public dropdownOptions?: Partial; + declare public inclusiveTo?: boolean; + declare public monthPickerOptions?: Record; + declare public maxValueErrorText: string; + declare public maxExclusiveErrorText: string; + declare public minValueErrorText: string; + declare public minExclusiveErrorText: string; + declare public inputErrorText?: string; + declare public minExclusive?: BooleanProp; + declare public maxExclusive?: BooleanProp; + declare public minValue?: Prop; + declare public maxValue?: Prop; + declare public placeholder?: StringProp; + declare public reactOn: string; + + declareData(...args: Record[]): void { + if (this.mode == "range") { + this.range = true; + this.mode = "edit"; + Console.warn('Please use the range flag on MonthFields. Syntax mode="range" is deprecated.', this); + } + + let values: Record = {}; + + if (this.range) { + values = { + from: null, + to: null, + }; + } else { + values = { + value: this.emptyValue, + }; + } + + super.declareData( + values, + { + disabled: undefined, + readOnly: undefined, + enabled: undefined, + placeholder: undefined, + required: undefined, + minValue: undefined, + minExclusive: undefined, + maxValue: undefined, + maxExclusive: undefined, + icon: undefined, + }, + ...args, + ); + } + + isEmpty(data: Record): boolean { + return this.range ? data.from == null : data.value == null; + } + + init(): void { + if (!this.culture) this.culture = Culture.getDateTimeCulture(); + + if (isDefined(this.hideClear)) this.showClear = !this.hideClear; + + if (this.alwaysShowClear) this.showClear = true; + + super.init(); + } + + prepareData(context: RenderingContext, instance: MonthFieldInstance): void { + super.prepareData(context, instance); + + let { data } = instance; + + let formatOptions = { + year: "numeric", + month: "short", + }; + + if (!this.range && data.value) { + data.date = parseDateInvariant(data.value); + data.formatted = this.culture.format(data.date, formatOptions); + } else if (this.range && data.from && data.to) { + data.from = parseDateInvariant(data.from); + data.to = parseDateInvariant(data.to); + if (!this.inclusiveTo) data.to.setDate(data.to.getDate() - 1); + let fromStr = this.culture.format(data.from, formatOptions); + let toStr = this.culture.format(data.to, formatOptions); + if (fromStr != toStr) data.formatted = fromStr + " - " + toStr; + else data.formatted = fromStr; + } + + if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); + + if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); + + if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); + + instance.lastDropdown = context.lastDropdown; + } + + validateRequired(context: RenderingContext, instance: MonthFieldInstance): string | undefined { + const { data } = instance; + if (this.range) { + if (!data.from || !data.to) return this.requiredText; + } else return super.validateRequired(context, instance); + } + + validate(context: RenderingContext, instance: MonthFieldInstance): void { + super.validate(context, instance); + var { data } = instance; + if (!data.error && data.date) { + var d; + if (data.maxValue) { + d = dateDiff(data.date, data.maxValue); + if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); + else if (d == 0 && data.maxExclusive) + data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); + } + + if (data.minValue) { + d = dateDiff(data.date, data.minValue); + if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); + else if (d == 0 && data.minExclusive) + data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); + } + } + } + + renderInput(context: RenderingContext, instance: MonthFieldInstance, key: string): React.ReactNode { + return ( + + ); + } + + formatValue(context: RenderingContext, instance: Instance): string { + return instance.data.formatted || ""; + } + + parseDate(date: string | Date | null): Date | null { + if (!date) return null; + if (date instanceof Date) return date; + let parsed = this.culture.parse(date, { useCurrentDateForDefaults: true }); + return parsed; + } + + handleSelect(instance: MonthFieldInstance, date1: Date | null, date2: Date | null): void { + let { widget } = instance; + let encode = widget.encoding ?? Culture.getDefaultDateEncoding(); + instance.setState({ + inputError: false, + }); + if (this.range) { + let d1 = date1 ? encode(date1) : this.emptyValue; + let toDate = date2; + if (date2 && this.inclusiveTo) { + toDate = new Date(date2); + toDate.setDate(toDate.getDate() - 1); + } + let d2 = toDate ? encode(toDate) : this.emptyValue; + instance.set("from", d1); + instance.set("to", d2); + } else { + let value = date1 ? encode(date1) : this.emptyValue; + instance.set("value", value); + } + } +} + +MonthField.prototype.baseClass = "monthfield"; +MonthField.prototype.maxValueErrorText = "Select {0:d} or before."; +MonthField.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; +MonthField.prototype.minValueErrorText = "Select {0:d} or later."; +MonthField.prototype.minExclusiveErrorText = "Select a date after {0:d}."; +MonthField.prototype.inputErrorText = "Invalid date entered"; +MonthField.prototype.suppressErrorsUntilVisited = true; +MonthField.prototype.icon = "calendar"; +MonthField.prototype.showClear = true; +MonthField.prototype.alwaysShowClear = false; +MonthField.prototype.range = false; +MonthField.prototype.reactOn = "enter blur"; +MonthField.prototype.inclusiveTo = false; + +Localization.registerPrototype("cx/widgets/MonthField", MonthField); + +Widget.alias("monthfield", MonthField); + +interface MonthInputProps { + instance: MonthFieldInstance; + data: Record; + monthPicker: Record; + label?: React.ReactNode; + help?: React.ReactNode; + icon?: React.ReactNode; +} + +interface MonthInputState { + dropdownOpen: boolean; + focus: boolean; +} + +class MonthInput extends VDOM.Component { + input?: HTMLInputElement | null; + dropdown?: Widget; + openDropdownOnFocus: boolean = false; + scrollableParents?: Element[]; + updateDropdownPosition: () => void = () => {}; + + constructor(props: MonthInputProps) { + super(props); + this.props.instance.component = this; + this.state = { + dropdownOpen: false, + focus: false, + }; + } + + getDropdown(): Widget { + if (this.dropdown) return this.dropdown; + + let { widget, lastDropdown } = this.props.instance; + + var dropdown = { + scrollTracking: true, + inline: !isTouchDevice() || !!lastDropdown, + placementOrder: + "down down-left down-right up up-left up-right right right-up right-down left left-up left-down", + touchFriendly: true, + ...widget.dropdownOptions, + type: Dropdown, + relatedElement: this.input, + items: { + type: MonthPicker, + ...this.props.monthPicker, + encoding: widget.encoding, + inclusiveTo: widget.inclusiveTo, + autoFocus: true, + onFocusOut: (e: React.MouseEvent) => { + this.closeDropdown(e); + }, + onKeyDown: (e: React.KeyboardEvent) => this.onKeyDown(e), + onSelect: (e: React.MouseEvent) => { + let touch = isTouchEvent(); + this.closeDropdown(e, () => { + if (!touch) this.input!.focus(); + }); + }, + }, + constrain: true, + firstChildDefinesWidth: true, + }; + + return (this.dropdown = Widget.create(dropdown)); + } + + render(): React.ReactNode { + const { instance, label, help, data, icon: iconVDOM } = this.props; + const { widget, state } = instance; + var { CSS, baseClass, suppressErrorsUntilVisited } = widget; + + let insideButton, icon; + + if (!data.readOnly && !data.disabled) { + if ( + widget.showClear && + (((widget.alwaysShowClear || !data.required) && !data.empty) || instance.state.inputError) + ) + insideButton = ( +
    { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={(e) => { + this.onClearClick(e); + }} + > + +
    + ); + else + insideButton = ( +
    + +
    + ); + } + + if (iconVDOM) { + icon =
    {iconVDOM}
    ; + } + + var dropdown: React.ReactElement | false = false; + if (this.state.dropdownOpen) + dropdown = ( + + ); + + let empty = this.input ? !this.input.value : data.empty; + + return ( +
    + { + this.input = el; + }} + type="text" + className={CSS.expand(CSS.element(baseClass, "input"), data.inputClass)} + style={data.inputStyle} + defaultValue={data.formatted} + disabled={data.disabled} + readOnly={data.readOnly} + tabIndex={data.tabIndex} + placeholder={data.placeholder} + onInput={(e) => this.onChange((e.target as HTMLInputElement).value, "input")} + onChange={(e) => this.onChange((e.target as HTMLInputElement).value, "change")} + onKeyDown={(e) => this.onKeyDown(e)} + onBlur={(e) => { + this.onBlur(e); + }} + onFocus={(e) => { + this.onFocus(e); + }} + onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} + onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} + /> + {icon} + {insideButton} + {dropdown} + {label} + {help} +
    + ); + } + + onMouseDown(e: React.MouseEvent): void { + e.stopPropagation(); + + if (this.state.dropdownOpen) this.closeDropdown(e); + else { + this.openDropdownOnFocus = true; + } + + //icon click + if (e.target != this.input) { + e.preventDefault(); + if (!this.state.dropdownOpen) this.openDropdown(e); + else this.input!.focus(); + } + } + + onFocus(e: React.FocusEvent): void { + let { instance } = this.props; + let { widget } = instance; + if (widget.trackFocus) { + this.setState({ + focus: true, + }); + } + if (this.openDropdownOnFocus) this.openDropdown(e); + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + if (instance.widget.handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.enter: + e.stopPropagation(); + this.onChange((e.target as HTMLInputElement).value, "enter"); + break; + + case KeyCode.esc: + if (this.state.dropdownOpen) { + e.stopPropagation(); + this.closeDropdown(e, () => { + this.input!.focus(); + }); + } + break; + + case KeyCode.left: + case KeyCode.right: + e.stopPropagation(); + break; + + case KeyCode.down: + this.openDropdown(e); + e.stopPropagation(); + e.preventDefault(); + break; + } + } + + onBlur(e: React.FocusEvent): void { + if (!this.state.dropdownOpen) this.props.instance.setState({ visited: true }); + + if (this.state.focus) + this.setState({ + focus: false, + }); + this.onChange((e.target as HTMLInputElement).value, "blur"); + } + + closeDropdown(e?: React.KeyboardEvent | React.MouseEvent, callback?: () => void): void { + if (this.state.dropdownOpen) { + if (this.scrollableParents) + this.scrollableParents.forEach((el) => { + el.removeEventListener("scroll", this.updateDropdownPosition); + }); + + this.props.instance.setState({ visited: true }); + this.setState({ dropdownOpen: false }, callback); + } else if (callback) callback(); + } + + openDropdown(e?: React.KeyboardEvent | React.MouseEvent | React.FocusEvent): void { + const { data } = this.props.instance; + this.openDropdownOnFocus = false; + + if (!this.state.dropdownOpen && !(data.disabled || data.readOnly)) { + this.setState({ dropdownOpen: true }); + } + } + + onClearClick(e: React.MouseEvent): void { + e.stopPropagation(); + e.preventDefault(); + + const { instance } = this.props; + const { widget } = instance; + + widget.handleSelect(instance, null, null); + } + + UNSAFE_componentWillReceiveProps(props: MonthInputProps): void { + const { data, state } = props.instance; + if (data.formatted != this.input!.value && (data.formatted != this.props.data.formatted || !state.inputError)) { + this.input!.value = data.formatted || ""; + props.instance.setState({ + inputError: false, + }); + } + tooltipParentWillReceiveProps(this.input!, ...getFieldTooltip(this.props.instance)); + } + + componentDidMount(): void { + tooltipParentDidMount(this.input!, ...getFieldTooltip(this.props.instance)); + autoFocus(this.input, this); + } + + componentDidUpdate(): void { + autoFocus(this.input, this); + } + + componentWillUnmount(): void { + if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { + this.onChange(this.input.value, "blur"); + } + tooltipParentWillUnmount(this.props.instance); + } + + onChange(inputValue: string, eventType: string): void { + const { instance } = this.props; + const { widget } = instance; + + if (widget.reactOn.indexOf(eventType) == -1) return; + + var parts = inputValue.split("-"); + var date1 = widget.parseDate(parts[0]); + var date2 = widget.parseDate(parts[1]) || date1; + + if ((date1 != null && isNaN(date1.getTime())) || (date2 != null && isNaN(date2.getTime()))) { + instance.setState({ + inputError: widget.inputErrorText, + }); + } else if (eventType == "blur" || eventType == "enter") { + if (date2) date2 = new Date(date2.getFullYear(), date2.getMonth() + 1, 1); + instance.setState({ + visited: true, + }); + widget.handleSelect(instance, date1, date2); + } + } +} diff --git a/packages/cx/src/widgets/form/MonthPicker.d.ts b/packages/cx/src/widgets/form/MonthPicker.d.ts deleted file mode 100644 index 7ed499163..000000000 --- a/packages/cx/src/widgets/form/MonthPicker.d.ts +++ /dev/null @@ -1,97 +0,0 @@ -import * as Cx from "../../core"; -import { Instance } from "../../ui"; -import { FieldProps } from "./Field"; - -interface MonthPickerProps extends FieldProps { - /** Set to `true` to allow range select. */ - range?: Cx.BooleanProp; - - /** - * Start of the selected month range. This should be a Date object or a valid date string consumable by Date.parse function. - * Used only if `range` is set to `true`. - */ - from?: Cx.Prop; - - /** - * End of the selected month range. This should be a Date object or a valid date string consumable by Date.parse function. - * Used only if `range` is set to `true`. - */ - to?: Cx.Prop; - - /** - * Selected month date. This should be a Date object or a valid date string consumable by Date.parse function. - * Used only if `range` is set to `false` (default). - */ - value?: Cx.Prop; - - /** View reference date. If no date is selected, this date is used to determine which month to show in the calendar. */ - refDate?: Cx.Prop; - - /** Minimum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - minValue?: Cx.Prop; - - /** Set to `true` to disallow the `minValue`. Default value is `false`. */ - minExclusive?: Cx.BooleanProp; - - /** Maximum date value. This should be a Date object or a valid date string consumable by Date.parse function. */ - maxValue?: Cx.Prop; - - /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ - maxExclusive?: Cx.BooleanProp; - - /** Base CSS class to be applied on the field. Defaults to `monthfield`. */ - baseClass?: string; - - /** Minimum year available in the range. */ - startYear?: number; - - /** Max year available in the range. */ - endYear?: number; - - /** Number of years to be rendered in each render chunk. */ - bufferSize?: number; - - /** Maximum value error text. */ - maxValueErrorText?: string; - - /** Maximum exclusive value error text. */ - maxExclusiveErrorText?: string; - - /** Minimum value error text. */ - minValueErrorText?: string; - - /** Minimum exclusive value error text. */ - minExclusiveErrorText?: string; - - /** The function that will be used to convert Date objects before writing data to the store. - * Default implementation is Date.toISOString. - * See also Culture.setDefaultDateEncoding. - */ - encoding?: (date: Date) => any; - - /** A boolean flag that determines whether the `to` date is included in the range. - * When set to true the value stored in the to field would be the last day of the month, i.e. `2024-12-31`. */ - inclusiveTo?: boolean; - - /** Callback function that is called before writing data to the store. Return false to short-circuit updating the state. */ - onBeforeSelect: (e: Event, instance: Instance, dateFrom?: Date, dateTo?: Date) => boolean; - - /** Callback function that is called after value or date range has changed */ - onSelect: (instance: Instance, dateFrom?: Date, dateTo?: Date) => void; - - /** - * Optional parameter to hide the quarters period section on the picker. - * When true, the quarters section will not render. - */ - hideQuarters?: boolean; - - /** - * Callback to create a function that determines if a date is selectable. - * Return `false` on factory method to disable specific month, quarter or a whole year. - * - * Note: Use the `onValidate` callback for validation purposes. - */ - onCreateIsMonthDateSelectable?: (validationParams: Cx.Config, instance: Instance) => (monthDate: Date) => boolean; -} - -export class MonthPicker extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/MonthPicker.js b/packages/cx/src/widgets/form/MonthPicker.js deleted file mode 100644 index 169784507..000000000 --- a/packages/cx/src/widgets/form/MonthPicker.js +++ /dev/null @@ -1,687 +0,0 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { Culture } from "../../ui/Culture"; -import { FocusManager, oneFocusOut, offFocusOut, preventFocusOnTouch } from "../../ui/FocusManager"; -import { StringTemplate } from "../../data/StringTemplate"; -import { monthStart } from "../../util/date/monthStart"; -import { dateDiff } from "../../util/date/dateDiff"; -import { minDate } from "../../util/date/minDate"; -import { maxDate } from "../../util/date/maxDate"; -import { lowerBoundCheck } from "../../util/date/lowerBoundCheck"; -import { upperBoundCheck } from "../../util/date/upperBoundCheck"; -import { Console } from "../../util/Console"; -import { KeyCode } from "../../util/KeyCode"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { Localization } from "../../ui/Localization"; -import { scrollElementIntoView } from "../../util/scrollElementIntoView"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { isString } from "../../util/isString"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { getCursorPos } from "../overlay/captureMouse"; - -import { enableCultureSensitiveFormatting } from "../../ui/Format"; -import { parseDateInvariant } from "../../util"; -enableCultureSensitiveFormatting(); - -export class MonthPicker extends Field { - declareData() { - let values = {}; - - if (this.mode == "range") { - this.range = true; - this.mode = "edit"; - Console.warn('Please use the range flag on MonthPickers. Syntax mode="range" is deprecated.', this); - } - - if (this.range) { - values = { - from: null, - to: null, - }; - } else { - values = { - value: this.emptyValue, - }; - } - - super.declareData( - values, - { - refDate: undefined, - disabled: undefined, - minValue: undefined, - minExclusive: undefined, - maxValue: undefined, - maxExclusive: undefined, - }, - ...arguments, - ); - } - - init() { - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - data.stateMods = { - disabled: data.disabled, - }; - - if (!this.range && data.value) data.date = monthStart(parseDateInvariant(data.value)); - - if (this.range) { - if (data.from) data.from = monthStart(parseDateInvariant(data.from)); - - if (data.to) { - let date = parseDateInvariant(data.to); - if (this.inclusiveTo) date.setDate(date.getDate() + 1); - data.to = monthStart(date); - } - } - - if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); - - if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); - - if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); - - if (this.onCreateIsMonthDateSelectable) { - instance.isMonthDateSelectable = instance.invoke( - "onCreateIsMonthDateSelectable", - data.validationParams, - instance, - ); - } - - super.prepareData(...arguments); - } - - validate(context, instance) { - super.validate(context, instance); - let { data } = instance; - if (!data.error && data.date) { - let d; - if (data.maxValue) { - d = dateDiff(data.date, data.maxValue); - if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); - else if (d == 0 && data.maxExclusive) - data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); - } - - if (data.minValue) { - d = dateDiff(data.date, data.minValue); - if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); - else if (d == 0 && data.minExclusive) - data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); - } - } - } - - renderInput(context, instance, key) { - return ( - - ); - } - - handleSelect(e, instance, date1, date2) { - let { data, widget, isMonthDateSelectable } = instance; - let encode = widget.encoding || Culture.getDefaultDateEncoding(); - - if (data.disabled) return; - - if (isMonthDateSelectable && !isMonthDateSelectable(date1)) return; - if (!dateSelectableCheck(date1, data)) return; - - if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date1, date2) === false) return; - - if (this.range) { - instance.set("from", encode(date1)); - let toDate = new Date(date2); - if (this.inclusiveTo) toDate.setDate(toDate.getDate() - 1); - instance.set("to", encode(toDate)); - } else instance.set("value", encode(date1)); - - if (this.onSelect) instance.invoke("onSelect", instance, date1, date2); - } -} - -MonthPicker.prototype.baseClass = "monthpicker"; -MonthPicker.prototype.range = false; -MonthPicker.prototype.startYear = 1980; -MonthPicker.prototype.endYear = 2030; -MonthPicker.prototype.bufferSize = 15; -MonthPicker.prototype.hideQuarters = false; - -// Localization -MonthPicker.prototype.maxValueErrorText = "Select {0:d} or before."; -MonthPicker.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; -MonthPicker.prototype.minValueErrorText = "Select {0:d} or later."; -MonthPicker.prototype.minExclusiveErrorText = "Select a date after {0:d}."; -MonthPicker.prototype.inclusiveTo = false; - -Localization.registerPrototype("cx/widgets/MonthPicker", MonthPicker); - -Widget.alias("month-picker", MonthPicker); - -const dateSelectableCheck = (date, data) => { - if (data.maxValue && !upperBoundCheck(date, data.maxValue, data.maxExclusive)) return false; - - if (data.minValue && !lowerBoundCheck(date, data.minValue, data.minExclusive)) return false; - - return true; -}; - -const monthNumber = (date) => { - return date.getFullYear() * 12 + date.getMonth(); -}; - -export class MonthPickerComponent extends VDOM.Component { - constructor(props) { - super(props); - let { data, widget } = props.instance; - - let cursor = monthStart(data.refDate ? data.refDate : data.date || data.from || new Date()); - - this.dom = {}; - - this.state = { - cursorYear: cursor.getFullYear(), - cursorMonth: cursor.getMonth() + 1, - cursorQuarter: cursor.getMonth() / 3, - column: "M", - start: widget.startYear, - end: Math.min(widget.startYear + widget.bufferSize, widget.endYear), - }; - - this.handleMouseDown = this.handleMouseDown.bind(this); - this.handleMouseUp = this.handleMouseUp.bind(this); - this.handleMouseEnter = this.handleMouseEnter.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - this.handleTouchMove = this.handleTouchMove.bind(this); - this.handleTouchEnd = this.handleTouchEnd.bind(this); - } - - extractCursorInfo(el) { - if (!el.attributes["data-point"].value) return false; - let parts = el.attributes["data-point"].value.split("-"); - if (parts[0] != "Y") return false; - let cursor = { - column: "Y", - cursorYear: Number(parts[1]), - }; - if (parts.length == 4) { - cursor.column = parts[2]; - if (cursor.column == "M") cursor.cursorMonth = Number(parts[3]); - else cursor.cursorQuarter = Number(parts[3]); - } - return cursor; - } - - moveCursor(e, data, options = {}) { - e.preventDefault(); - e.stopPropagation(); - - if (data.cursorYear) { - let { startYear, endYear } = this.props.instance.widget; - data.cursorYear = Math.max(startYear, Math.min(endYear, data.cursorYear)); - } - - if (Object.keys(data).every((k) => this.state[k] == data[k])) return; - - this.setState(data, () => { - if (options.ensureVisible) { - let index = this.state.cursorYear - this.state.start; - let tbody = this.dom.table.children[index]; - if (tbody) scrollElementIntoView(tbody); - } - }); - } - - handleKeyPress(e) { - let { widget } = this.props.instance; - let { cursorMonth, cursorYear, cursorQuarter, column } = this.state; - - switch (e.keyCode) { - case KeyCode.enter: - // if (widget.range && e.shiftKey && !this.dragStartDates) { - // this.handleMouseDown(e, {}, false); - // } else { - // this.handleMouseUp(e); - // } - this.handleMouseUp(e); - e.preventDefault(); - e.stopPropagation(); - break; - - case KeyCode.left: - if (column == "Y") this.moveCursor(e, { cursorQuarter: 3, cursorYear: cursorYear - 1, column: "Q" }); - else if (column == "Q") this.moveCursor(e, { cursorMonth: cursorQuarter * 4, column: "M" }); - else if (column == "M" && (cursorMonth - 1) % 3 == 0) this.moveCursor(e, { column: "Y" }); - else this.moveCursor(e, { cursorMonth: cursorMonth - 1 }); - break; - - case KeyCode.right: - if (column == "Y") this.moveCursor(e, { cursorMonth: 1, column: "M" }); - else if (column == "Q") - this.moveCursor(e, { column: "Y", cursorYear: cursorQuarter == 3 ? cursorYear + 1 : cursorYear }); - else if (column == "M" && (cursorMonth - 1) % 3 == 2) - this.moveCursor(e, { column: "Q", cursorQuarter: Math.floor((cursorMonth - 1) / 3) }); - else this.moveCursor(e, { cursorMonth: cursorMonth + 1 }); - break; - - case KeyCode.up: - if (column == "Y") this.moveCursor(e, { cursorYear: cursorYear - 1 }, { ensureVisible: true }); - else if (column == "Q") - this.moveCursor( - e, - { - cursorQuarter: (cursorQuarter + 3) % 4, - cursorYear: cursorQuarter == 0 ? cursorYear - 1 : cursorYear, - }, - { ensureVisible: true }, - ); - else if (column == "M") - if (cursorMonth > 3) this.moveCursor(e, { cursorMonth: cursorMonth - 3 }, { ensureVisible: true }); - else - this.moveCursor( - e, - { cursorMonth: cursorMonth + 9, cursorYear: cursorYear - 1 }, - { ensureVisible: true }, - ); - break; - - case KeyCode.down: - if (column == "Y") this.moveCursor(e, { cursorYear: cursorYear + 1 }, { ensureVisible: true }); - else if (column == "Q") - this.moveCursor( - e, - { - cursorQuarter: (cursorQuarter + 1) % 4, - cursorYear: cursorQuarter == 3 ? cursorYear + 1 : cursorYear, - }, - { ensureVisible: true }, - ); - else if (column == "M") - if (cursorMonth < 10) this.moveCursor(e, { cursorMonth: cursorMonth + 3 }, { ensureVisible: true }); - else - this.moveCursor( - e, - { cursorMonth: cursorMonth - 9, cursorYear: cursorYear + 1 }, - { ensureVisible: true }, - ); - break; - - case KeyCode.pageUp: - this.moveCursor(e, { cursorYear: this.state.cursorYear - 1 }); - break; - - case KeyCode.pageDown: - this.moveCursor(e, { cursorYear: this.state.cursorYear + 1 }); - break; - - default: - if (this.props.onKeyDown) this.props.onKeyDown(e, this.props.instance); - break; - } - } - - handleBlur(e) { - FocusManager.nudge(); - if (this.props.onBlur) this.props.onBlur(); - this.setState({ - focused: false, - }); - } - - handleFocus(e) { - this.setState({ - focused: true, - }); - if (this.props.onFocusOut) oneFocusOut(this, this.dom.el, this.handleFocusOut.bind(this)); - } - - handleFocusOut() { - if (this.props.onFocusOut) this.props.onFocusOut(); - } - - getCursorDates(cursor) { - let { cursorMonth, cursorYear, cursorQuarter, column } = cursor || this.state; - switch (column) { - case "M": - return [new Date(cursorYear, cursorMonth - 1, 1), new Date(cursorYear, cursorMonth, 1)]; - - case "Q": - return [new Date(cursorYear, cursorQuarter * 3, 1), new Date(cursorYear, cursorQuarter * 3 + 3, 1)]; - - case "Y": - return [new Date(cursorYear, 0, 1), new Date(cursorYear + 1, 0, 1)]; - } - } - - handleTouchMove(e) { - let cursor = getCursorPos(e); - let el = document.elementFromPoint(cursor.clientX, cursor.clientY); - if (this.dom.table.contains(el) && isString(el.dataset.point)) { - let cursor = this.extractCursorInfo(el); - this.moveCursor(e, cursor); - } - } - - handleTouchEnd(e) { - if (this.state.state == "drag") this.handleMouseUp(e); - } - - handleMouseEnter(e) { - let cursor = this.extractCursorInfo(e.target); - cursor.hover = !isTouchEvent(); - this.moveCursor(e, cursor); - } - - handleMouseDown(e, cursor, drag = true) { - let { instance } = this.props; - let { widget } = instance; - - if (!cursor) { - cursor = this.extractCursorInfo(e.currentTarget); - this.moveCursor(e, cursor); - } - - e.stopPropagation(); - preventFocusOnTouch(e); - - this.dragStartDates = this.getCursorDates(cursor); - if (drag) { - this.setState({ - state: "drag", - ...cursor, - }); - } - } - - handleMouseUp(e) { - let { instance } = this.props; - let { widget, data } = instance; - - e.stopPropagation(); - e.preventDefault(); - - let [cursorFromDate, cursorToDate] = this.getCursorDates(); - let originFromDate = cursorFromDate, - originToDate = cursorToDate; - if (widget.range && e.shiftKey) { - if (data.from) originFromDate = data.from; - if (data.to) originToDate = data.to; - } else if (this.state.state == "drag") { - if (widget.range) { - [originFromDate, originToDate] = this.dragStartDates; - } - this.setState({ state: "normal" }); - } else { - //skip mouse events originated somewhere else - if (e.type != "keydown") return; - } - widget.handleSelect(e, instance, minDate(originFromDate, cursorFromDate), maxDate(originToDate, cursorToDate)); - } - - render() { - let { instance } = this.props; - let { data, widget, isMonthDateSelectable } = instance; - let { CSS, baseClass, startYear, endYear, hideQuarters } = widget; - - let years = []; - - let { start, end } = this.state; - - let from = 10000, - to = 0, - a, - b; - - if (data.date && !widget.range) { - from = monthNumber(data.date); - to = from + 0.1; - } else if (widget.range) { - if (this.state.state == "drag") { - let [originFromDate, originToDate] = this.dragStartDates; - let [cursorFromDate, cursorToDate] = this.getCursorDates(); - a = Math.min(monthNumber(originFromDate), monthNumber(cursorFromDate)); - b = Math.max(monthNumber(originToDate), monthNumber(cursorToDate)); - from = Math.min(a, b); - to = Math.max(a, b); - } else if (data.from && data.to) { - a = monthNumber(data.from); - b = monthNumber(data.to); - from = Math.min(a, b); - to = Math.max(a, b); - } - } - - let monthNames = Culture.getDateTimeCulture().getMonthNames("short"); - let showCursor = this.state.hover || this.state.focused; - - for (let y = start; y <= end; y++) { - let selectableMonths = 0b111111111111; - // Loop through the months in a year to check if all months are unselectable - for (let i = 0; i < 12; i++) { - if ( - (isMonthDateSelectable && !isMonthDateSelectable(new Date(y, i, 1))) || - !dateSelectableCheck(new Date(y, i, 1), data) - ) { - // Set month as unselectable at specified bit - selectableMonths &= ~(1 << i); - } - } - - // All bits are 0 - all months are unselectable - const unselectableYear = selectableMonths === 0; - - let rows = []; - for (let q = 0; q < 4; q++) { - let row = []; - if (q == 0) { - row.push( - - {y} - , - ); - } - - for (let i = 0; i < 3; i++) { - let m = q * 3 + i + 1; - const unselectableMonth = (selectableMonths & (1 << (m - 1))) === 0; - let mno = y * 12 + m - 1; - let handle = true; //isTouchDevice(); //mno === from || mno === to - 1; - row.push( - = from && mno < to, - unselectable: unselectableMonth, - })} - data-point={`Y-${y}-M-${m}`} - onMouseEnter={unselectableMonth ? null : this.handleMouseEnter} - onMouseDown={unselectableMonth ? null : this.handleMouseDown} - onMouseUp={unselectableMonth ? null : this.handleMouseUp} - onTouchStart={unselectableMonth ? null : this.handleMouseDown} - onTouchMove={unselectableMonth ? null : this.handleTouchMove} - onTouchEnd={this.handleMouseUp} - > - {monthNames[m - 1].substr(0, 3)} - , - ); - } - - if (!hideQuarters) { - let unselectableQuarter = true; - const start = q * 3; - for (let i = start; i < start + 3; i++) { - if ((selectableMonths & (1 << i)) !== 0) { - // found a selectable month in a quarter - unselectableQuarter = false; - break; - } - } - - row.push( - - {`Q${q + 1}`} - , - ); - } - - rows.push(row); - } - years.push(rows); - } - - return ( -
    { - this.dom.el = el; - }} - className={data.classNames} - style={data.style} - tabIndex={data.disabled ? null : data.tabIndex || 0} - onKeyDown={this.handleKeyPress} - onMouseDown={stopPropagation} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={this.handleMouseLeave.bind(this)} - onFocus={(e) => this.handleFocus(e)} - onBlur={this.handleBlur.bind(this)} - onScroll={this.onScroll.bind(this)} - > - {this.state.yearHeight &&
    } - { - this.dom.table = el; - }} - > - {years.map((rows, y) => ( - - {rows.map((cells, i) => ( - {cells} - ))} - - ))} -
    - {this.state.yearHeight && ( -
    - )} -
    - ); - } - - onScroll() { - let { startYear, endYear, bufferSize } = this.props.instance.widget; - let visibleItems = ceil5(Math.ceil(this.dom.el.offsetHeight / this.state.yearHeight)); - let start = Math.max( - startYear, - startYear + floor5(Math.floor(this.dom.el.scrollTop / this.state.yearHeight)) - visibleItems, - ); - if (start != this.state.start && start + bufferSize <= endYear) { - this.setState({ - start, - end: start + 15, - }); - } - } - - handleMouseLeave(e) { - tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance)); - this.moveCursor(e, { - hover: false, - }); - } - - componentDidMount() { - //non-input, ok to focus on mobile - if (this.props.autoFocus) this.dom.el.focus(); - - tooltipParentDidMount(this.dom.el, ...getFieldTooltip(this.props.instance)); - let yearHeight = this.dom.table.scrollHeight / (this.props.instance.widget.bufferSize + 1); - this.setState( - { - yearHeight: yearHeight, - }, - () => { - let { widget, data } = this.props.instance; - let { startYear } = widget; - let yearCount = 1; - if (widget.range && data.from && data.to) { - yearCount = data.to.getFullYear() - data.from.getFullYear() + 1; - if (data.to.getMonth() == 0 && data.to.getDate() == 1) yearCount--; - } - this.dom.el.scrollTop = - (this.state.cursorYear - startYear + yearCount / 2) * this.state.yearHeight - - this.dom.el.offsetHeight / 2; - }, - ); - } - - UNSAFE_componentWillReceiveProps(props) { - this.setState({ - state: "normal", - }); - tooltipParentWillReceiveProps(this.dom.el, ...getFieldTooltip(props.instance)); - } - - componentWillUnmount() { - offFocusOut(this); - tooltipParentWillUnmount(this.props.instance); - } -} - -function ceil5(x) { - return Math.ceil(x / 5) * 5; -} - -function floor5(x) { - return Math.floor(x / 5) * 5; -} diff --git a/packages/cx/src/widgets/form/MonthPicker.scss b/packages/cx/src/widgets/form/MonthPicker.scss index 948dedd1f..8c4587fca 100644 --- a/packages/cx/src/widgets/form/MonthPicker.scss +++ b/packages/cx/src/widgets/form/MonthPicker.scss @@ -1,7 +1,10 @@ +@use "sass:map"; +@use "sass:color"; + @mixin cx-monthpicker($name: "monthpicker", $state-style-map: $cx-input-state-style-map, $besm: $cx-besm) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { overflow-y: scroll; @@ -26,7 +29,7 @@ th, td { - border-top: 1px solid transparentize($border-color, 0.5); + border-top: 1px solid color.adjust($border-color, $alpha: -0.5); -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } @@ -112,7 +115,7 @@ .#{$block}#{$name}.#{$state}disabled { background-color: transparent; border-color: transparent; - color: darken(#fff, 18); + color: color.adjust(#fff, $lightness: -18%); pointer-events: none; } } diff --git a/packages/cx/src/widgets/form/MonthPicker.tsx b/packages/cx/src/widgets/form/MonthPicker.tsx new file mode 100644 index 000000000..42610397d --- /dev/null +++ b/packages/cx/src/widgets/form/MonthPicker.tsx @@ -0,0 +1,822 @@ +/**@jsxImportSource react */ + +import { Widget, VDOM } from "../../ui/Widget"; +import { Field, getFieldTooltip, FieldInstance, FieldConfig } from "./Field"; +import { Culture } from "../../ui/Culture"; +import { FocusManager, oneFocusOut, offFocusOut, preventFocusOnTouch } from "../../ui/FocusManager"; +import { StringTemplate } from "../../data/StringTemplate"; +import { monthStart } from "../../util/date/monthStart"; +import { dateDiff } from "../../util/date/dateDiff"; +import { minDate } from "../../util/date/minDate"; +import { maxDate } from "../../util/date/maxDate"; +import { lowerBoundCheck } from "../../util/date/lowerBoundCheck"; +import { upperBoundCheck } from "../../util/date/upperBoundCheck"; +import { Console } from "../../util/Console"; +import { KeyCode } from "../../util/KeyCode"; +import { + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, + tooltipMouseMove, + tooltipMouseLeave, + tooltipParentDidMount, +} from "../overlay/tooltip-ops"; +import { Localization } from "../../ui/Localization"; +import { scrollElementIntoView } from "../../util/scrollElementIntoView"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { isString } from "../../util/isString"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { getCursorPos } from "../overlay/captureMouse"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import type { Prop, BooleanProp } from "../../ui/Prop"; + +import { enableCultureSensitiveFormatting } from "../../ui/Format"; +import { parseDateInvariant } from "../../util"; +import { HtmlElement } from "../HtmlElement"; +enableCultureSensitiveFormatting(); + +export class MonthPickerInstance extends FieldInstance { + isMonthDateSelectable?: (monthDate: Date) => boolean; +} + +export interface MonthPickerConfig extends FieldConfig { + range?: BooleanProp; + from?: Prop; + to?: Prop; + value?: Prop; + refDate?: Prop; + minValue?: Prop; + minExclusive?: BooleanProp; + maxValue?: Prop; + maxExclusive?: BooleanProp; + startYear?: number; + endYear?: number; + bufferSize?: number; + maxValueErrorText?: string; + maxExclusiveErrorText?: string; + minValueErrorText?: string; + minExclusiveErrorText?: string; + encoding?: (date: Date) => any; + inclusiveTo?: boolean; + onBeforeSelect?: (e: Event, instance: MonthPickerInstance, dateFrom?: Date, dateTo?: Date) => boolean; + onSelect?: (instance: MonthPickerInstance, dateFrom?: Date, dateTo?: Date) => void; + hideQuarters?: boolean; + onCreateIsMonthDateSelectable?: ( + validationParams: Record, + instance: MonthPickerInstance, + ) => (monthDate: Date) => boolean; + handleSelect?: (e: React.MouseEvent, instance: MonthPickerInstance, dateFrom?: Date, dateTo?: Date) => void; + onBlur?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + onFocusOut?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + autoFocus?: boolean; +} + +export class MonthPicker extends Field< + Config, + MonthPickerInstance +> { + declare public baseClass: string; + declare public mode?: string; + declare public range?: BooleanProp; + declare public from?: Prop; + declare public to?: Prop; + declare public value?: Prop; + declare public refDate?: Prop; + declare public minValue?: Prop; + declare public minExclusive?: BooleanProp; + declare public maxValue?: Prop; + declare public maxExclusive?: BooleanProp; + declare public startYear: number; + declare public endYear: number; + declare public bufferSize: number; + declare public maxValueErrorText: string; + declare public maxExclusiveErrorText: string; + declare public minValueErrorText: string; + declare public minExclusiveErrorText: string; + declare public encoding?: (date: Date) => any; + declare public inclusiveTo?: boolean; + declare public onBeforeSelect?: (e: Event, instance: MonthPickerInstance, dateFrom?: Date, dateTo?: Date) => boolean; + declare public onSelect?: (instance: MonthPickerInstance, dateFrom?: Date, dateTo?: Date) => void; + declare public hideQuarters?: boolean; + declare public onCreateIsMonthDateSelectable?: ( + validationParams: Record, + instance: MonthPickerInstance, + ) => (monthDate: Date) => boolean; + declare public onBlur?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + declare public onFocusOut?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + + declareData(...args: Record[]): void { + let values: Record = {}; + + if (this.mode == "range") { + this.range = true; + this.mode = "edit"; + Console.warn('Please use the range flag on MonthPickers. Syntax mode="range" is deprecated.', this); + } + + if (this.range) { + values = { + from: null, + to: null, + }; + } else { + values = { + value: this.emptyValue, + }; + } + + super.declareData( + values, + { + refDate: undefined, + disabled: undefined, + minValue: undefined, + minExclusive: undefined, + maxValue: undefined, + maxExclusive: undefined, + }, + ...args, + ); + } + + init(): void { + super.init(); + } + + prepareData(context: RenderingContext, instance: MonthPickerInstance): void { + let { data } = instance; + data.stateMods = { + disabled: data.disabled, + }; + + if (!this.range && data.value) data.date = monthStart(parseDateInvariant(data.value)); + + if (this.range) { + if (data.from) data.from = monthStart(parseDateInvariant(data.from)); + + if (data.to) { + let date = parseDateInvariant(data.to); + if (this.inclusiveTo) date.setDate(date.getDate() + 1); + data.to = monthStart(date); + } + } + + if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); + + if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); + + if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); + + if (this.onCreateIsMonthDateSelectable) { + instance.isMonthDateSelectable = instance.invoke( + "onCreateIsMonthDateSelectable", + data.validationParams, + instance, + ); + } + + super.prepareData(context, instance); + } + + validate(context: RenderingContext, instance: MonthPickerInstance): void { + super.validate(context, instance); + let { data } = instance; + if (!data.error && data.date) { + let d; + if (data.maxValue) { + d = dateDiff(data.date, data.maxValue); + if (d > 0) data.error = StringTemplate.format(this.maxValueErrorText, data.maxValue); + else if (d == 0 && data.maxExclusive) + data.error = StringTemplate.format(this.maxExclusiveErrorText, data.maxValue); + } + + if (data.minValue) { + d = dateDiff(data.date, data.minValue); + if (d < 0) data.error = StringTemplate.format(this.minValueErrorText, data.minValue); + else if (d == 0 && data.minExclusive) + data.error = StringTemplate.format(this.minExclusiveErrorText, data.minValue); + } + } + } + + renderInput(context: RenderingContext, instance: MonthPickerInstance, key: string): React.ReactNode { + return ; + } + + handleSelect( + e: React.KeyboardEvent | React.MouseEvent | React.TouchEvent, + instance: MonthPickerInstance, + date1: Date, + date2: Date, + ): void { + let { data, widget, isMonthDateSelectable } = instance; + let encode = widget.encoding || Culture.getDefaultDateEncoding(); + + if (data.disabled) return; + + if (isMonthDateSelectable && !isMonthDateSelectable(date1)) return; + if (!dateSelectableCheck(date1, data)) return; + + if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date1, date2) === false) return; + + if (this.range) { + instance.set("from", encode(date1)); + let toDate = new Date(date2); + if (this.inclusiveTo) toDate.setDate(toDate.getDate() - 1); + instance.set("to", encode(toDate)); + } else instance.set("value", encode(date1)); + + if (this.onSelect) instance.invoke("onSelect", instance, date1, date2); + } +} + +MonthPicker.prototype.baseClass = "monthpicker"; +MonthPicker.prototype.range = false; +MonthPicker.prototype.startYear = 1980; +MonthPicker.prototype.endYear = 2030; +MonthPicker.prototype.bufferSize = 15; +MonthPicker.prototype.hideQuarters = false; + +// Localization +MonthPicker.prototype.maxValueErrorText = "Select {0:d} or before."; +MonthPicker.prototype.maxExclusiveErrorText = "Select a date before {0:d}."; +MonthPicker.prototype.minValueErrorText = "Select {0:d} or later."; +MonthPicker.prototype.minExclusiveErrorText = "Select a date after {0:d}."; +MonthPicker.prototype.inclusiveTo = false; + +Localization.registerPrototype("cx/widgets/MonthPicker", MonthPicker); + +Widget.alias("month-picker", MonthPicker); + +const dateSelectableCheck = (date: Date, data: Record): boolean => { + if (data.maxValue && !upperBoundCheck(date, data.maxValue as Date, data.maxExclusive)) return false; + + if (data.minValue && !lowerBoundCheck(date, data.minValue as Date, data.minExclusive)) return false; + + return true; +}; + +const monthNumber = (date: Date): number => { + return date.getFullYear() * 12 + date.getMonth(); +}; + +interface MonthPickerComponentProps { + instance: MonthPickerInstance; + onBlur?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + onFocusOut?: string | ((e: React.FocusEvent, instance: MonthPickerInstance) => void); + onKeyDown?: string | ((e: React.KeyboardEvent, instance: MonthPickerInstance) => boolean | void); + autoFocus?: boolean; +} + +interface MonthPickerComponentState { + cursorYear: number; + cursorMonth: number; + cursorQuarter: number; + column: string; + start: number; + end: number; + state?: string; + hover?: boolean; + focused?: boolean; + yearHeight?: number; +} + +interface CursorInfo { + column: string; + cursorYear: number; + cursorMonth: number; + cursorQuarter: number; + hover?: boolean; +} + +export class MonthPickerComponent extends VDOM.Component { + dom: { + el?: HTMLDivElement | null; + table?: HTMLTableElement | null; + } = {}; + dragStartDates?: [Date, Date]; + + constructor(props: MonthPickerComponentProps) { + super(props); + let { data, widget } = props.instance; + + let cursor = monthStart(data.refDate ? data.refDate : data.date || data.from || new Date()); + + this.dom = {}; + + this.state = { + cursorYear: cursor.getFullYear(), + cursorMonth: cursor.getMonth() + 1, + cursorQuarter: cursor.getMonth() / 3, + column: "M", + start: widget.startYear, + end: Math.min(widget.startYear + widget.bufferSize, widget.endYear), + }; + + this.handleMouseDown = this.handleMouseDown.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + this.handleTouchMove = this.handleTouchMove.bind(this); + this.handleTouchEnd = this.handleTouchEnd.bind(this); + } + + extractCursorInfo(el: HTMLElement): CursorInfo | false { + const dataPoint = el.getAttribute("data-point"); + if (!dataPoint) return false; + let parts = dataPoint.split("-"); + if (parts[0] != "Y") return false; + let cursor: CursorInfo = { + column: "Y", + cursorYear: Number(parts[1]), + cursorMonth: 1, + cursorQuarter: 1, + }; + if (parts.length == 4) { + cursor.column = parts[2]; + if (cursor.column == "M") cursor.cursorMonth = Number(parts[3]); + else cursor.cursorQuarter = Number(parts[3]); + } + return cursor; + } + + moveCursor( + e: React.KeyboardEvent | React.MouseEvent | React.TouchEvent, + data: Pick, + options: { ensureVisible?: boolean } = {}, + ): void { + e.preventDefault(); + e.stopPropagation(); + + if ("cursorYear" in data && data.cursorYear !== undefined) { + let { startYear, endYear } = this.props.instance.widget; + (data as any).cursorYear = Math.max(startYear, Math.min(endYear, data.cursorYear as number)); + } + + if (Object.keys(data).every((k) => this.state[k as K] == (data as any)[k])) return; + + this.setState(data, () => { + if (options.ensureVisible) { + let index = this.state.cursorYear - this.state.start; + let tbody = this.dom.table?.children?.[index]; + if (tbody) scrollElementIntoView(tbody); + } + }); + } + + handleKeyPress(e: React.KeyboardEvent): void { + let { instance } = this.props; + let { widget } = this.props.instance; + let { cursorMonth, cursorYear, cursorQuarter, column } = this.state; + + switch (e.keyCode) { + case KeyCode.enter: + // if (widget.range && e.shiftKey && !this.dragStartDates) { + // this.handleMouseDown(e, {}, false); + // } else { + // this.handleMouseUp(e); + // } + this.handleMouseUp(e); + e.preventDefault(); + e.stopPropagation(); + break; + + case KeyCode.left: + if (column == "Y") this.moveCursor(e, { cursorQuarter: 3, cursorYear: cursorYear - 1, column: "Q" }); + else if (column == "Q") this.moveCursor(e, { cursorMonth: cursorQuarter * 4, column: "M" }); + else if (column == "M" && (cursorMonth - 1) % 3 == 0) this.moveCursor(e, { column: "Y" }); + else this.moveCursor(e, { cursorMonth: cursorMonth - 1 }); + break; + + case KeyCode.right: + if (column == "Y") this.moveCursor(e, { cursorMonth: 1, column: "M" }); + else if (column == "Q") + this.moveCursor(e, { column: "Y", cursorYear: cursorQuarter == 3 ? cursorYear + 1 : cursorYear }); + else if (column == "M" && (cursorMonth - 1) % 3 == 2) + this.moveCursor(e, { column: "Q", cursorQuarter: Math.floor((cursorMonth - 1) / 3) }); + else this.moveCursor(e, { cursorMonth: cursorMonth + 1 }); + break; + + case KeyCode.up: + if (column == "Y") this.moveCursor(e, { cursorYear: cursorYear - 1 }, { ensureVisible: true }); + else if (column == "Q") + this.moveCursor( + e, + { + cursorQuarter: (cursorQuarter + 3) % 4, + cursorYear: cursorQuarter == 0 ? cursorYear - 1 : cursorYear, + }, + { ensureVisible: true }, + ); + else if (column == "M") + if (cursorMonth > 3) this.moveCursor(e, { cursorMonth: cursorMonth - 3 }, { ensureVisible: true }); + else + this.moveCursor( + e, + { cursorMonth: cursorMonth + 9, cursorYear: cursorYear - 1 }, + { ensureVisible: true }, + ); + break; + + case KeyCode.down: + if (column == "Y") this.moveCursor(e, { cursorYear: cursorYear + 1 }, { ensureVisible: true }); + else if (column == "Q") + this.moveCursor( + e, + { + cursorQuarter: (cursorQuarter + 1) % 4, + cursorYear: cursorQuarter == 3 ? cursorYear + 1 : cursorYear, + }, + { ensureVisible: true }, + ); + else if (column == "M") + if (cursorMonth < 10) this.moveCursor(e, { cursorMonth: cursorMonth + 3 }, { ensureVisible: true }); + else + this.moveCursor( + e, + { cursorMonth: cursorMonth - 9, cursorYear: cursorYear + 1 }, + { ensureVisible: true }, + ); + break; + + case KeyCode.pageUp: + this.moveCursor(e, { cursorYear: this.state.cursorYear - 1 }); + break; + + case KeyCode.pageDown: + this.moveCursor(e, { cursorYear: this.state.cursorYear + 1 }); + break; + + default: + if (widget.onKeyDown) instance.invoke("onKeyDown", e, instance); + break; + } + } + + handleBlur(e: React.FocusEvent): void { + FocusManager.nudge(); + let { instance } = this.props; + let { widget } = instance; + if (widget.onBlur) instance.invoke("onBlur", e, instance); + this.setState({ + focused: false, + }); + } + + handleFocus(e: React.FocusEvent): void { + this.setState({ + focused: true, + }); + if (this.props.onFocusOut && this.dom.el) oneFocusOut(this, this.dom.el, this.handleFocusOut.bind(this)); + } + + handleFocusOut(e: React.FocusEvent): void { + let { instance } = this.props; + let { widget } = instance; + if (widget.onFocusOut) instance.invoke("onFocusOut", e, instance); + } + + getCursorDates(cursor?: CursorInfo): [Date, Date] { + let { cursorMonth, cursorYear, cursorQuarter, column } = cursor ?? this.state; + switch (column) { + case "M": + return [new Date(cursorYear, cursorMonth - 1, 1), new Date(cursorYear, cursorMonth, 1)]; + + case "Q": + return [new Date(cursorYear, cursorQuarter * 3, 1), new Date(cursorYear, cursorQuarter * 3 + 3, 1)]; + + default: + case "Y": + return [new Date(cursorYear, 0, 1), new Date(cursorYear + 1, 0, 1)]; + } + } + + handleTouchMove(e: React.TouchEvent): void { + let cursorPos = getCursorPos(e); + let el = document.elementFromPoint(cursorPos.clientX, cursorPos.clientY); + if ( + this.dom.table && + el && + this.dom.table.contains(el) && + el instanceof HTMLElement && + isString(el.dataset.point) + ) { + let cursor = this.extractCursorInfo(el); + if (cursor) this.moveCursor(e, cursor); + } + } + + handleTouchEnd(e: React.TouchEvent): void { + if (this.state.state == "drag") this.handleMouseUp(e); + } + + handleMouseEnter(e: React.MouseEvent): void { + let cursor = this.extractCursorInfo(e.target as HTMLElement); + if (cursor) { + cursor.hover = !isTouchEvent(); + this.moveCursor(e, cursor); + } + } + + handleMouseDown(e: React.MouseEvent | React.TouchEvent, cursor?: CursorInfo | false, drag: boolean = true): void { + let { instance } = this.props; + let { widget } = instance; + + if (!cursor) { + cursor = this.extractCursorInfo(e.currentTarget as HTMLElement); + if (!cursor) return; + this.moveCursor(e, cursor); + } + + e.stopPropagation(); + preventFocusOnTouch(e); + + this.dragStartDates = this.getCursorDates(cursor); + if (drag) { + this.setState({ + state: "drag", + ...cursor, + }); + } + } + + handleMouseUp(e: React.KeyboardEvent | React.MouseEvent | React.TouchEvent): void { + let { instance } = this.props; + let { widget, data } = instance; + + e.stopPropagation(); + e.preventDefault(); + + let [cursorFromDate, cursorToDate] = this.getCursorDates(); + let originFromDate = cursorFromDate, + originToDate = cursorToDate; + if (widget.range && e.shiftKey) { + if (data.from) originFromDate = data.from; + if (data.to) originToDate = data.to; + } else if (this.state.state == "drag") { + if (widget.range) { + [originFromDate, originToDate] = this.dragStartDates!; + } + this.setState({ state: "normal" }); + } else { + //skip mouse events originated somewhere else + if (e.type != "keydown") return; + } + widget.handleSelect(e, instance, minDate(originFromDate, cursorFromDate), maxDate(originToDate, cursorToDate)); + } + + render(): React.ReactNode { + let { instance } = this.props; + let { data, widget, isMonthDateSelectable } = instance; + let { CSS, baseClass, startYear, endYear, hideQuarters } = widget; + + let years = []; + + let { start, end } = this.state; + + let from = 10000, + to = 0, + a, + b; + + if (data.date && !widget.range) { + from = monthNumber(data.date); + to = from + 0.1; + } else if (widget.range) { + if (this.state.state == "drag") { + let [originFromDate, originToDate] = this.dragStartDates!; + let [cursorFromDate, cursorToDate] = this.getCursorDates(); + a = Math.min(monthNumber(originFromDate), monthNumber(cursorFromDate)); + b = Math.max(monthNumber(originToDate), monthNumber(cursorToDate)); + from = Math.min(a, b); + to = Math.max(a, b); + } else if (data.from && data.to) { + a = monthNumber(data.from); + b = monthNumber(data.to); + from = Math.min(a, b); + to = Math.max(a, b); + } + } + + let monthNames = Culture.getDateTimeCulture().getMonthNames("short"); + let showCursor = this.state.hover || this.state.focused; + + for (let y = start; y <= end; y++) { + let selectableMonths = 0b111111111111; + // Loop through the months in a year to check if all months are unselectable + for (let i = 0; i < 12; i++) { + if ( + (isMonthDateSelectable && !isMonthDateSelectable(new Date(y, i, 1))) || + !dateSelectableCheck(new Date(y, i, 1), data) + ) { + // Set month as unselectable at specified bit + selectableMonths &= ~(1 << i); + } + } + + // All bits are 0 - all months are unselectable + const unselectableYear = selectableMonths === 0; + + let rows = []; + for (let q = 0; q < 4; q++) { + let row = []; + if (q == 0) { + row.push( + + {y} + , + ); + } + + for (let i = 0; i < 3; i++) { + let m = q * 3 + i + 1; + const unselectableMonth = (selectableMonths & (1 << (m - 1))) === 0; + let mno = y * 12 + m - 1; + let handle = true; //isTouchDevice(); //mno === from || mno === to - 1; + row.push( + = from && mno < to, + unselectable: unselectableMonth, + })} + data-point={`Y-${y}-M-${m}`} + onMouseEnter={unselectableMonth ? undefined : this.handleMouseEnter} + onMouseDown={unselectableMonth ? undefined : this.handleMouseDown} + onMouseUp={unselectableMonth ? undefined : this.handleMouseUp} + onTouchStart={unselectableMonth ? undefined : this.handleMouseDown} + onTouchMove={unselectableMonth ? undefined : this.handleTouchMove} + onTouchEnd={this.handleMouseUp} + > + {monthNames[m - 1].substr(0, 3)} + , + ); + } + + if (!hideQuarters) { + let unselectableQuarter = true; + const start = q * 3; + for (let i = start; i < start + 3; i++) { + if ((selectableMonths & (1 << i)) !== 0) { + // found a selectable month in a quarter + unselectableQuarter = false; + break; + } + } + + row.push( + + {`Q${q + 1}`} + , + ); + } + + rows.push(row); + } + years.push(rows); + } + + return ( +
    { + this.dom.el = el; + }} + className={data.classNames} + style={data.style} + tabIndex={data.disabled ? null : data.tabIndex || 0} + onKeyDown={this.handleKeyPress} + onMouseDown={stopPropagation} + onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} + onMouseLeave={this.handleMouseLeave.bind(this)} + onFocus={(e) => this.handleFocus(e)} + onBlur={this.handleBlur.bind(this)} + onScroll={this.onScroll.bind(this)} + > + {this.state.yearHeight &&
    } + { + this.dom.table = el; + }} + > + {years.map((rows, y) => ( + + {rows.map((cells, i) => ( + {cells} + ))} + + ))} +
    + {this.state.yearHeight && ( +
    + )} +
    + ); + } + + onScroll(): void { + if (!this.dom.el || !this.state.yearHeight) return; + let { startYear, endYear, bufferSize } = this.props.instance.widget; + let visibleItems = ceil5(Math.ceil(this.dom.el.offsetHeight / this.state.yearHeight)); + let start = Math.max( + startYear, + startYear + floor5(Math.floor(this.dom.el.scrollTop / this.state.yearHeight)) - visibleItems, + ); + if (start != this.state.start && start + bufferSize <= endYear) { + this.setState({ + start, + end: start + 15, + }); + } + } + + handleMouseLeave(e: React.MouseEvent): void { + tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance)); + this.moveCursor(e, { + hover: false, + }); + } + + componentDidMount(): void { + //non-input, ok to focus on mobile + if (this.props.autoFocus && this.dom.el) this.dom.el.focus(); + + if (this.dom.el && this.dom.table) { + tooltipParentDidMount(this.dom.el, ...getFieldTooltip(this.props.instance)); + let yearHeight = this.dom.table.scrollHeight / (this.props.instance.widget.bufferSize + 1); + this.setState( + { + yearHeight: yearHeight, + }, + () => { + let { widget, data } = this.props.instance; + let { startYear } = widget; + let yearCount = 1; + if (widget.range && data.from && data.to) { + yearCount = data.to.getFullYear() - data.from.getFullYear() + 1; + if (data.to.getMonth() == 0 && data.to.getDate() == 1) yearCount--; + } + if (this.dom.el && this.state.yearHeight) { + this.dom.el.scrollTop = + (this.state.cursorYear - startYear + yearCount / 2) * this.state.yearHeight - + this.dom.el.offsetHeight / 2; + } + }, + ); + } + } + + UNSAFE_componentWillReceiveProps(props: MonthPickerComponentProps): void { + this.setState({ + state: "normal", + }); + if (this.dom.el) { + tooltipParentWillReceiveProps(this.dom.el, ...getFieldTooltip(props.instance)); + } + } + + componentWillUnmount(): void { + offFocusOut(this); + tooltipParentWillUnmount(this.props.instance); + } +} + +function ceil5(x: number): number { + return Math.ceil(x / 5) * 5; +} + +function floor5(x: number): number { + return Math.floor(x / 5) * 5; +} diff --git a/packages/cx/src/widgets/form/NumberField.d.ts b/packages/cx/src/widgets/form/NumberField.d.ts deleted file mode 100644 index c039eef3e..000000000 --- a/packages/cx/src/widgets/form/NumberField.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface NumberFieldProps extends FieldProps { - /** Value of the input. */ - value?: Cx.NumberProp; - - /** Defaults to false. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** Template used to format the value. Examples: `ps` - percentage sign; `n;2` - two decimals. - * By default, number formatting is applied with optional maximum decimal precision. */ - format?: Cx.StringProp; - - /** Minimum number value. */ - minValue?: Cx.NumberProp; - - /** Set to `true` to disallow the `minValue`. Default value is `false`. */ - minExclusive?: Cx.BooleanProp; - - /** Maximum number value. */ - maxValue?: Cx.NumberProp; - - /** Set to `true` to disallow the `maxValue`. Default value is `false`. */ - maxExclusive?: Cx.BooleanProp; - - /** When specified, values lower than `minValue` or higher than `maxValue` will be constrained to `minValue` that is, `maxValue`, respectively. */ - constrain?: Cx.BooleanProp; - - /** - * Percentage used to calculate the increment when it's not explicitly specified. - * Default value is `0.1` (10%). - */ - incrementPercentage?: Cx.NumberProp; - - /** Increment/decrement value when using arrow keys or mouse wheel. */ - increment?: Cx.NumberProp; - - /** Increment/decrement value when using arrow keys or mouse wheel. */ - step?: number; - - /** Base CSS class to be applied to the field. Defaults to `numberfield`. */ - baseClass?: string; - - /** Round values to the nearest tick. Default is `true`. */ - snapToIncrement?: boolean; - - /** Name or configuration of the icon to be put on the left side of the input. */ - icon?: Cx.StringProp | Cx.Record; - - /** Set to `false` to hide the clear button. It can be used interchangeably with the `hideClear` property. Default value is `true`. */ - showClear?: boolean; - - /** Set to `true` to hide the clear button. It can be used interchangeably with the `showClear` property. Default value is `false`. */ - hideClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** Defaults to `input`. Other permitted values are `enter` and `blur`. Multiple values should be separated by space, .e.g. 'enter blur'. */ - reactOn?: string; - - /** Defaults to `text`. Other permitted value is `password`. */ - inputType?: "text" | "password"; - - /** Maximum value error text. */ - maxValueErrorText?: string; - - /** Maximum exclusive value error text. */ - maxExclusiveErrorText?: string; - - /** Minimum value error text. */ - minValueErrorText?: string; - - /** Minimum exclusive value error text. */ - minExclusiveErrorText?: string; - - /** Invalid input error text. */ - inputErrorText?: string; - - /** A scale used to define mapping between displayed and stored values. E.g. 0.01 for percentages. DV = (SV - OFFSET) / SCALE */ - scale?: number; - - /** Offset to define mapping between displayed and stored values. DV = (SV - OFFSET) / SCALE */ - offset?: number; -} - -export class NumberField extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/NumberField.js b/packages/cx/src/widgets/form/NumberField.js deleted file mode 100644 index f35be6380..000000000 --- a/packages/cx/src/widgets/form/NumberField.js +++ /dev/null @@ -1,459 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { Format } from "../../ui/Format"; -import { Culture } from "../../ui/Culture"; -import { StringTemplate } from "../../data/StringTemplate"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { Localization } from "../../ui/Localization"; -import ClearIcon from "../icons/clear"; -import { isString } from "../../util/isString"; -import { isNumber } from "../../util/isNumber"; -import { isDefined } from "../../util/isDefined"; -import { getActiveElement } from "../../util/getActiveElement"; - -import { enableCultureSensitiveFormatting } from "../../ui/Format"; -import { KeyCode } from "../../util/KeyCode"; -import { autoFocus } from "../autoFocus"; - -enableCultureSensitiveFormatting(); - -export class NumberField extends Field { - declareData() { - super.declareData( - { - value: this.emptyValue, - disabled: undefined, - readOnly: undefined, - enabled: undefined, - placeholder: undefined, - required: undefined, - format: undefined, - minValue: undefined, - maxValue: undefined, - constrain: undefined, - minExclusive: undefined, - maxExclusive: undefined, - incrementPercentage: undefined, - increment: undefined, - icon: undefined, - scale: undefined, - offset: undefined, - }, - ...arguments, - ); - } - - init() { - if (isDefined(this.step)) this.increment = this.step; - - if (isDefined(this.hideClear)) this.showClear = !this.hideClear; - - if (this.alwaysShowClear) this.showClear = true; - - super.init(); - } - - prepareData(context, instance) { - let { data, state, cached } = instance; - data.formatted = Format.value(data.value, data.format); - - if (!cached.data || data.value != cached.data.value) state.empty = data.value == null; - - super.prepareData(context, instance); - } - - formatValue(context, { data }) { - return data.formatted; - } - - parseValue(value, instance) { - if (this.onParseInput) { - let result = instance.invoke("onParseInput", value, instance); - if (result !== undefined) return result; - } - return Culture.getNumberCulture().parse(value); - } - - validate(context, instance) { - super.validate(context, instance); - - let { data } = instance; - if (isNumber(data.value) && !data.error) { - if (isNumber(data.minValue)) { - if (data.value < data.minValue) - data.error = StringTemplate.format(this.minValueErrorText, Format.value(data.minValue, data.format)); - else if (data.value == data.minValue && data.minExclusive) - data.error = StringTemplate.format(this.minExclusiveErrorText, Format.value(data.minValue, data.format)); - } - - if (isNumber(data.maxValue)) { - if (data.value > data.maxValue) - data.error = StringTemplate.format(this.maxValueErrorText, Format.value(data.maxValue, data.format)); - else if (data.value == data.maxValue && data.maxExclusive) - data.error = StringTemplate.format(this.maxExclusiveErrorText, Format.value(data.maxValue, data.format)); - } - } - } - - renderInput(context, instance, key) { - return ( - - ); - } - - validateRequired(context, instance) { - return instance.state.empty && this.requiredText; - } -} - -NumberField.prototype.baseClass = "numberfield"; -NumberField.prototype.reactOn = "enter blur"; -NumberField.prototype.format = "n"; -NumberField.prototype.inputType = "text"; - -NumberField.prototype.maxValueErrorText = "Enter {0} or less."; -NumberField.prototype.maxExclusiveErrorText = "Enter a number less than {0}."; -NumberField.prototype.minValueErrorText = "Enter {0} or more."; -NumberField.prototype.minExclusiveErrorText = "Enter a number greater than {0}."; -NumberField.prototype.inputErrorText = "Invalid number entered."; -NumberField.prototype.suppressErrorsUntilVisited = true; - -NumberField.prototype.incrementPercentage = 0.1; -NumberField.prototype.scale = 1; -NumberField.prototype.offset = 0; -NumberField.prototype.snapToIncrement = true; -NumberField.prototype.icon = null; -NumberField.prototype.showClear = false; -NumberField.prototype.alwaysShowClear = false; - -Widget.alias("numberfield", NumberField); -Localization.registerPrototype("cx/widgets/NumberField", NumberField); - -class Input extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - focus: false, - }; - } - - render() { - let { data, instance, label, help, icon: iconVDOM } = this.props; - let { widget, state } = instance; - let { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let icon = iconVDOM &&
    {iconVDOM}
    ; - - let insideButton; - if (!data.readOnly && !data.disabled) { - if ( - widget.showClear && - (((widget.alwaysShowClear || !data.required) && !data.empty) || instance.state.inputError) - ) - insideButton = ( -
    e.preventDefault()} - onClick={(e) => this.onClearClick(e)} - > - -
    - ); - } - - let empty = this.input ? !this.input.value : data.empty; - - return ( -
    - { - this.input = el; - }} - style={data.inputStyle} - disabled={data.disabled} - readOnly={data.readOnly} - tabIndex={data.tabIndex} - placeholder={data.placeholder} - {...data.inputAttrs} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(this.props.instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance))} - onChange={(e) => this.onChange(e, "change")} - onKeyDown={this.onKeyDown.bind(this)} - onBlur={(e) => { - this.onChange(e, "blur"); - }} - onFocus={(e) => this.onFocus()} - onWheel={(e) => { - this.onChange(e, "wheel"); - }} - onClick={stopPropagation} - /> - {insideButton} - {icon} - {label} - {help} -
    - ); - } - - UNSAFE_componentWillReceiveProps(props) { - let { data, state } = props.instance; - if (this.props.data.formatted != data.formatted && !state.inputError) { - this.input.value = props.data.formatted || ""; - props.instance.setState({ - inputError: false, - }); - } - tooltipParentWillReceiveProps(this.input, ...getFieldTooltip(props.instance)); - } - - componentDidMount() { - tooltipParentDidMount(this.input, ...getFieldTooltip(this.props.instance)); - autoFocus(this.input, this); - } - - componentDidUpdate() { - autoFocus(this.input, this); - } - - componentWillUnmount() { - if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { - this.onChange({ target: { value: this.input.value } }, "blur"); - } - tooltipParentWillUnmount(this.props.instance); - } - - getPreCursorDigits(text, cursor, decimalSeparator) { - let res = ""; - for (let i = 0; i < cursor; i++) { - if ("0" <= text[i] && text[i] <= "9") res += text[i]; - else if (text[i] == decimalSeparator) res += "."; - else if (text[i] == "-") res += "-"; - } - return res; - } - - getLengthWithoutSuffix(text, decimalSeparator) { - let l = text.length; - while (l > 0) { - if ("0" <= text[l - 1] && text[l - 1] <= "9") break; - if (text[l - 1] == decimalSeparator) break; - l--; - } - return l; - } - - getDecimalSeparator(format) { - let text = Format.value(0.11111111, format); - for (let i = text.length - 1; i >= 0; i--) { - if ("0" <= text[i] && text[i] <= "9") continue; - if (text[i] == "," || text[i] == ".") return text[i]; - break; - } - return null; - } - - updateCursorPosition(preCursorText) { - if (isString(preCursorText)) { - let cursor = 0; - let preCursor = 0; - let text = this.input.value || ""; - while (preCursor < preCursorText.length && cursor < text.length) { - if (text[cursor] == preCursorText[preCursor]) { - cursor++; - preCursor++; - } else { - cursor++; - } - } - this.input.setSelectionRange(cursor, cursor); - } - } - - calculateIncrement(value, strength) { - if (value == 0) return 0.1; - let absValue = Math.abs(value * strength); - let log10 = Math.floor(Math.log10(absValue) + 0.001); - let size = Math.pow(10, log10); - if (absValue / size > 4.999) return 5 * size; - if (absValue / size > 1.999) return 2 * size; - return size; - } - - onClearClick(e) { - this.input.value = ""; - let { instance } = this.props; - instance.set("value", instance.widget.emptyValue, { immediate: true }); - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.enter: - this.onChange(e, "enter"); - break; - - case KeyCode.left: - case KeyCode.right: - e.stopPropagation(); - break; - } - } - - onChange(e, change) { - let { instance, data } = this.props; - let { widget } = instance; - - if (data.required) { - instance.setState({ - empty: !e.target.value, - }); - } - - if (change == "blur" && this.state.focus) { - this.setState({ - focus: false, - }); - } - - let immediate = change == "blur" || change == "enter"; - - if ((widget.reactOn.indexOf(change) == -1 && !immediate) || data.disabled || data.readOnly) return; - - if (immediate) instance.setState({ visited: true }); - - let value = null; - - if (e.target.value) { - let displayValue = widget.parseValue(e.target.value, instance); - if (isNaN(displayValue)) { - instance.setState({ - inputError: instance.widget.inputErrorText, - }); - return; - } - - value = displayValue * data.scale + data.offset; - - if (change == "wheel") { - e.preventDefault(); - let increment = - data.increment != null ? data.increment : this.calculateIncrement(value, data.incrementPercentage); - value = value + (e.deltaY < 0 ? increment : -increment); - if (widget.snapToIncrement) { - value = Math.round(value / increment) * increment; - } - - if (data.minValue != null) { - if (data.minExclusive) { - if (value <= data.minValue) return; - } else { - value = Math.max(value, data.minValue); - } - } - - if (data.maxValue != null) { - if (data.maxExclusive) { - if (value >= data.maxValue) return; - } else { - value = Math.min(value, data.maxValue); - } - } - } - - if (data.constrain) { - if (data.minValue != null) { - if (value < data.minValue) { - value = data.minValue; - } - } - - if (data.maxValue != null) { - if (value > data.maxValue) { - value = data.maxValue; - } - } - } - - let fmt = data.format; - let decimalSeparator = this.getDecimalSeparator(fmt) || Format.value(1.1, "n;1")[1]; - - let formatted = Format.value(value, fmt); - // Re-parse to avoid differences between formatted value and value in the store - - value = widget.parseValue(formatted, instance) * data.scale + data.offset; - - // Allow users to type numbers like 100.0003 or -0.05 without interruptions - // If the last typed character is zero or dot (decimal separator), skip processing it - if ( - change == "change" && - this.input.selectionStart == this.input.selectionEnd && - this.input.selectionEnd >= this.getLengthWithoutSuffix(this.input.value, decimalSeparator) && - (e.target.value[this.input.selectionEnd - 1] == decimalSeparator || - (e.target.value.indexOf(decimalSeparator) >= 0 && e.target.value[this.input.selectionEnd - 1] == "0") || - (this.input.selectionEnd == 2 && e.target.value[0] === "-" && e.target.value[1] === "0")) - ) - return; - - if (change != "blur") { - // Format, but keep the correct cursor position - let preCursorText = this.getPreCursorDigits(this.input.value, this.input.selectionStart, decimalSeparator); - this.input.value = formatted; - this.updateCursorPosition(preCursorText); - } else { - this.input.value = formatted; - } - } - - instance.set("value", value, { immediate }); - - instance.setState({ - inputError: false, - visited: true, - }); - } - - onFocus() { - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - } -} diff --git a/packages/cx/src/widgets/form/NumberField.scss b/packages/cx/src/widgets/form/NumberField.scss index 9b1df32eb..4185b9338 100644 --- a/packages/cx/src/widgets/form/NumberField.scss +++ b/packages/cx/src/widgets/form/NumberField.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-numberfield( $name: "numberfield", $state-style-map: $cx-std-field-state-style-map, @@ -10,9 +12,9 @@ $width: $cx-default-input-width, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $padding: cx-get-state-rule($state-style-map, default, "padding"); .#{$block}#{$name} { diff --git a/packages/cx/src/widgets/form/NumberField.tsx b/packages/cx/src/widgets/form/NumberField.tsx new file mode 100644 index 000000000..88c82ab53 --- /dev/null +++ b/packages/cx/src/widgets/form/NumberField.tsx @@ -0,0 +1,543 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, getContent } from "../../ui/Widget"; +import { Field, getFieldTooltip, FieldInstance, FieldConfig } from "./Field"; +import { Format } from "../../ui/Format"; +import { Culture } from "../../ui/Culture"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import { StringTemplate } from "../../data/StringTemplate"; +import { + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, + tooltipMouseMove, + tooltipMouseLeave, + tooltipParentDidMount, +} from "../overlay/tooltip-ops"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { Localization } from "../../ui/Localization"; +import ClearIcon from "../icons/clear"; +import { isString } from "../../util/isString"; +import { isNumber } from "../../util/isNumber"; +import { isDefined } from "../../util/isDefined"; +import { getActiveElement } from "../../util/getActiveElement"; + +import { enableCultureSensitiveFormatting } from "../../ui/Format"; +import { KeyCode } from "../../util/KeyCode"; +import { autoFocus } from "../autoFocus"; +import { Prop, NumberProp, StringProp, BooleanProp } from "../../ui/Prop"; + +enableCultureSensitiveFormatting(); + +export interface NumberFieldConfig extends FieldConfig { + value?: NumberProp; + readOnly?: BooleanProp; + enabled?: BooleanProp; + placeholder?: StringProp; + format?: StringProp; + minValue?: NumberProp; + minExclusive?: BooleanProp; + maxValue?: NumberProp; + maxExclusive?: BooleanProp; + constrain?: BooleanProp; + incrementPercentage?: NumberProp; + increment?: NumberProp; + step?: number; + baseClass?: string; + snapToIncrement?: boolean; + showClear?: boolean; + hideClear?: boolean; + alwaysShowClear?: boolean; + reactOn?: string; + inputType?: "text" | "password"; + maxValueErrorText?: string; + maxExclusiveErrorText?: string; + minValueErrorText?: string; + minExclusiveErrorText?: string; + inputErrorText?: string; + scale?: number; + offset?: number; + + /** Custom validation function. */ + onValidate?: string | ((value: number, instance: Instance, validationParams: Record) => unknown); +} + +export class NumberField extends Field { + declare public baseClass: string; + declare public reactOn: string; + declare public format: string; + declare public inputType: string; + declare public incrementPercentage: number; + declare public increment?: number; + declare public scale: number; + declare public offset: number; + declare public step?: number; + declare public hideClear?: boolean; + declare public showClear?: boolean; + declare public alwaysShowClear?: boolean; + declare public snapToIncrement?: boolean; + declare public onParseInput?: string | ((value: string, instance: Instance) => number | undefined); + declare public minValueErrorText: string; + declare public maxValueErrorText: string; + declare public minExclusiveErrorText: string; + declare public maxExclusiveErrorText: string; + declare public inputErrorText?: string; + + declareData(...args: Record[]): void { + super.declareData( + { + value: this.emptyValue, + disabled: undefined, + readOnly: undefined, + enabled: undefined, + placeholder: undefined, + required: undefined, + format: undefined, + minValue: undefined, + maxValue: undefined, + constrain: undefined, + minExclusive: undefined, + maxExclusive: undefined, + incrementPercentage: undefined, + increment: undefined, + icon: undefined, + scale: undefined, + offset: undefined, + }, + ...args, + ); + } + + init(): void { + if (isDefined(this.step)) this.increment = this.step; + + if (isDefined(this.hideClear)) this.showClear = !this.hideClear; + + if (this.alwaysShowClear) this.showClear = true; + + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + let { data, state, cached } = instance; + data.formatted = Format.value(data.value, data.format); + + if (!cached.data || data.value != cached.data.value) state.empty = data.value == null; + + super.prepareData(context, instance); + } + + formatValue(context: RenderingContext, { data }: FieldInstance): string | number | undefined { + return data.formatted; + } + + parseValue(value: string, instance: FieldInstance): number { + if (this.onParseInput) { + let result = instance.invoke("onParseInput", value, instance); + if (result !== undefined) return result; + } + return Culture.getNumberCulture().parse(value); + } + + validate(context: RenderingContext, instance: FieldInstance): void { + super.validate(context, instance); + + let { data } = instance; + if (isNumber(data.value) && !data.error) { + if (isNumber(data.minValue)) { + if (data.value < data.minValue) + data.error = StringTemplate.format(this.minValueErrorText, Format.value(data.minValue, data.format)); + else if (data.value == data.minValue && data.minExclusive) + data.error = StringTemplate.format(this.minExclusiveErrorText, Format.value(data.minValue, data.format)); + } + + if (isNumber(data.maxValue)) { + if (data.value > data.maxValue) + data.error = StringTemplate.format(this.maxValueErrorText, Format.value(data.maxValue, data.format)); + else if (data.value == data.maxValue && data.maxExclusive) + data.error = StringTemplate.format(this.maxExclusiveErrorText, Format.value(data.maxValue, data.format)); + } + } + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + return ( + + ); + } + + validateRequired(context: RenderingContext, instance: FieldInstance): string | undefined { + return instance.state.empty && this.requiredText; + } +} + +NumberField.prototype.baseClass = "numberfield"; +NumberField.prototype.reactOn = "enter blur"; +NumberField.prototype.format = "n"; +NumberField.prototype.inputType = "text"; + +NumberField.prototype.maxValueErrorText = "Enter {0} or less."; +NumberField.prototype.maxExclusiveErrorText = "Enter a number less than {0}."; +NumberField.prototype.minValueErrorText = "Enter {0} or more."; +NumberField.prototype.minExclusiveErrorText = "Enter a number greater than {0}."; +NumberField.prototype.inputErrorText = "Invalid number entered."; +NumberField.prototype.suppressErrorsUntilVisited = true; + +NumberField.prototype.incrementPercentage = 0.1; +NumberField.prototype.scale = 1; +NumberField.prototype.offset = 0; +NumberField.prototype.snapToIncrement = true; +NumberField.prototype.icon = null; +NumberField.prototype.showClear = false; +NumberField.prototype.alwaysShowClear = false; + +Widget.alias("numberfield", NumberField); +Localization.registerPrototype("cx/widgets/NumberField", NumberField); + +interface InputProps { + instance: FieldInstance; + data: Record; + label?: React.ReactNode; + help?: React.ReactNode; + icon?: React.ReactNode; +} + +interface InputState { + focus: boolean; +} + +class Input extends VDOM.Component { + input?: HTMLInputElement; + + constructor(props: InputProps) { + super(props); + this.state = { + focus: false, + }; + } + + render(): React.ReactNode { + let { data, instance, label, help, icon: iconVDOM } = this.props; + let { widget, state } = instance; + let { CSS, baseClass, suppressErrorsUntilVisited } = widget; + + let icon = iconVDOM &&
    {iconVDOM}
    ; + + let insideButton; + if (!data.readOnly && !data.disabled) { + if ( + widget.showClear && + (((widget.alwaysShowClear || !data.required) && !data.empty) || instance.state.inputError) + ) + insideButton = ( +
    e.preventDefault()} + onClick={(e) => this.onClearClick(e)} + > + +
    + ); + } + + let empty = this.input ? !this.input.value : data.empty; + + return ( +
    + { + this.input = el || undefined; + }} + style={data.inputStyle} + disabled={data.disabled} + readOnly={data.readOnly} + tabIndex={data.tabIndex} + placeholder={data.placeholder} + {...data.inputAttrs} + onMouseMove={(e: React.MouseEvent) => + tooltipMouseMove(e, ...getFieldTooltip(this.props.instance)) + } + onMouseLeave={(e: React.MouseEvent) => + tooltipMouseLeave(e, ...getFieldTooltip(this.props.instance)) + } + onChange={(e: React.ChangeEvent) => this.onChange(e, "change")} + onKeyDown={this.onKeyDown.bind(this)} + onBlur={(e: React.FocusEvent) => { + this.onChange(e, "blur"); + }} + onFocus={(e: React.FocusEvent) => this.onFocus()} + onWheel={(e: React.WheelEvent) => { + this.onChange(e, "wheel"); + }} + onClick={stopPropagation} + /> + {insideButton} + {icon} + {label} + {help} +
    + ); + } + + UNSAFE_componentWillReceiveProps(props: InputProps): void { + let { data, state } = props.instance; + if (this.props.data.formatted != data.formatted && !state.inputError) { + this.input!.value = props.data.formatted || ""; + props.instance.setState({ + inputError: false, + }); + } + tooltipParentWillReceiveProps(this.input!, ...getFieldTooltip(props.instance)); + } + + componentDidMount(): void { + tooltipParentDidMount(this.input!, ...getFieldTooltip(this.props.instance)); + autoFocus(this.input, this); + } + + componentDidUpdate(): void { + autoFocus(this.input, this); + } + + componentWillUnmount(): void { + if (this.input == getActiveElement() && this.input.value != this.props.data.formatted) { + this.onChange({ currentTarget: this.input } as React.SyntheticEvent, "blur"); + } + tooltipParentWillUnmount(this.props.instance); + } + + getPreCursorDigits(text: string, cursor: number, decimalSeparator: string): string { + let res = ""; + for (let i = 0; i < cursor; i++) { + if ("0" <= text[i] && text[i] <= "9") res += text[i]; + else if (text[i] == decimalSeparator) res += "."; + else if (text[i] == "-") res += "-"; + } + return res; + } + + getLengthWithoutSuffix(text: string, decimalSeparator: string): number { + let l = text.length; + while (l > 0) { + if ("0" <= text[l - 1] && text[l - 1] <= "9") break; + if (text[l - 1] == decimalSeparator) break; + l--; + } + return l; + } + + getDecimalSeparator(format: string): string | null { + let text = Format.value(0.11111111, format); + for (let i = text.length - 1; i >= 0; i--) { + if ("0" <= text[i] && text[i] <= "9") continue; + if (text[i] == "," || text[i] == ".") return text[i]; + break; + } + return null; + } + + updateCursorPosition(preCursorText: string | undefined): void { + if (isString(preCursorText)) { + let cursor = 0; + let preCursor = 0; + let text = this.input!.value || ""; + while (preCursor < preCursorText.length && cursor < text.length) { + if (text[cursor] == preCursorText[preCursor]) { + cursor++; + preCursor++; + } else { + cursor++; + } + } + this.input!.setSelectionRange(cursor, cursor); + } + } + + calculateIncrement(value: number, strength: number): number { + if (value == 0) return 0.1; + let absValue = Math.abs(value * strength); + let log10 = Math.floor(Math.log10(absValue) + 0.001); + let size = Math.pow(10, log10); + if (absValue / size > 4.999) return 5 * size; + if (absValue / size > 1.999) return 2 * size; + return size; + } + + onClearClick(e: React.MouseEvent): void { + this.input!.value = ""; + let { instance } = this.props; + instance.set("value", instance.widget.emptyValue, { immediate: true }); + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + if (instance.widget.handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.enter: + this.onChange(e, "enter"); + break; + + case KeyCode.left: + case KeyCode.right: + e.stopPropagation(); + break; + } + } + + onChange(e: React.SyntheticEvent | React.WheelEvent, change: string): void { + let { instance, data } = this.props; + let { widget } = instance; + let inputValue = e.currentTarget.value; + + if (data.required) { + instance.setState({ + empty: !inputValue, + }); + } + + if (change == "blur" && this.state.focus) { + this.setState({ + focus: false, + }); + } + + let immediate = change == "blur" || change == "enter"; + + if ((widget.reactOn.indexOf(change) == -1 && !immediate) || data.disabled || data.readOnly) return; + + if (immediate) instance.setState({ visited: true }); + + let value = null; + + if (inputValue) { + let displayValue = widget.parseValue(inputValue, instance); + if (isNaN(displayValue)) { + instance.setState({ + inputError: instance.widget.inputErrorText, + }); + return; + } + + value = displayValue * data.scale + data.offset; + + if (change == "wheel") { + e.preventDefault(); + let increment = + data.increment != null ? data.increment : this.calculateIncrement(value, data.incrementPercentage); + let deltaY = (e as React.WheelEvent).deltaY; + value = value + (deltaY < 0 ? increment : -increment); + if (widget.snapToIncrement) { + value = Math.round(value / increment) * increment; + } + + if (data.minValue != null) { + if (data.minExclusive) { + if (value <= data.minValue) return; + } else { + value = Math.max(value, data.minValue); + } + } + + if (data.maxValue != null) { + if (data.maxExclusive) { + if (value >= data.maxValue) return; + } else { + value = Math.min(value, data.maxValue); + } + } + } + + if (data.constrain) { + if (data.minValue != null) { + if (value < data.minValue) { + value = data.minValue; + } + } + + if (data.maxValue != null) { + if (value > data.maxValue) { + value = data.maxValue; + } + } + } + + let fmt = data.format; + let decimalSeparator = this.getDecimalSeparator(fmt) || Format.value(1.1, "n;1")[1]; + + let formatted = Format.value(value, fmt); + // Re-parse to avoid differences between formatted value and value in the store + + value = widget.parseValue(formatted, instance) * data.scale + data.offset; + + // Allow users to type numbers like 100.0003 or -0.05 without interruptions + // If the last typed character is zero or dot (decimal separator), skip processing it + let selectionEnd = this.input!.selectionEnd; + if ( + change == "change" && + this.input!.selectionStart == selectionEnd && + selectionEnd != null && + selectionEnd >= this.getLengthWithoutSuffix(this.input!.value, decimalSeparator) && + (inputValue[selectionEnd - 1] == decimalSeparator || + (inputValue.indexOf(decimalSeparator) >= 0 && inputValue[selectionEnd - 1] == "0") || + (selectionEnd == 2 && inputValue[0] === "-" && inputValue[1] === "0")) + ) + return; + + if (change != "blur") { + // Format, but keep the correct cursor position + let preCursorText = this.getPreCursorDigits( + this.input!.value, + this.input!.selectionStart!, + decimalSeparator, + ); + this.input!.value = formatted; + this.updateCursorPosition(preCursorText); + } else { + this.input!.value = formatted; + } + } + + instance.set("value", value, { immediate }); + + instance.setState({ + inputError: false, + visited: true, + }); + } + + onFocus(): void { + let { instance } = this.props; + let { widget } = instance; + if (widget.trackFocus) { + this.setState({ + focus: true, + }); + } + } +} diff --git a/packages/cx/src/widgets/form/Radio.d.ts b/packages/cx/src/widgets/form/Radio.d.ts deleted file mode 100644 index 331443c57..000000000 --- a/packages/cx/src/widgets/form/Radio.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as Cx from '../../core'; -import { FieldProps } from './Field'; - -interface RadioProps extends FieldProps { - - /** Selected value. If the value is equal to `option`, the radio button appears checked. */ - value?: Cx.Prop< number | string | boolean >, - - /** Selected value. If the value is equal to `option`, the radio button appears checked. */ - selection?: Cx.Prop< number | string | boolean >, - - /** Value to be written into `value` if radio button is clicked. */ - option?: Cx.Prop< number | string | boolean >, - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp, - - /** Text description. */ - text?: Cx.StringProp, - - /** Base CSS class to be applied to the field. Defaults to `radio`. */ - baseClass?: string, - - /** - * Use native radio HTML element (``). - * Default is `false`. Native radio buttons are difficult to style. - */ - native?: boolean, - - /** - * Set to `true` to set the make the radio initially selected. - */ - default?: boolean - -} - -export class Radio extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Radio.js b/packages/cx/src/widgets/form/Radio.js deleted file mode 100644 index a7fc1d69f..000000000 --- a/packages/cx/src/widgets/form/Radio.js +++ /dev/null @@ -1,188 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { tooltipMouseMove, tooltipMouseLeave } from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { KeyCode } from "../../util/KeyCode"; -import { isUndefined } from "../../util/isUndefined"; - -export class Radio extends Field { - declareData() { - super.declareData( - { - value: undefined, - selection: undefined, - option: undefined, - disabled: undefined, - enabled: undefined, - readOnly: undefined, - required: undefined, - text: undefined, - }, - ...arguments - ); - } - - init() { - if (this.selection) this.value = this.selection; - - super.init(); - } - - formatValue(context, { data }) { - return data.text; - } - - prepareData(context, instance) { - super.prepareData(...arguments); - let { data } = instance; - data.checked = data.value === data.option; - if (this.default && isUndefined(data.value)) instance.set("value", data.option); - } - - renderValue(context, { data }) { - if (data.value === data.option) return super.renderValue(...arguments); - return null; - } - - renderWrap(context, instance, key, content) { - var { data } = instance; - return ( - - ); - } - - renderNativeCheck(context, instance) { - var { CSS, baseClass } = this; - var { data } = instance; - return ( - { - this.handleChange(e, instance); - }} - /> - ); - } - - renderCheck(context, instance) { - return ; - } - - renderInput(context, instance, key) { - var { data } = instance; - var text = data.text || this.renderChildren(context, instance); - var { CSS, baseClass } = this; - return this.renderWrap(context, instance, key, [ - this.native ? this.renderNativeCheck(context, instance) : this.renderCheck(context, instance), - text ? ( -
    - {text} -
    - ) : ( - -   - - ), - ]); - } - - handleClick(e, instance) { - if (this.native) e.stopPropagation(); - else { - var el = document.getElementById(instance.data.id); - if (el) el.focus(); - e.preventDefault(); - this.handleChange(e, instance); - } - } - - handleChange(e, instance) { - var { data } = instance; - if (data.disabled || data.readOnly || data.viewMode) return; - instance.set("value", data.option); - } -} - -Radio.prototype.baseClass = "radio"; -Radio.prototype.native = false; -Radio.prototype.default = false; - -Widget.alias("radio", Radio); - -class RadioCmp extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - value: props.data.checked, - }; - } - - UNSAFE_componentWillReceiveProps(props) { - this.setState({ - value: props.data.checked, - }); - } - - render() { - var { instance, data } = this.props; - var { widget } = instance; - var { baseClass, CSS } = widget; - - return ( - - ); - } - - onClick(e) { - var { instance, data } = this.props; - var { widget } = instance; - if (!data.disabled && !data.readOnly) { - e.stopPropagation(); - e.preventDefault(); - widget.handleChange(e, instance); - } - } - - onKeyDown(e) { - let { instance } = this.props; - if (instance.widget.handleKeyDown(e, instance) === false) return; - - switch (e.keyCode) { - case KeyCode.space: - this.onClick(e); - break; - } - } -} diff --git a/packages/cx/src/widgets/form/Radio.scss b/packages/cx/src/widgets/form/Radio.scss index 5d3f9dc2f..29f539ef1 100644 --- a/packages/cx/src/widgets/form/Radio.scss +++ b/packages/cx/src/widgets/form/Radio.scss @@ -1,12 +1,14 @@ +@use "sass:map"; + @mixin cx-radio( $name: "radio", $state-style-map: $cx-radio-state-style-map, $size: $cx-default-radio-size, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $padding: cx-get-state-rule($state-style-map, default, padding); $border-width: cx-get-state-rule($state-style-map, default, border-width); diff --git a/packages/cx/src/widgets/form/Radio.tsx b/packages/cx/src/widgets/form/Radio.tsx new file mode 100644 index 000000000..c4cf05c84 --- /dev/null +++ b/packages/cx/src/widgets/form/Radio.tsx @@ -0,0 +1,249 @@ +/** @jsxImportSource react */ +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import { Widget, VDOM, getContent } from "../../ui/Widget"; +import { Field, FieldConfig, getFieldTooltip, FieldInstance } from "./Field"; +import { tooltipMouseMove, tooltipMouseLeave } from "../overlay/tooltip-ops"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { KeyCode } from "../../util/KeyCode"; +import { isUndefined } from "../../util/isUndefined"; +import { BooleanProp, Prop, StringProp } from "../../ui/Prop"; + +export interface RadioConfig extends FieldConfig { + /** Selected value. If the value is equal to `option`, the radio button appears checked. */ + value?: Prop; + + /** Selected value. If the value is equal to `option`, the radio button appears checked. */ + selection?: Prop; + + /** Value to be written into `value` if radio button is clicked. */ + option?: Prop; + + /** Defaults to `false`. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** Text description. */ + text?: StringProp; + + /** Base CSS class to be applied to the field. Defaults to `radio`. */ + baseClass?: string; + + /** Use native radio HTML element. Default is `false`. */ + native?: boolean; + + /** Set to `true` to make the radio initially selected. */ + default?: boolean; + + /** Custom validation function. */ + onValidate?: + | string + | ((value: number | string | boolean, instance: Instance, validationParams: Record) => unknown); +} + +export class Radio extends Field { + declare public selection?: Prop; + declare public option?: Prop; + declare public native?: boolean; + declare public default?: boolean; + declare public value?: Prop; + + constructor(config?: RadioConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + value: undefined, + selection: undefined, + option: undefined, + disabled: undefined, + enabled: undefined, + readOnly: undefined, + required: undefined, + text: undefined, + }, + ...args, + ); + } + + init(): void { + if (this.selection) this.value = this.selection; + + super.init(); + } + + formatValue(context: RenderingContext, { data }: Instance): React.ReactNode { + return data.text; + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + super.prepareData(context, instance); + let { data } = instance; + data.checked = data.value === data.option; + if (this.default && isUndefined(data.value)) instance.set("value", data.option); + } + + renderValue(context: RenderingContext, { data }: FieldInstance): React.ReactNode { + if (data.value === data.option) return super.renderValue(context, { data } as FieldInstance); + return null; + } + + renderWrap( + context: RenderingContext, + instance: FieldInstance, + key: string, + content: React.ReactNode, + ): React.ReactElement { + var { data } = instance; + return ( + + ); + } + + renderNativeCheck(context: RenderingContext, instance: Instance): React.ReactElement { + var { CSS, baseClass } = this; + var { data } = instance; + return ( + ) => { + this.handleChange(e, instance); + }} + /> + ); + } + + renderCheck(context: RenderingContext, instance: FieldInstance): React.ReactElement { + return ; + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactElement { + var { data } = instance; + var text = data.text || this.renderChildren(context, instance); + var { CSS, baseClass } = this; + return this.renderWrap(context, instance, key, [ + this.native ? this.renderNativeCheck(context, instance) : this.renderCheck(context, instance), + text ? ( +
    + {text} +
    + ) : ( + +   + + ), + ]); + } + + handleClick(e: React.MouseEvent, instance: Instance): void { + if (this.native) e.stopPropagation(); + else { + var el = document.getElementById(instance.data.id); + if (el) el.focus(); + e.preventDefault(); + this.handleChange(e, instance); + } + } + + handleChange(e: React.ChangeEvent | React.MouseEvent, instance: Instance): void { + var { data } = instance; + if (data.disabled || data.readOnly || data.viewMode) return; + instance.set("value", data.option); + } +} + +Radio.prototype.baseClass = "radio"; +Radio.prototype.native = false; +Radio.prototype.default = false; + +Widget.alias("radio", Radio); + +interface RadioCmpProps { + key?: string; + instance: FieldInstance; + data: Record; +} + +interface RadioCmpState { + value: unknown; +} + +class RadioCmp extends VDOM.Component { + constructor(props: RadioCmpProps) { + super(props); + this.state = { + value: props.data.checked, + }; + } + + UNSAFE_componentWillReceiveProps(props: RadioCmpProps): void { + this.setState({ + value: props.data.checked, + }); + } + + render(): React.ReactElement { + var { instance, data } = this.props; + var { widget } = instance; + var { baseClass, CSS } = widget; + + return ( + + ); + } + + onClick(e: React.MouseEvent): void { + var { instance, data } = this.props; + var { widget } = instance; + if (!data.disabled && !data.readOnly) { + e.stopPropagation(); + e.preventDefault(); + (widget as Radio).handleChange(e, instance); + } + } + + onKeyDown(e: React.KeyboardEvent): void { + let { instance } = this.props; + const widget = instance.widget as Radio; + if (widget.handleKeyDown && widget.handleKeyDown(e, instance) === false) return; + + switch (e.keyCode) { + case KeyCode.space: + this.onClick(e as unknown as React.MouseEvent); + break; + } + } +} diff --git a/packages/cx/src/widgets/form/Select.d.ts b/packages/cx/src/widgets/form/Select.d.ts deleted file mode 100644 index 4de1ba5fa..000000000 --- a/packages/cx/src/widgets/form/Select.d.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as Cx from "../../core"; -import { FieldProps } from "./Field"; - -interface SelectProps extends FieldProps { - /** Select value. */ - value?: Cx.Prop; - - /** Value when no selection is made. Default is `undefined` */ - emptyValue?: any; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Default text displayed when the field is empty. */ - placeholder?: Cx.StringProp; - - /** - * Set to `true` to hide the clear button. It can be used interchangeably with the `showClear` property. Default value is `false`. - * Note, the `placeholder` needs to be specified for the clear button to render. - */ - hideClear?: boolean; - - /** - * Set to `false` to hide the clear button. It can be used interchangeably with the `hideClear` property. Default value is `true`. - * Note, the `placeholder` needs to be specified for the clear button to render. - */ - showClear?: boolean; - - /** - * Set to `true` to display the clear button even if `required` is set. Default is `false`. - */ - alwaysShowClear?: boolean; - - /** Base CSS class to be applied to the element. Defaults to `select`. */ - baseClass?: string; - - /** Defaults to `false`. Set to `true` to enable multiple selection. */ - multiple?: boolean; - - /** - * Convert values before selection. - * Useful for converting strings into numbers. Default is `true`. - */ - convertValues?: boolean; - - /** String value used to indicate a `null` entry */ - nullString?: string; - - /** Name or configuration of the icon to be put on the left side of the input. */ - icon?: Cx.StringProp | Cx.Record; -} - -export class Select extends Cx.Widget {} - -interface OptionProps extends Cx.HtmlElementProps { - /** Value property. */ - value?: Cx.StringProp; - - /** Defaults to `false`. Set to `true` to disable the field. */ - disabled?: Cx.BooleanProp; - - /** The opposite of `disabled`. */ - enabled?: Cx.BooleanProp; - - /** Defaults to `false`. Set to `true` to select the the option. */ - selected?: Cx.BooleanProp; -} - -export class Option extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Select.js b/packages/cx/src/widgets/form/Select.js deleted file mode 100644 index 7c636197b..000000000 --- a/packages/cx/src/widgets/form/Select.js +++ /dev/null @@ -1,269 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { HtmlElement } from "../HtmlElement"; -import { Field, getFieldTooltip } from "./Field"; -import { - tooltipParentWillReceiveProps, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { stopPropagation, preventDefault } from "../../util/eventCallbacks"; -import DropdownIcon from "../icons/drop-down"; -import ClearIcon from "../icons/clear"; -import { Localization } from "../../ui/Localization"; -import { isString } from "../../util/isString"; -import { isDefined } from "../../util/isDefined"; -import { KeyCode } from "../../util/KeyCode"; -import { autoFocus } from "../autoFocus"; - -export class Select extends Field { - declareData() { - super.declareData( - { - value: undefined, - disabled: undefined, - enabled: undefined, - required: undefined, - placeholder: undefined, - icon: undefined, - }, - ...arguments, - ); - } - - init() { - if (isDefined(this.hideClear)) this.showClear = !this.hideClear; - if (this.alwaysShowClear) this.showClear = true; - super.init(); - } - - renderInput(context, instance, key) { - return ( - this.select(v, instance)} - label={this.labelPlacement && getContent(this.renderLabel(context, instance, "label"))} - help={this.helpPlacement && getContent(this.renderHelp(context, instance, "help"))} - icon={this.renderIcon(context, instance, "icon")} - > - {this.renderChildren(context, instance)} - - ); - } - - convert(value) { - if (value == this.nullString) return null; - if (value == "true") return true; - if (value == "false") return false; - if (value.match(/^\d+(\.\d+)?$/)) return Number(value); - return value; - } - - select(value, instance) { - if (this.convertValues && value != null) value = this.convert(value); - instance.set("value", value); - } - - add(item) { - if (isString(item)) return; - super.add(item); - } -} - -Select.prototype.baseClass = "select"; -Select.prototype.multiple = false; -Select.prototype.convertValues = true; -Select.prototype.nullString = ""; -Select.prototype.suppressErrorsUntilVisited = true; -Select.prototype.showClear = true; -Select.prototype.alwaysShowClear = false; -Select.prototype.icon = null; - -Widget.alias("select", Select); -Localization.registerPrototype("cx/widgets/Select", Select); - -class SelectComponent extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - visited: false, - focus: false, - }; - } - - render() { - let { multiple, select, instance, label, help, icon: iconVDOM } = this.props; - let { data, widget, state } = instance; - let { CSS, baseClass } = widget; - - let icon = iconVDOM &&
    {iconVDOM}
    ; - - let insideButton, - readOnly = data.disabled || data.readOnly; - - if ( - widget.showClear && - !readOnly && - !this.props.multiple && - (widget.alwaysShowClear || !data.required) && - data.placeholder && - !data.empty - ) { - insideButton = ( -
    this.onClearClick(e)} - className={CSS.element(baseClass, "clear")} - > - -
    - ); - } else { - insideButton = ( -
    - -
    - ); - } - - let placeholder; - if (data.placeholder) { - placeholder = ( - - ); - } - - return ( -
    - - {insideButton} - {icon} - {label} - {help} -
    - ); - } - - onBlur() { - this.props.instance.setState({ visited: true }); - if (this.state.focus) - this.setState({ - focus: false, - }); - } - - onFocus() { - let { instance } = this.props; - let { widget } = instance; - if (widget.trackFocus) { - this.setState({ - focus: true, - }); - } - } - - onClearClick(e) { - e.preventDefault(); - e.stopPropagation(); - let { instance } = this.props; - let { widget } = instance; - instance.set("value", widget.emptyValue); - } - - onKeyDown(e) { - switch (e.keyCode) { - case KeyCode.up: - case KeyCode.down: - e.stopPropagation(); - break; - } - } - - componentDidMount() { - var { select } = this.props; - select(this.select.value); - tooltipParentDidMount(this.select, ...getFieldTooltip(this.props.instance)); - autoFocus(this.select, this); - } - - componentDidUpdate() { - autoFocus(this.select, this); - } - - UNSAFE_componentWillReceiveProps(props) { - tooltipParentWillReceiveProps(this.select, ...getFieldTooltip(props.instance)); - } -} - -export class Option extends HtmlElement { - declareData() { - super.declareData( - { - value: undefined, - disabled: undefined, - enabled: undefined, - selected: undefined, - text: undefined, - }, - ...arguments, - ); - } - - prepareData(context, { data }) { - super.prepareData(...arguments); - if (!data.empty) data.value = data.value.toString(); - } - - render(context, instance, key) { - var { data } = instance; - return ( - - ); - } -} - -Widget.alias("option", Option); diff --git a/packages/cx/src/widgets/form/Select.scss b/packages/cx/src/widgets/form/Select.scss index 401fbc622..cf3007e8d 100644 --- a/packages/cx/src/widgets/form/Select.scss +++ b/packages/cx/src/widgets/form/Select.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @import "../overlay/Dropdown"; @mixin cx-select( @@ -12,10 +14,10 @@ $icon-size: $cx-default-input-icon-size, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); - $mod: map-get($besm, mod); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); + $mod: map.get($besm, mod); .#{$block}#{$name} { @include cxb-field($besm, $state-style-map: $state-style-map, $width: $width, $input: true); diff --git a/packages/cx/src/widgets/form/Select.tsx b/packages/cx/src/widgets/form/Select.tsx new file mode 100644 index 000000000..65ddc2157 --- /dev/null +++ b/packages/cx/src/widgets/form/Select.tsx @@ -0,0 +1,327 @@ +/** @jsxImportSource react */ + +import { Widget, VDOM, getContent } from "../../ui/Widget"; +import { HtmlElement, HtmlElementInstance } from "../HtmlElement"; +import { Field, getFieldTooltip, FieldInstance } from "./Field"; +import { + tooltipParentWillReceiveProps, + tooltipMouseMove, + tooltipMouseLeave, + tooltipParentDidMount, +} from "../overlay/tooltip-ops"; +import { stopPropagation, preventDefault } from "../../util/eventCallbacks"; +import DropdownIcon from "../icons/drop-down"; +import ClearIcon from "../icons/clear"; +import { Localization } from "../../ui/Localization"; +import { isString } from "../../util/isString"; +import { isDefined } from "../../util/isDefined"; +import { KeyCode } from "../../util/KeyCode"; +import { autoFocus } from "../autoFocus"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import { FieldConfig } from "./Field"; +import { Prop, StringProp, BooleanProp } from "../../ui/Prop"; + +export interface SelectConfig extends FieldConfig { + value?: Prop; + emptyValue?: unknown; + enabled?: BooleanProp; + placeholder?: StringProp; + hideClear?: boolean; + showClear?: boolean; + alwaysShowClear?: boolean; + baseClass?: string; + multiple?: boolean; + convertValues?: boolean; + nullString?: string; + + /** Custom validation function. */ + onValidate?: + | string + | ((value: number | string, instance: Instance, validationParams: Record) => unknown); +} + +export class Select extends Field { + declare public baseClass: string; + declare public hideClear?: boolean; + declare public showClear: boolean; + declare public alwaysShowClear: boolean; + declare public multiple: boolean; + declare public convertValues: boolean; + declare public nullString: string; + + declareData(...args: Record[]): void { + super.declareData( + { + value: undefined, + disabled: undefined, + enabled: undefined, + required: undefined, + placeholder: undefined, + icon: undefined, + }, + ...args, + ); + } + + init(): void { + if (isDefined(this.hideClear)) this.showClear = !this.hideClear; + if (this.alwaysShowClear) this.showClear = true; + super.init(); + } + + renderInput(context: RenderingContext, instance: FieldInstance; + multiple: boolean; + select: (value: string) => void; + label?: React.ReactNode; + help?: React.ReactNode; + icon?: React.ReactNode; + children?: React.ReactNode; +} + +interface SelectComponentState { + visited: boolean; + focus: boolean; +} + +class SelectComponent extends VDOM.Component { + select: HTMLSelectElement | null = null; + + constructor(props: SelectComponentProps) { + super(props); + this.state = { + visited: false, + focus: false, + }; + } + + render(): React.ReactNode { + let { multiple, select, instance, label, help, icon: iconVDOM } = this.props; + let { data, widget, state } = instance; + let { CSS, baseClass } = widget; + + let icon = iconVDOM &&
    {iconVDOM}
    ; + + let insideButton, + readOnly = data.disabled || data.readOnly; + + if ( + widget.showClear && + !readOnly && + !this.props.multiple && + (widget.alwaysShowClear || !data.required) && + data.placeholder && + !data.empty + ) { + insideButton = ( +
    this.onClearClick(e)} + className={CSS.element(baseClass, "clear")} + > + +
    + ); + } else { + insideButton = ( +
    + +
    + ); + } + + let placeholder; + if (data.placeholder) { + placeholder = ( + + ); + } + + return ( +
    + + {insideButton} + {icon} + {label} + {help} +
    + ); + } + + onBlur(): void { + this.props.instance.setState({ visited: true }); + if (this.state.focus) + this.setState({ + focus: false, + }); + } + + onFocus(): void { + let { instance } = this.props; + let { widget } = instance; + if (widget.trackFocus) { + this.setState({ + focus: true, + }); + } + } + + onClearClick(e: React.MouseEvent): void { + e.preventDefault(); + e.stopPropagation(); + let { instance } = this.props; + let { widget } = instance; + instance.set("value", widget.emptyValue); + } + + onKeyDown(e: React.KeyboardEvent): void { + switch (e.keyCode) { + case KeyCode.up: + case KeyCode.down: + e.stopPropagation(); + break; + } + } + + componentDidMount(): void { + const { select } = this.props; + if (this.select) { + select(this.select.value); + tooltipParentDidMount(this.select, ...getFieldTooltip(this.props.instance)); + autoFocus(this.select, this); + } + } + + componentDidUpdate(): void { + if (this.select) { + autoFocus(this.select, this); + } + } + + UNSAFE_componentWillReceiveProps(props: SelectComponentProps): void { + if (this.select) { + tooltipParentWillReceiveProps(this.select, ...getFieldTooltip(props.instance)); + } + } +} + +export class Option extends HtmlElement { + declareData(...args: Record[]): void { + super.declareData( + { + value: undefined, + disabled: undefined, + enabled: undefined, + selected: undefined, + text: undefined, + }, + ...args, + ); + } + + prepareData(context: RenderingContext, instance: HtmlElementInstance): void { + super.prepareData(context, instance); + const { data } = instance; + if (!data.empty) data.value = data.value.toString(); + } + + render(context: RenderingContext, instance: HtmlElementInstance, key: string): React.ReactNode { + const { data } = instance; + return ( + + ); + } +} + +Widget.alias("option", Option); diff --git a/packages/cx/src/widgets/form/Slider.d.ts b/packages/cx/src/widgets/form/Slider.d.ts deleted file mode 100644 index d65bd5b7d..000000000 --- a/packages/cx/src/widgets/form/Slider.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as Cx from '../../core'; -import { FieldProps } from './Field'; - -interface SliderProps extends FieldProps { - - /** Low value of the slider range. */ - from?: Cx.NumberProp, - - /** High value of the slider range. */ - to?: Cx.NumberProp, - - /** Rounding step. */ - step?: Cx.NumberProp, - - /** Minimum allowed value. Default is `0`. */ - minValue?: Cx.NumberProp, - - /** Maximum allowed value. Default is `100`. */ - maxValue?: Cx.NumberProp, - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp, - - /** Style object to be applied on the selected axis range. */ - rangeStyle?: Cx.StyleProp, - - /** Style object to be applied on the handle. */ - handleStyle?: Cx.StyleProp, - - /** Minimum allowed value. Default is `0`. */ - min?: Cx.NumberProp, - - /** Maximum allowed value. Default is `100`. */ - max?: Cx.NumberProp, - - /** High value of the slider range. */ - value?: Cx.NumberProp, - - /** Base CSS class to be applied to the field. Defaults to `slider`. */ - baseClass?: string, - - /** Set to `true` to orient the slider vertically. */ - vertical?: boolean, - - /** Invert vertical slider behavior. Set this to `true` if you want the slider to go from `top` to `bottom`. */ - invert?: boolean, - - /** Range tooltip configuration. */ - toTooltip?: Cx.StringProp | Cx.StructuredProp, - - /** Range tooltip configuration. */ - valueTooltip?: Cx.StringProp | Cx.StructuredProp, - - /** Range tooltip configuration. */ - fromTooltip?: Cx.StringProp | Cx.StructuredProp, - - /** When set to `true`, slider respondes to mouse wheel events, while hovering it. It will not work if both `from` and `to` values are used. Default value is `false`. */ - wheel?: Cx.BooleanProp, - - /** Value increment/decrement, when controlling the slider with mouse wheel. Default value is set to `1%` of range. */ - increment?: Cx.NumberProp - -} - -export class Slider extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Slider.js b/packages/cx/src/widgets/form/Slider.js deleted file mode 100644 index 7b59c8f02..000000000 --- a/packages/cx/src/widgets/form/Slider.js +++ /dev/null @@ -1,351 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { captureMouseOrTouch, getCursorPos } from "../overlay/captureMouse"; -import { isUndefined } from "../../util/isUndefined"; -import { isDefined } from "../../util/isDefined"; -import { isArray } from "../../util/isArray"; -import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; -import { addEventListenerWithOptions } from "../../util/addEventListenerWithOptions"; - -export class Slider extends Field { - declareData() { - super.declareData( - { - from: 0, - to: 0, - step: undefined, - minValue: undefined, - maxValue: undefined, - increment: undefined, - incrementPercentage: undefined, - wheel: undefined, - disabled: undefined, - enabled: undefined, - readOnly: undefined, - rangeStyle: { - structured: true, - }, - handleStyle: { - structured: true, - }, - invert: false, - }, - ...arguments - ); - } - - init() { - if (isDefined(this.min)) this.minValue = this.min; - - if (isDefined(this.max)) this.maxValue = this.max; - - if (this.value != null) this.to = this.value; - - if (isUndefined(this.from)) this.from = this.minValue; - else this.showFrom = true; - - if (isUndefined(this.to)) this.to = this.maxValue; - else this.showTo = true; - - if (this.valueTooltip) this.toTooltip = this.valueTooltip; - - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - data.stateMods = { - ...data.stateMods, - horizontal: !this.vertical, - vertical: this.vertical, - disabled: data.disabled, - }; - super.prepareData(context, instance); - } - - renderInput(context, instance, key) { - return ( - - ); - } -} - -Slider.prototype.baseClass = "slider"; -Slider.prototype.minValue = 0; -Slider.prototype.maxValue = 100; -Slider.prototype.vertical = false; -Slider.prototype.incrementPercentage = 0.01; -Slider.prototype.wheel = false; -Slider.prototype.invert = false; - -Widget.alias("slider", Slider); - -class SliderComponent extends VDOM.Component { - constructor(props) { - super(props); - this.dom = {}; - let { data } = props; - this.state = { - from: data.from, - to: data.to, - }; - } - - render() { - let { instance, data, label } = this.props; - let { widget } = instance; - let { CSS, baseClass } = widget; - let { minValue, maxValue } = data; - let { from, to } = this.state; - - from = Math.min(maxValue, Math.max(minValue, from)); - to = Math.min(maxValue, Math.max(minValue, to)); - - let handleStyle = CSS.parseStyle(data.handleStyle); - let anchor = widget.vertical ? (widget.invert ? "top" : "bottom") : "left"; - let rangeStart = from - minValue; - let rangeSize = to - from; - - let fromHandleStyle = { - ...handleStyle, - [anchor]: `${(100 * (from - minValue)) / (maxValue - minValue)}%`, - }; - - let toHandleStyle = { - ...handleStyle, - [anchor]: `${(100 * (to - minValue)) / (maxValue - minValue)}%`, - }; - - let rangeStyle = { - ...CSS.parseStyle(data.rangeStyle), - [anchor]: `${(100 * rangeStart) / (maxValue - minValue)}%`, - [widget.vertical ? "height" : "width"]: `${(100 * rangeSize) / (maxValue - minValue)}%`, - }; - - return ( -
    this.onClick(e)} - ref={(el) => (this.dom.el = el)} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(instance))} - > - {label}  -
    - {rangeSize > 0 &&
    } -
    (this.dom.range = c)}> - {widget.showFrom && ( -
    this.onHandleMouseDown(e, "from")} - onMouseMove={(e) => - tooltipMouseMove(e, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }) - } - onMouseLeave={(e) => this.onHandleMouseLeave(e, "from")} - onTouchStart={(e) => this.onHandleMouseDown(e, "from")} - ref={(c) => (this.dom.from = c)} - /> - )} - {widget.showTo && ( -
    this.onHandleMouseDown(e, "to")} - onMouseMove={(e) => - tooltipMouseMove(e, instance, widget.toTooltip, { tooltipName: "toTooltip" }) - } - onMouseLeave={(e) => this.onHandleMouseLeave(e, "to")} - onTouchStart={(e) => this.onHandleMouseDown(e, "to")} - ref={(c) => (this.dom.to = c)} - /> - )} -
    -
    -
    - ); - } - - UNSAFE_componentWillReceiveProps(props) { - this.setState({ - from: props.data.from, - to: props.data.to, - }); - - let { instance } = props; - let { widget } = instance; - tooltipParentWillReceiveProps(this.dom.to, instance, widget.toTooltip, { tooltipName: "toTooltip" }); - tooltipParentWillReceiveProps(this.dom.from, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }); - } - - componentWillUnmount() { - tooltipParentWillUnmount(this.props.instance); - this.unsubscribeOnWheel(); - } - - componentDidMount() { - let { instance } = this.props; - let { widget } = instance; - tooltipParentDidMount(this.dom.to, instance, widget.toTooltip, { tooltipName: "toTooltip" }); - tooltipParentDidMount(this.dom.from, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }); - - this.unsubscribeOnWheel = addEventListenerWithOptions(this.dom.el, "wheel", (e) => this.onWheel(e), { - passive: false, - }); - } - - onHandleMouseLeave(e, handle) { - if (!this.state.drag) { - let tooltipName = handle + "Tooltip"; - let { instance } = this.props; - let tooltip = instance.widget[tooltipName]; - tooltipMouseLeave(e, instance, tooltip, { tooltipName }); - } - } - - onHandleMouseDown(e, handle) { - e.preventDefault(); - e.stopPropagation(); - - let { instance } = this.props; - let { data, widget } = instance; - if (data.disabled || data.readOnly) return; - - let handleEl = this.dom[handle]; - let b = getTopLevelBoundingClientRect(handleEl); - let pos = getCursorPos(e); - let dx = pos.clientX - (b.left + b.right) / 2; - let dy = pos.clientY - (b.top + b.bottom) / 2; - - let tooltipName = handle + "Tooltip"; - let tooltip = widget[tooltipName]; - - this.setState({ - drag: true, - }); - - captureMouseOrTouch( - e, - (e) => { - let { value } = this.getValues(e, widget.vertical ? dy : dx); - if (handle === "from") { - if (instance.set("from", value)) this.setState({ from: value }); - if (value > this.state.to) { - if (instance.set("to", value)) this.setState({ to: value }); - } - } else if (handle === "to") { - if (instance.set("to", value)) this.setState({ to: value }); - if (value < this.state.from) { - if (instance.set("from", value)) this.setState({ from: value }); - } - } - tooltipMouseMove(e, instance, tooltip, { tooltipName, target: handleEl }); - }, - (e) => { - this.setState({ - drag: false, - }); - let pos = getCursorPos(e); - let el = document.elementFromPoint(pos.clientX, pos.clientY); - if (el !== handleEl) tooltipMouseLeave(e, instance, tooltip, { tooltipName, target: handleEl }); - } - ); - } - - getValues(e, d = 0) { - let { data, widget } = this.props.instance; - let { minValue, maxValue } = data; - let b = getTopLevelBoundingClientRect(this.dom.range); - let pos = getCursorPos(e); - let pct = widget.vertical - ? widget.invert - ? Math.max(0, Math.min(1, (pos.clientY - b.top - d) / this.dom.range.offsetHeight)) - : Math.max(0, Math.min(1, (b.bottom - pos.clientY + d) / this.dom.range.offsetHeight)) - : Math.max(0, Math.min(1, (pos.clientX - b.left - d) / this.dom.range.offsetWidth)); - let delta = (maxValue - minValue) * pct; - if (data.step) { - let currentValue = Math.round(delta / data.step) * data.step + minValue; - let value = this.checkBoundaries(currentValue); - - if (maxValue % data.step === 0) delta = Math.round(delta / data.step) * data.step; - - delta = value - minValue; - } - - return { - percent: delta / (maxValue - minValue), - value: minValue + delta, - }; - } - - onClick(e) { - let { instance } = this.props; - let { data, widget } = instance; - if (!data.disabled && !data.readOnly) { - let { value } = this.getValues(e); - this.props.instance.set("value", value, { immediate: true }); - - if (widget.showFrom) { - this.setState({ from: value }); - this.props.instance.set("from", value, { immediate: true }); - } - if (widget.showTo) { - this.setState({ to: value }); - this.props.instance.set("to", value, { immediate: true }); - } - } - } - - onWheel(e) { - let { instance } = this.props; - let { data, widget } = instance; - if ((widget.showFrom && widget.showTo) || !data.wheel) return; - - e.preventDefault(); - e.stopPropagation(); - - let increment = e.deltaY > 0 ? this.getIncrement() : -this.getIncrement(); - - if (!data.disabled && !data.readOnly) { - if (widget.showFrom) { - let value = this.checkBoundaries(data.from + increment); - if (instance.set("from", value)) this.setState({ from: value }); - } else if (widget.showTo) { - let value = this.checkBoundaries(data.to + increment); - if (instance.set("to", value)) this.setState({ to: value }); - } - } - } - - checkBoundaries(value) { - let { data } = this.props.instance; - if (value > data.maxValue) value = data.maxValue; - else if (value < data.minValue) value = data.minValue; - return value; - } - - getIncrement() { - let { instance } = this.props; - let { data } = instance; - let increment = data.increment || (data.maxValue - data.minValue) * data.incrementPercentage; - return increment; - } -} diff --git a/packages/cx/src/widgets/form/Slider.scss b/packages/cx/src/widgets/form/Slider.scss index e90862376..42f997d1f 100644 --- a/packages/cx/src/widgets/form/Slider.scss +++ b/packages/cx/src/widgets/form/Slider.scss @@ -1,3 +1,6 @@ + +@use "sass:map"; + @mixin cx-slider( $name: "slider", $state-style-map: $cx-input-state-style-map, @@ -9,9 +12,9 @@ $range-background-color: $cx-default-slider-range-background-color, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); .#{$block}#{$name} { width: $width; diff --git a/packages/cx/src/widgets/form/Slider.tsx b/packages/cx/src/widgets/form/Slider.tsx new file mode 100644 index 000000000..d86607cac --- /dev/null +++ b/packages/cx/src/widgets/form/Slider.tsx @@ -0,0 +1,473 @@ +/** @jsxImportSource react */ + +import { BooleanProp, NumberProp, StringProp, StructuredProp, StyleProp } from "../../ui/Prop"; +import type { RenderingContext } from "../../ui/RenderingContext"; +import { VDOM, Widget, getContent } from "../../ui/Widget"; +import { addEventListenerWithOptions } from "../../util/addEventListenerWithOptions"; +import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; +import { isDefined } from "../../util/isDefined"; +import { isUndefined } from "../../util/isUndefined"; +import { captureMouseOrTouch, getCursorPos } from "../overlay/captureMouse"; +import { + tooltipMouseLeave, + tooltipMouseMove, + tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, + type TooltipConfig, +} from "../overlay/tooltip-ops"; +import { Field, FieldConfig, FieldInstance, getFieldTooltip } from "./Field"; +import type { Instance } from "../../ui/Instance"; + +export interface SliderConfig extends FieldConfig { + /** Low value of the slider range. */ + from?: NumberProp; + + /** High value of the slider range. */ + to?: NumberProp; + + /** Rounding step. */ + step?: NumberProp; + + /** Minimum allowed value. Default is `0`. */ + minValue?: NumberProp; + + /** Maximum allowed value. Default is `100`. */ + maxValue?: NumberProp; + + /** Style object to be applied on the selected axis range. */ + rangeStyle?: StyleProp; + + /** Style object to be applied on the handle. */ + handleStyle?: StyleProp; + + /** Minimum allowed value. Default is `0`. */ + min?: NumberProp; + + /** Maximum allowed value. Default is `100`. */ + max?: NumberProp; + + /** High value of the slider range. */ + value?: NumberProp; + + /** Set to `true` to orient the slider vertically. */ + vertical?: boolean; + + /** Invert vertical slider behavior. Set this to `true` if you want the slider to go from `top` to `bottom`. */ + invert?: boolean; + + /** Range tooltip configuration. */ + toTooltip?: StringProp | StructuredProp; + + /** Range tooltip configuration. */ + valueTooltip?: StringProp | StructuredProp; + + /** Range tooltip configuration. */ + fromTooltip?: StringProp | StructuredProp; + + /** When set to `true`, slider responds to mouse wheel events, while hovering it. It will not work if both `from` and `to` values are used. Default value is `false`. */ + wheel?: BooleanProp; + + /** Value increment/decrement, when controlling the slider with mouse wheel. Default value is set to `1%` of range. */ + increment?: NumberProp; + + /** Increment percentage. Default value is `0.01` (1%). */ + incrementPercentage?: number; + + /** Set to `true` to make the slider read-only. */ + readOnly?: BooleanProp; + + /** Custom validation function. */ + onValidate?: string | ((value: number, instance: Instance, validationParams: Record) => unknown); +} + +export class Slider extends Field> { + declare baseClass: string; + declare min?: number; + declare max?: number; + declare minValue: number; + declare maxValue: number; + declare value?: number; + declare vertical: boolean; + declare invert: boolean; + declare from?: number; + declare to?: number; + declare showFrom?: boolean; + declare showTo?: boolean; + declare toTooltip?: TooltipConfig; + declare fromTooltip?: TooltipConfig; + declare valueTooltip?: TooltipConfig; + declare incrementPercentage: number; + declare wheel: boolean; + + declareData(...args: Record[]): void { + super.declareData( + { + from: 0, + to: 0, + step: undefined, + minValue: undefined, + maxValue: undefined, + increment: undefined, + incrementPercentage: undefined, + wheel: undefined, + disabled: undefined, + enabled: undefined, + readOnly: undefined, + rangeStyle: { + structured: true, + }, + handleStyle: { + structured: true, + }, + invert: false, + }, + ...args, + ); + } + + init(): void { + if (isDefined(this.min)) this.minValue = this.min; + + if (isDefined(this.max)) this.maxValue = this.max; + + if (this.value != null) this.to = this.value; + + if (isUndefined(this.from)) this.from = this.minValue; + else this.showFrom = true; + + if (isUndefined(this.to)) this.to = this.maxValue; + else this.showTo = true; + + if (this.valueTooltip) this.toTooltip = this.valueTooltip; + + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + let { data } = instance; + data.stateMods = { + ...data.stateMods, + horizontal: !this.vertical, + vertical: this.vertical, + disabled: data.disabled, + }; + super.prepareData(context, instance); + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactNode { + return ( + + ); + } +} + +Slider.prototype.baseClass = "slider"; +Slider.prototype.minValue = 0; +Slider.prototype.maxValue = 100; +Slider.prototype.vertical = false; +Slider.prototype.incrementPercentage = 0.01; +Slider.prototype.wheel = false; +Slider.prototype.invert = false; + +Widget.alias("slider", Slider); + +interface SliderComponentProps { + instance: FieldInstance; + data: Record; + label?: React.ReactNode; +} + +interface SliderComponentState { + from: number; + to: number; + drag?: boolean; +} + +interface DomRefs { + el?: HTMLElement; + range?: HTMLElement; + from?: HTMLElement; + to?: HTMLElement; +} + +class SliderComponent extends VDOM.Component { + dom: DomRefs; + unsubscribeOnWheel?: () => void; + + constructor(props: SliderComponentProps) { + super(props); + this.dom = {}; + let { data } = props; + this.state = { + from: data.from as number, + to: data.to as number, + }; + } + + render(): React.ReactNode { + let { instance, data, label } = this.props; + let { widget } = instance; + let { CSS, baseClass } = widget; + let { minValue, maxValue } = data; + let { from, to } = this.state; + + from = Math.min(maxValue, Math.max(minValue, from)); + to = Math.min(maxValue, Math.max(minValue, to)); + + let handleStyle = CSS.parseStyle(data.handleStyle); + let anchor = widget.vertical ? (widget.invert ? "top" : "bottom") : "left"; + let rangeStart = from - minValue; + let rangeSize = to - from; + + let fromHandleStyle = { + ...handleStyle, + [anchor]: `${(100 * (from - minValue)) / (maxValue - minValue)}%`, + }; + + let toHandleStyle = { + ...handleStyle, + [anchor]: `${(100 * (to - minValue)) / (maxValue - minValue)}%`, + }; + + let rangeStyle = { + ...CSS.parseStyle(data.rangeStyle), + [anchor]: `${(100 * rangeStart) / (maxValue - minValue)}%`, + [widget.vertical ? "height" : "width"]: `${(100 * rangeSize) / (maxValue - minValue)}%`, + }; + + return ( +
    this.onClick(e)} + ref={(el: HTMLDivElement | null) => { + this.dom.el = el || undefined; + }} + onMouseMove={(e: React.MouseEvent) => tooltipMouseMove(e, ...getFieldTooltip(instance))} + onMouseLeave={(e: React.MouseEvent) => tooltipMouseLeave(e, ...getFieldTooltip(instance))} + > + {label}  +
    + {rangeSize > 0 &&
    } +
    { + this.dom.range = c || undefined; + }} + > + {widget.showFrom && ( +
    this.onHandleMouseDown(e, "from")} + onMouseMove={(e: React.MouseEvent) => + tooltipMouseMove(e, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }) + } + onMouseLeave={(e: React.MouseEvent) => this.onHandleMouseLeave(e, "from")} + onTouchStart={(e: React.TouchEvent) => this.onHandleMouseDown(e, "from")} + ref={(c: HTMLDivElement | null) => { + this.dom.from = c || undefined; + }} + /> + )} + {widget.showTo && ( +
    this.onHandleMouseDown(e, "to")} + onMouseMove={(e: React.MouseEvent) => + tooltipMouseMove(e, instance, widget.toTooltip, { tooltipName: "toTooltip" }) + } + onMouseLeave={(e: React.MouseEvent) => this.onHandleMouseLeave(e, "to")} + onTouchStart={(e: React.TouchEvent) => this.onHandleMouseDown(e, "to")} + ref={(c: HTMLDivElement | null) => { + this.dom.to = c || undefined; + }} + /> + )} +
    +
    +
    + ); + } + + UNSAFE_componentWillReceiveProps(props: SliderComponentProps): void { + this.setState({ + from: props.data.from, + to: props.data.to, + }); + + let { instance } = props; + let { widget } = instance; + tooltipParentWillReceiveProps(this.dom.to!, instance, widget.toTooltip, { tooltipName: "toTooltip" }); + tooltipParentWillReceiveProps(this.dom.from!, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }); + } + + componentWillUnmount(): void { + tooltipParentWillUnmount(this.props.instance); + this.unsubscribeOnWheel?.(); + } + + componentDidMount(): void { + let { instance } = this.props; + let { widget } = instance; + tooltipParentDidMount(this.dom.to!, instance, widget.toTooltip, { tooltipName: "toTooltip" }); + tooltipParentDidMount(this.dom.from!, instance, widget.fromTooltip, { tooltipName: "fromTooltip" }); + + this.unsubscribeOnWheel = addEventListenerWithOptions(this.dom.el!, "wheel", (e) => this.onWheel(e), { + passive: false, + }); + } + + onHandleMouseLeave(e: React.MouseEvent, handle: "from" | "to"): void { + if (!this.state.drag) { + let tooltipName = handle + "Tooltip"; + let { instance } = this.props; + let tooltip = handle == "from" ? instance.widget.fromTooltip : instance.widget.toTooltip; + tooltipMouseLeave(e, instance, tooltip, { tooltipName }); + } + } + + onHandleMouseDown(e: React.MouseEvent | React.TouchEvent, handle: "from" | "to"): void { + e.preventDefault(); + e.stopPropagation(); + + let { instance } = this.props; + let { data, widget } = instance; + if (data.disabled || data.readOnly) return; + + let handleEl = this.dom[handle]; + let b = getTopLevelBoundingClientRect(handleEl!); + let pos = getCursorPos(e); + let dx = pos.clientX - (b.left + b.right) / 2; + let dy = pos.clientY - (b.top + b.bottom) / 2; + + let tooltipName = handle + "Tooltip"; + let tooltip = handle == "from" ? widget.fromTooltip : widget.toTooltip; + + this.setState({ + drag: true, + }); + + captureMouseOrTouch( + e, + (e) => { + let { value } = this.getValues(e, widget.vertical ? dy : dx); + if (handle === "from") { + if (instance.set("from", value)) this.setState({ from: value }); + if (value > this.state.to) { + if (instance.set("to", value)) this.setState({ to: value }); + } + } else if (handle === "to") { + if (instance.set("to", value)) this.setState({ to: value }); + if (value < this.state.from) { + if (instance.set("from", value)) this.setState({ from: value }); + } + } + tooltipMouseMove(e, instance, tooltip, { tooltipName, target: handleEl }); + }, + (e: any) => { + this.setState({ + drag: false, + }); + let pos = getCursorPos(e); + let el = document.elementFromPoint(pos.clientX, pos.clientY); + if (el !== handleEl) tooltipMouseLeave(e, instance, tooltip as any, { tooltipName, target: handleEl }); + }, + ); + } + + getValues( + e: React.MouseEvent | React.TouchEvent | MouseEvent | TouchEvent, + d: number = 0, + ): { percent: number; value: number } { + let { data, widget } = this.props.instance; + let { minValue, maxValue } = data; + let b = getTopLevelBoundingClientRect(this.dom.range!); + let pos = getCursorPos(e as any); + let pct = widget.vertical + ? widget.invert + ? Math.max(0, Math.min(1, (pos.clientY - b.top - d) / this.dom.range!.offsetHeight)) + : Math.max(0, Math.min(1, (b.bottom - pos.clientY + d) / this.dom.range!.offsetHeight)) + : Math.max(0, Math.min(1, (pos.clientX - b.left - d) / this.dom.range!.offsetWidth)); + let delta = (maxValue - minValue) * pct; + if (data.step) { + let currentValue = Math.round(delta / data.step) * data.step + minValue; + let value = this.checkBoundaries(currentValue); + + if (maxValue % data.step === 0) delta = Math.round(delta / data.step) * data.step; + + delta = value - minValue; + } + + return { + percent: delta / (maxValue - minValue), + value: minValue + delta, + }; + } + + onClick(e: React.MouseEvent): void { + let { instance } = this.props; + let { data, widget } = instance; + if (!data.disabled && !data.readOnly) { + let { value } = this.getValues(e); + this.props.instance.set("value", value, { immediate: true }); + + if (widget.showFrom) { + this.setState({ from: value }); + this.props.instance.set("from", value, { immediate: true }); + } + if (widget.showTo) { + this.setState({ to: value }); + this.props.instance.set("to", value, { immediate: true }); + } + } + } + + onWheel(e: WheelEvent): void { + let { instance } = this.props; + let { data, widget } = instance; + if ((widget.showFrom && widget.showTo) || !data.wheel) return; + + e.preventDefault(); + e.stopPropagation(); + + let increment = e.deltaY > 0 ? this.getIncrement() : -this.getIncrement(); + + if (!data.disabled && !data.readOnly) { + if (widget.showFrom) { + let value = this.checkBoundaries(data.from + increment); + if (instance.set("from", value)) this.setState({ from: value }); + } else if (widget.showTo) { + let value = this.checkBoundaries(data.to + increment); + if (instance.set("to", value)) this.setState({ to: value }); + } + } + } + + checkBoundaries(value: number): number { + let { data } = this.props.instance; + if (value > data.maxValue) value = data.maxValue; + else if (value < data.minValue) value = data.minValue; + return value; + } + + getIncrement(): number { + let { instance } = this.props; + let { data } = instance; + let increment = data.increment || (data.maxValue - data.minValue) * data.incrementPercentage; + return increment; + } +} diff --git a/packages/cx/src/widgets/form/Switch.d.ts b/packages/cx/src/widgets/form/Switch.d.ts deleted file mode 100644 index 9e0ad80c3..000000000 --- a/packages/cx/src/widgets/form/Switch.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as Cx from '../../core'; -import { FieldProps } from './Field'; - -interface SwitchProps extends FieldProps { - - /** Value indicating that switch is on. */ - on?: Cx.BooleanProp, - - /** Value indicating that switch is off. */ - off?: Cx.BooleanProp, - - /** Value indicating that switch is on. */ - value?: Cx.BooleanProp, - - /** Defaults to `false`. Used to make the field read-only. */ - readOnly?: Cx.BooleanProp, - - /** Text description. */ - text?: Cx.StringProp, - - /** Style object to be applied on the axis range when the switch is on. */ - rangeStyle?: Cx.StyleProp, - - /** Style object to be applied on the switch handle. */ - handleStyle?: Cx.StyleProp, - - /** Base CSS class to be applied to the field. Defaults to `switch`. */ - baseClass?: string, - - /** - * Determines if button should receive focus on mousedown event. - * Default is `false`, which means that focus can be set only using the keyboard `Tab` key. - */ - focusOnMouseDown?: boolean - -} - -export class Switch extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/Switch.js b/packages/cx/src/widgets/form/Switch.js deleted file mode 100644 index c31cd1b1a..000000000 --- a/packages/cx/src/widgets/form/Switch.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { KeyCode } from "../../util/KeyCode"; -import { parseStyle } from "../../util/parseStyle"; -import { Field, getFieldTooltip } from "./Field"; -import { tooltipMouseMove, tooltipMouseLeave } from "../overlay/tooltip-ops"; -import { preventFocus } from "../../ui/FocusManager"; -import { isDefined } from "../../util/isDefined"; - -export class Switch extends Field { - declareData() { - super.declareData( - { - on: false, - off: true, - value: undefined, - disabled: undefined, - enabled: undefined, - readOnly: undefined, - text: undefined, - rangeStyle: { - structured: true, - }, - handleStyle: { - structured: true, - }, - }, - ...arguments - ); - } - - isEmpty() { - return false; - } - - init() { - if (isDefined(this.value)) this.on = this.value; - - this.rangeStyle = parseStyle(this.rangeStyle); - this.handleStyle = parseStyle(this.handleStyle); - - super.init(); - } - - prepareData(context, instance) { - let { data } = instance; - - if (isDefined(this.off)) data.on = !data.off; - - data.stateMods = { - ...data.stateMods, - on: data.on, - disabled: data.disabled, - }; - super.prepareData(context, instance); - } - - renderInput(context, instance, key) { - let { data, widget } = instance; - let { rangeStyle, handleStyle } = data; - let { CSS, baseClass } = this; - - let text = data.text || this.renderChildren(context, instance); - let renderTextElement = text?.length != 0; - - return ( -
    { - e.stopPropagation(); - if (!this.focusOnMouseDown) preventFocus(e); - }} - onClick={(e) => { - this.toggle(e, instance); - }} - onKeyDown={(e) => { - if (widget.handleKeyDown(e, instance) === false) return; - if (e.keyCode == KeyCode.space) { - this.toggle(e, instance); - } - }} - onMouseMove={(e) => tooltipMouseMove(e, ...getFieldTooltip(instance))} - onMouseLeave={(e) => tooltipMouseLeave(e, ...getFieldTooltip(instance))} - > - {this.labelPlacement && getContent(this.renderLabel(context, instance, "label"))} -   -
    -
    -
    -
    -
    -
    - {renderTextElement && ( -
    - {text} -
    - )} -
    - ); - } - - toggle(e, instance) { - let { data } = instance; - if (data.readOnly || data.disabled) return; - instance.set("on", !data.on); - instance.set("off", data.on); - e.preventDefault(); - e.stopPropagation(); - } -} - -Switch.prototype.baseClass = "switch"; -Switch.prototype.focusOnMouseDown = false; - -Widget.alias("switch", Switch); diff --git a/packages/cx/src/widgets/form/Switch.scss b/packages/cx/src/widgets/form/Switch.scss index 70c0e0e50..d25a4ec7c 100644 --- a/packages/cx/src/widgets/form/Switch.scss +++ b/packages/cx/src/widgets/form/Switch.scss @@ -1,3 +1,5 @@ +@use "sass:map"; + @mixin cx-switch( $name: "switch", $state-style-map: $cx-input-state-style-map, @@ -10,9 +12,9 @@ $empty-text: $cx-empty-text, $besm: $cx-besm ) { - $block: map-get($besm, block); - $element: map-get($besm, element); - $state: map-get($besm, state); + $block: map.get($besm, block); + $element: map.get($besm, element); + $state: map.get($besm, state); $padding: cx-get-state-rule($state-style-map, default, padding); $border-width: cx-get-state-rule($state-style-map, default, border-width); diff --git a/packages/cx/src/widgets/form/Switch.tsx b/packages/cx/src/widgets/form/Switch.tsx new file mode 100644 index 000000000..e061a7887 --- /dev/null +++ b/packages/cx/src/widgets/form/Switch.tsx @@ -0,0 +1,176 @@ +/** @jsxImportSource react */ +import type { RenderingContext } from "../../ui/RenderingContext"; +import type { Instance } from "../../ui/Instance"; +import { Widget, VDOM, getContent } from "../../ui/Widget"; +import { KeyCode } from "../../util/KeyCode"; +import { parseStyle } from "../../util/parseStyle"; +import { Field, FieldConfig, getFieldTooltip, FieldInstance } from "./Field"; +import { tooltipMouseMove, tooltipMouseLeave } from "../overlay/tooltip-ops"; +import { preventFocus } from "../../ui/FocusManager"; +import { isDefined } from "../../util/isDefined"; +import { BooleanProp, StringProp, StyleProp } from "../../ui/Prop"; + +export interface SwitchConfig extends FieldConfig { + /** Value indicating that switch is on. */ + on?: BooleanProp; + + /** Value indicating that switch is off. */ + off?: BooleanProp; + + /** Value indicating that switch is on. */ + value?: BooleanProp; + + /** Defaults to `false`. Used to make the field read-only. */ + readOnly?: BooleanProp; + + /** Text description. */ + text?: StringProp; + + /** Style object to be applied on the axis range when the switch is on. */ + rangeStyle?: StyleProp; + + /** Style object to be applied on the switch handle. */ + handleStyle?: StyleProp; + + /** Base CSS class to be applied to the field. Defaults to `switch`. */ + baseClass?: string; + + /** Determines if button should receive focus on mousedown event. Default is `false`. */ + focusOnMouseDown?: boolean; + + /** Custom validation function. */ + onValidate?: string | ((value: boolean, instance: Instance, validationParams: Record) => unknown); +} + +export class Switch extends Field { + declare public on?: unknown; + declare public off?: unknown; + declare public value?: unknown; + declare public rangeStyle?: Record | string; + declare public handleStyle?: Record | string; + declare public focusOnMouseDown?: boolean; + + constructor(config?: SwitchConfig) { + super(config); + } + + declareData(...args: Record[]): void { + super.declareData( + { + on: false, + off: true, + value: undefined, + disabled: undefined, + enabled: undefined, + readOnly: undefined, + text: undefined, + rangeStyle: { + structured: true, + }, + handleStyle: { + structured: true, + }, + }, + ...args, + ); + } + + isEmpty(): boolean { + return false; + } + + init(): void { + if (isDefined(this.value)) this.on = this.value; + + this.rangeStyle = parseStyle(this.rangeStyle); + this.handleStyle = parseStyle(this.handleStyle); + + super.init(); + } + + prepareData(context: RenderingContext, instance: FieldInstance): void { + let { data } = instance; + + if (isDefined(this.off)) data.on = !data.off; + + data.stateMods = { + ...data.stateMods, + on: data.on, + disabled: data.disabled, + }; + super.prepareData(context, instance); + } + + renderInput(context: RenderingContext, instance: FieldInstance, key: string): React.ReactElement { + let { data, widget } = instance; + let { rangeStyle, handleStyle } = data; + let { CSS, baseClass } = this; + + let text = data.text || this.renderChildren(context, instance); + let renderTextElement = text?.length != 0; + + return ( +
    { + e.stopPropagation(); + if (!this.focusOnMouseDown) preventFocus(e); + }} + onClick={(e: React.MouseEvent) => { + this.toggle(e, instance); + }} + onKeyDown={(e: React.KeyboardEvent) => { + const switchWidget = widget as Switch; + if (switchWidget.handleKeyDown && switchWidget.handleKeyDown(e, instance) === false) return; + if (e.keyCode == KeyCode.space) { + this.toggle(e, instance); + } + }} + onMouseMove={(e: React.MouseEvent) => { + const tooltip = getFieldTooltip(instance); + if (Array.isArray(tooltip)) { + tooltipMouseMove(e, ...tooltip); + } + }} + onMouseLeave={(e: React.MouseEvent) => { + const tooltip = getFieldTooltip(instance); + if (Array.isArray(tooltip)) { + tooltipMouseLeave(e, ...tooltip); + } + }} + > + {this.labelPlacement && getContent(this.renderLabel(context, instance, "label"))} +   +
    +
    +
    +
    +
    +
    + {renderTextElement && ( +
    + {text} +
    + )} +
    + ); + } + + toggle(e: React.MouseEvent | React.KeyboardEvent, instance: Instance): void { + let { data } = instance; + if (data.readOnly || data.disabled) return; + instance.set("on", !data.on); + instance.set("off", data.on); + e.preventDefault(); + e.stopPropagation(); + } +} + +Switch.prototype.baseClass = "switch"; +Switch.prototype.focusOnMouseDown = false; + +Widget.alias("switch", Switch); diff --git a/packages/cx/src/widgets/form/TextArea.d.ts b/packages/cx/src/widgets/form/TextArea.d.ts deleted file mode 100644 index 8120bcf65..000000000 --- a/packages/cx/src/widgets/form/TextArea.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Cx from '../../core'; -import { TextFieldProps } from './TextField'; - -interface TextAreaProps extends TextFieldProps { - - /** Specifies the number of visible lines. */ - rows?: Cx.NumberProp, - - /** Event used to report on a new value. Defaults to `blur`. Other permitted value is `input`. */ - reachOn?: string, - - /** Base CSS class to be applied to the element. Defaults to `textarea`. */ - baseClass?: string - -} - -export class TextArea extends Cx.Widget {} diff --git a/packages/cx/src/widgets/form/TextArea.js b/packages/cx/src/widgets/form/TextArea.js deleted file mode 100644 index 06d3ac3a5..000000000 --- a/packages/cx/src/widgets/form/TextArea.js +++ /dev/null @@ -1,195 +0,0 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { TextField } from "./TextField"; -import { getFieldTooltip } from "./Field"; -import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, - tooltipMouseLeave, - tooltipParentDidMount, -} from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { KeyCode } from "../../util/KeyCode"; -import { autoFocus } from "../autoFocus"; -import { getActiveElement } from "../../util/getActiveElement"; - -export class TextArea extends TextField { - declareData() { - super.declareData( - { - rows: undefined, - }, - ...arguments - ); - } - - prepareData(context, instance) { - let { state, data, cached } = instance; - if (!cached.data || data.value != cached.data.value) state.empty = !data.value; - super.prepareData(context, instance); - } - - renderInput(context, instance, key) { - return ( - - ); - } - - validateRequired(context, instance) { - return instance.state.empty && this.requiredText; - } -} - -TextArea.prototype.baseClass = "textarea"; -TextArea.prototype.reactOn = "blur"; -TextArea.prototype.suppressErrorsUntilVisited = true; - -class Input extends VDOM.Component { - constructor(props) { - super(props); - this.state = { - focus: false, - }; - } - - render() { - let { instance, label, help } = this.props; - let { widget, data, state } = instance; - let { CSS, baseClass, suppressErrorsUntilVisited } = widget; - - let empty = this.input ? !this.input.value : data.empty; - - return ( -
    -