├── src ├── styles │ ├── host.scss │ ├── _reset.scss │ ├── _utils.scss │ ├── styles.scss │ ├── _input_text.scss │ ├── _switch.scss │ ├── _input_radio.scss │ ├── _positions.scss │ ├── host │ │ └── _color-picker.scss │ ├── _range.scss │ ├── _control.scss │ ├── _select.scss │ ├── _input_color.scss │ ├── _toggleIcon.scss │ ├── _knob.scss │ └── _knobs.scss ├── utils │ ├── isObject.js │ ├── isModernBrowser.js │ ├── getKnobsGroups.js │ ├── parseHTML.js │ ├── mergeDeep.js │ ├── extend.js │ └── EventDispatcher.js ├── defaults.js ├── persist.js ├── cloneKnobs.js ├── templates.js ├── events.js └── knobs.js ├── .gitignore ├── demo.apng ├── demo1.apng ├── rollup.config.prod.mjs ├── rollup.config.dev.mjs ├── TODO.md ├── package.json ├── index.html ├── README.md ├── LICENSE └── knobs.min.js /src/styles/host.scss: -------------------------------------------------------------------------------- 1 | @import './host/color-picker'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | TODO.md 4 | .vscode -------------------------------------------------------------------------------- /demo.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yairEO/knobs/HEAD/demo.apng -------------------------------------------------------------------------------- /demo1.apng: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yairEO/knobs/HEAD/demo1.apng -------------------------------------------------------------------------------- /src/utils/isObject.js: -------------------------------------------------------------------------------- 1 | export default obj => (obj+"") === "[object Object]" 2 | -------------------------------------------------------------------------------- /src/utils/isModernBrowser.js: -------------------------------------------------------------------------------- 1 | export default () => window.CSS && CSS.supports('top', 'var(--a)') 2 | -------------------------------------------------------------------------------- /src/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | label, button, input{ cursor:pointer; font:12px Arial, sans-serif; } 2 | 3 | body, form{ padding:0; margin:0; } 4 | 5 | -------------------------------------------------------------------------------- /src/styles/_utils.scss: -------------------------------------------------------------------------------- 1 | [css-util-before]::before{ 2 | content: ''; 3 | opacity: .2; 4 | position: absolute; 5 | top:0; right:0; bottom:0; left:0; 6 | } -------------------------------------------------------------------------------- /src/utils/getKnobsGroups.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export default knobs => knobs.reduce((acc, knobData) => { 4 | if( !isObject(knobData ) && acc[acc.length - 1].length ) acc.push([]) 5 | acc[acc.length - 1].push(knobData) 6 | return acc 7 | }, [[]]) 8 | -------------------------------------------------------------------------------- /src/utils/parseHTML.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utility method 3 | * https://stackoverflow.com/a/35385518/104380 4 | * @param {String} s [HTML string] 5 | * @return {Object} [DOM node] 6 | */ 7 | export default function( s ){ 8 | var parser = new DOMParser(), 9 | node = parser.parseFromString(s.trim(), "text/html"); 10 | 11 | return node.body.firstElementChild; 12 | } -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import './reset'; 2 | @import './utils'; 3 | @import './knobs'; 4 | @import './knob'; 5 | @import './toggleIcon'; 6 | @import './positions'; 7 | @import './select'; 8 | @import './range'; // overrides 9 | @import '@yaireo/ui-range/ui-range.scss'; 10 | @import './switch'; // overrides 11 | @import '@yaireo/ui-switch/src/switch.scss'; 12 | @import './control'; 13 | @import './input_color'; 14 | @import './input_text'; 15 | @import './input_radio'; -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | export default { 2 | visible: 0, 3 | live: true, 4 | theme: { 5 | flow: 'horizontal', 6 | styles: '', 7 | RTL: false, 8 | position: 'top right', 9 | primaryColor: '#0366D6', 10 | "range-value-background": '#FFF', 11 | "base-color": '#000', // (HS)L - the lightness is defined by default "--base-color-l: 0%" 12 | // "base-color-l": '0%', // HSL lightness" 13 | // background: "rgba(0,0,0,1)", 14 | textColor: "white", 15 | border: 'none', 16 | } 17 | } -------------------------------------------------------------------------------- /src/styles/_input_text.scss: -------------------------------------------------------------------------------- 1 | /* wrapper for "color" inputs */ 2 | label[data-type="text"], 3 | label[data-type="number"]{ 4 | input{ 5 | cursor: text; 6 | padding: 5px; 7 | border-radius: 3px; 8 | color: var(--textColor); 9 | outline: none; 10 | border: 0; 11 | background: var(--opaqueColor-15); 12 | 13 | &:focus{ 14 | box-shadow: 0 0 0 1px HSL(var(--base-color), calc(var(--base-color-l) + 22%)); 15 | } 16 | 17 | &:invalid{ 18 | box-shadow: 0 0 0 1px #d75d4a inset; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/mergeDeep.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | const mergeDeep = (target, ...sources) => { 4 | if (!sources.length) return target; 5 | const source = sources.shift(); 6 | 7 | if (isObject(target) && isObject(source)) { 8 | for (const key in source) { 9 | if (isObject(source[key])) { 10 | if (!target[key]) Object.assign(target, { [key]: {} }); 11 | mergeDeep(target[key], source[key]); 12 | } else { 13 | Object.assign(target, { [key]: source[key] }); 14 | } 15 | } 16 | } 17 | 18 | return mergeDeep(target, ...sources); 19 | } 20 | 21 | export default mergeDeep; 22 | -------------------------------------------------------------------------------- /src/styles/_switch.scss: -------------------------------------------------------------------------------- 1 | .knobs{ 2 | &[data-flow='compact']{ 3 | .switch{ 4 | --size: 10px; 5 | --thumb-scale: 1.3; 6 | 7 | &__gfx{ 8 | padding: 0; 9 | } 10 | } 11 | } 12 | 13 | .switch{ 14 | --color-bg: #444; 15 | --color-bg-on: #444; 16 | --thumb-color-off: #d75d4a; 17 | --thumb-color-on: #4ec964; 18 | --thumb-scale: 1.1; 19 | --width-multiplier: 2.5; 20 | --thumb-animation-pad: 15%; 21 | --size: 1em; 22 | 23 | .switch__gfx{ 24 | background: none; 25 | border: 1px solid var(--bg, var(--color-bg)); 26 | } 27 | 28 | input:focus + div{ outline: none; } 29 | } 30 | } -------------------------------------------------------------------------------- /src/utils/extend.js: -------------------------------------------------------------------------------- 1 | import isObject from './isObject' 2 | 3 | export function extend( o, o1, o2) { 4 | if( !(o instanceof Object) ) o = {}; 5 | 6 | copy(o, o1); 7 | if( o2 ) 8 | copy(o, o2) 9 | 10 | function copy(a,b){ 11 | // copy o2 to o 12 | for( var key in b ) 13 | if( b.hasOwnProperty(key) ){ 14 | if( isObject(b[key]) ){ 15 | if( !isObject(a[key]) ) 16 | a[key] = Object.assign({}, b[key]); 17 | else 18 | copy(a[key], b[key]) 19 | } 20 | else 21 | a[key] = b[key]; 22 | } 23 | } 24 | 25 | return o; 26 | } -------------------------------------------------------------------------------- /src/styles/_input_radio.scss: -------------------------------------------------------------------------------- 1 | label[data-type="radio"]{ 2 | > .knobs__knob__inputWrap{ 3 | display: flex; 4 | gap: var(--radio-group-gap, 1.5em); 5 | align-items: center; 6 | justify-content: flex-end; 7 | 8 | > label{ 9 | flex: 0; 10 | display: inline-flex; 11 | gap: .5em; 12 | align-items: center; 13 | 14 | &:hover{ 15 | > *:not(input){ 16 | opacity: .7; 17 | } 18 | } 19 | 20 | input{ 21 | margin: 0; 22 | 23 | ~ * { 24 | opacity: .5; 25 | transition: .25s; 26 | } 27 | 28 | &:checked ~ * { 29 | opacity: 1; 30 | transition: 0s; 31 | } 32 | } 33 | } 34 | 35 | svg{ 36 | fill: var(--textColor); 37 | height: 20px; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/styles/_positions.scss: -------------------------------------------------------------------------------- 1 | #knobsToggle:checked + .knobs{ 2 | &[data-position~='top']{ 3 | .knobs__bg{ 4 | bottom: auto; 5 | } 6 | } 7 | 8 | &[data-position~='right']{ 9 | .knobs__bg{ 10 | left: auto; 11 | } 12 | } 13 | 14 | 15 | 16 | &[data-position~='bottom']{ 17 | > label{ 18 | top: auto; 19 | bottom: var(--offset); 20 | } 21 | 22 | .knobs__bg{ 23 | top: auto; 24 | } 25 | } 26 | 27 | 28 | 29 | &[data-position~='left']{ 30 | > label{ 31 | right: auto; 32 | left: var(--offset); 33 | } 34 | 35 | .knobs__bg{ 36 | right: auto; 37 | } 38 | 39 | &[data-position~='bottom']{ 40 | --control-left-pad: var(--toggleSize); 41 | } 42 | } 43 | } 44 | 45 | .knobs[data-position~='left']{ 46 | --LTR-Bool: -1; 47 | } -------------------------------------------------------------------------------- /src/styles/host/_color-picker.scss: -------------------------------------------------------------------------------- 1 | .color-picker{ 2 | h1{ 3 | margin: 0 0 .5em; 4 | font-size: 1em; 5 | text-align: center; 6 | } 7 | &.hidden{ 8 | opacity: 0; 9 | pointer-events: none; 10 | } 11 | } 12 | 13 | .color-picker[positioned]{ 14 | --x: calc(var(--pos-left) + var(--window-scroll-x)); 15 | --y: calc(var(--pos-top) + var(--window-scroll-y)); 16 | position: absolute; 17 | z-index: 9999991; 18 | border-radius: 10px; 19 | padding: .5em; 20 | box-shadow: 0 5px 20px #00000044; 21 | backdrop-filter: blur(3px); 22 | background: white; 23 | top: 0; 24 | left: 0; 25 | transform: translate(calc(var(--x) * 1px), 26 | calc(var(--y) * 1px)); 27 | 28 | @media only screen and (max-device-width : 640px) { 29 | max-width: 70%; 30 | } 31 | 32 | &:not(.hidden) ~ .knobsIframe{ 33 | pointer-events: none; 34 | filter: blur(2px); 35 | } 36 | } -------------------------------------------------------------------------------- /src/styles/_range.scss: -------------------------------------------------------------------------------- 1 | // https://github.com/yairEO/ui-range 2 | // overrides: 3 | .knobs .range-slider { 4 | --fill-color: var(--range-track-color); 5 | --primaryColor: var(--range-value-background); 6 | 7 | --value-active-color: var(--range-track-color); 8 | --value-background: transparent; 9 | --value-background-hover: white; 10 | --value-offset-y: 9px; 11 | 12 | --progress-background: #444; 13 | --thumb-size: 14px; 14 | --track-height: calc(var(--thumb-size)/3); 15 | --ticks-thickness: 1px; 16 | --ticks-height: 0px; 17 | --show-min-max: none; 18 | --thumb-color: var(--range-track-color); 19 | --thumb-shadow: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/6) inset white; 20 | --thumb-shadow-active: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/4) inset white; 21 | 22 | color: transparent; 23 | 24 | > input:hover + output { 25 | box-shadow: 0 0 0 3px var(--value-background), 26 | 0 0 6px 4px var(--background); 27 | } 28 | } -------------------------------------------------------------------------------- /rollup.config.prod.mjs: -------------------------------------------------------------------------------- 1 | import scss from 'rollup-plugin-scss' 2 | import babel from '@rollup/plugin-babel' 3 | import terser from '@rollup/plugin-terser'; 4 | import cleanup from 'rollup-plugin-cleanup' 5 | import { nodeResolve } from '@rollup/plugin-node-resolve' 6 | import commonjs from '@rollup/plugin-commonjs' 7 | import {readFileSync} from 'fs' 8 | 9 | const pkg = JSON.parse(readFileSync('./package.json')) 10 | const banner = `/*! Knobs ${pkg.version} MIT | https://github.com/yairEO/knobs */\n`; 11 | 12 | export default [ 13 | { 14 | input: 'src/knobs.js', 15 | output: { 16 | banner, 17 | file: 'knobs.min.js', 18 | format: 'umd', 19 | name: 'Knobs', 20 | }, 21 | plugins: [ 22 | terser(), 23 | babel({ babelHelpers: 'bundled' }), 24 | scss({ output: false, outputStyle: 'compressed', watch: 'src/styles', }), 25 | cleanup(), 26 | nodeResolve(), 27 | commonjs() 28 | ] 29 | } 30 | ] 31 | 32 | -------------------------------------------------------------------------------- /src/styles/_control.scss: -------------------------------------------------------------------------------- 1 | /* bottom controls (apply/reset) */ 2 | .knobs__controls { 3 | display: flex; 4 | align-items: center; 5 | opacity: 0; 6 | flex-direction: row-reverse; 7 | // border-top: 1px solid var(--textColor); 8 | margin: var(--side-pad) 9 | var(--control-right-pad, var(--side-pad)) 10 | 5px 11 | var(--control-left-pad, var(--side-pad)); 12 | position: relative; 13 | z-index: 1; 14 | 15 | > input { 16 | color: var(--textColor); 17 | border: 0; 18 | background: none; 19 | margin-left: 1em; 20 | line-height: 1; 21 | padding: 5px 8px; 22 | border-radius: 3px; 23 | position: relative; 24 | 25 | &:hover:not(:active){ 26 | background: var(--opaqueColor-15); 27 | } 28 | } 29 | } 30 | 31 | .poweredBy{ 32 | margin-right: auto; 33 | text-decoration: none; 34 | color: inherit; 35 | padding: 3px; 36 | font-size: 10px; 37 | opacity: .5; 38 | transition: .15s; 39 | } 40 | 41 | .poweredBy:hover{ 42 | color: var(--primaryColor); 43 | opacity: 1; 44 | } 45 | -------------------------------------------------------------------------------- /rollup.config.dev.mjs: -------------------------------------------------------------------------------- 1 | import scss from 'rollup-plugin-scss' 2 | import babel from '@rollup/plugin-babel' 3 | import cleanup from 'rollup-plugin-cleanup' 4 | import serve from 'rollup-plugin-serve' 5 | import livereload from 'rollup-plugin-livereload' 6 | import { nodeResolve } from '@rollup/plugin-node-resolve' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import {readFileSync} from 'fs' 9 | 10 | const pkg = JSON.parse(readFileSync('./package.json')) 11 | const banner = `/*! Knobs ${pkg.version} MIT | https://github.com/yairEO/knobs */\n`; 12 | 13 | export default [ 14 | { 15 | input: 'src/knobs.js', 16 | output: { 17 | banner, 18 | file: 'knobs.min.js', 19 | format: 'umd', 20 | name: 'Knobs', 21 | }, 22 | plugins: [ 23 | serve(), // index.html should be in root of project 24 | livereload({ watch:'src', delay:1500, exts: [ 'html', 'js', 'scss', 'css' ] }), 25 | babel({ babelHelpers: 'bundled' }), 26 | scss({ output: false, watch: 'src/styles', }), 27 | cleanup(), 28 | nodeResolve(), 29 | commonjs() 30 | ] 31 | } 32 | ] 33 | 34 | -------------------------------------------------------------------------------- /src/styles/_select.scss: -------------------------------------------------------------------------------- 1 | .knobs label[data-type='select']{ 2 | .knobs__knob__inputWrap{ 3 | &::before{ 4 | // "--value" variable only exists for valid select values. if this variable exists, hide this psuedo element. 5 | // this calc must reside within a varible or the space between the minus signs is removed 6 | --hide: Calc(var(--value) - var(--value)); 7 | 8 | content: 'N/A'; 9 | font-style: italic; 10 | opacity: var(--hide); 11 | filter: opacity(0.5); 12 | position: absolute; 13 | right: 2em; 14 | pointer-events: none; 15 | } 16 | } 17 | 18 | &::after{ 19 | content: '❯'; 20 | pointer-events: none; 21 | align-self: center; 22 | transform: translate(-100%, var(--offset-y, -1px)) rotate(90deg) scaleY(.8); 23 | transition: .1s; 24 | } 25 | 26 | &:hover{ 27 | --offset-y: 1px; 28 | } 29 | 30 | select{ 31 | font: inherit; 32 | background: none; 33 | color: var(--textColor); 34 | padding: 3px 0; 35 | cursor: pointer; 36 | border: none; 37 | outline: none; 38 | text-align-last: right; 39 | appearance: none; 40 | padding: 0 1.1em 0 0; 41 | 42 | } 43 | 44 | option{ 45 | background: var(--background); 46 | } 47 | } -------------------------------------------------------------------------------- /src/styles/_input_color.scss: -------------------------------------------------------------------------------- 1 | /* wrapper for "color" inputs */ 2 | label[data-type="color"]{ 3 | > .knobs__knob__inputWrap{ 4 | & > div{ 5 | display: inline-block; 6 | border-radius: 5px; 7 | overflow: hidden; 8 | width: calc(var(--color-size) * 4); 9 | height: calc(var(--color-size) - 2px); // same as border 10 | // transition: .2s var(--in-easing); 11 | transform-origin: center right; 12 | background: var(--background) repeating-conic-gradient(#FFFFFF33 0% 25%, transparent 0% 50%) 0/6px 6px; // checkboard pattern 13 | } 14 | } 15 | 16 | &:hover > .knobs__knob__inputWrap > div{ 17 | animation: colorHover .5s ease-out; 18 | // box-shadow: 0 0 0 1px #FFFFFF66; // needed in case a color is same as knobs background 19 | } 20 | 21 | input{ 22 | width: 100%; 23 | height: 100%; 24 | border: 0; 25 | background: var(--value); 26 | color: transparent; 27 | outline: none; 28 | caret-color: transparent; 29 | text-transform: uppercase; 30 | font-weight: 600; 31 | &::selection{ 32 | color: transparent; 33 | } 34 | } 35 | } 36 | 37 | @keyframes colorHover{ 38 | 20%{ transform: scale(1.2) } 39 | 40%{ transform: scale(1) } 40 | 60%{ transform: scale(1.1) } 41 | } -------------------------------------------------------------------------------- /src/persist.js: -------------------------------------------------------------------------------- 1 | const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped. 2 | const STORE_KEY = '@yaireo/knobs/knobs' 3 | 4 | const isStringOrNumber = a => typeof a == 'string' || typeof a == 'number'; 5 | 6 | export function getPersistedData(){ 7 | // if "persist" is "false", do not save to localstorage 8 | let _store = this.settings.persist, 9 | customKey = isStringOrNumber(_store) ? '/'+_store : '', 10 | persistedData, 11 | versionMatch = localStorage.getItem(`${STORE_KEY + customKey}/v`) == VERSION 12 | 13 | if( versionMatch ){ 14 | try{ persistedData = JSON.parse(localStorage[STORE_KEY + customKey]) } 15 | catch(err){} 16 | } 17 | 18 | return persistedData 19 | } 20 | 21 | export function setPersistedData(){ 22 | // if "persist" is "false", do not save to localstorage 23 | let _store = this.settings.persist, 24 | customKey = isStringOrNumber(_store) ? '/'+_store : ''; 25 | 26 | // when wishing to set data: 27 | if ( _store ){ 28 | let persistedData = JSON.stringify(this.knobs) 29 | 30 | localStorage.setItem(`${STORE_KEY + customKey}/v`, VERSION) // pesisted 31 | localStorage.setItem(STORE_KEY + customKey, persistedData) 32 | 33 | dispatchEvent( new Event('storage') ) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/utils/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | import mergeDeep from './mergeDeep' 2 | 3 | export default function EventDispatcher(){ 4 | // Create a DOM EventTarget object 5 | var target = document.createTextNode('') 6 | 7 | function addRemove(op, events, cb){ 8 | if( cb ) 9 | events.split(/\s+/g).forEach(name => target[op + 'EventListener'].call(target, name, cb)) 10 | } 11 | 12 | // Pass EventTarget interface calls to DOM EventTarget object 13 | return { 14 | off(events, cb){ 15 | addRemove('remove', events, cb) 16 | return this 17 | }, 18 | 19 | on(events, cb){ 20 | if(cb && typeof cb == 'function') 21 | addRemove('add', events, cb) 22 | return this 23 | }, 24 | 25 | trigger(eventName, data, opts){ 26 | var e; 27 | 28 | opts = opts || { 29 | cloneData:true 30 | } 31 | 32 | if( !eventName ) return; 33 | 34 | else{ 35 | try { 36 | var eventData = typeof data === 'object' 37 | ? data 38 | : {value:data}; 39 | 40 | eventData = opts.cloneData ? mergeDeep({}, eventData) : eventData 41 | eventData.knobs = this 42 | 43 | // TODO: move the below to the "mergeDeep" function 44 | if( data instanceof Object ) 45 | for( var prop in data ) 46 | if(data[prop] instanceof HTMLElement) 47 | eventData[prop] = data[prop] 48 | 49 | e = new CustomEvent(eventName, {"detail":eventData}) 50 | } 51 | catch(err){ console.warn(err) } 52 | 53 | target.dispatchEvent(e); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/cloneKnobs.js: -------------------------------------------------------------------------------- 1 | import isObject from './utils/isObject' 2 | 3 | const generateId = () => Math.random().toString(36).slice(-6); 4 | 5 | export default function(knobs, persistedData){ 6 | return knobs.map(k => { 7 | if( k && k.type ){ 8 | k.__name = k.__name || (k.label?.replaceAll(/[^a-zA-Z0-9 ]/g, '').replaceAll(' ','-').toLowerCase() || '') + '-' + generateId() 9 | k.defaultValue = k.defaultValue ?? k.value ?? this.getKnobValueFromCSSVar(k) ?? ''// value to revert to, if wished to reset 10 | k.defaultChecked = k.defaultChecked ?? !!k.checked 11 | 12 | k.isToggled = k.isToggled ?? true; 13 | // if( !this.settings.knobsToggle && k.isToggled === false ) 14 | // delete k.isToggled 15 | 16 | if( persistedData ){ 17 | // if current iterated knob exists in the persisted data array, use it 18 | let thisKnobPersistedData = persistedData.find(a => a.label && a.label == k.label) 19 | 20 | if( thisKnobPersistedData ){ 21 | if( k.defaultValue ) 22 | thisKnobPersistedData.defaultValue = k.defaultValue 23 | // override persisted "select" knob options to make sure they are the latest 24 | if( k.options ){ 25 | thisKnobPersistedData.options = k.options 26 | } 27 | 28 | return thisKnobPersistedData 29 | } 30 | } 31 | 32 | // cast to type "number" if needed (per input type) 33 | if( k.type == 'range' ){ 34 | k.value = +k.value || k.defaultValue 35 | k.defaultValue = +k.defaultValue 36 | } 37 | else if( k.type == 'checkbox' ){ 38 | k.checked = k.checked || k.defaultChecked 39 | } 40 | else{ 41 | // value is not necessarily defined, if is wished to be feched from the CSS automatically 42 | k.value = k.value || k.defaultValue 43 | } 44 | } 45 | 46 | if( k.render ) { 47 | k.__name = "custom-" + generateId() 48 | } 49 | 50 | return k.cssVar 51 | ? {...k, cssVar:[...k.cssVar]} 52 | : isObject(k) // labels are not objects, so use us-is 53 | ? {...k} 54 | : k 55 | }) 56 | } -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Allow setting units in a way that it's a part of knob's title but not part of the variable (presentational-only) 2 | - [x] Auto-detect initial CSS variables values from the target element, and populate knobs, if "value" prop is not specified 3 | - [x] Support wheel event for sliders 4 | - [x] Change native color picker to custom one 5 | - [x] Persist changes (setting) to localstorage 6 | - [x] Add a toggle button next to each knob (if supplied in setting to allow it) 7 | - [x] Make knobs setting to allow creating a non-iframe knobs element (which can be used/places however desired) 8 | - [x] automatically-detected values are not working as expecetd (maybe detecting the wrong element) 9 | - [x] max height - allow scroll of knobs when height is more than viewport (use resize observer) 10 | - [x] fix color knobs unable to revert 11 | - [x] Fix checkbox alignment when in compact-mode 12 | - [x] Add ability to pre-define a knob as non-checked 13 | - [x] Presist knob checkbox (enable/disable) as well 14 | - [x] Add title attribute (the input's value) to color input elements 15 | - [x] add dropdown knob (select) 16 | - [x] Allows separator line without any text 17 | - [x] when changing slider value & disabling & enabling - does not re-apply the slider's value 18 | - [x] color inputs - add knob settings for `default format` & 19 | - [x] color inputs - expose color-converion functions from color-picker so they could be used in a knob's `onChange` callback 20 | - [x] Allow custom radio inputs (as icons for example) 21 | - [x] Add Label titles - when hovering a label, should show a tooltip with optional explanation 22 | - [ ] color inputs - add `formats` knob property to enforce only certain formats (requires "color-picker" feature support) 23 | - [ ] Allow customizing the in/out transition duration 24 | - [ ] Add custom buttons 25 | - [ ] Add pre-defined value point buttons for the slider, like here: https://codepen.io/thebabydino/pen/zYvEqMd 26 | - [ ] outline which DOM node (element) is affected by the knob (if custom target is specified) 27 | - [ ] Optionally Add (-) (+) buttons to range sliders for sensetive changes (vaie Knob JSON) 28 | - [ ] Allow definding "options" alongside "slider" to show the dropdown at the same line as the slider 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@yaireo/knobs", 3 | "version": "1.3.6", 4 | "homepage": "https://github.com/yairEO/knobs", 5 | "description": "UI knobs controllers for JS/CSS live manipulation of various parameters", 6 | "keywords": [ 7 | "knobs", 8 | "controller", 9 | "ui", 10 | "component", 11 | "visual", 12 | "parameters" 13 | ], 14 | "scripts": { 15 | "start": "rollup -c rollup.config.dev.mjs -w", 16 | "build:prod": "npm run clean && npm run bundle:prod", 17 | "clean": "rm -rf knobs.min.js", 18 | "bundle:prod": "rollup -c rollup.config.prod.mjs", 19 | "test": "echo \"No test specified\"", 20 | "header": "headr knobs.min.js -o=knobs.min.js --version --homepage", 21 | "version": "npm run build:prod && npm run header && git add .", 22 | "prepublishOnly": "pkg-ok" 23 | }, 24 | "main": "./knobs.min.js", 25 | "files": [ 26 | "knobs.min.js", 27 | "src/styles" 28 | ], 29 | "license": "MIT", 30 | "browserslist": [ 31 | ">3% and ie 11", 32 | "not dead", 33 | "not ie < 11", 34 | "not IE_Mob 11", 35 | "not op_mini all" 36 | ], 37 | "author": { 38 | "name": "Yair Even-Or", 39 | "email": "vsync.design@gmail.com" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/yairEO/knobs.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/yaireo/knobs/issues" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.21.4", 50 | "@babel/preset-env": "^7.21.4", 51 | "@rollup/plugin-babel": "^6.0.3", 52 | "@rollup/plugin-commonjs": "^24.0.1", 53 | "@rollup/plugin-node-resolve": "^15.0.1", 54 | "@rollup/plugin-terser": "^0.4.0", 55 | "headr": "^0.0.4", 56 | "rollup": "^3.20.2", 57 | "rollup-plugin-cleanup": "^3.2.1", 58 | "rollup-plugin-livereload": "^2.0.5", 59 | "rollup-plugin-scss": "^4.0.0", 60 | "rollup-plugin-serve": "^2.0.2", 61 | "sass": "^1.60.0" 62 | }, 63 | "dependencies": { 64 | "@yaireo/color-picker": "~0.12.0", 65 | "@yaireo/position": "^1.1.1", 66 | "@yaireo/ui-range": "^2.1.15", 67 | "@yaireo/ui-switch": "^1.0.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/_toggleIcon.scss: -------------------------------------------------------------------------------- 1 | .leversIcon{ 2 | $size: 5px; 3 | 4 | width: 56px; 5 | transform: scale(.4); 6 | transform-origin: 0 0; 7 | 8 | > div{ 9 | display: flex; 10 | align-items: center; 11 | transition: transform .2s ease; 12 | 13 | &:nth-child(1){ 14 | &::before{ flex: .33; transition-delay: .3s; } 15 | } 16 | 17 | &:nth-child(2){ 18 | margin: 2px 0; 19 | &::after{ flex: .33; } 20 | } 21 | 22 | &:nth-child(3){ 23 | &::before{ flex: .8; transition-delay: .1s; } 24 | } 25 | 26 | > b{ 27 | display: inline-block; 28 | width: $size*1.5; 29 | height: $size*1.5; 30 | border-radius: 50%; 31 | border: #{$size - 1px} solid currentColor; 32 | margin: 0 $size; 33 | } 34 | 35 | &::before, 36 | &::after{ 37 | content: ''; 38 | height: $size; 39 | background: currentColor; 40 | border-radius: $size; 41 | flex: 1; 42 | transition: flex .1s ease; 43 | } 44 | 45 | &::after{ 46 | flex: auto; 47 | opacity: .33; 48 | } 49 | } 50 | } 51 | 52 | @keyframes leversIcon{ 53 | 30%{ flex: .2; } 54 | 80%{ flex: 5; } 55 | } 56 | 57 | #knobsToggle:not(:checked) + .knobs > label{ 58 | &:hover{ 59 | .leversIcon > div{ 60 | &:nth-child(1){ 61 | &::before{ animation: 1s leversIcon ease infinite; } 62 | } 63 | 64 | &:nth-child(2){ 65 | margin: 1px 0; 66 | &::after{ animation: 1s .1s leversIcon ease reverse infinite; } 67 | } 68 | 69 | &:nth-child(3){ 70 | &::before{ animation: 1.2s .15s leversIcon ease alternate infinite; } 71 | } 72 | } 73 | } 74 | } 75 | 76 | 77 | #knobsToggle:checked + .knobs > label{ 78 | --size: 18px; 79 | --offset: calc(var(--toggleOffset) + var(--size)/3); 80 | 81 | .leversIcon{ 82 | width: 65px; 83 | color: var(--textColor); 84 | transition: color .2s; 85 | transform: scale(.3) translate(0, 6px); 86 | opacity: .7; 87 | 88 | &:hover{ 89 | opacity: 1; 90 | } 91 | 92 | b{ 93 | transform: scale(0); 94 | margin: 0; 95 | width: 0; 96 | } 97 | 98 | > div{ 99 | &::after{ 100 | flex: 0; 101 | } 102 | &::before{ 103 | flex: 3; 104 | height: 8px; 105 | } 106 | 107 | &:nth-child(1){ 108 | transform: rotate(45deg); 109 | transform-origin: 20% 50%; 110 | } 111 | 112 | &:nth-child(2){ 113 | opacity: 0; 114 | } 115 | 116 | &:nth-child(3){ 117 | transform: rotate(-45deg); 118 | transform-origin: 0 0; 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/styles/_knob.scss: -------------------------------------------------------------------------------- 1 | .knobs{ 2 | &[data-flow='compact']{ 3 | .knobs__knob__toggle{ 4 | align-self: flex-start; 5 | margin-top: 6px; 6 | } 7 | } 8 | 9 | &__knob{ 10 | $self: &; 11 | display: flex; 12 | justify-content: flex-end; 13 | position: relative; 14 | line-height: var(--line-height); 15 | min-height: 24px; 16 | 17 | &:hover{ 18 | #{$self}__reset{ 19 | // transform: none; 20 | } 21 | 22 | #{$self}__label__text{ 23 | opacity: 1; 24 | } 25 | } 26 | 27 | // only show the "rest" button if knob has changed. 28 | // this allows the user to know which one was changed, where many knobs exist 29 | &[data-changed]{ 30 | #{$self}__reset{ 31 | opacity: .75; 32 | pointer-events: auto; 33 | 34 | &:hover{ 35 | opacity: 1; 36 | background: var(--textColor); 37 | color: var(--background); 38 | transition: 0s; 39 | } 40 | } 41 | } 42 | 43 | &__toggle{ 44 | display: var(--knobs-toggle, none); 45 | order: 1; 46 | align-self: center; 47 | margin: 0 5px 0 0; 48 | appearance: none; 49 | width: 12px; 50 | height: 12px; 51 | outline: none; 52 | border-radius: 50%; 53 | position: relative; 54 | text-align: center; 55 | line-height: 10px; 56 | 57 | &::before{ 58 | border: 1px solid var(--textColor); 59 | opacity: .4; 60 | border-radius: 3px; 61 | } 62 | 63 | &::after{ 64 | content: ''; 65 | height: 100%; 66 | z-index: 5; 67 | width: 999px; 68 | position: absolute; 69 | left: 0; 70 | pointer-events: none; 71 | } 72 | 73 | &:hover{ 74 | &::before{ 75 | opacity: 1; 76 | } 77 | } 78 | 79 | &:checked{ 80 | &:hover ~ *{ 81 | text-decoration: line-through; 82 | transition: .15s; 83 | } 84 | 85 | &::after{ 86 | content: '✔'; 87 | color: var(--textColor); 88 | font-size: 12px; 89 | text-shadow: -1px -2px var(--background), 3px -2px var(--background); 90 | position: relative; 91 | z-index: 1; 92 | } 93 | } 94 | 95 | &:not(:checked){ 96 | ~ *{ 97 | pointer-events: none !important; 98 | filter: grayscale(50%); 99 | opacity: .4; 100 | transition: .2s; 101 | 102 | ::-webkit-slider-thumb { pointer-events: none !important; } 103 | ::-moz-slider-thumb { pointer-events: none !important; } 104 | } 105 | } 106 | } 107 | 108 | &__reset{ 109 | order: 0; 110 | pointer-events: none; 111 | margin-right: .5em; 112 | padding: 0; 113 | align-self: center; 114 | color: inherit; 115 | background: none; 116 | border: 0; 117 | cursor: pointer; 118 | opacity: .33; 119 | outline: none; 120 | border-radius: 50%; 121 | width: 2ch; 122 | height: 2ch; 123 | user-select: none; 124 | transition: .15s ease-out; 125 | } 126 | 127 | &__label__text { 128 | margin-right: 2ch; 129 | white-space: nowrap; 130 | display: flex; 131 | align-items: center; 132 | opacity: .8; 133 | transition: 80ms; 134 | 135 | // variable units (if exists) 136 | &::after{ 137 | content: attr(data-units); 138 | opacity: .5; 139 | margin-left: 1ch; 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/templates.js: -------------------------------------------------------------------------------- 1 | import isObject from './utils/isObject' 2 | 3 | var settingsIcon = `
4 |
5 |
6 |
7 |
8 | ` 9 | 10 | export function scope(){ 11 | const {visible} = this.settings; 12 | 13 | return ` 14 | 15 | ${knobs.call(this, {})} 16 | ` 17 | } 18 | 19 | export function knobs({ withToggler = true}){ 20 | const {visible, live, theme} = this.settings; 21 | 22 | return ` 23 | 39 | ` 40 | } 41 | 42 | // group of knobs which is described by a label or a separator 43 | export function fieldset(knobsGroup){ 44 | var legend, knobs; 45 | 46 | if( isObject(knobsGroup[0]) ){ 47 | knobs = knobsGroup 48 | } 49 | 50 | else{ 51 | [legend, ...knobs] = knobsGroup; 52 | let getLegendParams = legend instanceof Array ? { label:legend[0], checked:!!legend[1] } : { label:legend, checked:true } 53 | legend = getLegend({ ...getLegendParams, knobsCount:knobs.length }) 54 | } 55 | 56 | return `
57 | ${legend ? legend : ''} 58 |
59 |
60 | ${knobs.map(knob.bind(this)).join("")} 61 |
62 |
63 |
` 64 | } 65 | 66 | export function knob(data){ 67 | if( data.render && !data.label ) 68 | return `
${data.render}
` 69 | 70 | 71 | if( data ) 72 | return `
73 | 74 | 80 | 81 |
82 | ` 83 | } 84 | 85 | function getLegend({ label, checked, knobsCount }){ 86 | var id = label.replace(/ /g, '-') + Math.random().toString(36).slice(-6); 87 | 88 | return ` 89 | ` 95 | } 96 | 97 | function getInput( data ){ 98 | let { label, type, step, min, max, value, name, options } = data 99 | 100 | if( type == 'range' ) 101 | return ` 102 |
103 | 104 | 105 |
106 |
` 107 | 108 | if( type == 'checkbox' ) 109 | return ` 110 |
111 | 112 |
113 |
` 114 | 115 | if( type == 'radio' && options?.length ){ 116 | data.name = data.name || label.toLowerCase().replaceAll(' ','-'); 117 | 118 | return options.map((v, i) => ``).join('') 119 | } 120 | 121 | if( type == 'select' && options?.length ) 122 | return ` 123 | ` 126 | 127 | if( type == 'color' ) 128 | type = 'text' 129 | 130 | return `
` 131 | } -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | import { changeColorFormat, CSStoHSLA } from '@yaireo/color-picker' 2 | 3 | const raf = window.requestAnimationFrame || (cb => window.setTimeout(cb, 1000 / 60)) 4 | const is = (elm, cls) => elm.classList.contains(cls) 5 | 6 | export function bindEvents(){ 7 | this.eventsRefs = this.eventsRefs || { 8 | change(e){ 9 | // only knobs' inputs have a "data-name" attribute 10 | if( !e.target.dataset.name ) return 11 | 12 | this.onChange(e) 13 | }, 14 | 15 | input(e){ 16 | try{ 17 | let isSectionToggler = is(e.target, 'toggleSection'), 18 | // sectionHeight, 19 | groupElm; 20 | 21 | // previously used "resizeObserver", but in Firefox the resize callback is called only after and not during every frame of the resize, 22 | // so this hacky-approach beflow is needed to adjust the offset of the iframe *before* the knobs group section is expanded 23 | if( isSectionToggler && e.target.checked ){ 24 | groupElm = e.target.parentNode.querySelector('.fieldset__group') 25 | // sectionHeight = groupElm.style.getPropertyValue('--height'); 26 | this.setIframeProps({ heightOffset:9999 }) // better to temporarly set a large height and then at "transitionend" put the exact height 27 | } 28 | } 29 | catch(err){} 30 | 31 | if( e.target.hasAttribute('is-knob-input') ){ 32 | this.onInput(e) 33 | this.onChange(e) 34 | } 35 | 36 | else if ( is(e.target, 'knobs__knob__toggle') ) 37 | this.toggleKnob( e.target.dataset.forKnob, e.target.checked ) 38 | }, 39 | 40 | transitionstart(e){ 41 | // this dirty trick is needed to add "overflow:hidden" to the group while transitied 42 | if( is(e.target, 'fieldset__group__wrap') ){ 43 | e.target.parentNode.setAttribute('transitioned', 1) 44 | } 45 | }, 46 | 47 | transitionend(e){ 48 | if( is(e.target, 'fieldset__group__wrap') ){ 49 | e.target.parentNode.removeAttribute('transitioned') 50 | this.setIframeProps() 51 | } 52 | }, 53 | 54 | wheel(e){ 55 | const { value, max, step, type } = e.target, 56 | delta = Math.sign(e.deltaY) * (+step||1) * -1 // normalize jump value to either -1 or 1 57 | 58 | // disable window scroll: https://stackoverflow.com/a/23606063/104380 59 | if( type == 'range' ) 60 | e.preventDefault() 61 | 62 | if( value && max ){ 63 | e.target.value = Math.min(Math.max(+value + delta, 0), +max) 64 | this.onInput(e) 65 | this.onChange(e) 66 | } 67 | }, 68 | mainToggler(e){ this.toggle(e.target.checked) }, 69 | reset : this.applyKnobs.bind(this, null, true), 70 | submit: this.onSubmit.bind(this), 71 | click : this.onClick.bind(this), 72 | focusin : this.onFocus.bind(this) 73 | }; 74 | 75 | [ 76 | ['scope', 'click'], 77 | ['form', 'change'], 78 | ['form', 'input'], 79 | ['form', 'reset'], 80 | ['form', 'submit'], 81 | ['form', 'focusin'], 82 | ['form', 'transitionend'], 83 | ['form', 'transitionstart'], 84 | ['scope', 'wheel'], 85 | ['mainToggler', 'change', this.eventsRefs.mainToggler.bind(this)], 86 | ].forEach(([elm, event, cb]) => 87 | this.DOM[elm] && this.DOM[elm].addEventListener(event, cb || this.eventsRefs[event].bind(this), { passive:false }) 88 | ) 89 | 90 | whenKnobsParentResizes.call(this) 91 | // window.addEventListener('storage', this.eventsRefs.onStorage) 92 | } 93 | 94 | function whenKnobsParentResizes(){ 95 | let debounceTimer, 96 | that = this; 97 | 98 | // if the page which added Knobs is resized, re-calculate iframe height 99 | const resizeObserver = new ResizeObserver(entries => { 100 | clearTimeout(debounceTimer) 101 | debounceTimer = setTimeout(()=> { 102 | that.setIframeProps() 103 | }, 500) 104 | }) 105 | 106 | resizeObserver.observe( this.settings.appendTo ) 107 | } 108 | 109 | export function onFocus(e) { 110 | // if( e.target.dataset.type == 'color' ) 111 | // setTimeout(_ => this.toggleColorPicker(e.target), 100) 112 | } 113 | 114 | /** 115 | * only for knobs inputs 116 | */ 117 | export function onInput(e){ 118 | const inputElm = e.target, 119 | { type, value, checked, dataset:{name} } = inputElm, 120 | isCheckbox = type == 'checkbox', 121 | { label } = this.getKnobDataByName(name) 122 | 123 | this.setParentNodeValueVars(inputElm) 124 | this.setKnobDataByName(name, isCheckbox ? {checked} : {value}) 125 | 126 | if( value != undefined && label ) 127 | // save knob's new value 128 | this.setPersistedData() 129 | // this.getSetPersistedData({ [label]:isCheckbox ? [inputElm.checked, value] : value }) 130 | } 131 | 132 | /** 133 | * only for knobs inputs 134 | */ 135 | export function onChange(e, ignoreSimilar){ 136 | const name = e.target.dataset.name; 137 | 138 | this.setKnobChangedFlag( this.getKnobElm(name) ) 139 | 140 | const knobData = this.getKnobDataByName(name), 141 | runOnInput = e.type == 'input' && knobData && knobData.type != 'range', // forgot why I wrote this 142 | isCheckbox = knobData && knobData.type == 'checkbox', 143 | extraData = {} 144 | 145 | if( !knobData ){ 146 | console.warn("Knob data was not found:", {name, knobData}) 147 | return 148 | } 149 | 150 | const similarKnobs = ignoreSimilar ? [] : this.getSimilarKnobs(knobData) 151 | 152 | if( similarKnobs.length ){ 153 | similarKnobs.forEach(knob => { 154 | const inputElm = this.getInputByName(knob.__name) 155 | inputElm.value = knobData.value 156 | this.onInput({ target:inputElm }) 157 | }) 158 | } 159 | 160 | if( !isCheckbox && !this.settings.live ) 161 | return 162 | 163 | if( e.type == 'input' && runOnInput ) 164 | return 165 | 166 | raf(() => this.updateDOM(knobData)) 167 | 168 | if( knobData.type === 'color' ) 169 | extraData.hsla = CSStoHSLA(changeColorFormat(knobData.value, 'HSL')) 170 | 171 | typeof knobData.onChange == 'function' && knobData.onChange(e, knobData, extraData) 172 | } 173 | 174 | /** 175 | * Applys changes manually if `settings.live` is `false` 176 | */ 177 | export function onSubmit(e){ 178 | e.preventDefault() 179 | 180 | var elements = e.target.querySelectorAll('input[data-name]') 181 | this.settings.live = true 182 | elements.forEach(elm => this.onChange({ target:{value:elm.value, type:elm.type, dataset:{name:elm.dataset.name}} })) 183 | this.settings.live = false 184 | return false 185 | } 186 | 187 | export function onClick(e){ 188 | const {target} = e 189 | 190 | if( is(target, 'knobs__knob__reset') ) 191 | this.resetKnobByName(target.name) 192 | 193 | if( target.dataset.type == 'color' ) 194 | setTimeout(_ => this.toggleColorPicker(target), 100) 195 | } -------------------------------------------------------------------------------- /src/styles/_knobs.scss: -------------------------------------------------------------------------------- 1 | #knobsToggle{ 2 | // main toggle button 3 | + .knobs > label{ 4 | --size: calc(var(--toggleSize)/2); 5 | --offset: calc(var(--toggleOffset)); // takes the circular background (next element) into account 6 | position: absolute; 7 | width: var(--size); 8 | height: var(--size); 9 | top: var(--offset); 10 | right: var(--offset); 11 | padding: calc((var(--toggleSize) - var(--size))/2); 12 | font-size: 20px; 13 | line-height: 1; 14 | z-index: 1; // enough to make sure slider's output value is over the icon and not under 15 | color: var(--textColor); 16 | } 17 | 18 | &:not(:checked){ 19 | + .knobs > label{ 20 | &:hover{ 21 | + .knobs__bg{ 22 | opacity: 1; 23 | transform: scale(1.15); 24 | } 25 | } 26 | } 27 | } 28 | 29 | &:checked{ 30 | + .knobs{ 31 | // This is super-important. 32 | // "inline-block" is used so the content will only fill the minimum poissible size inside the body of the iframe, 33 | // but if the knobs are hidden, then this hinders the position of the toggler button. 34 | display: inline-block; 35 | 36 | > label{ 37 | padding: 0; 38 | } 39 | 40 | .knobs__bg{ 41 | --corner-radius: 8px; 42 | --offset: calc(var(--corner-radius) * -1); 43 | top: var(--offset); 44 | right: var(--offset); 45 | bottom: var(--offset); 46 | left: var(--offset); 47 | 48 | border-radius: var(--corner-radius); 49 | margin: 0; 50 | width: calc(100% + var(--corner-radius)); 51 | height: calc(100% + var(--corner-radius)); 52 | opacity: 1; 53 | transition: .3s cubic-bezier(.45, 0, .2, 1), margin .2s, border-radius .2s; 54 | } 55 | 56 | .knobs__labels { 57 | transform: none; 58 | transition: calc(var(--in-duration) * 1s) var(--in-easing); 59 | 60 | fieldset, .knobs__controls { 61 | transform: none; 62 | opacity: 1; 63 | transition: calc(var(--in-duration) * 1s) calc(var(--in-duration) * .5s) ease-out; 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | html, body{ 71 | overflow: hidden; 72 | } 73 | 74 | .knobs { 75 | --background: hsla(var(--base-color), var(--base-color-l), var(--base-color-a)); 76 | --opaqueColor-15: HSL(var(--base-color), calc(var(--base-color-l) + 15%)); 77 | --range-track-color: var(--primaryColor); 78 | --knobs-gap: 3px; 79 | --side-pad: 12px; 80 | --toggleSize: 40px; 81 | --toggleOffset: 6px; 82 | --in-easing: cubic-bezier(.75,0,.35,1); 83 | --in-duration: .3; 84 | --color-size: 20px; 85 | --line-height: Max(0px, var(--color-size)); // units must be the same 86 | --knobs-group-transition: .33s cubic-bezier(.45, 0, .2, 1); 87 | --LTR-Bool: 1; /* -1 for RTL */ 88 | 89 | 90 | font: 12px/1 'Fira Sans Condensed', sans-serif; 91 | color: var(--textColor); 92 | position: relative; 93 | // position: fixed; 94 | overflow: hidden; 95 | 96 | &[data-flow='compact']{ 97 | --color-size: 16px; 98 | 99 | label[data-type='range']{ 100 | flex-flow: column; 101 | gap: var(--knobs-gap); 102 | padding-top: 6px; 103 | 104 | .range-slider{ 105 | --thumb-size: 12px; 106 | --track-height: calc(var(--thumb-size)/2); 107 | width: 100%; 108 | } 109 | 110 | & ~ .knobs__knob__reset{ 111 | // margin-bottom: calc(var(--knobs-gap, 6px) * 2); 112 | align-self: flex-start; 113 | margin-top: .5ch; 114 | } 115 | 116 | .knobs__label__text{ 117 | margin: 0; 118 | padding: 0; 119 | } 120 | } 121 | } 122 | 123 | label { 124 | user-select: none; 125 | cursor: pointer; 126 | } 127 | 128 | &__bg{ 129 | pointer-events: none; 130 | position: absolute; 131 | top: 0; 132 | right: 0; 133 | z-index: -1; 134 | margin: var(--toggleOffset); 135 | width: var(--toggleSize); 136 | height: var(--toggleSize); 137 | border-radius: 50%; 138 | background: var(--background); 139 | opacity: .8; 140 | backdrop-filter: blur(8px); 141 | transition: 120ms; 142 | } 143 | 144 | /* the
element which is the actual knobs wrapper */ 145 | &__labels { 146 | // background: var(--background); 147 | display: flex; 148 | flex-flow: column; 149 | max-height: 100%; 150 | border: var(--border); 151 | transform: translateX(calc(100.1% * var(--LTR-Bool))); 152 | 153 | fieldset { 154 | display: table; 155 | border: 0; 156 | padding: 0; 157 | margin: 0; 158 | opacity: 0; 159 | transform: translateX(calc(22% * var(--LTR-Bool))); 160 | 161 | // if only a single fieldset and it has a legend: disallow collapsing 162 | &:only-of-type{ 163 | > label{ 164 | pointer-events: none; 165 | } 166 | } 167 | 168 | &:first-child { 169 | // this allows the output of a range slider to be seen 170 | // on the first fieldset if it has no legend 171 | &:not([data-has-legend]){ 172 | overflow: visible; 173 | } 174 | } 175 | } 176 | 177 | .fieldset__group{ 178 | &[transition-done]{ 179 | // needed for the hover tooltips (of range sliders) 180 | overflow: visible; 181 | } 182 | 183 | &__wrap{ 184 | display: flex; 185 | flex-flow: column; 186 | gap: var(--knobs-gap); 187 | padding: var(--side-pad); 188 | transition: var(--knobs-group-transition); 189 | } 190 | } 191 | 192 | hr{ 193 | border: 0; 194 | border-top: 1px solid var(--textColor); 195 | opacity: .25; 196 | 197 | &:last-of-type{ 198 | margin-bottom: 0; 199 | } 200 | } 201 | 202 | label:not(.knobs__legend) { 203 | order: 5; 204 | flex: 1; 205 | display: flex; 206 | position: relative; 207 | z-index: 1; 208 | // background: var(--background); 209 | 210 | > * { 211 | // padding: var(--knobs-gap, 6px) 0; 212 | } 213 | } 214 | 215 | & .range-slider, 216 | & input[type=text]:not([size]), 217 | & input[type=number]:not([size]) { 218 | min-width: 200px; 219 | } 220 | 221 | & label:not(.knobs__legend) > :last-child { 222 | width: 100%; 223 | flex: 1; 224 | text-align: right; 225 | align-self: center; 226 | } 227 | } 228 | 229 | &__groups{ 230 | flex: 1; 231 | margin-top: calc(var(--side-pad) * 2.5); 232 | overflow-y: scroll; 233 | scrollbar-width: none; // hides scrollbar (currently only in Firefox) 234 | 235 | &::-webkit-scrollbar { 236 | display: none; 237 | } 238 | 239 | > fieldset:first-child .knobs__knob:first-child .range-slider{ 240 | --value-offset-y: 14px; 241 | } 242 | } 243 | 244 | &__legend{ 245 | $lineGap: 2ch; 246 | 247 | display: flex; 248 | align-items: center; 249 | font-weight: 700; 250 | opacity: .66; 251 | line-height: 1.6; 252 | cursor: pointer; 253 | transition: .2s cubic-bezier(.45, 0, .2, 1); 254 | 255 | &[data-has-label]{ 256 | gap: $lineGap; 257 | &:hover{ 258 | gap: $lineGap * 2; 259 | } 260 | } 261 | 262 | &::before, 263 | &::after{ 264 | content: ''; 265 | height: 1px; 266 | background: var(--textColor); 267 | flex: 1; 268 | opacity: .5; 269 | transition: inherit; 270 | } 271 | 272 | &:hover{ 273 | opacity: .85; 274 | } 275 | 276 | > div{ 277 | display: flex; 278 | align-items: center; 279 | gap: $lineGap; 280 | } 281 | 282 | &__knobsCount{ 283 | display: inline-block; 284 | border-radius: 50%; 285 | width: 1.5em; 286 | height: 1.5em; 287 | line-height: 1.6; 288 | font-size: .9em; 289 | text-align: center; 290 | overflow: hidden; 291 | position: relative; 292 | transition: var(--knobs-group-transition); 293 | 294 | &::before{ 295 | background: var(--textColor); 296 | opacity: .3; 297 | } 298 | 299 | &:only-child{ 300 | margin: 0 $lineGap; 301 | } 302 | } 303 | } 304 | 305 | .toggleSection{ 306 | // visible knobs group 307 | &:checked{ 308 | ~ .knobs__legend .knobs__legend__knobsCount{ 309 | transform: scale(0); 310 | margin: 0; 311 | width: 0; 312 | } 313 | 314 | ~ .fieldset__group[transitioned]{ 315 | overflow: hidden; 316 | } 317 | } 318 | 319 | // hidden knobs group 320 | &:not(:checked){ 321 | ~ .knobs__legend{ 322 | margin-bottom: 1em; 323 | } 324 | 325 | ~ .fieldset__group{ 326 | overflow: hidden; 327 | 328 | .fieldset__group__wrap{ 329 | opacity: 0; 330 | margin-top: calc(var(--height) * -1px); 331 | text-shadow: 0px 3px 2px; 332 | } 333 | } 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Knobs 8 | 9 | 33 | 34 | 35 | 36 |
37 | 261 | 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 | Knobs 5 | 6 |
7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 |

19 | Knobs 🎛️ UI controllers for JS/CSS manipulation 20 |

21 | 22 |

23 | 👉 Demo 👈 24 |

25 |

26 | 27 | 28 | 29 | 45 | 63 | 64 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Minified54kb
Brotli13.5kb
GZIP15kb
44 |
46 |

What is this:

47 |

48 | Started as something I needed for my many Codepens - A way to provide viewers, and myself, a set 49 | of controllers, for manipulating the DOM instantaneously. 50 |

51 |

52 | Imagine certain aspects of a web page, or a specific *component*, which you would like to add the ability 53 | to control on-the-fly. Change the visual looks or certain javascript parameters with a move or a slider. 54 |

55 |

56 | CSS-variables (custom properties) are a great match for this script as they compute in real-time. Javascript is of course a benefitor because every knobs can be attached with a callback that recieves the current value, and additional data, as to what that value should be applied on. 57 |

58 |

59 | It's so easy & quick to use Knobs, about 1 minute! 60 |

61 | ⚠️ Supported only in modern browsers
62 |
65 | 66 | ### Features: 67 | 68 | * `range` input (*wheel*-supported) 69 | * `color` input with awesome custom [color picker](https://github.com/yairEO/color-picker) 70 | * `checkbox` input 71 | * `radio` inputs group 72 | * `select` dropdown (native) 73 | * Custom HTML-Knobs (add your own buttons or whatever) 74 | * Resset all knobs (to defaults) 75 | * Reset individual knob 76 | * Labels - group all the knobb defined after a certain label 77 | * **Expand/Collapse** knobs groups 78 | * Apply changes live, on-the-fly, or with an Apply button 79 | * Auto-detect CSS variables defined in knobs as their initialvalues, if possible 80 | * Knobs are completely **isolated** within an *iframe* (unaffected by your page styles) 81 | * Allows 3 states of visibility: 82 | - `0` - Starts as hidden 83 | - `1` - Starts as visible 84 | - `2` - Always visible 85 | * Knobs component placement `position` (relative to window viewport): 86 | * `top right` (default) 87 | * `bottom right` 88 | * `top left` 89 | * `bottom left` 90 | * Allows theme customization (*currently very limited*) 91 | 92 | ## Configuration: 93 | 94 | All is needed is to include the knobs script on the page, and set it up. 95 | 96 | ```js 97 | new Knobs(settings) 98 | ``` 99 | 100 | 101 | ### Settings 102 | 103 | | Name | Type | Default | Info 104 | |--------------|-----------------------|---------|-------------------------------------------------------------------------------------------------------------------- 105 | | theme | `Object` | | Knobs theme variables. Since the Knobs are encapsulated within an iframe, they cannot be be styled from outside. 106 | | live | `Boolean` | `true` | Should changes be immediately applied 107 | | persist | `Boolean` | `false` | Persist changes using the browser's localstorage. Store `key/value` per knob, where `key` is the knob's *label*. 108 | | visible | `Number` | `0` | `0` - Starts as hidden
`1` - Starts as visible
`2` - Always visible 109 | | CSSVarTarget | `Element`/`NodeList ` | | Global HTML element(s) for which to apply the CSS custom properties.
Can also be configured per-knob. 110 | | knobsToggle | `Boolean` | `false` | if `true` - adds a checkbox next to each knob, which allows temporarily disabling the knob, reverting to default 111 | | knobs | `Array` | | Array of Objects describing the knobs controllers on-screen 112 | | standalone | `Boolean` | `false` | if `true` - does not create an iframe and appends it to the page, but simply gives the developer the DOM node, as is, to inject manually with `knobs.DOM.scope` node. Note that CSS in also needed ('./src/styles/styles.scss`) 113 |
114 | theme (defaults) 115 | 116 | ```js 117 | { 118 | styles : ``, // optioanlly add any CSS and it will be injected into the iframe 119 | flow : 'horizontal', // use 'compact' to keep things tight 120 | position : 'top right', 121 | primaryColor: '#0366D6', // mainly for links / range sliders 122 | 'base-color': "rgba(0,0,0,1)", // mainly for the background color but also for input fields such as text or number 123 | textColor : "#CCC", 124 | border : 'none' 125 | } 126 | ``` 127 |
128 | 129 |
130 | knobs 131 | 132 | An array of Objects, where the properties describe a *knob*. 133 | 134 | It is ***possible*** to define/update the `knobs` Array **after** instance initialization, like so: 135 | 136 | ```js 137 | var myKnobs = new Knobs({ CSSVarTarget:document.body }) // only if working with CSS variables 138 | 139 | myKnobs.knobs = [{...}, ...] // Add/change the knobs. will automatically re-render (see example further below) 140 | ``` 141 | 142 | All defined *knob* properties, beside a special few, are attributes that 143 | are applied on the HTML *input* element that controls the knob, so it is up 144 | to the developer who set up the knobs to use the appropriate attributes, for 145 | each type of of the supported knobs (`range`, `color`, `checkbox`). 146 | 147 | The special other properties are: 148 | 149 | **`onChange`** 150 | 151 | Callback which fires on every `input` event 152 | 153 | **`cssVar`** 154 | 155 | Optional. An array of 3 items: 156 | 1. (`String`) - CSS variable name 157 | 2. (`String`) - Units (*optional* - Ex. `%` or `px`) 158 | 3. (`HTML NODE`) - Reference to an HTML node to apply the knob's CSS variable on (*optional*) 159 | 160 | Sometimes it is wanted for variables to be defined unitless, for calculation-purposes, like so: 161 | 162 | ```css 163 | div{ 164 | --size: 10; 165 | /* limits with width to a minimum of 10px by using unitless variable for the "max" function */ 166 | width: calc(Max(50, var(--size)) * 1px); 167 | } 168 | ``` 169 | 170 | So, when a unitless-variable is desired, but ultimatly it will have a unit, then `units` (*2nd* item in the array) 171 | should be written with a dash prefix, Ex.: `-px`, and it will be displayed in the label correctly but ignored when 172 | applying the variable. 173 | 174 | **`cssVarsHSLA`** (boolean) 175 | 176 | Applies only to *color* knobs and if set to `true` will generate 4 CSS variables for the HSLA version of the color. 177 | 178 | `--main-color-h`, `--main-color-s`, `--main-color-l` & `--main-color-a`. 179 | 180 | ```js 181 | { 182 | cssVar: ['main-color'], 183 | cssVarsHSLA: true, 184 | label: 'Page background', 185 | type: 'color', 186 | defaultFormat: 'hsla', 187 | }, 188 | ``` 189 | 190 | **`defaultFormat`** (string) 191 | 192 | Applies only to *color* knobs. Sets the default format displayed to the user and also the value which will 193 | be set to the input. Possible values are: `hsla`, `rgba`, `hex`. 194 | 195 | 196 | **`label`** (string) 197 | 198 | A text which is displayed alongside the knob 199 | 200 | **`labelTitle`** (string) 201 | 202 | Optional `title` attribute for the knob's label 203 | 204 | **`value`** (string, number) 205 | 206 | Acts as the initial value of the *knob*, except for `checkbox` *knobs*, in which case, 207 | if the knob also has `cssVar` property set, then the checkbox is *checked*, that CSS variable 208 | `value` will be the `value` property of the knob, Ex. 209 | 210 | ```js 211 | { 212 | cssVar: ['hide'], // CSS variable name "--hide" 213 | label: 'Show', 214 | type: 'checkbox', 215 | // checked: true, // not checked by default 216 | value: 'none', // if checked: --hide: none; 217 | } 218 | ``` 219 | 220 | Then in your CSS you can write the below, so when `--hide` is not defined, 221 | `block` is used as the `display` property value. 222 | 223 | ```css 224 | display: var(--hide, block); 225 | ``` 226 | 227 | It is possible to use an *already-declared* CSS-varaible (on the target element) by emmiting the `value` 228 | prop from the *knob* decleration. The program will try to get the value using `getComputedStyle` and `getPropertyValue`. 229 | 230 | Variables which has `calc` or any other computations might result in `NaN`. In which case, a `console.warn` will be presented 231 | and a manually typed `value` property for the *knob* would be advised. 232 | 233 | **`isToggled`** (boolean) 234 | If this property is set to `false`, the knob will be toggled *off* by default. 235 | 236 | Will only take affect if `knobsToggle` setting is set to `true` 237 | 238 | **`options`** (array) 239 | Used for knobs of type `select`. An Array of options to render. 240 | 241 | [20, 150, [200, '200 nice pixels'], 500] 242 | 243 | An option can be split to the actual value it represents and its textual value, as the above example shows. 244 | 245 | **`knobClass`** (string) 246 | Add your own *class* to the knob (row) element itself (for styling purposes). 247 | Remember that in order to add custom styles, the `theme.styles` setting should be used, because all knobs 248 | are encapsulated within an *iframe* so your page styles won't affect anything that's inside. 249 | 250 | **`render`** (string) 251 | Allows to render anything you want in the knob area. 252 | Should return a *string* of HTML, for example: 253 | 254 | ```js 255 | { 256 | render: ` 257 | 258 | 259 | `, 260 | knobClass: 'custom-actions' 261 | } 262 | ``` 263 | 264 | **`script`** (function) 265 | A function to be called which has logic related to the custom HTML in the `render` property (shown above). 266 | The function recieves 2 arguments: The knobs instance referece and the (auto)generated knob `name` string. 267 | 268 | ```js 269 | { 270 | label: 'Custom HTML with label', 271 | render: ` 272 | 273 | 274 | `, 275 | script(knobs, name){ 276 | knobs.getKnobElm(name).addEventListener("click", e => { 277 | if( e.target.tagName == 'BUTTON' ) 278 | alert(e.target.textContent) 279 | }) 280 | }, 281 | }, 282 | ``` 283 |
284 | 285 | 286 | ## Install: 287 | 288 | ``` 289 | npm i @yaireo/knobs 290 | ``` 291 | 292 | **CDN source:** 293 | 294 | [https://unpkg.com/@yaireo/knobs@latest](https://unpkg.com/@yaireo/knobs@latest) 295 | 296 | 297 | ## Example: 298 | 299 | ### When Using with NPM, first import `Knobs` 300 | ```js 301 | import Knobs from '@yaireo/knobs' 302 | ``` 303 | 304 | #### Color manipulation methods: 305 | 306 | `format` & `CSStoHSLA` are defined on Knobs' instances in `color` property, for example: 307 | 308 | ```js 309 | const myKnobs = new Knobs({ 310 | ..., 311 | knobs: [ 312 | { 313 | cssVar: ['bg'], // alias for the CSS variable 314 | label: 'Color', 315 | type: 'color', 316 | value: '#45FDA9', 317 | onChange(e, knobData, hsla) => { 318 | console.log( myKnobs.format(knobData.value, 'rgb') ) // will print a color string in RGBA 319 | } 320 | } 321 | ] 322 | }) 323 | 324 | myKnobs.color.format() 325 | ``` 326 | 327 | See [color-picker docs](https://github.com/yairEO/color-picker#helper-methods-exported-alongside-the-default-colorpicker) 328 | 329 | 330 | ### Defining Knobs: 331 | 332 | ```js 333 | var settings = { 334 | theme: { 335 | position: 'bottom right', // default is 'top left' 336 | }, 337 | 338 | // should update immediately (default true) 339 | live: false, 340 | 341 | // 0 - starts as hidden, 1 - starts as visible, 2 - always visible 342 | visible: 0, 343 | 344 | CSSVarTarget: document.querySelector('.testSubject'), 345 | 346 | knobs: [ 347 | { 348 | cssVar: ['width', '-px'], // prefix unit with '-' makes it only a part of the title but not of the variable 349 | label: 'Width', 350 | labelTitle: 'Changes the width at steps of 50 pixels', 351 | type: 'range', 352 | value: 200, 353 | min: 0, 354 | max: 500, 355 | step: 50, 356 | onChange: console.log // javascript callback on every "input" event 357 | }, 358 | 359 | { 360 | cssVar: ['width', '-px'], 361 | label: 'Width preset', 362 | type: 'select', 363 | options: [20, 150, [200, '200 nice pixels'], 500], 364 | value: 150, // should be one of the options 365 | defaultValue: 150 // value for which to reset to (optional) 366 | isToggled: false, // this knob will not take affect by default 367 | }, 368 | 369 | { 370 | cssVar: ['height', 'vh'], 371 | label: 'Height', 372 | type: 'range', 373 | // value: 20, // if a value is not defined, Knobs will try to get it from the CSS ("CSSVarTarget" selector) automatically 374 | min: 0, 375 | max: 100, 376 | onChange: console.log 377 | }, 378 | 379 | { 380 | cssVar: ['align'], 381 | label: 'Align boxes', 382 | type: 'radio', 383 | name: 'align-radio-group', 384 | options: [ 385 | { value:'left', hidden:true, label: ' console.log(e, knobData, hsla, knobData.value) 414 | }, 415 | 416 | { 417 | cssVar: ['main-bg', null, document.body], // [alias for the CSS variable, units, applies on element] 418 | label: 'Background', 419 | type: 'color', 420 | value: '#FFFFFF', 421 | onChange: (e, knobData, hsla) => console.log(e, knobData, hsla, knobData.value) 422 | }, 423 | 424 | ["Label example", false] // group is collapsed by default 425 | { 426 | cssVar: ['hide'], // alias for the CSS variable 427 | label: 'Show', 428 | type: 'checkbox', 429 | // checked: true, // default 430 | value: 'none', 431 | onChange: console.log 432 | }, 433 | 434 | { 435 | label: 'Custom with label', 436 | render: ` 437 | 438 | 439 | `, 440 | script(knobs, name){ 441 | knobs.getKnobElm(name).addEventListener("click", e => { 442 | if( e.target.tagName == 'BUTTON' ) 443 | alert(e.target.textContent) 444 | }) 445 | }, 446 | }, 447 | 448 | { 449 | render: ` 450 | 451 | `, 452 | script(knobs){ 453 | const elm = knobs.DOM.scope.querySelector('.specialBtn3') 454 | elm.addEventListener("click", () => alert('😎')) 455 | }, 456 | knobClass: 'custom-actions' 457 | } 458 | ] 459 | } 460 | 461 | var penKnobs = new Knobs(settings) 462 | ``` 463 | -------------------------------------------------------------------------------- /src/knobs.js: -------------------------------------------------------------------------------- 1 | import ColorPicker, { changeColorFormat, CSStoHSLA } from '@yaireo/color-picker' 2 | import position from '@yaireo/position' 3 | import mainStyles from './styles/styles.scss' 4 | import hostStyles from './styles/host.scss' 5 | import colorPickerStyles from '@yaireo/color-picker/dist/styles.css' 6 | import isObject from './utils/isObject' 7 | import parseHTML from './utils/parseHTML' 8 | import mergeDeep from './utils/mergeDeep' 9 | import isModernBrowser from './utils/isModernBrowser' 10 | import getKnobsGroups from './utils/getKnobsGroups' 11 | import cloneKnobs from './cloneKnobs' 12 | import * as templates from './templates' 13 | import * as events from './events' 14 | import * as persist from './persist' 15 | import DEFAULTS from './defaults' 16 | import EventDispatcher from './utils/EventDispatcher' 17 | 18 | function Knobs(settings){ 19 | // since Knobs relies on CSS variables, no need to proceed if browser support is inadequate 20 | if ( !isModernBrowser()) 21 | return this 22 | 23 | const { knobs = [], ...restOfSettings } = settings || {} 24 | 25 | // for the rest, deep cloining appear to work fine 26 | this.settings = mergeDeep({...DEFAULTS, appendTo:document.body}, restOfSettings) 27 | mergeDeep(this, EventDispatcher()); 28 | this.knobs = knobs 29 | this.DOM = {} 30 | this.state = {} 31 | this.build() 32 | } 33 | 34 | Knobs.prototype = { 35 | _types: ['range', 'color', 'checkbox', 'text'], 36 | 37 | ...events, 38 | ...persist, 39 | 40 | cloneKnobs, 41 | 42 | /** 43 | * "Knobs" property setter 44 | */ 45 | set knobs(knobs){ 46 | if( knobs && knobs instanceof Array ){ 47 | // manual deep-clone the "knobs" setting, because for hours I couldn't find a single piece of code 48 | // on the internet which was able to correctly clone it 49 | this._knobs = this.cloneKnobs(knobs, this.getPersistedData()) 50 | this.DOM && this.render() 51 | } 52 | }, 53 | 54 | /** 55 | * "knobs" property getter 56 | */ 57 | get knobs(){ 58 | return this._knobs 59 | }, 60 | 61 | /** 62 | * Generate styles for the iframe's using the "theme" property in the settings 63 | * @param {Object} vars 64 | */ 65 | getCSSVariables({ flow, styles, RTL, position, ...vars }){ 66 | var output = '', p; 67 | 68 | // "knobsToggle" is not in the "theme" prop, and it's a special case where a variable is needed 69 | if( this.settings.knobsToggle ) 70 | vars['knobs-toggle'] = 1 71 | 72 | const hslColor = changeColorFormat(vars['base-color'], 'hsl'); // example: "hsla(0, 0%, 0%, 100%)" 73 | const baseColor = CSStoHSLA(hslColor) 74 | vars['base-color'] = `${baseColor[0]}, ${baseColor[1]}%` 75 | vars['base-color-l'] = `${baseColor[2]}%` 76 | vars['base-color-a'] = `${baseColor[3]}%` 77 | 78 | for( p in vars ) 79 | output += `--${p}:${vars[p]}; ` 80 | 81 | return output 82 | }, 83 | 84 | /** 85 | * Try to extract & parse CSS variables which would be used as default values 86 | * for knobs wgucg has no "value" property defined. 87 | * @param {Object} data Knobs data 88 | */ 89 | getKnobValueFromCSSVar( data ){ 90 | let value 91 | 92 | // when/if "value" property is unspecified in the knob's data, assume 93 | // there's a CSS variable already set, so try to get the value from it: 94 | if( !("value" in data) && data.cssVar && data.cssVar.length ){ 95 | let CSSVarTarget = data.cssVar[2] || this.settings.CSSVarTarget 96 | 97 | if( CSSVarTarget.length ) 98 | CSSVarTarget = CSSVarTarget[0] 99 | 100 | value = getComputedStyle(CSSVarTarget).getPropertyValue(`--${data.cssVar[0]}`).trim() 101 | 102 | // if type "range" - parse value as unitless 103 | if( data.type == 'range' ) 104 | value = parseInt(value) 105 | 106 | // if type "color" - parse value as color 107 | if( data.type == 'color' && !value ) 108 | value = 'transparent' 109 | 110 | // if type "checkbox" - if variable exists it means the value should be "true" 111 | 112 | // if( isNaN(value) ) 113 | // console.warn("@yaireo/knobs -", "Unable to parse variable value:", data.cssVar[0]) 114 | 115 | return value 116 | } 117 | }, 118 | 119 | templates, 120 | 121 | /* // not in use since onClickOutside is fired (which is managed by color-picker) 122 | hideColorPickers( exceptNode ){ 123 | document.querySelectorAll('.color-picker').forEach(elm => { 124 | if( elm != exceptNode ){ 125 | elm.classList.add('hidden') 126 | } 127 | }) 128 | }, 129 | */ 130 | toggleColorPicker( inputElm ){ 131 | const value = inputElm.value, 132 | name = inputElm.dataset.name, 133 | knobData = this.getKnobDataByName(name), 134 | // { position } = this.settings.theme, 135 | // totalHeight = this.DOM.scope.clientHeight, 136 | that = this 137 | 138 | let cPicker = inputElm.colorPicker 139 | 140 | // if already visible, do nothing 141 | if( cPicker ){ 142 | cPicker.DOM.scope.classList.remove('hidden') 143 | return 144 | } 145 | 146 | cPicker = cPicker || new ColorPicker({ 147 | defaultFormat: knobData.defaultFormat, 148 | color: value, 149 | className: 'hidden', 150 | swatches: knobData.swatches || [], 151 | swatchesLocalStorage: true, 152 | 153 | // because the color-picker is outside the iframe, "onClickOutside" will not register 154 | // clicked within the iframe (knobs area). 155 | onClickOutside(e){ 156 | const isHidden = cPicker.DOM.scope.classList.contains('hidden'); 157 | 158 | resizeObserver.observe(document.body) 159 | intersectionObserver.observe(cPicker.DOM.scope) 160 | 161 | // if( !isHidden ) 162 | // that.hideColorPickers( cPicker.DOM.scope ) // hides any other shown color-picker except this one 163 | 164 | let action = 'add' 165 | 166 | // if clicked on the input element, toggle picker's visibility 167 | if( e.target == inputElm ) action = 'toggle' 168 | // if "escape" key was pressed, add the "hidden" class 169 | if( e.key == 'Escape' ) action = 'add' 170 | 171 | if( !isHidden ){ 172 | resizeObserver.unobserve(document.body) 173 | intersectionObserver.unobserve(cPicker.DOM.scope) 174 | } 175 | 176 | cPicker.DOM.scope.classList[action]('hidden') 177 | }, 178 | 179 | onInput(color){ 180 | inputElm.value = color 181 | that.onInput({ type:'input', target:inputElm }) 182 | that.onChange({ type:'change', target:inputElm }) 183 | }, 184 | }) 185 | 186 | // cPicker.setColor(value) 187 | 188 | if( !document.body.contains(cPicker.DOM.scope) ){ 189 | cPicker.DOM.scope.insertAdjacentHTML('afterbegin', `

${knobData.label}

`) 190 | inputElm.colorPicker = cPicker 191 | this.DOM.iframe.before(cPicker.DOM.scope) 192 | // document.body.appendChild(cPicker.DOM.scope) 193 | } 194 | 195 | const observerCallback = () => { 196 | position({ target:cPicker.DOM.scope, ref:inputElm }) 197 | } 198 | 199 | const resizeObserver = new ResizeObserver(observerCallback) 200 | const intersectionObserver = new IntersectionObserver(observerCallback, {root:document, threshold:1}); 201 | 202 | resizeObserver.observe(document.body) 203 | intersectionObserver.observe(cPicker.DOM.scope) 204 | observerCallback() 205 | 206 | setTimeout(() => { 207 | cPicker.DOM.scope.querySelector('input').focus() 208 | cPicker.DOM.scope.classList.remove('hidden') 209 | }, 100) 210 | 211 | // adjust screen position offsets to the color picker 212 | // const colorPickerHeight = cPicker.DOM.scope.clientHeight 213 | // if( totalHeight >= colorPickerHeight ){ 214 | // if( position.includes('top') ){ 215 | // cPicker.DOM.scope.style.setProperty('--offset', colorPickerHeight + (totalHeight - colorPickerHeight)/2) 216 | // } 217 | // } 218 | }, 219 | 220 | /** 221 | * 222 | * @param {Object} data Knob input properties to be applied, excluding some 223 | * @returns String 224 | */ 225 | knobAttrs(data){ 226 | var attributes = `data-name="${data.__name}" is-knob-input`, 227 | blacklist = ['label', 'type', 'onchange', 'options', 'selected', 'cssvar', '__name', 'istoggled', 'defaultchecked', 'defaultvalue'] 228 | 229 | // each radio input has a `value` prop and the group itself (the knob) also has a value (initially from cloneKnobs.js) 230 | if( data.type === 'radio' && data.groupValue === data.value ) 231 | data.checked = true; 232 | 233 | for( var attr in data ){ 234 | if( attr == 'checked' && !data[attr] ) continue // if "checked" is "false", do not add this attribute 235 | if( !blacklist.includes(attr.toLowerCase()) ) 236 | attributes += ` ${attr}="${data[attr]}"` 237 | } 238 | 239 | return attributes 240 | }, 241 | 242 | getKnobDataByName( name ){ 243 | return this.knobs.filter(Boolean).find(d => d.__name == name) 244 | }, 245 | 246 | /** 247 | * Set (multiple) key/value properties for a certain Knob 248 | * @param {String} name knob's name 249 | * @param {Object} d Data to set (key/value) 250 | */ 251 | setKnobDataByName( name, d){ 252 | if( name && d && isObject(d) ){ 253 | const knobData = this.getKnobDataByName(name) 254 | 255 | for( let key in d ) 256 | // if type number, cast 257 | knobData[key] = +d[key] === d[key] ? +d[key] : d[key] 258 | } 259 | }, 260 | 261 | getInputByName( name ){ 262 | const inputs = this.getKnobElm(name).querySelectorAll(`[data-name="${name}"`); 263 | 264 | return inputs.length > 1 ? inputs : inputs[0] 265 | }, 266 | 267 | getKnobElm( name ){ 268 | return this.DOM.scope.querySelector(`#${name}`) 269 | }, 270 | 271 | /** 272 | * Get all other knobs which also affect the same CSS variables and change their value 273 | */ 274 | getSimilarKnobs( toKnob ){ 275 | return this.knobs.filter(knob => 276 | knob?.cssVar?.[0] && 277 | knob?.cssVar?.[0] == toKnob?.cssVar?.[0] && // affects same CSS variable 278 | knob.__name != toKnob.__name // exclude itself 279 | ) 280 | }, 281 | 282 | /** 283 | * sets the parent of an input element with some CSS variables 284 | * @param {HTMLElement} inputElm input element 285 | */ 286 | setParentNodeValueVars( inputElm ){ 287 | inputElm && [ 288 | ['--value', inputElm.value], 289 | ['--text-value', JSON.stringify(inputElm.value)] 290 | ].forEach(([name, value]) => inputElm?.parentNode.style.setProperty(name, value)) 291 | }, 292 | 293 | /** 294 | * updates the relevant DOM node (if CSS variable is applied) 295 | * should fire from a knob's input's (onChange) event listener 296 | * @param {Object} 297 | */ 298 | updateDOM({ cssVar, value, type, isToggled, cssVarsHSLA, __name:name }){ 299 | if( !cssVar || !cssVar.length ) return 300 | 301 | var [cssVarName, cssVarUnit, CSSVarTarget] = cssVar, 302 | targetElms = CSSVarTarget || this.settings.CSSVarTarget, 303 | knobInput = this.getInputByName(name), 304 | action = 'setProperty', 305 | vars = [[cssVarName, value]]; 306 | 307 | // units which are prefixed with '-' should not be used. 308 | // exit only to inform the user about the final units outcome, 309 | // when there is a CSS calculation involved with the raw number before applying the units (in css) 310 | if( cssVarUnit && cssVarUnit[0] != '-' ) 311 | vars[0][1] += cssVarUnit||'' 312 | 313 | if( !isToggled || (type == 'checkbox' && knobInput && !knobInput.checked) ) 314 | action = 'removeProperty'; 315 | 316 | if( type == 'color' && cssVarsHSLA ){ 317 | const hsla = CSStoHSLA(changeColorFormat(value, 'HSL')) 318 | vars.push([`${cssVarName}-h`, hsla[0]], 319 | [`${cssVarName}-s`, hsla[1] + '%'], 320 | [`${cssVarName}-l`, hsla[2] + '%'], 321 | [`${cssVarName}-a`, hsla[3] + '%']) 322 | } 323 | 324 | // if is a refference to a single-node, place in an array. 325 | // cannot use instanceof to check if is an element because some elements might be in iframes: 326 | // https://stackoverflow.com/a/14391528/104380 327 | if( Object.prototype.toString.call(targetElms).includes("Element") ) 328 | targetElms = [targetElms] 329 | 330 | if( targetElms && targetElms.length && value !== undefined && cssVarName ) 331 | for( let elm of targetElms ) 332 | for( let [prop, value] of vars ) 333 | elm.style[action](`--${prop}`, value) 334 | }, 335 | 336 | /** 337 | * Apply all knobs (or a single knob) changes and fire all knobs' "onChange" callbacks 338 | * @param {Object} knobsData specific knobs to apply to 339 | * @param {Boolean} reset should the value reset before applying 340 | */ 341 | applyKnobs( knobsData, reset ){ 342 | (knobsData || this.knobs).forEach(d => { 343 | if( !d || !d.__name || d.render ) return; // do not procceed if is a seperator 344 | 345 | var isType = name => d.type == name, 346 | inputElm = this.getInputByName(d.__name), 347 | e, 348 | vKey = reset ? 'defaultValue' : 'value', 349 | checkedKey = reset ? 'defaultChecked' : 'checked', 350 | resetTitle; 351 | 352 | // knob of type "radio" is the only one which has multiple inputs, 353 | // but only the seelcted (checked) one is the important one in this case 354 | if( isType('radio') ){ 355 | inputElm = [...inputElm]; 356 | 357 | if( reset ){ 358 | inputElm = inputElm.find(el => el.value == d[vKey]) // when resetting - find the input which should now be checked 359 | inputElm.checked = true 360 | } 361 | else 362 | inputElm = inputElm.find(el => el.checked) 363 | } 364 | 365 | e = { target:inputElm } 366 | this.setParentNodeValueVars(inputElm) 367 | 368 | if( !d.type || d.isToggled === false ) return 369 | 370 | if( isType('checkbox') ){ 371 | resetTitle = inputElm.checked = !!d.checked 372 | inputElm.checked = d[checkedKey] 373 | } 374 | else 375 | resetTitle = inputElm.value = d[vKey] 376 | 377 | this.setResetKnobTitle(d.__name, resetTitle) 378 | 379 | // wrote this specifically for knobs of type "select" which other knobs might also affect the same CSS variable 380 | // so the select value won't take affect if the current value of the input is not one of the possible options. 381 | // This can happen if a range slider, which has more free-range, set the value to something else, which also affected 382 | // the "select" knob. 383 | if( inputElm.value !== '' || inputElm.value === d[vKey] ){ 384 | this.onInput(e) 385 | this.onChange(e, true) 386 | } 387 | 388 | // for some reason, if the form was reset through the "reset" input, 389 | // the range slider's thumb is not moved because the value has not been refistered by the browser.. 390 | // so need to set the value again.. 391 | // SEEMS THE BUG HAS BEEN FIXED IN LATEST CHROME 392 | setTimeout(() => { 393 | if( !isType('checkbox') ) 394 | inputElm.value = d[vKey] 395 | 396 | if( isType('color') ) 397 | inputElm.title = inputElm.value 398 | }) 399 | 400 | this.setKnobChangedFlag(this.getKnobElm(d.__name), d.value != d.defaultValue) 401 | }) 402 | }, 403 | 404 | /** 405 | * 406 | * Sets the "title" attribute of the knob's "reset" button 407 | * @param {String} name [Knob name] 408 | * @param {String} title [text title, which is actually the default value of that knob] 409 | */ 410 | setResetKnobTitle( name, title ){ 411 | try{ 412 | title = "Reset to " + title 413 | this.getKnobElm(name).querySelector('.knobs__knob__reset').title = title 414 | } 415 | catch(err){} 416 | }, 417 | 418 | resetKnobByName( name ){ 419 | this.setKnobChangedFlag(this.getKnobElm(name), false) 420 | this.applyKnobs([this.getKnobDataByName(name)], true) 421 | }, 422 | 423 | calculateGroupsHeights(){ 424 | var groupElms = this.DOM.form.querySelectorAll('.fieldset__group__wrap') 425 | 426 | groupElms.forEach(groupElm => { 427 | groupElm.style.setProperty('--height', groupElm.clientHeight) 428 | }) 429 | }, 430 | 431 | setIframeProps( opts ){ 432 | var action = (this.state.visible == false ? 'remove' : 'set') + 'Property', 433 | // iframeBodyElm = this.DOM.iframe.contentWindow.document.body, 434 | style = this.DOM.iframe.style, 435 | { heightOffset = 0 } = opts || {}; 436 | 437 | 438 | if( action == 'setProperty' ){ 439 | style.setProperty(`--knobsWidth`, '2000px') 440 | style.setProperty(`--knobsHeight`, '10000px') 441 | } 442 | 443 | var { clientWidth, clientHeight } = this.DOM.scope 444 | 445 | style[action](`--knobsWidth`, clientWidth + 'px') 446 | style[action](`--knobsHeight`, (+clientHeight + +heightOffset) + 'px') 447 | }, 448 | 449 | // show/hide Knobs (as a whole) 450 | toggle( state ){ 451 | if( !this.DOM.mainToggler ) 452 | return 453 | 454 | if( state === undefined ) 455 | state = !this.DOM.mainToggler.checked 456 | 457 | this.state.visible = state; 458 | this.DOM.mainToggler.checked = state; 459 | 460 | // briefly set a big width/height for the iframe so it could be meassured correctly 461 | this.setIframeProps() 462 | }, 463 | 464 | toggleKnob( name, isToggled ){ 465 | let knobData = this.getKnobDataByName(name), 466 | key = knobData.type == 'checkbox' ? 'checked' : 'value', 467 | // knob can be either a chekbox or an input element with an actual value 468 | keyVal = isToggled 469 | ? key == 'checked' ? knobData.checked : knobData.value 470 | : key == 'checked' ? knobData.defaultChecked : knobData.value 471 | 472 | knobData.isToggled = isToggled 473 | knobData[key] = keyVal 474 | 475 | this.updateDOM(knobData) 476 | 477 | typeof knobData.onChange == 'function' && knobData.onChange(null, knobData) 478 | 479 | this.setPersistedData() // { [knobData.label]:knobData.type == 'checkbox' ? [inputElm.checked, knobData.value] : knobData.value } 480 | }, 481 | 482 | /** 483 | * This flag marks a knobs as "dirty" (one that was changed by the user), so the "reset" icon would be highlighted 484 | * @param {*} knobElm 485 | * @param {*} action 486 | */ 487 | setKnobChangedFlag( knobElm, action ){ 488 | knobElm && knobElm[(action == false ? 'remove' : 'set') + 'Attribute']('data-changed', true) 489 | }, 490 | 491 | build(){ 492 | if( this.settings.standalone ){ 493 | this.DOM.scope = parseHTML(this.templates.knobs.call(this, {withToggler:false})) 494 | } 495 | else{ 496 | const iframeDoc = this.createIframe() 497 | this.DOM.scope = iframeDoc.body.querySelector('.knobs') 498 | this.DOM.groups = iframeDoc.body.querySelector('.knobs__groups') 499 | this.DOM.mainToggler = iframeDoc.getElementById('knobsToggle') 500 | } 501 | 502 | this.DOM.form = this.DOM.scope.querySelector('form') 503 | 504 | this.render() 505 | setTimeout(this.bindEvents.bind(this)) 506 | }, 507 | 508 | /** 509 | * all the knobs are encapsulated inside an iframe for complete 510 | * sandboxing against outside styles/js potential interference. 511 | * 512 | * Creates & appends the iframe to the DOM 513 | * Also appends all the styles to the iframe 514 | */ 515 | createIframe(){ 516 | var iframeDoc, 517 | theme = this.settings.theme, 518 | cssText; 519 | 520 | this.DOM.iframe = document.createElement('iframe') 521 | this.DOM.iframe.setAttribute('class', 'knobsIframe') 522 | this.DOM.iframe.style.cssText = ` 523 | border: none; 524 | position: fixed; 525 | z-index: 999999; 526 | ${(theme.position+" ").split(" ").join(":0;")} 527 | width: var(--knobsWidth, 56px); 528 | height: clamp(56px, var(--knobsHeight, 56px), 100%); 529 | ` 530 | 531 | // first append the iframe to the DOM 532 | this.settings.appendTo.appendChild(this.DOM.iframe) 533 | 534 | // now access is obtained to the iframe's document 535 | iframeDoc = this.DOM.iframe.contentWindow.document 536 | 537 | // inject HTML template to the iframe 538 | iframeDoc.open() 539 | 540 | // dump all the HTML & styles into the iframe 541 | iframeDoc.write(this.templates.scope.call(this)) 542 | 543 | cssText = mainStyles + theme.styles + `.knobs{ ${this.getCSSVariables(theme)} }` 544 | 545 | iframeDoc.head.insertAdjacentHTML("beforeend", ``) 546 | 547 | // done manipulating the iframe's content 548 | iframeDoc.close() 549 | 550 | return iframeDoc 551 | }, 552 | 553 | render(){ 554 | // maps a flat knobs array into multiple groups, after each label (if label exists) 555 | // this step is needed so each group (after item in the knobs array after a "label" item) could be 556 | // expanded/collapsed individually. 557 | var knobsGroups = getKnobsGroups(this.knobs) 558 | 559 | //create an HTML-string from the template 560 | var fieldsetElms = knobsGroups.map(this.templates.fieldset.bind(this)).join("") 561 | 562 | // cleanup & inject knobs into the
element 563 | this.DOM.groups.innerHTML = fieldsetElms 564 | 565 | this.calculateGroupsHeights() 566 | 567 | // calculate iframe size 568 | this.DOM.mainToggler && this.toggle(this.DOM.mainToggler.checked) 569 | 570 | this.applyKnobs() 571 | 572 | // apply custom scripts (per knob) 573 | this.knobs.forEach(knob => knob && knob.script && knob.script(this, knob.__name)) 574 | 575 | // color picker CSS 576 | const hostCSSExists = [...document.styleSheets].some(s => s.title == '@yaireo/knobs') 577 | 578 | if( !hostCSSExists ) 579 | document.head.insertAdjacentHTML('beforeend', ``) 583 | 584 | this.trigger('render'); 585 | this.settings?.callbacks?.render() 586 | }, 587 | 588 | color: { 589 | format: changeColorFormat, 590 | CSStoHSLA 591 | } 592 | }; 593 | 594 | export default Knobs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /knobs.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @yaireo/knobs - UI knobs controllers for JS/CSS live manipulation of various parameters 3 | * 4 | * @version v1.3.6 5 | * @homepage https://github.com/yairEO/knobs 6 | */ 7 | 8 | /*! Knobs 1.3.6 MIT | https://github.com/yairEO/knobs */ 9 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Knobs=t()}(this,(function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var t={}; 10 | /*! Color-Picker 0.12.0 MIT | https://github.com/yairEO/color-picker */ 11 | !function(e,t){!function(e){var t=e=>(new DOMParser).parseFromString(e.trim(),"text/html").body.firstElementChild,o={color:"white",onInput:e=>e,onChange:e=>e,buttons:{undo:{icon:"↶",title:"Undo"},format:{icon:"⇆",title:"Switch Color Format"},add:{icon:"+",title:"Add to Swatches"}}};const a=e=>e.match(/\((.*)\)/)[1].split(",").map(Number),r=e=>Object.assign([0,0,0,1],e.match(/\((.*)\)/)[1].split(",").map(((e,t)=>3!=t||e.includes("%")?parseFloat(e):100*parseFloat(e)))),n=e=>`hsla(${e.h}, ${e.s}%, ${e.l}%, ${e.a}%)`,i=e=>e.toFixed(1).replace(".0",""),s=e=>{const[t,o,a,r]=(e=>e.match(/\w\w/g))(e),[n,i,s]=[t,o,a].map((e=>parseInt(e,16)));return`rgba(${n},${i},${s},${r?(parseInt(r,16)/255).toFixed(2):1})`},l=e=>{var t,o=document.createElement("canvas").getContext("2d");return o.fillStyle=e,"#"==(t=o.fillStyle)[0]?t:c(t)},c=e=>{const[t,o,r,n]=a(e),i="#"+[t,o,r].map((e=>e.toString(16).padStart(2,"0"))).join("");return 3==e.length?i:i+Math.round(255*n).toString(16).padStart(2,"0")},h=e=>{let[t,o,r,n]=a(e);t/=255,o/=255,r/=255;let s=Math.max(t,o,r),l=Math.min(t,o,r),c=s-l,h=0,d=0,p=((s+l)/2).toPrecision(5);return c&&(d=p>.5?c/(2-s-l):c/(s+l),h=s==t?(o-r)/c+(o(t=(t+"").toLowerCase(),e=l(e),"hex"==t?e:t.startsWith("hsl")?n(h(s(e))):t.startsWith("rgb")?s(e):e);function p({name:e,min:t=0,max:o=100,value:a}){return`
\n \n \n
\n
`}function b(e){const{buttons:{undo:t,format:o}}=this.settings;return`\n
\n \n \n \n
\n
\n `}function u(e,t){const{buttons:{add:o}}=this.settings;return`\n
\n \n ${e.map((e=>g(e,t.includes(e)))).join("")}\n
\n `}function g(e,t){return`
${t?"":''}
`}var v=Object.freeze({__proto__:null,scope:function(){const{h:e,s:t,l:o,a:a}=this.color;return`\n
\n ${p({name:"hue",value:e,max:"360"})}\n ${p({name:"saturation",value:t})}\n ${p({name:"lightness",value:o})}\n ${p({name:"alpha",value:a})}\n \n ${b.call(this,this.color)}\n ${this.swatches?u.call(this,this.swatches,this.initialSwatches):""}\n
\n `},slider:p,swatch:g,swatches:u,value:b});function f(){this.syncGlobalSwatchesWithLocal()}function m(e){e.preventDefault();const{value:t,max:o}=e.target,a=-1*Math.sign(e.deltaY);t&&o&&(e.target.value=Math.min(Math.max(+t+a,0),+o),x.call(this,e))}function k(e){"Escape"==e.key&&this.settings.onClickOutside(e)}function _(e){this.DOM.scope.contains(e.target)||this.settings.onClickOutside(e)}function x(e){const{name:t,value:o,type:a}=e.target;"range"==a&&this.setColor({...this.color,[t[0]]:+o})}function y(e){const{type:t}=e.target;"range"!=t&&"text"!=t||(this.history.last=this.color)}function w(e){this.setColor(this.getHSLA(e.target.value)),this.DOM.value.blur()}function S(e){const{name:t,parentNode:o,classList:a,title:r}=e.target;"format"==t?this.swithFormat():"undo"==t?this.history.undo():"addSwatch"==t?this.addSwatch():"removeSwatch"==t?this.removeSwatch(o,o.title):a.contains("color-picker__swatch")&&r&&(this.history.last=this.color,this.setColor(this.getHSLA(r)))}var C=Object.freeze({__proto__:null,bindEvents:function(){[["scope","input",x],["scope","change",y],["scope","click",S],["scope","wheel",m],["value","change",w]].forEach((([e,t,o])=>this.DOM[e].addEventListener(t,o.bind(this),{pasive:!1}))),window.addEventListener("storage",f.bind(this)),this.settings.onClickOutside&&(document.body.addEventListener("click",_.bind(this)),window.addEventListener("keydown",k.bind(this)))}});function z(){const e=()=>this.settings.onChange(this.CSSColor),t=this.setColor.bind(this);return{_value:[this.color],get pop(){return this._value.pop()},get previous(){return this._value[this._value.length-2]},set last(t){this._value.push(t),e()},undo(){if(this._value.length>1){this.pop;let o=this._value[this._value.length-1];return t(o),e(),o}}}}const $=(e,t)=>e.some((e=>l(e)==l(t))),O="@yaireo/color-picker/swatches";var M=Object.freeze({__proto__:null,addSwatch:function(e=this.CSSColor){if($(this.swatches,e))return;const o=t(this.templates.swatch(e));o.classList.add("cp_remove"),this.DOM.swatches.prepend(o),setTimeout((()=>o.classList.remove("cp_remove")),0),this.swatches.unshift(e),this.sharedSwatches.unshift(e),this.getSetGlobalSwatches(this.sharedSwatches)},getSetGlobalSwatches:function(e){const t=this.settings.swatchesLocalStorage,o="string"==typeof t?t:"";return t&&e&&(localStorage.setItem(O+o,e.join(";")),dispatchEvent(new Event("storage"))),localStorage[O+o]?.split(";").filter(String)||[]},removeSwatch:function(e,t){e.classList.add("cp_remove"),setTimeout((()=>e.remove()),200);const o=e=>e!=t;this.swatches=this.swatches.filter(o),this.sharedSwatches=this.sharedSwatches.filter(o),this.getSetGlobalSwatches(this.sharedSwatches)},syncGlobalSwatchesWithLocal:function(){this.sharedSwatches=this.getSetGlobalSwatches(),this.swatches=this.sharedSwatches.concat(this.initialSwatches),this.DOM.swatches&&setTimeout((()=>{const e=t(this.templates.swatches.call(this,this.swatches,this.initialSwatches));this.DOM.swatches.replaceWith(e),this.DOM.swatches=e}),500)}});function T(e){this.settings=Object.assign({},o,e);const{color:t,defaultFormat:a,swatches:r}=this.settings;this.DOM={},this.sharedSwatches=this.getSetGlobalSwatches(),this.initialSwatches=r||[],this.swatches=r&&this.sharedSwatches.concat(this.initialSwatches),this.color=d(t,a),this.history=z.call(this),this.init()}T.prototype={templates:v,...M,...C,getColorFormat:e=>"#"==e[0]?"hex":e.indexOf("hsl")?e.indexOf("rgb")?"":"rgba":"hsla",getHSLA(e){let t;if(e)return e+""=="[object Object]"&&Object.keys(e).join("").startsWith("hsl")?e:(this.colorFormat=this.getColorFormat(e),e.indexOf("hsla")?(e=l(e),e=s(e),t=h(e)):(t=r(e),t={h:t[0],s:t[1],l:t[2],a:t[3]}),t)},swithFormat(){switch(this.colorFormat){case"":case"hex":this.colorFormat="rgba";break;case"rgba":this.colorFormat="hsla";break;case"hsla":this.colorFormat="hex"}this.setCSSColor(),this.DOM.value.value=this.CSSColor},updateRangeSlider(e,t){const o=this.DOM.scope.querySelector(`input[name="${e}"]`);o&&(o.value=t,o.parentNode.style.setProperty("--value",t),o.parentNode.style.setProperty("--text-value",JSON.stringify(""+Math.round(t))),this.updateCSSVar(e,t))},setCSSColor(){this.CSSColor=l(n(this.color)),"rgba"==this.colorFormat?this.CSSColor=s(this.CSSColor):"hsla"==this.colorFormat&&(this.CSSColor=n(this.color)),this.DOM.scope&&this.DOM.scope.setAttribute("data-color-format",this.colorFormat),this.settings.onInput(this.CSSColor)},setColor(e){e&&(e=this.getHSLA(e),this.color=e,this.setCSSColor(),this.DOM.scope&&this.updateAllCSSVars(),this.DOM.value&&(this.DOM.value.value=this.CSSColor))},updateCSSVar(e,t){this.DOM.scope.style.setProperty(`--${e}`,t)},updateAllCSSVars(){const e=this.NamedHSLA(this.color);Object.entries(e).forEach((([e,t])=>{this.updateRangeSlider(e,t)}))},NamedHSLA:e=>({hue:e.h,saturation:e.s,lightness:e.l,alpha:e.a}),build(){const e=this.templates.scope.call(this);this.DOM.scope=t(e),this.DOM.value=this.DOM.scope.querySelector('input[name="value"]'),this.DOM.swatches=this.DOM.scope.querySelector(".color-picker__swatches")},init(){this.build(),this.setColor(this.color),this.bindEvents()}},e.CSStoHSLA=r,e.HSLAtoCSS=n,e.any_to_hex=l,e.changeColorFormat=d,e.default=T,e.hex_rgba=s,e.rgba_hsla=h,Object.defineProperty(e,"__esModule",{value:!0})}(t)}(0,t);var o=e(t);const a=e=>{var t,{target:o,ref:n,offset:i,placement:s,prevPlacement:l,useRaf:c=!0,track:h}=e,d={x:n.x,y:n.y,h:0,w:0},p=n&&n.x?{...n}:{},b=document.documentElement,u=b.clientWidth,g=b.clientHeight,v={w:o.clientWidth,h:o.clientHeight};if(r=c?r:e=>e(),l=l||[],s=(s||" ").split(" ").map(((e,t)=>e||["center","below"][t])),i=i?[i[0]||0,i[1]||i[0]||0]:[0,0],n.parentNode&&(t=n.ownerDocument.defaultView,p=n.getBoundingClientRect(),d.x=p.x,d.y=p.y,d.w=p.width,d.h=p.height,t!=t.parent))for(let k of t.parent.document.getElementsByTagName("iframe"))if(k.contentWindow===t){let _=k.getBoundingClientRect();d.x+=_.x,d.y+=_.y}"left"==s[0]?d.x-=v.w+i[0]:"right"==s[0]?d.x+=d.w+i[0]:d.x-=v.w/2-d.w/2,"above"==s[1]?d.y-=v.h+i[1]:"below"==s[1]?d.y+=d.h+i[1]:d.y-=v.h/2-d.h/2;const f={top:d.y<0,bottom:d.y+v.h>g,left:d.x<0,right:d.x+v.w>u},m=t=>a({...e,placement:t.join(" "),prevPlacement:s});if(f.left&&"right"!=l[0])return m(["right",s[1]]);if(f.right&&"left"!=l[0])return m(["left",s[1]]);if(f.bottom&&"above"!=l[1])return m([s[0],"above"]);if(f.top&&"below"!=l[1])return m([s[0],"below"]);if(r((()=>{o.setAttribute("positioned",!0),o.setAttribute("data-placement",s.join(" ")),o.setAttribute("data-pos-overflow",Object.entries(f).reduce(((e,[t,o])=>o?`${e} ${t}`:e),"").trim()),[["pos-left",f.right?u-v.w:d.x],["pos-top",d.y],["pos-target-width",v.w],["pos-target-height",v.h],["pos-ref-width",p.width||0],["pos-ref-height",p.height||0],["pos-ref-left",p.x],["pos-ref-top",p.y],["window-scroll-y",window.scrollY],["window-scroll-x",window.scrollX]].forEach((([e,t])=>o.style.setProperty("--"+e,Math.round(t))))})),h?.scroll&&!o.position__trackedScroll){function x(t){t.target.contains(refElement)&&a(e)}o.position__trackedScroll=!0,window.addEventListener("scroll",x,!0)}return{pos:d,placement:s}};let r=requestAnimationFrame||(e=>setTimeout(e,1e3/60));var n=e=>e+""=="[object Object]";const i=(e,...t)=>{if(!t.length)return e;const o=t.shift();if(n(e)&&n(o))for(const t in o)n(o[t])?(e[t]||Object.assign(e,{[t]:{}}),i(e[t],o[t])):Object.assign(e,{[t]:o[t]});return i(e,...t)};const s=()=>Math.random().toString(36).slice(-6);var l="
\n
\n
\n
\n
\n";function c({withToggler:e=!0}){const{visible:t,live:o,theme:a}=this.settings;return`\n \n `}function h(e){return e.render&&!e.label?`
${e.render}
`:e?`
\n \n \n \n
\n `:void 0}function d(e){let{label:t,type:o,step:a,min:r,max:n,value:i,name:s,options:l}=e;return"range"==o?`\n
\n \n \n
\n
`:"checkbox"==o?`\n
\n \n
\n
`:"radio"==o&&l?.length?(e.name=e.name||t.toLowerCase().replaceAll(" ","-"),l.map(((t,o)=>``)).join("")):"select"==o&&l?.length?`\n `:("color"==o&&(o="text"),`
`)}var p=Object.freeze({__proto__:null,fieldset:function(e){var t,o;if(n(e[0]))o=e;else{[t,...o]=e,t=function({label:e,checked:t,knobsCount:o}){var a=e.replace(/ /g,"-")+Math.random().toString(36).slice(-6);return`\n `}({...t instanceof Array?{label:t[0],checked:!!t[1]}:{label:t,checked:!0},knobsCount:o.length})}return`
\n ${t||""}\n
\n
\n ${o.map(h.bind(this)).join("")}\n
\n
\n
`},knob:h,knobs:c,scope:function(){const{visible:e}=this.settings;return`\n \n ${c.call(this,{})}\n `}});const b=window.requestAnimationFrame||(e=>window.setTimeout(e,1e3/60)),u=(e,t)=>e.classList.contains(t);function g(){let e,t=this;new ResizeObserver((o=>{clearTimeout(e),e=setTimeout((()=>{t.setIframeProps()}),500)})).observe(this.settings.appendTo)}var v=Object.freeze({__proto__:null,bindEvents:function(){this.eventsRefs=this.eventsRefs||{change(e){e.target.dataset.name&&this.onChange(e)},input(e){try{let t;u(e.target,"toggleSection")&&e.target.checked&&(t=e.target.parentNode.querySelector(".fieldset__group"),this.setIframeProps({heightOffset:9999}))}catch(e){}e.target.hasAttribute("is-knob-input")?(this.onInput(e),this.onChange(e)):u(e.target,"knobs__knob__toggle")&&this.toggleKnob(e.target.dataset.forKnob,e.target.checked)},transitionstart(e){u(e.target,"fieldset__group__wrap")&&e.target.parentNode.setAttribute("transitioned",1)},transitionend(e){u(e.target,"fieldset__group__wrap")&&(e.target.parentNode.removeAttribute("transitioned"),this.setIframeProps())},wheel(e){const{value:t,max:o,step:a,type:r}=e.target,n=Math.sign(e.deltaY)*(+a||1)*-1;"range"==r&&e.preventDefault(),t&&o&&(e.target.value=Math.min(Math.max(+t+n,0),+o),this.onInput(e),this.onChange(e))},mainToggler(e){this.toggle(e.target.checked)},reset:this.applyKnobs.bind(this,null,!0),submit:this.onSubmit.bind(this),click:this.onClick.bind(this),focusin:this.onFocus.bind(this)},[["scope","click"],["form","change"],["form","input"],["form","reset"],["form","submit"],["form","focusin"],["form","transitionend"],["form","transitionstart"],["scope","wheel"],["mainToggler","change",this.eventsRefs.mainToggler.bind(this)]].forEach((([e,t,o])=>this.DOM[e]&&this.DOM[e].addEventListener(t,o||this.eventsRefs[t].bind(this),{passive:!1}))),g.call(this)},onChange:function(e,o){const a=e.target.dataset.name;this.setKnobChangedFlag(this.getKnobElm(a));const r=this.getKnobDataByName(a),n="input"==e.type&&r&&"range"!=r.type,i=r&&"checkbox"==r.type,s={};if(!r)return void console.warn("Knob data was not found:",{name:a,knobData:r});const l=o?[]:this.getSimilarKnobs(r);l.length&&l.forEach((e=>{const t=this.getInputByName(e.__name);t.value=r.value,this.onInput({target:t})})),(i||this.settings.live)&&("input"==e.type&&n||(b((()=>this.updateDOM(r))),"color"===r.type&&(s.hsla=t.CSStoHSLA(t.changeColorFormat(r.value,"HSL"))),"function"==typeof r.onChange&&r.onChange(e,r,s)))},onClick:function(e){const{target:t}=e;u(t,"knobs__knob__reset")&&this.resetKnobByName(t.name),"color"==t.dataset.type&&setTimeout((e=>this.toggleColorPicker(t)),100)},onFocus:function(e){},onInput:function(e){const t=e.target,{type:o,value:a,checked:r,dataset:{name:n}}=t,i="checkbox"==o,{label:s}=this.getKnobDataByName(n);this.setParentNodeValueVars(t),this.setKnobDataByName(n,i?{checked:r}:{value:a}),null!=a&&s&&this.setPersistedData()},onSubmit:function(e){e.preventDefault();var t=e.target.querySelectorAll("input[data-name]");return this.settings.live=!0,t.forEach((e=>this.onChange({target:{value:e.value,type:e.type,dataset:{name:e.dataset.name}}}))),this.settings.live=!1,!1}});const f="@yaireo/knobs/knobs",m=e=>"string"==typeof e||"number"==typeof e;var k=Object.freeze({__proto__:null,getPersistedData:function(){let e,t=this.settings.persist,o=m(t)?"/"+t:"";if(1==localStorage.getItem(`${f+o}/v`))try{e=JSON.parse(localStorage[f+o])}catch(e){}return e},setPersistedData:function(){let e=this.settings.persist,t=m(e)?"/"+e:"";if(e){let e=JSON.stringify(this.knobs);localStorage.setItem(`${f+t}/v`,1),localStorage.setItem(f+t,e),dispatchEvent(new Event("storage"))}}}),_={visible:0,live:!0,theme:{flow:"horizontal",styles:"",RTL:!1,position:"top right",primaryColor:"#0366D6","range-value-background":"#FFF","base-color":"#000",textColor:"white",border:"none"}};function x(e){if(!window.CSS||!CSS.supports("top","var(--a)"))return this;const{knobs:t=[],...o}=e||{};this.settings=i({..._,appendTo:document.body},o),i(this,function(){var e=document.createTextNode("");function t(t,o,a){a&&o.split(/\s+/g).forEach((o=>e[t+"EventListener"].call(e,o,a)))}return{off(e,o){return t("remove",e,o),this},on(e,o){return o&&"function"==typeof o&&t("add",e,o),this},trigger(t,o,a){var r;if(a=a||{cloneData:!0},t){try{var n="object"==typeof o?o:{value:o};if((n=a.cloneData?i({},n):n).knobs=this,o instanceof Object)for(var s in o)o[s]instanceof HTMLElement&&(n[s]=o[s]);r=new CustomEvent(t,{detail:n})}catch(e){console.warn(e)}e.dispatchEvent(r)}}}}()),this.knobs=t,this.DOM={},this.state={},this.build()}return x.prototype={_types:["range","color","checkbox","text"],...v,...k,cloneKnobs:function(e,t){return e.map((e=>{if(e&&e.type){if(e.__name=e.__name||(e.label?.replaceAll(/[^a-zA-Z0-9 ]/g,"").replaceAll(" ","-").toLowerCase()||"")+"-"+s(),e.defaultValue=e.defaultValue??e.value??this.getKnobValueFromCSSVar(e)??"",e.defaultChecked=e.defaultChecked??!!e.checked,e.isToggled=e.isToggled??!0,t){let o=t.find((t=>t.label&&t.label==e.label));if(o)return e.defaultValue&&(o.defaultValue=e.defaultValue),e.options&&(o.options=e.options),o}"range"==e.type?(e.value=+e.value||e.defaultValue,e.defaultValue=+e.defaultValue):"checkbox"==e.type?e.checked=e.checked||e.defaultChecked:e.value=e.value||e.defaultValue}return e.render&&(e.__name="custom-"+s()),e.cssVar?{...e,cssVar:[...e.cssVar]}:n(e)?{...e}:e}))},set knobs(e){e&&e instanceof Array&&(this._knobs=this.cloneKnobs(e,this.getPersistedData()),this.DOM&&this.render())},get knobs(){return this._knobs},getCSSVariables({flow:e,styles:o,RTL:a,position:r,...n}){var i,s="";this.settings.knobsToggle&&(n["knobs-toggle"]=1);const l=t.changeColorFormat(n["base-color"],"hsl"),c=t.CSStoHSLA(l);for(i in n["base-color"]=`${c[0]}, ${c[1]}%`,n["base-color-l"]=`${c[2]}%`,n["base-color-a"]=`${c[3]}%`,n)s+=`--${i}:${n[i]}; `;return s},getKnobValueFromCSSVar(e){let t;if(!("value"in e)&&e.cssVar&&e.cssVar.length){let o=e.cssVar[2]||this.settings.CSSVarTarget;return o.length&&(o=o[0]),t=getComputedStyle(o).getPropertyValue(`--${e.cssVar[0]}`).trim(),"range"==e.type&&(t=parseInt(t)),"color"!=e.type||t||(t="transparent"),t}},templates:p,toggleColorPicker(e){const t=e.value,r=e.dataset.name,n=this.getKnobDataByName(r),i=this;let s=e.colorPicker;if(s)return void s.DOM.scope.classList.remove("hidden");s=s||new o({defaultFormat:n.defaultFormat,color:t,className:"hidden",swatches:n.swatches||[],swatchesLocalStorage:!0,onClickOutside(t){const o=s.DOM.scope.classList.contains("hidden");c.observe(document.body),h.observe(s.DOM.scope);let a="add";t.target==e&&(a="toggle"),"Escape"==t.key&&(a="add"),o||(c.unobserve(document.body),h.unobserve(s.DOM.scope)),s.DOM.scope.classList[a]("hidden")},onInput(t){e.value=t,i.onInput({type:"input",target:e}),i.onChange({type:"change",target:e})}}),document.body.contains(s.DOM.scope)||(s.DOM.scope.insertAdjacentHTML("afterbegin",`

${n.label}

`),e.colorPicker=s,this.DOM.iframe.before(s.DOM.scope));const l=()=>{a({target:s.DOM.scope,ref:e})},c=new ResizeObserver(l),h=new IntersectionObserver(l,{root:document,threshold:1});c.observe(document.body),h.observe(s.DOM.scope),l(),setTimeout((()=>{s.DOM.scope.querySelector("input").focus(),s.DOM.scope.classList.remove("hidden")}),100)},knobAttrs(e){var t=`data-name="${e.__name}" is-knob-input`,o=["label","type","onchange","options","selected","cssvar","__name","istoggled","defaultchecked","defaultvalue"];for(var a in"radio"===e.type&&e.groupValue===e.value&&(e.checked=!0),e)("checked"!=a||e[a])&&(o.includes(a.toLowerCase())||(t+=` ${a}="${e[a]}"`));return t},getKnobDataByName(e){return this.knobs.filter(Boolean).find((t=>t.__name==e))},setKnobDataByName(e,t){if(e&&t&&n(t)){const o=this.getKnobDataByName(e);for(let e in t)o[e]=+t[e]===t[e]?+t[e]:t[e]}},getInputByName(e){const t=this.getKnobElm(e).querySelectorAll(`[data-name="${e}"`);return t.length>1?t:t[0]},getKnobElm(e){return this.DOM.scope.querySelector(`#${e}`)},getSimilarKnobs(e){return this.knobs.filter((t=>t?.cssVar?.[0]&&t?.cssVar?.[0]==e?.cssVar?.[0]&&t.__name!=e.__name))},setParentNodeValueVars(e){e&&[["--value",e.value],["--text-value",JSON.stringify(e.value)]].forEach((([t,o])=>e?.parentNode.style.setProperty(t,o)))},updateDOM({cssVar:e,value:o,type:a,isToggled:r,cssVarsHSLA:n,__name:i}){if(e&&e.length){var[s,l,c]=e,h=c||this.settings.CSSVarTarget,d=this.getInputByName(i),p="setProperty",b=[[s,o]];if(l&&"-"!=l[0]&&(b[0][1]+=l||""),(!r||"checkbox"==a&&d&&!d.checked)&&(p="removeProperty"),"color"==a&&n){const e=t.CSStoHSLA(t.changeColorFormat(o,"HSL"));b.push([`${s}-h`,e[0]],[`${s}-s`,e[1]+"%"],[`${s}-l`,e[2]+"%"],[`${s}-a`,e[3]+"%"])}if(Object.prototype.toString.call(h).includes("Element")&&(h=[h]),h&&h.length&&void 0!==o&&s)for(let e of h)for(let[t,o]of b)e.style[p](`--${t}`,o)}},applyKnobs(e,t){(e||this.knobs).forEach((e=>{if(e&&e.__name&&!e.render){var o,a,r=t=>e.type==t,n=this.getInputByName(e.__name),i=t?"defaultValue":"value",s=t?"defaultChecked":"checked";r("radio")&&(n=[...n],t?(n=n.find((t=>t.value==e[i]))).checked=!0:n=n.find((e=>e.checked))),o={target:n},this.setParentNodeValueVars(n),e.type&&!1!==e.isToggled&&(r("checkbox")?(a=n.checked=!!e.checked,n.checked=e[s]):a=n.value=e[i],this.setResetKnobTitle(e.__name,a),""===n.value&&n.value!==e[i]||(this.onInput(o),this.onChange(o,!0)),setTimeout((()=>{r("checkbox")||(n.value=e[i]),r("color")&&(n.title=n.value)})),this.setKnobChangedFlag(this.getKnobElm(e.__name),e.value!=e.defaultValue))}}))},setResetKnobTitle(e,t){try{t="Reset to "+t,this.getKnobElm(e).querySelector(".knobs__knob__reset").title=t}catch(e){}},resetKnobByName(e){this.setKnobChangedFlag(this.getKnobElm(e),!1),this.applyKnobs([this.getKnobDataByName(e)],!0)},calculateGroupsHeights(){this.DOM.form.querySelectorAll(".fieldset__group__wrap").forEach((e=>{e.style.setProperty("--height",e.clientHeight)}))},setIframeProps(e){var t=(0==this.state.visible?"remove":"set")+"Property",o=this.DOM.iframe.style,{heightOffset:a=0}=e||{};"setProperty"==t&&(o.setProperty("--knobsWidth","2000px"),o.setProperty("--knobsHeight","10000px"));var{clientWidth:r,clientHeight:n}=this.DOM.scope;o[t]("--knobsWidth",r+"px"),o[t]("--knobsHeight",+n+ +a+"px")},toggle(e){this.DOM.mainToggler&&(void 0===e&&(e=!this.DOM.mainToggler.checked),this.state.visible=e,this.DOM.mainToggler.checked=e,this.setIframeProps())},toggleKnob(e,t){let o=this.getKnobDataByName(e),a="checkbox"==o.type?"checked":"value",r=t?"checked"==a?o.checked:o.value:"checked"==a?o.defaultChecked:o.value;o.isToggled=t,o[a]=r,this.updateDOM(o),"function"==typeof o.onChange&&o.onChange(null,o),this.setPersistedData()},setKnobChangedFlag(e,t){e&&e[(0==t?"remove":"set")+"Attribute"]("data-changed",!0)},build(){if(this.settings.standalone)this.DOM.scope=(e=this.templates.knobs.call(this,{withToggler:!1}),(new DOMParser).parseFromString(e.trim(),"text/html").body.firstElementChild);else{const e=this.createIframe();this.DOM.scope=e.body.querySelector(".knobs"),this.DOM.groups=e.body.querySelector(".knobs__groups"),this.DOM.mainToggler=e.getElementById("knobsToggle")}var e;this.DOM.form=this.DOM.scope.querySelector("form"),this.render(),setTimeout(this.bindEvents.bind(this))},createIframe(){var e,t,o=this.settings.theme;return this.DOM.iframe=document.createElement("iframe"),this.DOM.iframe.setAttribute("class","knobsIframe"),this.DOM.iframe.style.cssText=`\n border: none;\n position: fixed;\n z-index: 999999;\n ${(o.position+" ").split(" ").join(":0;")}\n width: var(--knobsWidth, 56px);\n height: clamp(56px, var(--knobsHeight, 56px), 100%);\n `,this.settings.appendTo.appendChild(this.DOM.iframe),(e=this.DOM.iframe.contentWindow.document).open(),e.write(this.templates.scope.call(this)),t='\ufefflabel,button,input{cursor:pointer;font:12px Arial,sans-serif}body,form{padding:0;margin:0}[css-util-before]::before{content:"";opacity:.2;position:absolute;top:0;right:0;bottom:0;left:0}#knobsToggle+.knobs>label{--size: calc(var(--toggleSize)/2);--offset: calc(var(--toggleOffset));position:absolute;width:var(--size);height:var(--size);top:var(--offset);right:var(--offset);padding:calc((var(--toggleSize) - var(--size))/2);font-size:20px;line-height:1;z-index:1;color:var(--textColor)}#knobsToggle:not(:checked)+.knobs>label:hover+.knobs__bg{opacity:1;transform:scale(1.15)}#knobsToggle:checked+.knobs{display:inline-block}#knobsToggle:checked+.knobs>label{padding:0}#knobsToggle:checked+.knobs .knobs__bg{--corner-radius: 8px;--offset: calc(var(--corner-radius) * -1);top:var(--offset);right:var(--offset);bottom:var(--offset);left:var(--offset);border-radius:var(--corner-radius);margin:0;width:calc(100% + var(--corner-radius));height:calc(100% + var(--corner-radius));opacity:1;transition:.3s cubic-bezier(0.45, 0, 0.2, 1),margin .2s,border-radius .2s}#knobsToggle:checked+.knobs .knobs__labels{transform:none;transition:calc(var(--in-duration)*1s) var(--in-easing)}#knobsToggle:checked+.knobs .knobs__labels fieldset,#knobsToggle:checked+.knobs .knobs__labels .knobs__controls{transform:none;opacity:1;transition:calc(var(--in-duration)*1s) calc(var(--in-duration)*.5s) ease-out}html,body{overflow:hidden}.knobs{--background: hsla(var(--base-color), var(--base-color-l), var(--base-color-a));--opaqueColor-15: HSL(var(--base-color), calc(var(--base-color-l) + 15%));--range-track-color: var(--primaryColor);--knobs-gap: 3px;--side-pad: 12px;--toggleSize: 40px;--toggleOffset: 6px;--in-easing: cubic-bezier(.75,0,.35,1);--in-duration: .3;--color-size: 20px;--line-height: Max(0px, var(--color-size));--knobs-group-transition: .33s cubic-bezier(.45, 0, .2, 1);--LTR-Bool: 1;font:12px/1 "Fira Sans Condensed",sans-serif;color:var(--textColor);position:relative;overflow:hidden}.knobs[data-flow=compact]{--color-size: 16px}.knobs[data-flow=compact] label[data-type=range]{flex-flow:column;gap:var(--knobs-gap);padding-top:6px}.knobs[data-flow=compact] label[data-type=range] .range-slider{--thumb-size: 12px;--track-height: calc(var(--thumb-size)/2);width:100%}.knobs[data-flow=compact] label[data-type=range]~.knobs__knob__reset{align-self:flex-start;margin-top:.5ch}.knobs[data-flow=compact] label[data-type=range] .knobs__label__text{margin:0;padding:0}.knobs label{user-select:none;cursor:pointer}.knobs__bg{pointer-events:none;position:absolute;top:0;right:0;z-index:-1;margin:var(--toggleOffset);width:var(--toggleSize);height:var(--toggleSize);border-radius:50%;background:var(--background);opacity:.8;backdrop-filter:blur(8px);transition:120ms}.knobs__labels{display:flex;flex-flow:column;max-height:100%;border:var(--border);transform:translateX(calc(100.1% * var(--LTR-Bool)))}.knobs__labels fieldset{display:table;border:0;padding:0;margin:0;opacity:0;transform:translateX(calc(22% * var(--LTR-Bool)))}.knobs__labels fieldset:only-of-type>label{pointer-events:none}.knobs__labels fieldset:first-child:not([data-has-legend]){overflow:visible}.knobs__labels .fieldset__group[transition-done]{overflow:visible}.knobs__labels .fieldset__group__wrap{display:flex;flex-flow:column;gap:var(--knobs-gap);padding:var(--side-pad);transition:var(--knobs-group-transition)}.knobs__labels hr{border:0;border-top:1px solid var(--textColor);opacity:.25}.knobs__labels hr:last-of-type{margin-bottom:0}.knobs__labels label:not(.knobs__legend){order:5;flex:1;display:flex;position:relative;z-index:1}.knobs__labels .range-slider,.knobs__labels input[type=text]:not([size]),.knobs__labels input[type=number]:not([size]){min-width:200px}.knobs__labels label:not(.knobs__legend)>:last-child{width:100%;flex:1;text-align:right;align-self:center}.knobs__groups{flex:1;margin-top:calc(var(--side-pad)*2.5);overflow-y:scroll;scrollbar-width:none}.knobs__groups::-webkit-scrollbar{display:none}.knobs__groups>fieldset:first-child .knobs__knob:first-child .range-slider{--value-offset-y: 14px}.knobs__legend{display:flex;align-items:center;font-weight:700;opacity:.66;line-height:1.6;cursor:pointer;transition:.2s cubic-bezier(0.45, 0, 0.2, 1)}.knobs__legend[data-has-label]{gap:2ch}.knobs__legend[data-has-label]:hover{gap:4ch}.knobs__legend::before,.knobs__legend::after{content:"";height:1px;background:var(--textColor);flex:1;opacity:.5;transition:inherit}.knobs__legend:hover{opacity:.85}.knobs__legend>div{display:flex;align-items:center;gap:2ch}.knobs__legend__knobsCount{display:inline-block;border-radius:50%;width:1.5em;height:1.5em;line-height:1.6;font-size:.9em;text-align:center;overflow:hidden;position:relative;transition:var(--knobs-group-transition)}.knobs__legend__knobsCount::before{background:var(--textColor);opacity:.3}.knobs__legend__knobsCount:only-child{margin:0 2ch}.knobs .toggleSection:checked~.knobs__legend .knobs__legend__knobsCount{transform:scale(0);margin:0;width:0}.knobs .toggleSection:checked~.fieldset__group[transitioned]{overflow:hidden}.knobs .toggleSection:not(:checked)~.knobs__legend{margin-bottom:1em}.knobs .toggleSection:not(:checked)~.fieldset__group{overflow:hidden}.knobs .toggleSection:not(:checked)~.fieldset__group .fieldset__group__wrap{opacity:0;margin-top:calc(var(--height)*-1px);text-shadow:0px 3px 2px}.knobs[data-flow=compact] .knobs__knob__toggle{align-self:flex-start;margin-top:6px}.knobs__knob{display:flex;justify-content:flex-end;position:relative;line-height:var(--line-height);min-height:24px}.knobs__knob:hover .knobs__knob__label__text{opacity:1}.knobs__knob[data-changed] .knobs__knob__reset{opacity:.75;pointer-events:auto}.knobs__knob[data-changed] .knobs__knob__reset:hover{opacity:1;background:var(--textColor);color:var(--background);transition:0s}.knobs__knob__toggle{display:var(--knobs-toggle, none);order:1;align-self:center;margin:0 5px 0 0;appearance:none;width:12px;height:12px;outline:none;border-radius:50%;position:relative;text-align:center;line-height:10px}.knobs__knob__toggle::before{border:1px solid var(--textColor);opacity:.4;border-radius:3px}.knobs__knob__toggle::after{content:"";height:100%;z-index:5;width:999px;position:absolute;left:0;pointer-events:none}.knobs__knob__toggle:hover::before{opacity:1}.knobs__knob__toggle:checked:hover~*{text-decoration:line-through;transition:.15s}.knobs__knob__toggle:checked::after{content:"✔";color:var(--textColor);font-size:12px;text-shadow:-1px -2px var(--background),3px -2px var(--background);position:relative;z-index:1}.knobs__knob__toggle:not(:checked)~*{pointer-events:none !important;filter:grayscale(50%);opacity:.4;transition:.2s}.knobs__knob__toggle:not(:checked)~* ::-webkit-slider-thumb{pointer-events:none !important}.knobs__knob__toggle:not(:checked)~* ::-moz-slider-thumb{pointer-events:none !important}.knobs__knob__reset{order:0;pointer-events:none;margin-right:.5em;padding:0;align-self:center;color:inherit;background:none;border:0;cursor:pointer;opacity:.33;outline:none;border-radius:50%;width:2ch;height:2ch;user-select:none;transition:.15s ease-out}.knobs__knob__label__text{margin-right:2ch;white-space:nowrap;display:flex;align-items:center;opacity:.8;transition:80ms}.knobs__knob__label__text::after{content:attr(data-units);opacity:.5;margin-left:1ch}.leversIcon{width:56px;transform:scale(0.4);transform-origin:0 0}.leversIcon>div{display:flex;align-items:center;transition:transform .2s ease}.leversIcon>div:nth-child(1)::before{flex:.33;transition-delay:.3s}.leversIcon>div:nth-child(2){margin:2px 0}.leversIcon>div:nth-child(2)::after{flex:.33}.leversIcon>div:nth-child(3)::before{flex:.8;transition-delay:.1s}.leversIcon>div>b{display:inline-block;width:7.5px;height:7.5px;border-radius:50%;border:4px solid currentColor;margin:0 5px}.leversIcon>div::before,.leversIcon>div::after{content:"";height:5px;background:currentColor;border-radius:5px;flex:1;transition:flex .1s ease}.leversIcon>div::after{flex:auto;opacity:.33}@keyframes leversIcon{30%{flex:.2}80%{flex:5}}#knobsToggle:not(:checked)+.knobs>label:hover .leversIcon>div:nth-child(1)::before{animation:1s leversIcon ease infinite}#knobsToggle:not(:checked)+.knobs>label:hover .leversIcon>div:nth-child(2){margin:1px 0}#knobsToggle:not(:checked)+.knobs>label:hover .leversIcon>div:nth-child(2)::after{animation:1s .1s leversIcon ease reverse infinite}#knobsToggle:not(:checked)+.knobs>label:hover .leversIcon>div:nth-child(3)::before{animation:1.2s .15s leversIcon ease alternate infinite}#knobsToggle:checked+.knobs>label{--size: 18px;--offset: calc(var(--toggleOffset) + var(--size)/3)}#knobsToggle:checked+.knobs>label .leversIcon{width:65px;color:var(--textColor);transition:color .2s;transform:scale(0.3) translate(0, 6px);opacity:.7}#knobsToggle:checked+.knobs>label .leversIcon:hover{opacity:1}#knobsToggle:checked+.knobs>label .leversIcon b{transform:scale(0);margin:0;width:0}#knobsToggle:checked+.knobs>label .leversIcon>div::after{flex:0}#knobsToggle:checked+.knobs>label .leversIcon>div::before{flex:3;height:8px}#knobsToggle:checked+.knobs>label .leversIcon>div:nth-child(1){transform:rotate(45deg);transform-origin:20% 50%}#knobsToggle:checked+.knobs>label .leversIcon>div:nth-child(2){opacity:0}#knobsToggle:checked+.knobs>label .leversIcon>div:nth-child(3){transform:rotate(-45deg);transform-origin:0 0}#knobsToggle:checked+.knobs[data-position~=top] .knobs__bg{bottom:auto}#knobsToggle:checked+.knobs[data-position~=right] .knobs__bg{left:auto}#knobsToggle:checked+.knobs[data-position~=bottom]>label{top:auto;bottom:var(--offset)}#knobsToggle:checked+.knobs[data-position~=bottom] .knobs__bg{top:auto}#knobsToggle:checked+.knobs[data-position~=left]>label{right:auto;left:var(--offset)}#knobsToggle:checked+.knobs[data-position~=left] .knobs__bg{right:auto}#knobsToggle:checked+.knobs[data-position~=left][data-position~=bottom]{--control-left-pad: var(--toggleSize)}.knobs[data-position~=left]{--LTR-Bool: -1}.knobs label[data-type=select] .knobs__knob__inputWrap::before{--hide: Calc(var(--value) - var(--value));content:"N/A";font-style:italic;opacity:var(--hide);filter:opacity(0.5);position:absolute;right:2em;pointer-events:none}.knobs label[data-type=select]::after{content:"❯";pointer-events:none;align-self:center;transform:translate(-100%, var(--offset-y, -1px)) rotate(90deg) scaleY(0.8);transition:.1s}.knobs label[data-type=select]:hover{--offset-y: 1px}.knobs label[data-type=select] select{font:inherit;background:none;color:var(--textColor);padding:3px 0;cursor:pointer;border:none;outline:none;text-align-last:right;appearance:none;padding:0 1.1em 0 0}.knobs label[data-type=select] option{background:var(--background)}.knobs .range-slider{--fill-color: var(--range-track-color);--primaryColor: var(--range-value-background);--value-active-color: var(--range-track-color);--value-background: transparent;--value-background-hover: white;--value-offset-y: 9px;--progress-background: #444;--thumb-size: 14px;--track-height: calc(var(--thumb-size)/3);--ticks-thickness: 1px;--ticks-height: 0px;--show-min-max: none;--thumb-color: var(--range-track-color);--thumb-shadow: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/6) inset white;--thumb-shadow-active: 0 0 3px rgba(0,0,0,.2), 0 0 0 calc(var(--thumb-size)/4) inset white;color:rgba(0,0,0,0)}.knobs .range-slider>input:hover+output{box-shadow:0 0 0 3px var(--value-background),0 0 6px 4px var(--background)}.range-slider{--primary-color: #0366D6;--value-offset-y: var(--ticks-gap);--value-active-color: white;--value-background: transparent;--value-background-hover: var(--primary-color);--value-font: 700 12px/1 Arial;--fill-color: var(--primary-color);--progress-background: #EEE;--progress-radius: 20px;--track-height: calc(var(--thumb-size)/2);--min-max-font: 12px Arial;--min-max-opacity: .5;--min-max-x-offset: 10%;--thumb-size: 22px;--thumb-color: white;--thumb-shadow: 0 0 3px rgba(0,0,0,.4), 0 0 1px rgba(0,0,0,.5) inset, 0 0 0 99px var(--thumb-color) inset;--thumb-shadow-active: 0 0 0 calc(var(--thumb-size)/4) inset var(--thumb-color), 0 0 0 99px var(--primary-color) inset, 0 0 3px rgba(0,0,0,.4);--thumb-shadow-hover: var(--thumb-shadow);--ticks-thickness: 1px;--ticks-height: 5px;--ticks-gap: var(--ticks-height, 0);--ticks-color: silver;--step: 1;--ticks-count: (var(--max) - var(--min)) / var(--step);--maxTicksAllowed: 30;--too-many-ticks: Min(1, Max(var(--ticks-count) - var(--maxTicksAllowed), 0));--x-step: Max( var(--step), var(--too-many-ticks) * (var(--max) - var(--min)) );--tickIntervalPerc_1: Calc( (var(--max) - var(--min)) / var(--x-step) );--tickIntervalPerc: calc( (100% - var(--thumb-size))/var(--tickIntervalPerc_1) * var(--tickEvery, 1) );--value-a: Clamp(var(--min), var(--value, 0), var(--max));--value-b: var(--value, 0);--text-value-a: var(--text-value, "");--completed-a: calc((var(--value-a) - var(--min) ) / (var(--max) - var(--min)) * 100);--completed-b: calc((var(--value-b) - var(--min) ) / (var(--max) - var(--min)) * 100);--ca: Min(var(--completed-a), var(--completed-b));--cb: Max(var(--completed-a), var(--completed-b));--thumbs-too-close: Clamp( -1, 1000 * (Min(1, Max(var(--cb) - var(--ca) - 5, -1)) + 0.001), 1 );--thumb-close-to-min: Min(1, Max(var(--ca) - 5, 0));--thumb-close-to-max: Min(1, Max(95 - var(--cb), 0));box-sizing:content-box;display:inline-block;height:max(var(--track-height),var(--thumb-size));background:linear-gradient(to right, var(--ticks-color) var(--ticks-thickness), transparent 1px) repeat-x;background-size:var(--tickIntervalPerc) var(--ticks-height);background-position-x:calc(var(--thumb-size)/2 - var(--ticks-thickness)/2);background-position-y:var(--flip-y, bottom);padding-bottom:var(--flip-y, var(--ticks-gap));padding-top:calc(var(--flip-y)*var(--ticks-gap));position:relative;z-index:1}.range-slider[data-ticks-position=top]{--flip-y: 1}.range-slider::before,.range-slider::after{--offset: calc(var(--thumb-size)/2);content:counter(x);display:var(--show-min-max, block);font:var(--min-max-font);position:absolute;bottom:var(--flip-y, -2.5ch);top:calc(-2.5ch*var(--flip-y));opacity:clamp(0,var(--at-edge),var(--min-max-opacity));transform:translateX(calc(var(--min-max-x-offset) * var(--before, -1) * -1)) scale(var(--at-edge));pointer-events:none}.range-slider::before{--before: 1;--at-edge: var(--thumb-close-to-min);counter-reset:x var(--min);left:var(--offset)}.range-slider::after{--at-edge: var(--thumb-close-to-max);counter-reset:x var(--max);right:var(--offset)}.range-slider__values{position:relative;top:50%;line-height:0;text-align:justify;width:100%;pointer-events:none;margin:0 auto;z-index:5}.range-slider__values::after{content:"";width:100%;display:inline-block;height:0;background:red}.range-slider__progress{--start-end: calc(var(--thumb-size)/2);--clip-end: calc(100% - (var(--cb) ) * 1%);--clip-start: calc(var(--ca) * 1%);--clip: inset(-20px var(--clip-end) -20px var(--clip-start));position:absolute;left:var(--start-end);right:var(--start-end);top:calc(var(--ticks-gap)*var(--flip-y, 0) + var(--thumb-size)/2 - var(--track-height)/2);height:calc(var(--track-height));background:var(--progress-background, #EEE);pointer-events:none;z-index:-1;border-radius:var(--progress-radius)}.range-slider__progress::before{content:"";position:absolute;left:0;right:0;clip-path:var(--clip);top:0;bottom:0;background:var(--fill-color, black);box-shadow:var(--progress-flll-shadow);z-index:1;border-radius:inherit}.range-slider__progress::after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;box-shadow:var(--progress-shadow);pointer-events:none;border-radius:inherit}.range-slider>input{-webkit-appearance:none;width:100%;height:var(--thumb-size);margin:0;position:absolute;left:0;top:calc(50% - max(var(--track-height),var(--thumb-size))/2 + var(--ticks-gap)/2*var(--flip-y, -1));cursor:-webkit-grab;cursor:grab;outline:none;background:none}.range-slider>input:not(:only-of-type){pointer-events:none}.range-slider>input::-webkit-slider-thumb{appearance:none;border:none;height:var(--thumb-size);width:var(--thumb-size);transform:var(--thumb-transform);border-radius:var(--thumb-radius, 50%);background:var(--thumb-color);box-shadow:var(--thumb-shadow);pointer-events:auto;transition:.1s}.range-slider>input::-moz-range-thumb{appearance:none;border:none;height:var(--thumb-size);width:var(--thumb-size);transform:var(--thumb-transform);border-radius:var(--thumb-radius, 50%);background:var(--thumb-color);box-shadow:var(--thumb-shadow);pointer-events:auto;transition:.1s}.range-slider>input::-ms-thumb{appearance:none;border:none;height:var(--thumb-size);width:var(--thumb-size);transform:var(--thumb-transform);border-radius:var(--thumb-radius, 50%);background:var(--thumb-color);box-shadow:var(--thumb-shadow);pointer-events:auto;transition:.1s}.range-slider>input:hover{--thumb-shadow: var(--thumb-shadow-hover)}.range-slider>input:hover+output{--value-background: var(--value-background-hover);--y-offset: -5px;color:var(--value-active-color);box-shadow:0 0 0 3px var(--value-background)}.range-slider>input:active{--thumb-shadow: var(--thumb-shadow-active);cursor:grabbing;z-index:2}.range-slider>input:active+output{transition:0s}.range-slider>input:nth-of-type(1){--is-left-most: Clamp(0, (var(--value-a) - var(--value-b)) * 99999 ,1)}.range-slider>input:nth-of-type(1)+output{--value: var(--value-a);--x-offset: calc(var(--completed-a) * -1%)}.range-slider>input:nth-of-type(1)+output:not(:only-of-type){--flip: calc(var(--thumbs-too-close) * -1)}.range-slider>input:nth-of-type(1)+output::after{content:var(--prefix, "") var(--text-value-a) var(--suffix, "")}.range-slider>input:nth-of-type(2){--is-left-most: Clamp(0, (var(--value-b) - var(--value-a)) * 99999 ,1)}.range-slider>input:nth-of-type(2)+output{--value: var(--value-b)}.range-slider>input:only-of-type~.range-slider__progress{--clip-start: 0}.range-slider>input+output{--flip: -1;--x-offset: calc(var(--completed-b) * -1%);--pos: calc(((var(--value) - var(--min))/(var(--max) - var(--min))) * 100%);pointer-events:none;position:absolute;z-index:5;background:var(--value-background);border-radius:10px;padding:2px 4px;left:var(--pos);transform:translate(var(--x-offset), calc(150% * var(--flip) - (var(--y-offset, 0px) + var(--value-offset-y)) * var(--flip)));transition:all .12s ease-out,left 0s}.range-slider>input+output::after{content:var(--prefix, "") var(--text-value-b) var(--suffix, "");font:var(--value-font)}.knobs[data-flow=compact] .switch{--size: 10px;--thumb-scale: 1.3}.knobs[data-flow=compact] .switch__gfx{padding:0}.knobs .switch{--color-bg: #444;--color-bg-on: #444;--thumb-color-off: #d75d4a;--thumb-color-on: #4ec964;--thumb-scale: 1.1;--width-multiplier: 2.5;--thumb-animation-pad: 15%;--size: 1em}.knobs .switch .switch__gfx{background:none;border:1px solid var(--bg, var(--color-bg))}.knobs .switch input:focus+div{outline:none}.switch{--color-bg: #E1E1E1;--color-bg-on: #16B5FF;--thumb-color-on: white;--thumb-color-off: var(--thumb-color-on);--thumb-scale: 1;--size: 16px;--duration: .18s;--width-multiplier: 2.5;--thumb-animation-pad: 15%;user-select:none;display:inline-flex;align-items:center}@keyframes switchMoveThumb{50%{padding:0 var(--thumb-animation-pad)}}@keyframes switchMoveThumb1{50%{padding:0 var(--thumb-animation-pad)}}.switch--textRight .switch__label{order:10;padding:0 0 0 .4em}.switch>div{cursor:pointer}.switch__label{order:0;padding-right:.4em;color:var(--label-color)}.switch__gfx{--thumb-left: 0%;--transform: translateX(calc(var(--thumb-left) * -1)) scale(var(--thumb-scale));order:5;padding:3px;position:relative;background:var(--bg, var(--color-bg));border-radius:50px;width:calc(var(--size)*var(--width-multiplier));transition:var(--duration);background-size:4px 4px}.switch__gfx::before{content:"";display:block;position:relative;left:var(--thumb-left);background:var(--thumb-color, var(--thumb-color-off));border-radius:var(--size);width:var(--size);height:var(--size);transform:var(--transform);transition:var(--duration);animation:switchMoveThumb var(--duration) ease 1}.switch input{position:absolute;opacity:0}.switch input[disabled]+div{background-image:linear-gradient(45deg, white 25%, transparent 25%, transparent 50%, white 50%, white 75%, transparent 75%)}.switch input:disabled~div{cursor:not-allowed}.switch input:indeterminate+div{--thumb-left: 50%}.switch input:checked+div{--bg: var(--color-bg-on);--thumb-left: 100%;--thumb-color: var(--thumb-color-on)}.switch input:checked+div::before{animation-name:switchMoveThumb1}.switch input:focus+div{outline:1px dotted silver}.switch input:focus:not(:focus-visible)+div{outline:none}.knobs__controls{display:flex;align-items:center;opacity:0;flex-direction:row-reverse;margin:var(--side-pad) var(--control-right-pad, var(--side-pad)) 5px var(--control-left-pad, var(--side-pad));position:relative;z-index:1}.knobs__controls>input{color:var(--textColor);border:0;background:none;margin-left:1em;line-height:1;padding:5px 8px;border-radius:3px;position:relative}.knobs__controls>input:hover:not(:active){background:var(--opaqueColor-15)}.poweredBy{margin-right:auto;text-decoration:none;color:inherit;padding:3px;font-size:10px;opacity:.5;transition:.15s}.poweredBy:hover{color:var(--primaryColor);opacity:1}label[data-type=color]>.knobs__knob__inputWrap>div{display:inline-block;border-radius:5px;overflow:hidden;width:calc(var(--color-size)*4);height:calc(var(--color-size) - 2px);transform-origin:center right;background:var(--background) repeating-conic-gradient(rgba(255, 255, 255, 0.2) 0% 25%, transparent 0% 50%) 0/6px 6px}label[data-type=color]:hover>.knobs__knob__inputWrap>div{animation:colorHover .5s ease-out}label[data-type=color] input{width:100%;height:100%;border:0;background:var(--value);color:rgba(0,0,0,0);outline:none;caret-color:rgba(0,0,0,0);text-transform:uppercase;font-weight:600}label[data-type=color] input::selection{color:rgba(0,0,0,0)}@keyframes colorHover{20%{transform:scale(1.2)}40%{transform:scale(1)}60%{transform:scale(1.1)}}label[data-type=text] input,label[data-type=number] input{cursor:text;padding:5px;border-radius:3px;color:var(--textColor);outline:none;border:0;background:var(--opaqueColor-15)}label[data-type=text] input:focus,label[data-type=number] input:focus{box-shadow:0 0 0 1px HSL(var(--base-color), calc(var(--base-color-l) + 22%))}label[data-type=text] input:invalid,label[data-type=number] input:invalid{box-shadow:0 0 0 1px #d75d4a inset}label[data-type=radio]>.knobs__knob__inputWrap{display:flex;gap:var(--radio-group-gap, 1.5em);align-items:center;justify-content:flex-end}label[data-type=radio]>.knobs__knob__inputWrap>label{flex:0;display:inline-flex;gap:.5em;align-items:center}label[data-type=radio]>.knobs__knob__inputWrap>label:hover>*:not(input){opacity:.7}label[data-type=radio]>.knobs__knob__inputWrap>label input{margin:0}label[data-type=radio]>.knobs__knob__inputWrap>label input~*{opacity:.5;transition:.25s}label[data-type=radio]>.knobs__knob__inputWrap>label input:checked~*{opacity:1;transition:0s}label[data-type=radio]>.knobs__knob__inputWrap svg{fill:var(--textColor);height:20px}'+o.styles+`.knobs{ ${this.getCSSVariables(o)} }`,e.head.insertAdjacentHTML("beforeend",``),e.close(),e},render(){var e=(e=>e.reduce(((e,t)=>(!n(t)&&e[e.length-1].length&&e.push([]),e[e.length-1].push(t),e)),[[]]))(this.knobs),t=e.map(this.templates.fieldset.bind(this)).join("");this.DOM.groups.innerHTML=t,this.calculateGroupsHeights(),this.DOM.mainToggler&&this.toggle(this.DOM.mainToggler.checked),this.applyKnobs(),this.knobs.forEach((e=>e&&e.script&&e.script(this,e.__name)));[...document.styleSheets].some((e=>"@yaireo/knobs"==e.title))||document.head.insertAdjacentHTML("beforeend",''),this.trigger("render"),this.settings?.callbacks?.render()},color:{format:t.changeColorFormat,CSStoHSLA:t.CSStoHSLA}},x})); 12 | --------------------------------------------------------------------------------