diff --git a/.gitignore b/.gitignore index 679d051b..ef94072f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +.storybook/utils/ # testing /coverage @@ -16,6 +17,8 @@ /react/dist/ /react/types/ /styles/dist/ +/react/unstyled/dist/ +/unstyled/dist/ # misc .DS_Store diff --git a/.stylelintrc.json b/.stylelintrc.json index 28064340..5f2190cb 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,7 +1,6 @@ { "extends": "stylelint-config-standard-scss", "rules": { - "indentation": 4, "font-family-no-missing-generic-family-keyword": [ true, { @@ -13,7 +12,6 @@ "no-descending-specificity": null, "no-duplicate-selectors": null, "scss/no-global-function-names": null, - "scss/at-extend-no-missing-placeholder": null, - "string-quotes": "single" + "scss/at-extend-no-missing-placeholder": null } } diff --git a/README.md b/README.md index f6272451..ae80f74c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ document.body.appendChild(label.dom); If you'd like to include PCUI in your React project, you can import the individual components as follows: ```javascript -import React from 'react'; +import * as React from 'react'; import ReactDOM from 'react-dom'; import { TextInput } from '@playcanvas/pcui/react'; import '@playcanvas/pcui/styles'; diff --git a/docs/README.md b/docs/README.md index 28783bb2..d87b01d7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,16 +4,18 @@ The PCUI documentation website is built using a Jekyll template. The markdown pages for the site can be found and edited in the `docs/pages` directory. The doc site also makes use of storybook to display React components and typedocs to display the API reference. If you are developing the PCUI library, you should use `npm run storybook` and `npm run typedocs` directly to generate the storybook and typedocs respectively. The following guide is for updating and publishing the documentation site. -### Testing docs locally (macOS) -Ensure you have Ruby 3.x installed. +### Testing docs locally + +Ensure you have Ruby 3.x installed. Go [here](https://rubyinstaller.org/downloads/) for a Windows installer. In the `pcui/docs` directory run: + `bundle install` To install the ruby dependencies. If you are having trouble with the install, try deleting the `Gemfile.lock` file. -Then in the main pcui directory run: +Then in the main PCUI directory run: `npm run build:typedocs` to build the latest typedocs API reference site which will be copied into the doc site in the next step @@ -27,7 +29,7 @@ Visit http://localhost:3497/ If you haven't cloned the [playcanvas.github.io](https://github.com/playcanvas/playcanvas.github.io) repo, install it locally to a projects folder which will now be referenced as ``. -Then in the pcui main directory run: +Then in the PCUI main directory run: `npm run build:docsite:production` diff --git a/docs/pages/2-react.markdown b/docs/pages/2-react.markdown index 394795c6..9cf044f2 100644 --- a/docs/pages/2-react.markdown +++ b/docs/pages/2-react.markdown @@ -8,7 +8,7 @@ nav_order: 3 If you are more familiar with React, you can import React elements into your own `.jsx` files and use them as follows: ```jsx -import React from 'react'; +import * as React from 'react'; import ReactDOM from 'react-dom'; import { TextInput } from '@playcanvas/pcui/react'; import '@playcanvas/pcui/styles'; diff --git a/examples/arrayinput.html b/examples/elements/arrayinput.html similarity index 100% rename from examples/arrayinput.html rename to examples/elements/arrayinput.html diff --git a/examples/booleaninput.html b/examples/elements/booleaninput.html similarity index 100% rename from examples/booleaninput.html rename to examples/elements/booleaninput.html diff --git a/examples/button.html b/examples/elements/button.html similarity index 100% rename from examples/button.html rename to examples/elements/button.html diff --git a/examples/canvas.html b/examples/elements/canvas.html similarity index 100% rename from examples/canvas.html rename to examples/elements/canvas.html diff --git a/examples/code.html b/examples/elements/code.html similarity index 100% rename from examples/code.html rename to examples/elements/code.html diff --git a/examples/colorpicker.html b/examples/elements/colorpicker.html similarity index 100% rename from examples/colorpicker.html rename to examples/elements/colorpicker.html diff --git a/examples/divider.html b/examples/elements/divider.html similarity index 100% rename from examples/divider.html rename to examples/elements/divider.html diff --git a/examples/gradientpicker.html b/examples/elements/gradientpicker.html similarity index 100% rename from examples/gradientpicker.html rename to examples/elements/gradientpicker.html diff --git a/examples/gridview.html b/examples/elements/gridview.html similarity index 100% rename from examples/gridview.html rename to examples/elements/gridview.html diff --git a/examples/infobox.html b/examples/elements/infobox.html similarity index 100% rename from examples/infobox.html rename to examples/elements/infobox.html diff --git a/examples/label.html b/examples/elements/label.html similarity index 100% rename from examples/label.html rename to examples/elements/label.html diff --git a/examples/labelgroup.html b/examples/elements/labelgroup.html similarity index 100% rename from examples/labelgroup.html rename to examples/elements/labelgroup.html diff --git a/examples/menu.html b/examples/elements/menu.html similarity index 100% rename from examples/menu.html rename to examples/elements/menu.html diff --git a/examples/numericinput.html b/examples/elements/numericinput.html similarity index 100% rename from examples/numericinput.html rename to examples/elements/numericinput.html diff --git a/examples/overlay.html b/examples/elements/overlay.html similarity index 100% rename from examples/overlay.html rename to examples/elements/overlay.html diff --git a/examples/panel.html b/examples/elements/panel.html similarity index 100% rename from examples/panel.html rename to examples/elements/panel.html diff --git a/examples/progress.html b/examples/elements/progress.html similarity index 100% rename from examples/progress.html rename to examples/elements/progress.html diff --git a/examples/radiobutton.html b/examples/elements/radiobutton.html similarity index 100% rename from examples/radiobutton.html rename to examples/elements/radiobutton.html diff --git a/examples/selectinput.html b/examples/elements/selectinput.html similarity index 100% rename from examples/selectinput.html rename to examples/elements/selectinput.html diff --git a/examples/sliderinput.html b/examples/elements/sliderinput.html similarity index 100% rename from examples/sliderinput.html rename to examples/elements/sliderinput.html diff --git a/examples/spinner.html b/examples/elements/spinner.html similarity index 100% rename from examples/spinner.html rename to examples/elements/spinner.html diff --git a/examples/textareainput.html b/examples/elements/textareainput.html similarity index 100% rename from examples/textareainput.html rename to examples/elements/textareainput.html diff --git a/examples/textinput.html b/examples/elements/textinput.html similarity index 100% rename from examples/textinput.html rename to examples/elements/textinput.html diff --git a/examples/treeview.html b/examples/elements/treeview.html similarity index 100% rename from examples/treeview.html rename to examples/elements/treeview.html diff --git a/examples/vectorinput.html b/examples/elements/vectorinput.html similarity index 100% rename from examples/vectorinput.html rename to examples/elements/vectorinput.html diff --git a/examples/index.html b/examples/index.html index 14b25618..21b40346 100644 --- a/examples/index.html +++ b/examples/index.html @@ -64,50 +64,66 @@ allowDragging: false }); - const treeRoot = new TreeViewItem({ - allowSelect: false, - text: 'Elements' - }); - treeView.append(treeRoot); - const iframe = document.createElement('iframe'); - const examples = [ - 'ArrayInput', - 'BooleanInput', - 'Button', - 'Canvas', - 'Code', - 'ColorPicker', - 'Divider', - 'GradientPicker', - 'GridView', - 'InfoBox', - 'Label', - 'LabelGroup', - 'Menu', - 'NumericInput', - 'Overlay', - 'Panel', - 'Progress', - 'RadioButton', - 'SelectInput', - 'SliderInput', - 'Spinner', - 'TextAreaInput', - 'TextInput', - 'TreeView', - 'VectorInput' + const categories = [ + { + categoryName: 'Elements', + examples: [ + 'ArrayInput', + 'BooleanInput', + 'Button', + 'Canvas', + 'Code', + 'ColorPicker', + 'Divider', + 'GradientPicker', + 'GridView', + 'InfoBox', + 'Label', + 'LabelGroup', + 'Menu', + 'NumericInput', + 'Overlay', + 'Panel', + 'Progress', + 'RadioButton', + 'SelectInput', + 'SliderInput', + 'Spinner', + 'TextAreaInput', + 'TextInput', + 'TreeView', + 'VectorInput' + ] + }, + { + categoryName: 'Utilities', + examples: [ + 'Icon Browser', + ] + } ]; - for (const example of examples) { - const item = new TreeViewItem({ - text: example - }); - item.on('select', () => { - iframe.src = example.toLowerCase() + '.html'; + + for (const category of categories) { + const categoryItem = new TreeViewItem({ + allowSelect: false, + text: category.categoryName }); + treeView.append(categoryItem); + + for (const example of category.examples) { + const item = new TreeViewItem({ + text: example + }); + item.on('select', () => { + const path = category.categoryName.toLowerCase(); + const name = example.toLowerCase().replace(/ /g, '-'); + iframe.src = `${path}/${name}.html`; + }); - treeRoot.append(item); + categoryItem.append(item); + } } const filter = new TextInput({ diff --git a/examples/utilities/icon-browser.html b/examples/utilities/icon-browser.html new file mode 100644 index 00000000..e04f514f --- /dev/null +++ b/examples/utilities/icon-browser.html @@ -0,0 +1,70 @@ + + + + PCUI GridView + + + + + + + + + diff --git a/package.json b/package.json index 10f6c9da..107909b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@playcanvas/pcui", - "version": "3.3.1", + "version": "4.0.11", "author": "PlayCanvas ", "homepage": "https://playcanvas.github.io/pcui", "description": "User interface component library for the web", @@ -45,8 +45,8 @@ "build:react:es6": "rollup -c --environment target:react:es6", "build": "rollup -c && npm run bundle:styles", "build:icons": "cd ./utils && node ./build-font-icons.mjs", - "build:docsite:local": "cd docs && bundle exec jekyll build --config _config_local.yml && mkdir _site/storybook && cd .. && ENVIRONMENT=production build-storybook --no-dll -o ./docs/_site/storybook && cp -r ./typedocs ./docs/_site/typedocs", - "build:docsite:production": "cd docs && bundle exec jekyll build --config _config.yml && mkdir _site/storybook && cd .. && ENVIRONMENT=production build-storybook --no-dll -o ./docs/_site/storybook && cp -r ./typedocs ./docs/_site/typedocs", + "build:docsite:local": "cd docs && bundle exec jekyll build --config _config_local.yml && shx mkdir _site/storybook && cd .. && cross-env ENVIRONMENT=production build-storybook --no-dll -o ./docs/_site/storybook && shx cp -r ./typedocs ./docs/_site/typedocs", + "build:docsite:production": "cd docs && bundle exec jekyll build --config _config.yml && shx mkdir _site/storybook && cd .. && cross-env ENVIRONMENT=production build-storybook --no-dll -o ./docs/_site/storybook && shx cp -r ./typedocs ./docs/_site/typedocs", "docsite:serve": "serve docs/_site -p 3497", "build:types": "tsc --project ./tsconfig.json --declaration --emitDeclarationOnly --outDir types && tsc --project ./react/tsconfig.json --declaration --emitDeclarationOnly --outDir ./react/types", "build:typedocs": "typedoc --tsconfig ./tsconfig.json", @@ -88,7 +88,6 @@ "rules": { "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", "jsdoc/require-returns": "off", @@ -105,32 +104,29 @@ "@playcanvas/observer": "^1.3.6" }, "devDependencies": { - "@babel/core": "^7.20.12", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.7", - "@playcanvas/eslint-config": "^1.0.16", - "@rollup/plugin-babel": "^6.0.3", - "@rollup/plugin-node-resolve": "^15.0.1", + "@playcanvas/eslint-config": "^1.4.1", + "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-replace": "^5.0.2", - "@storybook/addon-actions": "^6.5.15", - "@storybook/addon-backgrounds": "^6.5.15", - "@storybook/addon-controls": "^6.5.15", - "@storybook/addon-docs": "^6.5.15", - "@storybook/addon-links": "^6.5.15", - "@storybook/addons": "^6.5.15", - "@storybook/builder-webpack5": "^6.5.15", - "@storybook/manager-webpack5": "^6.5.15", + "@rollup/plugin-typescript": "^11.1.0", + "@storybook/addon-actions": "^6.5.16", + "@storybook/addon-backgrounds": "^6.5.16", + "@storybook/addon-controls": "^6.5.16", + "@storybook/addon-docs": "^6.5.16", + "@storybook/addon-links": "^6.5.16", + "@storybook/addons": "^6.5.16", + "@storybook/builder-webpack5": "^6.5.16", + "@storybook/manager-webpack5": "^6.5.16", "@storybook/preset-create-react-app": "^4.0.0", - "@storybook/react": "^6.5.15", + "@storybook/react": "^6.5.16", "@types/estree": "^1.0.0", - "@types/react": "^18.0.26", - "@typescript-eslint/eslint-plugin": "^5.48.2", - "@typescript-eslint/parser": "^5.48.2", - "autoprefixer": "^10.4.13", - "babel-loader": "^9.1.2", - "eslint": "^8.32.0", - "eslint-import-resolver-typescript": "^3.5.3", - "playcanvas": "^1.60.0", + "@types/react": "^18.0.27", + "@typescript-eslint/eslint-plugin": "^5.60.0", + "@typescript-eslint/parser": "^5.60.0", + "autoprefixer": "^10.4.14", + "cross-env": "^7.0.3", + "eslint": "^8.43.0", + "eslint-import-resolver-typescript": "^3.5.5", + "playcanvas": "^1.63.1", "postcss": "^8.4.21", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -139,16 +135,15 @@ "react-dom": "^18.2.0", "react-is": "^18.2.0", "react-scripts": "^5.0.0", - "rollup": "^3.10.0", - "rollup-plugin-rename": "^1.0.1", - "rollup-plugin-sass": "^1.12.17", - "rollup-plugin-typescript2": "^0.34.1", + "rollup": "^3.21.5", + "rollup-plugin-sass": "^1.12.19", "scss-bundle": "^3.1.2", - "serve": "^14.1.2", - "stylelint": "^14.16.1", - "stylelint-config-standard-scss": "^6.1.0", - "typedoc": "^0.23.24", - "typedoc-plugin-mdn-links": "^2.0.2", + "serve": "^14.2.0", + "shx": "^0.3.4", + "stylelint": "^15.8.0", + "stylelint-config-standard-scss": "^9.0.0", + "typedoc": "^0.24.7", + "typedoc-plugin-mdn-links": "^3.0.3", "typescript": "^4.9.4" }, "directories": { diff --git a/react/tsconfig.json b/react/tsconfig.json index bba88b05..f060ca30 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -6,9 +6,9 @@ "jsx": "react", "lib": [ "es2019", - "dom" + "dom", + "dom.iterable" ], - "allowSyntheticDefaultImports" : true, "esModuleInterop" : true, "sourceMap": true, "moduleResolution": "node" diff --git a/rollup.config.mjs b/rollup.config.mjs index d2ac819c..5f333608 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,8 +1,41 @@ +import fs from 'fs'; +import { execSync } from 'child_process'; import { nodeResolve } from '@rollup/plugin-node-resolve'; +import replace from '@rollup/plugin-replace'; +import typescript from '@rollup/plugin-typescript'; import autoprefixer from 'autoprefixer'; import postcss from 'postcss'; import sass from 'rollup-plugin-sass'; -import typescript from 'rollup-plugin-typescript2'; + +/** + * @returns {string} Version string like `1.58.0-dev` + */ +const getVersion = () => { + const text = fs.readFileSync('./package.json', 'utf8'); + const json = JSON.parse(text); + return json.version; +} + +/** + * @returns {string} Revision string like `644d08d39` (9 digits/chars). + */ +const getRevision = () => { + let revision; + try { + revision = execSync('git rev-parse --short HEAD').toString().trim(); + } catch (e) { + revision = 'unknown'; + } + return revision; +} + +const replacements = { + values: { + 'PACKAGE_VERSION': getVersion(), + 'PACKAGE_REVISION': getRevision() + }, + preventAssignment: true +}; const module = { input: 'src/index.ts', @@ -15,9 +48,10 @@ const module = { }, plugins: [ nodeResolve(), + replace(replacements), typescript({ - tsconfig: 'tsconfig.json', - clean: true + noEmitOnError: true, + tsconfig: 'tsconfig.json' }) ], treeshake: 'smallest', @@ -38,9 +72,10 @@ const react_module = { }, plugins: [ nodeResolve(), + replace(replacements), typescript({ - tsconfig: 'react/tsconfig.json', - clean: true + noEmitOnError: true, + tsconfig: 'react/tsconfig.json' }) ], treeshake: 'smallest', diff --git a/src/binding/BindingBase/index.ts b/src/binding/BindingBase/index.ts index 1dc6dddd..886c14c6 100644 --- a/src/binding/BindingBase/index.ts +++ b/src/binding/BindingBase/index.ts @@ -33,11 +33,11 @@ export interface BindingBaseArgs { * Base class for data binding between {@link IBindable} {@link Element}s and Observers. */ class BindingBase extends Events { - protected _observers: Observer[]; + protected _observers: Observer[] = []; - protected _paths: string[]; + protected _paths: string[] = []; - protected _applyingChange: boolean; + protected _applyingChange = false; protected _element?: IBindable; @@ -51,22 +51,16 @@ class BindingBase extends Events { protected _historyCombine: boolean; - protected _linked: boolean; + protected _linked = false; /** * Creates a new binding. * * @param args - The arguments. */ - constructor(args: BindingBaseArgs) { + constructor(args: Readonly) { super(); - // the observers we are binding to - this._observers = []; - // the paths to use for the observers - this._paths = []; - - this._applyingChange = false; this._element = args.element; this._history = args.history; @@ -74,8 +68,6 @@ class BindingBase extends Events { this._historyPostfix = args.historyPostfix; this._historyName = args.historyName; this._historyCombine = args.historyCombine || false; - - this._linked = false; } // Returns the path at the specified index @@ -116,7 +108,7 @@ class BindingBase extends Events { /** * Clones the binding. To be implemented by derived classes. */ - clone() { + clone(): BindingBase { throw new Error('BindingBase#clone: Not implemented'); } diff --git a/src/binding/BindingElementToObservers/index.ts b/src/binding/BindingElementToObservers/index.ts index 3a810f08..b95ef079 100644 --- a/src/binding/BindingElementToObservers/index.ts +++ b/src/binding/BindingElementToObservers/index.ts @@ -27,7 +27,7 @@ class BindingElementToObservers extends BindingBase { // to the observers private _setValue(value: any, isArrayOfValues: boolean) { if (this.applyingChange) return; - if (!this._observers) return; + if (!this._observers.length) return; this.applyingChange = true; @@ -69,7 +69,6 @@ class BindingElementToObservers extends BindingBase { this.emit('history:undo', context); } }); - } execute(); @@ -176,11 +175,11 @@ class BindingElementToObservers extends BindingBase { } const execute = () => { - for (let i = 0; i < records.length; i++) { - const latest = records[i].observer.latest(); + for (const record of records) { + const latest = record.observer.latest(); if (!latest) continue; - const path = records[i].path; + const path = record.path; let history = false; if (latest.history) { @@ -188,7 +187,7 @@ class BindingElementToObservers extends BindingBase { latest.history.enabled = false; } - latest.insert(path, records[i].value); + latest.insert(path, record.value); if (history) { latest.history.enabled = true; @@ -202,11 +201,11 @@ class BindingElementToObservers extends BindingBase { redo: execute, combine: this._historyCombine, undo: () => { - for (let i = 0; i < records.length; i++) { - const latest = records[i].observer.latest(); + for (const record of records) { + const latest = record.observer.latest(); if (!latest) continue; - const path = records[i].path; + const path = record.path; let history = false; if (latest.history) { @@ -214,7 +213,7 @@ class BindingElementToObservers extends BindingBase { latest.history.enabled = false; } - latest.removeValue(path, records[i].value); + latest.removeValue(path, record.value); if (history) { latest.history.enabled = true; @@ -259,11 +258,11 @@ class BindingElementToObservers extends BindingBase { } const execute = () => { - for (let i = 0; i < records.length; i++) { - const latest = records[i].observer.latest(); + for (const record of records) { + const latest = record.observer.latest(); if (!latest) continue; - const path = records[i].path; + const path = record.path; let history = false; if (latest.history) { @@ -271,7 +270,7 @@ class BindingElementToObservers extends BindingBase { latest.history.enabled = false; } - latest.removeValue(path, records[i].value); + latest.removeValue(path, record.value); if (history) { latest.history.enabled = true; @@ -285,11 +284,11 @@ class BindingElementToObservers extends BindingBase { redo: execute, combine: this._historyCombine, undo: () => { - for (let i = 0; i < records.length; i++) { - const latest = records[i].observer.latest(); + for (const record of records) { + const latest = record.observer.latest(); if (!latest) continue; - const path = records[i].path; + const path = record.path; let history = false; if (latest.history) { @@ -297,8 +296,8 @@ class BindingElementToObservers extends BindingBase { latest.history.enabled = false; } - if (latest.get(path).indexOf(records[i].value) === -1) { - latest.insert(path, records[i].value, records[i].index); + if (latest.get(path).indexOf(record.value) === -1) { + latest.insert(path, record.value, record.index); } if (history) { diff --git a/src/binding/BindingObserversToElement/index.ts b/src/binding/BindingObserversToElement/index.ts index af001802..d194117b 100644 --- a/src/binding/BindingObserversToElement/index.ts +++ b/src/binding/BindingObserversToElement/index.ts @@ -1,11 +1,12 @@ -import { Observer } from '@playcanvas/observer'; +import { EventHandle, Observer } from '@playcanvas/observer'; +import { IBindable } from '../../components'; import BindingBase, { BindingBaseArgs } from '../BindingBase'; export interface BindingObserversToElementArgs extends BindingBaseArgs { /** * Custom update function. */ - customUpdate?: any + customUpdate?: (element: IBindable, observers: Observer[], paths: string[]) => void; } /** @@ -13,23 +14,21 @@ export interface BindingObserversToElementArgs extends BindingBaseArgs { * changes from the observers will be propagated to the element. */ class BindingObserversToElement extends BindingBase { - _customUpdate: any; + _customUpdate: (element: IBindable, observers: Observer[], paths: string[]) => void; - _events: any[]; + _events: EventHandle[] = []; - _updateTimeout: number; + _updateTimeout: number = null; /** * Creates a new BindingObserversToElement instance. * * @param args - The arguments. */ - constructor(args: BindingObserversToElementArgs = {}) { + constructor(args: Readonly = {}) { super(args); this._customUpdate = args.customUpdate; - this._events = []; - this._updateTimeout = null; } private _linkObserver(observer: Observer, path: string) { @@ -109,8 +108,8 @@ class BindingObserversToElement extends BindingBase { * Unlink the binding from its set of observers. */ unlink() { - for (let i = 0; i < this._events.length; i++) { - this._events[i].unbind(); + for (const event of this._events) { + event.unbind(); } this._events.length = 0; diff --git a/src/binding/BindingTwoWay/index.ts b/src/binding/BindingTwoWay/index.ts index 670e836a..5108ae14 100644 --- a/src/binding/BindingTwoWay/index.ts +++ b/src/binding/BindingTwoWay/index.ts @@ -21,22 +21,21 @@ export interface BindingTwoWayArgs extends BindingBaseArgs { * when the value of the Observers changes the IBindable will be updated and vice versa. */ class BindingTwoWay extends BindingBase { - _bindingElementToObservers: any; + _bindingElementToObservers: BindingElementToObservers; - _bindingObserversToElement: any; + _bindingObserversToElement: BindingObserversToElement; /** * Creates a new BindingTwoWay instance. * * @param args - The arguments. */ - constructor(args: BindingTwoWayArgs = {}) { + constructor(args: Readonly = {}) { super(args); this._bindingElementToObservers = args.bindingElementToObservers || new BindingElementToObservers(args); this._bindingObserversToElement = args.bindingObserversToElement || new BindingObserversToElement(args); - this._applyingChange = false; this._bindingElementToObservers.on('applyingChange', (value: any) => { this.applyingChange = value; }); diff --git a/src/components/ArrayInput/index.stories.tsx b/src/components/ArrayInput/index.stories.tsx index a85f84e4..0079fa19 100644 --- a/src/components/ArrayInput/index.stories.tsx +++ b/src/components/ArrayInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/ArrayInput/index.ts b/src/components/ArrayInput/index.ts index 4100e645..c1a6d671 100644 --- a/src/components/ArrayInput/index.ts +++ b/src/components/ArrayInput/index.ts @@ -1,5 +1,5 @@ +import { Observer } from '@playcanvas/observer'; import * as utils from '../../helpers/utils'; - import Element, { ElementArgs, IBindable, IBindableArgs, IFocusable } from '../Element'; import Container from '../Container'; import Panel from '../Panel'; @@ -229,8 +229,7 @@ class ArrayInput extends Element implements IFocusable, IBindable { if (this._getDefaultFn) { defaultValue = this._getDefaultFn(); } else { - // @ts-ignore - defaultValue = ArrayInput.DEFAULTS[this._valueType]; + defaultValue = ArrayInput.DEFAULTS[this._valueType as keyof typeof ArrayInput.DEFAULTS]; if (this._valueType === 'curveset') { defaultValue = utils.deepCopy(defaultValue); if (Array.isArray(this._elementArgs.curves)) { @@ -254,16 +253,14 @@ class ArrayInput extends Element implements IFocusable, IBindable { if (!array) { array = new Array(size); for (let i = 0; i < size; i++) { - // @ts-ignore - array[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType]); + array[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType as keyof typeof ArrayInput.DEFAULTS]); if (defaultValue === undefined) initDefaultValue(); array[i] = utils.deepCopy(defaultValue); } } else if (array.length < size) { const newArray = new Array(size - array.length); for (let i = 0; i < newArray.length; i++) { - // @ts-ignore - newArray[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType]); + newArray[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType as keyof typeof ArrayInput.DEFAULTS]); if (defaultValue === undefined) initDefaultValue(); newArray[i] = utils.deepCopy(defaultValue); } @@ -282,8 +279,7 @@ class ArrayInput extends Element implements IFocusable, IBindable { if (!values.length) { const array = new Array(size); for (let i = 0; i < size; i++) { - // @ts-ignore - array[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType]); + array[i] = utils.deepCopy(ArrayInput.DEFAULTS[this._valueType as keyof typeof ArrayInput.DEFAULTS]); if (defaultValue === undefined) initDefaultValue(); array[i] = utils.deepCopy(defaultValue); } @@ -518,6 +514,20 @@ class ArrayInput extends Element implements IFocusable, IBindable { this._inputSize.blur(); } + unlink() { + super.unlink(); + this._arrayElements.forEach((entry: { element: Element; }) => { + entry.element.unlink(); + }); + } + + link(observers: Observer|Observer[], paths: string|string[]) { + super.link(observers, paths); + this._arrayElements.forEach((entry: { element: Element; }, index: number) => { + this._linkArrayElement(entry.element, index); + }); + } + /** * Executes the specified function for each array element. * diff --git a/src/components/BooleanInput/index.stories.tsx b/src/components/BooleanInput/index.stories.tsx index f64606f5..4e59abd6 100644 --- a/src/components/BooleanInput/index.stories.tsx +++ b/src/components/BooleanInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/BooleanInput/index.ts b/src/components/BooleanInput/index.ts index 31210396..5071fb4e 100644 --- a/src/components/BooleanInput/index.ts +++ b/src/components/BooleanInput/index.ts @@ -1,5 +1,4 @@ import Element, { ElementArgs, IBindable, IBindableArgs, IFocusable } from '../Element'; -import Input from '../Input'; import * as pcuiClass from '../../class'; const CLASS_BOOLEAN_INPUT = 'pcui-boolean-input'; @@ -23,9 +22,11 @@ export interface BooleanInputArgs extends ElementArgs, IBindableArgs { /** * A checkbox element. */ -class BooleanInput extends Input implements IBindable, IFocusable { +class BooleanInput extends Element implements IBindable, IFocusable { protected _value: boolean; + protected _renderChanges: boolean; + constructor(args: Readonly = {}) { super({ tabIndex: 0, ...args }); @@ -145,6 +146,14 @@ class BooleanInput extends Input implements IBindable, IFocusable { this._updateValue(values[0]); } } + + set renderChanges(value: boolean) { + this._renderChanges = value; + } + + get renderChanges(): boolean { + return this._renderChanges; + } } Element.register('boolean', BooleanInput, { renderChanges: true }); diff --git a/src/components/Button/component.tsx b/src/components/Button/component.tsx index d0e7fe0f..292068af 100644 --- a/src/components/Button/component.tsx +++ b/src/components/Button/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { ButtonArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Button/index.stories.tsx b/src/components/Button/index.stories.tsx index ab30dcb2..75af516a 100644 --- a/src/components/Button/index.stories.tsx +++ b/src/components/Button/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; import { action } from '@storybook/addon-actions'; diff --git a/src/components/Canvas/component.tsx b/src/components/Canvas/component.tsx index 1dddddaa..deeba902 100644 --- a/src/components/Canvas/component.tsx +++ b/src/components/Canvas/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { CanvasArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Canvas/index.stories.tsx b/src/components/Canvas/index.stories.tsx index 72cadd0f..9f41e59c 100644 --- a/src/components/Canvas/index.stories.tsx +++ b/src/components/Canvas/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import CanvasComponent from './component'; diff --git a/src/components/Canvas/index.ts b/src/components/Canvas/index.ts index ec7d3669..6fd793fc 100644 --- a/src/components/Canvas/index.ts +++ b/src/components/Canvas/index.ts @@ -1,5 +1,7 @@ import Element, { ElementArgs } from '../Element'; +const CLASS_ROOT = 'pcui-canvas'; + /** * The arguments for the {@link Canvas} constructor. */ @@ -24,14 +26,13 @@ class Canvas extends Element { constructor(args: Readonly = {}) { super({ dom: 'canvas', ...args }); - const canvas = this._dom as HTMLCanvasElement; - canvas.classList.add('pcui-canvas'); + this.class.add(CLASS_ROOT); const { useDevicePixelRatio = false } = args; this._ratio = useDevicePixelRatio ? window.devicePixelRatio : 1; // Disable I-bar cursor on click+drag - canvas.onselectstart = (evt: Event) => { + this.dom.onselectstart = (evt: Event) => { return false; }; } diff --git a/src/components/Code/index.stories.tsx b/src/components/Code/index.stories.tsx index 562efe5f..2724da33 100644 --- a/src/components/Code/index.stories.tsx +++ b/src/components/Code/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/ColorPicker/component.tsx b/src/components/ColorPicker/component.tsx index 47b7e652..89707868 100644 --- a/src/components/ColorPicker/component.tsx +++ b/src/components/ColorPicker/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { ColorPickerArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/ColorPicker/index.stories.tsx b/src/components/ColorPicker/index.stories.tsx index d213b1ff..128ca716 100644 --- a/src/components/ColorPicker/index.stories.tsx +++ b/src/components/ColorPicker/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import ColorPickerComponent from './component'; diff --git a/src/components/ColorPicker/index.ts b/src/components/ColorPicker/index.ts index ddb4719b..5afd4fbf 100644 --- a/src/components/ColorPicker/index.ts +++ b/src/components/ColorPicker/index.ts @@ -4,11 +4,10 @@ import Element, { ElementArgs, IBindable, IBindableArgs } from '../Element'; import Overlay from '../Overlay'; import NumericInput from '../NumericInput'; import TextInput from '../TextInput'; +import * as pcuiClass from '../../class'; import { _hsv2rgb, _rgb2hsv } from '../../Math/color-value'; -const CLASS_COLOR_INPUT = 'pcui-color-input'; -const CLASS_NOT_FLEXIBLE = 'pcui-not-flexible'; -const CLASS_MULTIPLE_VALUES = 'pcui-multiple-values'; +const CLASS_ROOT = 'pcui-color-input'; /** * The arguments for the {@link ColorPicker} constructor. @@ -58,6 +57,8 @@ class ColorPicker extends Element implements IBindable { protected _pickRect: HTMLDivElement; + protected _pickRectPointerId: number = null; + protected _pickRectWhite: HTMLDivElement; protected _pickRectBlack: HTMLDivElement; @@ -66,10 +67,14 @@ class ColorPicker extends Element implements IBindable { protected _pickHue: HTMLDivElement; + protected _pickHuePointerId: number = null; + protected _pickHueHandle: HTMLDivElement; protected _pickOpacity: HTMLDivElement; + protected _pickOpacityPointerId: number = null; + protected _pickOpacityHandle: HTMLDivElement; protected _panelFields: HTMLDivElement; @@ -89,7 +94,7 @@ class ColorPicker extends Element implements IBindable { constructor(args: Readonly = {}) { super(args); - this.dom.classList.add(CLASS_COLOR_INPUT, CLASS_NOT_FLEXIBLE); + this.class.add(CLASS_ROOT, pcuiClass.NOT_FLEXIBLE); // this element shows the actual color. The // parent element shows the checkerboard pattern @@ -132,17 +137,10 @@ class ColorPicker extends Element implements IBindable { this._pickRect.classList.add('pick-rect'); this._overlay.append(this._pickRect); - this._pickRect.addEventListener('mousedown', function (evt: MouseEvent) { - this._pickRectMouseMove(evt); - - window.addEventListener('mousemove', this._pickRectMouseMove, false); - window.addEventListener('mouseup', this._pickRectMouseUp, false); - - evt.stopPropagation(); - evt.preventDefault(); - this.dragging = true; - this.emit('picker:color:start'); - }.bind(this)); + // color drag events + this._pickRect.addEventListener('pointerdown', this._pickRectPointerDown); + this._pickRect.addEventListener('pointermove', this._pickRectPointerMove); + this._pickRect.addEventListener('pointerup', this._pickRectPointerUp); // white this._pickRectWhite = document.createElement('div'); @@ -164,18 +162,10 @@ class ColorPicker extends Element implements IBindable { this._pickHue.classList.add('pick-hue'); this._overlay.append(this._pickHue); - // hue drag start - this._pickHue.addEventListener('mousedown', function (evt: MouseEvent) { - this._pickHueMouseMove(evt); - - window.addEventListener('mousemove', this._pickHueMouseMove, false); - window.addEventListener('mouseup', this._pickHueMouseUp, false); - - evt.stopPropagation(); - evt.preventDefault(); - this.dragging = true; - this.emit('picker:color:start'); - }.bind(this)); + // hue drag events + this._pickHue.addEventListener('pointerdown', this._pickHuePointerDown); + this._pickHue.addEventListener('pointermove', this._pickHuePointerMove); + this._pickHue.addEventListener('pointerup', this._pickHuePointerUp); // handle this._pickHueHandle = document.createElement('div'); @@ -187,18 +177,10 @@ class ColorPicker extends Element implements IBindable { this._pickOpacity.classList.add('pick-opacity'); this._overlay.append(this._pickOpacity); - // opacity drag start - this._pickOpacity.addEventListener('mousedown', function (evt: MouseEvent) { - this._pickOpacityMouseMove(evt); - - window.addEventListener('mousemove', this._pickOpacityMouseMove, false); - window.addEventListener('mouseup', this._pickOpacityMouseUp, false); - - evt.stopPropagation(); - evt.preventDefault(); - this.dragging = true; - this.emit('picker:color:start'); - }.bind(this)); + // opacity drag events + this._pickOpacity.addEventListener('pointerdown', this._pickOpacityPointerDown); + this._pickOpacity.addEventListener('pointermove', this._pickOpacityPointerMove); + this._pickOpacity.addEventListener('pointerup', this._pickOpacityPointerUp); // handle this._pickOpacityHandle = document.createElement('div'); @@ -238,7 +220,7 @@ class ColorPicker extends Element implements IBindable { // R const fieldR = createChannelInput('r'); fieldR.on('change', () => { - this._updateRects(); + this._onChangeRgb(); }); this._pickerChannels.push(fieldR); this._panelFields.appendChild(fieldR.dom); @@ -246,7 +228,7 @@ class ColorPicker extends Element implements IBindable { // G const fieldG = createChannelInput('g'); fieldG.on('change', () => { - this._updateRects(); + this._onChangeRgb(); }); this._pickerChannels.push(fieldG); this._panelFields.appendChild(fieldG.dom); @@ -254,7 +236,7 @@ class ColorPicker extends Element implements IBindable { // B const fieldB = createChannelInput('b'); fieldB.on('change', () => { - this._updateRects(); + this._onChangeRgb(); }); this._pickerChannels.push(fieldB); this._panelFields.appendChild(fieldB.dom); @@ -262,7 +244,7 @@ class ColorPicker extends Element implements IBindable { // A const fieldA = createChannelInput('a'); fieldA.on('change', (value: number) => { - this._updateRectAlpha(value); + this._onChangeAlpha(value); }); this._pickerChannels.push(fieldA); this._panelFields.appendChild(fieldA.dom); @@ -273,7 +255,7 @@ class ColorPicker extends Element implements IBindable { placeholder: '#' }); this._fieldHex.on('change', () => { - this._updateHex(); + this._onChangeHex(); }); this._panelFields.appendChild(this._fieldHex.dom); } @@ -285,6 +267,18 @@ class ColorPicker extends Element implements IBindable { this.dom.removeEventListener('focus', this._onFocus); this.dom.removeEventListener('blur', this._onBlur); + this._pickRect.removeEventListener('pointerdown', this._pickRectPointerDown); + this._pickRect.removeEventListener('pointermove', this._pickRectPointerMove); + this._pickRect.removeEventListener('pointerup', this._pickRectPointerUp); + + this._pickHue.removeEventListener('pointerdown', this._pickHuePointerDown); + this._pickHue.removeEventListener('pointermove', this._pickHuePointerMove); + this._pickHue.removeEventListener('pointerup', this._pickHuePointerUp); + + this._pickOpacity.removeEventListener('pointerdown', this._pickOpacityPointerDown); + this._pickOpacity.removeEventListener('pointermove', this._pickOpacityPointerMove); + this._pickOpacity.removeEventListener('pointerup', this._pickOpacityPointerUp); + super.destroy(); } @@ -452,7 +446,7 @@ class ColorPicker extends Element implements IBindable { } } - this.dom.classList.remove(CLASS_MULTIPLE_VALUES); + this.class.remove(pcuiClass.MULTIPLE_VALUES); if (dirty) { this._setValue(value); @@ -472,18 +466,37 @@ class ColorPicker extends Element implements IBindable { return hex; } + // rect drag start + protected _pickRectPointerDown = (event: PointerEvent) => { + if (this._pickRectPointerId !== null) return; + + this._pickRect.setPointerCapture(event.pointerId); + this._pickRectPointerId = event.pointerId; + + event.preventDefault(); + event.stopPropagation(); + + this.emit('picker:color:start'); + + this._pickRectPointerMove(event); + }; + // rect drag - protected _pickRectMouseMove = (evt: MouseEvent) => { + protected _pickRectPointerMove = (event: PointerEvent) => { + if (this._pickRectPointerId !== event.pointerId) return; + this._changing = true; + + // Get the pointer position relative to the element const rect = this._pickRect.getBoundingClientRect(); - const x = Math.max(0, Math.min(this._size, Math.floor(evt.clientX - rect.left))); - const y = Math.max(0, Math.min(this._size, Math.floor(evt.clientY - rect.top))); + const x = Math.max(0, Math.min(this._size, Math.floor(event.clientX - rect.left))); + const y = Math.max(0, Math.min(this._size, Math.floor(event.clientY - rect.top))); this._colorHSV[1] = x / this._size; this._colorHSV[2] = 1.0 - (y / this._size); this._directInput = false; - const rgb = _hsv2rgb([this._colorHSV[0], this._colorHSV[1], this._colorHSV[2]]); + const rgb = _hsv2rgb(this._colorHSV); for (let i = 0; i < 3; i++) { this._pickerChannels[i].value = rgb[i]; } @@ -492,22 +505,44 @@ class ColorPicker extends Element implements IBindable { this._pickRectHandle.style.left = Math.max(4, Math.min(this._size - 4, x)) + 'px'; this._pickRectHandle.style.top = Math.max(4, Math.min(this._size - 4, y)) + 'px'; + this._changing = false; }; // rect drag stop - protected _pickRectMouseUp = () => { - window.removeEventListener('mousemove', this._pickRectMouseMove, false); - window.removeEventListener('mouseup', this._pickRectMouseUp, false); - this._dragging = false; + protected _pickRectPointerUp = (event: PointerEvent) => { + if (this._pickRectPointerId !== event.pointerId) return; + this.emit('picker:color:end'); + + // Release the pointer + this._pickRect.releasePointerCapture(event.pointerId); + this._pickRectPointerId = null; + }; + + // hue drag start + protected _pickHuePointerDown = (event: PointerEvent) => { + if (this._pickHuePointerId !== null) return; + + this._pickHue.setPointerCapture(event.pointerId); + this._pickHuePointerId = event.pointerId; + + event.preventDefault(); + event.stopPropagation(); + + this.emit('picker:color:start'); + + this._pickHuePointerMove(event); }; // hue drag - protected _pickHueMouseMove = (evt: MouseEvent) => { + protected _pickHuePointerMove = (event: PointerEvent) => { + if (this._pickHuePointerId !== event.pointerId) return; + this._changing = true; + const rect = this._pickHue.getBoundingClientRect(); - const y = Math.max(0, Math.min(this._size, Math.floor(evt.clientY - rect.top))); + const y = Math.max(0, Math.min(this._size, Math.floor(event.clientY - rect.top))); const h = y / this._size; const rgb = _hsv2rgb([h, this._colorHSV[1], this._colorHSV[2]]); @@ -518,57 +553,84 @@ class ColorPicker extends Element implements IBindable { this._pickerChannels[i].value = rgb[i]; } this._fieldHex.value = this._getHex(); - this._updateRects(); + this._onChangeRgb(); this._directInput = true; + this._changing = false; }; // hue drag stop - protected _pickHueMouseUp = () => { - window.removeEventListener('mousemove', this._pickHueMouseMove, false); - window.removeEventListener('mouseup', this._pickHueMouseUp, false); - this._dragging = false; + protected _pickHuePointerUp = (event: PointerEvent) => { + if (this._pickHuePointerId !== event.pointerId) return; + this.emit('picker:color:end'); + + // Release the pointer + this._pickHue.releasePointerCapture(event.pointerId); + this._pickHuePointerId = null; + }; + + // opacity drag start + protected _pickOpacityPointerDown = (event: PointerEvent) => { + if (this._pickOpacityPointerId !== null) return; + + this._pickOpacity.setPointerCapture(event.pointerId); + this._pickOpacityPointerId = event.pointerId; + + event.preventDefault(); + event.stopPropagation(); + + this.emit('picker:color:start'); + + this._pickOpacityPointerMove(event); }; // opacity drag - protected _pickOpacityMouseMove = (evt: MouseEvent) => { + protected _pickOpacityPointerMove = (event: PointerEvent) => { + if (this._pickOpacityPointerId !== event.pointerId) return; + this._changing = true; + const rect = this._pickOpacity.getBoundingClientRect(); - const y = Math.max(0, Math.min(this._size, Math.floor(evt.clientY - rect.top))); + const y = Math.max(0, Math.min(this._size, Math.floor(event.clientY - rect.top))); const o = 1.0 - y / this._size; this._directInput = false; this._pickerChannels[3].value = Math.max(0, Math.min(255, Math.round(o * 255))); this._fieldHex.value = this._getHex(); this._directInput = true; + this._changing = false; }; - protected _pickOpacityMouseUp = () => { - window.removeEventListener('mousemove', this._pickOpacityMouseMove, false); - window.removeEventListener('mouseup', this._pickOpacityMouseUp, false); - this._dragging = false; + // opacity drag stop + protected _pickOpacityPointerUp = (event: PointerEvent) => { + if (this._pickOpacityPointerId !== event.pointerId) return; + this.emit('picker:color:end'); + + // Release the pointer + this._pickOpacity.releasePointerCapture(event.pointerId); + this._pickOpacityPointerId = null; }; - protected _updateHex() { + protected _onChangeHex() { if (!this._directInput) return; this._changing = true; - // @ts-ignore const hex = this._fieldHex.value.trim().toLowerCase(); if (/^([0-9a-f]{2}){3,4}$/.test(hex)) { for (let i = 0; i < this._channelsNumber; i++) { this._pickerChannels[i].value = parseInt(hex.slice(i * 2, i * 2 + 2), 16); } } + this._changing = false; } - protected _updateRects() { + protected _onChangeRgb() { const color = this._pickerChannels.map(function (channel) { return channel.value || 0; }).slice(0, this._channelsNumber); @@ -610,7 +672,7 @@ class ColorPicker extends Element implements IBindable { } // update alpha handle - protected _updateRectAlpha(value: number) { + protected _onChangeAlpha(value: number) { if (this._channelsNumber !== 4) return; @@ -618,7 +680,7 @@ class ColorPicker extends Element implements IBindable { this._pickOpacityHandle.style.top = Math.floor(this._size * (1.0 - (Math.max(0, Math.min(255, value)) / 255))) + 'px'; // color - this._pickOpacityHandle.style.backgroundColor = 'rgb(' + [value, value, value].join(',') + ')'; + this._pickOpacityHandle.style.backgroundColor = `rgb(${value}, ${value}, ${value})`; this.callCallback(); } @@ -676,9 +738,8 @@ class ColorPicker extends Element implements IBindable { } if (different) { - // @ts-ignore this.value = null; - this.dom.classList.add(CLASS_MULTIPLE_VALUES); + this.class.add(pcuiClass.MULTIPLE_VALUES); } else { // @ts-ignore this.value = values[0]; @@ -704,6 +765,7 @@ class ColorPicker extends Element implements IBindable { } } -Element.register('div', ColorPicker); +Element.register('rgb', ColorPicker); +Element.register('rgba', ColorPicker, { channels: 4 }); export default ColorPicker; diff --git a/src/components/ColorPicker/style.scss b/src/components/ColorPicker/style.scss index 6784abb5..464ad136 100644 --- a/src/components/ColorPicker/style.scss +++ b/src/components/ColorPicker/style.scss @@ -18,10 +18,7 @@ > div { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; } // checkerboard pattern @@ -113,6 +110,7 @@ box-sizing: border-box; margin: 8px 0 8px 8px; background-color: #f00; + touch-action: none; cursor: crosshair; > .white { @@ -156,6 +154,7 @@ margin: 8px 0 8px 8px; border: 1px solid #000; box-sizing: border-box; + touch-action: none; cursor: crosshair; background: #000; background: @@ -192,6 +191,7 @@ height: 144px; margin: 8px 0 8px 8px; border: 1px solid #000; + touch-action: none; cursor: crosshair; background: #000; background: linear-gradient(to bottom, #fff 0%, #000 100%); diff --git a/src/components/Container/component.tsx b/src/components/Container/component.tsx index 925a27db..c29048b9 100644 --- a/src/components/Container/component.tsx +++ b/src/components/Container/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { ContainerArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Container/index.stories.tsx b/src/components/Container/index.stories.tsx index 50453178..2eca5430 100644 --- a/src/components/Container/index.stories.tsx +++ b/src/components/Container/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Container from './component'; import Label from '../Label/component'; diff --git a/src/components/Container/index.ts b/src/components/Container/index.ts index 4fd27482..e77c65c6 100644 --- a/src/components/Container/index.ts +++ b/src/components/Container/index.ts @@ -122,9 +122,9 @@ class Container extends Element { protected _domResizeHandle: HTMLDivElement = null; - protected _resizeTouchId: number = null; + protected _resizePointerId: number = null; - protected _resizeData: any = null; + protected _resizeData: { x: number, y: number, width: number, height: number } = null; protected _resizeHorizontally = true; @@ -179,13 +179,9 @@ class Container extends Element { this.domContent = null; if (this._domResizeHandle) { - this._domResizeHandle.removeEventListener('mousedown', this._onResizeStart); - window.removeEventListener('mousemove', this._onResizeMove); - window.removeEventListener('mouseup', this._onResizeEnd); - - this._domResizeHandle.removeEventListener('touchstart', this._onResizeTouchStart); - window.removeEventListener('touchmove', this._onResizeTouchMove); - window.removeEventListener('touchend', this._onResizeTouchEnd); + this._domResizeHandle.removeEventListener('pointerdown', this._onResizeStart); + this._domResizeHandle.removeEventListener('pointermove', this._onResizeMove); + this._domResizeHandle.removeEventListener('pointerup', this._onResizeEnd); } super.destroy(); @@ -316,8 +312,9 @@ class Container extends Element { } if (this._domResizeHandle) { - this._domResizeHandle.removeEventListener('mousedown', this._onResizeStart); - this._domResizeHandle.removeEventListener('touchstart', this._onResizeTouchStart); + this._domResizeHandle.removeEventListener('pointerdown', this._onResizeStart); + this._domResizeHandle.removeEventListener('pointermove', this._onResizeMove); + this._domResizeHandle.removeEventListener('pointerup', this._onResizeEnd); this._domResizeHandle = null; } @@ -362,91 +359,44 @@ class Container extends Element { handle.classList.add(CLASS_RESIZABLE_HANDLE); handle.ui = this; - handle.addEventListener('mousedown', this._onResizeStart); - handle.addEventListener('touchstart', this._onResizeTouchStart, { passive: false }); + handle.addEventListener('pointerdown', this._onResizeStart); + handle.addEventListener('pointermove', this._onResizeMove); + handle.addEventListener('pointerup', this._onResizeEnd); this._domResizeHandle = handle; } - protected _onResizeStart = (evt: MouseEvent) => { + protected _onResizeStart = (evt: PointerEvent) => { + if (this._resizePointerId !== null) return; + evt.preventDefault(); evt.stopPropagation(); - window.addEventListener('mousemove', this._onResizeMove); - window.addEventListener('mouseup', this._onResizeEnd); + this._domResizeHandle.setPointerCapture(evt.pointerId); + this._resizePointerId = evt.pointerId; this._resizeStart(); }; - protected _onResizeMove = (evt: MouseEvent) => { + protected _onResizeMove = (evt: PointerEvent) => { + if (this._resizePointerId !== evt.pointerId) return; + evt.preventDefault(); evt.stopPropagation(); this._resizeMove(evt.clientX, evt.clientY); }; - protected _onResizeEnd = (evt: MouseEvent) => { - evt.preventDefault(); - evt.stopPropagation(); - - window.removeEventListener('mousemove', this._onResizeMove); - window.removeEventListener('mouseup', this._onResizeEnd); + protected _onResizeEnd = (evt: PointerEvent) => { + if (this._resizePointerId !== evt.pointerId) return; - this._resizeEnd(); - }; - - protected _onResizeTouchStart = (evt: TouchEvent) => { evt.preventDefault(); evt.stopPropagation(); - for (let i = 0; i < evt.changedTouches.length; i++) { - const touch = evt.changedTouches[i]; - if (touch.target === this._domResizeHandle) { - this._resizeTouchId = touch.identifier; - } - } - - window.addEventListener('touchmove', this._onResizeTouchMove); - window.addEventListener('touchend', this._onResizeTouchEnd); - - this._resizeStart(); - }; - - protected _onResizeTouchMove = (evt: TouchEvent) => { - for (let i = 0; i < evt.changedTouches.length; i++) { - const touch = evt.changedTouches[i]; - if (touch.identifier !== this._resizeTouchId) { - continue; - } - - evt.stopPropagation(); - evt.preventDefault(); - - this._resizeMove(touch.clientX, touch.clientY); - - break; - } - }; - - protected _onResizeTouchEnd = (evt: TouchEvent) => { - for (let i = 0; i < evt.changedTouches.length; i++) { - const touch = evt.changedTouches[i]; - if (touch.identifier === this._resizeTouchId) { - continue; - } - - this._resizeTouchId = null; - - evt.preventDefault(); - evt.stopPropagation(); - - window.removeEventListener('touchmove', this._onResizeTouchMove); - window.removeEventListener('touchend', this._onResizeTouchEnd); - - this._resizeEnd(); + this._resizeEnd(); - break; - } + this._domResizeHandle.releasePointerCapture(evt.pointerId); + this._resizePointerId = null; }; protected _resizeStart() { @@ -566,7 +516,7 @@ class Container extends Element { break; } } else if (i > childPanelIndex) { - if (y + (childPanel.height as number) >= otherTop + otherPanel.height) { + if (y + childPanel.height >= otherTop + otherPanel.height) { ind = i; break; } @@ -626,9 +576,9 @@ class Container extends Element { * @returns The recursively appended element node. * */ - protected _buildDomNode(node: { [x: string]: any; root?: any; children?: any; }) { + protected _buildDomNode(node: { [x: string]: any; root?: any; children?: any; }): Container { const keys = Object.keys(node); - let rootNode: { append: (arg0: any) => void; }; + let rootNode: Container; if (keys.includes('root')) { rootNode = this._buildDomNode(node.root); node.children.forEach((childNode: any) => { diff --git a/src/components/Container/style.scss b/src/components/Container/style.scss index b8c36ffd..8fc2c1a8 100644 --- a/src/components/Container/style.scss +++ b/src/components/Container/style.scss @@ -14,6 +14,7 @@ z-index: 1; opacity: 0; background-color: transparent; + touch-action: none; &:hover { opacity: 1; diff --git a/src/components/Divider/component.tsx b/src/components/Divider/component.tsx index 6cf458b7..b944faed 100644 --- a/src/components/Divider/component.tsx +++ b/src/components/Divider/component.tsx @@ -1,11 +1,12 @@ -import Element, { DividerArgs } from './index'; +import Element from './index'; +import { ElementArgs } from '../Element/index'; import BaseComponent from '../Element/component'; /** * Represents a vertical division between two elements */ -class Component extends BaseComponent { - constructor(props: DividerArgs) { +class Component extends BaseComponent { + constructor(props: ElementArgs) { super(props); this.elementClass = Element; } diff --git a/src/components/Divider/index.stories.tsx b/src/components/Divider/index.stories.tsx index 562efe5f..2724da33 100644 --- a/src/components/Divider/index.stories.tsx +++ b/src/components/Divider/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Divider/index.ts b/src/components/Divider/index.ts index 643ad029..a6f50e65 100644 --- a/src/components/Divider/index.ts +++ b/src/components/Divider/index.ts @@ -2,16 +2,11 @@ import Element, { ElementArgs } from '../Element'; const CLASS_ROOT = 'pcui-divider'; -/** - * The arguments for the {@link Divider} constructor. - */ -export interface DividerArgs extends ElementArgs {} - /** * Represents a vertical division between two elements. */ class Divider extends Element { - constructor(args: Readonly = {}) { + constructor(args: Readonly = {}) { super(args); this.class.add(CLASS_ROOT); diff --git a/src/components/Element/component.tsx b/src/components/Element/component.tsx index 483a6f31..cc85de25 100644 --- a/src/components/Element/component.tsx +++ b/src/components/Element/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { ElementArgs } from './index'; /** @@ -21,6 +21,8 @@ class Component

extends React.Component { onAttach?: any; + class: Set; + constructor(props: P) { super(props); this.elementClass = Element; @@ -57,6 +59,10 @@ class Component

extends React.Component { parent: undefined }); } + + const c = this.props.class; + this.class = new Set(c ? (Array.isArray(c) ? c.slice() : [c]) : undefined); + if (this.onClick) { this.element.on('click', this.onClick); } @@ -101,6 +107,21 @@ class Component

extends React.Component { // @ts-ignore this.element[prop] = this.props[prop]; } + } else if (prop === 'class') { + const c = this.props[prop]; + const classProp = new Set(c ? (Array.isArray(c) ? c.slice() : [c]) : undefined); + classProp.forEach((cls: string) => { + if (!this.class.has(cls)) { + this.element.class.add(cls); + this.class.add(cls); + } + }); + this.class.forEach((cls: string) => { + if (!classProp.has(cls)) { + this.element.class.remove(cls); + this.class.delete(cls); + } + }); } }); if (prevProps.link !== this.props.link && this.props.link) { diff --git a/src/components/Element/index.ts b/src/components/Element/index.ts index 9eb6991c..90658273 100644 --- a/src/components/Element/index.ts +++ b/src/components/Element/index.ts @@ -1,5 +1,5 @@ -import { EventHandle, Events, Observer } from '@playcanvas/observer'; -import React from 'react'; +import { EventHandle, Events, HandleEvent, Observer } from '@playcanvas/observer'; +import * as React from 'react'; import * as pcuiClass from '../../class'; import { BindingBase } from '../../binding'; @@ -22,7 +22,7 @@ const SIMPLE_CSS_PROPERTIES = [ ]; // Stores Element types by name and default arguments -const ELEMENT_REGISTRY: any = {}; +const elementRegistry: Map = new Map(); export interface IBindable { /** @@ -93,21 +93,29 @@ export interface IParentArgs { export interface IFlexArgs { /** - * Sets whether the Element supports flex layout. + * Sets whether the element uses flex layout. */ flex?: boolean, /** - * Sets whether the Element supports the flex shrink property. + * Sets the element's `flexBasis` CSS property. */ - flexShrink?: number, + flexBasis?: string, /** - * Sets whether the Element supports the flex grow property. + * Sets the element's `flexDirection` CSS property. */ - flexGrow?: number, + flexDirection?: string, /** - * Sets the Elements flex direction property. + * Sets the element's `flexGrow` CSS property. */ - flexDirection?: string, + flexGrow?: string, + /** + * Sets the element's `flexShrink` CSS property. + */ + flexShrink?: string, + /** + * Sets the element's `flexWrap` CSS property. + */ + flexWrap?: string } /** @@ -382,7 +390,7 @@ class Element extends Events { protected _suppressChange = false; - protected _binding: any; + protected _binding: BindingBase; protected _ignoreParent: boolean; @@ -483,8 +491,8 @@ class Element extends Events { if (this.parent) { const parent = this.parent; - for (let i = 0; i < this._eventsParent.length; i++) { - this._eventsParent[i].unbind(); + for (const event of this._eventsParent) { + event.unbind(); } this._eventsParent.length = 0; @@ -584,11 +592,7 @@ class Element extends Events { }; protected _onHiddenToRootChange(hiddenToRoot: boolean) { - if (hiddenToRoot) { - this.emit('hideToRoot'); - } else { - this.emit('showToRoot'); - } + this.emit(hiddenToRoot ? 'hideToRoot' : 'showToRoot'); } protected _onEnabledChange(enabled: boolean) { @@ -658,7 +662,7 @@ class Element extends Events { } } - unbind(name?: string, fn?: any): Events { + unbind(name?: string, fn?: HandleEvent): Events { return super.unbind(name, fn); } @@ -667,39 +671,33 @@ class Element extends Events { * @param cls - The actual class of the Element. * @param defaultArguments - Default arguments when creating this type. */ - static register(type: string, cls: any, defaultArguments?: any) { - ELEMENT_REGISTRY[type] = { cls, defaultArguments }; + static register(type: string, cls: new () => Type, defaultArguments?: any) { + elementRegistry.set(type, { cls, defaultArguments }); } /** * @param type - The type we want to unregister. */ static unregister(type: string) { - delete ELEMENT_REGISTRY[type]; + elementRegistry.delete(type); } /** - * Creates a new Element of the desired type. Returns undefined if type not found. + * Creates a new Element of the desired type. * * @param type - The type of the Element (registered by Element#register). * @param args - Arguments for the Element. + * @returns The new Element or undefined if type is not found. */ static create(type: string, args: ElementArgs): any { - const entry = ELEMENT_REGISTRY[type]; + const entry = elementRegistry.get(type); if (!entry) { console.error('Invalid type passed to Element.create:', type); - return; + return undefined; } const cls = entry.cls; - const clsArgs = {}; - - if (entry.defaultArguments) { - Object.assign(clsArgs, entry.defaultArguments); - } - if (args) { - Object.assign(clsArgs, args); - } + const clsArgs = { ...entry.defaultArguments, ...args }; return new cls(clsArgs); } @@ -928,7 +926,7 @@ class Element extends Events { /** * Gets / sets the Binding object for the element. */ - set binding(value: any) { + set binding(value: BindingBase) { if (this._binding === value) return; let prevObservers; @@ -946,6 +944,7 @@ class Element extends Events { this._binding = value; if (this._binding) { + // @ts-ignore this._binding.element = this; if (prevObservers && prevPaths) { this.link(prevObservers, prevPaths); @@ -953,7 +952,7 @@ class Element extends Events { } } - get binding(): any { + get binding(): BindingBase { return this._binding; } diff --git a/src/components/GradientPicker/component.tsx b/src/components/GradientPicker/component.tsx index 4296bbe5..63a54134 100644 --- a/src/components/GradientPicker/component.tsx +++ b/src/components/GradientPicker/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { GradientPickerArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/GradientPicker/index.stories.tsx b/src/components/GradientPicker/index.stories.tsx index 47f6a687..a557902b 100644 --- a/src/components/GradientPicker/index.stories.tsx +++ b/src/components/GradientPicker/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import GradientPickerComponent from './component'; diff --git a/src/components/GradientPicker/index.ts b/src/components/GradientPicker/index.ts index 619f62b8..107e9722 100644 --- a/src/components/GradientPicker/index.ts +++ b/src/components/GradientPicker/index.ts @@ -712,7 +712,6 @@ class GradientPicker extends Element { protected _onHexChanged() { if (!this._changing) { - // @ts-ignore const hex = this._hexField.value.trim().toLowerCase(); if (/^([0-9a-f]{2}){3,4}$/.test(hex)) { const rgb = [parseInt(hex.substring(0, 2), 16), diff --git a/src/components/GridView/component.tsx b/src/components/GridView/component.tsx index 05944f43..d84de3df 100644 --- a/src/components/GridView/component.tsx +++ b/src/components/GridView/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import GridViewElement, { GridViewArgs } from './index'; import GridViewItemElement from '../GridViewItem/index'; import BaseComponent from '../Element/component'; diff --git a/src/components/GridView/index.stories.tsx b/src/components/GridView/index.stories.tsx index 3de89614..1594a881 100644 --- a/src/components/GridView/index.stories.tsx +++ b/src/components/GridView/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import GridView from './component'; import GridViewItem from '../GridViewItem/component'; diff --git a/src/components/GridView/index.ts b/src/components/GridView/index.ts index 6b090f76..6de26e94 100644 --- a/src/components/GridView/index.ts +++ b/src/components/GridView/index.ts @@ -36,29 +36,25 @@ export interface GridViewArgs extends ContainerArgs { class GridView extends Container { protected _vertical: boolean; + protected _clickFn: (evt: MouseEvent, item: GridViewItem) => void; + protected _filterFn: (item: GridViewItem) => boolean; - protected _filterAnimationFrame: number; + protected _filterAnimationFrame: number = null; - protected _filterCanceled: boolean; + protected _filterCanceled = false; protected _multiSelect: boolean; protected _allowDeselect: boolean; - protected _selected: GridViewItem[]; - - protected _clickFn: any; + protected _selected: GridViewItem[] = []; constructor(args: Readonly = {}) { super(args); this._vertical = !!args.vertical; - if (this._vertical) { - this.class.add(CLASS_VERTICAL); - } else { - this.class.add(CLASS_ROOT); - } + this.class.add(this._vertical ? CLASS_VERTICAL : CLASS_ROOT); this.on('append', (element: Element) => { this._onAppendGridViewItem(element as GridViewItem); @@ -68,14 +64,10 @@ class GridView extends Container { }); this._filterFn = args.filterFn; - this._filterAnimationFrame = null; - this._filterCanceled = false; // Default options for GridView layout this._multiSelect = args.multiSelect ?? true; this._allowDeselect = args.allowDeselect ?? true; - - this._selected = []; } protected _onAppendGridViewItem(item: GridViewItem) { diff --git a/src/components/InfoBox/component.tsx b/src/components/InfoBox/component.tsx index 6d55bd09..2ccf5987 100644 --- a/src/components/InfoBox/component.tsx +++ b/src/components/InfoBox/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { InfoBoxArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/InfoBox/index.stories.tsx b/src/components/InfoBox/index.stories.tsx index e2335bac..21c321a7 100644 --- a/src/components/InfoBox/index.stories.tsx +++ b/src/components/InfoBox/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Input/index.ts b/src/components/Input/index.ts deleted file mode 100644 index 0eefefc5..00000000 --- a/src/components/Input/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import Element, { ElementArgs, IBindable } from '../Element'; - -/** - * The arguments for the {@link Input} constructor. - */ -export interface InputArgs extends ElementArgs {} - -class Input extends Element implements IBindable { - protected _renderChanges: boolean; - - set value(value: any) { - throw new Error('Not implemented!'); - } - - get value(): any { - throw new Error('Not implemented!'); - } - - set values(value: Array) { - throw new Error('Not implemented!'); - } - - get values(): Array { - throw new Error('Not implemented!'); - } - - set renderChanges(value: boolean) { - this._renderChanges = value; - } - - get renderChanges(): boolean { - return this._renderChanges; - } -} - -export default Input; diff --git a/src/components/InputElement/index.ts b/src/components/InputElement/index.ts new file mode 100644 index 00000000..0d30a719 --- /dev/null +++ b/src/components/InputElement/index.ts @@ -0,0 +1,268 @@ +import Element, { ElementArgs, IBindable, IBindableArgs, IFocusable, IPlaceholder, IPlaceholderArgs } from '../Element'; +import * as pcuiClass from '../../class'; + +const CLASS_INPUT_ELEMENT = 'pcui-input-element'; + +/** + * The arguments for the {@link InputElement} constructor. + */ +export interface InputElementArgs extends ElementArgs, IBindableArgs, IPlaceholderArgs { + /** + * Sets whether pressing Enter will blur (unfocus) the field. Defaults to `true`. + */ + blurOnEnter?: boolean, + /** + * Sets whether pressing Escape will blur (unfocus) the field. Defaults to `true`. + */ + blurOnEscape?: boolean, + /** + * Sets whether any key up event will cause a change event to be fired. + */ + keyChange?: boolean, + /** + * The input element to associate this {@link InputElement} with. If not supplied one will be created instead. + */ + input?: HTMLInputElement +} + +/** + * The InputElement is an abstract class that manages an input DOM element. It is the superclass of + * {@link TextInput} and {@link NumericInput}. It is not intended to be used directly. + */ +abstract class InputElement extends Element implements IBindable, IFocusable, IPlaceholder { + protected _domInput: HTMLInputElement; + + protected _suspendInputChangeEvt: boolean; + + protected _prevValue: string; + + protected _keyChange: boolean; + + protected _renderChanges: boolean; + + protected _blurOnEnter: boolean; + + protected _blurOnEscape: boolean; + + protected _onInputKeyDownEvt: (evt: KeyboardEvent) => void; + + protected _onInputChangeEvt: (evt: Event) => void; + + constructor(args: InputElementArgs = {}) { + super(args); + + this.class.add(CLASS_INPUT_ELEMENT); + + let input = args.input; + if (!input) { + input = document.createElement('input'); + input.type = 'text'; + } + + input.ui = this; + input.tabIndex = 0; + input.autocomplete = "off"; + + this._onInputKeyDownEvt = this._onInputKeyDown.bind(this); + this._onInputChangeEvt = this._onInputChange.bind(this); + + input.addEventListener('change', this._onInputChangeEvt); + input.addEventListener('keydown', this._onInputKeyDownEvt); + input.addEventListener('focus', this._onInputFocus); + input.addEventListener('blur', this._onInputBlur); + input.addEventListener('contextmenu', this._onInputCtxMenu, false); + + this.dom.appendChild(input); + + this._domInput = input; + + this._suspendInputChangeEvt = false; + + if (args.value !== undefined) { + this._domInput.value = args.value; + } + this.placeholder = args.placeholder ?? ''; + this.renderChanges = args.renderChanges ?? false; + this.blurOnEnter = args.blurOnEnter ?? true; + this.blurOnEscape = args.blurOnEscape ?? true; + this.keyChange = args.keyChange ?? false; + this._prevValue = null; + + this.on('change', () => { + if (this.renderChanges) { + this.flash(); + } + }); + this.on('disable', this._updateInputReadOnly); + this.on('enable', this._updateInputReadOnly); + this.on('readOnly', this._updateInputReadOnly); + + this._updateInputReadOnly(); + } + + destroy() { + if (this._destroyed) return; + + const input = this._domInput; + + input.removeEventListener('change', this._onInputChangeEvt); + input.removeEventListener('keydown', this._onInputKeyDownEvt); + input.removeEventListener('focus', this._onInputFocus); + input.removeEventListener('blur', this._onInputBlur); + input.removeEventListener('keyup', this._onInputKeyUp); + input.removeEventListener('contextmenu', this._onInputCtxMenu); + + super.destroy(); + } + + protected _onInputFocus = (evt: FocusEvent) => { + this.class.add(pcuiClass.FOCUS); + this.emit('focus', evt); + this._prevValue = this._domInput.value; + }; + + protected _onInputBlur = (evt: FocusEvent) => { + this.class.remove(pcuiClass.FOCUS); + this.emit('blur', evt); + }; + + protected _onInputKeyDown(evt: KeyboardEvent) { + if (evt.key === 'Enter' && this.blurOnEnter) { + // do not fire input change event on blur + // if keyChange is true (because a change event) + // will have already been fired before for the current + // value + this._suspendInputChangeEvt = this.keyChange; + this._domInput.blur(); + this._suspendInputChangeEvt = false; + } else if (evt.key === 'Escape') { + this._suspendInputChangeEvt = true; + const prev = this._domInput.value; + this._domInput.value = this._prevValue; + this._suspendInputChangeEvt = false; + + // manually fire change event + if (this.keyChange && prev !== this._prevValue) { + this._onInputChange(evt); + } + + if (this.blurOnEscape) { + this._domInput.blur(); + } + } + + this.emit('keydown', evt); + } + + protected _onInputChange(evt: Event) {} + + protected _onInputKeyUp = (evt: KeyboardEvent) => { + if (evt.key !== 'Escape') { + this._onInputChange(evt); + } + + this.emit('keyup', evt); + }; + + protected _onInputCtxMenu = (evt: MouseEvent) => { + this._domInput.select(); + }; + + protected _updateInputReadOnly = () => { + const readOnly = !this.enabled || this.readOnly; + if (readOnly) { + this._domInput.setAttribute('readonly', 'true'); + } else { + this._domInput.removeAttribute('readonly'); + } + }; + + focus(select?: boolean) { + this._domInput.focus(); + if (select) { + this._domInput.select(); + } + } + + blur() { + this._domInput.blur(); + } + + set placeholder(value: string) { + if (value) { + this.dom.setAttribute('placeholder', value); + } else { + this.dom.removeAttribute('placeholder'); + } + } + + get placeholder(): string { + return this.dom.getAttribute('placeholder') ?? ''; + } + + + /** + * Gets / sets the method to call when keyup is called on the input DOM element. + */ + set keyChange(value) { + if (this._keyChange === value) return; + + this._keyChange = value; + if (value) { + this._domInput.addEventListener('keyup', this._onInputKeyUp); + } else { + this._domInput.removeEventListener('keyup', this._onInputKeyUp); + } + } + + get keyChange() { + return this._keyChange; + } + + /** + * Gets the input DOM element. + */ + get input() { + return this._domInput; + } + + /** + * Gets / sets whether the input should blur when the enter key is pressed. + */ + set blurOnEnter(value: boolean) { + this._blurOnEnter = value; + } + + get blurOnEnter(): boolean { + return this._blurOnEnter; + } + + /** + * Gets / sets whether the input should blur when the escape key is pressed. + */ + set blurOnEscape(value: boolean) { + this._blurOnEscape = value; + } + + get blurOnEscape(): boolean { + return this._blurOnEscape; + } + + abstract set value(value: any); + + abstract get value(): any; + + abstract set values(value: Array); + + abstract get values(): Array; + + set renderChanges(value: boolean) { + this._renderChanges = value; + } + + get renderChanges(): boolean { + return this._renderChanges; + } +} + +export default InputElement; diff --git a/src/components/TextInput/style.scss b/src/components/InputElement/style.scss similarity index 87% rename from src/components/TextInput/style.scss rename to src/components/InputElement/style.scss index 338b8f19..ab7c3f71 100644 --- a/src/components/TextInput/style.scss +++ b/src/components/InputElement/style.scss @@ -1,4 +1,4 @@ -.pcui-text-input { +.pcui-input-element { display: inline-block; border: 1px solid $bcg-darker; border-radius: 2px; @@ -31,7 +31,7 @@ } } -.pcui-text-input.pcui-multiple-values { +.pcui-input-element.pcui-multiple-values { &::before { position: absolute; padding: 0 8px; @@ -46,7 +46,7 @@ } // focus / hover states -.pcui-text-input:not(.pcui-disabled, .pcui-readonly) { +.pcui-input-element:not(.pcui-disabled, .pcui-readonly) { &:hover { background-color: $bcg-darker; color: $text-primary; @@ -62,7 +62,7 @@ } } -.pcui-text-input { +.pcui-input-element { &.pcui-focus, &:hover { &::after, @@ -73,24 +73,24 @@ } // readonly state -.pcui-text-input.pcui-readonly { +.pcui-input-element.pcui-readonly { background-color: transparentize($color: $bcg-dark, $amount: 1 - $element-opacity-readonly); border-color: transparent; } // disabled state -.pcui-text-input.pcui-disabled { +.pcui-input-element.pcui-disabled { color: $text-darkest; } // error state -.pcui-text-input.pcui-error { +.pcui-input-element.pcui-error { color: $text-secondary; box-shadow: $element-shadow-error; } // placeholder -.pcui-text-input[placeholder] { +.pcui-input-element[placeholder] { position: relative; &::after { diff --git a/src/components/Label/component.tsx b/src/components/Label/component.tsx index 3f307576..22983e0b 100644 --- a/src/components/Label/component.tsx +++ b/src/components/Label/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { LabelArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Label/index.stories.tsx b/src/components/Label/index.stories.tsx index 4d1bbce1..fbd1b61e 100644 --- a/src/components/Label/index.stories.tsx +++ b/src/components/Label/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Label/index.ts b/src/components/Label/index.ts index 69da150b..486f577d 100644 --- a/src/components/Label/index.ts +++ b/src/components/Label/index.ts @@ -1,6 +1,5 @@ import * as pcuiClass from '../../class'; -import Element, { ElementArgs, IBindableArgs, IFlexArgs, IPlaceholder, IPlaceholderArgs } from '../Element'; -import Input from '../Input'; +import Element, { ElementArgs, IBindable, IBindableArgs, IFlexArgs, IPlaceholder, IPlaceholderArgs } from '../Element'; const CLASS_LABEL = 'pcui-label'; @@ -11,7 +10,7 @@ export interface LabelArgs extends ElementArgs, IBindableArgs, IPlaceholderArgs, /** * Sets the text of the Label. Defaults to ''. */ - text?: string | number, + text?: string, /** * If `true`, the {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML innerHTML} property will be * used to set the text. Otherwise, {@link https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent textContent} @@ -33,17 +32,19 @@ export interface LabelArgs extends ElementArgs, IBindableArgs, IPlaceholderArgs, /** * Sets the value of the Label. Defaults to ''. */ - value?: any, + value?: string } /** * The Label is a simple span element that displays some text. */ -class Label extends Input implements IPlaceholder { +class Label extends Element implements IPlaceholder, IBindable { protected _unsafe: boolean; protected _text: string; + protected _renderChanges: boolean; + constructor(args: Readonly = {}) { super({ dom: 'span', ...args }); @@ -138,6 +139,14 @@ class Label extends Input implements IPlaceholder { get placeholder(): string { return this.dom.getAttribute('placeholder'); } + + set renderChanges(value: boolean) { + this._renderChanges = value; + } + + get renderChanges(): boolean { + return this._renderChanges; + } } Element.register('label', Label); diff --git a/src/components/LabelGroup/index.stories.tsx b/src/components/LabelGroup/index.stories.tsx index 4d689932..72aa7bc6 100644 --- a/src/components/LabelGroup/index.stories.tsx +++ b/src/components/LabelGroup/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Menu/component.tsx b/src/components/Menu/component.tsx index 69b0d3da..be887b2d 100644 --- a/src/components/Menu/component.tsx +++ b/src/components/Menu/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { MenuArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Menu/index.stories.tsx b/src/components/Menu/index.stories.tsx index 7e8925a3..11df7f37 100644 --- a/src/components/Menu/index.stories.tsx +++ b/src/components/Menu/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Menu/index.ts b/src/components/Menu/index.ts index dc312661..4e88af8c 100644 --- a/src/components/Menu/index.ts +++ b/src/components/Menu/index.ts @@ -111,18 +111,18 @@ class Menu extends Container implements IFocusable { } // @ts-ignore - item._containerItems.dom.childNodes.forEach((child) => { + for (const child of item._containerItems.dom.childNodes) { this._filterMenuItems(child.ui as MenuItem); - }); + } } protected _onShowMenu() { this.focus(); // filter child menu items - this._containerMenuItems.dom.childNodes.forEach((child) => { + for (const child of this._containerMenuItems.dom.childNodes) { this._filterMenuItems(child.ui as MenuItem); - }); + } } protected _onKeyDown = (evt: KeyboardEvent) => { @@ -153,9 +153,9 @@ class Menu extends Container implements IFocusable { containerItems.style.right = '100%'; } - containerItems.dom.childNodes.forEach((child) => { + for (const child of containerItems.dom.childNodes) { this._limitSubmenuAtScreenEdges(child.ui as MenuItem); - }); + } } focus() { @@ -204,9 +204,9 @@ class Menu extends Container implements IFocusable { this._containerMenuItems.style.left = left + 'px'; this._containerMenuItems.style.top = top + 'px'; - this._containerMenuItems.dom.childNodes.forEach((child) => { + for (const child of this._containerMenuItems.dom.childNodes) { this._limitSubmenuAtScreenEdges(child.ui as MenuItem); - }); + } } /** diff --git a/src/components/Menu/style.scss b/src/components/Menu/style.scss index 04c5fad1..1de6ad3c 100644 --- a/src/components/Menu/style.scss +++ b/src/components/Menu/style.scss @@ -1,10 +1,7 @@ .pcui-menu { position: absolute; z-index: 401; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; width: auto; height: auto; } diff --git a/src/components/MenuItem/index.ts b/src/components/MenuItem/index.ts index d64c6d1c..7cacfb6a 100644 --- a/src/components/MenuItem/index.ts +++ b/src/components/MenuItem/index.ts @@ -65,11 +65,11 @@ class MenuItem extends Container implements IBindable { protected _menu: any = null; - protected _onSelect: any; + protected _onSelect: (evt?: MouseEvent) => void; - protected _onIsEnabled: any; + protected _onIsEnabled: () => boolean; - protected _onIsVisible: any; + protected _onIsVisible: () => boolean; protected _renderChanges: boolean; @@ -250,11 +250,11 @@ class MenuItem extends Container implements IBindable { // set menu on child menu items if (!this._containerItems.destroyed) { - this._containerItems.dom.childNodes.forEach((child) => { + for (const child of this._containerItems.dom.childNodes) { if (child.ui instanceof MenuItem) { child.ui.menu = value; } - }); + } } } diff --git a/src/components/NumericInput/component.tsx b/src/components/NumericInput/component.tsx index 157f309d..52603ccc 100644 --- a/src/components/NumericInput/component.tsx +++ b/src/components/NumericInput/component.tsx @@ -10,18 +10,6 @@ class Component extends BaseComponent { constructor(props: NumericInputArgs) { super(props); this.elementClass = Element; - - if (props.onValidate) { - this.onValidate = props.onValidate; - } - - this.onAttach = this.onAttachFn.bind(this); - } - - onAttachFn() { - if (this.onValidate) { - this.element.onValidate = this.onValidate; - } } render() { diff --git a/src/components/NumericInput/index.stories.tsx b/src/components/NumericInput/index.stories.tsx index b2a558e7..b867aa30 100644 --- a/src/components/NumericInput/index.stories.tsx +++ b/src/components/NumericInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/NumericInput/index.ts b/src/components/NumericInput/index.ts index 182d57c9..c89065a8 100644 --- a/src/components/NumericInput/index.ts +++ b/src/components/NumericInput/index.ts @@ -1,5 +1,5 @@ import Element from '../Element'; -import TextInput, { TextInputArgs } from '../TextInput'; +import InputElement, { InputElementArgs } from '../InputElement'; import * as pcuiClass from '../../class'; const CLASS_NUMERIC_INPUT = 'pcui-numeric-input'; @@ -12,7 +12,7 @@ const REGEX_COMMA = /,/g; /** * The arguments for the {@link NumericInput} constructor. */ -export interface NumericInputArgs extends TextInputArgs { +export interface NumericInputArgs extends InputElementArgs { /** * Sets the minimum value this field can take. */ @@ -22,7 +22,7 @@ export interface NumericInputArgs extends TextInputArgs { */ max?: number, /** - * Sets the decimal precision of this field. Defaults to 7. + * Sets the decimal precision of this field. Defaults to 2. */ precision?: number, /** @@ -46,7 +46,7 @@ export interface NumericInputArgs extends TextInputArgs { /** * The NumericInput represents an input element that holds numbers. */ -class NumericInput extends TextInput { +class NumericInput extends InputElement { protected _min: number; protected _max: number; @@ -59,7 +59,7 @@ class NumericInput extends TextInput { protected _stepPrecision: number; - protected _oldValue: any; + protected _oldValue: number; protected _historyCombine: boolean; @@ -90,8 +90,10 @@ class NumericInput extends TextInput { if (Number.isFinite(args.step)) { this._step = args.step; - } else { + } else if (args.precision) { this._step = 10 / Math.pow(10, args.precision); + } else { + this._step = 1; } if (Number.isFinite(args.stepPrecision)) { @@ -101,7 +103,11 @@ class NumericInput extends TextInput { } this._oldValue = undefined; - this.value = args.value; + if (Number.isFinite(args.value)) { + this.value = args.value; + } else if (!this._allowNull) { + this.value = 0; + } this._historyCombine = false; this._historyPostfix = null; @@ -178,22 +184,21 @@ class NumericInput extends TextInput { this._historyCombine = false; this._historyPostfix = null; } + this.focus(); }; - protected _onInputChange(evt: any) { - // get the content of the input and pass it - // @ts-ignore through our value setter - this.value = this._domInput.value; + protected _onInputChange(evt: Event) { + // get the content of the input, normalize it and set it as the current value + this.value = this._normalizeValue(this._domInput.value); } protected _onInputKeyDown(evt: KeyboardEvent) { - if (!this.enabled || this.readOnly) return super._onInputKeyDown(evt); + if (!this.enabled || this.readOnly) return; // increase / decrease value with arrow keys if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown') { const inc = evt.key === 'ArrowDown' ? -1 : 1; this.value += (evt.shiftKey ? this._stepPrecision : this._step) * inc; - return; } super._onInputKeyDown(evt); @@ -219,6 +224,9 @@ class NumericInput extends TextInput { protected _normalizeValue(value: any) { try { if (typeof value === 'string') { + // check for 0 + if (value === '0') return 0; + // replace commas with dots (for some international keyboards) value = value.replace(REGEX_COMMA, '.'); @@ -269,13 +277,17 @@ class NumericInput extends TextInput { return value; } - protected _updateValue(value: any, force?: boolean) { + protected _updateValue(value: number, force?: boolean) { const different = (value !== this._oldValue || force); // always set the value to the input because // we always want it to show an actual number or nothing this._oldValue = value; - this._domInput.value = value; + if (value === null) { + this._domInput.value = ''; + } else { + this._domInput.value = String(value); + } this.class.remove(pcuiClass.MULTIPLE_VALUES); @@ -286,9 +298,8 @@ class NumericInput extends TextInput { return different; } - set value(value) { + set value(value: number) { value = this._normalizeValue(value); - const forceUpdate = this.class.contains(pcuiClass.MULTIPLE_VALUES) && value === null && this._allowNull; const changed = this._updateValue(value, forceUpdate); @@ -300,9 +311,8 @@ class NumericInput extends TextInput { } } - get value() { - const val = super.value; - // @ts-ignore + get value() : number { + const val = this._domInput.value; return val !== '' ? parseFloat(val) : null; } diff --git a/src/components/Overlay/index.stories.tsx b/src/components/Overlay/index.stories.tsx index 3b348247..934ce85c 100644 --- a/src/components/Overlay/index.stories.tsx +++ b/src/components/Overlay/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Overlay/style.scss b/src/components/Overlay/style.scss index cb4e6c72..6cba4280 100644 --- a/src/components/Overlay/style.scss +++ b/src/components/Overlay/style.scss @@ -1,10 +1,7 @@ .pcui-overlay { width: auto; height: auto; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; z-index: 101; transition: opacity 100ms, visibility 100ms; @@ -21,10 +18,7 @@ position: absolute; width: auto; height: auto; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; background-color: rgba($bcg-darkest, 0.7); } diff --git a/src/components/Panel/component.tsx b/src/components/Panel/component.tsx index 0c2cfc7c..5de37da6 100644 --- a/src/components/Panel/component.tsx +++ b/src/components/Panel/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { PanelArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Panel/index.stories.tsx b/src/components/Panel/index.stories.tsx index 2a3e521e..2b10b295 100644 --- a/src/components/Panel/index.stories.tsx +++ b/src/components/Panel/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/Panel/index.ts b/src/components/Panel/index.ts index 2d466f6b..ca46ecc9 100644 --- a/src/components/Panel/index.ts +++ b/src/components/Panel/index.ts @@ -122,6 +122,8 @@ class Panel extends Container { protected _headerSize: number; + protected _onDragEndEvt: (event: MouseEvent) => void; + /** * Creates a new Panel. * @@ -170,6 +172,8 @@ class Panel extends Container { // execute reflow now after all fields have been initialized this._suspendReflow = false; this._reflow(); + + this._onDragEndEvt = this._onDragEnd.bind(this); } destroy() { @@ -180,8 +184,8 @@ class Panel extends Container { this._reflowTimeout = null; } - window.removeEventListener('mouseup', this._onDragEnd); - window.removeEventListener('mouseleave', this._onDragEnd); + window.removeEventListener('mouseup', this._onDragEndEvt); + window.removeEventListener('mouseleave', this._onDragEndEvt); window.removeEventListener('mousemove', this._onDragMove); super.destroy(); @@ -217,12 +221,12 @@ class Panel extends Container { this.collapsed = !this.collapsed; }; - protected _onClickRemove = (evt: MouseEvent) => { + protected _onClickRemove(evt: MouseEvent) { evt.preventDefault(); evt.stopPropagation(); this.emit('click:remove'); - }; + } protected _initializeContent(args: PanelArgs) { // containers container @@ -311,8 +315,8 @@ class Panel extends Container { evt.stopPropagation(); evt.preventDefault(); - window.addEventListener('mouseup', this._onDragEnd); - window.addEventListener('mouseleave', this._onDragEnd); + window.addEventListener('mouseup', this._onDragEndEvt); + window.addEventListener('mouseleave', this._onDragEndEvt); window.addEventListener('mousemove', this._onDragMove); this.emit('dragstart'); @@ -332,9 +336,9 @@ class Panel extends Container { } }; - protected _onDragEnd = (evt: MouseEvent) => { - window.removeEventListener('mouseup', this._onDragEnd); - window.removeEventListener('mouseleave', this._onDragEnd); + protected _onDragEnd(evt: MouseEvent) { + window.removeEventListener('mouseup', this._onDragEndEvt); + window.removeEventListener('mouseleave', this._onDragEndEvt); window.removeEventListener('mousemove', this._onDragMove); if (this._draggedChild === this) { @@ -347,7 +351,7 @@ class Panel extends Container { // @ts-ignore accessing protected methods this.parent._onChildDragEnd(evt, this); } - }; + } /** * Gets / sets whether the Element is collapsible. @@ -431,7 +435,7 @@ class Panel extends Container { icon: 'E289', class: CLASS_PANEL_REMOVE }); - this._btnRemove.on('click', this._onClickRemove); + this._btnRemove.on('click', this._onClickRemove.bind(this)); this.header.append(this._btnRemove); } else { this._btnRemove.destroy(); diff --git a/src/components/Panel/style.scss b/src/components/Panel/style.scss index 8fe76552..f0216074 100644 --- a/src/components/Panel/style.scss +++ b/src/components/Panel/style.scss @@ -40,11 +40,9 @@ // expanded icon &::before { - left: 0; - content: '\E179'; - @extend .font-icon; + content: '\E179'; font-size: 14px; margin-right: 10px; text-align: center; diff --git a/src/components/Progress/index.stories.tsx b/src/components/Progress/index.stories.tsx index 562efe5f..2724da33 100644 --- a/src/components/Progress/index.stories.tsx +++ b/src/components/Progress/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/RadioButton/index.stories.tsx b/src/components/RadioButton/index.stories.tsx index 4d0220a5..a24e887b 100644 --- a/src/components/RadioButton/index.stories.tsx +++ b/src/components/RadioButton/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/SelectInput/component.tsx b/src/components/SelectInput/component.tsx index ffccdbc0..8fd7472f 100644 --- a/src/components/SelectInput/component.tsx +++ b/src/components/SelectInput/component.tsx @@ -5,9 +5,22 @@ import BaseComponent from '../Element/component'; * An input that allows selecting from a dropdown or entering tags. */ class Component extends BaseComponent { + onSelect?: (value: string) => void; + constructor(props: SelectInputArgs) { super(props); this.elementClass = Element; + + this.onSelect = props.onSelect; + + this.onAttach = this.onAttachFn.bind(this); + + } + + onAttachFn() { + if (this.props.onSelect) { + this.element.on('select', this.onSelect); + } } render() { diff --git a/src/components/SelectInput/index.stories.tsx b/src/components/SelectInput/index.stories.tsx index b2a558e7..b867aa30 100644 --- a/src/components/SelectInput/index.stories.tsx +++ b/src/components/SelectInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/SelectInput/index.ts b/src/components/SelectInput/index.ts index 718c9c06..535ae2f1 100644 --- a/src/components/SelectInput/index.ts +++ b/src/components/SelectInput/index.ts @@ -9,6 +9,8 @@ import { searchItems } from '../../helpers/search'; const CLASS_SELECT_INPUT = 'pcui-select-input'; const CLASS_SELECT_INPUT_CONTAINER_VALUE = CLASS_SELECT_INPUT + '-container-value'; const CLASS_MULTI_SELECT = CLASS_SELECT_INPUT + '-multi'; +const CLASS_DISABLED_VALUE = CLASS_SELECT_INPUT + '-disabled-value'; +const CLASS_HAS_DISABLED_VALUE = CLASS_SELECT_INPUT + '-has-disabled-value'; const CLASS_ALLOW_INPUT = 'pcui-select-input-allow-input'; const CLASS_VALUE = CLASS_SELECT_INPUT + '-value'; const CLASS_ICON = CLASS_SELECT_INPUT + '-icon'; @@ -75,6 +77,22 @@ export interface SelectInputArgs extends ElementArgs, IBindableArgs, IPlaceholde * The type of each value. Can be one of 'string', 'number' or 'boolean'. Defaults to 'string'. */ type?: 'string' | 'number' | 'boolean'; + /** + * The order that the options should be checked in to find a valid fallback option that isn't included in the disabledOptions object. + */ + fallbackOrder?: Array; + /** + * All the option values that should be disabled. The keys of the object are the values of the options and the values are the text to show when the option is disabled. + */ + disabledOptions?: Record; + /** + * If provided, this function will be called each time an option is selected. + */ + onSelect?: (value: string) => void; + /** + * Text to display in the SelectInput before the selected option. + */ + prefix?: string; } @@ -130,7 +148,7 @@ class SelectInput extends Element implements IBindable, IFocusable { protected _value: any; - protected _createLabelContainer: any; + protected _createLabelContainer: Container; protected _options: any; @@ -138,6 +156,16 @@ class SelectInput extends Element implements IBindable, IFocusable { protected _renderChanges: boolean; + protected _disabledOptions: Record = {}; + + protected _fallbackOrder: Array; + + protected _disabledValue: string; + + protected _onSelect: (value: string) => void; + + protected _prefix = ''; + constructor(args: Readonly = {}) { // main container const container = new Container({ @@ -273,6 +301,11 @@ class SelectInput extends Element implements IBindable, IFocusable { }); this._updateInputFieldsVisibility(false); + + this._onSelect = args.onSelect; + this.fallbackOrder = args.fallbackOrder; + this.disabledOptions = args.disabledOptions; + this._prefix = args.prefix ?? ''; } destroy() { @@ -304,12 +337,12 @@ class SelectInput extends Element implements IBindable, IFocusable { }); const label = new Label({ - text: (this._input.value as string), + text: this._input.value, tabIndex: -1 }); container.append(label); - let evtChange: any = this._input.on('change', (value) => { + let evtChange = this._input.on('change', (value) => { // check if label is destroyed // during change event if (label.destroyed) return; @@ -468,7 +501,7 @@ class SelectInput extends Element implements IBindable, IFocusable { // when the value is changed show the correct title protected _onValueChange(value: any) { if (!this.multiSelect) { - this._labelValue.value = this._valueToText[value] || ''; + this._labelValue.value = this._prefix + (this._valueToText[value] || ''); value = '' + value; for (const key in this._valueToLabel) { @@ -571,7 +604,7 @@ class SelectInput extends Element implements IBindable, IFocusable { return container; } - protected _removeTag(tagElement: any, value: string) { + protected _removeTag(tagElement: Container, value: string) { tagElement.destroy(); const label = this._valueToLabel[value]; @@ -957,8 +990,58 @@ class SelectInput extends Element implements IBindable, IFocusable { } } + _updateValue(value: string) { + if (value === this._value) return; + this._value = value; + this._onValueChange(value); + + if (!this._suppressChange) { + this.emit('change', value); + } + + if (this._binding) { + this._binding.setValue(value); + } + } + + _updateDisabledValue(value: string) { + const labels: Record = {}; + this._containerOptions.forEachChild((label: Label) => { + labels[label.dom.id] = label; + if (this._disabledOptions[label.dom.id]) { + label.enabled = false; + label.text = this._disabledOptions[label.dom.id]; + } else { + label.enabled = true; + label.text = this._valueToText[label.dom.id]; + } + label.class.remove(CLASS_DISABLED_VALUE); + }); + + const disabledValue = this._disabledOptions[value] ? value : null; + let newValue = null; + if (disabledValue) { + if (this._fallbackOrder) { + for (let i = 0; i < this._fallbackOrder.length; i++) { + if (this._fallbackOrder[i] === value) continue; + newValue = this._fallbackOrder[i]; + break; + } + } + this.disabledValue = disabledValue; + labels[disabledValue].class.add(CLASS_DISABLED_VALUE); + } else if (this._disabledValue) { + newValue = this._disabledValue; + this.disabledValue = null; + } else { + newValue = value; + this.disabledValue = null; + } + return newValue; + } + set options(value) { - if (this._options && this._options === value) return; + if (this._options && JSON.stringify(this._options) === JSON.stringify(value)) return; this._containerOptions.clear(); this._labelHighlighted = null; @@ -973,7 +1056,8 @@ class SelectInput extends Element implements IBindable, IFocusable { const label = new Label({ text: option.t, - tabIndex: -1 + tabIndex: -1, + id: option.v }); this._labelToValue.set(label, option.v); @@ -986,6 +1070,9 @@ class SelectInput extends Element implements IBindable, IFocusable { e.stopPropagation(); this._onSelectValue(option.v); this.close(); + if (this._onSelect) { + this._onSelect(this.value); + } }); this._containerOptions.append(label); } @@ -1018,6 +1105,26 @@ class SelectInput extends Element implements IBindable, IFocusable { return this._invalidOptions; } + set disabledValue(value: string | null) { + this._disabledValue = value; + if (this._disabledValue !== null) { + this.class.add(CLASS_HAS_DISABLED_VALUE); + } else { + this.class.remove(CLASS_HAS_DISABLED_VALUE); + } + } + + set disabledOptions(value: any) { + if (JSON.stringify(this._disabledOptions) === JSON.stringify(value)) return; + this._disabledOptions = value || {}; + const newValue = this._updateDisabledValue(this._value); + this._updateValue(newValue); + } + + set fallbackOrder(value: string[]) { + this._fallbackOrder = value || null; + } + get multiSelect() { return this.class.contains(CLASS_MULTI_SELECT); } @@ -1046,16 +1153,8 @@ class SelectInput extends Element implements IBindable, IFocusable { } } - this._value = value; - this._onValueChange(value); - - if (!this._suppressChange) { - this.emit('change', value); - } - - if (this._binding) { - this._binding.setValue(value); - } + this.disabledValue = null; + this._updateValue(value); } get value() { diff --git a/src/components/SelectInput/style.scss b/src/components/SelectInput/style.scss index e40fa107..b9d09da1 100644 --- a/src/components/SelectInput/style.scss +++ b/src/components/SelectInput/style.scss @@ -84,6 +84,27 @@ } } +.pcui-select-input-has-disabled-value .pcui-container.pcui-select-input-list .pcui-label.pcui-selected { + &::after { + font-family: inherit; + content: 'fallback'; + color: $text-primary; + font-size: 10px; + position: absolute; + right: 6px; + } +} + +.pcui-label.pcui-select-input-disabled-value { + &::after { + @extend .font-icon; + + content: '\e133' !important; + position: absolute; + right: 6px; + } +} + .pcui-select-input.pcui-open { .pcui-select-input-shadow { box-shadow: $element-shadow-hover; @@ -264,6 +285,12 @@ opacity: $element-opacity-disabled; } +.pcui-select-input { + .pcui-label.pcui-disabled { + opacity: $element-opacity-disabled; + } +} + .pcui-select-input.pcui-readonly { .pcui-select-input-icon { display: none; diff --git a/src/components/SliderInput/index.stories.tsx b/src/components/SliderInput/index.stories.tsx index 27c775b3..539a45eb 100644 --- a/src/components/SliderInput/index.stories.tsx +++ b/src/components/SliderInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/SliderInput/index.ts b/src/components/SliderInput/index.ts index 1a57414a..3a4c41c0 100644 --- a/src/components/SliderInput/index.ts +++ b/src/components/SliderInput/index.ts @@ -1,4 +1,4 @@ -import Element, { ElementArgs, IBindable, IBindableArgs, IFlexArgs, IFocusable } from '../Element'; +import Element, { ElementArgs, IBindable, IBindableArgs, IFlexArgs, IFocusable, IPlaceholder, IPlaceholderArgs } from '../Element'; import NumericInput from '../NumericInput'; import * as pcuiClass from '../../class'; @@ -13,13 +13,17 @@ const IS_CHROME = /Chrome\//.test(navigator.userAgent); /** * The arguments for the {@link SliderInput} constructor. */ -export interface SliderInputArgs extends ElementArgs, IBindableArgs, IFlexArgs { +export interface SliderInputArgs extends ElementArgs, IBindableArgs, IFlexArgs, IPlaceholderArgs { /** - * Sets the minimum value that the numeric input field can take. Defaults to 0. + * Sets whether any key up event will cause a change event to be fired. + */ + keyChange?: boolean, + /** + * Sets the minimum value that the numeric input field can take. */ min?: number, /** - * Sets the maximum value that the numeric input field can take. Defaults to 1. + * Sets the maximum value that the numeric input field can take. */ max?: number, /** @@ -49,7 +53,7 @@ export interface SliderInputArgs extends ElementArgs, IBindableArgs, IFlexArgs { * The SliderInput shows a NumericInput and a slider widget next to it. It acts as a proxy of the * NumericInput. */ -class SliderInput extends Element implements IBindable, IFocusable { +class SliderInput extends Element implements IBindable, IFocusable, IPlaceholder { protected _historyCombine = false; protected _historyPostfix: string = null; @@ -83,11 +87,9 @@ class SliderInput extends Element implements IBindable, IFocusable { const numericInput = new NumericInput({ allowNull: args.allowNull, hideSlider: true, - min: args.min ?? 0, - max: args.max ?? 1, - // @ts-ignore + min: args.min, + max: args.max, keyChange: args.keyChange, - // @ts-ignore placeholder: args.placeholder, precision: args.precision ?? 2, renderChanges: args.renderChanges, diff --git a/src/components/Spinner/component.tsx b/src/components/Spinner/component.tsx index 88f36b35..e36e5fbb 100644 --- a/src/components/Spinner/component.tsx +++ b/src/components/Spinner/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Element, { SpinnerArgs } from './index'; import BaseComponent from '../Element/component'; diff --git a/src/components/Spinner/index.stories.tsx b/src/components/Spinner/index.stories.tsx index d59b1ed4..9ec8e260 100644 --- a/src/components/Spinner/index.stories.tsx +++ b/src/components/Spinner/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import SpinnerComponent from './component'; export default { diff --git a/src/components/TextAreaInput/index.stories.tsx b/src/components/TextAreaInput/index.stories.tsx index b2a558e7..b867aa30 100644 --- a/src/components/TextAreaInput/index.stories.tsx +++ b/src/components/TextAreaInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/TextInput/index.stories.tsx b/src/components/TextInput/index.stories.tsx index b2a558e7..b867aa30 100644 --- a/src/components/TextInput/index.stories.tsx +++ b/src/components/TextInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/TextInput/index.ts b/src/components/TextInput/index.ts index 8be303b7..f0e79e2a 100644 --- a/src/components/TextInput/index.ts +++ b/src/components/TextInput/index.ts @@ -1,5 +1,5 @@ -import Element, { IBindableArgs, IFocusable, IPlaceholder, IPlaceholderArgs } from '../Element'; -import Input, { InputArgs } from '../Input'; +import Element, { IBindableArgs, IPlaceholderArgs } from '../Element'; +import InputElement, { InputElementArgs } from '../InputElement'; import * as pcuiClass from '../../class'; const CLASS_TEXT_INPUT = 'pcui-text-input'; @@ -7,124 +7,29 @@ const CLASS_TEXT_INPUT = 'pcui-text-input'; /** * The arguments for the {@link TextInput} constructor. */ -export interface TextInputArgs extends InputArgs, IBindableArgs, IPlaceholderArgs { +export interface TextInputArgs extends InputElementArgs, IBindableArgs, IPlaceholderArgs { /** - * Sets whether pressing Enter will blur (unfocus) the field. Defaults to `true`. - */ - blurOnEnter?: boolean, - /** - * Sets whether pressing Escape will blur (unfocus) the field. Defaults to `true`. - */ - blurOnEscape?: boolean, - /** - * Sets whether any key up event will cause a change event to be fired. - */ - keyChange?: boolean, - /** - * A function that validates the value that is entered into the input and returns `true` if it is valid or `false` otherwise. - * If `false` then the input will be set in an error state and the value will not propagate to the binding. + * A function that validates the value that is entered into the input and returns `true` if it + * is valid or `false` otherwise. If `false` then the input will be set in an error state and + * the value will not propagate to the binding. */ onValidate?: (value: string) => boolean, - /** - * The input element to associate this {@link TextInput} with. If not supplied one will be created instead. - */ - input?: HTMLInputElement } /** * The TextInput is an input element of type text. */ -class TextInput extends Input implements IFocusable, IPlaceholder { - protected _domInput: HTMLInputElement; - - protected _suspendInputChangeEvt: boolean; - - protected _prevValue: any; - - protected _onValidate: any; - - protected _keyChange: boolean; - - protected _renderChanges: boolean; - - protected _blurOnEnter: boolean; - - protected _blurOnEscape: boolean; - - protected _onInputKeyDownEvt: (evt: KeyboardEvent) => void; - - protected _onInputChangeEvt: (evt: Event) => void; +class TextInput extends InputElement { + protected _onValidate: (value: string) => boolean; constructor(args: Readonly = {}) { super(args); this.class.add(CLASS_TEXT_INPUT); - let input = args.input; - if (!input) { - input = document.createElement('input'); - input.type = 'text'; - } - - input.ui = this; - input.tabIndex = 0; - input.autocomplete = "off"; - - this._onInputKeyDownEvt = this._onInputKeyDown.bind(this); - this._onInputChangeEvt = this._onInputChange.bind(this); - - input.addEventListener('change', this._onInputChangeEvt); - input.addEventListener('focus', this._onInputFocus); - input.addEventListener('blur', this._onInputBlur); - input.addEventListener('keydown', this._onInputKeyDownEvt); - input.addEventListener('contextmenu', this._onInputCtxMenu, false); - - this.dom.appendChild(input); - - this._domInput = input; - - this._suspendInputChangeEvt = false; - - if (args.value !== undefined) { - this.value = args.value; - } - this.placeholder = args.placeholder ?? null; - this.renderChanges = args.renderChanges ?? false; - this.blurOnEnter = args.blurOnEnter ?? true; - this.blurOnEscape = args.blurOnEscape ?? true; - this.keyChange = args.keyChange ?? false; - this._prevValue = null; - if (args.onValidate) { this.onValidate = args.onValidate; } - - this.on('change', () => { - if (this.renderChanges) { - this.flash(); - } - }); - this.on('disable', this._updateInputReadOnly); - this.on('enable', this._updateInputReadOnly); - this.on('readOnly', this._updateInputReadOnly); - - this._updateInputReadOnly(); - } - - destroy() { - if (this._destroyed) return; - - const input = this._domInput; - input.removeEventListener('change', this._onInputChangeEvt); - input.removeEventListener('focus', this._onInputFocus); - input.removeEventListener('blur', this._onInputBlur); - input.removeEventListener('keydown', this._onInputKeyDownEvt); - input.removeEventListener('keyup', this._onInputKeyUp); - input.removeEventListener('contextmenu', this._onInputCtxMenu); - - this._domInput = null; - - super.destroy(); } protected _onInputChange(evt: Event) { @@ -147,67 +52,7 @@ class TextInput extends Input implements IFocusable, IPlaceholder { } } - protected _onInputFocus = (evt: FocusEvent) => { - this.class.add(pcuiClass.FOCUS); - this.emit('focus', evt); - this._prevValue = this.value; - }; - - protected _onInputBlur = (evt: FocusEvent) => { - this.class.remove(pcuiClass.FOCUS); - this.emit('blur', evt); - }; - - protected _onInputKeyDown(evt: KeyboardEvent) { - if (evt.key === 'Enter' && this.blurOnEnter) { - // do not fire input change event on blur - // if keyChange is true (because a change event) - // will have already been fired before for the current - // value - this._suspendInputChangeEvt = this.keyChange; - this._domInput.blur(); - this._suspendInputChangeEvt = false; - } else if (evt.key === 'Escape') { - this._suspendInputChangeEvt = true; - const prev = this._domInput.value; - this._domInput.value = this._prevValue; - this._suspendInputChangeEvt = false; - - // manually fire change event - if (this.keyChange && prev !== this._prevValue) { - this._onInputChange(evt); - } - - if (this.blurOnEscape) { - this._domInput.blur(); - } - } - - this.emit('keydown', evt); - } - - protected _onInputKeyUp = (evt: KeyboardEvent) => { - if (evt.key !== 'Escape') { - this._onInputChange(evt); - } - - this.emit('keyup', evt); - }; - - protected _onInputCtxMenu = (evt: MouseEvent) => { - this._domInput.select(); - }; - - protected _updateInputReadOnly = () => { - const readOnly = !this.enabled || this.readOnly; - if (readOnly) { - this._domInput.setAttribute('readonly', 'true'); - } else { - this._domInput.removeAttribute('readonly'); - } - }; - - protected _updateValue(value: string | number | Array) { + protected _updateValue(value: string | Array) { this.class.remove(pcuiClass.MULTIPLE_VALUES); if (value && typeof (value) === 'object') { @@ -239,18 +84,7 @@ class TextInput extends Input implements IFocusable, IPlaceholder { return true; } - focus(select?: boolean) { - this._domInput.focus(); - if (select) { - this._domInput.select(); - } - } - - blur() { - this._domInput.blur(); - } - - set value(value: string | number | Array) { + set value(value: string | Array) { const changed = this._updateValue(value); if (changed) { @@ -263,12 +97,12 @@ class TextInput extends Input implements IFocusable, IPlaceholder { } } - get value(): string | number { + get value(): string { return this._domInput.value; } /* eslint accessor-pairs: 0 */ - set values(values: Array) { + set values(values: Array) { const different = values.some(v => v !== values[0]); if (different) { @@ -279,44 +113,6 @@ class TextInput extends Input implements IFocusable, IPlaceholder { } } - set placeholder(value) { - if (value) { - this.dom.setAttribute('placeholder', value); - } else { - this.dom.removeAttribute('placeholder'); - } - } - - get placeholder() { - return this.dom.getAttribute('placeholder'); - } - - - /** - * Gets / sets the method to call when keyup is called on the input DOM element. - */ - set keyChange(value) { - if (this._keyChange === value) return; - - this._keyChange = value; - if (value) { - this._domInput.addEventListener('keyup', this._onInputKeyUp); - } else { - this._domInput.removeEventListener('keyup', this._onInputKeyUp); - } - } - - get keyChange() { - return this._keyChange; - } - - /** - * Gets the input DOM element. - */ - get input() { - return this._domInput; - } - /** * Gets / sets the validate method for the input. */ @@ -327,28 +123,6 @@ class TextInput extends Input implements IFocusable, IPlaceholder { get onValidate() { return this._onValidate; } - - /** - * Gets / sets whether the input should blur when the enter key is pressed. - */ - set blurOnEnter(value: boolean) { - this._blurOnEnter = value; - } - - get blurOnEnter(): boolean { - return this._blurOnEnter; - } - - /** - * Gets / sets whether the input should blur when the escape key is pressed. - */ - set blurOnEscape(value: boolean) { - this._blurOnEnter = value; - } - - get blurOnEscape(): boolean { - return this._blurOnEnter; - } } Element.register('string', TextInput, { renderChanges: true }); diff --git a/src/components/TreeView/component.tsx b/src/components/TreeView/component.tsx index 42d9360f..196d28b6 100644 --- a/src/components/TreeView/component.tsx +++ b/src/components/TreeView/component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import TreeViewElement, { TreeViewArgs } from './index'; import TreeViewItemElement from '../TreeViewItem/index'; import BaseComponent from '../Element/component'; diff --git a/src/components/TreeView/index.stories.tsx b/src/components/TreeView/index.stories.tsx index c0a2732b..bc72efbc 100644 --- a/src/components/TreeView/index.stories.tsx +++ b/src/components/TreeView/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import TreeView from './component'; import TreeViewItem from '../TreeViewItem/component'; diff --git a/src/components/TreeView/index.ts b/src/components/TreeView/index.ts index 5b7bd713..601ce95c 100644 --- a/src/components/TreeView/index.ts +++ b/src/components/TreeView/index.ts @@ -36,7 +36,7 @@ export interface TreeViewArgs extends ContainerArgs { /** * A function to be called when we right click on a {@link TreeViewItem}. */ - onContextMenu?: any, + onContextMenu?: (evt: MouseEvent, item: TreeViewItem) => void, /** * A function to be called when we try to reparent tree items. If a function is provided then the * tree items will not be reparented by the {@link TreeView} but instead will rely on the function to @@ -166,7 +166,7 @@ class TreeView extends Container { protected _dragScrollElement: any; - protected _onContextMenu: any; + protected _onContextMenu: (evt: MouseEvent, item: TreeViewItem) => void; protected _onReparentFn: any; @@ -506,14 +506,14 @@ class TreeView extends Container { fn(item); if (item.numChildren) { - for (let i = 0; i < item.dom.childNodes.length; i++) { - traverse(item.dom.childNodes[i].ui); + for (const child of item.dom.childNodes) { + traverse(child.ui); } } } - for (let i = 0; i < this.dom.childNodes.length; i++) { - traverse(this.dom.childNodes[i].ui); + for (const child of this.dom.childNodes) { + traverse(child.ui); } } @@ -523,13 +523,15 @@ class TreeView extends Container { * is above the other. Performance wise this means it traverses * all tree items every time however seems to be pretty fast even with 15 - 20 K entities. */ - protected _updateTreeOrder() { + protected _getTreeOrder(): Map { + const treeOrder = new Map(); let order = 0; this._traverseDepthFirst((item: TreeViewItem) => { - // @ts-ignore - item._treeOrder = order++; + treeOrder.set(item, order++); }); + + return treeOrder; } protected _getChildIndex(item: TreeViewItem, parent: TreeViewItem) { @@ -613,10 +615,9 @@ class TreeView extends Container { if (!isRootDragged && this._dragOverItem) { if (this._dragItems.length > 1) { // sort items based on order in the hierarchy - this._updateTreeOrder(); + const treeOrder = this._getTreeOrder(); this._dragItems.sort((a, b) => { - // @ts-ignore - return a._treeOrder - b._treeOrder; + return treeOrder.get(a) - treeOrder.get(b); }); } @@ -642,21 +643,19 @@ class TreeView extends Container { reparented.forEach((r, i) => { if (this._dragArea === DRAG_AREA_BEFORE) { // If dragged before a TreeViewItem... - r.newParent = this._dragOverItem.parent; - // @ts-ignore - this._dragOverItem.parent.appendBefore(r.item, this._dragOverItem); + r.newParent = this._dragOverItem.parent as Container; + r.newParent.appendBefore(r.item, this._dragOverItem); r.newChildIndex = this._getChildIndex(r.item, r.newParent); } else if (this._dragArea === DRAG_AREA_INSIDE) { // If dragged inside a TreeViewItem... r.newParent = this._dragOverItem; - this._dragOverItem.append(r.item); - this._dragOverItem.open = true; + r.newParent.append(r.item); + r.newParent.open = true; r.newChildIndex = this._getChildIndex(r.item, r.newParent); } else if (this._dragArea === DRAG_AREA_AFTER) { // If dragged after a TreeViewItem... - r.newParent = this._dragOverItem.parent; - // @ts-ignore - this._dragOverItem.parent.appendAfter(r.item, i > 0 ? reparented[i - 1].item : this._dragOverItem); + r.newParent = this._dragOverItem.parent as Container; + r.newParent.appendAfter(r.item, i > 0 ? reparented[i - 1].item : this._dragOverItem); r.newChildIndex = this._getChildIndex(r.item, r.newParent); } }); @@ -666,9 +665,9 @@ class TreeView extends Container { // but will instead calculate the new indexes and pass that data to the reparent function // to perform the reparenting - const fakeDom: { parent: any; children: any; }[] = []; + const fakeDom: { parent: TreeViewItem; children: ChildNode[]; }[] = []; - const getChildren = (treeviewItem: { dom: { childNodes: any; }; }) => { + const getChildren = (treeviewItem: TreeViewItem) => { let idx = fakeDom.findIndex(entry => entry.parent === treeviewItem); if (idx === -1) { fakeDom.push({ parent: treeviewItem, children: [...treeviewItem.dom.childNodes] }); @@ -687,7 +686,7 @@ class TreeView extends Container { }); // add array of parent's child nodes to fakeDom array - const parentChildren = getChildren(item.parent); + const parentChildren = getChildren(item.parent as TreeViewItem); // remove this item from the children array in fakeDom const childIdx = parentChildren.indexOf(item.dom); @@ -699,7 +698,7 @@ class TreeView extends Container { if (this._dragArea === DRAG_AREA_BEFORE) { // If dragged before a TreeViewItem... r.newParent = this._dragOverItem.parent; - const parentChildren = getChildren(this._dragOverItem.parent); + const parentChildren = getChildren(this._dragOverItem.parent as TreeViewItem); const index = parentChildren.indexOf(this._dragOverItem.dom); parentChildren.splice(index, 0, r.item.dom); r.newChildIndex = index; @@ -712,7 +711,7 @@ class TreeView extends Container { } else if (this._dragArea === DRAG_AREA_AFTER) { // If dragged after a TreeViewItem... r.newParent = this._dragOverItem.parent; - const parentChildren = getChildren(this._dragOverItem.parent); + const parentChildren = getChildren(this._dragOverItem.parent as TreeViewItem); const after = i > 0 ? reparented[i - 1].item : this._dragOverItem; const index = parentChildren.indexOf(after.dom); parentChildren.splice(index + 1, 0, r.item.dom); diff --git a/src/components/TreeViewItem/index.ts b/src/components/TreeViewItem/index.ts index 32d8f0a7..715f43d5 100644 --- a/src/components/TreeViewItem/index.ts +++ b/src/components/TreeViewItem/index.ts @@ -120,8 +120,6 @@ class TreeViewItem extends Container { protected _numChildren = 0; - protected _treeOrder = -1; - protected _treeView: any; protected _allowDrag: boolean; @@ -561,9 +559,9 @@ class TreeViewItem extends Container { */ get firstChild() { if (this._numChildren) { - for (let i = 0; i < this.dom.childNodes.length; i++) { - if (this.dom.childNodes[i].ui instanceof TreeViewItem) { - return this.dom.childNodes[i].ui as TreeViewItem; + for (const child of this.dom.childNodes) { + if (child.ui instanceof TreeViewItem) { + return child.ui as TreeViewItem; } } } diff --git a/src/components/VectorInput/index.stories.tsx b/src/components/VectorInput/index.stories.tsx index b2a558e7..b867aa30 100644 --- a/src/components/VectorInput/index.stories.tsx +++ b/src/components/VectorInput/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import Component from './component'; import '../../scss/index.js'; diff --git a/src/components/VectorInput/index.ts b/src/components/VectorInput/index.ts index 8acfd21f..7a733bc1 100644 --- a/src/components/VectorInput/index.ts +++ b/src/components/VectorInput/index.ts @@ -59,7 +59,7 @@ class VectorInput extends Element implements IBindable, IFocusable, IPlaceholder min: args.min, max: args.max, precision: args.precision ?? 7, - step: args.step, + step: args.step ?? 1, stepPrecision: args.stepPrecision, renderChanges: args.renderChanges, placeholder: args.placeholder ? (Array.isArray(args.placeholder) ? args.placeholder[i] : args.placeholder) : null diff --git a/src/components/index.ts b/src/components/index.ts index c578fe32..f37ab276 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,12 +5,13 @@ import Canvas, { CanvasArgs } from './Canvas'; import Code, { CodeArgs } from './Code'; import ColorPicker, { ColorPickerArgs } from './ColorPicker'; import Container, { ContainerArgs } from './Container'; -import Divider, { DividerArgs } from './Divider'; +import Divider from './Divider'; import Element, { ElementArgs, IBindable, IBindableArgs, IFlexArgs, IFocusable, IParentArgs, IPlaceholder, IPlaceholderArgs } from './Element'; import GradientPicker, { GradientPickerArgs } from './GradientPicker'; import GridView, { GridViewArgs } from './GridView'; import GridViewItem, { GridViewItemArgs } from './GridViewItem'; import InfoBox, { InfoBoxArgs } from './InfoBox'; +import InputElement, { InputElementArgs } from './InputElement'; import Label, { LabelArgs } from './Label'; import LabelGroup, { LabelGroupArgs } from './LabelGroup'; import Menu, { MenuArgs } from './Menu'; @@ -27,7 +28,6 @@ import TextAreaInput, { TextAreaInputArgs } from './TextAreaInput'; import TextInput, { TextInputArgs } from './TextInput'; import TreeView, { TreeViewArgs } from './TreeView'; import TreeViewItem, { TreeViewItemArgs } from './TreeViewItem'; -import Input, { InputArgs } from './Input'; import VectorInput, { VectorInputArgs } from './VectorInput'; export { @@ -46,7 +46,6 @@ export { Container, ContainerArgs, Divider, - DividerArgs, Element, ElementArgs, GradientPicker, @@ -57,6 +56,8 @@ export { GridViewItemArgs, InfoBox, InfoBoxArgs, + InputElement, + InputElementArgs, Label, LabelArgs, LabelGroup, @@ -91,8 +92,6 @@ export { TreeViewItemArgs, VectorInput, VectorInputArgs, - Input, - InputArgs, IBindable, IBindableArgs, IPlaceholder, diff --git a/src/examples/BidirectionalBinding/index.stories.tsx b/src/examples/BidirectionalBinding/index.stories.tsx index 03a576c2..5ff8d2dd 100644 --- a/src/examples/BidirectionalBinding/index.stories.tsx +++ b/src/examples/BidirectionalBinding/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Observer } from '@playcanvas/observer'; import Container from '../../components/Container/component'; diff --git a/src/examples/Observer/index.stories.tsx b/src/examples/Observer/index.stories.tsx index 6fb92e10..0db3ab8b 100644 --- a/src/examples/Observer/index.stories.tsx +++ b/src/examples/Observer/index.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; import { Observer } from '@playcanvas/observer'; import Container from '../../components/Container/component'; diff --git a/src/index.ts b/src/index.ts index 7648e2dd..d6098620 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,9 @@ export * from './binding'; export * from './components'; + +// these strings are set by the build script +const version = 'PACKAGE_VERSION'; +const revision = 'PACKAGE_REVISION'; + +export { version }; +export { revision }; diff --git a/src/index.tsx b/src/index.tsx index ee5b3c73..607c6441 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,2 +1,9 @@ export * from './binding'; export * from './components/components'; + +// these strings are set by the build script +const version = 'PACKAGE_VERSION'; +const revision = 'PACKAGE_REVISION'; + +export { version }; +export { revision }; diff --git a/src/scss/components.scss b/src/scss/components.scss index be2d109a..0b36f1e3 100644 --- a/src/scss/components.scss +++ b/src/scss/components.scss @@ -1,6 +1,6 @@ // Components @import '../components/Element/style'; -@import '../components/TextInput/style'; +@import '../components/InputElement/style'; @import '../components/TextAreaInput/style'; @import '../components/NumericInput/style'; @import '../components/SliderInput/style'; diff --git a/tsconfig.json b/tsconfig.json index b05ccc82..9c26ba7c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,9 +6,9 @@ "jsx": "react", "lib": [ "es2019", - "dom" + "dom", + "dom.iterable" ], - "allowSyntheticDefaultImports" : true, "esModuleInterop" : true, "sourceMap": true, "moduleResolution": "node"