├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE.txt ├── README.md ├── package.json ├── packages ├── core │ ├── .npmignore │ ├── .npmrc │ ├── lib │ │ └── sass │ │ │ ├── _plugin-test.scss │ │ │ ├── _tp.scss │ │ │ ├── common │ │ │ └── _defs.scss │ │ │ └── view │ │ │ ├── _button.scss │ │ │ ├── _checkbox.scss │ │ │ ├── _color-picker.scss │ │ │ ├── _color-swatch.scss │ │ │ ├── _color-text.scss │ │ │ ├── _color.scss │ │ │ ├── _default-wrapper.scss │ │ │ ├── _folder.scss │ │ │ ├── _graph-log.scss │ │ │ ├── _label.scss │ │ │ ├── _list.scss │ │ │ ├── _log.scss │ │ │ ├── _point-2d-picker.scss │ │ │ ├── _point-2d.scss │ │ │ ├── _point-nd-text.scss │ │ │ ├── _popup.scss │ │ │ ├── _slider.scss │ │ │ ├── _tab.scss │ │ │ ├── _text.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _views.scss │ │ │ ├── common │ │ │ └── _color.scss │ │ │ └── placeholder │ │ │ ├── _button.scss │ │ │ ├── _container.scss │ │ │ ├── _folder.scss │ │ │ ├── _input.scss │ │ │ ├── _list.scss │ │ │ ├── _monitor.scss │ │ │ ├── _texts.scss │ │ │ └── _theme.scss │ ├── package.json │ ├── scripts │ │ └── replace-version.js │ └── src │ │ ├── blade │ │ ├── binding │ │ │ ├── api │ │ │ │ ├── binding-test.ts │ │ │ │ ├── binding.ts │ │ │ │ ├── input-binding-test.ts │ │ │ │ ├── input-binding.ts │ │ │ │ ├── monitor-binding-test.ts │ │ │ │ └── monitor-binding.ts │ │ │ └── controller │ │ │ │ ├── binding-test.ts │ │ │ │ ├── binding.ts │ │ │ │ ├── input-binding-test.ts │ │ │ │ ├── input-binding.ts │ │ │ │ ├── monitor-binding-test.ts │ │ │ │ └── monitor-binding.ts │ │ ├── button │ │ │ ├── api │ │ │ │ ├── button-test.ts │ │ │ │ └── button.ts │ │ │ ├── controller │ │ │ │ ├── button-blade-test.ts │ │ │ │ ├── button-blade.ts │ │ │ │ ├── button-test.ts │ │ │ │ └── button.ts │ │ │ ├── plugin-test.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ └── button.ts │ │ ├── common │ │ │ ├── api │ │ │ │ ├── blade-test.ts │ │ │ │ ├── blade.ts │ │ │ │ ├── container-blade.ts │ │ │ │ ├── container-test.ts │ │ │ │ ├── container.ts │ │ │ │ ├── event-listenable.ts │ │ │ │ ├── events.ts │ │ │ │ ├── params.ts │ │ │ │ ├── rack-test.ts │ │ │ │ ├── rack.ts │ │ │ │ ├── refreshable-test.ts │ │ │ │ ├── refreshable.ts │ │ │ │ ├── test-util.ts │ │ │ │ └── tp-event.ts │ │ │ ├── controller │ │ │ │ ├── blade-state-test.ts │ │ │ │ ├── blade-state.ts │ │ │ │ ├── blade-test.ts │ │ │ │ ├── blade.ts │ │ │ │ ├── container-blade-test.ts │ │ │ │ ├── container-blade.ts │ │ │ │ ├── rack.ts │ │ │ │ └── value-blade.ts │ │ │ ├── model │ │ │ │ ├── blade-positions.ts │ │ │ │ ├── blade.ts │ │ │ │ ├── foldable.ts │ │ │ │ ├── nested-ordered-set-test.ts │ │ │ │ ├── nested-ordered-set.ts │ │ │ │ ├── rack-test.ts │ │ │ │ └── rack.ts │ │ │ └── view │ │ │ │ └── blade-container.ts │ │ ├── folder │ │ │ ├── api │ │ │ │ ├── folder-test.ts │ │ │ │ └── folder.ts │ │ │ ├── controller │ │ │ │ ├── folder-test.ts │ │ │ │ └── folder.ts │ │ │ ├── plugin-test.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ └── folder.ts │ │ ├── label │ │ │ └── controller │ │ │ │ ├── value-test.ts │ │ │ │ └── value.ts │ │ ├── plugin.ts │ │ ├── tab │ │ │ ├── api │ │ │ │ ├── tab-page-test.ts │ │ │ │ ├── tab-page.ts │ │ │ │ ├── tab-test.ts │ │ │ │ └── tab.ts │ │ │ ├── controller │ │ │ │ ├── tab-item-test.ts │ │ │ │ ├── tab-item.ts │ │ │ │ ├── tab-page-test.ts │ │ │ │ ├── tab-page.ts │ │ │ │ ├── tab-test.ts │ │ │ │ └── tab.ts │ │ │ ├── model │ │ │ │ ├── tab-test.ts │ │ │ │ └── tab.ts │ │ │ ├── plugin-test.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ ├── tab-item.ts │ │ │ │ ├── tab-page.ts │ │ │ │ └── tab.ts │ │ └── test-util.ts │ │ ├── common │ │ ├── api │ │ │ ├── list-test.ts │ │ │ └── list.ts │ │ ├── binding │ │ │ ├── binding.ts │ │ │ ├── read-write-test.ts │ │ │ ├── read-write.ts │ │ │ ├── readonly.ts │ │ │ ├── target-test.ts │ │ │ ├── target.ts │ │ │ ├── ticker │ │ │ │ ├── interval-test.ts │ │ │ │ ├── interval.ts │ │ │ │ ├── manual-test.ts │ │ │ │ ├── manual.ts │ │ │ │ └── ticker.ts │ │ │ └── value │ │ │ │ ├── binding.ts │ │ │ │ ├── input-binding-test.ts │ │ │ │ ├── input-binding.ts │ │ │ │ ├── monitor-binding-test.ts │ │ │ │ └── monitor-binding.ts │ │ ├── compat.ts │ │ ├── constraint │ │ │ ├── composite-test.ts │ │ │ ├── composite.ts │ │ │ ├── constraint.ts │ │ │ ├── definite-range-test.ts │ │ │ ├── definite-range.ts │ │ │ ├── list-test.ts │ │ │ ├── list.ts │ │ │ ├── range-test.ts │ │ │ ├── range.ts │ │ │ ├── step-test.ts │ │ │ └── step.ts │ │ ├── controller │ │ │ ├── controller.ts │ │ │ ├── list-test.ts │ │ │ ├── list.ts │ │ │ ├── popup.ts │ │ │ ├── text-test.ts │ │ │ ├── text.ts │ │ │ └── value.ts │ │ ├── converter │ │ │ ├── boolean-test.ts │ │ │ ├── boolean.ts │ │ │ ├── ecma │ │ │ │ ├── nodes-test.ts │ │ │ │ ├── nodes.ts │ │ │ │ ├── parser-test.ts │ │ │ │ ├── parser.ts │ │ │ │ └── reader.ts │ │ │ ├── formatter.ts │ │ │ ├── number-test.ts │ │ │ ├── number.ts │ │ │ ├── parser-test.ts │ │ │ ├── parser.ts │ │ │ ├── percentage-test.ts │ │ │ ├── percentage.ts │ │ │ └── string.ts │ │ ├── dom-util-test.ts │ │ ├── dom-util.ts │ │ ├── label │ │ │ ├── controller │ │ │ │ ├── label-test.ts │ │ │ │ └── label.ts │ │ │ └── view │ │ │ │ └── label.ts │ │ ├── list-util.ts │ │ ├── micro-parsers-test.ts │ │ ├── micro-parsers.ts │ │ ├── model │ │ │ ├── buffered-value-test.ts │ │ │ ├── buffered-value.ts │ │ │ ├── complex-value.ts │ │ │ ├── emitter-test.ts │ │ │ ├── emitter.ts │ │ │ ├── primitive-value-test.ts │ │ │ ├── primitive-value.ts │ │ │ ├── reactive.ts │ │ │ ├── readonly-primitive-value.ts │ │ │ ├── test-util.ts │ │ │ ├── value-map-test.ts │ │ │ ├── value-map.ts │ │ │ ├── value-sync-test.ts │ │ │ ├── value-sync.ts │ │ │ ├── value.ts │ │ │ ├── values-test.ts │ │ │ ├── values.ts │ │ │ ├── view-props-test.ts │ │ │ └── view-props.ts │ │ ├── number │ │ │ ├── controller │ │ │ │ ├── number-text-test.ts │ │ │ │ ├── number-text.ts │ │ │ │ ├── slider-text-test.ts │ │ │ │ ├── slider-text.ts │ │ │ │ └── slider.ts │ │ │ ├── util-test.ts │ │ │ ├── util.ts │ │ │ └── view │ │ │ │ ├── number-text.ts │ │ │ │ ├── slider-test.ts │ │ │ │ ├── slider-text.ts │ │ │ │ └── slider.ts │ │ ├── params.ts │ │ ├── picker-util.ts │ │ ├── point-nd │ │ │ ├── point-axis.ts │ │ │ ├── test-util.ts │ │ │ └── util.ts │ │ ├── primitive.ts │ │ ├── tp-error-test.ts │ │ ├── tp-error.ts │ │ ├── ui.ts │ │ └── view │ │ │ ├── class-name.ts │ │ │ ├── css-vars.ts │ │ │ ├── list-test.ts │ │ │ ├── list.ts │ │ │ ├── plain.ts │ │ │ ├── pointer-handler.ts │ │ │ ├── popup.ts │ │ │ ├── reactive.ts │ │ │ ├── text-test.ts │ │ │ ├── text.ts │ │ │ └── view.ts │ │ ├── index.ts │ │ ├── input-binding │ │ ├── boolean │ │ │ ├── controller │ │ │ │ └── checkbox.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ └── checkbox.ts │ │ ├── color │ │ │ ├── controller │ │ │ │ ├── a-palette.ts │ │ │ │ ├── color-picker-test.ts │ │ │ │ ├── color-picker.ts │ │ │ │ ├── color-swatch.ts │ │ │ │ ├── color-texts-test.ts │ │ │ │ ├── color-texts.ts │ │ │ │ ├── color.ts │ │ │ │ ├── h-palette.ts │ │ │ │ └── sv-palette.ts │ │ │ ├── converter │ │ │ │ ├── color-number-test.ts │ │ │ │ ├── color-number.ts │ │ │ │ ├── color-object-test.ts │ │ │ │ ├── color-object.ts │ │ │ │ ├── color-string-test.ts │ │ │ │ ├── color-string.ts │ │ │ │ ├── writer-test.ts │ │ │ │ └── writer.ts │ │ │ ├── model │ │ │ │ ├── color-model-test.ts │ │ │ │ ├── color-model.ts │ │ │ │ ├── color-test.ts │ │ │ │ ├── color.ts │ │ │ │ ├── colors-test.ts │ │ │ │ ├── colors.ts │ │ │ │ ├── float-color-test.ts │ │ │ │ ├── float-color.ts │ │ │ │ ├── int-color-test.ts │ │ │ │ └── int-color.ts │ │ │ ├── plugin-number-test.ts │ │ │ ├── plugin-number.ts │ │ │ ├── plugin-object-test.ts │ │ │ ├── plugin-object.ts │ │ │ ├── plugin-string-test.ts │ │ │ ├── plugin-string.ts │ │ │ ├── util.ts │ │ │ └── view │ │ │ │ ├── a-palette.ts │ │ │ │ ├── color-picker.ts │ │ │ │ ├── color-swatch.ts │ │ │ │ ├── color-texts-test.ts │ │ │ │ ├── color-texts.ts │ │ │ │ ├── color.ts │ │ │ │ ├── h-palette.ts │ │ │ │ └── sv-palette.ts │ │ ├── common │ │ │ ├── constraint │ │ │ │ ├── point-nd-test.ts │ │ │ │ └── point-nd.ts │ │ │ ├── controller │ │ │ │ ├── point-nd-text-test.ts │ │ │ │ └── point-nd-text.ts │ │ │ ├── model │ │ │ │ └── point-nd.ts │ │ │ └── view │ │ │ │ └── point-nd-text.ts │ │ ├── number │ │ │ ├── api │ │ │ │ ├── slider-test.ts │ │ │ │ └── slider.ts │ │ │ ├── plugin-test.ts │ │ │ └── plugin.ts │ │ ├── plugin-test.ts │ │ ├── plugin.ts │ │ ├── point-2d │ │ │ ├── controller │ │ │ │ ├── point-2d-picker.ts │ │ │ │ └── point-2d.ts │ │ │ ├── converter │ │ │ │ ├── point-2d-test.ts │ │ │ │ └── point-2d.ts │ │ │ ├── model │ │ │ │ └── point-2d.ts │ │ │ ├── plugin-test.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ ├── point-2d-picker.ts │ │ │ │ └── point-2d.ts │ │ ├── point-3d │ │ │ ├── converter │ │ │ │ ├── point-3d-test.ts │ │ │ │ └── point-3d.ts │ │ │ ├── model │ │ │ │ ├── point-3d-test.ts │ │ │ │ └── point-3d.ts │ │ │ ├── plugin-test.ts │ │ │ └── plugin.ts │ │ ├── point-4d │ │ │ ├── converter │ │ │ │ ├── point-4d-test.ts │ │ │ │ └── point-4d.ts │ │ │ ├── model │ │ │ │ ├── point-4d-test.ts │ │ │ │ └── point-4d.ts │ │ │ ├── plugin-test.ts │ │ │ └── plugin.ts │ │ └── string │ │ │ └── plugin.ts │ │ ├── misc │ │ ├── constants.ts │ │ ├── dom-test-util.ts │ │ ├── semver-test.ts │ │ ├── semver.ts │ │ ├── test-util.ts │ │ ├── type-util-test.ts │ │ └── type-util.ts │ │ ├── monitor-binding │ │ ├── boolean │ │ │ └── plugin.ts │ │ ├── common │ │ │ ├── controller │ │ │ │ ├── multi-log.ts │ │ │ │ └── single-log.ts │ │ │ └── view │ │ │ │ ├── multi-log.ts │ │ │ │ └── single-log.ts │ │ ├── number │ │ │ ├── api │ │ │ │ ├── graph-log-test.ts │ │ │ │ └── graph-log.ts │ │ │ ├── controller │ │ │ │ ├── graph-log-test.ts │ │ │ │ └── graph-log.ts │ │ │ ├── plugin-test.ts │ │ │ ├── plugin.ts │ │ │ └── view │ │ │ │ └── graph-log.ts │ │ ├── plugin-test.ts │ │ ├── plugin.ts │ │ └── string │ │ │ └── plugin.ts │ │ ├── plugin │ │ ├── blade-api-cache-test.ts │ │ ├── blade-api-cache.ts │ │ ├── plugin.ts │ │ ├── plugins.ts │ │ └── pool.ts │ │ ├── tsconfig.json │ │ └── version.ts └── tweakpane │ ├── .npmignore │ ├── .npmrc │ ├── package.json │ ├── rollup-doc.config.js │ ├── rollup.config.js │ ├── scripts │ ├── assets-version.js │ ├── doc-build-html.js │ └── main-test-ts-module-pre.js │ ├── src │ ├── doc │ │ ├── img │ │ │ └── og.png │ │ ├── sass │ │ │ ├── _base.scss │ │ │ ├── _defs.scss │ │ │ ├── _pages.scss │ │ │ ├── _reset.scss │ │ │ ├── bundle.scss │ │ │ └── components │ │ │ │ ├── _catalog.scss │ │ │ │ ├── _code-block.scss │ │ │ │ ├── _concept.scss │ │ │ │ ├── _demo.scss │ │ │ │ ├── _global-footer.scss │ │ │ │ ├── _global-header.scss │ │ │ │ ├── _global-nav.scss │ │ │ │ ├── _hljs.scss │ │ │ │ ├── _logo.scss │ │ │ │ ├── _main.scss │ │ │ │ ├── _migration.scss │ │ │ │ ├── _page-header.scss │ │ │ │ ├── _pane-container.scss │ │ │ │ ├── _photo-credit.scss │ │ │ │ ├── _rel.scss │ │ │ │ ├── _sponsor.scss │ │ │ │ └── _theme-builder.scss │ │ ├── template │ │ │ ├── _template.html │ │ │ ├── blades │ │ │ │ └── index.html │ │ │ ├── catalog.html │ │ │ ├── getting-started │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── input-bindings │ │ │ │ └── index.html │ │ │ ├── migration │ │ │ │ ├── datgui │ │ │ │ │ └── index.html │ │ │ │ ├── index.html │ │ │ │ └── v4 │ │ │ │ │ └── index.html │ │ │ ├── misc │ │ │ │ └── index.html │ │ │ ├── monitor-bindings │ │ │ │ └── index.html │ │ │ ├── partial │ │ │ │ ├── _global-footer.html │ │ │ │ ├── _global-header.html │ │ │ │ └── _global-nav.html │ │ │ ├── plugins │ │ │ │ ├── dev │ │ │ │ │ └── index.html │ │ │ │ └── index.html │ │ │ ├── quick-tour │ │ │ │ └── index.html │ │ │ ├── theming │ │ │ │ └── index.html │ │ │ └── ui-components │ │ │ │ └── index.html │ │ ├── ts │ │ │ ├── bundle.ts │ │ │ ├── panepaint.ts │ │ │ ├── plugins │ │ │ │ ├── counter │ │ │ │ │ ├── bundle.ts │ │ │ │ │ ├── controller.ts │ │ │ │ │ ├── plugin.ts │ │ │ │ │ └── view.ts │ │ │ │ └── placeholder │ │ │ │ │ └── bundle.ts │ │ │ ├── preset-test.ts │ │ │ ├── preset.ts │ │ │ ├── route │ │ │ │ ├── blades.ts │ │ │ │ ├── catalog.ts │ │ │ │ ├── getting-started.ts │ │ │ │ ├── index.ts │ │ │ │ ├── input-bindings.ts │ │ │ │ ├── migration-datgui.ts │ │ │ │ ├── migration-v4.ts │ │ │ │ ├── misc.ts │ │ │ │ ├── monitor-bindings.ts │ │ │ │ ├── plugins-dev.ts │ │ │ │ ├── plugins.ts │ │ │ │ ├── quick-tour.ts │ │ │ │ ├── theming.ts │ │ │ │ └── ui-components.ts │ │ │ ├── screw.ts │ │ │ ├── simple-router.ts │ │ │ ├── sketch.ts │ │ │ ├── sp-menu.ts │ │ │ ├── themes.ts │ │ │ └── util.ts │ │ ├── tsconfig.json │ │ └── typedoc.json │ └── main │ │ ├── sass │ │ ├── bundle.scss │ │ └── view │ │ │ ├── _root.scss │ │ │ └── _separator.scss │ │ ├── ts │ │ ├── blade │ │ │ ├── list │ │ │ │ ├── api │ │ │ │ │ ├── list-test.ts │ │ │ │ │ └── list.ts │ │ │ │ ├── plugin-test.ts │ │ │ │ └── plugin.ts │ │ │ ├── root │ │ │ │ ├── api │ │ │ │ │ ├── root-test.ts │ │ │ │ │ └── root.ts │ │ │ │ └── controller │ │ │ │ │ └── root.ts │ │ │ ├── separator │ │ │ │ ├── api │ │ │ │ │ ├── separator-test.ts │ │ │ │ │ └── separator.ts │ │ │ │ ├── controller │ │ │ │ │ └── separator.ts │ │ │ │ ├── plugin-test.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── view │ │ │ │ │ └── separator.ts │ │ │ ├── slider │ │ │ │ ├── api │ │ │ │ │ ├── slider-test.ts │ │ │ │ │ └── slider.ts │ │ │ │ ├── plugin-test.ts │ │ │ │ └── plugin.ts │ │ │ └── text │ │ │ │ ├── api │ │ │ │ ├── text-test.ts │ │ │ │ └── text.ts │ │ │ │ ├── plugin-test.ts │ │ │ │ └── plugin.ts │ │ ├── index.ts │ │ ├── misc │ │ │ └── test-util.ts │ │ └── pane │ │ │ ├── blade-test.ts │ │ │ ├── event-test.ts │ │ │ ├── input-test.ts │ │ │ ├── monitor-test.ts │ │ │ ├── pane-config.ts │ │ │ ├── pane-test.ts │ │ │ ├── pane.ts │ │ │ └── ui-test.ts │ │ ├── tsconfig-dts.json │ │ └── tsconfig.json │ └── test-module │ ├── .npmrc │ ├── node │ └── index.js │ ├── package.json │ ├── plugin │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ ├── test-blade.ts │ │ ├── test-input.ts │ │ └── test-view.ts │ ├── test │ │ ├── browser.html │ │ └── node.js │ └── tsconfig.json │ └── tsc │ ├── index.ts │ └── tsconfig.json ├── prettier.config.js └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.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', 'prettier', 'simple-import-sort'], 10 | root: true, 11 | rules: { 12 | camelcase: 'off', 13 | 'no-console': ['warn', {allow: ['warn', 'error']}], 14 | 'no-unused-vars': 'off', 15 | 'sort-imports': 'off', 16 | 17 | 'prettier/prettier': 'error', 18 | 'simple-import-sort/imports': 'error', 19 | '@typescript-eslint/naming-convention': [ 20 | 'error', 21 | { 22 | selector: 'variable', 23 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'], 24 | custom: { 25 | regex: '^opt_', 26 | match: false, 27 | }, 28 | }, 29 | ], 30 | '@typescript-eslint/explicit-function-return-type': 'off', 31 | '@typescript-eslint/no-empty-function': 'off', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-unused-vars': [ 34 | 'error', 35 | { 36 | argsIgnorePattern: '^_', 37 | }, 38 | ], 39 | 40 | // TODO: Resolve latest lint warnings 41 | '@typescript-eslint/explicit-module-boundary-types': 'off', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cocopon 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | release: 5 | types: [released] 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | # https://github.com/npm/cli/issues/3847 14 | node-version: '16.1.0' 15 | - run: npm install 16 | - run: npm run setup 17 | - run: npm run test --workspaces 18 | - run: npm run coverage 19 | - uses: coverallsapp/github-action@master 20 | continue-on-error: true 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | path-to-lcov: ./coverage/lcov.info 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ -------------------------------------------------------------------------------- /packages/core/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /packages/core/lib/sass/_plugin-test.scss: -------------------------------------------------------------------------------- 1 | @use './tp'; 2 | 3 | .#{tp.$prefix}-test { 4 | @extend %tp-button; 5 | @extend %tp-input; 6 | @extend %tp-monitor; 7 | 8 | background-color: tp.cssVar('input-bg'); 9 | color: tp.cssVar('input-fg'); 10 | transition-duration: tp.$fold-transition-duration; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/lib/sass/_tp.scss: -------------------------------------------------------------------------------- 1 | // Entry point for plugins 2 | 3 | @forward './common/defs'; 4 | 5 | @forward './view/placeholder/button'; 6 | @forward './view/placeholder/container'; 7 | @forward './view/placeholder/folder'; 8 | @forward './view/placeholder/input'; 9 | @forward './view/placeholder/list'; 10 | @forward './view/placeholder/monitor'; 11 | @forward './view/placeholder/texts'; 12 | @forward './view/placeholder/theme'; 13 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_button.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-btnv { 4 | &_b { 5 | @extend %tp-button; 6 | 7 | width: 100%; 8 | } 9 | &_t { 10 | text-align: center; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_checkbox.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-ckbv { 4 | &_l { 5 | display: block; 6 | position: relative; 7 | } 8 | &_i { 9 | @extend %tp-resetUserAgent; 10 | 11 | left: 0; 12 | opacity: 0; 13 | position: absolute; 14 | top: 0; 15 | } 16 | &_w { 17 | background-color: tp.cssVar('input-bg'); 18 | border-radius: tp.cssVar('blade-border-radius'); 19 | cursor: pointer; 20 | display: block; 21 | height: tp.cssVar('container-unit-size'); 22 | position: relative; 23 | width: tp.cssVar('container-unit-size'); 24 | 25 | svg { 26 | display: block; 27 | height: 16px; 28 | inset: 0; 29 | margin: auto; 30 | opacity: 0; 31 | position: absolute; 32 | width: 16px; 33 | 34 | path { 35 | fill: none; 36 | stroke: tp.cssVar('input-fg'); 37 | stroke-width: 2; 38 | } 39 | } 40 | } 41 | &_i:hover + &_w { 42 | background-color: tp.cssVar('input-bg-hover'); 43 | } 44 | &_i:focus + &_w { 45 | background-color: tp.cssVar('input-bg-focus'); 46 | } 47 | &_i:active + &_w { 48 | background-color: tp.cssVar('input-bg-active'); 49 | } 50 | &_i:checked + &_w svg { 51 | opacity: 1; 52 | } 53 | &.#{tp.$disabled} &_w { 54 | opacity: 0.5; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_color-swatch.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | @use './common/color'; 3 | 4 | .#{tp.$prefix}-colswv { 5 | @include color.checkerboard(10px); 6 | 7 | border-radius: tp.cssVar('blade-border-radius'); 8 | overflow: hidden; 9 | 10 | &.#{tp.$disabled} { 11 | opacity: 0.5; 12 | } 13 | 14 | &_sw { 15 | @extend %tp-input; 16 | 17 | border-radius: 0; 18 | } 19 | &_b { 20 | @extend %tp-resetUserAgent; 21 | 22 | cursor: pointer; 23 | display: block; 24 | height: tp.cssVar('container-unit-size'); 25 | left: 0; 26 | position: absolute; 27 | top: 0; 28 | width: tp.cssVar('container-unit-size'); 29 | 30 | &:focus::after { 31 | border: rgba(white, 0.75) solid 2px; 32 | border-radius: tp.cssVar('blade-border-radius'); 33 | content: ''; 34 | display: block; 35 | inset: 0; 36 | position: absolute; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_color-text.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-coltxtv { 4 | display: flex; 5 | width: 100%; 6 | 7 | // Mode selector wrapper 8 | &_m { 9 | @extend %tp-list; 10 | 11 | margin-right: 4px; 12 | } 13 | // Mode selector 14 | &_ms { 15 | @extend %tp-resetUserAgent; 16 | 17 | border-radius: tp.cssVar('blade-border-radius'); 18 | color: tp.cssVar('label-fg'); 19 | cursor: pointer; 20 | height: tp.cssVar('container-unit-size'); 21 | line-height: tp.cssVar('container-unit-size'); 22 | padding: 0 18px 0 4px; 23 | 24 | &:hover { 25 | background-color: tp.cssVar('input-bg-hover'); 26 | } 27 | &:focus { 28 | background-color: tp.cssVar('input-bg-focus'); 29 | } 30 | &:active { 31 | background-color: tp.cssVar('input-bg-active'); 32 | } 33 | } 34 | // Mode selector mark 35 | &_mm { 36 | @extend %tp-list_arrow; 37 | 38 | color: tp.cssVar('label-fg'); 39 | } 40 | &.#{tp.$disabled} &_mm { 41 | opacity: 0.5; 42 | } 43 | // Text components wrapper 44 | &_w { 45 | @extend %tp-texts; 46 | 47 | flex: 1; 48 | } 49 | // Text component 50 | &_c { 51 | @extend %tp-texts_item; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_color.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-colv { 4 | position: relative; 5 | 6 | &_h { 7 | display: flex; 8 | } 9 | &_s { 10 | flex-grow: 0; 11 | flex-shrink: 0; 12 | width: tp.cssVar('container-unit-size'); 13 | } 14 | &_t { 15 | flex: 1; 16 | margin-left: 4px; 17 | } 18 | &_p { 19 | height: 0; 20 | margin-top: 0; 21 | opacity: 0; 22 | overflow: hidden; 23 | transition: height tp.$fold-transition-duration ease-in-out, 24 | opacity tp.$fold-transition-duration linear, 25 | margin tp.$fold-transition-duration ease-in-out; 26 | } 27 | &#{&}-expanded#{&}-cpl &_p { 28 | overflow: visible; 29 | } 30 | &#{&}-expanded &_p { 31 | margin-top: tp.cssVar('container-unit-spacing'); 32 | opacity: 1; 33 | } 34 | 35 | .#{tp.$prefix}-popv { 36 | left: calc(-1 * #{tp.cssVar('container-h-padding')}); 37 | right: calc(-1 * #{tp.cssVar('container-h-padding')}); 38 | top: tp.cssVar('container-unit-size'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_default-wrapper.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-dfwv { 4 | position: absolute; 5 | top: 8px; 6 | right: 8px; 7 | width: 256px; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_folder.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-fldv { 4 | position: relative; 5 | 6 | // Title button 7 | &_b { 8 | @extend %tp-folder_title; 9 | } 10 | // Title 11 | &_t { 12 | padding-left: 4px; 13 | } 14 | // Marker 15 | &_m { 16 | @extend %tp-folder_mark; 17 | } 18 | &_b:disabled &_m { 19 | display: none; 20 | } 21 | &#{&}-expanded > &_b > &_m { 22 | @extend %tp-folder_mark-expanded; 23 | } 24 | // Container 25 | &_c { 26 | @extend %tp-folder_container; 27 | @extend %tp-container_children; 28 | @extend %tp-container_subcontainers; 29 | 30 | padding-left: 4px; 31 | } 32 | &#{&}-expanded > &_c { 33 | @extend %tp-folder_container-expanded; 34 | } 35 | &#{&}-cpl:not(#{&}-expanded) > &_c { 36 | @extend %tp-folder_container-shrinkedCompletely; 37 | } 38 | // Indent 39 | &_i { 40 | bottom: 0; 41 | color: tp.cssVar('container-bg'); 42 | left: 0; 43 | overflow: hidden; 44 | position: absolute; 45 | top: calc(#{tp.cssVar('container-unit-size')} + 4px); 46 | width: max(tp.cssVar('base-border-radius'), 4px); 47 | 48 | &::before { 49 | background-color: currentColor; 50 | bottom: 0; 51 | content: ''; 52 | left: 0; 53 | position: absolute; 54 | top: 0; 55 | width: 4px; 56 | } 57 | } 58 | &_b:hover + &_i { 59 | color: tp.cssVar('container-bg-hover'); 60 | } 61 | &_b:focus + &_i { 62 | color: tp.cssVar('container-bg-focus'); 63 | } 64 | &_b:active + &_i { 65 | color: tp.cssVar('container-bg-active'); 66 | } 67 | &.#{tp.$disabled} > &_i { 68 | opacity: 0.5; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_graph-log.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-grlv { 4 | position: relative; 5 | 6 | &_g { 7 | @extend %tp-monitor; 8 | 9 | display: block; 10 | height: calc(#{tp.cssVar('container-unit-size')} * 3); 11 | 12 | polyline { 13 | fill: none; 14 | stroke: tp.cssVar('monitor-fg'); 15 | stroke-linejoin: round; 16 | } 17 | } 18 | &_t { 19 | margin-top: -4px; 20 | transition: left 0.05s, top 0.05s; 21 | visibility: hidden; 22 | 23 | &#{&}-a { 24 | visibility: visible; 25 | } 26 | &#{&}-in { 27 | transition: none; 28 | } 29 | } 30 | &.#{tp.$disabled} &_g { 31 | opacity: 0.5; 32 | } 33 | 34 | .#{tp.$prefix}-ttv { 35 | background-color: tp.cssVar('monitor-fg'); 36 | 37 | &::before { 38 | border-top-color: tp.cssVar('monitor-fg'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_label.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-lblv { 4 | align-items: center; 5 | display: flex; 6 | line-height: 1.3; 7 | padding-left: tp.cssVar('container-h-padding'); 8 | padding-right: tp.cssVar('container-h-padding'); 9 | 10 | &#{&}-nol { 11 | display: block; 12 | } 13 | 14 | &_l { 15 | color: tp.cssVar('label-fg'); 16 | flex: 1; 17 | hyphens: auto; 18 | overflow: hidden; 19 | padding-left: 4px; 20 | padding-right: 16px; 21 | } 22 | &.#{tp.$disabled} &_l { 23 | opacity: 0.5; 24 | } 25 | &#{&}-nol &_l { 26 | display: none; 27 | } 28 | &_v { 29 | align-self: flex-start; 30 | flex-grow: 0; 31 | flex-shrink: 0; 32 | width: tp.cssVar('blade-value-width'); 33 | } 34 | &#{&}-nol &_v { 35 | width: 100%; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_list.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-lstv { 4 | @extend %tp-list; 5 | 6 | &_s { 7 | @extend %tp-list_select; 8 | 9 | padding: 0 (16px + 2px * 2) 0 tp.cssVar('blade-h-padding'); 10 | width: 100%; 11 | } 12 | &_m { 13 | @extend %tp-list_arrow; 14 | 15 | color: tp.cssVar('button-fg'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_log.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-sglv { 4 | &_i { 5 | @extend %tp-monitor; 6 | 7 | padding-left: tp.cssVar('blade-h-padding'); 8 | padding-right: tp.cssVar('blade-h-padding'); 9 | } 10 | &.#{tp.$disabled} &_i { 11 | opacity: 0.5; 12 | } 13 | } 14 | 15 | .#{tp.$prefix}-mllv { 16 | &_i { 17 | @extend %tp-monitor; 18 | 19 | display: block; 20 | height: calc(#{tp.cssVar('container-unit-size')} * 3); 21 | line-height: tp.cssVar('container-unit-size'); 22 | padding-left: tp.cssVar('blade-h-padding'); 23 | padding-right: tp.cssVar('blade-h-padding'); 24 | resize: none; 25 | white-space: pre; 26 | } 27 | &.#{tp.$disabled} &_i { 28 | opacity: 0.5; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_point-2d-picker.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-p2dpv { 4 | padding-left: calc(#{tp.cssVar('container-unit-size')} + 4px); 5 | 6 | // Pad 7 | &_p { 8 | @extend %tp-input; 9 | 10 | cursor: crosshair; 11 | height: 0; 12 | overflow: hidden; 13 | padding-bottom: 100%; 14 | position: relative; 15 | } 16 | &.#{tp.$disabled} &_p { 17 | opacity: 0.5; 18 | } 19 | // Graphics 20 | &_g { 21 | display: block; 22 | height: 100%; 23 | left: 0; 24 | pointer-events: none; 25 | position: absolute; 26 | top: 0; 27 | width: 100%; 28 | } 29 | // Axis 30 | &_ax { 31 | opacity: 0.1; 32 | stroke: tp.cssVar('input-fg'); 33 | stroke-dasharray: 1; 34 | } 35 | // Line 36 | &_l { 37 | opacity: 0.5; 38 | stroke: tp.cssVar('input-fg'); 39 | stroke-dasharray: 1; 40 | } 41 | // Marker 42 | &_m { 43 | border: tp.cssVar('input-fg') solid 1px; 44 | border-radius: 50%; 45 | box-sizing: border-box; 46 | height: 4px; 47 | margin-left: -2px; 48 | margin-top: -2px; 49 | position: absolute; 50 | width: 4px; 51 | } 52 | &_p:focus &_m { 53 | background-color: tp.cssVar('input-fg'); 54 | border-width: 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_point-2d.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @use '../tp'; 3 | 4 | .#{tp.$prefix}-p2dv { 5 | position: relative; 6 | 7 | // Head 8 | &_h { 9 | display: flex; 10 | } 11 | // Button 12 | &_b { 13 | @extend %tp-button; 14 | 15 | height: tp.cssVar('container-unit-size'); 16 | margin-right: 4px; 17 | position: relative; 18 | width: tp.cssVar('container-unit-size'); 19 | 20 | svg { 21 | display: block; 22 | height: 16px; 23 | left: 50%; 24 | margin-left: math.div(-16px, 2); 25 | margin-top: math.div(-16px, 2); 26 | position: absolute; 27 | top: 50%; 28 | width: 16px; 29 | 30 | path { 31 | stroke: currentColor; 32 | stroke-width: 2; 33 | } 34 | circle { 35 | fill: currentColor; 36 | } 37 | } 38 | } 39 | // Text 40 | &_t { 41 | flex: 1; 42 | } 43 | // Inline picker 44 | &_p { 45 | height: 0; 46 | margin-top: 0; 47 | opacity: 0; 48 | overflow: hidden; 49 | transition: height tp.$fold-transition-duration ease-in-out, 50 | opacity tp.$fold-transition-duration linear, 51 | margin tp.$fold-transition-duration ease-in-out; 52 | } 53 | &#{&}-expanded &_p { 54 | margin-top: tp.cssVar('container-unit-spacing'); 55 | opacity: 1; 56 | } 57 | 58 | // Popup 59 | .#{tp.$prefix}-popv { 60 | left: calc(-1 * #{tp.cssVar('container-h-padding')}); 61 | right: calc(-1 * #{tp.cssVar('container-h-padding')}); 62 | top: tp.cssVar('container-unit-size'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_point-nd-text.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-pndtxtv { 4 | @extend %tp-texts; 5 | 6 | &_a { 7 | @extend %tp-texts_item; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_popup.scss: -------------------------------------------------------------------------------- 1 | @use '../tp'; 2 | 3 | .#{tp.$prefix}-popv { 4 | background-color: tp.cssVar('base-bg'); 5 | border-radius: tp.cssVar('base-border-radius'); 6 | box-shadow: 0 2px 4px tp.cssVar('base-shadow'); 7 | display: none; 8 | max-width: tp.cssVar('blade-value-width'); 9 | padding: tp.cssVar('container-v-padding') tp.cssVar('container-h-padding'); 10 | position: absolute; 11 | visibility: hidden; 12 | z-index: tp.$z-index-picker; 13 | 14 | &#{&}-v { 15 | display: block; 16 | visibility: visible; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_tooltip.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @use '../tp'; 3 | 4 | .#{tp.$prefix}-ttv { 5 | background-color: tp.cssVar('input-fg'); 6 | border-radius: tp.cssVar('blade-border-radius'); 7 | color: tp.cssVar('base-bg'); 8 | padding: 2px 4px; 9 | pointer-events: none; 10 | position: absolute; 11 | transform: translate(-50%, -100%); 12 | 13 | &::before { 14 | $size: 4px; 15 | 16 | border-color: tp.cssVar('input-fg') transparent transparent transparent; 17 | border-style: solid; 18 | border-width: math.div($size, 2); 19 | box-sizing: border-box; 20 | content: ''; 21 | font-size: 0.9em; 22 | height: $size; 23 | left: 50%; 24 | margin-left: math.div(-$size, 2); 25 | position: absolute; 26 | top: 100%; 27 | width: $size; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/_views.scss: -------------------------------------------------------------------------------- 1 | // Entry point for full features 2 | 3 | @forward './button'; 4 | @forward './checkbox'; 5 | @forward './color'; 6 | @forward './color-picker'; 7 | @forward './color-swatch'; 8 | @forward './color-text'; 9 | @forward './default-wrapper'; 10 | @forward './folder'; 11 | @forward './graph-log'; 12 | @forward './label'; 13 | @forward './list'; 14 | @forward './log'; 15 | @forward './point-2d'; 16 | @forward './point-2d-picker'; 17 | @forward './point-nd-text'; 18 | @forward './popup'; 19 | @forward './slider'; 20 | @forward './tab'; 21 | @forward './text'; 22 | @forward './tooltip'; 23 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/common/_color.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | @mixin checkerboard($size) { 4 | $checkerboard-color-dark: #ddd; 5 | $checkerboard-color-light: white; 6 | 7 | background-color: $checkerboard-color-light; 8 | background-image: linear-gradient( 9 | to top right, 10 | $checkerboard-color-dark 25%, 11 | transparent 25%, 12 | transparent 75%, 13 | $checkerboard-color-dark 75% 14 | ), 15 | linear-gradient( 16 | to top right, 17 | $checkerboard-color-dark 25%, 18 | transparent 25%, 19 | transparent 75%, 20 | $checkerboard-color-dark 75% 21 | ); 22 | background-size: $size $size; 23 | background-position: 0 0, math.div($size, 2) math.div($size, 2); 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/placeholder/_button.scss: -------------------------------------------------------------------------------- 1 | @use '../../common/defs'; 2 | 3 | %tp-button { 4 | @extend %tp-resetUserAgent; 5 | 6 | background-color: defs.cssVar('button-bg'); 7 | border-radius: defs.cssVar('blade-border-radius'); 8 | color: defs.cssVar('button-fg'); 9 | cursor: pointer; 10 | display: block; 11 | font-weight: bold; 12 | height: defs.cssVar('container-unit-size'); 13 | line-height: defs.cssVar('container-unit-size'); 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | 18 | &:hover { 19 | background-color: defs.cssVar('button-bg-hover'); 20 | } 21 | &:focus { 22 | background-color: defs.cssVar('button-bg-focus'); 23 | } 24 | &:active { 25 | background-color: defs.cssVar('button-bg-active'); 26 | } 27 | &:disabled { 28 | opacity: 0.5; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/placeholder/_input.scss: -------------------------------------------------------------------------------- 1 | @use '../../common/defs'; 2 | 3 | %tp-input { 4 | @extend %tp-resetUserAgent; 5 | 6 | background-color: defs.cssVar('input-bg'); 7 | border-radius: defs.cssVar('blade-border-radius'); 8 | box-sizing: border-box; 9 | color: defs.cssVar('input-fg'); 10 | font-family: inherit; 11 | height: defs.cssVar('container-unit-size'); 12 | line-height: defs.cssVar('container-unit-size'); 13 | min-width: 0; 14 | width: 100%; 15 | 16 | &:hover { 17 | background-color: defs.cssVar('input-bg-hover'); 18 | } 19 | &:focus { 20 | background-color: defs.cssVar('input-bg-focus'); 21 | } 22 | &:active { 23 | background-color: defs.cssVar('input-bg-active'); 24 | } 25 | &:disabled { 26 | opacity: 0.5; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/placeholder/_list.scss: -------------------------------------------------------------------------------- 1 | @use './button'; 2 | 3 | %tp-list { 4 | position: relative; 5 | } 6 | 7 | %tp-list_select { 8 | @extend %tp-button; 9 | 10 | padding: 0 (16px + 2px * 2) 0 4px; 11 | width: 100%; 12 | } 13 | 14 | %tp-list_arrow { 15 | bottom: 0; 16 | margin: auto; 17 | pointer-events: none; 18 | position: absolute; 19 | right: 2px; 20 | top: 0; 21 | 22 | svg { 23 | bottom: 0; 24 | height: 16px; 25 | margin: auto; 26 | position: absolute; 27 | right: 0; 28 | top: 0; 29 | width: 16px; 30 | 31 | path { 32 | fill: currentColor; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/placeholder/_monitor.scss: -------------------------------------------------------------------------------- 1 | @use '../../common/defs'; 2 | 3 | %tp-monitor { 4 | @extend %tp-resetUserAgent; 5 | 6 | background-color: defs.cssVar('monitor-bg'); 7 | border-radius: defs.cssVar('blade-border-radius'); 8 | box-sizing: border-box; 9 | color: defs.cssVar('monitor-fg'); 10 | height: defs.cssVar('container-unit-size'); 11 | scrollbar-color: currentColor transparent; 12 | scrollbar-width: thin; 13 | width: 100%; 14 | 15 | &::-webkit-scrollbar { 16 | height: 8px; 17 | width: 8px; 18 | } 19 | &::-webkit-scrollbar-corner { 20 | background-color: transparent; 21 | } 22 | &::-webkit-scrollbar-thumb { 23 | background-clip: padding-box; 24 | background-color: currentColor; 25 | border: transparent solid 2px; 26 | border-radius: 4px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/lib/sass/view/placeholder/_texts.scss: -------------------------------------------------------------------------------- 1 | %tp-texts { 2 | display: flex; 3 | } 4 | 5 | %tp-texts_item { 6 | width: 100%; 7 | 8 | & + & { 9 | margin-left: 2px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/scripts/replace-version.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 | const path = process.argv[2]; 11 | const f = Fs.readFileSync(path, 'utf-8'); 12 | Fs.writeFileSync(path, f.replace('0.0.0-core.0', Package.version)); 13 | -------------------------------------------------------------------------------- /packages/core/src/blade/binding/api/input-binding.ts: -------------------------------------------------------------------------------- 1 | import {InputBindingController} from '../controller/input-binding.js'; 2 | import {BindingApi} from './binding.js'; 3 | 4 | /** 5 | * The API for input binding between the parameter and the pane. 6 | * @template In The internal type. 7 | * @template Ex The external type. 8 | */ 9 | export type InputBindingApi = BindingApi< 10 | In, 11 | Ex, 12 | InputBindingController 13 | >; 14 | -------------------------------------------------------------------------------- /packages/core/src/blade/binding/api/monitor-binding.ts: -------------------------------------------------------------------------------- 1 | import {TpBuffer} from '../../../common/model/buffered-value.js'; 2 | import {MonitorBindingController} from '../controller/monitor-binding.js'; 3 | import {BindingApi} from './binding.js'; 4 | 5 | /** 6 | * The API for the monitor binding between the parameter and the pane. 7 | * @template T 8 | */ 9 | export type MonitorBindingApi = BindingApi< 10 | TpBuffer, 11 | T, 12 | MonitorBindingController 13 | >; 14 | -------------------------------------------------------------------------------- /packages/core/src/blade/binding/controller/input-binding.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InputBindingValue, 3 | isInputBindingValue, 4 | } from '../../../common/binding/value/input-binding.js'; 5 | import {ValueController} from '../../../common/controller/value.js'; 6 | import {BladeController} from '../../common/controller/blade.js'; 7 | import { 8 | BladeState, 9 | importBladeState, 10 | } from '../../common/controller/blade-state.js'; 11 | import {isValueBladeController} from '../../common/controller/value-blade.js'; 12 | import {BindingController} from './binding.js'; 13 | 14 | /** 15 | * @hidden 16 | */ 17 | export class InputBindingController< 18 | In = unknown, 19 | Vc extends ValueController = ValueController, 20 | > extends BindingController> { 21 | override importState(state: BladeState): boolean { 22 | return importBladeState( 23 | state, 24 | (s) => super.importState(s), 25 | (p) => ({ 26 | binding: p.required.object({ 27 | value: p.required.raw, 28 | }), 29 | }), 30 | (result) => { 31 | this.value.binding.inject(result.binding.value); 32 | this.value.fetch(); 33 | return true; 34 | }, 35 | ); 36 | } 37 | } 38 | 39 | export function isInputBindingController( 40 | bc: BladeController, 41 | ): bc is InputBindingController { 42 | return isValueBladeController(bc) && isInputBindingValue(bc.value); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/blade/binding/controller/monitor-binding.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isMonitorBindingValue, 3 | MonitorBindingValue, 4 | } from '../../../common/binding/value/monitor-binding.js'; 5 | import {ValueController} from '../../../common/controller/value.js'; 6 | import {BufferedValue, TpBuffer} from '../../../common/model/buffered-value.js'; 7 | import {View} from '../../../common/view/view.js'; 8 | import {BladeController} from '../../common/controller/blade.js'; 9 | import { 10 | BladeState, 11 | exportBladeState, 12 | } from '../../common/controller/blade-state.js'; 13 | import {isValueBladeController} from '../../common/controller/value-blade.js'; 14 | import {BindingController} from './binding.js'; 15 | 16 | export type BufferedValueController< 17 | T, 18 | Vw extends View = View, 19 | > = ValueController, Vw>; 20 | 21 | /** 22 | * @hidden 23 | */ 24 | export class MonitorBindingController< 25 | T = unknown, 26 | Vc extends BufferedValueController = BufferedValueController, 27 | > extends BindingController, Vc, MonitorBindingValue> { 28 | override exportState(): BladeState { 29 | return exportBladeState(() => super.exportState(), { 30 | binding: { 31 | readonly: true, 32 | }, 33 | }); 34 | } 35 | } 36 | 37 | export function isMonitorBindingController( 38 | bc: BladeController, 39 | ): bc is MonitorBindingController { 40 | return ( 41 | isValueBladeController(bc) && 42 | isMonitorBindingValue(bc.value as BufferedValue) 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/blade/button/api/button.ts: -------------------------------------------------------------------------------- 1 | import {BladeApi} from '../../common/api/blade.js'; 2 | import {EventListenable} from '../../common/api/event-listenable.js'; 3 | import {TpMouseEvent} from '../../common/api/tp-event.js'; 4 | import {ButtonBladeController} from '../controller/button-blade.js'; 5 | 6 | export interface ButtonApiEvents { 7 | click: TpMouseEvent; 8 | } 9 | 10 | export class ButtonApi 11 | extends BladeApi 12 | implements EventListenable 13 | { 14 | get label(): string | null | undefined { 15 | return this.controller.labelController.props.get('label'); 16 | } 17 | 18 | set label(label: string | null | undefined) { 19 | this.controller.labelController.props.set('label', label); 20 | } 21 | 22 | get title(): string { 23 | return this.controller.buttonController.props.get('title') ?? ''; 24 | } 25 | 26 | set title(title: string) { 27 | this.controller.buttonController.props.set('title', title); 28 | } 29 | 30 | public on( 31 | eventName: EventName, 32 | handler: (ev: ButtonApiEvents[EventName]) => void, 33 | ): this { 34 | const bh = handler.bind(this); 35 | const emitter = this.controller.buttonController.emitter; 36 | emitter.on( 37 | eventName, 38 | (ev) => { 39 | bh(new TpMouseEvent(this, ev.nativeEvent)); 40 | }, 41 | {key: handler}, 42 | ); 43 | return this; 44 | } 45 | 46 | public off( 47 | eventName: EventName, 48 | handler: (ev: ButtonApiEvents[EventName]) => void, 49 | ): this { 50 | const emitter = this.controller.buttonController.emitter; 51 | emitter.off(eventName, handler); 52 | return this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/blade/button/controller/button-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {ValueMap} from '../../../common/model/value-map.js'; 5 | import {ViewProps} from '../../../common/model/view-props.js'; 6 | import {createTestWindow} from '../../../misc/dom-test-util.js'; 7 | import {ButtonPropsObject} from '../view/button.js'; 8 | import {ButtonController} from './button.js'; 9 | 10 | function createController(doc: Document, title: string) { 11 | return new ButtonController(doc, { 12 | props: ValueMap.fromObject({ 13 | title: title, 14 | }), 15 | viewProps: ViewProps.create(), 16 | }); 17 | } 18 | 19 | describe(ButtonController.name, () => { 20 | it('should emit click event', () => { 21 | const doc = createTestWindow().document; 22 | const c = createController(doc, 'Push'); 23 | 24 | let count = 0; 25 | c.emitter.on('click', () => { 26 | count += 1; 27 | }); 28 | 29 | c.view.buttonElement.click(); 30 | assert.strictEqual(count, 1); 31 | }); 32 | 33 | it('should export state', () => { 34 | const doc = createTestWindow().document; 35 | const c = createController(doc, 'foo'); 36 | 37 | assert.deepStrictEqual(c.exportProps(), { 38 | title: 'foo', 39 | }); 40 | }); 41 | 42 | it('should import state', () => { 43 | const doc = createTestWindow().document; 44 | const c = createController(doc, 'foo'); 45 | 46 | assert.strictEqual( 47 | c.importProps({ 48 | title: 'bar', 49 | }), 50 | true, 51 | ); 52 | assert.strictEqual(c.props.get('title'), 'bar'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/core/src/blade/button/plugin.ts: -------------------------------------------------------------------------------- 1 | import {LabelPropsObject} from '../../common/label/view/label.js'; 2 | import {parseRecord} from '../../common/micro-parsers.js'; 3 | import {ValueMap} from '../../common/model/value-map.js'; 4 | import {BaseBladeParams} from '../../common/params.js'; 5 | import {createPlugin} from '../../plugin/plugin.js'; 6 | import {BladePlugin} from '../plugin.js'; 7 | import {ButtonApi} from './api/button.js'; 8 | import {ButtonBladeController} from './controller/button-blade.js'; 9 | import {ButtonPropsObject} from './view/button.js'; 10 | 11 | export interface ButtonBladeParams extends BaseBladeParams { 12 | title: string; 13 | view: 'button'; 14 | 15 | label?: string; 16 | } 17 | 18 | export const ButtonBladePlugin: BladePlugin = createPlugin({ 19 | id: 'button', 20 | type: 'blade', 21 | accept(params) { 22 | const result = parseRecord(params, (p) => ({ 23 | title: p.required.string, 24 | view: p.required.constant('button'), 25 | 26 | label: p.optional.string, 27 | })); 28 | return result ? {params: result} : null; 29 | }, 30 | controller(args) { 31 | return new ButtonBladeController(args.document, { 32 | blade: args.blade, 33 | buttonProps: ValueMap.fromObject({ 34 | title: args.params.title, 35 | }), 36 | labelProps: ValueMap.fromObject({ 37 | label: args.params.label, 38 | }), 39 | viewProps: args.viewProps, 40 | }); 41 | }, 42 | api(args) { 43 | if (args.controller instanceof ButtonBladeController) { 44 | return new ButtonApi(args.controller); 45 | } 46 | return null; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/core/src/blade/button/view/button.ts: -------------------------------------------------------------------------------- 1 | import {ValueMap} from '../../../common/model/value-map.js'; 2 | import {ViewProps} from '../../../common/model/view-props.js'; 3 | import {ClassName} from '../../../common/view/class-name.js'; 4 | import {bindValueToTextContent} from '../../../common/view/reactive.js'; 5 | import {View} from '../../../common/view/view.js'; 6 | 7 | /** 8 | * @hidden 9 | */ 10 | export type ButtonPropsObject = { 11 | title: string | undefined; 12 | }; 13 | 14 | /** 15 | * @hidden 16 | */ 17 | export type ButtonProps = ValueMap; 18 | 19 | /** 20 | * @hidden 21 | */ 22 | interface Config { 23 | props: ButtonProps; 24 | viewProps: ViewProps; 25 | } 26 | 27 | const cn = ClassName('btn'); 28 | 29 | /** 30 | * @hidden 31 | */ 32 | export class ButtonView implements View { 33 | public readonly element: HTMLElement; 34 | public readonly buttonElement: HTMLButtonElement; 35 | 36 | constructor(doc: Document, config: Config) { 37 | this.element = doc.createElement('div'); 38 | this.element.classList.add(cn()); 39 | config.viewProps.bindClassModifiers(this.element); 40 | 41 | const buttonElem = doc.createElement('button'); 42 | buttonElem.classList.add(cn('b')); 43 | config.viewProps.bindDisabled(buttonElem); 44 | this.element.appendChild(buttonElem); 45 | this.buttonElement = buttonElem; 46 | 47 | const titleElem = doc.createElement('div'); 48 | titleElem.classList.add(cn('t')); 49 | bindValueToTextContent(config.props.value('title'), titleElem); 50 | this.buttonElement.appendChild(titleElem); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/blade-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {ViewProps} from '../../../common/model/view-props.js'; 5 | import {View} from '../../../common/view/view.js'; 6 | import {createTestWindow} from '../../../misc/dom-test-util.js'; 7 | import {BladeController} from '../controller/blade.js'; 8 | import {createBlade} from '../model/blade.js'; 9 | import {BladeApi} from './blade.js'; 10 | 11 | class TestView implements View { 12 | readonly element: HTMLElement; 13 | 14 | constructor(doc: Document) { 15 | this.element = doc.createElement('div'); 16 | } 17 | } 18 | 19 | describe(BladeApi.name, () => { 20 | it('should get element', () => { 21 | const doc = createTestWindow().document; 22 | const v = new TestView(doc); 23 | const c = new BladeController({ 24 | blade: createBlade(), 25 | view: v, 26 | viewProps: ViewProps.create(), 27 | }); 28 | const api = new BladeApi(c); 29 | assert.strictEqual(api.element, v.element); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/blade.ts: -------------------------------------------------------------------------------- 1 | import {BladeController} from '../controller/blade.js'; 2 | import {BladeState} from '../controller/blade-state.js'; 3 | 4 | export class BladeApi { 5 | /** 6 | * @hidden 7 | */ 8 | public readonly controller: C; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | constructor(controller: C) { 14 | this.controller = controller; 15 | } 16 | 17 | get element(): HTMLElement { 18 | return this.controller.view.element; 19 | } 20 | 21 | get disabled(): boolean { 22 | return this.controller.viewProps.get('disabled'); 23 | } 24 | 25 | set disabled(disabled: boolean) { 26 | this.controller.viewProps.set('disabled', disabled); 27 | } 28 | 29 | get hidden(): boolean { 30 | return this.controller.viewProps.get('hidden'); 31 | } 32 | 33 | set hidden(hidden: boolean) { 34 | this.controller.viewProps.set('hidden', hidden); 35 | } 36 | 37 | public dispose(): void { 38 | this.controller.viewProps.set('disposed', true); 39 | } 40 | 41 | public importState(state: BladeState): boolean { 42 | return this.controller.importState(state); 43 | } 44 | 45 | public exportState(): BladeState { 46 | return this.controller.exportState(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/container-blade.ts: -------------------------------------------------------------------------------- 1 | import {PluginPool} from '../../../plugin/pool.js'; 2 | import {ContainerBladeController} from '../controller/container-blade.js'; 3 | import {BladeApi} from './blade.js'; 4 | import {RackApi} from './rack.js'; 5 | import {Refreshable} from './refreshable.js'; 6 | 7 | export class ContainerBladeApi 8 | extends BladeApi 9 | implements Refreshable 10 | { 11 | /** 12 | * @hidden 13 | */ 14 | protected readonly rackApi_: RackApi; 15 | 16 | /** 17 | * @hidden 18 | */ 19 | constructor(controller: C, pool: PluginPool) { 20 | super(controller); 21 | 22 | this.rackApi_ = new RackApi(controller.rackController, pool); 23 | } 24 | 25 | public refresh(): void { 26 | this.rackApi_.refresh(); 27 | } 28 | } 29 | 30 | export function isContainerBladeApi( 31 | api: BladeApi, 32 | ): api is ContainerBladeApi { 33 | return 'rackApi_' in api; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/event-listenable.ts: -------------------------------------------------------------------------------- 1 | export interface EventListenable { 2 | off( 3 | eventName: EventName, 4 | handler: (ev: E[EventName]) => void, 5 | ): this; 6 | on( 7 | eventName: EventName, 8 | handler: (ev: E[EventName]) => void, 9 | ): this; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/events.ts: -------------------------------------------------------------------------------- 1 | import {BladeApi} from './blade.js'; 2 | import {TpChangeEvent} from './tp-event.js'; 3 | 4 | export interface ApiChangeEvents { 5 | change: TpChangeEvent; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/refreshable-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {isRefreshable} from './refreshable.js'; 5 | 6 | describe(isRefreshable.name, () => { 7 | it('should determine Refreshable', () => { 8 | assert.strictEqual( 9 | isRefreshable({ 10 | refresh: () => {}, 11 | }), 12 | true, 13 | ); 14 | }); 15 | 16 | it('should not determine variable as Refreshable', () => { 17 | assert.strictEqual( 18 | isRefreshable({ 19 | refresh: 1, 20 | }), 21 | false, 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/refreshable.ts: -------------------------------------------------------------------------------- 1 | import {isObject} from '../../../misc/type-util.js'; 2 | 3 | export interface Refreshable { 4 | /** 5 | * Refreshes the target. 6 | */ 7 | refresh(): void; 8 | } 9 | 10 | export function isRefreshable(value: unknown): value is Refreshable { 11 | if (!isObject(value)) { 12 | return false; 13 | } 14 | return 'refresh' in value && typeof value.refresh === 'function'; 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/api/test-util.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import {BladeApi} from './blade.js'; 4 | 5 | export function assertInitialState(api: BladeApi) { 6 | assert.strictEqual(api.disabled, false); 7 | assert.strictEqual(api.hidden, false); 8 | assert.strictEqual(api.controller.viewProps.get('disposed'), false); 9 | } 10 | 11 | export function assertDisposes(api: BladeApi) { 12 | api.dispose(); 13 | assert.strictEqual(api.controller.viewProps.get('disposed'), true); 14 | } 15 | 16 | export function assertUpdates(api: BladeApi) { 17 | api.disabled = true; 18 | assert.strictEqual(api.disabled, true); 19 | assert.strictEqual( 20 | api.controller.view.element.classList.contains('tp-v-disabled'), 21 | true, 22 | ); 23 | 24 | api.hidden = true; 25 | assert.strictEqual(api.hidden, true); 26 | assert.strictEqual( 27 | api.controller.view.element.classList.contains('tp-v-hidden'), 28 | true, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/controller/container-blade.ts: -------------------------------------------------------------------------------- 1 | import {View} from '../../../common/view/view.js'; 2 | import {Blade} from '../model/blade.js'; 3 | import {BladeController} from './blade.js'; 4 | import {BladeState, exportBladeState, importBladeState} from './blade-state.js'; 5 | import {RackController} from './rack.js'; 6 | 7 | /** 8 | * @hidden 9 | */ 10 | interface Config { 11 | blade: Blade; 12 | rackController: RackController; 13 | view: V; 14 | } 15 | 16 | /** 17 | * @hidden 18 | */ 19 | export class ContainerBladeController< 20 | V extends View = View, 21 | > extends BladeController { 22 | public readonly rackController: RackController; 23 | 24 | constructor(config: Config) { 25 | super({ 26 | blade: config.blade, 27 | view: config.view, 28 | viewProps: config.rackController.viewProps, 29 | }); 30 | this.rackController = config.rackController; 31 | } 32 | 33 | public importState(state: BladeState): boolean { 34 | return importBladeState( 35 | state, 36 | (s) => super.importState(s), 37 | (p) => ({ 38 | children: p.required.array(p.required.raw), 39 | }), 40 | (result) => { 41 | return this.rackController.rack.children.every((c, index) => { 42 | return c.importState(result.children[index] as BladeState); 43 | }); 44 | }, 45 | ); 46 | } 47 | 48 | public exportState(): BladeState { 49 | return exportBladeState(() => super.exportState(), { 50 | children: this.rackController.rack.children.map((c) => c.exportState()), 51 | }); 52 | } 53 | } 54 | 55 | export function isContainerBladeController( 56 | bc: BladeController, 57 | ): bc is ContainerBladeController { 58 | return 'rackController' in bc; 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/controller/rack.ts: -------------------------------------------------------------------------------- 1 | import {insertElementAt, removeElement} from '../../../common/dom-util.js'; 2 | import {ViewProps} from '../../../common/model/view-props.js'; 3 | import {Blade} from '../model/blade.js'; 4 | import {Rack, RackEvents} from '../model/rack.js'; 5 | 6 | /** 7 | * @hidden 8 | */ 9 | interface Config { 10 | blade: Blade; 11 | element: HTMLElement; 12 | viewProps: ViewProps; 13 | 14 | root?: boolean; 15 | } 16 | 17 | /** 18 | * @hidden 19 | */ 20 | export class RackController { 21 | public readonly element: HTMLElement; 22 | public readonly rack: Rack; 23 | public readonly viewProps: ViewProps; 24 | 25 | constructor(config: Config) { 26 | this.onRackAdd_ = this.onRackAdd_.bind(this); 27 | this.onRackRemove_ = this.onRackRemove_.bind(this); 28 | 29 | this.element = config.element; 30 | this.viewProps = config.viewProps; 31 | 32 | const rack = new Rack({ 33 | blade: config.root ? undefined : config.blade, 34 | viewProps: config.viewProps, 35 | }); 36 | rack.emitter.on('add', this.onRackAdd_); 37 | rack.emitter.on('remove', this.onRackRemove_); 38 | this.rack = rack; 39 | 40 | this.viewProps.handleDispose(() => { 41 | for (let i = this.rack.children.length - 1; i >= 0; i--) { 42 | const bc = this.rack.children[i]; 43 | bc.viewProps.set('disposed', true); 44 | } 45 | }); 46 | } 47 | 48 | private onRackAdd_(ev: RackEvents['add']): void { 49 | if (!ev.root) { 50 | return; 51 | } 52 | insertElementAt(this.element, ev.bladeController.view.element, ev.index); 53 | } 54 | 55 | private onRackRemove_(ev: RackEvents['remove']): void { 56 | if (!ev.root) { 57 | return; 58 | } 59 | removeElement(ev.bladeController.view.element); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/controller/value-blade.ts: -------------------------------------------------------------------------------- 1 | import {ValueController} from '../../../common/controller/value.js'; 2 | import {BladeController} from './blade.js'; 3 | 4 | export function isValueBladeController( 5 | bc: BladeController, 6 | ): bc is BladeController & ValueController { 7 | return 'value' in bc; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/model/blade-positions.ts: -------------------------------------------------------------------------------- 1 | export type BladePosition = 'veryfirst' | 'first' | 'last' | 'verylast'; 2 | 3 | export function getAllBladePositions(): BladePosition[] { 4 | return ['veryfirst', 'first', 'last', 'verylast']; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/model/blade.ts: -------------------------------------------------------------------------------- 1 | import {ValueMap} from '../../../common/model/value-map.js'; 2 | import {createValue} from '../../../common/model/values.js'; 3 | import {deepEqualsArray} from '../../../misc/type-util.js'; 4 | import {BladePosition} from './blade-positions.js'; 5 | 6 | export type Blade = ValueMap<{ 7 | positions: BladePosition[]; 8 | }>; 9 | 10 | export function createBlade(): Blade { 11 | return new ValueMap({ 12 | positions: createValue([], { 13 | equals: deepEqualsArray, 14 | }), 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/blade/common/view/blade-container.ts: -------------------------------------------------------------------------------- 1 | import {ClassName} from '../../../common/view/class-name.js'; 2 | 3 | export const bladeContainerClassName = ClassName('cnt'); 4 | -------------------------------------------------------------------------------- /packages/core/src/blade/folder/plugin-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe} from 'mocha'; 3 | 4 | import {createTestWindow} from '../../misc/dom-test-util.js'; 5 | import {createDefaultPluginPool} from '../../plugin/plugins.js'; 6 | import {createEmptyBladeController} from '../test-util.js'; 7 | import {FolderBladePlugin} from './plugin.js'; 8 | 9 | describe(FolderBladePlugin.id, () => { 10 | [(doc: Document) => createEmptyBladeController(doc)].forEach( 11 | (createController) => { 12 | it('should not create API', () => { 13 | const doc = createTestWindow().document; 14 | const c = createController(doc); 15 | const api = FolderBladePlugin.api({ 16 | controller: c, 17 | pool: createDefaultPluginPool(), 18 | }); 19 | assert.strictEqual(api, null); 20 | }); 21 | }, 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/core/src/blade/folder/plugin.ts: -------------------------------------------------------------------------------- 1 | import {parseRecord} from '../../common/micro-parsers.js'; 2 | import {ValueMap} from '../../common/model/value-map.js'; 3 | import {BaseBladeParams} from '../../common/params.js'; 4 | import {createPlugin} from '../../plugin/plugin.js'; 5 | import {BladePlugin} from '../plugin.js'; 6 | import {FolderApi} from './api/folder.js'; 7 | import {FolderController} from './controller/folder.js'; 8 | import {FolderPropsObject} from './view/folder.js'; 9 | 10 | export interface FolderBladeParams extends BaseBladeParams { 11 | title: string; 12 | view: 'folder'; 13 | 14 | expanded?: boolean; 15 | } 16 | 17 | export const FolderBladePlugin: BladePlugin = createPlugin({ 18 | id: 'folder', 19 | type: 'blade', 20 | accept(params) { 21 | const result = parseRecord(params, (p) => ({ 22 | title: p.required.string, 23 | view: p.required.constant('folder'), 24 | 25 | expanded: p.optional.boolean, 26 | })); 27 | return result ? {params: result} : null; 28 | }, 29 | controller(args) { 30 | return new FolderController(args.document, { 31 | blade: args.blade, 32 | expanded: args.params.expanded, 33 | props: ValueMap.fromObject({ 34 | title: args.params.title, 35 | }), 36 | viewProps: args.viewProps, 37 | }); 38 | }, 39 | api(args) { 40 | if (!(args.controller instanceof FolderController)) { 41 | return null; 42 | } 43 | return new FolderApi(args.controller, args.pool); 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/core/src/blade/tab/controller/tab-item.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '../../../common/controller/controller.js'; 2 | import {Emitter} from '../../../common/model/emitter.js'; 3 | import {ViewProps} from '../../../common/model/view-props.js'; 4 | import {TabItemProps, TabItemView} from '../view/tab-item.js'; 5 | 6 | /** 7 | * @hidden 8 | */ 9 | export interface TabItemEvents { 10 | click: { 11 | sender: TabItemController; 12 | }; 13 | } 14 | 15 | /** 16 | * @hidden 17 | */ 18 | interface Config { 19 | props: TabItemProps; 20 | viewProps: ViewProps; 21 | } 22 | 23 | /** 24 | * @hidden 25 | */ 26 | export class TabItemController implements Controller { 27 | public readonly emitter: Emitter = new Emitter(); 28 | public readonly props: TabItemProps; 29 | public readonly view: TabItemView; 30 | public readonly viewProps: ViewProps; 31 | 32 | constructor(doc: Document, config: Config) { 33 | this.onClick_ = this.onClick_.bind(this); 34 | 35 | this.props = config.props; 36 | this.viewProps = config.viewProps; 37 | 38 | this.view = new TabItemView(doc, { 39 | props: config.props, 40 | viewProps: config.viewProps, 41 | }); 42 | this.view.buttonElement.addEventListener('click', this.onClick_); 43 | } 44 | 45 | private onClick_(): void { 46 | this.emitter.emit('click', { 47 | sender: this, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/blade/tab/view/tab-page.ts: -------------------------------------------------------------------------------- 1 | import {ViewProps} from '../../../common/model/view-props.js'; 2 | import {ClassName} from '../../../common/view/class-name.js'; 3 | import {View} from '../../../common/view/view.js'; 4 | 5 | interface Config { 6 | viewProps: ViewProps; 7 | } 8 | 9 | const cn = ClassName('tbp'); 10 | 11 | /** 12 | * @hidden 13 | */ 14 | export class TabPageView implements View { 15 | public readonly element: HTMLElement; 16 | public readonly containerElement: HTMLElement; 17 | 18 | constructor(doc: Document, config: Config) { 19 | this.element = doc.createElement('div'); 20 | this.element.classList.add(cn()); 21 | config.viewProps.bindClassModifiers(this.element); 22 | 23 | const containerElem = doc.createElement('div'); 24 | containerElem.classList.add(cn('c')); 25 | this.element.appendChild(containerElem); 26 | this.containerElement = containerElem; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/blade/tab/view/tab.ts: -------------------------------------------------------------------------------- 1 | import {bindValue} from '../../../common/model/reactive.js'; 2 | import {Value} from '../../../common/model/value.js'; 3 | import {ViewProps} from '../../../common/model/view-props.js'; 4 | import {ClassName} from '../../../common/view/class-name.js'; 5 | import {valueToClassName} from '../../../common/view/reactive.js'; 6 | import {View} from '../../../common/view/view.js'; 7 | import {bladeContainerClassName} from '../../common/view/blade-container.js'; 8 | 9 | /** 10 | * @hidden 11 | */ 12 | interface Config { 13 | empty: Value; 14 | viewProps: ViewProps; 15 | } 16 | 17 | const cn = ClassName('tab'); 18 | 19 | /** 20 | * @hidden 21 | */ 22 | export class TabView implements View { 23 | public readonly element: HTMLElement; 24 | public readonly itemsElement: HTMLElement; 25 | public readonly contentsElement: HTMLElement; 26 | 27 | constructor(doc: Document, config: Config) { 28 | this.element = doc.createElement('div'); 29 | this.element.classList.add(cn(), bladeContainerClassName()); 30 | config.viewProps.bindClassModifiers(this.element); 31 | bindValue( 32 | config.empty, 33 | valueToClassName(this.element, cn(undefined, 'nop')), 34 | ); 35 | 36 | const titleElem = doc.createElement('div'); 37 | titleElem.classList.add(cn('t')); 38 | this.element.appendChild(titleElem); 39 | this.itemsElement = titleElem; 40 | 41 | const indentElem = doc.createElement('div'); 42 | indentElem.classList.add(cn('i')); 43 | this.element.appendChild(indentElem); 44 | 45 | const contentsElem = doc.createElement('div'); 46 | contentsElem.classList.add(cn('c')); 47 | this.element.appendChild(contentsElem); 48 | this.contentsElement = contentsElem; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/common/api/list.ts: -------------------------------------------------------------------------------- 1 | import {BindingApi} from '../../blade/binding/api/binding.js'; 2 | import {InputBindingApi} from '../../blade/binding/api/input-binding.js'; 3 | import {InputBindingController} from '../../blade/binding/controller/input-binding.js'; 4 | import {ListItem} from '../constraint/list.js'; 5 | import {ListController} from '../controller/list.js'; 6 | 7 | export class ListInputBindingApi 8 | extends BindingApi>> 9 | implements InputBindingApi 10 | { 11 | get options(): ListItem[] { 12 | return this.controller.valueController.props.get('options'); 13 | } 14 | 15 | set options(options: ListItem[]) { 16 | this.controller.valueController.props.set('options', options); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/binding.ts: -------------------------------------------------------------------------------- 1 | import {isObject} from '../../misc/type-util.js'; 2 | import {BindingTarget} from './target.js'; 3 | 4 | /** 5 | * Converts an external unknown value into the internal value. 6 | * @template In The type of the internal value. 7 | */ 8 | export interface BindingReader { 9 | /** 10 | * @param exValue The bound value. 11 | * @return A converted value. 12 | */ 13 | (exValue: unknown): In; 14 | } 15 | 16 | /** 17 | * Writes the internal value to the bound target. 18 | * @template In The type of the internal value. 19 | */ 20 | export interface BindingWriter { 21 | /** 22 | * @param target The target to be written. 23 | * @param inValue The value to write. 24 | */ 25 | (target: BindingTarget, inValue: In): void; 26 | } 27 | 28 | /** 29 | * @hidden 30 | */ 31 | export interface Binding { 32 | readonly target: BindingTarget; 33 | } 34 | 35 | export function isBinding(value: unknown): value is Binding { 36 | if (!isObject(value)) { 37 | return false; 38 | } 39 | return 'target' in value; 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/read-write-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {numberFromUnknown} from '../converter/number.js'; 5 | import {writePrimitive} from '../primitive.js'; 6 | import {ReadWriteBinding} from './read-write.js'; 7 | import {BindingTarget} from './target.js'; 8 | 9 | describe(ReadWriteBinding.name, () => { 10 | it('should read value', () => { 11 | const obj = { 12 | foo: 123, 13 | }; 14 | const target = new BindingTarget(obj, 'foo'); 15 | const b = new ReadWriteBinding({ 16 | reader: numberFromUnknown, 17 | target: target, 18 | writer: (v) => v, 19 | }); 20 | 21 | assert.strictEqual(b.read(), 123); 22 | }); 23 | 24 | it('should write value', () => { 25 | const obj = { 26 | foo: 123, 27 | }; 28 | const target = new BindingTarget(obj, 'foo'); 29 | const b = new ReadWriteBinding({ 30 | reader: numberFromUnknown, 31 | target: target, 32 | writer: writePrimitive, 33 | }); 34 | b.write(456); 35 | 36 | assert.strictEqual(obj.foo, 456); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/read-write.ts: -------------------------------------------------------------------------------- 1 | import {Binding, BindingReader, BindingWriter} from './binding.js'; 2 | import {BindingTarget} from './target.js'; 3 | 4 | /** 5 | * @hidden 6 | */ 7 | interface Config { 8 | reader: BindingReader; 9 | target: BindingTarget; 10 | writer: BindingWriter; 11 | } 12 | 13 | /** 14 | * A binding that can read and write the target. 15 | * @hidden 16 | * @template In The type of the internal value. 17 | */ 18 | export class ReadWriteBinding implements Binding { 19 | public readonly target: BindingTarget; 20 | private readonly reader_: BindingReader; 21 | private readonly writer_: BindingWriter; 22 | 23 | constructor(config: Config) { 24 | this.target = config.target; 25 | this.reader_ = config.reader; 26 | this.writer_ = config.writer; 27 | } 28 | 29 | public read(): In { 30 | return this.reader_(this.target.read()); 31 | } 32 | 33 | public write(value: In): void { 34 | this.writer_(this.target, value); 35 | } 36 | 37 | public inject(value: unknown): void { 38 | this.write(this.reader_(value)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/readonly.ts: -------------------------------------------------------------------------------- 1 | import {Binding, BindingReader} from './binding.js'; 2 | import {BindingTarget} from './target.js'; 3 | 4 | /** 5 | * @hidden 6 | */ 7 | interface Config { 8 | reader: BindingReader; 9 | target: BindingTarget; 10 | } 11 | 12 | /** 13 | * A binding that can read the target. 14 | * @hidden 15 | * @template In The type of the internal value. 16 | */ 17 | export class ReadonlyBinding implements Binding { 18 | public readonly target: BindingTarget; 19 | private readonly reader_: BindingReader; 20 | 21 | constructor(config: Config) { 22 | this.target = config.target; 23 | this.reader_ = config.reader; 24 | } 25 | 26 | public read(): In { 27 | return this.reader_(this.target.read()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/target-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {BindingTarget} from './target.js'; 5 | 6 | describe(BindingTarget.name, () => { 7 | it('should get properties', () => { 8 | const obj = {foo: 'bar'}; 9 | const target = new BindingTarget(obj, 'foo'); 10 | assert.strictEqual(target.key, 'foo'); 11 | }); 12 | 13 | it('should read value', () => { 14 | const obj = {foo: 'bar'}; 15 | const target = new BindingTarget(obj, 'foo'); 16 | assert.strictEqual(target.read(), 'bar'); 17 | }); 18 | 19 | it('should write value', () => { 20 | const obj = {foo: 'bar'}; 21 | const target = new BindingTarget(obj, 'foo'); 22 | target.write('wrote'); 23 | assert.strictEqual(obj.foo, 'wrote'); 24 | }); 25 | 26 | it('should bind static class field', () => { 27 | class Test { 28 | static foo = 1; 29 | } 30 | 31 | assert.doesNotThrow(() => { 32 | new BindingTarget(Test, 'foo'); 33 | }); 34 | }); 35 | 36 | it('should determine class is bindable', () => { 37 | class Test { 38 | static foo = 1; 39 | } 40 | 41 | assert.strictEqual(BindingTarget.isBindable(Test), true); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/target.ts: -------------------------------------------------------------------------------- 1 | import {TpError} from '../tp-error.js'; 2 | 3 | export type Bindable = Record; 4 | 5 | /** 6 | * A binding target. 7 | */ 8 | export class BindingTarget { 9 | /** 10 | * The property name of the binding. 11 | */ 12 | public readonly key: string; 13 | private readonly obj_: Bindable; 14 | 15 | /** 16 | * @hidden 17 | */ 18 | constructor(obj: Bindable, key: string) { 19 | this.obj_ = obj; 20 | this.key = key; 21 | } 22 | 23 | public static isBindable(obj: unknown): obj is Bindable { 24 | if (obj === null) { 25 | return false; 26 | } 27 | if (typeof obj !== 'object' && typeof obj !== 'function') { 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | /** 34 | * Read a bound value. 35 | * @return A bound value 36 | */ 37 | public read(): unknown { 38 | return this.obj_[this.key]; 39 | } 40 | 41 | /** 42 | * Write a value. 43 | * @param value The value to write to the target. 44 | */ 45 | public write(value: unknown): void { 46 | this.obj_[this.key] = value; 47 | } 48 | 49 | /** 50 | * Write a value to the target property. 51 | * @param name The property name. 52 | * @param value The value to write to the target. 53 | */ 54 | public writeProperty(name: string, value: unknown): void { 55 | const valueObj = this.read(); 56 | 57 | if (!BindingTarget.isBindable(valueObj)) { 58 | throw TpError.notBindable(); 59 | } 60 | if (!(name in valueObj)) { 61 | throw TpError.propertyNotFound(name); 62 | } 63 | valueObj[name] = value; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/ticker/interval-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {createTestWindow} from '../../../misc/dom-test-util.js'; 5 | import {IntervalTicker} from './interval.js'; 6 | 7 | describe(IntervalTicker.name, () => { 8 | it('should not create timer for negative interval', (done) => { 9 | const doc = createTestWindow().document; 10 | 11 | const t0 = new IntervalTicker(doc, 0); 12 | t0.emitter.on('tick', () => { 13 | assert.fail('should not be called'); 14 | }); 15 | const tn = new IntervalTicker(doc, -100); 16 | tn.emitter.on('tick', () => { 17 | assert.fail('should not be called'); 18 | }); 19 | 20 | setTimeout(() => { 21 | done(); 22 | }, 10); 23 | }); 24 | 25 | it('should tick', (done) => { 26 | const doc = createTestWindow().document; 27 | const t = new IntervalTicker(doc, 1); 28 | 29 | t.emitter.on('tick', () => { 30 | t.dispose(); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('should be enabled by default', () => { 36 | const doc = createTestWindow().document; 37 | const t = new IntervalTicker(doc, 0); 38 | 39 | assert.strictEqual(t.disabled, false); 40 | }); 41 | 42 | it('should not tick if disabled', (done) => { 43 | const doc = createTestWindow().document; 44 | const t = new IntervalTicker(doc, 1); 45 | t.disabled = true; 46 | t.emitter.on('tick', () => { 47 | assert.fail('should not called'); 48 | }); 49 | 50 | setTimeout(done, 10); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/ticker/manual-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {ManualTicker} from './manual.js'; 5 | 6 | describe(ManualTicker.name, () => { 7 | it('should be enabled by default', () => { 8 | const t = new ManualTicker(); 9 | assert.strictEqual(t.disabled, false); 10 | }); 11 | 12 | it('should fire tick event', () => { 13 | const t = new ManualTicker(); 14 | let count = 0; 15 | t.emitter.on('tick', () => { 16 | count += 1; 17 | }); 18 | 19 | assert.strictEqual(count, 0); 20 | t.tick(); 21 | assert.strictEqual(count, 1); 22 | }); 23 | 24 | it('should not fire tick event from disabled ticker', () => { 25 | const t = new ManualTicker(); 26 | t.emitter.on('tick', () => { 27 | assert.fail('should not be called'); 28 | }); 29 | 30 | t.disabled = true; 31 | assert.doesNotThrow(() => { 32 | t.tick(); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/ticker/manual.ts: -------------------------------------------------------------------------------- 1 | import {Emitter} from '../../model/emitter.js'; 2 | import {Ticker, TickerEvents} from './ticker.js'; 3 | 4 | /** 5 | * @hidden 6 | */ 7 | export class ManualTicker implements Ticker { 8 | public readonly emitter: Emitter; 9 | public disabled = false; 10 | 11 | constructor() { 12 | this.emitter = new Emitter(); 13 | } 14 | 15 | public dispose(): void {} 16 | 17 | public tick(): void { 18 | if (this.disabled) { 19 | return; 20 | } 21 | 22 | this.emitter.emit('tick', { 23 | sender: this, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/ticker/ticker.ts: -------------------------------------------------------------------------------- 1 | import {Emitter} from '../../model/emitter.js'; 2 | 3 | /** 4 | * @hidden 5 | */ 6 | export interface TickerEvents { 7 | tick: { 8 | sender: Ticker; 9 | }; 10 | } 11 | 12 | /** 13 | * @hidden 14 | */ 15 | export interface Ticker { 16 | readonly emitter: Emitter; 17 | disabled: boolean; 18 | 19 | dispose(): void; 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/value/binding.ts: -------------------------------------------------------------------------------- 1 | import {isObject} from '../../../misc/type-util.js'; 2 | import {Value} from '../../model/value.js'; 3 | import {Binding, isBinding} from '../binding.js'; 4 | 5 | /** 6 | * @hidden 7 | */ 8 | export interface BindingValue extends Value { 9 | readonly binding: Binding; 10 | fetch(): void; 11 | } 12 | 13 | export function isBindingValue(v: unknown): v is BindingValue { 14 | if (!isObject(v) || !('binding' in v)) { 15 | return false; 16 | } 17 | const b = (v as {binding: unknown}).binding; 18 | return isBinding(b); 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/common/binding/value/input-binding-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {PrimitiveValue} from '../../model/primitive-value.js'; 5 | import {ReadWriteBinding} from '../read-write.js'; 6 | import {BindingTarget} from '../target.js'; 7 | import {InputBindingValue} from './input-binding.js'; 8 | 9 | describe(InputBindingValue.name, () => { 10 | it('should apply rawValue to target', () => { 11 | const iv = new PrimitiveValue(0); 12 | const target = new BindingTarget({foo: 0}, 'foo'); 13 | const bv = new InputBindingValue( 14 | iv, 15 | new ReadWriteBinding({ 16 | reader: (v: unknown) => Number(v), 17 | writer: (t, v) => t.write(v), 18 | target: target, 19 | }), 20 | ); 21 | 22 | bv.rawValue = 1; 23 | assert.strictEqual(target.read(), 1); 24 | }); 25 | 26 | it('should have its own sender', (done) => { 27 | const iv = new PrimitiveValue(0); 28 | const bv = new InputBindingValue( 29 | iv, 30 | new ReadWriteBinding({ 31 | reader: (v: unknown) => Number(v), 32 | writer: (t, v) => t.write(v), 33 | target: new BindingTarget({foo: 0}, 'foo'), 34 | }), 35 | ); 36 | bv.emitter.on('change', (ev) => { 37 | assert.strictEqual(ev.sender, bv); 38 | done(); 39 | }); 40 | iv.rawValue = 1; 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/core/src/common/compat.ts: -------------------------------------------------------------------------------- 1 | import {Semver} from '../misc/semver.js'; 2 | import {VERSION} from '../version.js'; 3 | 4 | export function warnDeprecation(info: { 5 | name: string; 6 | alternative?: string; 7 | postscript?: string; 8 | }) { 9 | console.warn( 10 | [ 11 | `${info.name} is deprecated.`, 12 | info.alternative ? `use ${info.alternative} instead.` : '', 13 | info.postscript ?? '', 14 | ].join(' '), 15 | ); 16 | } 17 | 18 | export function warnMissing(info: { 19 | key: string; 20 | target: string; 21 | place: string; 22 | }) { 23 | console.warn( 24 | [ 25 | `Missing '${info.key}' of ${info.target} in ${info.place}.`, 26 | 'Please rebuild plugins with the latest core package.', 27 | ].join(' '), 28 | ); 29 | } 30 | 31 | export function isCompatible(ver: Semver | undefined): boolean { 32 | if (!ver) { 33 | // Version 1.x 34 | return false; 35 | } 36 | return ver.major === VERSION.major; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/composite.ts: -------------------------------------------------------------------------------- 1 | import {Class} from '../../misc/type-util.js'; 2 | import {Constraint} from './constraint.js'; 3 | 4 | /** 5 | * A constraint to combine multiple constraints. 6 | * @template T The type of the value. 7 | */ 8 | export class CompositeConstraint implements Constraint { 9 | public readonly constraints: Constraint[]; 10 | 11 | constructor(constraints: Constraint[]) { 12 | this.constraints = constraints; 13 | } 14 | 15 | public constrain(value: T): T { 16 | return this.constraints.reduce((result, c) => { 17 | return c.constrain(result); 18 | }, value); 19 | } 20 | } 21 | 22 | export function findConstraint( 23 | c: Constraint, 24 | constraintClass: Class, 25 | ): C | null { 26 | if (c instanceof constraintClass) { 27 | return c; 28 | } 29 | 30 | if (c instanceof CompositeConstraint) { 31 | const result = c.constraints.reduce((tmpResult: C | null, sc) => { 32 | if (tmpResult) { 33 | return tmpResult; 34 | } 35 | 36 | return sc instanceof constraintClass ? sc : null; 37 | }, null); 38 | if (result) { 39 | return result; 40 | } 41 | } 42 | 43 | return null; 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/constraint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A constraint for the value. 3 | * @template T The type of the value. 4 | */ 5 | export interface Constraint { 6 | /** 7 | * Constrains the value. 8 | * @param value The value. 9 | * @return A constarined value. 10 | */ 11 | constrain(value: T): T; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/definite-range-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {DefiniteRangeConstraint} from './definite-range.js'; 5 | 6 | describe(DefiniteRangeConstraint.name, () => { 7 | it('should constrain value with minimun and maximum value', () => { 8 | const c = new DefiniteRangeConstraint({ 9 | max: 123, 10 | min: -123, 11 | }); 12 | assert.strictEqual(c.constrain(-124), -123); 13 | assert.strictEqual(c.constrain(0), 0); 14 | assert.strictEqual(c.constrain(124), 123); 15 | }); 16 | 17 | it('should constrain value with updated range', () => { 18 | const c = new DefiniteRangeConstraint({ 19 | max: 1, 20 | min: 0, 21 | }); 22 | c.values.set('min', -10); 23 | c.values.set('max', 10); 24 | 25 | assert.strictEqual(c.constrain(-100), -10); 26 | assert.strictEqual(c.constrain(100), 10); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/definite-range.ts: -------------------------------------------------------------------------------- 1 | import {ValueMap} from '../model/value-map.js'; 2 | import {Constraint} from './constraint.js'; 3 | 4 | interface Config { 5 | max: number; 6 | min: number; 7 | } 8 | 9 | /** 10 | * A number range constraint that cannot be undefined. Used for slider control. 11 | */ 12 | export class DefiniteRangeConstraint implements Constraint { 13 | public readonly values: ValueMap<{ 14 | max: number; 15 | min: number; 16 | }>; 17 | 18 | constructor(config: Config) { 19 | this.values = ValueMap.fromObject({ 20 | max: config.max, 21 | min: config.min, 22 | }); 23 | } 24 | 25 | public constrain(value: number): number { 26 | const max = this.values.get('max'); 27 | const min = this.values.get('min'); 28 | return Math.min(Math.max(value, min), max); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/list-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {ListConstraint} from './list.js'; 5 | 6 | describe(ListConstraint.name, () => { 7 | it('should get list options', () => { 8 | const options = [ 9 | {text: 'foo', value: 1.41}, 10 | {text: 'bar', value: 2.72}, 11 | {text: 'baz', value: 3.14}, 12 | ]; 13 | const c = new ListConstraint(options); 14 | assert.deepStrictEqual(options, c.values.get('options')); 15 | }); 16 | 17 | it('should constrain value with list options', () => { 18 | const c = new ListConstraint([ 19 | {text: 'foo', value: 1.41}, 20 | {text: 'bar', value: 2.72}, 21 | {text: 'baz', value: 3.14}, 22 | ]); 23 | assert.strictEqual(c.constrain(2.72), 2.72); 24 | }); 25 | 26 | it('should not constrain value without list options', () => { 27 | const c = new ListConstraint([]); 28 | assert.strictEqual(c.constrain(3.14), 3.14); 29 | }); 30 | 31 | it('should constrain an invalid value with list options', () => { 32 | const c = new ListConstraint([ 33 | {text: 'foo', value: 1.41}, 34 | {text: 'bar', value: 2.72}, 35 | {text: 'baz', value: 3.14}, 36 | ]); 37 | assert.strictEqual(c.constrain(9.81), 1.41); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/list.ts: -------------------------------------------------------------------------------- 1 | import {ValueMap} from '../model/value-map.js'; 2 | import {Constraint} from './constraint.js'; 3 | 4 | export interface ListItem { 5 | text: string; 6 | value: T; 7 | } 8 | 9 | /** 10 | * A list constranit. 11 | * @template T The type of the value. 12 | */ 13 | export class ListConstraint implements Constraint { 14 | public readonly values: ValueMap<{ 15 | options: ListItem[]; 16 | }>; 17 | 18 | constructor(options: ListItem[]) { 19 | this.values = ValueMap.fromObject({ 20 | options: options, 21 | }); 22 | } 23 | 24 | public constrain(value: T): T { 25 | const opts = this.values.get('options'); 26 | 27 | if (opts.length === 0) { 28 | return value; 29 | } 30 | 31 | const matched = 32 | opts.filter((item) => { 33 | return item.value === value; 34 | }).length > 0; 35 | 36 | return matched ? value : opts[0].value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/range-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {RangeConstraint} from './range.js'; 5 | 6 | describe(RangeConstraint.name, () => { 7 | it('should constrain value with minimum value', () => { 8 | const c = new RangeConstraint({ 9 | min: -10, 10 | }); 11 | assert.strictEqual(c.values.get('min'), -10); 12 | assert.strictEqual(c.constrain(-11), -10); 13 | assert.strictEqual(c.constrain(-10), -10); 14 | assert.strictEqual(c.constrain(-9), -9); 15 | }); 16 | 17 | it('should constrain value with maximum value', () => { 18 | const c = new RangeConstraint({ 19 | max: 123, 20 | }); 21 | assert.strictEqual(c.values.get('max'), 123); 22 | assert.strictEqual(c.constrain(122), 122); 23 | assert.strictEqual(c.constrain(123), 123); 24 | assert.strictEqual(c.constrain(123.5), 123); 25 | }); 26 | 27 | it('should constrain value with minimun and maximum value', () => { 28 | const c = new RangeConstraint({ 29 | max: 123, 30 | min: -123, 31 | }); 32 | assert.strictEqual(c.constrain(-124), -123); 33 | assert.strictEqual(c.constrain(0), 0); 34 | assert.strictEqual(c.constrain(124), 123); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/range.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty} from '../../misc/type-util.js'; 2 | import {ValueMap} from '../model/value-map.js'; 3 | import {Constraint} from './constraint.js'; 4 | 5 | interface Config { 6 | max?: number; 7 | min?: number; 8 | } 9 | 10 | /** 11 | * A number range constraint. 12 | */ 13 | export class RangeConstraint implements Constraint { 14 | public readonly values: ValueMap<{ 15 | max: number | undefined; 16 | min: number | undefined; 17 | }>; 18 | 19 | constructor(config: Config) { 20 | this.values = ValueMap.fromObject({ 21 | max: config.max, 22 | min: config.min, 23 | }); 24 | } 25 | 26 | public constrain(value: number): number { 27 | const max = this.values.get('max'); 28 | const min = this.values.get('min'); 29 | 30 | let result = value; 31 | if (!isEmpty(min)) { 32 | result = Math.max(result, min); 33 | } 34 | if (!isEmpty(max)) { 35 | result = Math.min(result, max); 36 | } 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/common/constraint/step.ts: -------------------------------------------------------------------------------- 1 | import {Constraint} from './constraint.js'; 2 | 3 | /** 4 | * A number step range constraint. 5 | */ 6 | export class StepConstraint implements Constraint { 7 | public readonly step: number; 8 | public readonly origin: number; 9 | 10 | constructor(step: number, origin = 0) { 11 | this.step = step; 12 | this.origin = origin; 13 | } 14 | 15 | public constrain(value: number): number { 16 | const o = this.origin % this.step; 17 | const r = Math.round((value - o) / this.step); 18 | return o + r * this.step; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/common/controller/controller.ts: -------------------------------------------------------------------------------- 1 | import {ViewProps} from '../model/view-props.js'; 2 | import {View} from '../view/view.js'; 3 | 4 | /** 5 | * A controller that has a view to control. 6 | */ 7 | export interface Controller { 8 | readonly view: V; 9 | readonly viewProps: ViewProps; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/common/controller/popup.ts: -------------------------------------------------------------------------------- 1 | import {Value} from '../model/value.js'; 2 | import {createValue} from '../model/values.js'; 3 | import {ViewProps} from '../model/view-props.js'; 4 | import {PopupView} from '../view/popup.js'; 5 | import {Controller} from './controller.js'; 6 | 7 | interface Config { 8 | viewProps: ViewProps; 9 | } 10 | 11 | export class PopupController implements Controller { 12 | public readonly shows: Value = createValue(false); 13 | public readonly view: PopupView; 14 | public readonly viewProps: ViewProps; 15 | 16 | constructor(doc: Document, config: Config) { 17 | this.viewProps = config.viewProps; 18 | this.view = new PopupView(doc, { 19 | shows: this.shows, 20 | viewProps: this.viewProps, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/common/controller/text.ts: -------------------------------------------------------------------------------- 1 | import {forceCast, isEmpty} from '../../misc/type-util.js'; 2 | import {Parser} from '../converter/parser.js'; 3 | import {Value} from '../model/value.js'; 4 | import {ViewProps} from '../model/view-props.js'; 5 | import {TextProps, TextView} from '../view/text.js'; 6 | import {ValueController} from './value.js'; 7 | 8 | /** 9 | * @hidden 10 | */ 11 | export interface Config { 12 | props: TextProps; 13 | parser: Parser; 14 | value: Value; 15 | viewProps: ViewProps; 16 | } 17 | 18 | /** 19 | * @hidden 20 | */ 21 | export class TextController implements ValueController> { 22 | public readonly props: TextProps; 23 | public readonly value: Value; 24 | public readonly view: TextView; 25 | public readonly viewProps: ViewProps; 26 | private readonly parser_: Parser; 27 | 28 | constructor(doc: Document, config: Config) { 29 | this.onInputChange_ = this.onInputChange_.bind(this); 30 | 31 | this.parser_ = config.parser; 32 | this.props = config.props; 33 | this.value = config.value; 34 | this.viewProps = config.viewProps; 35 | 36 | this.view = new TextView(doc, { 37 | props: config.props, 38 | value: this.value, 39 | viewProps: this.viewProps, 40 | }); 41 | this.view.inputElement.addEventListener('change', this.onInputChange_); 42 | } 43 | 44 | private onInputChange_(e: Event): void { 45 | const inputElem: HTMLInputElement = forceCast(e.currentTarget); 46 | const value = inputElem.value; 47 | 48 | const parsedValue = this.parser_(value); 49 | if (!isEmpty(parsedValue)) { 50 | this.value.rawValue = parsedValue; 51 | } 52 | this.view.refresh(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/common/controller/value.ts: -------------------------------------------------------------------------------- 1 | import {Value} from '../model/value.js'; 2 | import {View} from '../view/view.js'; 3 | import {Controller} from './controller.js'; 4 | 5 | export interface ValueController< 6 | T, 7 | Vw extends View = View, 8 | Va extends Value = Value, 9 | > extends Controller { 10 | readonly value: Va; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/boolean-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {boolFromUnknown, boolToString} from './boolean.js'; 5 | 6 | describe('booleanConverter', () => { 7 | it('should convert boolean to string', () => { 8 | assert.strictEqual(boolToString(true), 'true'); 9 | assert.strictEqual(boolToString(false), 'false'); 10 | }); 11 | 12 | [ 13 | { 14 | arg: true, 15 | expected: true, 16 | }, 17 | { 18 | arg: false, 19 | expected: false, 20 | }, 21 | { 22 | arg: 'false', 23 | expected: false, 24 | }, 25 | ].forEach((testCase) => { 26 | context(`when ${JSON.stringify(testCase.arg)}`, () => { 27 | it(`should convert to ${String(testCase.expected)}`, () => { 28 | assert.strictEqual(boolFromUnknown(testCase.arg), testCase.expected); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/boolean.ts: -------------------------------------------------------------------------------- 1 | export function boolToString(value: boolean): string { 2 | return String(value); 3 | } 4 | 5 | export function boolFromUnknown(value: unknown): boolean { 6 | if (value === 'false') { 7 | return false; 8 | } 9 | return !!value; 10 | } 11 | 12 | export function BooleanFormatter(value: boolean): string { 13 | return boolToString(value); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/formatter.ts: -------------------------------------------------------------------------------- 1 | export type Formatter = (value: T) => string; 2 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/number.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty} from '../../misc/type-util.js'; 2 | import {parseEcmaNumberExpression} from './ecma/parser.js'; 3 | import {Formatter} from './formatter.js'; 4 | 5 | export function parseNumber(text: string): number | null { 6 | const r = parseEcmaNumberExpression(text); 7 | return r?.evaluate() ?? null; 8 | } 9 | 10 | export function numberFromUnknown(value: unknown): number { 11 | if (typeof value === 'number') { 12 | return value; 13 | } 14 | 15 | if (typeof value === 'string') { 16 | const pv = parseNumber(value); 17 | if (!isEmpty(pv)) { 18 | return pv; 19 | } 20 | } 21 | 22 | return 0; 23 | } 24 | 25 | export function numberToString(value: number): string { 26 | return String(value); 27 | } 28 | 29 | export function createNumberFormatter(digits: number): Formatter { 30 | return (value: number): string => { 31 | // toFixed() of Safari doesn't support digits greater than 20 32 | // https://github.com/cocopon/tweakpane/pull/19 33 | return value.toFixed(Math.max(Math.min(digits, 20), 0)); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/parser-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {composeParsers} from './parser.js'; 5 | 6 | describe(composeParsers.name, () => { 7 | it('should use the first parser', () => { 8 | const p = composeParsers([ 9 | (t) => parseInt(t) * 10, 10 | (t) => parseInt(t) * 100, 11 | ]); 12 | assert.strictEqual(p('123'), 1230); 13 | }); 14 | 15 | it('should delegate a value to the next parser', () => { 16 | const p = composeParsers([(_) => null, (t) => parseInt(t) * 100]); 17 | assert.strictEqual(p('123'), 12300); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/parser.ts: -------------------------------------------------------------------------------- 1 | export type Parser = (text: string) => T | null; 2 | 3 | export function composeParsers(parsers: Parser[]): Parser { 4 | return (text) => { 5 | return parsers.reduce((result: T | null, parser) => { 6 | if (result !== null) { 7 | return result; 8 | } 9 | return parser(text); 10 | }, null); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/percentage-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {formatPercentage} from './percentage.js'; 5 | 6 | describe('converter/percentage', () => { 7 | [ 8 | { 9 | expected: '0%', 10 | params: { 11 | value: 0, 12 | }, 13 | }, 14 | { 15 | expected: '12%', 16 | params: { 17 | value: 12, 18 | }, 19 | }, 20 | { 21 | expected: '100%', 22 | params: { 23 | value: 100, 24 | }, 25 | }, 26 | ].forEach((testCase) => { 27 | context(`when ${JSON.stringify(testCase.params)}`, () => { 28 | it(`it should format to ${JSON.stringify(testCase.expected)}`, () => { 29 | assert.strictEqual( 30 | formatPercentage(testCase.params.value), 31 | testCase.expected, 32 | ); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/percentage.ts: -------------------------------------------------------------------------------- 1 | import {createNumberFormatter} from './number.js'; 2 | 3 | const innerFormatter = createNumberFormatter(0); 4 | 5 | export function formatPercentage(value: number): string { 6 | return innerFormatter(value) + '%'; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/common/converter/string.ts: -------------------------------------------------------------------------------- 1 | export function stringFromUnknown(value: unknown): string { 2 | return String(value); 3 | } 4 | 5 | export function formatString(value: string): string { 6 | return value; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/common/dom-util-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {createTestWindow} from '../misc/dom-test-util.js'; 5 | import {indexOfChildElement, removeElement} from './dom-util.js'; 6 | 7 | describe('DomUtil', () => { 8 | it('should get index of child element', () => { 9 | const w = createTestWindow(); 10 | const parent = w.document.createElement('div'); 11 | const child = w.document.createElement('div'); 12 | parent.appendChild(child); 13 | 14 | removeElement(child); 15 | assert.strictEqual(child.parentElement, null); 16 | }); 17 | it('should get index of child element', () => { 18 | const w = createTestWindow(); 19 | const parent = w.document.createElement('div'); 20 | parent.appendChild(w.document.createElement('div')); 21 | parent.appendChild(w.document.createElement('div')); 22 | const child = w.document.createElement('div'); 23 | parent.appendChild(child); 24 | parent.appendChild(w.document.createElement('div')); 25 | 26 | assert.strictEqual(indexOfChildElement(child), 2); 27 | }); 28 | 29 | it('should return negative index if not found', () => { 30 | const w = createTestWindow(); 31 | const parent = w.document.createElement('div'); 32 | parent.appendChild(w.document.createElement('div')); 33 | parent.appendChild(w.document.createElement('div')); 34 | 35 | const elem = w.document.createElement('div'); 36 | assert.strictEqual(indexOfChildElement(elem), -1); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/common/model/buffered-value-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe as context, describe, it} from 'mocha'; 3 | 4 | import {createPushedBuffer, initializeBuffer} from './buffered-value.js'; 5 | 6 | describe(initializeBuffer.name, () => { 7 | [ 8 | { 9 | expected: [undefined], 10 | params: { 11 | bufferSize: 1, 12 | }, 13 | }, 14 | { 15 | expected: [undefined, undefined, undefined, undefined], 16 | params: { 17 | bufferSize: 4, 18 | }, 19 | }, 20 | ].forEach(({expected, params}) => { 21 | context(`when ${JSON.stringify(params)}`, () => { 22 | it('should initialize buffer', () => { 23 | assert.deepStrictEqual(initializeBuffer(params.bufferSize), expected); 24 | }); 25 | }); 26 | }); 27 | }); 28 | 29 | describe(createPushedBuffer.name, () => { 30 | [ 31 | { 32 | expected: [0, 1, undefined], 33 | params: { 34 | buffer: [0, undefined, undefined], 35 | newValue: 1, 36 | }, 37 | }, 38 | { 39 | expected: [1, 2, 3, 4], 40 | params: { 41 | buffer: [0, 1, 2, 3], 42 | newValue: 4, 43 | }, 44 | }, 45 | ].forEach(({expected, params}) => { 46 | context(`when ${JSON.stringify(params)}`, () => { 47 | it('should create pushed buffer', () => { 48 | assert.deepStrictEqual( 49 | createPushedBuffer(params.buffer, params.newValue), 50 | expected, 51 | ); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/core/src/common/model/buffered-value.ts: -------------------------------------------------------------------------------- 1 | import {forceCast} from '../../misc/type-util.js'; 2 | import {Value, ValueEvents} from './value.js'; 3 | 4 | /** 5 | * A buffer. Prefixed to avoid conflicts with the Node.js built-in class. 6 | * @template T 7 | */ 8 | export type TpBuffer = (T | undefined)[]; 9 | export type BufferedValue = Value>; 10 | export type BufferedValueEvents = ValueEvents>; 11 | 12 | function fillBuffer(buffer: TpBuffer, bufferSize: number) { 13 | while (buffer.length < bufferSize) { 14 | buffer.push(undefined); 15 | } 16 | } 17 | 18 | export function initializeBuffer(bufferSize: number): TpBuffer { 19 | const buffer: TpBuffer = []; 20 | fillBuffer(buffer, bufferSize); 21 | return buffer; 22 | } 23 | 24 | function createTrimmedBuffer(buffer: TpBuffer): T[] { 25 | const index = buffer.indexOf(undefined); 26 | return forceCast(index < 0 ? buffer : buffer.slice(0, index)); 27 | } 28 | 29 | export function createPushedBuffer( 30 | buffer: TpBuffer, 31 | newValue: T, 32 | ): TpBuffer { 33 | const newBuffer = [...createTrimmedBuffer(buffer), newValue]; 34 | 35 | if (newBuffer.length > buffer.length) { 36 | newBuffer.splice(0, newBuffer.length - buffer.length); 37 | } else { 38 | fillBuffer(newBuffer, buffer.length); 39 | } 40 | 41 | return newBuffer; 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/common/model/primitive-value-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {PrimitiveValue} from './primitive-value.js'; 5 | 6 | describe(PrimitiveValue.name, () => { 7 | it('should get raw value', () => { 8 | const v = new PrimitiveValue(123); 9 | assert.strictEqual(v.rawValue, 123); 10 | }); 11 | 12 | it('should set raw value', () => { 13 | const v = new PrimitiveValue(0); 14 | v.rawValue = 456; 15 | assert.strictEqual(v.rawValue, 456); 16 | }); 17 | 18 | it('should emit change event', (done) => { 19 | const v = new PrimitiveValue(1); 20 | v.emitter.on('change', (ev) => { 21 | assert.strictEqual(v.rawValue, 2); 22 | assert.strictEqual(ev.previousRawValue, 1); 23 | assert.strictEqual(ev.rawValue, 2); 24 | assert.strictEqual(ev.sender, v); 25 | done(); 26 | }); 27 | v.rawValue = 2; 28 | }); 29 | 30 | it('should emit beforechange event', (done) => { 31 | const v = new PrimitiveValue(1); 32 | let count = 0; 33 | 34 | v.emitter.on('beforechange', (ev) => { 35 | assert.strictEqual(v.rawValue, 1); 36 | assert.strictEqual(ev.sender, v); 37 | assert.strictEqual(count, 0); 38 | count += 1; 39 | }); 40 | v.emitter.on('change', () => { 41 | assert.strictEqual(count, 1); 42 | done(); 43 | }); 44 | 45 | v.rawValue = 2; 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/core/src/common/model/primitive-value.ts: -------------------------------------------------------------------------------- 1 | import {Emitter} from './emitter.js'; 2 | import {Value, ValueChangeOptions, ValueEvents} from './value.js'; 3 | 4 | /** 5 | * A value that has a primitive raw value. 6 | * @template T the type of the raw value. 7 | */ 8 | export class PrimitiveValue implements Value { 9 | public readonly emitter: Emitter>; 10 | private value_: T; 11 | 12 | constructor(initialValue: T) { 13 | this.emitter = new Emitter(); 14 | this.value_ = initialValue; 15 | } 16 | 17 | get rawValue(): T { 18 | return this.value_; 19 | } 20 | 21 | set rawValue(value: T) { 22 | this.setRawValue(value, { 23 | forceEmit: false, 24 | last: true, 25 | }); 26 | } 27 | 28 | public setRawValue(value: T, options?: ValueChangeOptions): void { 29 | const opts = options ?? { 30 | forceEmit: false, 31 | last: true, 32 | }; 33 | 34 | const prevValue = this.value_; 35 | if (prevValue === value && !opts.forceEmit) { 36 | return; 37 | } 38 | 39 | this.emitter.emit('beforechange', { 40 | sender: this, 41 | }); 42 | 43 | this.value_ = value; 44 | 45 | this.emitter.emit('change', { 46 | options: opts, 47 | previousRawValue: prevValue, 48 | rawValue: this.value_, 49 | sender: this, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/common/model/reactive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReadonlyValue, 3 | ReadonlyValueEvents, 4 | Value, 5 | ValueEvents, 6 | } from '../model/value.js'; 7 | import {ValueMap} from '../model/value-map.js'; 8 | 9 | export function bindValue( 10 | value: Value | ReadonlyValue, 11 | applyValue: (value: T) => void, 12 | ) { 13 | value.emitter.on( 14 | 'change', 15 | (ev: ValueEvents['change'] | ReadonlyValueEvents['change']) => { 16 | applyValue(ev.rawValue); 17 | }, 18 | ); 19 | applyValue(value.rawValue); 20 | } 21 | 22 | export function bindValueMap< 23 | O extends Record, 24 | Key extends keyof O, 25 | >(valueMap: ValueMap, key: Key, applyValue: (value: O[Key]) => void) { 26 | bindValue(valueMap.value(key), applyValue); 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/common/model/readonly-primitive-value.ts: -------------------------------------------------------------------------------- 1 | import {Emitter} from './emitter.js'; 2 | import { 3 | ReadonlyValue, 4 | ReadonlyValueEvents, 5 | Value, 6 | ValueEvents, 7 | } from './value.js'; 8 | 9 | /** 10 | * @hidden 11 | */ 12 | export class ReadonlyPrimitiveValue implements ReadonlyValue { 13 | /** 14 | * The event emitter for value changes. 15 | */ 16 | public readonly emitter: Emitter> = new Emitter(); 17 | private value_: Value; 18 | 19 | constructor(value: Value) { 20 | this.onValueBeforeChange_ = this.onValueBeforeChange_.bind(this); 21 | this.onValueChange_ = this.onValueChange_.bind(this); 22 | 23 | this.value_ = value; 24 | this.value_.emitter.on('beforechange', this.onValueBeforeChange_); 25 | this.value_.emitter.on('change', this.onValueChange_); 26 | } 27 | 28 | /** 29 | * The raw value of the model. 30 | */ 31 | get rawValue(): T { 32 | return this.value_.rawValue; 33 | } 34 | 35 | private onValueBeforeChange_(ev: ValueEvents['beforechange']): void { 36 | this.emitter.emit('beforechange', { 37 | ...ev, 38 | sender: this, 39 | }); 40 | } 41 | 42 | private onValueChange_(ev: ValueEvents['change']): void { 43 | this.emitter.emit('change', { 44 | ...ev, 45 | sender: this, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/common/model/test-util.ts: -------------------------------------------------------------------------------- 1 | import {InputBindingValue} from '../binding/value/input-binding.js'; 2 | import {Value} from './value.js'; 3 | 4 | export function getBoundValue(v: InputBindingValue): Value { 5 | return (v as any).value_; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/common/model/value-map-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe} from 'mocha'; 3 | 4 | import {ValueMap} from './value-map.js'; 5 | 6 | describe(ValueMap.name, () => { 7 | it('should get initial value', () => { 8 | const m = ValueMap.fromObject({ 9 | foo: 'bar', 10 | baz: 'qux', 11 | }); 12 | assert.strictEqual(m.get('foo'), 'bar'); 13 | assert.strictEqual(m.get('baz'), 'qux'); 14 | }); 15 | 16 | it('should set value', () => { 17 | const m = ValueMap.fromObject({ 18 | foo: 'bar', 19 | }); 20 | m.set('foo', 'baz'); 21 | assert.strictEqual(m.get('foo'), 'baz'); 22 | }); 23 | 24 | it('should fire change event', (done) => { 25 | const m = ValueMap.fromObject({ 26 | foo: 'bar', 27 | baz: 'qux', 28 | }); 29 | 30 | m.emitter.on('change', (ev) => { 31 | assert.strictEqual(ev.key, 'baz'); 32 | assert.strictEqual(m.get('baz'), 'changed'); 33 | done(); 34 | }); 35 | m.set('baz', 'changed'); 36 | }); 37 | 38 | it('should not fire change event when setting the same value', () => { 39 | const m = ValueMap.fromObject({ 40 | foo: 'bar', 41 | }); 42 | 43 | m.emitter.on('change', () => { 44 | assert.fail('should not be called'); 45 | }); 46 | 47 | assert.doesNotThrow(() => { 48 | m.set('foo', 'bar'); 49 | }); 50 | }); 51 | 52 | it('should return signle value emitter', (done) => { 53 | const m = ValueMap.fromObject({ 54 | foo: 'bar', 55 | baz: 'qux', 56 | }); 57 | 58 | m.value('baz').emitter.on('change', (ev) => { 59 | assert.strictEqual(ev.rawValue, 'changed'); 60 | assert.strictEqual(m.get('baz'), 'changed'); 61 | done(); 62 | }); 63 | m.set('baz', 'changed'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/core/src/common/model/value-sync-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {Point2d} from '../../input-binding/point-2d/model/point-2d.js'; 5 | import {Constraint} from '../constraint/constraint.js'; 6 | import {connectValues} from './value-sync.js'; 7 | import {createValue} from './values.js'; 8 | 9 | describe(connectValues.name, () => { 10 | it('should set initial value', () => { 11 | const pv = createValue(new Point2d(1, 2)); 12 | const sv = createValue(0); 13 | connectValues({ 14 | primary: pv, 15 | secondary: sv, 16 | 17 | forward: (p) => { 18 | return p.x; 19 | }, 20 | backward: (p, s) => { 21 | const comps = p.getComponents(); 22 | comps[0] = s; 23 | return new Point2d(...comps); 24 | }, 25 | }); 26 | assert.strictEqual(sv.rawValue, 1); 27 | }); 28 | 29 | it('should apply constraint of primary value', () => { 30 | class TestConstraint implements Constraint { 31 | constrain(): Point2d { 32 | // Secondary value will be changed by constraint of primary value 33 | return new Point2d(-1, -1); 34 | } 35 | } 36 | 37 | const pv = createValue(new Point2d(1, 2), { 38 | constraint: new TestConstraint(), 39 | }); 40 | 41 | const sv = createValue(0); 42 | connectValues({ 43 | primary: pv, 44 | secondary: sv, 45 | 46 | forward: (p) => p.x, 47 | backward: (p, s) => { 48 | const comps = p.getComponents(); 49 | comps[0] = s; 50 | return new Point2d(...comps); 51 | }, 52 | }); 53 | 54 | // Try to change secondary value 55 | sv.rawValue = 10; 56 | // ...and it should be updated by primary constraint 57 | 58 | assert.strictEqual(sv.rawValue, -1); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/core/src/common/model/value-sync.ts: -------------------------------------------------------------------------------- 1 | import {Value} from './value.js'; 2 | 3 | /** 4 | * Synchronizes two values. 5 | */ 6 | export function connectValues({ 7 | primary, 8 | secondary, 9 | forward, 10 | backward, 11 | }: { 12 | primary: Value; 13 | secondary: Value; 14 | forward: (primary: T1, secondary: T2) => T2; 15 | backward: (primary: T1, secondary: T2) => T1; 16 | }) { 17 | // Prevents an event firing loop 18 | // e.g. 19 | // primary changed 20 | // -> applies changes to secondary 21 | // -> secondary changed 22 | // -> applies changes to primary 23 | // -> ... 24 | let changing = false; 25 | function preventFeedback(callback: () => void) { 26 | if (changing) { 27 | return; 28 | } 29 | changing = true; 30 | callback(); 31 | changing = false; 32 | } 33 | 34 | primary.emitter.on('change', (ev) => { 35 | preventFeedback(() => { 36 | secondary.setRawValue( 37 | forward(primary.rawValue, secondary.rawValue), 38 | ev.options, 39 | ); 40 | }); 41 | }); 42 | secondary.emitter.on('change', (ev) => { 43 | preventFeedback(() => { 44 | primary.setRawValue( 45 | backward(primary.rawValue, secondary.rawValue), 46 | ev.options, 47 | ); 48 | }); 49 | 50 | // Re-update secondary value 51 | // to apply change from constraint of primary value 52 | preventFeedback(() => { 53 | secondary.setRawValue( 54 | forward(primary.rawValue, secondary.rawValue), 55 | ev.options, 56 | ); 57 | }); 58 | }); 59 | 60 | preventFeedback(() => { 61 | secondary.setRawValue(forward(primary.rawValue, secondary.rawValue), { 62 | forceEmit: false, 63 | last: true, 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /packages/core/src/common/model/value.ts: -------------------------------------------------------------------------------- 1 | import {Emitter} from './emitter.js'; 2 | 3 | export interface ValueChangeOptions { 4 | /** 5 | * The flag indicating whether an event should be fired even if the value doesn't change. 6 | */ 7 | forceEmit: boolean; 8 | /** 9 | * The flag indicating whether the event is for the last change. 10 | */ 11 | last: boolean; 12 | } 13 | 14 | export interface ValueEvents> { 15 | beforechange: { 16 | sender: V; 17 | }; 18 | change: { 19 | options: ValueChangeOptions; 20 | previousRawValue: T; 21 | rawValue: T; 22 | sender: V; 23 | }; 24 | } 25 | 26 | /** 27 | * A value that handles changes. 28 | * @template T The type of the raw value. 29 | */ 30 | export interface Value { 31 | /** 32 | * The event emitter for value changes. 33 | */ 34 | readonly emitter: Emitter>; 35 | /** 36 | * The raw value of the model. 37 | */ 38 | rawValue: T; 39 | 40 | setRawValue(rawValue: T, options?: ValueChangeOptions): void; 41 | } 42 | 43 | export type ReadonlyValueEvents = ValueEvents>; 44 | 45 | /** 46 | * A readonly value that can be changed elsewhere. 47 | * @template T The type of the raw value. 48 | */ 49 | export interface ReadonlyValue { 50 | /** 51 | * The event emitter for value changes. 52 | */ 53 | readonly emitter: Emitter>; 54 | /** 55 | * The raw value of the model. 56 | */ 57 | readonly rawValue: T; 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/src/common/model/values-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {createReadonlyValue, createValue} from './values.js'; 5 | 6 | describe(createReadonlyValue.name, () => { 7 | it('should get raw value', () => { 8 | const v = createValue(123); 9 | const [rv] = createReadonlyValue(v); 10 | assert.strictEqual(rv.rawValue, v.rawValue); 11 | 12 | v.rawValue = 456; 13 | assert.strictEqual(rv.rawValue, v.rawValue); 14 | }); 15 | 16 | it('should set raw value', () => { 17 | const v = createValue(123); 18 | const [rv, setRawValue] = createReadonlyValue(v); 19 | assert.strictEqual(rv.rawValue, v.rawValue); 20 | 21 | setRawValue(456); 22 | assert.strictEqual(rv.rawValue, v.rawValue); 23 | }); 24 | 25 | it('should emit beforechange event', (done) => { 26 | const v = createValue(123); 27 | const [rv, setRawValue] = createReadonlyValue(v); 28 | 29 | rv.emitter.on('beforechange', (ev) => { 30 | assert.strictEqual(ev.sender, rv); 31 | done(); 32 | }); 33 | setRawValue(456); 34 | }); 35 | 36 | it('should emit change event', (done) => { 37 | const v = createValue(123); 38 | const [rv, setRawValue] = createReadonlyValue(v); 39 | 40 | rv.emitter.on('change', (ev) => { 41 | assert.strictEqual(ev.previousRawValue, 123); 42 | assert.strictEqual(ev.rawValue, 456); 43 | assert.strictEqual(ev.sender, rv); 44 | done(); 45 | }); 46 | setRawValue(456); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/core/src/common/model/values.ts: -------------------------------------------------------------------------------- 1 | import {Constraint} from '../constraint/constraint.js'; 2 | import {ComplexValue} from './complex-value.js'; 3 | import {PrimitiveValue} from './primitive-value.js'; 4 | import {ReadonlyPrimitiveValue} from './readonly-primitive-value.js'; 5 | import {ReadonlyValue, Value, ValueChangeOptions} from './value.js'; 6 | 7 | interface Config { 8 | constraint?: Constraint; 9 | equals?: (v1: T, v2: T) => boolean; 10 | } 11 | 12 | export function createValue(initialValue: T, config?: Config): Value { 13 | const constraint = config?.constraint; 14 | const equals = config?.equals; 15 | if (!constraint && !equals) { 16 | return new PrimitiveValue(initialValue); 17 | } 18 | return new ComplexValue(initialValue, config); 19 | } 20 | 21 | export type SetRawValue = ( 22 | rawValue: T, 23 | options?: ValueChangeOptions | undefined, 24 | ) => void; 25 | 26 | export function createReadonlyValue( 27 | value: Value, 28 | ): [ReadonlyValue, SetRawValue] { 29 | return [ 30 | new ReadonlyPrimitiveValue(value), 31 | (rawValue: T, options: ValueChangeOptions | undefined) => { 32 | value.setRawValue(rawValue, options); 33 | }, 34 | ]; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/common/number/view/slider-text.ts: -------------------------------------------------------------------------------- 1 | import {ClassName} from '../../../common/view/class-name.js'; 2 | import {View} from '../../../common/view/view.js'; 3 | import {NumberTextView} from './number-text.js'; 4 | import {SliderView} from './slider.js'; 5 | 6 | /** 7 | * @hidden 8 | */ 9 | interface Config { 10 | sliderView: SliderView; 11 | textView: NumberTextView; 12 | } 13 | 14 | const cn = ClassName('sldtxt'); 15 | 16 | /** 17 | * @hidden 18 | */ 19 | export class SliderTextView implements View { 20 | public readonly element: HTMLElement; 21 | private readonly sliderView_: SliderView; 22 | private readonly textView_: NumberTextView; 23 | 24 | constructor(doc: Document, config: Config) { 25 | this.element = doc.createElement('div'); 26 | this.element.classList.add(cn()); 27 | 28 | const sliderElem = doc.createElement('div'); 29 | sliderElem.classList.add(cn('s')); 30 | this.sliderView_ = config.sliderView; 31 | sliderElem.appendChild(this.sliderView_.element); 32 | this.element.appendChild(sliderElem); 33 | 34 | const textElem = doc.createElement('div'); 35 | textElem.classList.add(cn('t')); 36 | this.textView_ = config.textView; 37 | textElem.appendChild(this.textView_.element); 38 | this.element.appendChild(textElem); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/common/params.ts: -------------------------------------------------------------------------------- 1 | import {Formatter} from './converter/formatter.js'; 2 | 3 | export interface BaseParams { 4 | disabled?: boolean; 5 | hidden?: boolean; 6 | index?: number; 7 | } 8 | 9 | export type ArrayStyleListOptions = {text: string; value: T}[]; 10 | export type ObjectStyleListOptions = {[text: string]: T}; 11 | export type ListParamsOptions = 12 | | ArrayStyleListOptions 13 | | ObjectStyleListOptions; 14 | 15 | export type PickerLayout = 'inline' | 'popup'; 16 | 17 | interface BindingParams extends BaseParams { 18 | label?: string; 19 | tag?: string | undefined; 20 | view?: string; 21 | } 22 | 23 | export interface BaseInputParams 24 | extends BindingParams, 25 | Record { 26 | readonly?: false; 27 | } 28 | 29 | export interface BaseMonitorParams 30 | extends BindingParams, 31 | Record { 32 | bufferSize?: number; 33 | interval?: number; 34 | readonly: true; 35 | } 36 | 37 | export interface BaseBladeParams extends BaseParams, Record {} 38 | 39 | export interface NumberTextInputParams { 40 | format?: Formatter; 41 | /** 42 | * The unit scale for key input. 43 | */ 44 | keyScale?: number; 45 | max?: number; 46 | min?: number; 47 | /** 48 | * The unit scale for pointer input. 49 | */ 50 | pointerScale?: number; 51 | step?: number; 52 | } 53 | 54 | export type PointDimensionParams = NumberTextInputParams; 55 | -------------------------------------------------------------------------------- /packages/core/src/common/picker-util.ts: -------------------------------------------------------------------------------- 1 | import {PickerLayout} from './params.js'; 2 | 3 | export function parsePickerLayout(value: unknown): PickerLayout | undefined { 4 | if (value === 'inline' || value === 'popup') { 5 | return value; 6 | } 7 | return undefined; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/common/point-nd/point-axis.ts: -------------------------------------------------------------------------------- 1 | import {Constraint} from '../constraint/constraint.js'; 2 | import {ValueMap} from '../model/value-map.js'; 3 | import {createNumberTextPropsObject} from '../number/util.js'; 4 | import {NumberTextProps} from '../number/view/number-text.js'; 5 | import {PointDimensionParams} from '../params.js'; 6 | 7 | export interface PointAxis { 8 | constraint: Constraint | undefined; 9 | textProps: NumberTextProps; 10 | } 11 | 12 | export function createPointAxis(config: { 13 | constraint: Constraint | undefined; 14 | initialValue: number; 15 | params: PointDimensionParams; 16 | }): PointAxis { 17 | return { 18 | constraint: config.constraint, 19 | textProps: ValueMap.fromObject( 20 | createNumberTextPropsObject(config.params, config.initialValue), 21 | ), 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/common/point-nd/test-util.ts: -------------------------------------------------------------------------------- 1 | import {findConstraint} from '../constraint/composite.js'; 2 | import {Constraint} from '../constraint/constraint.js'; 3 | import {DefiniteRangeConstraint} from '../constraint/definite-range.js'; 4 | import {RangeConstraint} from '../constraint/range.js'; 5 | import {StepConstraint} from '../constraint/step.js'; 6 | 7 | /** 8 | * Finds a range from number constraint. 9 | * @param c The number constraint. 10 | * @return A list that contains a minimum value and a max value. 11 | */ 12 | function findNumberRange( 13 | c: Constraint, 14 | ): [number | undefined, number | undefined] { 15 | const drc = findConstraint(c, DefiniteRangeConstraint); 16 | if (drc) { 17 | return [drc.values.get('min'), drc.values.get('max')]; 18 | } 19 | const rc = findConstraint(c, RangeConstraint); 20 | if (rc) { 21 | return [rc.values.get('min'), rc.values.get('max')]; 22 | } 23 | return [undefined, undefined]; 24 | } 25 | 26 | export function getDimensionProps(c: Constraint) { 27 | const [min, max] = findNumberRange(c); 28 | const sc = findConstraint(c, StepConstraint); 29 | return { 30 | max: max, 31 | min: min, 32 | step: sc?.step, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/common/point-nd/util.ts: -------------------------------------------------------------------------------- 1 | import {isRecord} from '../../misc/type-util.js'; 2 | import {CompositeConstraint} from '../constraint/composite.js'; 3 | import {Constraint} from '../constraint/constraint.js'; 4 | import {MicroParsers, parseRecord} from '../micro-parsers.js'; 5 | import { 6 | createNumberTextInputParamsParser, 7 | createRangeConstraint, 8 | createStepConstraint, 9 | } from '../number/util.js'; 10 | import {PointDimensionParams} from '../params.js'; 11 | 12 | export function createPointDimensionParser(p: typeof MicroParsers) { 13 | return createNumberTextInputParamsParser(p); 14 | } 15 | 16 | export function parsePointDimensionParams( 17 | value: unknown, 18 | ): PointDimensionParams | undefined { 19 | if (!isRecord(value)) { 20 | return undefined; 21 | } 22 | return parseRecord(value, createPointDimensionParser); 23 | } 24 | 25 | export function createDimensionConstraint( 26 | params: PointDimensionParams | undefined, 27 | initialValue: number, 28 | ): Constraint | undefined { 29 | if (!params) { 30 | return undefined; 31 | } 32 | 33 | const constraints: Constraint[] = []; 34 | const cs = createStepConstraint(params, initialValue); 35 | if (cs) { 36 | constraints.push(cs); 37 | } 38 | const rs = createRangeConstraint(params); 39 | if (rs) { 40 | constraints.push(rs); 41 | } 42 | return new CompositeConstraint(constraints); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/common/primitive.ts: -------------------------------------------------------------------------------- 1 | import {BindingTarget} from './binding/target.js'; 2 | 3 | /** 4 | * The union of primitive types. 5 | */ 6 | export type Primitive = boolean | number | string; 7 | 8 | /** 9 | * Writes the primitive value. 10 | * @param target The target to be written. 11 | * @param value The value to write. 12 | */ 13 | export function writePrimitive( 14 | target: BindingTarget, 15 | value: T, 16 | ): void { 17 | target.write(value); 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/common/tp-error-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {TpError} from './tp-error.js'; 5 | 6 | describe(TpError.name, () => { 7 | it('should instanciate for invalid parameters', () => { 8 | const e = new TpError({ 9 | context: { 10 | name: 'foo', 11 | }, 12 | type: 'invalidparams', 13 | }); 14 | 15 | assert.strictEqual(e.type, 'invalidparams'); 16 | }); 17 | 18 | it('should use message for toString()', () => { 19 | const e = TpError.shouldNeverHappen(); 20 | assert.strictEqual(e.message, e.toString()); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/src/common/ui.ts: -------------------------------------------------------------------------------- 1 | interface StepKeys { 2 | altKey: boolean; 3 | downKey: boolean; 4 | shiftKey: boolean; 5 | upKey: boolean; 6 | } 7 | 8 | export function getStepForKey(keyScale: number, keys: StepKeys): number { 9 | const step = keyScale * (keys.altKey ? 0.1 : 1) * (keys.shiftKey ? 10 : 1); 10 | 11 | if (keys.upKey) { 12 | return +step; 13 | } else if (keys.downKey) { 14 | return -step; 15 | } 16 | return 0; 17 | } 18 | 19 | export function getVerticalStepKeys(ev: KeyboardEvent): StepKeys { 20 | return { 21 | altKey: ev.altKey, 22 | downKey: ev.key === 'ArrowDown', 23 | shiftKey: ev.shiftKey, 24 | upKey: ev.key === 'ArrowUp', 25 | }; 26 | } 27 | 28 | export function getHorizontalStepKeys(ev: KeyboardEvent): StepKeys { 29 | return { 30 | altKey: ev.altKey, 31 | downKey: ev.key === 'ArrowLeft', 32 | shiftKey: ev.shiftKey, 33 | upKey: ev.key === 'ArrowRight', 34 | }; 35 | } 36 | 37 | export function isVerticalArrowKey(key: string): boolean { 38 | return key === 'ArrowUp' || key === 'ArrowDown'; 39 | } 40 | 41 | export function isArrowKey(key: string): boolean { 42 | return isVerticalArrowKey(key) || key === 'ArrowLeft' || key === 'ArrowRight'; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/common/view/class-name.ts: -------------------------------------------------------------------------------- 1 | const PREFIX = 'tp'; 2 | 3 | /** 4 | * A utility function for generating BEM-like class name. 5 | * @param viewName The name of the view. Used as part of the block name. 6 | * @return A class name generator function. 7 | */ 8 | export function ClassName(viewName: string) { 9 | /** 10 | * Generates a class name. 11 | * @param [opt_elementName] The name of the element. 12 | * @param [opt_modifier] The name of the modifier. 13 | * @return A class name. 14 | */ 15 | const fn = (opt_elementName?: string, opt_modifier?: string): string => { 16 | return [ 17 | PREFIX, 18 | '-', 19 | viewName, 20 | 'v', 21 | opt_elementName ? `_${opt_elementName}` : '', 22 | opt_modifier ? `-${opt_modifier}` : '', 23 | ].join(''); 24 | }; 25 | return fn; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/common/view/css-vars.ts: -------------------------------------------------------------------------------- 1 | const CSS_VAR_MAP = { 2 | containerUnitSize: 'cnt-usz', 3 | }; 4 | 5 | /** 6 | * Gets a name of the internal CSS variable. 7 | * @param key The key for the CSS variable. 8 | * @return A name of the internal CSS variable. 9 | */ 10 | export function getCssVar(key: keyof typeof CSS_VAR_MAP): string { 11 | return `--${CSS_VAR_MAP[key]}`; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/common/view/plain.ts: -------------------------------------------------------------------------------- 1 | import {ViewProps} from '../model/view-props.js'; 2 | import {ClassName} from './class-name.js'; 3 | import {View} from './view.js'; 4 | 5 | /** 6 | * @hidden 7 | */ 8 | interface Config { 9 | viewName: string; 10 | viewProps: ViewProps; 11 | } 12 | 13 | /** 14 | * @hidden 15 | */ 16 | export class PlainView implements View { 17 | public readonly element: HTMLElement; 18 | 19 | /** 20 | * @hidden 21 | */ 22 | constructor(doc: Document, config: Config) { 23 | const cn = ClassName(config.viewName); 24 | this.element = doc.createElement('div'); 25 | this.element.classList.add(cn()); 26 | config.viewProps.bindClassModifiers(this.element); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/common/view/popup.ts: -------------------------------------------------------------------------------- 1 | import {bindValue} from '../model/reactive.js'; 2 | import {Value} from '../model/value.js'; 3 | import {ViewProps} from '../model/view-props.js'; 4 | import {ClassName} from './class-name.js'; 5 | import {valueToClassName} from './reactive.js'; 6 | import {View} from './view.js'; 7 | 8 | interface Config { 9 | shows: Value; 10 | viewProps: ViewProps; 11 | } 12 | 13 | const cn = ClassName('pop'); 14 | 15 | /** 16 | * @hidden 17 | */ 18 | export class PopupView implements View { 19 | public readonly element: HTMLElement; 20 | 21 | constructor(doc: Document, config: Config) { 22 | this.element = doc.createElement('div'); 23 | this.element.classList.add(cn()); 24 | config.viewProps.bindClassModifiers(this.element); 25 | bindValue(config.shows, valueToClassName(this.element, cn(undefined, 'v'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/common/view/reactive.ts: -------------------------------------------------------------------------------- 1 | import {bindValue} from '../model/reactive.js'; 2 | import {Value} from '../model/value.js'; 3 | 4 | function applyClass(elem: HTMLElement, className: string, active: boolean) { 5 | if (active) { 6 | elem.classList.add(className); 7 | } else { 8 | elem.classList.remove(className); 9 | } 10 | } 11 | 12 | export function valueToClassName( 13 | elem: HTMLElement, 14 | className: string, 15 | ): (value: boolean) => void { 16 | return (value) => { 17 | applyClass(elem, className, value); 18 | }; 19 | } 20 | 21 | export function bindValueToTextContent( 22 | value: Value, 23 | elem: HTMLElement, 24 | ) { 25 | bindValue(value, (text) => { 26 | elem.textContent = text ?? ''; 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/common/view/view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A view interface. 3 | */ 4 | export interface View { 5 | /** 6 | * A root element of the view. 7 | */ 8 | readonly element: HTMLElement; 9 | } 10 | 11 | /** 12 | * @hidden 13 | */ 14 | export interface InputView extends View { 15 | readonly inputElement: HTMLInputElement; 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/controller/color-swatch.ts: -------------------------------------------------------------------------------- 1 | import {ValueController} from '../../../common/controller/value.js'; 2 | import {Value} from '../../../common/model/value.js'; 3 | import {ViewProps} from '../../../common/model/view-props.js'; 4 | import {IntColor} from '../model/int-color.js'; 5 | import {ColorSwatchView} from '../view/color-swatch.js'; 6 | 7 | interface Config { 8 | value: Value; 9 | viewProps: ViewProps; 10 | } 11 | 12 | /** 13 | * @hidden 14 | */ 15 | export class ColorSwatchController 16 | implements ValueController 17 | { 18 | public readonly value: Value; 19 | public readonly view: ColorSwatchView; 20 | public readonly viewProps: ViewProps; 21 | 22 | constructor(doc: Document, config: Config) { 23 | this.value = config.value; 24 | this.viewProps = config.viewProps; 25 | 26 | this.view = new ColorSwatchView(doc, { 27 | value: this.value, 28 | viewProps: this.viewProps, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/converter/color-number.ts: -------------------------------------------------------------------------------- 1 | import {mapRange} from '../../../common/number/util.js'; 2 | import {removeAlphaComponent} from '../model/color-model.js'; 3 | import {IntColor} from '../model/int-color.js'; 4 | 5 | export function colorToRgbNumber(value: IntColor): number { 6 | return removeAlphaComponent(value.getComponents('rgb')).reduce( 7 | (result, comp) => { 8 | return (result << 8) | (Math.floor(comp) & 0xff); 9 | }, 10 | 0, 11 | ); 12 | } 13 | 14 | export function colorToRgbaNumber(value: IntColor): number { 15 | return ( 16 | value.getComponents('rgb').reduce((result, comp, index) => { 17 | const hex = Math.floor(index === 3 ? comp * 255 : comp) & 0xff; 18 | return (result << 8) | hex; 19 | }, 0) >>> 0 20 | ); 21 | } 22 | 23 | export function numberToRgbColor(num: number): IntColor { 24 | return new IntColor( 25 | [(num >> 16) & 0xff, (num >> 8) & 0xff, num & 0xff], 26 | 'rgb', 27 | ); 28 | } 29 | 30 | export function numberToRgbaColor(num: number): IntColor { 31 | return new IntColor( 32 | [ 33 | (num >> 24) & 0xff, 34 | (num >> 16) & 0xff, 35 | (num >> 8) & 0xff, 36 | mapRange(num & 0xff, 0, 255, 0, 1), 37 | ], 38 | 'rgb', 39 | ); 40 | } 41 | 42 | export function colorFromRgbNumber(value: unknown): IntColor { 43 | if (typeof value !== 'number') { 44 | return IntColor.black(); 45 | } 46 | return numberToRgbColor(value); 47 | } 48 | 49 | export function colorFromRgbaNumber(value: unknown): IntColor { 50 | if (typeof value !== 'number') { 51 | return IntColor.black(); 52 | } 53 | return numberToRgbaColor(value); 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/converter/color-object.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Color, 3 | createColorComponentsFromRgbObject, 4 | isColorObject, 5 | } from '../model/color.js'; 6 | import {ColorType} from '../model/color-model.js'; 7 | import {mapColorType} from '../model/colors.js'; 8 | import {FloatColor} from '../model/float-color.js'; 9 | import {IntColor} from '../model/int-color.js'; 10 | 11 | export function colorFromObject(value: unknown, type: 'int'): IntColor; 12 | export function colorFromObject(value: unknown, type: 'float'): FloatColor; 13 | export function colorFromObject(value: unknown, type: ColorType): Color; 14 | export function colorFromObject(value: unknown, type: ColorType): Color { 15 | if (!isColorObject(value)) { 16 | return mapColorType(IntColor.black(), type); 17 | } 18 | if (type === 'int') { 19 | const comps = createColorComponentsFromRgbObject(value); 20 | return new IntColor(comps, 'rgb'); 21 | } 22 | if (type === 'float') { 23 | const comps = createColorComponentsFromRgbObject(value); 24 | return new FloatColor(comps, 'rgb'); 25 | } 26 | return mapColorType(IntColor.black(), 'int'); 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/converter/writer-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {BindingTarget} from '../../../common/binding/target.js'; 5 | import {IntColor} from '../model/int-color.js'; 6 | import {writeRgbaColorObject, writeRgbColorObject} from './writer.js'; 7 | 8 | describe('writer/color', () => { 9 | it('should write RGBA color object value without destruction', () => { 10 | const obj = { 11 | foo: {r: 0, g: 127, b: 255, a: 0.5}, 12 | }; 13 | const objFoo = obj.foo; 14 | const t = new BindingTarget(obj, 'foo'); 15 | writeRgbaColorObject(t, new IntColor([128, 255, 0, 0.7], 'rgb'), 'int'); 16 | 17 | assert.strictEqual(obj.foo, objFoo, 'instance'); 18 | assert.strictEqual(obj.foo.r, 128, 'r'); 19 | assert.strictEqual(obj.foo.g, 255, 'g'); 20 | assert.strictEqual(obj.foo.b, 0, 'b'); 21 | assert.strictEqual(obj.foo.a, 0.7, 'a'); 22 | }); 23 | 24 | it('should write RGB color object value without destruction', () => { 25 | const obj = { 26 | foo: {r: 0, g: 127, b: 255, a: 0.5}, 27 | }; 28 | const objFoo = obj.foo; 29 | const t = new BindingTarget(obj, 'foo'); 30 | writeRgbColorObject(t, new IntColor([128, 255, 0, 0.7], 'rgb'), 'int'); 31 | 32 | assert.strictEqual(obj.foo, objFoo, 'instance'); 33 | assert.strictEqual(obj.foo.r, 128, 'r'); 34 | assert.strictEqual(obj.foo.g, 255, 'g'); 35 | assert.strictEqual(obj.foo.b, 0, 'b'); 36 | // should not overwrite alpha component 37 | assert.strictEqual(obj.foo.a, 0.5, 'a'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/model/float-color.ts: -------------------------------------------------------------------------------- 1 | import {Color, RgbaColorObject} from './color.js'; 2 | import { 3 | appendAlphaComponent, 4 | ColorComponents3, 5 | ColorComponents4, 6 | ColorMode, 7 | ColorType, 8 | constrainColorComponents, 9 | convertColor, 10 | removeAlphaComponent, 11 | } from './color-model.js'; 12 | 13 | export class FloatColor implements Color { 14 | private readonly comps_: ColorComponents4; 15 | public readonly mode: ColorMode; 16 | public readonly type: ColorType = 'float'; 17 | 18 | constructor(comps: ColorComponents3 | ColorComponents4, mode: ColorMode) { 19 | this.mode = mode; 20 | this.comps_ = constrainColorComponents(comps, mode, this.type); 21 | } 22 | 23 | public getComponents(opt_mode?: ColorMode): ColorComponents4 { 24 | return appendAlphaComponent( 25 | convertColor( 26 | removeAlphaComponent(this.comps_), 27 | {mode: this.mode, type: this.type}, 28 | {mode: opt_mode ?? this.mode, type: this.type}, 29 | ), 30 | this.comps_[3], 31 | ); 32 | } 33 | 34 | public toRgbaObject(): RgbaColorObject { 35 | const rgbComps = this.getComponents('rgb'); 36 | return { 37 | r: rgbComps[0], 38 | g: rgbComps[1], 39 | b: rgbComps[2], 40 | a: rgbComps[3], 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/model/int-color.ts: -------------------------------------------------------------------------------- 1 | import {Color, RgbaColorObject} from './color.js'; 2 | import { 3 | appendAlphaComponent, 4 | ColorComponents3, 5 | ColorComponents4, 6 | ColorMode, 7 | ColorType, 8 | constrainColorComponents, 9 | convertColor, 10 | removeAlphaComponent, 11 | } from './color-model.js'; 12 | 13 | export class IntColor implements Color { 14 | public static black(): IntColor { 15 | return new IntColor([0, 0, 0], 'rgb'); 16 | } 17 | 18 | private readonly comps_: ColorComponents4; 19 | public readonly mode: ColorMode; 20 | public readonly type: ColorType = 'int'; 21 | 22 | constructor(comps: ColorComponents3 | ColorComponents4, mode: ColorMode) { 23 | this.mode = mode; 24 | this.comps_ = constrainColorComponents(comps, mode, this.type); 25 | } 26 | 27 | public getComponents(opt_mode?: ColorMode): ColorComponents4 { 28 | return appendAlphaComponent( 29 | convertColor( 30 | removeAlphaComponent(this.comps_), 31 | {mode: this.mode, type: this.type}, 32 | {mode: opt_mode ?? this.mode, type: this.type}, 33 | ), 34 | this.comps_[3], 35 | ); 36 | } 37 | 38 | public toRgbaObject(): RgbaColorObject { 39 | const rgbComps = this.getComponents('rgb'); 40 | return { 41 | r: rgbComps[0], 42 | g: rgbComps[1], 43 | b: rgbComps[2], 44 | a: rgbComps[3], 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/plugin-number-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe} from 'mocha'; 3 | 4 | import {BindingTarget} from '../../common/binding/target.js'; 5 | import {NumberColorInputPlugin} from './plugin-number.js'; 6 | 7 | describe('NumberColorInputPlugin', () => { 8 | [ 9 | { 10 | view: 'color', 11 | }, 12 | { 13 | color: {}, 14 | }, 15 | { 16 | color: {type: 'float'}, 17 | }, 18 | ].forEach((params) => { 19 | context(`when params=${JSON.stringify(params)}`, () => { 20 | const input = { 21 | color: 0x00000000, 22 | }; 23 | const result = NumberColorInputPlugin.accept(input.color, params); 24 | 25 | it('should accept params', () => { 26 | assert.ok(result !== null); 27 | }); 28 | }); 29 | }); 30 | 31 | [ 32 | { 33 | params: { 34 | view: 'color', 35 | }, 36 | expected: 1, 37 | }, 38 | { 39 | params: { 40 | color: { 41 | alpha: true, 42 | }, 43 | }, 44 | expected: 0, 45 | }, 46 | ].forEach(({params, expected}) => { 47 | context(`when params=${JSON.stringify(params)}`, () => { 48 | const input = { 49 | color: 0xffffff00, 50 | }; 51 | const result = NumberColorInputPlugin.accept(input.color, params); 52 | if (!result) { 53 | throw new Error('unexpected result'); 54 | } 55 | const reader = NumberColorInputPlugin.binding.reader({ 56 | initialValue: input.color, 57 | params: result.params, 58 | target: new BindingTarget(input, 'color'), 59 | }); 60 | 61 | it('should apply alpha', () => { 62 | const c = reader(input.color); 63 | assert.strictEqual(c.getComponents('rgb')[3], expected); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/util.ts: -------------------------------------------------------------------------------- 1 | import {ColorInputParams} from '../../blade/common/api/params.js'; 2 | import {parseRecord} from '../../common/micro-parsers.js'; 3 | import {parsePickerLayout} from '../../common/picker-util.js'; 4 | import {ColorType} from './model/color-model.js'; 5 | 6 | function parseColorType(value: unknown): ColorType | undefined { 7 | return value === 'int' ? 'int' : value === 'float' ? 'float' : undefined; 8 | } 9 | 10 | export function parseColorInputParams( 11 | params: Record, 12 | ): ColorInputParams | undefined { 13 | return parseRecord(params, (p) => ({ 14 | color: p.optional.object({ 15 | alpha: p.optional.boolean, 16 | type: p.optional.custom(parseColorType), 17 | }), 18 | expanded: p.optional.boolean, 19 | picker: p.optional.custom(parsePickerLayout), 20 | readonly: p.optional.constant(false), 21 | })); 22 | } 23 | 24 | export function getKeyScaleForColor(forAlpha: boolean): number { 25 | return forAlpha ? 0.1 : 1; 26 | } 27 | 28 | export function extractColorType( 29 | params: ColorInputParams, 30 | ): ColorType | undefined { 31 | return params.color?.type; 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/color/view/color-texts-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe} from 'mocha'; 3 | 4 | import {ValueMap} from '../../../common/model/value-map.js'; 5 | import {createValue} from '../../../common/model/values.js'; 6 | import {ViewProps} from '../../../common/model/view-props.js'; 7 | import {NumberTextView} from '../../../common/number/view/number-text.js'; 8 | import {createTestWindow} from '../../../misc/dom-test-util.js'; 9 | import {Tuple3} from '../../../misc/type-util.js'; 10 | import {ColorTextsMode, ColorTextsView} from './color-texts.js'; 11 | 12 | function createTextViews( 13 | doc: Document, 14 | viewProps: ViewProps, 15 | ): Tuple3 { 16 | return [0, 1, 2].map( 17 | () => 18 | new NumberTextView(doc, { 19 | dragging: createValue(0), 20 | props: ValueMap.fromObject({ 21 | formatter: (v) => String(v), 22 | keyScale: 1, 23 | pointerScale: 1, 24 | }), 25 | value: createValue(0), 26 | viewProps: viewProps, 27 | }), 28 | ) as Tuple3; 29 | } 30 | 31 | describe(ColorTextsView.name, () => { 32 | it('should bind disabled', () => { 33 | const doc = createTestWindow().document; 34 | const viewProps = ViewProps.create(); 35 | const v = new ColorTextsView(doc, { 36 | mode: createValue('rgb'), 37 | inputViews: createTextViews(doc, viewProps), 38 | viewProps: viewProps, 39 | }); 40 | assert.strictEqual(v.modeSelectElement.disabled, false); 41 | 42 | viewProps.set('disabled', true); 43 | assert.strictEqual(v.modeSelectElement.disabled, true); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/common/constraint/point-nd-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe as context, describe, it} from 'mocha'; 3 | 4 | import {Constraint} from '../../../common/constraint/constraint.js'; 5 | import {RangeConstraint} from '../../../common/constraint/range.js'; 6 | import { 7 | Point2d, 8 | Point2dAssembly, 9 | Point2dObject, 10 | } from '../../point-2d/model/point-2d.js'; 11 | import {PointNdConstraint} from './point-nd.js'; 12 | 13 | interface TestCase { 14 | expected: Point2dObject; 15 | params: { 16 | config: { 17 | x?: Constraint; 18 | y?: Constraint; 19 | }; 20 | value: Point2dObject; 21 | }; 22 | } 23 | 24 | describe(PointNdConstraint.name, () => { 25 | [ 26 | { 27 | expected: {x: 123, y: -123}, 28 | params: { 29 | config: {}, 30 | value: {x: 123, y: -123}, 31 | }, 32 | }, 33 | { 34 | expected: {x: 0, y: -50}, 35 | params: { 36 | config: { 37 | x: new RangeConstraint({min: 0}), 38 | y: new RangeConstraint({min: -50}), 39 | }, 40 | value: {x: -100, y: -100}, 41 | }, 42 | }, 43 | ].forEach((testCase: TestCase) => { 44 | context(`when params = ${JSON.stringify(testCase.params)}`, () => { 45 | it(`should constrain value to ${JSON.stringify( 46 | testCase.expected, 47 | )}`, () => { 48 | const c = new PointNdConstraint({ 49 | assembly: Point2dAssembly, 50 | components: [testCase.params.config.x, testCase.params.config.y], 51 | }); 52 | const v = c.constrain( 53 | new Point2d(testCase.params.value.x, testCase.params.value.y), 54 | ); 55 | assert.deepStrictEqual(v.toObject(), testCase.expected); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/common/constraint/point-nd.ts: -------------------------------------------------------------------------------- 1 | import {Constraint} from '../../../common/constraint/constraint.js'; 2 | import {PointNdAssembly} from '../model/point-nd.js'; 3 | 4 | interface Config { 5 | assembly: PointNdAssembly; 6 | components: (Constraint | undefined)[]; 7 | } 8 | 9 | /** 10 | * @hidden 11 | */ 12 | export class PointNdConstraint implements Constraint { 13 | public readonly components: (Constraint | undefined)[]; 14 | private readonly asm_: PointNdAssembly; 15 | 16 | constructor(config: Config) { 17 | this.components = config.components; 18 | this.asm_ = config.assembly; 19 | } 20 | 21 | public constrain(value: PointNd): PointNd { 22 | const comps = this.asm_ 23 | .toComponents(value) 24 | .map((comp, index) => this.components[index]?.constrain(comp) ?? comp); 25 | return this.asm_.fromComponents(comps); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/common/model/point-nd.ts: -------------------------------------------------------------------------------- 1 | export interface PointNdAssembly { 2 | toComponents: (p: PointNd) => number[]; 3 | fromComponents: (comps: number[]) => PointNd; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/common/view/point-nd-text.ts: -------------------------------------------------------------------------------- 1 | import {NumberTextView} from '../../../common/number/view/number-text.js'; 2 | import {ClassName} from '../../../common/view/class-name.js'; 3 | import {View} from '../../../common/view/view.js'; 4 | 5 | interface Config { 6 | textViews: NumberTextView[]; 7 | } 8 | 9 | const cn = ClassName('pndtxt'); 10 | 11 | /** 12 | * @hidden 13 | */ 14 | export class PointNdTextView implements View { 15 | public readonly element: HTMLElement; 16 | public readonly textViews: NumberTextView[]; 17 | 18 | constructor(doc: Document, config: Config) { 19 | this.textViews = config.textViews; 20 | 21 | this.element = doc.createElement('div'); 22 | this.element.classList.add(cn()); 23 | 24 | this.textViews.forEach((v) => { 25 | const axisElem = doc.createElement('div'); 26 | axisElem.classList.add(cn('a')); 27 | axisElem.appendChild(v.element); 28 | this.element.appendChild(axisElem); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/number/api/slider.ts: -------------------------------------------------------------------------------- 1 | import {BindingApi} from '../../../blade/binding/api/binding.js'; 2 | import {InputBindingApi} from '../../../blade/binding/api/input-binding.js'; 3 | import {InputBindingController} from '../../../blade/binding/controller/input-binding.js'; 4 | import {SliderTextController} from '../../../common/number/controller/slider-text.js'; 5 | 6 | export class SliderInputBindingApi 7 | extends BindingApi< 8 | number, 9 | number, 10 | InputBindingController 11 | > 12 | implements InputBindingApi 13 | { 14 | get max(): number { 15 | return this.controller.valueController.sliderController.props.get('max'); 16 | } 17 | 18 | set max(max: number) { 19 | this.controller.valueController.sliderController.props.set('max', max); 20 | } 21 | 22 | get min(): number { 23 | return this.controller.valueController.sliderController.props.get('min'); 24 | } 25 | 26 | set min(max: number) { 27 | this.controller.valueController.sliderController.props.set('min', max); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-2d/converter/point-2d.ts: -------------------------------------------------------------------------------- 1 | import {BindingTarget} from '../../../common/binding/target.js'; 2 | import {Point2d} from '../model/point-2d.js'; 3 | 4 | export function point2dFromUnknown(value: unknown): Point2d { 5 | return Point2d.isObject(value) 6 | ? new Point2d(value.x, value.y) 7 | : new Point2d(); 8 | } 9 | 10 | export function writePoint2d(target: BindingTarget, value: Point2d) { 11 | target.writeProperty('x', value.x); 12 | target.writeProperty('y', value.y); 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-2d/model/point-2d.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty} from '../../../misc/type-util.js'; 2 | import {PointNdAssembly} from '../../common/model/point-nd.js'; 3 | 4 | export interface Point2dObject { 5 | x: number; 6 | y: number; 7 | } 8 | 9 | export class Point2d { 10 | public x: number; 11 | public y: number; 12 | 13 | constructor(x = 0, y = 0) { 14 | this.x = x; 15 | this.y = y; 16 | } 17 | 18 | public getComponents(): [number, number] { 19 | return [this.x, this.y]; 20 | } 21 | 22 | public static isObject(obj: any): obj is Point2dObject { 23 | if (isEmpty(obj)) { 24 | return false; 25 | } 26 | 27 | const x = obj.x; 28 | const y = obj.y; 29 | if (typeof x !== 'number' || typeof y !== 'number') { 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public static equals(v1: Point2d, v2: Point2d): boolean { 37 | return v1.x === v2.x && v1.y === v2.y; 38 | } 39 | 40 | public toObject(): Point2dObject { 41 | return { 42 | x: this.x, 43 | y: this.y, 44 | }; 45 | } 46 | } 47 | 48 | export const Point2dAssembly: PointNdAssembly = { 49 | toComponents: (p: Point2d) => p.getComponents(), 50 | fromComponents: (comps: number[]) => new Point2d(...comps), 51 | }; 52 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-3d/converter/point-3d-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {BindingTarget} from '../../../common/binding/target.js'; 5 | import {Point3d} from '../model/point-3d.js'; 6 | import {writePoint3d} from './point-3d.js'; 7 | 8 | describe(writePoint3d.name, () => { 9 | it('should write value without destruction', () => { 10 | const obj = { 11 | foo: {x: 12, y: 34, z: -56}, 12 | }; 13 | const objFoo = obj.foo; 14 | const t = new BindingTarget(obj, 'foo'); 15 | writePoint3d(t, new Point3d(56, 78, 901)); 16 | 17 | assert.strictEqual(obj.foo, objFoo); 18 | assert.strictEqual(obj.foo.x, 56); 19 | assert.strictEqual(obj.foo.y, 78); 20 | assert.strictEqual(obj.foo.z, 901); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-3d/converter/point-3d.ts: -------------------------------------------------------------------------------- 1 | import {BindingTarget} from '../../../common/binding/target.js'; 2 | import {Point3d} from '../model/point-3d.js'; 3 | 4 | export function point3dFromUnknown(value: unknown): Point3d { 5 | return Point3d.isObject(value) 6 | ? new Point3d(value.x, value.y, value.z) 7 | : new Point3d(); 8 | } 9 | 10 | export function writePoint3d(target: BindingTarget, value: Point3d) { 11 | target.writeProperty('x', value.x); 12 | target.writeProperty('y', value.y); 13 | target.writeProperty('z', value.z); 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-3d/model/point-3d-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe as context, describe, it} from 'mocha'; 3 | 4 | import {Point3d} from './point-3d.js'; 5 | 6 | describe(Point3d.name, () => { 7 | [ 8 | { 9 | object: null, 10 | expected: false, 11 | }, 12 | { 13 | object: undefined, 14 | expected: false, 15 | }, 16 | { 17 | object: {x: 0, y: 1}, 18 | expected: false, 19 | }, 20 | { 21 | object: {x: -1, y: 0, z: 1}, 22 | expected: true, 23 | }, 24 | { 25 | object: {x: 1, y: 2, z: '3'}, 26 | expected: false, 27 | }, 28 | ].forEach((testCase) => { 29 | context(`when object = ${JSON.stringify(testCase.object)}`, () => { 30 | it(`should regard input as object: ${testCase.expected}`, () => { 31 | assert.strictEqual( 32 | Point3d.isObject(testCase.object), 33 | testCase.expected, 34 | ); 35 | }); 36 | }); 37 | }); 38 | 39 | [ 40 | { 41 | object: new Point3d(0, 1, 2), 42 | expected: {x: 0, y: 1, z: 2}, 43 | }, 44 | { 45 | object: new Point3d(), 46 | expected: {x: 0, y: 0, z: 0}, 47 | }, 48 | { 49 | object: new Point3d(-100), 50 | expected: {x: -100, y: 0, z: 0}, 51 | }, 52 | ].forEach((testCase) => { 53 | context(`when Point3d = ${JSON.stringify(testCase.object)}`, () => { 54 | it(`should convert into object: ${testCase.expected}`, () => { 55 | assert.deepStrictEqual(testCase.object.toObject(), testCase.expected); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-3d/model/point-3d.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty} from '../../../misc/type-util.js'; 2 | import {PointNdAssembly} from '../../common/model/point-nd.js'; 3 | 4 | export interface Point3dObject { 5 | x: number; 6 | y: number; 7 | z: number; 8 | } 9 | 10 | export class Point3d { 11 | public x: number; 12 | public y: number; 13 | public z: number; 14 | 15 | constructor(x = 0, y = 0, z = 0) { 16 | this.x = x; 17 | this.y = y; 18 | this.z = z; 19 | } 20 | 21 | public getComponents(): [number, number, number] { 22 | return [this.x, this.y, this.z]; 23 | } 24 | 25 | public static isObject(obj: any): obj is Point3dObject { 26 | if (isEmpty(obj)) { 27 | return false; 28 | } 29 | 30 | const x = obj.x; 31 | const y = obj.y; 32 | const z = obj.z; 33 | if ( 34 | typeof x !== 'number' || 35 | typeof y !== 'number' || 36 | typeof z !== 'number' 37 | ) { 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | public static equals(v1: Point3d, v2: Point3d): boolean { 45 | return v1.x === v2.x && v1.y === v2.y && v1.z === v2.z; 46 | } 47 | 48 | public toObject(): Point3dObject { 49 | return { 50 | x: this.x, 51 | y: this.y, 52 | z: this.z, 53 | }; 54 | } 55 | } 56 | 57 | export const Point3dAssembly: PointNdAssembly = { 58 | toComponents: (p: Point3d) => p.getComponents(), 59 | fromComponents: (comps: number[]) => new Point3d(...comps), 60 | }; 61 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-4d/converter/point-4d-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {BindingTarget} from '../../../common/binding/target.js'; 5 | import {Point4d} from '../model/point-4d.js'; 6 | import {writePoint4d} from './point-4d.js'; 7 | 8 | describe(writePoint4d.name, () => { 9 | it('should write value without destruction', () => { 10 | const obj = { 11 | foo: {x: 12, y: 34, z: -56, w: 78}, 12 | }; 13 | const objFoo = obj.foo; 14 | const t = new BindingTarget(obj, 'foo'); 15 | writePoint4d(t, new Point4d(56, 78, 901, 23)); 16 | 17 | assert.strictEqual(obj.foo, objFoo); 18 | assert.strictEqual(obj.foo.x, 56); 19 | assert.strictEqual(obj.foo.y, 78); 20 | assert.strictEqual(obj.foo.z, 901); 21 | assert.strictEqual(obj.foo.w, 23); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-4d/converter/point-4d.ts: -------------------------------------------------------------------------------- 1 | import {BindingTarget} from '../../../common/binding/target.js'; 2 | import {Point4d} from '../model/point-4d.js'; 3 | 4 | export function point4dFromUnknown(value: unknown): Point4d { 5 | return Point4d.isObject(value) 6 | ? new Point4d(value.x, value.y, value.z, value.w) 7 | : new Point4d(); 8 | } 9 | 10 | export function writePoint4d(target: BindingTarget, value: Point4d) { 11 | target.writeProperty('x', value.x); 12 | target.writeProperty('y', value.y); 13 | target.writeProperty('z', value.z); 14 | target.writeProperty('w', value.w); 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-4d/model/point-4d-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe as context, describe, it} from 'mocha'; 3 | 4 | import {Point4d} from './point-4d.js'; 5 | 6 | describe(Point4d.name, () => { 7 | [ 8 | { 9 | object: null, 10 | expected: false, 11 | }, 12 | { 13 | object: undefined, 14 | expected: false, 15 | }, 16 | { 17 | object: {x: 0, y: 1}, 18 | expected: false, 19 | }, 20 | { 21 | object: {x: -1, y: 0, z: 1}, 22 | expected: false, 23 | }, 24 | { 25 | object: {x: -1, y: 0, z: 1, w: 0}, 26 | expected: true, 27 | }, 28 | { 29 | object: {x: 1, y: 2, z: 3, w: '4'}, 30 | expected: false, 31 | }, 32 | ].forEach((testCase) => { 33 | context(`when object = ${JSON.stringify(testCase.object)}`, () => { 34 | it(`should regard input as object: ${testCase.expected}`, () => { 35 | assert.strictEqual( 36 | Point4d.isObject(testCase.object), 37 | testCase.expected, 38 | ); 39 | }); 40 | }); 41 | }); 42 | 43 | [ 44 | { 45 | object: new Point4d(0, 1, 2), 46 | expected: {x: 0, y: 1, z: 2, w: 0}, 47 | }, 48 | { 49 | object: new Point4d(), 50 | expected: {x: 0, y: 0, z: 0, w: 0}, 51 | }, 52 | { 53 | object: new Point4d(-100), 54 | expected: {x: -100, y: 0, z: 0, w: 0}, 55 | }, 56 | { 57 | object: new Point4d(1, 2, 3, 4), 58 | expected: {x: 1, y: 2, z: 3, w: 4}, 59 | }, 60 | ].forEach((testCase) => { 61 | context(`when Point3d = ${JSON.stringify(testCase.object)}`, () => { 62 | it(`should convert into object: ${testCase.expected}`, () => { 63 | assert.deepStrictEqual(testCase.object.toObject(), testCase.expected); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/core/src/input-binding/point-4d/model/point-4d.ts: -------------------------------------------------------------------------------- 1 | import {isEmpty, Tuple4} from '../../../misc/type-util.js'; 2 | import {PointNdAssembly} from '../../common/model/point-nd.js'; 3 | 4 | export interface Point4dObject { 5 | x: number; 6 | y: number; 7 | z: number; 8 | w: number; 9 | } 10 | 11 | export class Point4d { 12 | public x: number; 13 | public y: number; 14 | public z: number; 15 | public w: number; 16 | 17 | constructor(x = 0, y = 0, z = 0, w = 0) { 18 | this.x = x; 19 | this.y = y; 20 | this.z = z; 21 | this.w = w; 22 | } 23 | 24 | public getComponents(): Tuple4 { 25 | return [this.x, this.y, this.z, this.w]; 26 | } 27 | 28 | public static isObject(obj: any): obj is Point4dObject { 29 | if (isEmpty(obj)) { 30 | return false; 31 | } 32 | 33 | const x = obj.x; 34 | const y = obj.y; 35 | const z = obj.z; 36 | const w = obj.w; 37 | if ( 38 | typeof x !== 'number' || 39 | typeof y !== 'number' || 40 | typeof z !== 'number' || 41 | typeof w !== 'number' 42 | ) { 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | public static equals(v1: Point4d, v2: Point4d): boolean { 50 | return v1.x === v2.x && v1.y === v2.y && v1.z === v2.z && v1.w === v2.w; 51 | } 52 | 53 | public toObject(): Point4dObject { 54 | return { 55 | x: this.x, 56 | y: this.y, 57 | z: this.z, 58 | w: this.w, 59 | }; 60 | } 61 | } 62 | 63 | export const Point4dAssembly: PointNdAssembly = { 64 | toComponents: (p: Point4d) => p.getComponents(), 65 | fromComponents: (comps: number[]) => new Point4d(...comps), 66 | }; 67 | -------------------------------------------------------------------------------- /packages/core/src/misc/constants.ts: -------------------------------------------------------------------------------- 1 | export const Constants = { 2 | monitor: { 3 | defaultInterval: 200, 4 | defaultRows: 3, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/core/src/misc/dom-test-util.ts: -------------------------------------------------------------------------------- 1 | import {JSDOM} from 'jsdom'; 2 | 3 | import {forceCast} from './type-util.js'; 4 | 5 | export function createTestWindow(): Window { 6 | return forceCast(new JSDOM('').window); 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/misc/semver-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe as context, describe, it} from 'mocha'; 3 | 4 | import {Semver} from './semver.js'; 5 | 6 | describe(Semver.name, () => { 7 | [ 8 | { 9 | expected: { 10 | major: 0, 11 | minor: 0, 12 | patch: 0, 13 | prerelease: null, 14 | text: '0.0.0', 15 | }, 16 | text: '0.0.0', 17 | }, 18 | { 19 | expected: { 20 | major: 3, 21 | minor: 14, 22 | patch: 16, 23 | prerelease: null, 24 | text: '3.14.16', 25 | }, 26 | text: '3.14.16', 27 | }, 28 | { 29 | expected: { 30 | major: 0, 31 | minor: 1, 32 | patch: 100, 33 | prerelease: null, 34 | text: '0.1.100', 35 | }, 36 | text: '0.01.0100', 37 | }, 38 | { 39 | expected: { 40 | major: 1, 41 | minor: 2, 42 | patch: 3, 43 | prerelease: 'beta.0', 44 | text: '1.2.3-beta.0', 45 | }, 46 | text: '1.2.3-beta.0', 47 | }, 48 | { 49 | expected: { 50 | major: 1, 51 | minor: 1, 52 | patch: 5, 53 | prerelease: '0', 54 | text: '1.1.5-0', 55 | }, 56 | text: '1.1.5-0', 57 | }, 58 | ].forEach(({expected, text}) => { 59 | context(`when ${JSON.stringify(text)}`, () => { 60 | it('should compare array deeply', () => { 61 | const semver = new Semver(text); 62 | assert.strictEqual(semver.major, expected.major); 63 | assert.strictEqual(semver.minor, expected.minor); 64 | assert.strictEqual(semver.patch, expected.patch); 65 | assert.strictEqual(semver.prerelease, expected.prerelease); 66 | assert.strictEqual(semver.toString(), expected.text); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/core/src/misc/semver.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * A simple semantic versioning perser. 3 | */ 4 | export class Semver { 5 | public readonly major: number; 6 | public readonly minor: number; 7 | public readonly patch: number; 8 | public readonly prerelease: string | null; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | constructor(text: string) { 14 | const [core, prerelease] = text.split('-'); 15 | const coreComps = core.split('.'); 16 | this.major = parseInt(coreComps[0], 10); 17 | this.minor = parseInt(coreComps[1], 10); 18 | this.patch = parseInt(coreComps[2], 10); 19 | this.prerelease = prerelease ?? null; 20 | } 21 | 22 | public toString(): string { 23 | const core = [this.major, this.minor, this.patch].join('.'); 24 | return this.prerelease !== null ? [core, this.prerelease].join('-') : core; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/misc/test-util.ts: -------------------------------------------------------------------------------- 1 | export const TestUtil = { 2 | createEvent: ( 3 | win: Window, 4 | type: string, 5 | options?: Record, 6 | ): Event => { 7 | return options 8 | ? new (win as any).Event(type, options) 9 | : new (win as any).Event(type); 10 | }, 11 | 12 | createKeyboardEvent: ( 13 | win: Window, 14 | type: string, 15 | options: Record, 16 | ): Event => { 17 | return new (win as any).KeyboardEvent(type, options); 18 | }, 19 | 20 | closeTo: (actual: number, expected: number, delta: number): boolean => { 21 | return Math.abs(actual - expected) < delta; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/monitor-binding/common/controller/multi-log.ts: -------------------------------------------------------------------------------- 1 | import {BufferedValueController} from '../../../blade/binding/controller/monitor-binding.js'; 2 | import {Formatter} from '../../../common/converter/formatter.js'; 3 | import {BufferedValue} from '../../../common/model/buffered-value.js'; 4 | import {ViewProps} from '../../../common/model/view-props.js'; 5 | import {MultiLogView} from '../view/multi-log.js'; 6 | 7 | interface Config { 8 | formatter: Formatter; 9 | rows: number; 10 | value: BufferedValue; 11 | viewProps: ViewProps; 12 | } 13 | 14 | /** 15 | * @hidden 16 | */ 17 | export class MultiLogController 18 | implements BufferedValueController> 19 | { 20 | public readonly value: BufferedValue; 21 | public readonly view: MultiLogView; 22 | public readonly viewProps: ViewProps; 23 | 24 | constructor(doc: Document, config: Config) { 25 | this.value = config.value; 26 | this.viewProps = config.viewProps; 27 | 28 | this.view = new MultiLogView(doc, { 29 | formatter: config.formatter, 30 | rows: config.rows, 31 | value: this.value, 32 | viewProps: this.viewProps, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/monitor-binding/common/controller/single-log.ts: -------------------------------------------------------------------------------- 1 | import {BufferedValueController} from '../../../blade/binding/controller/monitor-binding.js'; 2 | import {Formatter} from '../../../common/converter/formatter.js'; 3 | import {BufferedValue} from '../../../common/model/buffered-value.js'; 4 | import {ViewProps} from '../../../common/model/view-props.js'; 5 | import {SingleLogView} from '../view/single-log.js'; 6 | 7 | interface Config { 8 | formatter: Formatter; 9 | value: BufferedValue; 10 | viewProps: ViewProps; 11 | } 12 | 13 | /** 14 | * @hidden 15 | */ 16 | export class SingleLogController 17 | implements BufferedValueController> 18 | { 19 | public readonly value: BufferedValue; 20 | public readonly view: SingleLogView; 21 | public readonly viewProps: ViewProps; 22 | 23 | constructor(doc: Document, config: Config) { 24 | this.value = config.value; 25 | this.viewProps = config.viewProps; 26 | 27 | this.view = new SingleLogView(doc, { 28 | formatter: config.formatter, 29 | value: this.value, 30 | viewProps: this.viewProps, 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/monitor-binding/number/api/graph-log.ts: -------------------------------------------------------------------------------- 1 | import {BindingApi} from '../../../blade/binding/api/binding.js'; 2 | import {MonitorBindingApi} from '../../../blade/binding/api/monitor-binding.js'; 3 | import {MonitorBindingController} from '../../../blade/binding/controller/monitor-binding.js'; 4 | import {TpBuffer} from '../../../common/model/buffered-value.js'; 5 | import {GraphLogController} from '../controller/graph-log.js'; 6 | 7 | export class GraphLogMonitorBindingApi 8 | extends BindingApi< 9 | TpBuffer, 10 | number, 11 | MonitorBindingController 12 | > 13 | implements MonitorBindingApi 14 | { 15 | get max(): number { 16 | return this.controller.valueController.props.get('max'); 17 | } 18 | 19 | set max(max: number) { 20 | this.controller.valueController.props.set('max', max); 21 | } 22 | 23 | get min(): number { 24 | return this.controller.valueController.props.get('min'); 25 | } 26 | 27 | set min(min: number) { 28 | this.controller.valueController.props.set('min', min); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/monitor-binding/number/plugin-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe} from 'mocha'; 3 | 4 | import {MonitorBindingController} from '../../blade/binding/controller/monitor-binding.js'; 5 | import {BindingTarget} from '../../common/binding/target.js'; 6 | import {createTestWindow} from '../../misc/dom-test-util.js'; 7 | import {SingleLogController} from '../common/controller/single-log.js'; 8 | import {createMonitorBindingController} from '../plugin.js'; 9 | import {NumberMonitorPlugin} from './plugin.js'; 10 | 11 | describe(NumberMonitorPlugin.id, () => { 12 | it('should apply `format`', () => { 13 | const doc = createTestWindow().document; 14 | const obj = { 15 | foo: 1, 16 | }; 17 | const bc = createMonitorBindingController(NumberMonitorPlugin, { 18 | document: doc, 19 | params: { 20 | format: () => 'formatted', 21 | interval: 0, 22 | readonly: true, 23 | }, 24 | target: new BindingTarget(obj, 'foo'), 25 | }) as MonitorBindingController; 26 | 27 | const c = bc.valueController as SingleLogController; 28 | assert.strictEqual(c.view.inputElement.value, 'formatted'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/core/src/plugin/blade-api-cache-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import { 5 | createAppropriateBladeApi, 6 | createAppropriateBladeController, 7 | } from '../blade/test-util.js'; 8 | import {createTestWindow} from '../misc/dom-test-util.js'; 9 | import {BladeApiCache} from './blade-api-cache.js'; 10 | 11 | describe(BladeApiCache.name, () => { 12 | it('should add cache', () => { 13 | const doc = createTestWindow().document; 14 | const bc = createAppropriateBladeController(doc); 15 | const api = createAppropriateBladeApi(doc); 16 | const cache = new BladeApiCache(); 17 | 18 | assert.strictEqual(cache.get(bc), null); 19 | cache.add(bc, api); 20 | assert.strictEqual(cache.get(bc), api); 21 | }); 22 | 23 | it('should get existance', () => { 24 | const doc = createTestWindow().document; 25 | const bc = createAppropriateBladeController(doc); 26 | const api = createAppropriateBladeApi(doc); 27 | const cache = new BladeApiCache(); 28 | 29 | assert.strictEqual(cache.has(bc), false); 30 | cache.add(bc, api); 31 | assert.strictEqual(cache.has(bc), true); 32 | }); 33 | 34 | it('should remove disposed API', () => { 35 | const doc = createTestWindow().document; 36 | const bc = createAppropriateBladeController(doc); 37 | const api = createAppropriateBladeApi(doc); 38 | const cache = new BladeApiCache(); 39 | 40 | api.dispose(); 41 | assert.strictEqual(cache.has(bc), false); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/core/src/plugin/blade-api-cache.ts: -------------------------------------------------------------------------------- 1 | import {BladeApi} from '../blade/common/api/blade.js'; 2 | import {BladeController} from '../blade/common/controller/blade.js'; 3 | 4 | /** 5 | * A cache that maps blade controllers and APIs. 6 | * @hidden 7 | */ 8 | export class BladeApiCache { 9 | private map_: Map = new Map(); 10 | 11 | public get(bc: BladeController): BladeApi | null { 12 | return this.map_.get(bc) ?? null; 13 | } 14 | 15 | public has(bc: BladeController): boolean { 16 | return this.map_.has(bc); 17 | } 18 | 19 | public add(bc: BladeController, api: BladeApi): typeof api { 20 | this.map_.set(bc, api); 21 | bc.viewProps.handleDispose(() => { 22 | this.map_.delete(bc); 23 | }); 24 | return api; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/plugin/plugin.ts: -------------------------------------------------------------------------------- 1 | import {Semver} from '../misc/semver.js'; 2 | import {VERSION} from '../version.js'; 3 | 4 | export type PluginType = 'blade' | 'input' | 'monitor'; 5 | 6 | /** 7 | * A base interface of the plugin. 8 | */ 9 | export interface BasePlugin { 10 | /** 11 | * The identifier of the plugin. 12 | */ 13 | id: string; 14 | 15 | /** 16 | * The type of the plugin. 17 | */ 18 | type: PluginType; 19 | 20 | /** 21 | * The version of the core used for this plugin. 22 | */ 23 | core?: Semver; 24 | } 25 | 26 | /** 27 | * Creates a plugin with the current core. 28 | * @param plugin The plugin without the core version. 29 | * @return A plugin with the core version. 30 | */ 31 | export function createPlugin

(plugin: Omit): P { 32 | return { 33 | core: VERSION, 34 | ...plugin, 35 | } as P; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../dist" 6 | }, 7 | "exclude": ["**/*-test.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/version.ts: -------------------------------------------------------------------------------- 1 | import {Semver} from './misc/semver.js'; 2 | 3 | export const VERSION = new Semver('0.0.0-core.0'); 4 | -------------------------------------------------------------------------------- /packages/tweakpane/.npmignore: -------------------------------------------------------------------------------- 1 | .circleci/ 2 | .nyc_output/ 3 | docs/ 4 | pages/ 5 | test/ 6 | 7 | .editorconfig 8 | -------------------------------------------------------------------------------- /packages/tweakpane/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /packages/tweakpane/rollup-doc.config.js: -------------------------------------------------------------------------------- 1 | import Alias from '@rollup/plugin-alias'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import Typescript from '@rollup/plugin-typescript'; 4 | import Cleanup from 'rollup-plugin-cleanup'; 5 | import {terser as Terser} from 'rollup-plugin-terser'; 6 | 7 | export default async () => { 8 | return { 9 | input: 'src/doc/ts/bundle.ts', 10 | external: ['dat.gui', 'tweakpane'], 11 | output: { 12 | file: `docs/assets/bundle.js`, 13 | format: 'umd', 14 | globals: { 15 | 'dat.gui': 'dat', 16 | tweakpane: 'Tweakpane', 17 | }, 18 | }, 19 | plugins: [ 20 | Alias({ 21 | entries: [ 22 | { 23 | find: '@tweakpane/core', 24 | replacement: '../../node_modules/@tweakpane/core/dist/index.js', 25 | }, 26 | ], 27 | }), 28 | Typescript({ 29 | tsconfig: 'src/doc/tsconfig.json', 30 | }), 31 | nodeResolve({ 32 | preferBuiltins: false, 33 | }), 34 | Terser(), 35 | // https://github.com/microsoft/tslib/issues/47 36 | Cleanup({ 37 | comments: 'none', 38 | }), 39 | ], 40 | 41 | onwarn(warning, warn) { 42 | if (warning.code === 'CIRCULAR_DEPENDENCY') { 43 | return; 44 | } 45 | warn(warning); 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/tweakpane/scripts/assets-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 | -------------------------------------------------------------------------------- /packages/tweakpane/scripts/doc-build-html.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-env node */ 3 | 4 | import Fs from 'fs-extra'; 5 | import Glob from 'glob'; 6 | import Nunjucks from 'nunjucks'; 7 | import Path from 'path'; 8 | 9 | const Package = JSON.parse( 10 | Fs.readFileSync(new URL('../package.json', import.meta.url)), 11 | ); 12 | 13 | const context = { 14 | description: Package.description, 15 | version: Package.version, 16 | }; 17 | 18 | const SRC_DIR = 'src/doc/template'; 19 | const SRC_PATTERN = 'src/doc/template/**/*.html'; 20 | const DST_DIR = 'docs'; 21 | 22 | Fs.mkdirsSync(DST_DIR); 23 | 24 | const srcPaths = Glob.sync(SRC_PATTERN).filter((path) => { 25 | return Path.basename(path).match(/^_.+$/) === null; 26 | }); 27 | console.log('Found sources:'); 28 | console.log(srcPaths.map((path) => ` ${path}`).join('\n')); 29 | 30 | const env = new Nunjucks.Environment(new Nunjucks.FileSystemLoader(SRC_DIR)); 31 | console.log('Compiling...'); 32 | srcPaths.forEach((srcPath) => { 33 | const srcBase = Path.relative(SRC_DIR, srcPath); 34 | const dstPath = Path.join(DST_DIR, srcBase); 35 | 36 | const dstDir = Path.dirname(dstPath); 37 | if (!Fs.existsSync(dstDir)) { 38 | Fs.mkdirsSync(dstDir); 39 | } 40 | 41 | console.log(` ${dstPath}`); 42 | Fs.writeFileSync(dstPath, env.render(srcBase, context)); 43 | }); 44 | 45 | console.log('done.'); 46 | -------------------------------------------------------------------------------- /packages/tweakpane/scripts/main-test-ts-module-pre.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-env node */ 3 | 4 | import Fs from 'fs'; 5 | 6 | const corePackage = JSON.parse( 7 | Fs.readFileSync(new URL('../../core/package.json', import.meta.url)), 8 | ); 9 | const panePackage = JSON.parse( 10 | Fs.readFileSync(new URL('../package.json', import.meta.url)), 11 | ); 12 | 13 | // Remove version of core tgz file 14 | process.chdir('../core'); 15 | 16 | const coreTgz = `tweakpane-core-${corePackage.version}.tgz`; 17 | if (Fs.existsSync(coreTgz)) { 18 | Fs.renameSync(coreTgz, 'tweakpane-core.tgz'); 19 | } 20 | 21 | // Remove version of tweakpane tgz file 22 | process.chdir('../tweakpane'); 23 | 24 | const paneTgz = `tweakpane-${panePackage.version}.tgz`; 25 | if (Fs.existsSync(paneTgz)) { 26 | Fs.renameSync(paneTgz, 'tweakpane.tgz'); 27 | } 28 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/img/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocopon/tweakpane/c7c99a0e6c9779483c6c9dffa83e9aab255cbed9/packages/tweakpane/src/doc/img/og.png -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/_pages.scss: -------------------------------------------------------------------------------- 1 | @use './defs'; 2 | 3 | :root.index { 4 | .pageHeader { 5 | background-image: none; 6 | } 7 | .pageHeader_inner { 8 | position: static; 9 | 10 | @include defs.wide() { 11 | padding-bottom: 128px; 12 | padding-top: 128px; 13 | } 14 | } 15 | .pageHeader_text { 16 | mix-blend-mode: difference; 17 | } 18 | .pageHeader_title { 19 | color: white; 20 | } 21 | .pageHeader_text p { 22 | color: white; 23 | 24 | @include defs.nonwide() { 25 | hyphens: auto; 26 | } 27 | } 28 | .pageHeader_pane { 29 | position: relative; 30 | } 31 | } 32 | 33 | :root.theming { 34 | .pageHeader { 35 | background-color: #21292a; 36 | background-image: url(https://source.unsplash.com/u27Rrbs9Dwc/1600x900); 37 | background-position: center; 38 | background-repeat: no-repeat; 39 | background-size: cover; 40 | 41 | &::before { 42 | background-image: radial-gradient( 43 | rgba(255, 255, 255, 0.1) 1px, 44 | transparent 0 45 | ); 46 | background-position: center; 47 | background-size: 16px 16px; 48 | content: ''; 49 | inset: 0; 50 | position: absolute; 51 | } 52 | 53 | .paneContainer { 54 | -webkit-backdrop-filter: blur(4px); 55 | backdrop-filter: blur(4px); 56 | } 57 | } 58 | .pageHeader_title { 59 | h1, 60 | p { 61 | color: rgba(white, 0.8); 62 | } 63 | } 64 | } 65 | 66 | :root.catalog { 67 | &.readme { 68 | .main_menu { 69 | display: none; 70 | } 71 | .main_wrap { 72 | max-width: 1088px; 73 | } 74 | .paneCatalog { 75 | border-radius: 0; 76 | padding: 64px; 77 | } 78 | } 79 | 80 | .tp-dfwv { 81 | position: fixed; 82 | } 83 | &.readme .tp-dfwv { 84 | display: none; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/_reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | pre { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | h1, 8 | h2, 9 | h3, 10 | h4, 11 | h5, 12 | h6 { 13 | font-size: 1rem; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | p { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | pre { 22 | display: block; 23 | } 24 | dl, 25 | ol, 26 | ul { 27 | list-style-type: none; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | li { 32 | margin: 0; 33 | padding: 0; 34 | } 35 | button { 36 | appearance: none; 37 | background-color: transparent; 38 | border-width: 0; 39 | cursor: pointer; 40 | font-family: inherit; 41 | font-size: inherit; 42 | padding: 0; 43 | } 44 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/bundle.scss: -------------------------------------------------------------------------------- 1 | @use './reset'; 2 | @use './base'; 3 | 4 | @use './components/catalog'; 5 | @use './components/code-block'; 6 | @use './components/concept'; 7 | @use './components/demo'; 8 | @use './components/global-footer'; 9 | @use './components/global-header'; 10 | @use './components/global-nav'; 11 | @use './components/hljs'; 12 | @use './components/logo'; 13 | @use './components/main'; 14 | @use './components/migration'; 15 | @use './components/page-header'; 16 | @use './components/pane-container'; 17 | @use './components/photo-credit'; 18 | @use './components/rel'; 19 | @use './components/sponsor'; 20 | @use './components/theme-builder'; 21 | 22 | @use './pages'; 23 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_catalog.scss: -------------------------------------------------------------------------------- 1 | .paneCatalog { 2 | background-color: var(--bg-color-secondary); 3 | border-radius: 6px; 4 | display: grid; 5 | gap: 32px; 6 | grid-template-columns: repeat(auto-fit, 256px); 7 | justify-content: center; 8 | padding: 32px; 9 | 10 | &#{&}-theming { 11 | background-color: hsl(230, 7%, 57%); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_code-block.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .codeBlock { 4 | position: relative; 5 | 6 | &#{&}-compact { 7 | @media screen { 8 | max-height: 256px; 9 | } 10 | } 11 | 12 | pre { 13 | overflow: auto; 14 | padding: 32px; 15 | 16 | @include defs.nonwide() { 17 | padding: 24px; 18 | } 19 | 20 | code { 21 | font-size: 0.9rem; 22 | font-weight: 500; 23 | } 24 | } 25 | &#{&}-fig { 26 | pre { 27 | line-height: 0.9; 28 | } 29 | pre code { 30 | font-size: 0.8rem; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_concept.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .concepts { 4 | display: grid; 5 | margin-bottom: 1em; 6 | margin-top: 1em; 7 | 8 | @include defs.wide() { 9 | gap: 16px; 10 | grid-template-columns: repeat(3, 1fr); 11 | } 12 | @include defs.nonwide() { 13 | gap: 8px; 14 | grid-template-columns: repeat(1, 1fr); 15 | } 16 | } 17 | 18 | .concept { 19 | border: var(--bg-color-action) solid 2px; 20 | border-radius: 6px; 21 | padding: 24px 32px; 22 | 23 | &_title { 24 | font-weight: bold; 25 | } 26 | &_detail { 27 | color: var(--fg-color-secondary); 28 | margin-top: 16px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_demo.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .demo { 4 | background-color: var(--bg-color-secondary); 5 | border-radius: 6px; 6 | position: relative; 7 | 8 | @include defs.wide() { 9 | display: flex; 10 | } 11 | 12 | &_code { 13 | display: flex; 14 | flex: 3; 15 | overflow: auto; 16 | 17 | .codeBlock { 18 | flex: 1; 19 | } 20 | } 21 | &_result { 22 | @include defs.dotGrid(); 23 | 24 | flex: 2; 25 | padding: 32px; 26 | text-align: center; 27 | } 28 | &_chip { 29 | background-color: var(--hl-comment); 30 | border-bottom-left-radius: 1px; 31 | color: var(--bg-color-secondary); 32 | font-family: defs.$font-family-mono; 33 | font-size: 0.7rem; 34 | font-weight: 500; 35 | padding: 0.2em 0.5em; 36 | position: absolute; 37 | right: 0; 38 | top: 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_global-footer.scss: -------------------------------------------------------------------------------- 1 | .globalFooter { 2 | text-align: center; 3 | 4 | &_inner { 5 | box-sizing: border-box; 6 | margin-left: auto; 7 | margin-right: auto; 8 | padding-bottom: 16px; 9 | padding-top: 16px; 10 | } 11 | &_copy { 12 | color: var(--fg-color-secondary); 13 | font-size: 0.8rem; 14 | 15 | a { 16 | color: inherit; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_global-header.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .globalHeader { 4 | -webkit-backdrop-filter: blur(2px); 5 | backdrop-filter: blur(2px); 6 | background-color: var(--bg-color-translucent); 7 | box-shadow: 0 2px 4px rgba(black, 0.05); 8 | color: inherit; 9 | height: defs.$global-header-height; 10 | left: 0; 11 | line-height: defs.$global-header-height; 12 | position: fixed; 13 | right: 0; 14 | text-align: center; 15 | top: 0; 16 | z-index: defs.$z-index-global-header; 17 | 18 | &_inner { 19 | @include defs.responsiveContainer(); 20 | 21 | box-sizing: border-box; 22 | margin-left: auto; 23 | margin-right: auto; 24 | position: relative; 25 | } 26 | &_logo { 27 | display: inline-block; 28 | } 29 | &_spMenuButton { 30 | left: 0; 31 | position: absolute; 32 | top: 0; 33 | } 34 | } 35 | 36 | .spMenuButton { 37 | align-items: center; 38 | color: inherit; 39 | display: flex; 40 | height: defs.$global-header-height; 41 | justify-content: center; 42 | width: defs.$global-header-height; 43 | } 44 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_hljs.scss: -------------------------------------------------------------------------------- 1 | $prefix: 'hljs'; 2 | 3 | .#{$prefix}-attr { 4 | color: var(--fg-color); 5 | } 6 | .#{$prefix}-comment { 7 | color: var(--hl-comment); 8 | } 9 | .#{$prefix}-keyword { 10 | color: var(--hl-keyword); 11 | } 12 | .#{$prefix}-literal, 13 | .#{$prefix}-number { 14 | color: var(--hl-constant); 15 | } 16 | .#{$prefix}-string { 17 | color: var(--hl-string); 18 | } 19 | .#{$prefix}-tag { 20 | color: var(--hl-keyword); 21 | } 22 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_main.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .main { 4 | overflow: hidden; 5 | 6 | &_globalHeader { 7 | @include defs.wide() { 8 | display: none; 9 | } 10 | @include defs.nonwide() { 11 | height: defs.$global-header-height; 12 | } 13 | @media print { 14 | display: none; 15 | } 16 | } 17 | &_wrap { 18 | @include defs.responsiveContainer(); 19 | 20 | @include defs.wide() { 21 | display: flex; 22 | margin-top: 32px; 23 | } 24 | } 25 | &_menu { 26 | @include defs.wide() { 27 | margin-right: 32px; 28 | width: defs.$global-nav-width; 29 | } 30 | @include defs.nonwide() { 31 | margin-right: 0; 32 | width: 0; 33 | } 34 | @media print { 35 | display: none; 36 | } 37 | } 38 | &_main { 39 | flex: 1; 40 | 41 | @include defs.wide() { 42 | left: defs.$global-nav-width; 43 | overflow: hidden; 44 | top: 0; 45 | } 46 | @include defs.nonwide() { 47 | left: 0; 48 | top: defs.$global-header-height; 49 | } 50 | } 51 | &_pageHeader, 52 | &_inner { 53 | @include defs.document(); 54 | 55 | flex: 1; 56 | 57 | h1 + p { 58 | margin-top: 1em; 59 | padding-left: 0.1em; 60 | padding-right: 0.1em; 61 | } 62 | } 63 | &_pageHeader { 64 | @include defs.middle() { 65 | margin-left: -32px; 66 | margin-right: -32px; 67 | } 68 | @include defs.narrow() { 69 | margin-left: -16px; 70 | margin-right: -16px; 71 | } 72 | } 73 | &_inner { 74 | @include defs.wide() { 75 | padding-bottom: 64px; 76 | padding-top: 64px; 77 | } 78 | @include defs.nonwide() { 79 | padding-bottom: 32px; 80 | padding-top: 32px; 81 | } 82 | } 83 | &_media { 84 | margin-bottom: 1.5em; 85 | margin-top: 1.5em; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_migration.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .comparison { 4 | & > *:first-child { 5 | border-top-left-radius: 6px; 6 | border-top-right-radius: 6px; 7 | } 8 | & > *:last-child { 9 | border-bottom-left-radius: 6px; 10 | border-bottom-right-radius: 6px; 11 | } 12 | 13 | @include defs.nonwide() { 14 | overflow-x: auto; 15 | } 16 | 17 | &_codes, 18 | &_results, 19 | &_texts { 20 | background-color: var(--bg-color-secondary); 21 | display: flex; 22 | 23 | @include defs.nonwide() { 24 | min-width: 640px; 25 | } 26 | } 27 | &_codes { 28 | align-items: stretch; 29 | } 30 | &_code { 31 | display: flex; 32 | flex: 1; 33 | overflow-x: auto; 34 | } 35 | &_result { 36 | @include defs.dotGrid(); 37 | 38 | flex: 1; 39 | padding-bottom: 32px; 40 | padding-top: 32px; 41 | } 42 | &_text { 43 | display: flex; 44 | flex: 1; 45 | justify-content: center; 46 | padding: 32px; 47 | } 48 | &_code + &_code, 49 | &_result + &_result, 50 | &_text + &_text { 51 | border-left: var(--bg-color) solid 1px; 52 | } 53 | } 54 | 55 | // dat.GUI 56 | .datgui { 57 | .dg { 58 | li + li { 59 | margin-top: 0; 60 | } 61 | li.save-row .button { 62 | margin-left: 4px; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_photo-credit.scss: -------------------------------------------------------------------------------- 1 | .photoCredit { 2 | bottom: 8px; 3 | color: rgba(white, 0.5); 4 | font-size: 0.6rem; 5 | mix-blend-mode: exclusion; 6 | position: absolute; 7 | right: 8px; 8 | 9 | a { 10 | color: currentColor; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_rel.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .rel { 4 | display: grid; 5 | margin-bottom: 1em; 6 | margin-top: 1em; 7 | 8 | @include defs.wide() { 9 | gap: 16px; 10 | grid-template-columns: repeat(2, 1fr); 11 | } 12 | @include defs.nonwide() { 13 | gap: 8px; 14 | grid-template-columns: repeat(1, 1fr); 15 | } 16 | } 17 | .relItem { 18 | align-items: stretch; 19 | display: flex; 20 | 21 | a#{&}_anchor { 22 | align-items: center; 23 | background-color: var(--bg-color-action); 24 | border-radius: 6px; 25 | color: var(--fg-color); 26 | display: flex; 27 | flex: 1; 28 | padding: 24px 32px; 29 | text-decoration: none; 30 | 31 | &:focus, 32 | &:hover { 33 | background-color: var(--bg-color-action-active); 34 | } 35 | } 36 | &_icon { 37 | align-items: center; 38 | border: transparent solid 2px; 39 | border-radius: 50%; 40 | display: flex; 41 | height: 2em; 42 | justify-content: center; 43 | opacity: 0.5; 44 | width: 2em; 45 | 46 | &#{&}-circle { 47 | border-color: currentColor; 48 | } 49 | } 50 | &_text { 51 | display: flex; 52 | flex: 1; 53 | flex-direction: column; 54 | } 55 | &_icon + &_text, 56 | &_text + &_icon { 57 | margin-left: 32px; 58 | } 59 | &#{&}-oneline a#{&}_anchor { 60 | justify-content: center; 61 | } 62 | &_title { 63 | font-weight: 500; 64 | } 65 | &_detail { 66 | color: var(--fg-color-secondary); 67 | line-height: 1.3; 68 | margin-top: 8px; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_sponsor.scss: -------------------------------------------------------------------------------- 1 | ul.sponsors { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, 128px); 4 | gap: 32px; 5 | padding-left: 0; 6 | 7 | li { 8 | list-style-type: none; 9 | } 10 | li + li { 11 | margin-top: 0; 12 | } 13 | } 14 | 15 | .sponsor { 16 | a#{&}_anchor { 17 | color: inherit; 18 | display: block; 19 | text-decoration: none; 20 | 21 | &:hover { 22 | text-decoration: underline; 23 | } 24 | } 25 | &_image { 26 | background-color: var(--bg-color-action); 27 | border-radius: 50%; 28 | height: 80px; 29 | margin-left: auto; 30 | margin-right: auto; 31 | overflow: hidden; 32 | position: relative; 33 | width: 80px; 34 | 35 | img { 36 | display: block; 37 | height: 100%; 38 | left: 0; 39 | position: absolute; 40 | top: 0; 41 | width: 100%; 42 | } 43 | } 44 | a#{&}_anchor:hover &_image { 45 | background-color: var(--bg-color-action-active); 46 | 47 | img { 48 | filter: brightness(1.1) saturate(1.1); 49 | } 50 | } 51 | &_placeholder { 52 | left: 50%; 53 | margin-left: -12px; 54 | margin-top: -12px; 55 | position: absolute; 56 | top: 50%; 57 | } 58 | &_name { 59 | margin-top: 16px; 60 | text-align: center; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/sass/components/_theme-builder.scss: -------------------------------------------------------------------------------- 1 | @use '../defs'; 2 | 3 | .paint { 4 | background-color: var(--bg-color-secondary); 5 | border-radius: 6px; 6 | overflow: hidden; 7 | 8 | &_gui { 9 | border-bottom: var(--bg-color) solid 2px; 10 | 11 | @include defs.wide() { 12 | display: flex; 13 | } 14 | } 15 | &_controller { 16 | --tp-base-border-radius: 0px; 17 | --tp-base-shadow-color: transparent; 18 | background-color: var(--bg-color-secondary); 19 | 20 | @include defs.nonwide() { 21 | --tp-blade-value-width: 60vw; 22 | 23 | .paneContainer { 24 | width: 100%; 25 | } 26 | } 27 | } 28 | &_preview { 29 | align-items: center; 30 | display: flex; 31 | flex: 1; 32 | justify-content: center; 33 | overflow: hidden; 34 | padding: 32px; 35 | position: relative; 36 | 37 | .paneContainer { 38 | position: static; 39 | } 40 | .tp-rotv { 41 | position: relative; 42 | } 43 | } 44 | &_bgImage { 45 | background-color: black; 46 | background: url(https://source.unsplash.com/74ft3aciAY0/900x1600) no-repeat 47 | center; 48 | background-size: cover; 49 | inset: -16px; 50 | position: absolute; 51 | 52 | &::before { 53 | background-image: radial-gradient( 54 | rgba(255, 255, 255, 0.1) 1px, 55 | transparent 0 56 | ); 57 | background-position: center; 58 | background-size: 16px 16px; 59 | content: ''; 60 | inset: 0; 61 | position: absolute; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/template/migration/index.html: -------------------------------------------------------------------------------- 1 | {% set pageId = 'migration' %} 2 | {% set root = '../' %} 3 | {% set title = 'Migration' %} 4 | {% extends "_template.html" %} 5 | 6 | 7 | {% block pageHeader %} 8 |

16 | {% endblock %} 17 | 18 | 19 | {% block content %} 20 |

Major versions

21 | 24 | 25 |

Other libraries

26 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/template/partial/_global-footer.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/template/partial/_global-header.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/plugins/counter/bundle.ts: -------------------------------------------------------------------------------- 1 | import {TpPluginBundle} from '@tweakpane/core'; 2 | 3 | import {CounterInputPlugin} from './plugin'; 4 | 5 | export const CounterPluginBundle: TpPluginBundle = { 6 | // Identifier of the plugin bundle 7 | id: 'counter', 8 | // Plugins that should be registered 9 | plugins: [CounterInputPlugin], 10 | 11 | // Additional CSS for this bundle 12 | css: `.tp-counter { 13 | align-items: center; 14 | display: flex; 15 | } 16 | .tp-counter div { 17 | color: #00ffd680; 18 | flex: 1; 19 | } 20 | .tp-counter button { 21 | background-color: #00ffd6c0; 22 | border-radius: 2px; 23 | color: black; 24 | height: 20px; 25 | width: 20px; 26 | }`, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/plugins/counter/controller.ts: -------------------------------------------------------------------------------- 1 | import {Value, ValueController, ViewProps} from '@tweakpane/core'; 2 | 3 | import {CounterView} from './view'; 4 | 5 | interface Config { 6 | value: Value; 7 | viewProps: ViewProps; 8 | } 9 | 10 | export class CounterController implements ValueController { 11 | public readonly value: Value; 12 | public readonly view: CounterView; 13 | public readonly viewProps: ViewProps; 14 | 15 | constructor(doc: Document, config: Config) { 16 | // Models 17 | this.value = config.value; 18 | this.viewProps = config.viewProps; 19 | 20 | // Create a view 21 | this.view = new CounterView(doc, { 22 | value: config.value, 23 | viewProps: this.viewProps, 24 | }); 25 | 26 | // Handle user interaction 27 | this.view.buttonElement.addEventListener('click', () => { 28 | // Update a model 29 | this.value.rawValue += 1; 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/plugins/counter/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseInputParams, 3 | BindingTarget, 4 | createPlugin, 5 | InputBindingPlugin, 6 | } from '@tweakpane/core'; 7 | 8 | import {CounterController} from './controller'; 9 | 10 | type CounterParams = BaseInputParams; 11 | 12 | export const CounterInputPlugin: InputBindingPlugin< 13 | number, 14 | number, 15 | CounterParams 16 | > = createPlugin({ 17 | id: 'counter', 18 | type: 'input', 19 | accept(value: unknown, params: Record) { 20 | if (typeof value !== 'number') { 21 | return null; 22 | } 23 | if (params.view !== 'counter') { 24 | return null; 25 | } 26 | return { 27 | initialValue: value, 28 | params: params, 29 | }; 30 | }, 31 | binding: { 32 | reader: () => (value: unknown) => Number(value), 33 | writer: () => (target: BindingTarget, value: number) => { 34 | target.write(value); 35 | }, 36 | }, 37 | controller(args) { 38 | return new CounterController(args.document, { 39 | value: args.value, 40 | viewProps: args.viewProps, 41 | }); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/plugins/counter/view.ts: -------------------------------------------------------------------------------- 1 | import {Value, View, ViewProps} from '@tweakpane/core'; 2 | 3 | interface Config { 4 | value: Value; 5 | viewProps: ViewProps; 6 | } 7 | 8 | export class CounterView implements View { 9 | public readonly element: HTMLElement; 10 | public readonly buttonElement: HTMLButtonElement; 11 | 12 | constructor(doc: Document, config: Config) { 13 | // Create view elements 14 | this.element = doc.createElement('div'); 15 | this.element.classList.add('tp-counter'); 16 | 17 | // Apply value changes to the preview element 18 | const previewElem = doc.createElement('div'); 19 | const value = config.value; 20 | value.emitter.on('change', () => { 21 | previewElem.textContent = String(value.rawValue); 22 | }); 23 | previewElem.textContent = String(value.rawValue); 24 | this.element.appendChild(previewElem); 25 | 26 | // Create a button element for user interaction 27 | const buttonElem = doc.createElement('button'); 28 | buttonElem.textContent = '+'; 29 | this.element.appendChild(buttonElem); 30 | this.buttonElement = buttonElem; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/preset-test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import {presetToState, stateToPreset} from './preset.js'; 5 | 6 | describe(stateToPreset.name, () => { 7 | it('should convert state into preset', () => { 8 | const preset = stateToPreset({ 9 | disabled: false, 10 | children: [ 11 | {binding: {key: 'foo', value: 1}, disabled: false}, 12 | {disabled: false}, 13 | {binding: {key: 'bar', value: 'text'}, disabled: false}, 14 | { 15 | children: [{binding: {key: 'baz', value: true}, disabled: false}], 16 | }, 17 | ], 18 | }); 19 | assert.deepStrictEqual(preset, { 20 | foo: 1, 21 | bar: 'text', 22 | baz: true, 23 | }); 24 | }); 25 | }); 26 | 27 | describe(presetToState.name, () => { 28 | it('should convert preset into state', () => { 29 | const state = presetToState( 30 | { 31 | disabled: false, 32 | children: [ 33 | {binding: {key: 'foo', value: 0}, disabled: false}, 34 | {disabled: false}, 35 | {binding: {key: 'bar', value: ''}, disabled: false}, 36 | { 37 | children: [{binding: {key: 'baz', value: false}, disabled: false}], 38 | }, 39 | ], 40 | }, 41 | { 42 | foo: 1, 43 | bar: 'text', 44 | baz: true, 45 | }, 46 | ); 47 | assert.deepStrictEqual(state, { 48 | disabled: false, 49 | children: [ 50 | {binding: {key: 'foo', value: 1}, disabled: false}, 51 | {disabled: false}, 52 | {binding: {key: 'bar', value: 'text'}, disabled: false}, 53 | { 54 | children: [{binding: {key: 'baz', value: true}, disabled: false}], 55 | }, 56 | ], 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/preset.ts: -------------------------------------------------------------------------------- 1 | import {BladeState} from '@tweakpane/core'; 2 | 3 | export interface PresetObject { 4 | [key: string]: unknown; 5 | } 6 | 7 | export function stateToPreset(state: BladeState): PresetObject { 8 | // Container 9 | if ('children' in state && Array.isArray(state.children)) { 10 | return state.children.reduce((tmp: PresetObject, substate) => { 11 | return { 12 | ...tmp, 13 | ...stateToPreset(substate), 14 | }; 15 | }, {}); 16 | } 17 | // Binding 18 | const binding = (state.binding ?? {}) as Record; 19 | if ( 20 | 'key' in binding && 21 | typeof binding.key === 'string' && 22 | 'value' in binding 23 | ) { 24 | return { 25 | [binding.key]: binding.value, 26 | }; 27 | } 28 | return {}; 29 | } 30 | 31 | export function presetToState( 32 | state: BladeState, 33 | preset: PresetObject, 34 | ): BladeState { 35 | // Container 36 | if ('children' in state && Array.isArray(state.children)) { 37 | return { 38 | ...state, 39 | children: state.children.map((substate) => 40 | presetToState(substate, preset), 41 | ), 42 | }; 43 | } 44 | // Binding 45 | const binding = (state.binding ?? {}) as Record; 46 | if ( 47 | 'key' in binding && 48 | typeof binding.key === 'string' && 49 | 'value' in binding 50 | ) { 51 | return { 52 | ...state, 53 | binding: { 54 | ...binding, 55 | value: preset[binding.key] ?? state.value, 56 | }, 57 | }; 58 | } 59 | return state; 60 | } 61 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/route/getting-started.ts: -------------------------------------------------------------------------------- 1 | import {Pane} from 'tweakpane'; 2 | 3 | import {selectContainer} from '../util.js'; 4 | 5 | export function initGettingStarted() { 6 | const markerToFnMap: { 7 | [key: string]: (container: HTMLElement) => void; 8 | } = { 9 | hello: (container) => { 10 | new Pane({ 11 | container: container, 12 | }); 13 | }, 14 | }; 15 | Object.keys(markerToFnMap).forEach((marker) => { 16 | const initFn = markerToFnMap[marker]; 17 | const container = selectContainer(marker); 18 | initFn(container); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/screw.ts: -------------------------------------------------------------------------------- 1 | export class Screw { 2 | elem_: HTMLElement; 3 | 4 | constructor(elem: HTMLElement) { 5 | this.onWindowScroll_ = this.onWindowScroll_.bind(this); 6 | 7 | this.elem_ = elem; 8 | 9 | window.addEventListener('scroll', this.onWindowScroll_); 10 | } 11 | 12 | onWindowScroll_() { 13 | const angle = window.scrollY * 0.5; 14 | this.elem_.style.transform = `rotate(${angle}deg)`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/simple-router.ts: -------------------------------------------------------------------------------- 1 | type Matcher = (pathname: string) => boolean; 2 | 3 | interface Route { 4 | init: () => void; 5 | matcher: Matcher; 6 | } 7 | 8 | export class SimpleRouter { 9 | private routes_: Route[]; 10 | 11 | constructor() { 12 | this.routes_ = []; 13 | } 14 | 15 | public add(matcher: RegExp | Matcher, callback: () => void): void { 16 | this.routes_.push({ 17 | init: callback, 18 | matcher: 19 | matcher instanceof RegExp 20 | ? (pathname: string): boolean => { 21 | return matcher.test(pathname); 22 | } 23 | : matcher, 24 | }); 25 | } 26 | 27 | public route(pathname: string): void { 28 | this.routes_.forEach((route) => { 29 | if (route.matcher(pathname)) { 30 | route.init(); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/ts/util.ts: -------------------------------------------------------------------------------- 1 | export function selectContainer(marker: string, console = false): HTMLElement { 2 | const postfix = marker + (console ? 'console' : ''); 3 | const selector = `*[data-pane-${postfix}]`; 4 | const elem = document.querySelector(selector); 5 | if (!elem) { 6 | throw Error(`container not found: ${selector}`); 7 | } 8 | return elem as HTMLElement; 9 | } 10 | 11 | export function wave(t: number): number { 12 | const p = t * 0.02; 13 | return ( 14 | ((3 * 4) / Math.PI) * 15 | (Math.sin(p * 1 * Math.PI) + 16 | Math.sin(p * 3 * Math.PI) / 3 + 17 | Math.sin(p * 5 * Math.PI) / 5) * 18 | 0.25 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "moduleResolution": "Node", 6 | "paths": { 7 | "@tweakpane/core": ["../../../core/src/index"], 8 | "tweakpane": ["../main/ts/index"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/tweakpane/src/doc/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": [ 3 | "../../", 4 | ], 5 | "entryPointStrategy": "packages", 6 | "exclude": [ 7 | "**/*.d.ts", 8 | "**/*-test.ts" 9 | ], 10 | "excludePrivate": true, 11 | "intentionallyNotExported": [ 12 | "BindingController", 13 | "BindingValue", 14 | "BladeController", 15 | "InputBindingController", 16 | "MonitorBindingController" 17 | ], 18 | "out": "../../docs/api", 19 | "plugin": ["typedoc-plugin-missing-exports"], 20 | "readme": "none" 21 | } -------------------------------------------------------------------------------- /packages/tweakpane/src/main/sass/bundle.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../node_modules/@tweakpane/core/lib/sass/view/views'; 2 | 3 | @use './view/root'; 4 | @use './view/separator'; 5 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/sass/view/_separator.scss: -------------------------------------------------------------------------------- 1 | @use '../../../../../../node_modules/@tweakpane/core/lib/sass/tp'; 2 | 3 | .#{tp.$prefix}-sprv { 4 | &_r { 5 | background-color: tp.cssVar('groove-fg'); 6 | border-width: 0; 7 | display: block; 8 | height: tp.$separator-width; 9 | margin: 0; 10 | width: 100%; 11 | } 12 | &.#{tp.$disabled} &_r { 13 | opacity: 0.5; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/root/api/root.ts: -------------------------------------------------------------------------------- 1 | import {FolderApi, PluginPool} from '@tweakpane/core'; 2 | 3 | import {RootController} from '../controller/root.js'; 4 | 5 | export class RootApi extends FolderApi { 6 | /** 7 | * @hidden 8 | */ 9 | constructor(controller: RootController, pool: PluginPool) { 10 | super(controller, pool); 11 | } 12 | 13 | get element(): HTMLElement { 14 | return this.controller.view.element; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/root/controller/root.ts: -------------------------------------------------------------------------------- 1 | import {Blade, FolderController, FolderProps, ViewProps} from '@tweakpane/core'; 2 | 3 | /** 4 | * @hidden 5 | */ 6 | interface Config { 7 | blade: Blade; 8 | props: FolderProps; 9 | viewProps: ViewProps; 10 | 11 | expanded?: boolean; 12 | title?: string; 13 | } 14 | 15 | /** 16 | * @hidden 17 | */ 18 | export class RootController extends FolderController { 19 | constructor(doc: Document, config: Config) { 20 | super(doc, { 21 | expanded: config.expanded, 22 | blade: config.blade, 23 | props: config.props, 24 | root: true, 25 | viewProps: config.viewProps, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/api/separator-test.ts: -------------------------------------------------------------------------------- 1 | import {createBlade, ViewProps} from '@tweakpane/core'; 2 | import {describe, it} from 'mocha'; 3 | 4 | import { 5 | assertInitialState, 6 | assertUpdates, 7 | createTestWindow, 8 | } from '../../../misc/test-util.js'; 9 | import {SeparatorController} from '../controller/separator.js'; 10 | import {SeparatorBladeApi} from './separator.js'; 11 | 12 | describe(SeparatorBladeApi.name, () => { 13 | it('should have initial state', () => { 14 | const doc = createTestWindow().document; 15 | const c = new SeparatorController(doc, { 16 | blade: createBlade(), 17 | viewProps: ViewProps.create(), 18 | }); 19 | const api = new SeparatorBladeApi(c); 20 | assertInitialState(api); 21 | }); 22 | 23 | it('should update properties', () => { 24 | const doc = createTestWindow().document; 25 | const c = new SeparatorController(doc, { 26 | blade: createBlade(), 27 | viewProps: ViewProps.create(), 28 | }); 29 | const api = new SeparatorBladeApi(c); 30 | assertUpdates(api); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/api/separator.ts: -------------------------------------------------------------------------------- 1 | import {BladeApi} from '@tweakpane/core'; 2 | 3 | import {SeparatorController} from '../controller/separator.js'; 4 | 5 | export class SeparatorBladeApi extends BladeApi {} 6 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/controller/separator.ts: -------------------------------------------------------------------------------- 1 | import {Blade, BladeController, ViewProps} from '@tweakpane/core'; 2 | 3 | import {SeparatorView} from '../view/separator.js'; 4 | 5 | /** 6 | * @hidden 7 | */ 8 | interface Config { 9 | blade: Blade; 10 | viewProps: ViewProps; 11 | } 12 | 13 | /** 14 | * @hidden 15 | */ 16 | export class SeparatorController extends BladeController { 17 | /** 18 | * @hidden 19 | */ 20 | constructor(doc: Document, config: Config) { 21 | super({ 22 | ...config, 23 | view: new SeparatorView(doc, { 24 | viewProps: config.viewProps, 25 | }), 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/plugin-test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BladeController, 3 | createBladeController, 4 | createDefaultPluginPool, 5 | } from '@tweakpane/core'; 6 | import * as assert from 'assert'; 7 | import {describe as context, describe, it} from 'mocha'; 8 | 9 | import { 10 | createEmptyBladeController, 11 | createTestWindow, 12 | } from '../../misc/test-util.js'; 13 | import {SeparatorBladePlugin} from './plugin.js'; 14 | 15 | describe(SeparatorBladePlugin.id, () => { 16 | [{}].forEach((params) => { 17 | context(`when ${JSON.stringify(params)}`, () => { 18 | it('should not create API', () => { 19 | const doc = createTestWindow().document; 20 | const api = createBladeController(SeparatorBladePlugin, { 21 | document: doc, 22 | params: params, 23 | }); 24 | assert.strictEqual(api, null); 25 | }); 26 | }); 27 | }); 28 | 29 | it('should be created', () => { 30 | const doc = createTestWindow().document; 31 | const bc = createBladeController(SeparatorBladePlugin, { 32 | document: doc, 33 | params: { 34 | view: 'separator', 35 | }, 36 | }) as BladeController; 37 | const api = SeparatorBladePlugin.api({ 38 | controller: bc, 39 | pool: createDefaultPluginPool(), 40 | }); 41 | assert.notStrictEqual(api, null); 42 | }); 43 | 44 | [(doc: Document) => createEmptyBladeController(doc)].forEach( 45 | (createController) => { 46 | it('should not create API', () => { 47 | const doc = createTestWindow().document; 48 | const c = createController(doc); 49 | const api = SeparatorBladePlugin.api({ 50 | controller: c, 51 | pool: createDefaultPluginPool(), 52 | }); 53 | assert.strictEqual(api, null); 54 | }); 55 | }, 56 | ); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseBladeParams, 3 | BladePlugin, 4 | parseRecord, 5 | VERSION, 6 | } from '@tweakpane/core'; 7 | 8 | import {SeparatorBladeApi} from './api/separator.js'; 9 | import {SeparatorController} from './controller/separator.js'; 10 | 11 | export interface SeparatorBladeParams extends BaseBladeParams { 12 | view: 'separator'; 13 | } 14 | 15 | export const SeparatorBladePlugin: BladePlugin = { 16 | id: 'separator', 17 | type: 'blade', 18 | core: VERSION, 19 | accept(params) { 20 | const result = parseRecord(params, (p) => ({ 21 | view: p.required.constant('separator'), 22 | })); 23 | return result ? {params: result} : null; 24 | }, 25 | controller(args) { 26 | return new SeparatorController(args.document, { 27 | blade: args.blade, 28 | viewProps: args.viewProps, 29 | }); 30 | }, 31 | api(args) { 32 | if (!(args.controller instanceof SeparatorController)) { 33 | return null; 34 | } 35 | return new SeparatorBladeApi(args.controller); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/blade/separator/view/separator.ts: -------------------------------------------------------------------------------- 1 | import {ClassName, View, ViewProps} from '@tweakpane/core'; 2 | 3 | const cn = ClassName('spr'); 4 | 5 | /** 6 | * @hidden 7 | */ 8 | interface Config { 9 | viewProps: ViewProps; 10 | } 11 | 12 | /** 13 | * @hidden 14 | */ 15 | export class SeparatorView implements View { 16 | public readonly element: HTMLElement; 17 | 18 | constructor(doc: Document, config: Config) { 19 | this.element = doc.createElement('div'); 20 | this.element.classList.add(cn()); 21 | config.viewProps.bindClassModifiers(this.element); 22 | 23 | const hrElem = doc.createElement('hr'); 24 | hrElem.classList.add(cn('r')); 25 | this.element.appendChild(hrElem); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | import {Semver} from '@tweakpane/core'; 2 | 3 | export { 4 | ArrayStyleListOptions, 5 | BaseParams, 6 | BaseBladeParams, 7 | BindingApiEvents, 8 | BindingParams, 9 | BladeApi, 10 | BooleanInputParams, 11 | BooleanMonitorParams, 12 | ButtonApi, 13 | ButtonParams, 14 | ColorInputParams, 15 | FolderApi, 16 | FolderParams, 17 | InputBindingApi, 18 | ListInputBindingApi, 19 | ListParamsOptions, 20 | MonitorBindingApi, 21 | NumberInputParams, 22 | NumberMonitorParams, 23 | ObjectStyleListOptions, 24 | Point2dInputParams, 25 | Point3dInputParams, 26 | Point4dInputParams, 27 | Semver, 28 | SliderInputBindingApi, 29 | StringInputParams, 30 | StringMonitorParams, 31 | TabApi, 32 | TabPageApi, 33 | TabPageParams, 34 | TabParams, 35 | TpChangeEvent, 36 | TpPlugin, 37 | TpPluginBundle, 38 | } from '@tweakpane/core'; 39 | 40 | export {ListBladeApi} from './blade/list/api/list.js'; 41 | export {ListBladeParams} from './blade/list/plugin.js'; 42 | export {SeparatorBladeApi} from './blade/separator/api/separator.js'; 43 | export {SeparatorBladeParams} from './blade/separator/plugin.js'; 44 | export {SliderBladeApi} from './blade/slider/api/slider.js'; 45 | export {SliderBladeParams} from './blade/slider/plugin.js'; 46 | export {TextBladeApi} from './blade/text/api/text.js'; 47 | export {TextBladeParams} from './blade/text/plugin.js'; 48 | 49 | export {Pane} from './pane/pane.js'; 50 | 51 | export const VERSION = new Semver('0.0.0-tweakpane.0'); 52 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/pane/blade-test.ts: -------------------------------------------------------------------------------- 1 | import {TpError} from '@tweakpane/core'; 2 | import * as assert from 'assert'; 3 | 4 | import {SliderBladeApi} from '../blade/slider/api/slider.js'; 5 | import {createTestWindow} from '../misc/test-util.js'; 6 | import {Pane} from './pane.js'; 7 | 8 | describe(Pane.name, () => { 9 | it('should apply initial view properties', () => { 10 | const doc = createTestWindow().document; 11 | const pane = new Pane({ 12 | document: doc, 13 | }); 14 | 15 | const i1 = pane.addBlade({ 16 | title: '', 17 | view: 'button', 18 | }); 19 | assert.strictEqual(i1.disabled, false); 20 | assert.strictEqual(i1.hidden, false); 21 | 22 | const i2 = pane.addBlade({ 23 | disabled: true, 24 | hidden: true, 25 | title: '', 26 | view: 'button', 27 | }); 28 | assert.strictEqual(i2.disabled, true); 29 | assert.strictEqual(i2.hidden, true); 30 | }); 31 | 32 | it('should throw `alreadydisposed` error when calling dispose() inside blade change event', (done) => { 33 | const doc = createTestWindow().document; 34 | const pane = new Pane({ 35 | document: doc, 36 | }); 37 | const b = pane.addBlade({ 38 | max: 100, 39 | min: 0, 40 | view: 'slider', 41 | }) as SliderBladeApi; 42 | 43 | try { 44 | b.on('change', () => { 45 | b.dispose(); 46 | }); 47 | b.controller.value.rawValue = 1; 48 | } catch (err) { 49 | assert.strictEqual((err as TpError).type, 'alreadydisposed'); 50 | done(); 51 | } 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/pane/event-test.ts: -------------------------------------------------------------------------------- 1 | import {describe, it} from 'mocha'; 2 | 3 | import {createTestWindow} from '../misc/test-util.js'; 4 | import {Pane} from './pane.js'; 5 | 6 | function createPane(): Pane { 7 | return new Pane({ 8 | document: createTestWindow().document, 9 | title: 'Title', 10 | }); 11 | } 12 | 13 | describe(Pane.name, () => { 14 | it('should listen fold event', (done) => { 15 | const pane = createPane(); 16 | pane.on('fold', () => { 17 | done(); 18 | }); 19 | 20 | const folder = pane.controller.foldable; 21 | if (folder) { 22 | folder.set('expanded', false); 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/ts/pane/pane-config.ts: -------------------------------------------------------------------------------- 1 | export interface PaneConfig { 2 | /** 3 | * The custom container element of the pane. 4 | */ 5 | container?: HTMLElement; 6 | /** 7 | * The default expansion of the pane. 8 | */ 9 | expanded?: boolean; 10 | /** 11 | * The pane title that can expand/collapse the entire pane. 12 | */ 13 | title?: string; 14 | 15 | /** 16 | * @hidden 17 | */ 18 | document?: Document; 19 | } 20 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/tsconfig-dts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "../../dist/types" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tweakpane/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/node/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-env node */ 3 | 4 | import Fs from 'fs'; 5 | import {JSDOM as Jsdom} from 'jsdom'; 6 | // Require default module 7 | import {Pane, VERSION} from 'tweakpane'; 8 | 9 | const Package = JSON.parse( 10 | Fs.readFileSync(new URL('../../package.json', import.meta.url)), 11 | ); 12 | 13 | // Check version 14 | if (VERSION.toString() !== Package.version) { 15 | throw new Error('invalid version'); 16 | } 17 | 18 | const PARAMS = { 19 | foo: 1, 20 | }; 21 | 22 | // Create pane 23 | const doc = new Jsdom('').window.document; 24 | const pane = new Pane({ 25 | document: doc, 26 | }); 27 | 28 | // Add input 29 | const input = pane.addBinding(PARAMS, 'foo', { 30 | max: 1, 31 | min: 0, 32 | step: 1, 33 | }); 34 | console.log(input); 35 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tweakpane-test-module", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "private": true, 7 | "scripts": { 8 | "test": " run-p test:*", 9 | "test:node": "node node/index.js", 10 | "test:sass": "test -f node_modules/@tweakpane/core/lib/sass/_tp.scss", 11 | "test:tsc": "tsc --build --clean tsc/tsconfig.json && tsc --build tsc/tsconfig.json && node tsc/dist/index.js", 12 | "test:plugin": "rollup --config plugin/rollup.config.js && node plugin/test/node.js" 13 | }, 14 | "author": "cocopon", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@rollup/plugin-alias": "^3.1.2", 18 | "@rollup/plugin-node-resolve": "^13.0.0", 19 | "@rollup/plugin-typescript": "^8.2.0", 20 | "@tweakpane/core": "file:../../core/tweakpane-core.tgz", 21 | "@types/jsdom": "^16.2.13", 22 | "jsdom": "^16.7.0", 23 | "npm-run-all": "^4.1.5", 24 | "rollup": "^2.39.0", 25 | "tweakpane": "file:../tweakpane.tgz", 26 | "typescript": "^4.1.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/rollup.config.js: -------------------------------------------------------------------------------- 1 | import Alias from '@rollup/plugin-alias'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import Typescript from '@rollup/plugin-typescript'; 4 | 5 | export default () => { 6 | return { 7 | input: 'plugin/src/index.ts', 8 | external: ['tweakpane'], 9 | output: { 10 | file: 'plugin/dist/bundle.js', 11 | format: 'esm', 12 | globals: { 13 | tweakpane: 'Tweakpane', 14 | }, 15 | name: 'TweakpanePluginExample', 16 | }, 17 | plugins: [ 18 | Alias({ 19 | entries: [ 20 | { 21 | find: '@tweakpane/core', 22 | replacement: './node_modules/@tweakpane/core/dist/index.js', 23 | }, 24 | ], 25 | }), 26 | Typescript({ 27 | tsconfig: 'plugin/tsconfig.json', 28 | }), 29 | nodeResolve(), 30 | ], 31 | 32 | onwarn(warning, warn) { 33 | if (warning.code === 'CIRCULAR_DEPENDENCY') { 34 | return; 35 | } 36 | warn(warning); 37 | }, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | import {TestBladePlugin} from './test-blade'; 2 | import {TestInputPlugin} from './test-input'; 3 | 4 | export const plugins = [TestInputPlugin, TestBladePlugin]; 5 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/src/test-input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseInputParams, 3 | InputBindingPlugin, 4 | parseRecord, 5 | stringFromUnknown, 6 | Value, 7 | ValueController, 8 | VERSION, 9 | ViewProps, 10 | writePrimitive, 11 | } from '@tweakpane/core'; 12 | 13 | import {TestView} from './test-view'; 14 | 15 | class TestController implements ValueController { 16 | public readonly value: Value; 17 | public readonly view: TestView; 18 | public readonly viewProps: ViewProps; 19 | 20 | constructor( 21 | doc: Document, 22 | config: { 23 | value: Value; 24 | viewProps: ViewProps; 25 | }, 26 | ) { 27 | this.value = config.value; 28 | this.viewProps = config.viewProps; 29 | 30 | this.view = new TestView(doc, { 31 | value: this.value, 32 | }); 33 | } 34 | } 35 | 36 | export const TestInputPlugin: InputBindingPlugin< 37 | string, 38 | string, 39 | BaseInputParams 40 | > = { 41 | id: 'input-test', 42 | type: 'input', 43 | core: VERSION, 44 | accept(value, params) { 45 | if (typeof value !== 'string') { 46 | return null; 47 | } 48 | const result = parseRecord(params, (p) => ({ 49 | view: p.required.constant('test'), 50 | })); 51 | return result 52 | ? { 53 | initialValue: value, 54 | params: result, 55 | } 56 | : null; 57 | }, 58 | binding: { 59 | reader: () => stringFromUnknown, 60 | writer: () => writePrimitive, 61 | }, 62 | controller(args) { 63 | return new TestController(args.document, { 64 | value: args.value, 65 | viewProps: args.viewProps, 66 | }); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/src/test-view.ts: -------------------------------------------------------------------------------- 1 | import {ClassName, Value, View} from '@tweakpane/core'; 2 | 3 | export class TestView implements View { 4 | public readonly element: HTMLElement; 5 | 6 | constructor( 7 | doc: Document, 8 | config: { 9 | value: Value; 10 | }, 11 | ) { 12 | this.element = doc.createElement('div'); 13 | this.element.classList.add(ClassName('tst')()); 14 | config.value.emitter.on('change', (ev) => { 15 | this.element.textContent = ev.rawValue; 16 | }); 17 | this.element.textContent = config.value.rawValue; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/test/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/test/node.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-env node */ 3 | import {JSDOM as Jsdom} from 'jsdom'; 4 | import {Pane} from 'tweakpane'; 5 | 6 | import * as TestPlugin from '../dist/bundle.js'; 7 | 8 | const doc = new Jsdom('').window.document; 9 | const params = {foo: 'hello, world'}; 10 | const pane = new Pane({ 11 | document: doc, 12 | }); 13 | pane.registerPlugin(TestPlugin); 14 | 15 | // Create binding 16 | ((b) => { 17 | const elem = b.element.querySelector('.tp-tstv'); 18 | if (!elem) { 19 | throw new Error('custom view not found'); 20 | } 21 | 22 | if (elem.textContent !== params.foo) { 23 | throw new Error('invalid display value'); 24 | } 25 | })( 26 | pane.addBinding(params, 'foo', { 27 | view: 'test', 28 | }), 29 | ); 30 | 31 | // Create blade 32 | ((b) => { 33 | if (!b.element.classList.contains('tp-tstv')) { 34 | throw new Error('custom view not found'); 35 | } 36 | })( 37 | pane.addBlade({ 38 | value: 'foo', 39 | view: 'test', 40 | }), 41 | ); 42 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["DOM", "ES2015"], 5 | "strict": true, 6 | "target": "ES6" 7 | }, 8 | "include": ["src/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/tweakpane/test-module/tsc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "lib": ["DOM", "ES2015"], 5 | "module": "node16", 6 | "outDir": "dist", 7 | "strict": true, 8 | "target": "ES5" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: false, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | useTabs: true, 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2015"], 4 | "module": "node16", 5 | "noImplicitAny": true, 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "strict": true, 9 | "target": "ES6" 10 | } 11 | } --------------------------------------------------------------------------------