├── .eslintignore
├── .npmrc
├── .prettierrc.json
├── src
├── tsconfig.json
├── tsconfig-dts.json
├── index.ts
├── sass
│ └── plugin.scss
├── controller.ts
├── view.ts
└── plugin.ts
├── .editorconfig
├── scripts
├── dist-name.js
└── assets-append-version.js
├── test
└── browser.html
├── LICENSE.txt
├── .eslintrc.js
├── README.md
├── package.json
├── rollup.config.js
└── .gitignore
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": false,
4 | "singleQuote": true,
5 | "trailingComma": "all",
6 | "useTabs": true
7 | }
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["DOM", "ES2015"],
4 | "moduleResolution": "Node16",
5 | "strict": true,
6 | "target": "ES6"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/tsconfig-dts.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "emitDeclarationOnly": true,
6 | "outDir": "../dist/types"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {TemplateInputPlugin} from './plugin.js';
2 |
3 | // The identifier of the plugin bundle.
4 | export const id = 'template';
5 |
6 | // This plugin template injects a compiled CSS by @rollup/plugin-replace
7 | // See rollup.config.js for details
8 | export const css = '__css__';
9 |
10 | // Export your plugin(s) as a constant `plugins`
11 | export const plugins = [TemplateInputPlugin];
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.{html}]
4 | indent_size = 2
5 | indent_style = tab
6 |
7 | [*.{js,json,ts}]
8 | indent_size = 2
9 | indent_style = tab
10 |
11 | [*.md]
12 | indent_size = 2
13 | indent_style = space
14 |
15 | [*.scss]
16 | indent_size = 2
17 | indent_style = tab
18 |
19 | [*.yml]
20 | indent_size = 2
21 | indent_style = space
22 |
23 | [package.json]
24 | indent_size = 2
25 | indent_style = space
26 |
--------------------------------------------------------------------------------
/scripts/dist-name.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-env node */
3 |
4 | import Fs from 'fs';
5 |
6 | const Package = JSON.parse(
7 | Fs.readFileSync(new URL('../package.json', import.meta.url)),
8 | );
9 |
10 | // `@tweakpane/plugin-foobar` -> `tweakpane-plugin-foobar`
11 | // `tweakpane-plugin-foobar` -> `tweakpane-plugin-foobar`
12 | const name = Package.name
13 | .split(/[@/-]/)
14 | .reduce((comps, comp) => (comp !== '' ? [...comps, comp] : comps), [])
15 | .join('-');
16 | console.log(name);
17 |
--------------------------------------------------------------------------------
/test/browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
29 |
30 |
--------------------------------------------------------------------------------
/scripts/assets-append-version.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /* eslint-env node */
3 |
4 | import Fs from 'fs';
5 | import Glob from 'glob';
6 | import Path from 'path';
7 |
8 | const Package = JSON.parse(
9 | Fs.readFileSync(new URL('../package.json', import.meta.url)),
10 | );
11 |
12 | const PATTERN = 'dist/*';
13 |
14 | const paths = Glob.sync(PATTERN);
15 | paths.forEach((path) => {
16 | const fileName = Path.basename(path);
17 | if (Fs.statSync(path).isDirectory()) {
18 | return;
19 | }
20 |
21 | const ext = fileName.match(/(\..+)$/)[1];
22 | const base = Path.basename(fileName, ext);
23 | const versionedPath = Path.join(
24 | Path.dirname(path),
25 | `${base}-${Package.version}${ext}`,
26 | );
27 | Fs.renameSync(path, versionedPath);
28 | });
29 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 cocopon
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'plugin:@typescript-eslint/eslint-recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:prettier/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | plugins: ['@typescript-eslint', 'simple-import-sort'],
10 | root: true,
11 | rules: {
12 | camelcase: 'off',
13 | 'no-unused-vars': 'off',
14 | 'sort-imports': 'off',
15 |
16 | 'prettier/prettier': 'error',
17 | 'simple-import-sort/imports': 'error',
18 | '@typescript-eslint/naming-convention': [
19 | 'error',
20 | {
21 | selector: 'variable',
22 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
23 | custom: {
24 | regex: '^opt_',
25 | match: false,
26 | },
27 | },
28 | ],
29 | '@typescript-eslint/explicit-function-return-type': 'off',
30 | '@typescript-eslint/no-empty-function': 'off',
31 | '@typescript-eslint/no-explicit-any': 'off',
32 | '@typescript-eslint/no-unused-vars': [
33 | 'error',
34 | {
35 | argsIgnorePattern: '^_',
36 | },
37 | ],
38 |
39 | // TODO: Resolve latest lint warnings
40 | '@typescript-eslint/explicit-module-boundary-types': 'off',
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/src/sass/plugin.scss:
--------------------------------------------------------------------------------
1 | // Import core styles
2 | @use '../../node_modules/@tweakpane/core/lib/sass/tp';
3 |
4 | // Additional style for the plugin
5 | .#{tp.$prefix}-tmpv {
6 | // Extend a general input view style
7 | @extend %tp-input;
8 |
9 | cursor: pointer;
10 | display: grid;
11 | grid-template-columns: repeat(10, 1fr);
12 | grid-template-rows: repeat(auto-fit, 10px);
13 | height: calc(#{tp.cssVar('container-unit-size')} * 3);
14 | overflow: hidden;
15 | position: relative;
16 |
17 | &.#{tp.$prefix}-v-disabled {
18 | opacity: 0.5;
19 | }
20 |
21 | &_text {
22 | // You can use CSS variables for styling. See declarations for details:
23 | // ../../node_modules/@tweakpane/core/lib/sass/common/_defs.scss
24 | color: tp.cssVar('input-fg');
25 |
26 | bottom: 2px;
27 | font-size: 0.9em;
28 | line-height: 0.9;
29 | opacity: 0.5;
30 | position: absolute;
31 | right: 2px;
32 | }
33 | &_dot {
34 | height: 10px;
35 | line-height: 10px;
36 | position: relative;
37 | text-align: center;
38 |
39 | &::before {
40 | background-color: tp.cssVar('input-fg');
41 | content: '';
42 | border-radius: 1px;
43 | height: 2px;
44 | left: 50%;
45 | margin-left: -1px;
46 | margin-top: -1px;
47 | position: absolute;
48 | top: 50%;
49 | width: 2px;
50 | }
51 | }
52 | &_frac {
53 | background-color: tp.cssVar('input-fg');
54 | border-radius: 1px;
55 | height: 2px;
56 | left: 50%;
57 | margin-top: -1px;
58 | position: absolute;
59 | top: 50%;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | constrainRange,
3 | Controller,
4 | PointerHandler,
5 | PointerHandlerEvent,
6 | Value,
7 | ViewProps,
8 | } from '@tweakpane/core';
9 |
10 | import {PluginView} from './view.js';
11 |
12 | interface Config {
13 | value: Value;
14 | viewProps: ViewProps;
15 | }
16 |
17 | // Custom controller class should implement `Controller` interface
18 | export class PluginController implements Controller {
19 | public readonly value: Value;
20 | public readonly view: PluginView;
21 | public readonly viewProps: ViewProps;
22 |
23 | constructor(doc: Document, config: Config) {
24 | this.onPoint_ = this.onPoint_.bind(this);
25 |
26 | // Receive the bound value from the plugin
27 | this.value = config.value;
28 |
29 | // and also view props
30 | this.viewProps = config.viewProps;
31 | this.viewProps.handleDispose(() => {
32 | // Called when the controller is disposing
33 | console.log('TODO: dispose controller');
34 | });
35 |
36 | // Create a custom view
37 | this.view = new PluginView(doc, {
38 | value: this.value,
39 | viewProps: this.viewProps,
40 | });
41 |
42 | // You can use `PointerHandler` to handle pointer events in the same way as Tweakpane do
43 | const ptHandler = new PointerHandler(this.view.element);
44 | ptHandler.emitter.on('down', this.onPoint_);
45 | ptHandler.emitter.on('move', this.onPoint_);
46 | ptHandler.emitter.on('up', this.onPoint_);
47 | }
48 |
49 | private onPoint_(ev: PointerHandlerEvent) {
50 | const data = ev.data;
51 | if (!data.point) {
52 | return;
53 | }
54 |
55 | // Update the value by user input
56 | const dx =
57 | constrainRange(data.point.x / data.bounds.width + 0.05, 0, 1) * 10;
58 | const dy = data.point.y / 10;
59 | this.value.rawValue = Math.floor(dy) * 10 + dx;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tweakpane plugin template
2 | Plugin template of an input binding for [Tweakpane][tweakpane].
3 |
4 |
5 | # For plugin developers
6 | TODO: Delete this section before publishing your plugin.
7 |
8 |
9 | ## Quick start
10 | - Install dependencies:
11 | ```
12 | % npm install
13 | ```
14 | - Build source codes and watch changes:
15 | ```
16 | % npm start
17 | ```
18 | - Open `test/browser.html` to see the result.
19 |
20 |
21 | ## File structure
22 | ```
23 | |- src
24 | | |- sass ............ Plugin CSS
25 | | |- index.ts ........ Entrypoint
26 | | |- plugin.ts ....... Plugin
27 | | |- controller.ts ... Controller for the custom view
28 | | `- view.ts ......... Custom view
29 | |- dist ............... Compiled files
30 | `- test
31 | `- browser.html .... Plugin labo
32 | ```
33 |
34 |
35 | ## Version compatibility
36 |
37 | | Tweakpane | plugin-template |
38 | | --------- | --------------- |
39 | | 4.x | [main](https://github.com/tweakpane/plugin-template/tree/main) |
40 | | 3.x | [v3](https://github.com/tweakpane/plugin-template/tree/v3) |
41 |
42 |
43 | # For plugin users
44 |
45 |
46 | ## Installation
47 |
48 |
49 | ### Browser
50 | ```html
51 |
58 | ```
59 |
60 |
61 | ### Package
62 | ```js
63 | import {Pane} from 'tweakpane';
64 | import * as TemplatePlugin from 'tweakpane-plugin-template';
65 |
66 | const pane = new Pane();
67 | pane.registerPlugin(TemplatePlugin);
68 | ```
69 |
70 |
71 | ## Usage
72 | ```js
73 | const params = {
74 | prop: 3,
75 | };
76 |
77 | // TODO: Update parameters for your plugin
78 | pane.addInput(params, 'prop', {
79 | view: 'dots',
80 | }).on('change', (ev) => {
81 | console.log(ev.value);
82 | });
83 | ```
84 |
85 |
86 | [tweakpane]: https://github.com/cocopon/tweakpane/
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweakpane-plugin-template",
3 | "version": "0.0.0",
4 | "description": "Plugin template for Tweakpane",
5 | "main": "dist/tweakpane-plugin-template.js",
6 | "type": "module",
7 | "types": "dist/types/index.d.ts",
8 | "author": "cocopon",
9 | "license": "MIT",
10 | "files": [
11 | "dist"
12 | ],
13 | "scripts": {
14 | "prepare": "run-s clean build",
15 | "prepublishOnly": "npm test",
16 | "start": "run-p watch server",
17 | "test": "eslint --ext .ts \"src/**/*.ts\"",
18 | "assets": "run-s clean build assets:version assets:zip",
19 | "assets:version": "node scripts/assets-append-version.js",
20 | "assets:zip": "zip -x '*types*' -j -r $(node scripts/dist-name.js)-$(cat package.json | npx json version).zip dist",
21 | "clean": "rimraf dist *.tgz *.zip",
22 | "build": "run-p build:*",
23 | "build:dev": "rollup --config rollup.config.js",
24 | "build:dts": "tsc --project src/tsconfig-dts.json",
25 | "build:prod": "rollup --config rollup.config.js --environment BUILD:production",
26 | "format": "run-p format:*",
27 | "format:scss": "prettier --parser scss --write \"src/sass/**/*.scss\"",
28 | "format:ts": "eslint --ext .ts --fix \"src/**/*.ts\"",
29 | "server": "http-server -c-1 -o /test/browser.html",
30 | "watch": "run-p watch:*",
31 | "watch:sass": "onchange --initial --kill \"src/sass/**/*.scss\" -- npm run build:dev",
32 | "watch:ts": "onchange --initial --kill \"src/**/*.ts\" -- rollup --config rollup.config.js"
33 | },
34 | "devDependencies": {
35 | "@rollup/plugin-alias": "^3.1.2",
36 | "@rollup/plugin-node-resolve": "^13.0.0",
37 | "@rollup/plugin-replace": "^2.4.1",
38 | "@rollup/plugin-typescript": "^8.2.0",
39 | "@tweakpane/core": "^2.0.0-beta.2",
40 | "@typescript-eslint/eslint-plugin": "^5.62.0",
41 | "@typescript-eslint/parser": "^5.62.0",
42 | "autoprefixer": "^10.2.4",
43 | "eslint": "^8.46.0",
44 | "eslint-config-prettier": "^8.1.0",
45 | "eslint-plugin-prettier": "^3.3.1",
46 | "eslint-plugin-simple-import-sort": "^7.0.0",
47 | "http-server": "^14.1.1",
48 | "npm-run-all": "^4.1.5",
49 | "onchange": "^7.1.0",
50 | "postcss": "^8.2.6",
51 | "prettier": "^2.2.1",
52 | "rimraf": "^3.0.2",
53 | "rollup": "^2.39.1",
54 | "rollup-plugin-cleanup": "^3.2.1",
55 | "rollup-plugin-terser": "^7.0.2",
56 | "sass": "^1.49.9",
57 | "typescript": "^4.9.5"
58 | },
59 | "peerDependencies": {
60 | "tweakpane": "^4.0.0-beta.2"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import Alias from '@rollup/plugin-alias';
4 | import {nodeResolve} from '@rollup/plugin-node-resolve';
5 | import Replace from '@rollup/plugin-replace';
6 | import Typescript from '@rollup/plugin-typescript';
7 | import Autoprefixer from 'autoprefixer';
8 | import Postcss from 'postcss';
9 | import Cleanup from 'rollup-plugin-cleanup';
10 | import {terser as Terser} from 'rollup-plugin-terser';
11 | import Sass from 'sass';
12 |
13 | import Package from './package.json';
14 |
15 | async function compileCss() {
16 | const css = Sass.renderSync({
17 | file: 'src/sass/plugin.scss',
18 | outputStyle: 'compressed',
19 | }).css.toString();
20 |
21 | const result = await Postcss([Autoprefixer]).process(css, {
22 | from: undefined,
23 | });
24 | return result.css.replace(/'/g, "\\'").trim();
25 | }
26 |
27 | function getPlugins(css, shouldMinify) {
28 | const plugins = [
29 | Alias({
30 | entries: [
31 | {
32 | find: '@tweakpane/core',
33 | replacement: './node_modules/@tweakpane/core/dist/index.js',
34 | },
35 | ],
36 | }),
37 | Typescript({
38 | tsconfig: 'src/tsconfig.json',
39 | }),
40 | nodeResolve(),
41 | Replace({
42 | __css__: css,
43 | preventAssignment: false,
44 | }),
45 | ];
46 | if (shouldMinify) {
47 | plugins.push(Terser());
48 | }
49 | return [
50 | ...plugins,
51 | // https://github.com/microsoft/tslib/issues/47
52 | Cleanup({
53 | comments: 'none',
54 | }),
55 | ];
56 | }
57 |
58 | function getDistName(packageName) {
59 | // `@tweakpane/plugin-foobar` -> `tweakpane-plugin-foobar`
60 | // `tweakpane-plugin-foobar` -> `tweakpane-plugin-foobar`
61 | return packageName
62 | .split(/[@/-]/)
63 | .reduce((comps, comp) => (comp !== '' ? [...comps, comp] : comps), [])
64 | .join('-');
65 | }
66 |
67 | export default async () => {
68 | const production = process.env.BUILD === 'production';
69 | const postfix = production ? '.min' : '';
70 |
71 | const distName = getDistName(Package.name);
72 | const css = await compileCss();
73 | return {
74 | input: 'src/index.ts',
75 | external: ['tweakpane'],
76 | output: {
77 | file: `dist/${distName}${postfix}.js`,
78 | format: 'esm',
79 | globals: {
80 | tweakpane: 'Tweakpane',
81 | },
82 | },
83 | plugins: getPlugins(css, production),
84 |
85 | // Suppress `Circular dependency` warning
86 | onwarn(warning, rollupWarn) {
87 | if (warning.code === 'CIRCULAR_DEPENDENCY') {
88 | return;
89 | }
90 | rollupWarn(warning);
91 | },
92 | };
93 | };
94 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode/
2 | /dist/
3 | /*.zip
4 |
5 | ### https://raw.github.com/github/gitignore//Node.gitignore
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log*
14 |
15 | # Diagnostic reports (https://nodejs.org/api/report.html)
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 |
18 | # Runtime data
19 | pids
20 | *.pid
21 | *.seed
22 | *.pid.lock
23 |
24 | # Directory for instrumented libs generated by jscoverage/JSCover
25 | lib-cov
26 |
27 | # Coverage directory used by tools like istanbul
28 | coverage
29 | *.lcov
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (https://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 | jspm_packages/
49 |
50 | # Snowpack dependency directory (https://snowpack.dev/)
51 | web_modules/
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variables file
78 | .env
79 | .env.test
80 |
81 | # parcel-bundler cache (https://parceljs.org/)
82 | .cache
83 | .parcel-cache
84 |
85 | # Next.js build output
86 | .next
87 | out
88 |
89 | # Nuxt.js build / generate output
90 | .nuxt
91 | dist
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and not Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # Serverless directories
103 | .serverless/
104 |
105 | # FuseBox cache
106 | .fusebox/
107 |
108 | # DynamoDB Local files
109 | .dynamodb/
110 |
111 | # TernJS port file
112 | .tern-port
113 |
114 | # Stores VSCode versions used for testing VSCode extensions
115 | .vscode-test
116 |
117 | # yarn v2
118 | .yarn/cache
119 | .yarn/unplugged
120 | .yarn/build-state.yml
121 | .yarn/install-state.gz
122 | .pnp.*
123 |
124 |
125 |
--------------------------------------------------------------------------------
/src/view.ts:
--------------------------------------------------------------------------------
1 | import {ClassName, mapRange, Value, View, ViewProps} from '@tweakpane/core';
2 |
3 | interface Config {
4 | value: Value;
5 | viewProps: ViewProps;
6 | }
7 |
8 | // Create a class name generator from the view name
9 | // ClassName('tmp') will generate a CSS class name like `tp-tmpv`
10 | const className = ClassName('tmp');
11 |
12 | // Custom view class should implement `View` interface
13 | export class PluginView implements View {
14 | public readonly element: HTMLElement;
15 | private value_: Value;
16 | private dotElems_: HTMLElement[] = [];
17 | private textElem_: HTMLElement;
18 |
19 | constructor(doc: Document, config: Config) {
20 | // Create a root element for the plugin
21 | this.element = doc.createElement('div');
22 | this.element.classList.add(className());
23 | // Bind view props to the element
24 | config.viewProps.bindClassModifiers(this.element);
25 |
26 | // Receive the bound value from the controller
27 | this.value_ = config.value;
28 | // Handle 'change' event of the value
29 | this.value_.emitter.on('change', this.onValueChange_.bind(this));
30 |
31 | // Create child elements
32 | this.textElem_ = doc.createElement('div');
33 | this.textElem_.classList.add(className('text'));
34 | this.element.appendChild(this.textElem_);
35 |
36 | // Apply the initial value
37 | this.refresh_();
38 |
39 | config.viewProps.handleDispose(() => {
40 | // Called when the view is disposing
41 | console.log('TODO: dispose view');
42 | });
43 | }
44 |
45 | private refresh_(): void {
46 | const rawValue = this.value_.rawValue;
47 |
48 | this.textElem_.textContent = rawValue.toFixed(2);
49 |
50 | while (this.dotElems_.length > 0) {
51 | const elem = this.dotElems_.shift();
52 | if (elem) {
53 | this.element.removeChild(elem);
54 | }
55 | }
56 |
57 | const doc = this.element.ownerDocument;
58 | const dotCount = Math.floor(rawValue);
59 | for (let i = 0; i < dotCount; i++) {
60 | const dotElem = doc.createElement('div');
61 | dotElem.classList.add(className('dot'));
62 |
63 | if (i === dotCount - 1) {
64 | const fracElem = doc.createElement('div');
65 | fracElem.classList.add(className('frac'));
66 | const frac = rawValue - Math.floor(rawValue);
67 | fracElem.style.width = `${frac * 100}%`;
68 | fracElem.style.opacity = String(mapRange(frac, 0, 1, 1, 0.2));
69 | dotElem.appendChild(fracElem);
70 | }
71 |
72 | this.dotElems_.push(dotElem);
73 | this.element.appendChild(dotElem);
74 | }
75 | }
76 |
77 | private onValueChange_() {
78 | this.refresh_();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseInputParams,
3 | BindingTarget,
4 | CompositeConstraint,
5 | createPlugin,
6 | createRangeConstraint,
7 | createStepConstraint,
8 | InputBindingPlugin,
9 | parseRecord,
10 | } from '@tweakpane/core';
11 |
12 | import {PluginController} from './controller.js';
13 |
14 | export interface PluginInputParams extends BaseInputParams {
15 | max?: number;
16 | min?: number;
17 | step?: number;
18 | view: 'dots';
19 | }
20 |
21 | // NOTE: JSDoc comments of `InputBindingPlugin` can be useful to know details about each property
22 | //
23 | // `InputBindingPlugin` means...
24 | // - The plugin receives the bound value as `Ex`,
25 | // - converts `Ex` into `In` and holds it
26 | // - P is the type of the parsed parameters
27 | //
28 | export const TemplateInputPlugin: InputBindingPlugin<
29 | number,
30 | number,
31 | PluginInputParams
32 | > = createPlugin({
33 | id: 'input-template',
34 |
35 | // type: The plugin type.
36 | // - 'input': Input binding
37 | // - 'monitor': Monitor binding
38 | // - 'blade': Blade without binding
39 | type: 'input',
40 |
41 | accept(exValue: unknown, params: Record) {
42 | if (typeof exValue !== 'number') {
43 | // Return null to deny the user input
44 | return null;
45 | }
46 |
47 | // Parse parameters object
48 | const result = parseRecord(params, (p) => ({
49 | // `view` option may be useful to provide a custom control for primitive values
50 | view: p.required.constant('dots'),
51 |
52 | max: p.optional.number,
53 | min: p.optional.number,
54 | step: p.optional.number,
55 | }));
56 | if (!result) {
57 | return null;
58 | }
59 |
60 | // Return a typed value and params to accept the user input
61 | return {
62 | initialValue: exValue,
63 | params: result,
64 | };
65 | },
66 |
67 | binding: {
68 | reader(_args) {
69 | return (exValue: unknown): number => {
70 | // Convert an external unknown value into the internal value
71 | return typeof exValue === 'number' ? exValue : 0;
72 | };
73 | },
74 |
75 | constraint(args) {
76 | // Create a value constraint from the user input
77 | const constraints = [];
78 | // You can reuse existing functions of the default plugins
79 | const cr = createRangeConstraint(args.params);
80 | if (cr) {
81 | constraints.push(cr);
82 | }
83 | const cs = createStepConstraint(args.params);
84 | if (cs) {
85 | constraints.push(cs);
86 | }
87 | // Use `CompositeConstraint` to combine multiple constraints
88 | return new CompositeConstraint(constraints);
89 | },
90 |
91 | writer(_args) {
92 | return (target: BindingTarget, inValue) => {
93 | // Use `target.write()` to write the primitive value to the target,
94 | // or `target.writeProperty()` to write a property of the target
95 | target.write(inValue);
96 | };
97 | },
98 | },
99 |
100 | controller(args) {
101 | // Create a controller for the plugin
102 | return new PluginController(args.document, {
103 | value: args.value,
104 | viewProps: args.viewProps,
105 | });
106 | },
107 | });
108 |
--------------------------------------------------------------------------------