├── .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 | --------------------------------------------------------------------------------