├── .babelrc
├── .editorconfig
├── .eslintrc
├── .github
└── workflows
│ ├── npm-publish.yml
│ └── test.yml
├── .gitignore
├── .meta-files
└── images
│ └── Hero-Screenshot.png
├── .nvmrc
├── changelog.md
├── contributing.md
├── docs
├── Guify.png
└── api.md
├── example
└── index.html
├── lib
├── guify.js
├── guify.js.map
├── guify.min.js
├── guify.min.js.LICENSE.txt
└── guify.min.js.map
├── license.md
├── package-lock.json
├── package.json
├── postcss.config.js
├── readme.md
├── src
├── component-manager.js
├── components
│ ├── component-base.js
│ ├── internal
│ │ ├── menu-bar.css
│ │ ├── menu-bar.js
│ │ ├── panel.css
│ │ ├── panel.js
│ │ ├── toast-area.css
│ │ └── toast-area.js
│ ├── partials
│ │ ├── container.css
│ │ ├── container.js
│ │ ├── header.js
│ │ ├── label.css
│ │ ├── label.js
│ │ ├── value.css
│ │ └── value.js
│ ├── public
│ │ ├── button.css
│ │ ├── button.js
│ │ ├── checkbox.css
│ │ ├── checkbox.js
│ │ ├── color.css
│ │ ├── color.js
│ │ ├── display.css
│ │ ├── display.js
│ │ ├── file.css
│ │ ├── file.js
│ │ ├── folder.css
│ │ ├── folder.js
│ │ ├── interval.css
│ │ ├── interval.js
│ │ ├── range.css
│ │ ├── range.js
│ │ ├── select.css
│ │ ├── select.js
│ │ ├── text.css
│ │ ├── text.js
│ │ ├── title.css
│ │ └── title.js
│ └── variables.css
├── gui.css
├── gui.js
├── guify.js
├── theme.js
├── themes.js
└── utils
│ └── math-utils.js
├── test
└── library.spec.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": []
4 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = LF
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "env": {
4 | "browser": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended"
9 | ],
10 | "ignorePatterns": [
11 | "webpack.config.js",
12 | "lib",
13 | "test/**/*.spec.js"
14 | ],
15 | "rules": {
16 | "semi": ["error", "always"],
17 | "quotes": ["error", "double"],
18 | "no-unused-vars": 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: 14
18 | cache: 'npm'
19 | - run: npm ci
20 | - run: npm test
21 |
22 | publish-npm:
23 | needs: build
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2
28 | with:
29 | node-version: 14
30 | cache: 'npm'
31 | registry-url: https://registry.npmjs.org/
32 | - run: npm ci
33 | - run: npm publish
34 | env:
35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
36 |
37 | # publish-gpr:
38 | # needs: [build, publish-npm]
39 | # runs-on: ubuntu-latest
40 | # permissions:
41 | # contents: read
42 | # packages: write
43 | # steps:
44 | # - uses: actions/checkout@v2
45 | # - uses: actions/setup-node@v2
46 | # with:
47 | # node-version: 14
48 | # cache: 'npm'
49 | # registry-url: https://npm.pkg.github.com/
50 | # - run: npm ci
51 | # - run: npm publish
52 | # env:
53 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
54 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [12.x, 14.x, 16.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | todo.md
3 | notes.md
4 |
5 | # Created by https://www.gitignore.io/api/osx,node,linux,windows
6 |
7 | ### Linux ###
8 | *~
9 |
10 | # temporary files which can be created if a process still has a handle open of a deleted file
11 | .fuse_hidden*
12 |
13 | # KDE directory preferences
14 | .directory
15 |
16 | # Linux trash folder which might appear on any partition or disk
17 | .Trash-*
18 |
19 | # .nfs files are created when an open file is removed but is still being accessed
20 | .nfs*
21 |
22 | ### Node ###
23 | # Logs
24 | logs
25 | *.log
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # Runtime data
31 | pids
32 | *.pid
33 | *.seed
34 | *.pid.lock
35 |
36 | # Directory for instrumented libs generated by jscoverage/JSCover
37 | lib-cov
38 |
39 | # Coverage directory used by tools like istanbul
40 | coverage
41 |
42 | # nyc test coverage
43 | .nyc_output
44 |
45 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
46 | .grunt
47 |
48 | # Bower dependency directory (https://bower.io/)
49 | bower_components
50 |
51 | # node-waf configuration
52 | .lock-wscript
53 |
54 | # Compiled binary addons (http://nodejs.org/api/addons.html)
55 | build/Release
56 |
57 | # Dependency directories
58 | node_modules/
59 | jspm_packages/
60 |
61 | # Typescript v1 declaration files
62 | typings/
63 |
64 | # Optional npm cache directory
65 | .npm
66 |
67 | # Optional eslint cache
68 | .eslintcache
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variables file
80 | .env
81 |
82 |
83 | ### OSX ###
84 | *.DS_Store
85 | .AppleDouble
86 | .LSOverride
87 |
88 | # Icon must end with two \r
89 | Icon
90 |
91 | # Thumbnails
92 | ._*
93 |
94 | # Files that might appear in the root of a volume
95 | .DocumentRevisions-V100
96 | .fseventsd
97 | .Spotlight-V100
98 | .TemporaryItems
99 | .Trashes
100 | .VolumeIcon.icns
101 | .com.apple.timemachine.donotpresent
102 |
103 | # Directories potentially created on remote AFP share
104 | .AppleDB
105 | .AppleDesktop
106 | Network Trash Folder
107 | Temporary Items
108 | .apdisk
109 |
110 | ### Windows ###
111 | # Windows thumbnail cache files
112 | Thumbs.db
113 | ehthumbs.db
114 | ehthumbs_vista.db
115 |
116 | # Folder config file
117 | Desktop.ini
118 |
119 | # Recycle Bin used on file shares
120 | $RECYCLE.BIN/
121 |
122 | # Windows Installer files
123 | *.cab
124 | *.msi
125 | *.msm
126 | *.msp
127 |
128 | # Windows shortcuts
129 | *.lnk
130 |
131 | # End of https://www.gitignore.io/api/osx,node,linux,windows
132 |
--------------------------------------------------------------------------------
/.meta-files/images/Hero-Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/colejd/guify/4cafddd568859fd7c33faaf9550c5e6ab3fe8e13/.meta-files/images/Hero-Screenshot.png
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v6.10
2 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ## 0.15.1
2 |
3 | - Fixed a bug preventing folders from being removed with `Remove()`.
4 | - There was an issue introduced by 0.15.0 where the guify-container covers all content and has a higher z-index than the content underneath, eating all touch/click events. To fix this, the z-index of guify-container is now unset, and the sub-elements are given the z-index that guify-container used to get (9999).
5 |
6 | ## 0.15.0
7 |
8 | - POTENTIALLY BREAKING CHANGE: Modified the way the GUI elements are constructed internally. If you're modifying the internals in your CSS, make sure everything looks right!
9 | - Made menu bar visible in fullscreen
10 | - If `panelMode` is `outer`, the menu will become `inner` when fullscreen
11 | - Introduced `.guify-fullscreen` CSS class that attaches to the root when fullscreen is enabled
12 | - Fixed up `barMode = "none"` behavior to match docs
13 | - Added `panelOverflowBehavior` parameter to GUI opts, which lets you make the panel scrollable if it grows beyond the edge of the container.
14 | - Fixed brief display of incorrect value when initializing `range` and `display`
15 | - Added a bit of top margin for `title` components
16 | - Fixed styling issues on Safari iOS for `text`, `range`, and `checkbox`
17 | - Fixed incorrect font use on Safari iOS.
18 | - Added an `inputFontFamily` param to theme objects, allowing a secondary font just for input fields
19 | - If you provide your own font URL to the theme object, the default `"Hack"` font won't be downloaded
20 | - Made `range` and `interval` components respect `precision` more closely. `precision` now affects the value itself, meaning the value and its display will always match.
21 | - Fixed a bug in `interval` components with a `log` scale, wherein setting the value text would cause the wrong value to be used
22 |
23 | ## 0.14.3
24 |
25 | - Fixed vertical alignment of arrow in `folder` component
26 |
27 | ## 0.14.2
28 |
29 | - Fixed extra padding below `range` and `color` components
30 | - Fixed vertical alignment of `title` component
31 |
32 | ## 0.14.1
33 |
34 | - Fixed incorrect `interval` height and background
35 | - Made component height `2rem` by default
36 | - Should fix component height issues on some pages
37 | - Reduced line height for `display` component
38 |
39 | ## 0.14.0
40 |
41 | - Allow setting input listening mode on `text` components using a new `listenMode` option. New values are `"input"` (default) and `"change"`.
42 | - Rewrote `interval` component, and added the new features from the `range` improvements in 0.13.0.
43 | - `steps` has been removed for logarithmic sliders.
44 | - You can now specify `precision` for the readouts.
45 | - Added the ability to enable/disable components with `SetEnabled(Bool)`.
46 | - I added new theme elements `"colorTextDisabled"` and `"colorComponentBackgroundDisabled"` to support this. If you're using a custom theme, make sure you add values for these!
47 | - This involved totally rewriting the way styles are added to components internally. This shouldn't cause any issues externally, but if you encounter anything, please file an issue!
48 | - Updated dependencies
49 | - Redid NPM build scripts. See readme for updated commands
50 | - Fixed checkbox bug reported in #6
51 | - Checkbox can now be toggled by clicking anywhere in the row
52 | - Made it possible to have nested folders with identical names
53 |
54 | Thank you to @indivisualvj for your PR (#20)!
55 |
56 | ## 0.13.1
57 |
58 | - Fix missing upload artifacts
59 |
60 | ## 0.13.0
61 |
62 | - Rewrote logic for `range` component.
63 | - `steps` has been removed for logarithmic sliders.
64 | - Updated dependencies.
65 |
66 | ## 0.12.0
67 |
68 | - Added Interval control type (Thank you @ccrisrober!)
69 | - Step for Range and Interval controls is now 0.01 if not specified. Fixes weirdness with values changing in unexpected ways when typing in a new value.
70 |
71 | #### 0.11.1
72 |
73 | - Improved fullscreen API; Safari is now supported
74 |
75 | ## 0.11.0
76 |
77 | - Addded a fullscreen button
78 | - Updated NPM dependencies to fix vulnerabilities
79 |
80 | #### 0.10.1
81 |
82 | - Fix issue where checkboxes cannot be focused with tab
83 |
84 | ## 0.10.0
85 |
86 | - **Breaking change:** Export syntax has been simplified. Instead of `new guify.GUI(...)`,
87 | the call is now `new guify(...)`.
88 | - Styling:
89 | - Components now grow to fit the `componentHeight` property of the current theme
90 | - Set z-index on entire plugin so it overlaps everything else
91 | - Styles moved from component files to style files where appropriate
92 | - Fix Range vertical spacing
93 | - Improve Checkbox style coverage (should fix issues with iOS)
94 | - Simplify document model for container
95 |
96 | #### 0.9.2
97 |
98 | - Fix Range slider vertical offset in Firefox
99 |
100 | #### 0.9.1
101 |
102 | - Adjusted spacing of Range and Color's subcomponents
103 | - Improved example layout
104 | - Make Color's Value subcomponent `readonly` instead of `disabled`
105 |
106 | ## 0.9.0
107 |
108 | - Added `"panelMode"` initialization option
109 | - Allow user input in Range component value boxes
110 | - Fix button text vertical alignment
111 | - Force Value component font size to be the same across all themes
112 |
113 | #### 0.8.1
114 |
115 | - Fix Toast text coloring
116 | - Added `open` opt. Set to true to have the panel forced open on startup.
117 |
118 | ## 0.8.0
119 |
120 | - Updated Menu Bar look
121 | - Removed `menuBarContentHeight` property of themes
122 | - Added new button for opening/closing a Panel when `barMode` is `"none"`
123 | - Refactored theming code
124 | - Added new parameters for themes
125 | - Added new theme preset (`"yorha"`, based on https://metakirby5.github.io/yorha/)
126 | - File and Folder components now release focus if using a mouse to interact
127 | - Fix Range component not highlighting when focused in Firefox
128 | - Removed seam between the Panel and the container's edge on Chrome
129 |
130 |
131 | #### 0.7.2
132 |
133 | - Actually fixed `"above"` barMode
134 |
135 | #### 0.7.1
136 |
137 | - Fixed `"above"` barMode
138 |
139 | ## 0.7.0
140 |
141 | - Menu Bar, Panel and Toast Area have been moved into their own classes and files
142 | - Now using ES6-style imports in all source files
143 | - Added `"none"` option to `opts.barMode` (removed `opts.useMenuBar`)
144 | - Improve styling resistance against Bootstrap
145 | - Massive rewrite of styling:
146 | - Using CSJS instead of Sass so we can load CSS with dynamic variables
147 | - **Themes now work**
148 |
149 | ## 0.6.0
150 |
151 | - Toast notifications now have `aria-live="polite"` [accessibility]
152 | - Components with bound variables will now update themselves only if the bound value has changed
153 | - This is still checked every frame, which is something I'd like to avoid. I'm looking into it.
154 | - Made component polling rate part an option in GUI `opts`
155 | - Text and Range components will no longer update from their bound variables while focused
156 | - Styling update:
157 | - Component elements can now grow vertically
158 | - Range now defocuses after mouseup when using the mouse (stays focused if using a keyboard) [accessibility]
159 | - Checkbox now shows on/off styles when focused using a keyboard [accessibility]
160 | - Button and File components now give visual feedback when clicked
161 | - File component shows an outline when a file is dragged onto it
162 | - Select component now highlights on mouseover or focus
163 | - Fix focus highlighting issues on Firefox
164 | - Add font support to themes [tentative]
165 | - Changed Title component look
166 | - Display component text is now selectable.
167 | - Adjusted margin spacing for folders and titles
168 |
169 |
170 | ## 0.5.0
171 |
172 | - Add File component
173 | - Add Display component
174 |
175 |
176 | ## 0.4.0
177 |
178 | - Accessibility update: made components keyboard-accessible
179 | - Color component still needs work
180 | - Added folder component
181 | - Made `Register()` a method that can accept multiple options objects to instantiate many at once
182 | - Components now update themselves from bound variables
183 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | All work on this library is done against the `develop` branch, which gets merged into `main` when a new release is ready. Accordingly, if you'd like to contribute to Guify, please work off of the `develop` branch.
4 |
5 | This isn't strictly necessary, but please make sure you're not introducing any new ESLint warnings. If you're on VS Code, you can install the ESLint plugin to make this easier, or you can run it from the command line with `npm run lint`.
--------------------------------------------------------------------------------
/docs/Guify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/colejd/guify/4cafddd568859fd7c33faaf9550c5e6ab3fe8e13/docs/Guify.png
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 | All options are optional unless marked as required.
3 |
4 | ## GUI
5 |
6 | Creates and maintains the entire GUI. If you want to show or hide the menu bar or panel, use `SetVisible(show)` or `ToggleVisible()` on `menubar` or `panel`.
7 |
8 | ### `constructor(opts)`
9 | Creates the GUI using the `opts` object for configuration.
10 |
11 | `opts` may have the following properties:
12 | - `title` (String): The name used on the menu bar / panel.
13 | - `theme` (String or Object, default=`"dark"`): The name of the theme to be used, or an object matching one of the themes in `themes.js` if you want to create your own.
14 | - Values: `"light"`, `"dark"`, `"yorha"`, `custom theme object`
15 | - If you use a custom theme object, see `theme.js` for the variables you can set.
16 | - `root` (Element, default=`document.body`): The HTML element that is used as the parent of the created menu and panel.
17 | - `width` (String, default=`"300"`): The width of the panel. You can use any CSS-compatible value here, so you can use `"30%"` or `"20em"`, for example.
18 | - `align` (String, default=`"right"`): Aligns the panel to the left or right side of the root.
19 | - Values: `"left"`, `"right"`
20 | - `barMode` (String, default=`"offset"`): Changes the way the layout bar is inserted into the root.
21 | - Values:
22 | - `"none"`: No menu bar is created, and the panel will always show.
23 | - `"overlay"`: The menu bar is fixed to the top of the root, overlapping content beneath it.
24 | - `"above"`: The menu bar is fixed above the root. Does not alter layout within root.
25 | - In this mode, the menu bar can overlap content just above the root. If you don't want this, you can either use the `"offset"` mode, or set `margin-top: var(--size-menu-bar-height)`.
26 | - `"offset"`: Similar to `"above"`, but some `"margin-top"` is added to the root to compensate for the menu bar's height.
27 | - I've tried to cover a variety of use cases here. If yours isn't covered, you can use `var(--size-menu-bar-height)` in your CSS to offset things yourself.
28 | - `panelMode` (String, default=`"inner"`): Changes the way the panel is anchored relative to the container.
29 | - Values:
30 | - `"inner"`: The panel shows inside of the container.
31 | - `"outer"`: The panel shows outside the container, positioned along whichever side you specified with `align`.
32 | - If you want to put the panel anywhere, use `"inner"` and adjust the CSS however you'd like.
33 | - `panelOverflowBehavior` (String, default=`"scroll"`): Changes the way the panel behaves when its contents exceed the height of the container.
34 | - Values:
35 | - `"scroll"`: The contents will be scrollable.
36 | - `"overflow"`: The panel will grow beyond the edge of the container.
37 | - `opacity` (float, default=`1.0`): Opacity value for the panel.
38 | - `pollRateMS` (int, default=`100`): The rate in milliseconds at which the components will be refreshed from their bound variables.
39 | - `open` (bool, default=`false`): If true, the panel will be forced open at startup.
40 |
41 |
42 | ### `Toast(message, stayMS, transitionMS)`
43 | Displays a toast-style message on screen. `stayMS` and `transitionMS` are optional values that you can use to control the duration and removal animation time of the notification.
44 |
45 | ### `Register(opts, applyToAll)`
46 | Creates a new component in the panel based on `opts`. You can provide one `opts` object or an array if you want to create many components at once.
47 | Returns the component.
48 |
49 | ### `Remove(component)`
50 | Removes the specified component.
51 |
52 | All properties of `applyToAll` will be applied to each opts object.
53 |
54 | ## Components
55 |
56 | Components have a few shared methods you may call after initialization.
57 |
58 | - `SetEnabled(enabled)`: Sets the component enabled/disabled style.
59 | - `Remove()`: Removes the component from the GUI.
60 |
61 | ### `opts`
62 | The properties in this object determine the type and behavior of the created component. Pass this into `Register(opts)`.
63 |
64 | The common properties are:
65 |
66 | - `type` (String, required): The component type that will be created. Can be `"button"`, `"checkbox"`, `"color"`, `"range"`, `"select"`, `"text"`, and `"title"`
67 | - `label` (String): The text label that appears next to the component.
68 | - `initial` (Object): The initial value of the component. If you don't specify this, it will be copied from the bound value if there is one, or otherwise initialized to the variable type's default value.
69 | - `onChange` (callback): Fired every time the value governed by the component changes, with a single argument holding the value.
70 | - `onInitialize` (callback): Fired when the component is initialized.
71 | - `object` (Object): The object holding the property you want the component to be bound to.
72 | - `property` (String): The name of the property in `object` that you want the component to be bound to. `object[property]` and the value of the component will be bound (updating one will change the other).
73 | - `folder` (String): The label of the folder to put the component into. If none is specified it'll just go in the panel at the root level.
74 | - `enabled` (Bool): Whether the component starts out enabled or not (only works for interactive components). This can be modified at runtime with `component.SetEnabled(Bool)`.
75 |
76 | Some component types have their own options. These will be specified for each component listed below.
77 |
78 | ### Text
79 | `type: 'text'`
80 |
81 | Shows an editable text box.
82 |
83 | Special options:
84 | - `listenMode` (String, default=`"input"`): Corresponds to the string you'd pass to `addEventListener()` on a vanilla text field. Can be either `"input"` or `"change"`.
85 | - `"input"` makes it so that every keystroke sends an event.
86 | - `"change"` makes it so that an event is only sent when the field loses focus or you press Enter.
87 |
88 | ### Button
89 | `type: 'button'`
90 |
91 | Represents a button.
92 |
93 | Special options:
94 | - `action` (callback): Called when the button is clicked.
95 |
96 | ### Checkbox
97 | `type: 'checkbox'`
98 |
99 | Represents true/false.
100 |
101 | ### Color
102 | `type: 'color'`
103 |
104 | Represents a color. Can show RGB or hex colors.
105 |
106 | Special options:
107 | - `format` (String): Can be either `"rgb"` or `"hex"`.
108 |
109 | ### Display
110 | `type: 'display'`
111 |
112 | Displays the bound value.
113 |
114 | ### File
115 | `type: 'file'`
116 |
117 | Button / drop area for file selection.
118 |
119 | Special options:
120 | - `fileReadFunc` (String): The name of the method you want the FileReader inside this class to read files with. See the [FileReader docs](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) for more about what these methods do.
121 | - Values: `"readAsDataURL"` (default), `"readAsArrayBuffer"`, `"readAsBinaryString"`, `"readAsText"`
122 |
123 | ### Range
124 | `type: 'range'`
125 |
126 | Shows a slider representing a numerical value.
127 |
128 | Special options:
129 | - `min` (int): The smallest possible value on the slider.
130 | - `max` (int): The largest possible value on the slider.
131 | - `precision` (int, default=`3`): The maximum number of digits displayed for the value if it's a decimal.
132 | - `step` (int, default=`0.01` [see notes]): The amount that is incremented by each movement of the slider. Only effective when `"scale = linear"`.
133 | - If the `precision` is set, then the `step` will by default be the smallest value possible given the precision. For example, if `precision = 3`, then `step = 0.01`, or if `precision = 5`, then `step = 0.0001`.
134 | - `scale` (String): Specifies the scaling behavior of the slider.
135 | - Values: `"linear"`, `"log"`
136 |
137 | ### Interval
138 | `type: 'interval'`
139 |
140 | Shows an adjustable two-handle slider representing an interval.
141 | - `min` (int): The smallest possible value on the slider.
142 | - `max` (int): The largest possible value on the slider.
143 | - `precision` (int, default=`3`): The maximum number of digits displayed for the value if it's a decimal.
144 | - `step` (int, default=`0.01`): The amount that is incremented by each movement of the slider.
145 | - If the `precision` is set, then the `step` will by default be the smallest value possible given the precision. For example, if `precision = 3`, then `step = 0.01`, or if `precision = 5`, then `step = 0.0001`.
146 | - `scale` (String): Specifies the scaling behavior of the slider.
147 | - Values: `"linear"`, `"log"`
148 |
149 | ### Select
150 | `type: 'select'`
151 |
152 | Shows a dropdown with the specified options.
153 |
154 | Special options:
155 | - `options` (Array(String)): A list of strings representing the different selectable options.
156 |
157 | ### Folder
158 | `type: 'folder'`
159 |
160 | An expanding/collapsing area that you can put other components into. To do this, use `folder: 'folderLabel'` as an option of another component, where `folderLabel` is the label of a folder.
161 |
162 | Special options:
163 | - `open` (bool, default=`true`): Show or hide the folder by default
164 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Guify Example
5 |
6 |
7 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | [content]
57 |
58 |
59 |
60 |
61 |
317 |
--------------------------------------------------------------------------------
/lib/guify.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * EventEmitter v5.2.8 - git.io/ee
3 | * Unlicense - http://unlicense.org/
4 | * Oliver Caldwell - https://oli.me.uk/
5 | * @preserve
6 | */
7 |
8 | /*!
9 | * screenfull
10 | * v5.0.0 - 2019-09-09
11 | * (c) Sindre Sorhus; MIT License
12 | */
13 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | Copyright 2019 Jonathan Cole (jons.website)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | --------------------------------------------------------
10 |
11 | **Portions of this code are adapted from https://github.com/freeman-lab/control-panel, which is licensed and copyrighted as follows:**
12 |
13 | The MIT License (MIT)
14 |
15 | Copyright (c) 2016 Jeremy Freeman
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 |
24 | The above copyright notice and this permission notice shall be included in all
25 | copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33 | SOFTWARE.
34 |
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "//": "--------------- Script Info --------------- ",
3 | "name": "guify",
4 | "author": "Jonathan Cole ",
5 | "version": "0.15.1",
6 | "description": "A simple GUI for inspecting and changing JS variables",
7 | "keywords": [
8 | "gui",
9 | "ui",
10 | "inspect",
11 | "inspector",
12 | "bind",
13 | "binding",
14 | "project",
15 | "creative coding",
16 | "p5",
17 | "three"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/colejd/guify.git"
22 | },
23 | "bugs": {
24 | "url": "https://github.com/colejd/guify/issues"
25 | },
26 | "homepage": "https://github.com/colejd/guify#readme",
27 | "license": "MIT",
28 | "main": "lib/guify.min.js",
29 | "scripts": {
30 | "build:prod": "webpack --mode=production",
31 | "build:dev": "webpack --mode=development --progress",
32 | "buildall": "npm-run-all build:prod build:dev",
33 | "build:dev:watch": "webpack --mode=development --progress --watch",
34 | "serve": "webpack serve --mode=development",
35 | "develop": "npm-run-all --parallel build:dev:watch serve",
36 | "test": "mocha --require @babel/register --colors ./test/*.spec.js",
37 | "prepublish": "npm run-script buildall",
38 | "ci": "npm run-script buildall",
39 | "lint": "eslint ."
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.16.0",
43 | "@babel/core": "^7.16.0",
44 | "@babel/eslint-parser": "^7.16.3",
45 | "@babel/preset-env": "^7.16.0",
46 | "@babel/register": "^7.16.0",
47 | "babel-loader": "^8.2.3",
48 | "chai": "^4.3.4",
49 | "css-loader": "^6.5.1",
50 | "eslint": "^8.2.0",
51 | "eslint-webpack-plugin": "^3.1.0",
52 | "mocha": "^9.1.3",
53 | "npm-run-all": "^4.1.5",
54 | "postcss": "^8.3.11",
55 | "postcss-loader": "^6.2.0",
56 | "postcss-preset-env": "^7.0.0",
57 | "style-loader": "^3.3.1",
58 | "webpack": "^5.63.0",
59 | "webpack-cli": "^4.9.1",
60 | "webpack-dev-server": "^4.4.0"
61 | },
62 | "dependencies": {
63 | "dom-css": "^2.1.0",
64 | "is-numeric": "0.0.5",
65 | "is-string": "^1.0.4",
66 | "param-case": "^2.1.1",
67 | "screenfull": "^5.0.0",
68 | "simple-color-picker": "^1.0.5",
69 | "tinycolor2": "^1.4.1",
70 | "uuid": "^3.4.0",
71 | "wolfy87-eventemitter": "^5.2.2"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | [
4 | "postcss-preset-env",
5 | {
6 | stage: 3,
7 | features: {
8 | "nesting-rules": true,
9 | }
10 | },
11 | ],
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # guify
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Demo
12 | |
13 | Docs
14 |
15 |
16 | Guify is a runtime JS library that gives you a simple way to build a GUI for your JS projects. It pairs very well with [three.js](https://threejs.org/) and [p5.js](https://p5js.org/). Consider it an opinionated take on [dat.GUI](https://github.com/dataarts/dat.gui).
17 |
18 | Here are the big features:
19 |
20 | * Bind any UI component to any variable. Guify supports arbitrary text inputs, colors, ranges, file inputs, booleans, and more.
21 | * Guify is easy to graft onto any page and integrate with your existing JS code. Just point your components at the variables you already have:
22 | ```js
23 | var someVariable = 0;
24 | guify.Register([{
25 | {
26 | type: 'range',
27 | object: this, property: 'someProperty',
28 | label: 'Some Property',
29 | min: 0, max: 20, step: 1
30 | },
31 | }])
32 | ```
33 | * Give it that "web app" look with an optional header bar and easy toast notifications.
34 | * Style it however you'd like. You can use one of three built-in themes, or build your own to get exactly the look you want.
35 |
36 | ---
37 |
38 | ## Installation
39 |
40 | Below are some common ways to integrate Guify with your setup.
41 |
42 | ### Quick Start (Browser)
43 |
44 | To integrate on an existing page, you can use the transpiled version in [`/lib`](/lib), either by including it with your files or using a CDN:
45 |
46 | ```html
47 |
48 | ```
49 |
50 | This adds a `guify` function at the global level, which you can use to construct the GUI. For example:
51 |
52 | ```html
53 |
54 |
55 |
59 | ```
60 |
61 | See the Usage guide below for more details. [example.html](/example/index.html) also demonstrates this pattern.
62 |
63 | ### Quick Start (NPM)
64 |
65 | First, install with NPM: `npm install --save guify`
66 |
67 | Then you can import using either `require` or `import` depending on your preference:
68 | ```js
69 | // ES6
70 | import guify from 'guify'
71 |
72 | // Require
73 | let guify = require('guify');
74 | ```
75 |
76 | Then you can make a quick GUI this way:
77 | ```js
78 | var gui = new guify({ ... });
79 | gui.Register([ ... ]);
80 | ```
81 |
82 | See the Usage guide below for more details.
83 |
84 | ### Quick Start (React)
85 |
86 | Check out the unofficial [React port](https://github.com/dbismut/react-guify).
87 |
88 | ---
89 |
90 | ## Usage
91 |
92 | Once you have Guify available to construct in your project, make a `guify` instance:
93 |
94 | ```js
95 | var gui = new guify({
96 | title: "Some Title",
97 | });
98 | ```
99 |
100 | The various controls in Guify are called "components". You can feed component definitions to Guify as follows:
101 |
102 | ```js
103 | gui.Register([
104 | { // A slider representing a value between 0 and 20
105 | type: 'range', label: 'Range',
106 | min: 0, max: 20, step: 1,
107 | onChange: (value) => {
108 | console.log(value);
109 | }
110 | },
111 | {
112 | type: 'button', label: 'Button',
113 | action: () => {
114 | console.log('Button clicked!');
115 | }
116 | },
117 | {
118 | type: 'checkbox', label: 'Checkbox',
119 | onChange: (value) => {
120 | console.log(value);
121 | }
122 | }
123 | ]);
124 | ```
125 |
126 | You can also bind components representing a value to your JS variables instead of using an `onChange()` handler. For example:
127 |
128 | ```js
129 | var someNumber = 10;
130 | gui.Register([
131 | { // A slider representing `someNumber`, constrained between 0 and 20.
132 | type: 'range', label: 'Range',
133 | min: 0, max: 20, step: 1,
134 | object: this, property: 'someNumber'
135 | },
136 | ```
137 |
138 | There are many points of customization here. See the docs at [/docs/api.md](/docs/api.md). A much more robust example can also be found at [example.html](/example/index.html).
139 |
140 |
141 | ### Building This Package
142 | If you want to build this package, you can run `npm install` and then `npm run build:prod`, which will create `/lib/guify.min.js`.
143 |
144 | NPM commands:
145 |
146 | - `build:prod`: Creates `/lib/guify.min.js`, the default script used by this package.
147 | - `build:dev`: Creates `/lib/guify.js`.
148 | - `develop`: Runs `build:dev` and serves the `/example` directory as a static web page.
149 |
150 | ---
151 |
152 | ## Changelog
153 | See [changelog.md](/changelog.md).
154 |
155 |
156 | ## License
157 | MIT license. See [license.md](/license.md) for specifics.
158 |
159 |
160 | ## Credit
161 | This package is largely based on [control-panel](https://github.com/freeman-lab/control-panel).
162 | For setting this up, I used [webpack-library-starter](https://github.com/krasimir/webpack-library-starter).
163 |
164 | ## Alternatives
165 | - [dat.GUI](https://github.com/dataarts/dat.gui)
166 | - [Control-Panel](https://github.com/freeman-lab/control-panel)
167 | - [Oui](https://github.com/wearekuva/oui)
168 |
--------------------------------------------------------------------------------
/src/component-manager.js:
--------------------------------------------------------------------------------
1 | import { default as TitleComponent } from "./components/public/title";
2 | import { default as RangeComponent } from "./components/public/range";
3 | import { default as ButtonComponent } from "./components/public/button";
4 | import { default as CheckboxComponent } from "./components/public/checkbox";
5 | import { default as SelectComponent } from "./components/public/select";
6 | import { default as TextComponent } from "./components/public/text";
7 | import { default as ColorComponent } from "./components/public/color";
8 | import { default as FolderComponent } from "./components/public/folder";
9 | import { default as FileComponent } from "./components/public/file";
10 | import { default as DisplayComponent } from "./components/public/display";
11 | import { default as IntervalComponent } from "./components/public/interval";
12 |
13 | /**
14 | * Manages the loading and instantiation of Components.
15 | */
16 | export class ComponentManager {
17 | constructor(theme) {
18 | this.theme = theme;
19 | this.components = {
20 | "title": TitleComponent,
21 | "range": RangeComponent,
22 | "button": ButtonComponent,
23 | "checkbox": CheckboxComponent,
24 | "select": SelectComponent,
25 | "text": TextComponent,
26 | "color": ColorComponent,
27 | "folder": FolderComponent,
28 | "file": FileComponent,
29 | "display": DisplayComponent,
30 | "interval": IntervalComponent,
31 | };
32 |
33 | }
34 |
35 | /**
36 | * Creates the component specified by `opts` and appends it to the
37 | * document as a child of `root`.
38 | *
39 | * @param {HTMLElement} [root] Parent of the created component
40 | * @param {Object} [opts] Options used to create the component
41 | */
42 | Create(root, opts) {
43 | let initializer = this.components[opts.type];
44 | if(initializer === undefined) {
45 | throw new Error(`No component type named '${opts.type}' exists.`);
46 | }
47 |
48 | let newComponent = new initializer(root, opts, this.theme);
49 |
50 | return newComponent;
51 | }
52 |
53 |
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/component-base.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from "wolfy87-eventemitter";
2 | import { v4 as uuidv4 } from "uuid";
3 |
4 | import { default as ContainerPartial } from "./partials/container";
5 |
6 | export default class ComponentBase extends EventEmitter {
7 | SetEnabled(enabled) {
8 | this.enabled = enabled;
9 | if (enabled) {
10 | this.container?.classList.remove("disabled");
11 | } else {
12 | this.container?.classList.add("disabled");
13 | }
14 | }
15 |
16 | Remove() {
17 | if (this.container) {
18 | this.container.parentNode.removeChild(this.container);
19 | }
20 | }
21 |
22 | constructor(root, opts, theme, makeContainer=true) {
23 | super();
24 |
25 | this.root = root;
26 | this.opts = opts;
27 | this.theme = theme;
28 |
29 | this.uuid = uuidv4();
30 |
31 | if (makeContainer) {
32 | this.container = ContainerPartial(root, opts.label, theme);
33 | }
34 |
35 | this.SetEnabled(opts.enabled || true);
36 | }
37 | }
--------------------------------------------------------------------------------
/src/components/internal/menu-bar.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-bar {
4 | background-color: var(--color-menu-bar-background);
5 | height: var(--size-menu-bar-height);
6 | width: 100%;
7 | opacity: 1.0;
8 | position: relative;
9 | top: 0;
10 | cursor: default;
11 | }
12 |
13 | .guify-bar-title {
14 | color: var(--color-menu-bar-text);
15 | text-align: center;
16 | width: 100%;
17 | position: absolute;
18 | top: 0;
19 | line-height: var(--size-menu-bar-height);
20 | -webkit-user-select: none;
21 | -moz-user-select: none;
22 | -ms-user-select: none;
23 | user-select: none;
24 | }
25 |
26 | .guify-bar-button {
27 | text-align: center;
28 | border: none;
29 | cursor: pointer;
30 | font-family: inherit;
31 | height: 100%;
32 | position: absolute;
33 | top: 0;
34 | color: var(--color-text-primary);
35 | background-color: var(--color-component-background);
36 | -webkit-user-select: none;
37 | -moz-user-select: none;
38 | -ms-user-select: none;
39 | user-select: none;
40 | margin: 0;
41 |
42 | }
43 |
44 | /* Hide default accessibility outlines since we're providing our own visual feedback */
45 | .guify-bar-button:focus {
46 | outline: none;
47 | }
48 | .guify-bar-button::-moz-focus-inner {
49 | border: 0;
50 | }
51 |
52 | .guify-bar-button:hover,
53 | .guify-bar-button:focus {
54 | color: var(--color-text-hover);
55 | background-color: var(--color-component-foreground);
56 | }
57 |
58 | .guify-bar-button:active {
59 | color: var(--color-text-active) !important;
60 | background-color: var(--color-component-active) !important;
61 | }
--------------------------------------------------------------------------------
/src/components/internal/menu-bar.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import css from "dom-css";
4 | import screenfull from "screenfull";
5 |
6 | import "./menu-bar.css";
7 |
8 | export class MenuBar extends ComponentBase {
9 | constructor(root, opts, theme) {
10 | super(root, opts, theme, false);
11 |
12 | // Create menu bar
13 | this.element = document.createElement("div");
14 | this.element.classList.add("guify-bar");
15 | root.appendChild(this.element);
16 |
17 | if (opts.title) {
18 | // Create a text label inside of the bar
19 | let text = this.element.appendChild(document.createElement("div"));
20 | text.classList.add("guify-bar-title");
21 | text.innerHTML = opts.title;
22 | this.label = text;
23 | }
24 |
25 | // Make the menu collapse button
26 | let menuButton = this.element.appendChild(document.createElement("button"));
27 | menuButton.classList.add("guify-bar-button");
28 | menuButton.innerHTML = "Controls";
29 | css(menuButton, {
30 | left: opts.align == "left" ? "0" : "unset",
31 | right: opts.align == "left" ? "unset" : "0",
32 | });
33 | menuButton.onclick = () => {
34 | this.emit("ontogglepanel");
35 | };
36 |
37 | // Make the fullscreen button
38 | if (screenfull.isEnabled) {
39 | let fullscreenButton = this.element.appendChild(document.createElement("button"));
40 | fullscreenButton.classList.add("guify-bar-button");
41 | fullscreenButton.innerHTML = "「 」";
42 | fullscreenButton.setAttribute("aria-label", "Toggle Fullscreen");
43 | css(fullscreenButton, {
44 | left: opts.align == "left" ? "unset" : "0", // Place on opposite side from menuButton
45 | right: opts.align == "left" ? "0" : "unset",
46 | });
47 | fullscreenButton.onclick = () => {
48 | this.emit("onfullscreenrequested");
49 | };
50 | }
51 |
52 | }
53 |
54 | SetVisible(show) {
55 | this.element.style.display = show ? "block" : "none";
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/internal/panel.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | /* Container */
4 |
5 | .guify-panel-container {
6 | position: absolute;
7 | background: var(--color-panel-background);
8 | }
9 |
10 | .guify-panel-container-scrollable {
11 | max-height: calc(100% - var(--size-menu-bar-height));
12 | overflow: scroll;
13 | }
14 |
15 | /* Container modes (i.e. inner, outer) */
16 |
17 | .guify-panel-container-right-inner {
18 | right: 0;
19 | left: unset;
20 | }
21 |
22 | .guify-panel-container-left-inner {
23 | right: unset;
24 | left: 0;
25 | }
26 |
27 | .guify-panel-container-right-outer {
28 | right: unset;
29 | left: 100%;
30 | }
31 |
32 | .guify-panel-container-left-outer {
33 | right: 100%;
34 | left: unset;
35 | }
36 |
37 | .guify-fullscreen .guify-panel-container-right-inner,
38 | .guify-fullscreen .guify-panel-container-right-outer {
39 | right: 0;
40 | left: unset;
41 | }
42 |
43 | .guify-fullscreen .guify-panel-container-left-inner,
44 | .guify-fullscreen .guify-panel-container-left-outer {
45 | right: unset;
46 | left: 0;
47 | }
48 |
49 | /* Panel (in container) */
50 |
51 | .guify-panel {
52 | padding: 14px;
53 | /* Last component will have a margin, so reduce padding to account for this */
54 | padding-bottom: calc(14px - var(--size-component-spacing));
55 |
56 | /* all: initial; */
57 | -webkit-user-select: none;
58 | -moz-user-select: none;
59 | -ms-user-select: none;
60 | user-select: none;
61 | cursor: default;
62 | text-align: left;
63 | box-sizing: border-box;
64 | }
65 |
66 | .guify-panel.guify-panel-hidden {
67 | height: 0px;
68 | display: none;
69 | }
70 |
71 | .guify-panel * {
72 | box-sizing: initial;
73 | -webkit-box-sizing: initial;
74 | -moz-box-sizing: initial;
75 | }
76 |
77 | .guify-panel input {
78 | display: inline;
79 | }
80 |
81 | .guify-panel a {
82 | color: inherit;
83 | text-decoration: none;
84 | }
85 |
86 | .guify-panel-toggle-button {
87 | position: absolute;
88 | top: 0;
89 | margin: 0;
90 | padding: 0;
91 | width: 15px;
92 | height: 15px;
93 | line-height: 15px;
94 | text-align: center;
95 | border: none;
96 | cursor: pointer;
97 | font-family: inherit;
98 | color: var(--color-text-primary);
99 | background-color: var(--color-component-background);
100 |
101 | -webkit-user-select: none;
102 | -moz-user-select: none;
103 | -ms-user-select: none;
104 | user-select: none;
105 |
106 | }
107 |
108 | /* Open/Close button styling */
109 | .guify-panel-toggle-button svg {
110 | fill-opacity: 0;
111 | stroke-width: 3;
112 | stroke: var(--color-component-foreground);
113 | }
114 |
115 | /* Remove browser default outlines since we're providing our own */
116 | .guify-panel-toggle-button:focus {
117 | outline:none;
118 | }
119 | .guify-panel-toggle-button::-moz-focus-inner {
120 | border: 0;
121 | }
122 |
123 | .guify-panel-toggle-button:hover,
124 | .guify-panel-toggle-button:focus {
125 | color: var(--color-text-hover);
126 | background-color: var(--color-component-foreground);
127 | }
128 |
129 | .guify-panel-toggle-button:active {
130 | color: var(--color-text-active);
131 | background-color:var(--color-component-active);
132 | }
--------------------------------------------------------------------------------
/src/components/internal/panel.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import css from "dom-css";
4 |
5 | import "./panel.css";
6 |
7 | import { default as HeaderPartial } from "../partials/header";
8 |
9 | export class Panel extends ComponentBase {
10 | constructor(root, opts, theme) {
11 | super(root, opts, theme, false);
12 |
13 | // Container the panel will sit in
14 | this.container = root.appendChild(document.createElement("div"));
15 | this.container.classList.add("guify-panel-container");
16 | css(this.container, {
17 | width: opts.width,
18 | opacity: opts.opacity || 1.0,
19 | });
20 |
21 | if (opts.align == "left") {
22 | if (opts.panelMode == "outer") {
23 | this.container.classList.add("guify-panel-container-left-outer");
24 | } else if (opts.panelMode == "inner") {
25 | this.container.classList.add("guify-panel-container-left-inner");
26 | }
27 | } else {
28 | if (opts.panelMode == "outer") {
29 | this.container.classList.add("guify-panel-container-right-outer");
30 | } else if (opts.panelMode == "inner") {
31 | this.container.classList.add("guify-panel-container-right-inner");
32 | }
33 | }
34 |
35 | if (opts.panelOverflowBehavior == "scroll") {
36 | this.container.classList.add("guify-panel-container-scrollable");
37 | }
38 |
39 | if(opts.barMode === "none") {
40 | // this._MakeToggleButton();
41 | css(this.container, {
42 | maxHeight: "100%",
43 | });
44 | }
45 |
46 | // Create panel inside container
47 | this.panel = this.container.appendChild(document.createElement("div"));
48 | this.panel.classList.add("guify-panel");
49 |
50 | // Add a title to the panel
51 | if(opts.barMode === "none" && opts.title)
52 | HeaderPartial(this.panel, opts.title, theme);
53 |
54 | }
55 |
56 | /**
57 | * Makes the panel visible based on the truthiness of `show`.
58 | * @param {Bool} [show]
59 | */
60 | SetVisible(show) {
61 | if(show){
62 | // this.panel.style.height = Array.prototype.reduce.call(this.panel.childNodes, function(p, c) {return p + (c.offsetHeight || 0) + 5 + 1;}, 0) + 'px';
63 | // this.panel.style.paddingTop = '14px';
64 | // this.panel.style.paddingBottom = '8px';
65 | this.panel.classList.remove("guify-panel-hidden");
66 |
67 | if(this.menuButton) this.menuButton.setAttribute("alt", "Close GUI");
68 |
69 | }
70 | else {
71 | // this.panel.style.height = '0px';
72 | // this.panel.style.paddingTop = '0px';
73 | // this.panel.style.paddingBottom = '0px';
74 | this.panel.classList.add("guify-panel-hidden");
75 |
76 | if(this.menuButton) this.menuButton.setAttribute("alt", "Open GUI");
77 |
78 | }
79 | }
80 |
81 | /**
82 | * Toggles the visibility of the panel.
83 | */
84 | ToggleVisible() {
85 | if (this.panel.classList.contains("guify-panel-hidden"))
86 | this.SetVisible(true);
87 | else
88 | this.SetVisible(false);
89 | }
90 |
91 | /**
92 | * Makes a show/hide button that sits at the bottom of the panel.
93 | */
94 | _MakeToggleButton() {
95 | // Make the menu collapse button
96 | this.menuButton = this.container.appendChild(document.createElement("button"));
97 | this.menuButton.className = "guify-panel-toggle-button";
98 | css(this.menuButton, {
99 | left: this.opts.align == "left" ? "0px" : "unset",
100 | right: this.opts.align == "left" ? "unset" : "0px",
101 | });
102 |
103 | this.menuButton.onclick = () => {
104 | this.ToggleVisible();
105 | };
106 |
107 | // Defocus on mouse up (for non-accessibility users)
108 | this.menuButton.addEventListener("mouseup", () => {
109 | this.menuButton.blur();
110 | });
111 |
112 | this.menuButton.innerHTML = `
113 |
114 |
115 |
116 | `;
117 | }
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/src/components/internal/toast-area.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-toast-notification {
4 | box-sizing: border-box;
5 | position: relative;
6 | width: 100%;
7 | /* height: 20px; */
8 | padding: 8px;
9 | padding-left: 20px;
10 | padding-right: 20px;
11 | text-align: center;
12 |
13 | font-family: var(--font-family);
14 | font-size: var(--font-size);
15 | font-weight: var(--font-weight);
16 | }
17 |
18 | .guify-toast-area .guify-toast-notification:nth-child(odd) {
19 | color: var(--color-text-primary);
20 | background-color:var(--color-panel-background);
21 | }
22 |
23 | .guify-toast-area .guify-toast-notification:nth-child(even) {
24 | color: var(--color-text-primary);
25 | background-color: var(--color-menu-bar-background);
26 | }
27 |
28 | .guify-toast-close-button {
29 | color: var(--color-text-primary);
30 | background: transparent;
31 | position: absolute;
32 | text-align: center;
33 | margin-top: auto;
34 | margin-bottom: auto;
35 | border: none;
36 | cursor: pointer;
37 | top: 0;
38 | bottom: 0;
39 | right: 8px;
40 | }
--------------------------------------------------------------------------------
/src/components/internal/toast-area.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import css from "dom-css";
4 |
5 | import "./toast-area.css";
6 |
7 | /**
8 | * Represents a container div that creates and holds toast notifications.
9 | */
10 | export class ToastArea extends ComponentBase {
11 | constructor(root, opts, theme) {
12 | super(root, opts, theme, false);
13 |
14 | // Make toast area
15 | this.element = root.appendChild(document.createElement("div"));
16 | this.element.classList.add("guify-toast-area");
17 | css(this.element, {
18 | position: "absolute",
19 | "width": "100%",
20 | });
21 | }
22 |
23 | /**
24 | * Makes a message that appears under the menu bar. Transitions out
25 | * over `transitionMS` milliseconds after `stayMS` milliseconds have passed.
26 | */
27 | CreateToast(message, stayMS = 5000, transitionMS = 0) {
28 | console.log("[Toast] " + message);
29 |
30 | let toast = this.element.appendChild(document.createElement("div"));
31 | toast.classList.add("guify-toast-notification");
32 | toast.setAttribute("aria-live", "polite");
33 |
34 | toast.innerHTML = message;
35 |
36 | css(toast, {
37 | // Animation stuff
38 | // '-webkit-transition': 'opacity ' + transitionMS + 'ms linear',
39 | // 'transition': 'opacity ' + transitionMS + 'ms linear',
40 | });
41 |
42 | // Make close button in toast
43 | let closeButton = toast.appendChild(document.createElement("button"));
44 | closeButton.innerHTML = "✖";
45 | closeButton.classList.add("guify-toast-close-button");
46 |
47 | let timeout;
48 |
49 | let TransitionOut = () => {
50 | toast.blur();
51 | css(toast, {
52 | //'transform-style': 'flat',
53 | //'transform-style': 'preserve-3d',
54 |
55 | // Slide up
56 | // '-webkit-transition': '-webkit-transform ' + transitionMS + 'ms linear',
57 | // 'transition': 'transform ' + transitionMS + 'ms linear',
58 | // '-webkit-transform': 'translate3d(0, -100%, 0)',
59 | // 'transform:': 'translate3d(0, -100%, 0)',
60 |
61 | // Fade out
62 | //'-webkit-transition': '-webkit-opacity ' + transitionMS + 'ms linear',
63 | //'transition': 'opacity ' + transitionMS + 'ms linear',
64 | "opacity": "0",
65 | });
66 | clearTimeout(timeout);
67 | timeout = setTimeout(() => {
68 | if(toast)
69 | toast.parentNode.removeChild(toast);
70 | }, transitionMS);
71 | };
72 |
73 | timeout = setTimeout(TransitionOut, stayMS);
74 |
75 | closeButton.onclick = TransitionOut;
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/partials/container.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-component-container {
4 | position: relative;
5 | min-height: var(--size-component-height);
6 | line-height: var(--size-component-height);
7 | margin-bottom: var(--size-component-spacing);
8 | }
--------------------------------------------------------------------------------
/src/components/partials/container.js:
--------------------------------------------------------------------------------
1 | import "./container.css";
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | let Container = (root, label, theme) => {
5 | let container = root.appendChild(document.createElement("div"));
6 | container.classList.add("guify-component-container");
7 | return container;
8 | };
9 |
10 | export default Container;
11 |
--------------------------------------------------------------------------------
/src/components/partials/header.js:
--------------------------------------------------------------------------------
1 | import css from "dom-css";
2 |
3 | export default function (root, text, theme) {
4 | var title = root.appendChild(document.createElement("div"));
5 | title.innerHTML = text;
6 |
7 | css(title, {
8 | width: "100%",
9 | textAlign: "center",
10 | color: theme.colors.textSecondary,
11 | height: "20px",
12 | marginBottom: "4px"
13 | });
14 |
15 | return title;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/partials/label.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-component-label {
4 | left: 0;
5 | width: calc(var(--size-label-width) - 2%);
6 | display: inline-block;
7 | margin-right: 2%;
8 | vertical-align: top;
9 | min-height: var(--size-component-height);
10 | line-height: var(--size-component-height);
11 |
12 | color: var(--color-text-primary);
13 | }
14 |
15 | /* Disabled styles */
16 | .disabled .guify-component-label {
17 | color: var(--color-text-disabled);
18 | }
--------------------------------------------------------------------------------
/src/components/partials/label.js:
--------------------------------------------------------------------------------
1 | import "./label.css";
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | export default (root, text, theme) => {
5 | var label = root.appendChild(document.createElement("div"));
6 | label.classList.add("guify-component-label");
7 | label.innerHTML = text;
8 | return label;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/partials/value.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-value-input {
4 | position: absolute;
5 | background-color: var(--color-component-background);
6 | padding-left: 1%;
7 | height: var(--size-component-height);
8 | display: inline-block;
9 | overflow: hidden;
10 | border: none;
11 |
12 | color: var(--color-text-secondary);
13 | user-select: text;
14 | cursor: text;
15 | line-height: var(--size-component-height);
16 | word-break: break-all;
17 |
18 | box-sizing: border-box !important;
19 | -moz-box-sizing: border-box !important;
20 | -webkit-box-sizing: border-box !important;
21 |
22 | font-family: var(--font-family-for-input);
23 |
24 | border-radius: 0;
25 | }
26 |
27 | .guify-value-input-right {
28 | right: 0 !important;
29 | }
30 |
31 | .disabled .guify-value-input {
32 | pointer-events: none;
33 | background-color: var(--color-component-background-disabled);
34 | color: var(--color-text-disabled);
35 | }
--------------------------------------------------------------------------------
/src/components/partials/value.js:
--------------------------------------------------------------------------------
1 | import css from "dom-css";
2 |
3 | import "./value.css";
4 |
5 | export default (root, text, theme, width, left) => {
6 |
7 | let input = root.appendChild(document.createElement("input"));
8 | input.type = "text";
9 | input.classList.add("guify-value-input");
10 |
11 | input.value = text;
12 |
13 | if (!left) {
14 | input.classList.add("guify-value-input-right");
15 | }
16 |
17 | css(input, {
18 | "width": width,
19 | });
20 |
21 | return input;
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/public/button.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-button {
4 | box-sizing: border-box !important;
5 | color: var(--color-text-secondary);
6 | background-color: var(--color-component-background);
7 |
8 | position: absolute;
9 | text-align: center;
10 | height: var(--size-component-height);
11 | line-height: var(--size-component-height);
12 | padding-top: 0px;
13 | padding-bottom: 0px;
14 | width: calc(100% - var(--size-label-width));
15 | border: none;
16 | cursor: pointer;
17 | right: 0;
18 | font-family: inherit;
19 | }
20 |
21 | .guify-button:focus {
22 | outline:none;
23 | }
24 | .guify-button::-moz-focus-inner {
25 | border:0;
26 | }
27 |
28 | .guify-button:hover,
29 | .guify-button:focus {
30 | color: var(--color-text-hover);
31 | background-color: var(--color-component-foreground);
32 | }
33 |
34 | .guify-button:active {
35 | color: var(--color-text-active) !important;
36 | background-color: var(--color-component-active) !important;
37 | }
38 |
39 | *.disabled > .guify-button {
40 | pointer-events: none;
41 | background-color: var(--color-component-background-disabled);
42 | color: var(--color-text-disabled);
43 | }
--------------------------------------------------------------------------------
/src/components/public/button.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import { default as LabelPartial } from "../partials/label";
4 |
5 | import "./button.css";
6 |
7 | export default class Button extends ComponentBase {
8 | constructor(root, opts, theme) {
9 | super(root, opts, theme);
10 |
11 | this.label = LabelPartial(this.container, "", theme);
12 |
13 | this.input = this.container.appendChild(document.createElement("button"));
14 | this.input.classList.add("guify-button");
15 |
16 | this.input.textContent = opts.label;
17 | this.button = this.input;
18 |
19 | this.input.addEventListener("click", opts.action);
20 |
21 | // Defocus on mouse up (for non-accessibility users)
22 | this.input.addEventListener("mouseup", () => {
23 | this.input.blur();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/public/checkbox.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | :root {
4 | --checkbox-border-width: 4px;
5 | }
6 |
7 | input[type=checkbox].guify-checkbox {
8 | opacity: 0;
9 | appearance: none;
10 | -moz-appearance: none;
11 | -webkit-appearance: none;
12 | margin: 0;
13 | border-radius: 0;
14 | border: none;
15 | cursor: pointer;
16 | }
17 |
18 | input[type=checkbox].guify-checkbox + label {
19 | margin: 0;
20 | }
21 |
22 | input[type=checkbox].guify-checkbox + label:before {
23 | content: "";
24 | display: inline-block;
25 | width: var(--size-component-height);
26 | height: var(--size-component-height);
27 | padding: 0;
28 | margin: 0;
29 | vertical-align: middle;
30 | background-color: var(--color-component-background);
31 | border-radius: 0px;
32 | cursor: pointer;
33 | box-sizing: content-box;
34 | -moz-box-sizing: content-box;
35 | -webkit-box-sizing: content-box;
36 |
37 | }
38 |
39 | /* Hover style */
40 | input[type=checkbox].guify-checkbox:hover:not(:disabled) + label:before {
41 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
42 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
43 | background-color: var(--color-component-background-hover);
44 | border: solid 4px var(--color-component-background);
45 | }
46 |
47 | /* Checked style */
48 | input[type=checkbox]:checked.guify-checkbox + label:before {
49 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
50 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
51 | background-color: var(--color-component-foreground);
52 | border: solid var(--checkbox-border-width) var(--color-component-background);
53 | }
54 |
55 | /* Focused and checked */
56 | input[type=checkbox]:checked.guify-checkbox:focus + label:before {
57 | width: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
58 | height: calc(var(--size-component-height) - (var(--checkbox-border-width) * 2));
59 | background-color: var(--color-component-foreground);
60 | border: solid var(--checkbox-border-width) var(--color-component-background-hover);
61 | }
62 |
63 | /* Focus and unchecked */
64 | input[type=checkbox].guify-checkbox:focus + label:before {
65 | background-color: var(--color-component-background-hover);
66 | }
67 |
68 | /* Disabled styles */
69 | .disabled input[type=checkbox].guify-checkbox + label {
70 | pointer-events: none;
71 | }
72 | .disabled input[type="checkbox"].guify-checkbox + label::before {
73 | pointer-events: none;
74 | background-color: var(--color-component-background-disabled);
75 | }
--------------------------------------------------------------------------------
/src/components/public/checkbox.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import { default as LabelPartial } from "../partials/label";
4 |
5 | import "./checkbox.css";
6 |
7 | export default class Checkbox extends ComponentBase {
8 | constructor (root, opts, theme) {
9 | super(root, opts, theme);
10 |
11 | this.label = LabelPartial(this.container, opts.label, theme);
12 |
13 | this.input = this.container.appendChild(document.createElement("input"));
14 | this.input.id = "guify-checkbox-" + opts.label + this.uuid;
15 | this.input.type = "checkbox";
16 | this.input.checked = opts.initial;
17 | this.input.classList.add("guify-checkbox");
18 | // Add ARIA attribute to input based on label text
19 | if(opts.label) this.input.setAttribute("aria-label", opts.label);
20 |
21 | // This is a HTML `` element, not a LabelPartial.
22 | var labelElement = this.container.appendChild(document.createElement("label"));
23 | labelElement.htmlFor = this.input.id;
24 |
25 | setTimeout(() => {
26 | this.emit("initialized", this.input.checked);
27 | });
28 |
29 | this.input.onchange = (data) => {
30 | this.emit("input", data.target.checked);
31 | };
32 |
33 | }
34 |
35 | SetValue(value) {
36 | this.input.checked = value;
37 | }
38 |
39 | GetValue() {
40 | return this.input.checked;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/public/color.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | /* Styling for simple-color-picker */
4 |
5 | .guify-color .Scp {
6 | width: 125px;
7 | height: 100px;
8 | -webkit-user-select: none;
9 | -moz-user-select: none;
10 | -ms-user-select: none;
11 | user-select: none;
12 | position: relative;
13 | z-index: 1000;
14 | cursor: pointer;
15 | }
16 | .guify-color .Scp-saturation {
17 | position: relative;
18 | width: calc(100% - 25px);
19 | height: 100%;
20 | background: linear-gradient(to right, #fff 0%, #f00 100%);
21 | float: left;
22 | margin-right: 5px;
23 | }
24 | .guify-color .Scp-brightness {
25 | width: 100%;
26 | height: 100%;
27 | background: linear-gradient(to top, #000 0%, rgba(255,255,255,0) 100%);
28 | }
29 | .guify-color .Scp-sbSelector {
30 | border: 1px solid;
31 | position: absolute;
32 | width: 14px;
33 | height: 14px;
34 | background: #fff;
35 | border-radius: 10px;
36 | top: -7px;
37 | left: -7px;
38 | box-sizing: border-box;
39 | z-index: 10;
40 | }
41 | .guify-color .Scp-hue {
42 | width: 20px;
43 | height: 100%;
44 | position: relative;
45 | float: left;
46 | background: linear-gradient(to bottom, #f00 0%, #f0f 17%, #00f 34%, #0ff 50%, #0f0 67%, #ff0 84%, #f00 100%);
47 | }
48 | .guify-color .Scp-hSelector {
49 | position: absolute;
50 | background: #fff;
51 | border-bottom: 1px solid #000;
52 | right: -3px;
53 | width: 10px;
54 | height: 2px;
55 | }
56 |
57 | /* Disabled styles */
58 | .disabled .guify-color {
59 | pointer-events: none;
60 | }
--------------------------------------------------------------------------------
/src/components/public/color.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import ColorPicker from "simple-color-picker";
4 | import tinycolor from "tinycolor2";
5 |
6 | import css from "dom-css";
7 | import "./color.css";
8 |
9 | import { default as LabelPartial } from "../partials/label";
10 | import { default as ValuePartial } from "../partials/value";
11 |
12 | export default class Color extends ComponentBase {
13 | constructor (root, opts, theme) {
14 | super(root, opts, theme);
15 |
16 | opts.format = opts.format || "rgb";
17 | opts.initial = opts.initial || "#123456";
18 |
19 | this.label = LabelPartial(this.container, opts.label, theme);
20 |
21 | var icon = this.container.appendChild(document.createElement("span"));
22 | icon.classList.add("guify-color");
23 |
24 | var value = ValuePartial(this.container, "", theme, `calc(100% - ${theme.sizing.labelWidth} - 12% - 0.5em)`);
25 | value.setAttribute("readonly", "true");
26 |
27 | icon.onmouseover = () => {
28 | this.picker.$el.style.display = "";
29 | };
30 |
31 | var initial = opts.initial;
32 | switch (opts.format) {
33 | case "rgb":
34 | initial = tinycolor(initial).toHexString();
35 | break;
36 | case "hex":
37 | initial = tinycolor(initial).toHexString();
38 | break;
39 | case "array":
40 | initial = tinycolor.fromRatio({r: initial[0], g: initial[1], b: initial[2]}).toHexString();
41 | break;
42 | default:
43 | break;
44 | }
45 |
46 | this.picker = new ColorPicker({
47 | el: icon,
48 | color: initial,
49 | background: theme.colors.componentBackground,
50 | width: 125,
51 | height: 100
52 | });
53 |
54 | css(this.picker.$el, {
55 | marginTop: theme.sizing.componentHeight,
56 | display: "none",
57 | position: "absolute"
58 | });
59 |
60 | css(icon, {
61 | position: "absolute", // Fixes extra height being applied below for some reason
62 | display: "inline-block",
63 | width: "12.5%",
64 | height: theme.sizing.componentHeight,
65 | backgroundColor: this.picker.getHexString()
66 | });
67 |
68 | icon.onmouseout = () => {
69 | this.picker.$el.style.display = "none";
70 | };
71 |
72 | setTimeout(() => {
73 | this.emit("initialized", initial);
74 | });
75 |
76 | this.picker.onChange((hex) => {
77 | value.value = this.Format(hex);
78 | css(icon, {backgroundColor: hex});
79 | this.emit("input", this.Format(hex));
80 | });
81 | }
82 |
83 | Format(hex) {
84 | switch (this.opts.format) {
85 | case "rgb":
86 | return tinycolor(hex).toRgbString();
87 | case "hex":
88 | return tinycolor(hex).toHexString();
89 | case "array":
90 | var rgb = tinycolor(hex).toRgb();
91 | return [rgb.r / 255, rgb.g / 255, rgb.b / 255].map((x) => {
92 | return x.toFixed(2);
93 | });
94 | default:
95 | return hex;
96 | }
97 | }
98 |
99 | SetValue(value) {
100 | if (!this.picker.isChoosing) {
101 | this.picker.setColor(value);
102 | }
103 | }
104 |
105 | GetValue() {
106 | return this.Format(this.picker.getColor());
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/public/display.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-display {
4 | display: inline-block;
5 | height: unset;
6 | min-height: var(--size-component-height);
7 | width: calc(100% - var(--size-label-width));
8 | border: none;
9 | color: var(--color-text-secondary);
10 | font-family: inherit;
11 | box-sizing: border-box;
12 | -moz-box-sizing: border-box;
13 | -webkit-box-sizing: border-box;
14 | vertical-align: sub;
15 | line-height: 1rem;
16 | user-select: text;
17 | }
18 |
19 | .disabled .guify-display {
20 | color: var(--color-text-disabled);
21 | }
--------------------------------------------------------------------------------
/src/components/public/display.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import "./display.css";
4 |
5 | import { default as LabelPartial } from "../partials/label";
6 |
7 | /**
8 | * Display component. Shows the state of a variable.
9 | */
10 | export default class Display extends ComponentBase {
11 | constructor (root, opts, theme) {
12 | super(root, opts, theme);
13 |
14 | this.label = LabelPartial(this.container, opts.label, theme);
15 |
16 | this.text = this.container.appendChild(document.createElement("div"));
17 | this.text.classList.add("guify-display");
18 |
19 | if (opts.initial) {
20 | this.SetValue(opts.initial);
21 | }
22 |
23 | // Add ARIA attribute to text based on label text
24 | if(opts.label) this.text.setAttribute("aria-label", opts.label);
25 | }
26 |
27 | SetValue(value) {
28 | this.text.innerHTML = value.toString();
29 | }
30 |
31 | GetValue() {
32 | return this.text.innerHTML.toString();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/public/file.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-file-container {
4 | display: inline-block;
5 | outline: none;
6 | padding-top: 8px;
7 | padding-bottom: 8px;
8 | color: var(--color-text-primary);
9 | background-color: var(--color-component-background);
10 | cursor: pointer;
11 | }
12 |
13 | .guify-file-container:hover:not(:disabled),
14 | .guify-file-container:focus:not(:disabled) {
15 | color: var(--color-text-hover);
16 | background-color: var(--color-component-foreground);
17 | }
18 |
19 | .guify-file-container:active:not(:disabled) {
20 | color: var(--color-text-active) !important;
21 | background-color: var(--color-component-active) !important;
22 | }
23 |
24 | .guify-dragover:not(:disabled) {
25 | background-color: var(--color-component-background);
26 | box-shadow: inset 0 0 0 3px var(--color-component-foreground);
27 | }
28 |
29 | .disabled.guify-file-container {
30 | pointer-events: none;
31 | color: var(--color-text-disabled) !important;
32 | background-color: var(--color-component-background-disabled) !important;
33 | box-shadow: inset 0 0 0 3px var(--color-component-background-disabled) !important;
34 | }
--------------------------------------------------------------------------------
/src/components/public/file.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 | import css from "dom-css";
3 |
4 | import "./file.css";
5 |
6 | /**
7 | * File component. Drag and drop a file or click to choose a file.
8 | */
9 | export default class File extends ComponentBase {
10 | constructor (root, opts, theme) {
11 | super(root, opts, theme);
12 |
13 | this.opts.fileReadFunc = this.opts.fileReadFunc || "readAsDataURL";
14 |
15 | this.file = null;
16 | this.fileName = null;
17 |
18 | this.container.classList.add("guify-file-container");
19 | this.container.setAttribute("role", "button");
20 | this.container.setAttribute("tabIndex", "0"); // Make tabbable
21 | css(this.container, {
22 | width: "100%",
23 | "box-sizing": "border-box",
24 | "-moz-box-sizing": "border-box",
25 | "-webkit-box-sizing": "border-box",
26 | height: "unset", // Grow with content
27 | padding: "8px"
28 | });
29 |
30 | let label = this.container.appendChild(document.createElement("div"));
31 | label.innerHTML = opts.label;
32 | css(label, "padding-bottom", "5px");
33 |
34 | this.input = this.container.appendChild(document.createElement("input"));
35 | this.input.setAttribute("type", "file");
36 | this.input.setAttribute("multiple", false);
37 | this.input.style.display = "none";
38 | // Add ARIA attribute to input based on label text
39 | if(opts.label) this.input.setAttribute("aria-label", opts.label);
40 |
41 | this.fileLabel = this.container.appendChild(document.createElement("div"));
42 | this.fileLabel.innerHTML = "Choose a file...";
43 | //css(this.fileLabel, 'color', theme.colors.textSecondary);
44 |
45 | let FileDropped = (event) => {
46 | var files;
47 | if(event.dataTransfer) {
48 | files = event.dataTransfer.files;
49 | } else if(event.target) {
50 | files = event.target.files;
51 | }
52 |
53 | var reader = new FileReader();
54 | reader.onload = () => {
55 | this.file = reader.result;
56 | this.fileLabel.innerHTML = files[0].name;
57 | this.emit("input", this.file);
58 | };
59 |
60 | reader[this.opts.fileReadFunc](files[0]);
61 | };
62 |
63 | this.input.addEventListener("change", FileDropped);
64 |
65 | this.container.addEventListener("dragover", (event) => {
66 | event.preventDefault();
67 | event.stopPropagation();
68 | this.container.classList.add("guify-dragover");
69 | });
70 |
71 | this.container.addEventListener("dragleave", (event) => {
72 | event.preventDefault();
73 | event.stopPropagation();
74 | this.container.classList.remove("guify-dragover");
75 | });
76 |
77 | this.container.addEventListener("drop", (event) => {
78 | event.preventDefault();
79 | event.stopPropagation();
80 | this.container.classList.remove("guify-dragover");
81 | FileDropped(event);
82 | });
83 |
84 | this.container.onclick = () => {
85 | this.input.click();
86 | };
87 |
88 | this.container.addEventListener("keydown", (event) => {
89 | if (event.code === "Enter" || event.code === "Space") {
90 | event.preventDefault();
91 | this.input.click();
92 | }
93 | });
94 |
95 | // Defocus on mouse up (for non-accessibility users)
96 | this.container.addEventListener("mouseup", () => {
97 | this.container.blur();
98 | });
99 |
100 | }
101 |
102 | // eslint-disable-next-line no-unused-vars
103 | SetValue(value) {
104 | return;
105 | }
106 |
107 | GetValue() {
108 | return this.file;
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/src/components/public/folder.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-folder {
4 | cursor: pointer;
5 | padding-left: 0.5em;
6 | color: var(--color-text-primary);
7 | }
8 |
9 | .guify-folder div {
10 | display: inline-block;
11 | vertical-align: sub;
12 | line-height: var(--size-component-height);
13 | }
14 |
15 | .guify-folder:hover,
16 | .guify-folder:focus {
17 | color: var(--color-text-hover);
18 | background-color: var(--color-component-foreground);
19 | outline: none;
20 | }
21 |
22 |
23 | .guify-folder-contents {
24 | display: block;
25 | box-sizing: border-box;
26 | padding-left: 14px;
27 | margin-bottom: 5px;
28 | border-left: 2px solid var(--color-component-background);
29 | }
30 |
31 | .guify-folder-contents.guify-folder-closed {
32 | height: 0;
33 | display: none;
34 | }
35 |
36 | .guify-folder .guify-folder-arrow {
37 | width: 1.5em;
38 | vertical-align: middle;
39 | }
40 |
41 | /* Disabled styles */
42 |
43 | .guify-folder.disabled {
44 | pointer-events: none;
45 | color: var(--color-text-disabled);
46 | }
47 |
48 | .guify-folder.disabled + .guify-folder-contents {
49 | pointer-events: none;
50 | }
--------------------------------------------------------------------------------
/src/components/public/folder.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import "./folder.css";
4 |
5 | export default class Folder extends ComponentBase {
6 | constructor (root, opts, theme) {
7 | super(root, opts, theme);
8 |
9 | this.container.classList.add("guify-folder");
10 | this.container.setAttribute("role", "button");
11 | this.container.setAttribute("tabIndex", "0"); // Make tabbable
12 | // css(container, {
13 | // color: theme.colors.text1,
14 | // })
15 |
16 | this.arrow = this.container.appendChild(document.createElement("div"));
17 | this.arrow.classList.add("guify-folder-arrow");
18 | this.arrow.innerHTML = "▾";
19 |
20 | this.label = this.container.appendChild(document.createElement("div"));
21 | this.label.classList.add("guify-folder-text");
22 | this.label.innerHTML = opts.label;
23 |
24 | this.container.onclick = () => {
25 | this.Toggle();
26 | };
27 |
28 | // Defocus on mouse up (for non-accessibility users)
29 | this.container.addEventListener("mouseup", () => {
30 | this.container.blur();
31 | });
32 |
33 | this.container.addEventListener("keydown", (event) => {
34 | if (event.code === "Enter" || event.code === "Space") {
35 | event.preventDefault();
36 | this.Toggle();
37 | }
38 | });
39 |
40 | this.folderContainer = root.appendChild(document.createElement("div"));
41 | this.folderContainer.classList.add("guify-folder-contents");
42 |
43 | this.open = this.opts.open || false;
44 | this.SetOpen(this.open);
45 |
46 | }
47 |
48 | SetEnabled(enabled) {
49 | super.SetEnabled(enabled);
50 | // Disable everything in the folder
51 | if (enabled) {
52 | this.folderContainer?.classList.remove("disabled");
53 | } else {
54 | this.folderContainer?.classList.add("disabled");
55 | }
56 | }
57 |
58 | // Toggle visibility
59 | Toggle() {
60 | this.open = !this.open;
61 | this.SetOpen(this.open);
62 | }
63 |
64 | // Show or hide the contents
65 | SetOpen(show) {
66 | this.open = show;
67 | if(show) {
68 | this.folderContainer.classList.remove("guify-folder-closed");
69 | this.arrow.innerHTML = "▾"; // Down triangle
70 |
71 | }
72 | else {
73 | this.folderContainer.classList.add("guify-folder-closed");
74 | this.arrow.innerHTML = "▸"; // Right triangle
75 | }
76 | }
77 |
78 | Remove() {
79 | if (this.folderContainer) {
80 | this.folderContainer.parentNode.removeChild(this.folderContainer);
81 | }
82 | super.Remove();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/public/interval.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | :root {
4 | --interval-track-color: var(--color-component-background);
5 | --interval-thumb-color: var(--color-component-foreground);
6 | --interval-thumb-highlight: var(--color-component-active);
7 |
8 | --interval-track-color-disabled: var(--color-component-background-disabled);
9 | --interval-thumb-color-disabled: var(--color-text-disabled);
10 | }
11 |
12 | .guify-interval {
13 | -webkit-appearance: none;
14 | position: absolute;
15 | height: var(--size-component-height);
16 | margin: 0px 0;
17 | width: 33%;
18 | left: 54.5%;
19 | background-color: var(--interval-track-color);
20 | cursor: ew-resize;
21 |
22 | -webkit-touch-callout: none;
23 | -webkit-user-select: none;
24 | -khtml-user-select: none;
25 | -moz-user-select: none;
26 | -ms-user-select: none;
27 | user-select: none;
28 | }
29 | .guify-interval-handle {
30 | background-color: var(--interval-thumb-color);
31 | position: absolute;
32 | height: var(--size-component-height);
33 | min-width: 1px;
34 | }
35 | .guify-interval-handle:focus {
36 | background: var(--interval-thumb-highlight);
37 | }
38 |
39 | .disabled .guify-interval {
40 | pointer-events: none;
41 | background-color: var(--interval-track-color-disabled);
42 | }
43 |
44 | .disabled .guify-interval .guify-interval-handle {
45 | background: var(--interval-thumb-color-disabled);
46 | }
--------------------------------------------------------------------------------
/src/components/public/interval.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import css from "dom-css";
4 | import isnumeric from "is-numeric";
5 |
6 | import "./interval.css";
7 |
8 | import { default as LabelPartial } from "../partials/label";
9 | import { default as ValuePartial } from "../partials/value";
10 |
11 | import { lerp } from "../../utils/math-utils";
12 |
13 | function clamp(x, min, max)
14 | {
15 | return Math.min(Math.max(x, min), max);
16 | }
17 |
18 | export default class Interval extends ComponentBase {
19 | constructor (root, opts, theme) {
20 | super(root, opts, theme);
21 |
22 | this.label = LabelPartial(this.container, opts.label, theme);
23 |
24 | if (!!opts.step && !!opts.steps) {
25 | throw new Error("Cannot specify both step and steps. Got step = " + opts.step + ", steps = ", opts.steps);
26 | }
27 |
28 | this.input = this.container.appendChild(document.createElement("span"));
29 | this.input.classList.add("guify-interval");
30 |
31 | this.handle = document.createElement("span");
32 | this.handle.classList.add("guify-interval-handle");
33 | this.input.appendChild(this.handle);
34 |
35 |
36 | if (!Array.isArray(opts.initial))
37 | {
38 | opts.initial = [];
39 | }
40 |
41 | this.scale = opts.scale;
42 |
43 | // Get initial value:
44 | if( opts.scale === "log" )
45 | {
46 | // If logarithmic, we're going to set the slider to a known linear range. Then we'll
47 | // map that linear range to the user-set range using a log scale.
48 |
49 | // Check if all signs are valid
50 | if (opts.min * opts.max <= 0) {
51 | throw new Error("Log range min/max must have the same sign and not equal zero. Got min = " + opts.min + ", max = " + opts.max);
52 | }
53 |
54 | // Step is invalid for log scale slider
55 | if (isnumeric(opts.step)) {
56 | console.warn("Step is unused for log scale sliders.");
57 | }
58 |
59 | // Warn that `steps` was removed
60 | if (isnumeric(opts.steps)) {
61 | console.warn("\"steps\" option for log scale sliders has been removed.");
62 | }
63 |
64 | // Min/max are forced to a known range, and log value will be derived from slider position within.
65 | this.minPos = 0;
66 | this.maxPos = 1000000;
67 |
68 | this.min = Math.log( (isnumeric(opts.min)) ? opts.min : 0.000001 ); // This cannot be 0
69 | this.max = Math.log( (isnumeric(opts.max)) ? opts.max : 100 );
70 |
71 | this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
72 | this.logScale = (this.max - this.min) / (this.maxPos - this.minPos);
73 |
74 | this.initial = [
75 | isnumeric(opts.initial[0]) ? opts.initial[0] : this.min,
76 | isnumeric(opts.initial[1]) ? opts.initial[1] : this.max,
77 | ];
78 | }
79 | else
80 | {
81 | // If linear, this is much simpler. Pos and value can directly match.
82 | this.minPos = (isnumeric(opts.min)) ? opts.min : 0;
83 | this.maxPos = (isnumeric(opts.max)) ? opts.max : 100;
84 | this.min = this.minPos;
85 | this.max = this.maxPos;
86 |
87 | this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
88 | this.step = (isnumeric(opts.step)) ? opts.step : (10 / Math.pow(10, 3)); // Default is the lowest possible number given the precision. When precision = 3, step = 0.01.
89 |
90 | this.initial = [
91 | isnumeric(opts.initial[0]) ? opts.initial[0] : this.min,
92 | isnumeric(opts.initial[1]) ? opts.initial[1] : this.max,
93 | ];
94 |
95 | // Quantize the initial value to the nearest step:
96 | if (this.step != 0) {
97 | this.initial = this.initial.map((value) => {
98 | return this.min + this.step * Math.round((value - this.min) / this.step);
99 | });
100 | }
101 | }
102 |
103 | this.value = opts.initial;
104 |
105 | // Set handle positions from value
106 | this._RefreshHandles();
107 |
108 | // Display the values:
109 | this.lValue = ValuePartial(this.container, this.value[0], theme, "11%", true);
110 | this.rValue = ValuePartial(this.container, this.value[1], theme, "11%", false);
111 |
112 | // Add ARIA attribute to input based on label text
113 | if(opts.label) this.lValue.setAttribute("aria-label", opts.label + " lower value");
114 | if(opts.label) this.lValue.setAttribute("aria-label", opts.label + " upper value");
115 |
116 | // An index to track what's being dragged:
117 | this.activeIndex = -1;
118 |
119 | setTimeout(() => {
120 | this.emit("initialized", this.value);
121 | });
122 |
123 | // // Gain focus
124 | // this.input.addEventListener("focus", () => {
125 | // this.focused = true;
126 | // });
127 |
128 | // // Lose focus
129 | // this.input.addEventListener("blur", () => {
130 | // this.focused = false;
131 | // });
132 |
133 | let mouseX = (ev) =>
134 | {
135 | // Get mouse position in page coords relative to the container:
136 | return ev.pageX - this.input.getBoundingClientRect().left;
137 | };
138 |
139 | let mouseMoveListener = ( ev ) =>
140 | {
141 | let fraction = clamp(mouseX(ev) / this.input.offsetWidth, 0, 1);
142 |
143 | this._SetFromMousePosition(fraction);
144 | };
145 |
146 | let mouseUpListener = ( ev ) =>
147 | {
148 | let fraction = clamp(mouseX(ev) / this.input.offsetWidth, 0, 1);
149 |
150 | this._SetFromMousePosition(fraction);
151 |
152 | document.removeEventListener("mousemove", mouseMoveListener);
153 | document.removeEventListener("mouseup", mouseUpListener);
154 |
155 | this.activeIndex = -1;
156 | };
157 |
158 | this.input.addEventListener("mousedown", (ev) =>
159 | {
160 | // Get mouse position fraction:
161 | let fraction = clamp(mouseX(ev) / this.input.offsetWidth, 0, 1);
162 |
163 | let posForLeftValue = this._Position(this.value[0]);
164 | let posForRightValue = this._Position(this.value[1]);
165 |
166 | // Get the current fraction of position --> [0, 1]:
167 | let lofrac = (posForLeftValue - this.minPos) / (this.maxPos - this.minPos);
168 | let hifrac = (posForRightValue - this.minPos) / (this.maxPos - this.minPos);
169 |
170 | // This is just for making decisions, so perturb it ever
171 | // so slightly just in case the bounds are numerically equal:
172 | lofrac -= Math.abs(this.maxPos - this.minPos) * 1e-15;
173 | hifrac += Math.abs(this.maxPos - this.minPos) * 1e-15;
174 |
175 | // Figure out which is closer:
176 | let lodiff = Math.abs(lofrac - fraction);
177 | let hidiff = Math.abs(hifrac - fraction);
178 |
179 | this.activeIndex = lodiff < hidiff ? 0 : 1;
180 |
181 | console.log(this.activeIndex);
182 |
183 | // Attach this to *document* so that we can still drag if the mouse
184 | // passes outside the container:
185 | document.addEventListener("mousemove", mouseMoveListener);
186 | document.addEventListener("mouseup", mouseUpListener);
187 | });
188 |
189 | // Defocus on mouse up (for non-accessibility users)
190 | this.input.addEventListener("mouseup", () => {
191 | this.input.blur();
192 | });
193 |
194 | this.input.oninput = () => {
195 | // let position = parseFloat(data.target.value);
196 | // var scaledValue = this._Value(position);
197 | // this.valueComponent.value = this._RoundNumber(scaledValue, this.precision);
198 | this.lValue.value = this.value[0];
199 | this.rValue.value = this.value[1];
200 | this.emit("input", this.value);
201 | };
202 |
203 | // Handle lower bound input box changes
204 | this.lValue.onchange = () => {
205 | let rawValue = this.lValue.value;
206 | let otherValue = parseFloat(this.rValue.value);
207 | if (Number(parseFloat(rawValue)) == rawValue) {
208 | let min = (this.scale == "log") ? Math.exp(this.min) : this.min;
209 | let max = (this.scale == "log") ? Math.exp(this.max) : this.max;
210 |
211 | // Input number is valid
212 | var value = parseFloat(rawValue);
213 | // Clamp to input range
214 | value = Math.min(Math.max(value, min), max);
215 | // Map to nearest step
216 | if (this.step) {
217 | value = Math.ceil((value - min) / this.step ) * this.step + min;
218 | }
219 | // Prevent value from going beyond interval upper value
220 | value = Math.min(value, otherValue);
221 |
222 | value = this._RoundNumber(value, this.precision);
223 |
224 | this.lValue.value = value;
225 | this.value = [value, otherValue];
226 | this.emit("input", [value, otherValue]);
227 | this._RefreshHandles([value, otherValue]);
228 | } else {
229 | // Input number is invalid
230 | // Go back to before input change
231 | this.lValue.value = this.lastValue[0];
232 | }
233 | };
234 |
235 | // Handle upper bound input box changes
236 | this.rValue.onchange = () => {
237 | let rawValue = this.rValue.value;
238 | let otherValue = parseFloat(this.lValue.value);
239 | if (Number(parseFloat(rawValue)) == rawValue) {
240 | let min = (this.scale == "log") ? Math.exp(this.min) : this.min;
241 | let max = (this.scale == "log") ? Math.exp(this.max) : this.max;
242 |
243 | // Input number is valid
244 | var value = parseFloat(rawValue);
245 | // Clamp to input range
246 | value = Math.min(Math.max(value, min), max);
247 |
248 | // Map to nearest step
249 | if (this.step) {
250 | value = Math.ceil((value - min) / this.step ) * this.step + min;
251 | }
252 | // Prevent value from going below interval lower value
253 | value = Math.max(value, otherValue);
254 |
255 | value = this._RoundNumber(value, this.precision);
256 |
257 | this.rValue.value = value;
258 | this.value = [otherValue, value];
259 | this.emit("input", [otherValue, value]);
260 | this._RefreshHandles();
261 | } else {
262 | // Input number is invalid
263 | // Go back to before input change
264 | this.rValue.value = this.lastValue[1];
265 | }
266 | };
267 | }
268 |
269 | /**
270 | * Calculate value from slider position
271 | */
272 | _Value(position) {
273 | if (this.scale === "log") {
274 | // Map from slider position range to log value range
275 |
276 | // Map from slider range to min-max value range
277 | let rangePos = (position - this.minPos) * this.logScale + this.min;
278 | // Now convert to log space
279 | return Math.exp(rangePos);
280 | } else {
281 | // Position and value are equivalent
282 | return position;
283 | }
284 | }
285 |
286 | /**
287 | * Calculate slider position from value
288 | */
289 | _Position(value) {
290 | if (this.scale === "log") {
291 | // Map from log value range to the slider's position range
292 | return this.minPos + (Math.log(value) - this.min) / this.logScale;
293 | } else {
294 | // Value and position are equivalent
295 | return value;
296 | }
297 | }
298 |
299 | /**
300 | * Updates the current value given a mouse X position normalized from 0 to 1.
301 | */
302 | _SetFromMousePosition( fraction )
303 | {
304 | if (this.activeIndex === -1) {
305 | return;
306 | }
307 |
308 | // Clip against the other bound:
309 | if (this.activeIndex === 0) {
310 | // Get the right side in position-space [0, 1]:
311 | let hifrac = (this._Position(this.value[1]) - this.minPos) / (this.maxPos - this.minPos);
312 | // Prevent fraction from exceeding right-side position
313 | fraction = Math.min(hifrac, fraction);
314 | } else {
315 | // Get the right side in position-space [0, 1]:
316 | let lofrac = (this._Position(this.value[0]) - this.minPos) / (this.maxPos - this.minPos);
317 | // Prevent fraction from going below left-side position
318 | fraction = Math.max(lofrac, fraction);
319 | }
320 |
321 | // Map from 0-1 scale to position-scale
322 | let position = lerp(this.minPos, this.maxPos, fraction);
323 | // Map from position-scale to value-scale and assign to values
324 | var newValue = this._Value(position);
325 |
326 | // Quantize the value
327 | if (this.step) {
328 | newValue = this.min + this.step * Math.round((newValue - this.min) / this.step);
329 | }
330 |
331 | this.value[this.activeIndex] = this._RoundNumber(newValue, this.precision);
332 |
333 | // Update and send the event:
334 | this._RefreshHandles();
335 | this.input.oninput();
336 | }
337 |
338 | SetValue( value )
339 | {
340 | if(this.focused !== true)
341 | {
342 | this.lValue.value = this._RoundNumber(parseFloat(value[0]), this.precision);
343 | this.rValue.value = this._RoundNumber(parseFloat(value[1]), this.precision);
344 |
345 | this.lastValue = [ parseFloat(value[0]), parseFloat(value[1]) ];
346 | }
347 | }
348 |
349 | // Formats the number for display.
350 | // `opts.precision` lets you customize how many decimal places you want here at most.
351 | // The default is 3.
352 | _RoundNumber(value, precision) {
353 | // https://stackoverflow.com/a/12830454/7138792
354 | return +parseFloat(value).toFixed(precision);
355 | }
356 |
357 | GetValue() {
358 | return [ this.lValue.value, this.rValue.value ];
359 | }
360 |
361 | _RefreshHandles() {
362 | let leftPercent = ((this._Position(this.value[0]) - this.minPos) / (this.maxPos - this.minPos)) * 100;
363 | let rightPercent = 100 - (((this._Position(this.value[1]) - this.minPos) / (this.maxPos - this.minPos)) * 100);
364 | css(this.handle, {
365 | left: `${leftPercent}%`,
366 | right: `${rightPercent}%`,
367 | });
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/src/components/public/range.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | :root {
4 | --range-track-color: var(--color-component-background);
5 | --range-thumb-color: var(--color-component-foreground);
6 | --range-thumb-highlight: var(--color-component-active);
7 |
8 | --range-track-color-disabled: var(--color-component-background-disabled);
9 | --range-thumb-color-disabled: var(--color-text-disabled);
10 | }
11 |
12 | input[type=range].guify-range {
13 | position: absolute; /* Gets rid of weird spacing below slider that I can't figure out the source of, seems internal */
14 | -webkit-appearance: none;
15 | width: 100%;
16 | height: var(--size-component-height);
17 | margin: 0px 0;
18 | padding: 0;
19 | display: inline-block;
20 |
21 | /* Fixes for Safari iOS */
22 | border-radius: 0;
23 | border: none;
24 | background-color: transparent;
25 | }
26 |
27 | .disabled input[type=range].guify-range {
28 | pointer-events: none;
29 | }
30 |
31 | /* Remove outlines since we'll be adding our own */
32 | input[type=range].guify-range:focus {
33 | outline: none;
34 | }
35 | input[type=range].guify-range::-moz-focus-outer {
36 | border: 0;
37 | }
38 |
39 | /* Webkit */
40 | input[type=range].guify-range::-webkit-slider-runnable-track {
41 | width: 100%;
42 | height: var(--size-component-height);
43 | cursor: ew-resize;
44 | background: var(--range-track-color);
45 | }
46 | input[type=range].guify-range::-webkit-slider-thumb {
47 | height: var(--size-component-height);
48 | width: 10px;
49 | background: var(--range-thumb-color);
50 | cursor: ew-resize;
51 | -webkit-appearance: none;
52 | margin-top: 0px;
53 | border: 0;
54 | }
55 | input[type=range].guify-range:focus::-webkit-slider-runnable-track {
56 | background: var(--range-thumb-highlight);
57 | outline: none;
58 | }
59 |
60 | .disabled input[type=range].guify-range::-webkit-slider-runnable-track { /* Disabled track */
61 | pointer-events: none;
62 | background: var(--range-track-color-disabled);
63 | }
64 |
65 | .disabled input[type=range].guify-range::-webkit-slider-thumb { /* Disabled thumb */
66 | pointer-events: none;
67 | background: var(--color-text-disabled);
68 | }
69 |
70 | /* Gecko */
71 | input[type=range].guify-range::-moz-range-track {
72 | width: 100%;
73 | height: var(--size-component-height);
74 | cursor: ew-resize;
75 | background: var(--range-track-color);
76 | }
77 | input[type=range].guify-range:focus::-moz-range-track {
78 | background: var(--range-thumb-highlight);
79 | }
80 | input[type=range].guify-range::-moz-range-thumb {
81 | height: var(--size-component-height);
82 | width: 10px;
83 | background: var(--range-thumb-color);
84 | cursor: ew-resize;
85 | border: none;
86 | border-radius: 0;
87 | }
88 |
89 | .disabled input[type=range].guify-range::-moz-range-track { /* Disabled track */
90 | pointer-events: none;
91 | background: var(--range-track-color-disabled);
92 | }
93 |
94 | .disabled input[type=range].guify-range::-moz-range-thumb { /* Disabled thumb */
95 | pointer-events: none;
96 | background: var(--range-thumb-color-disabled);
97 | }
--------------------------------------------------------------------------------
/src/components/public/range.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import css from "dom-css";
4 | import isnumeric from "is-numeric";
5 |
6 | import "./range.css";
7 |
8 | import { default as LabelPartial } from "../partials/label";
9 | import { default as ValuePartial } from "../partials/value";
10 |
11 | export default class Range extends ComponentBase {
12 | constructor (root, opts, theme) {
13 | super(root, opts, theme);
14 |
15 | this.scale = opts.scale;
16 |
17 | this.label = LabelPartial(this.container, opts.label, theme);
18 |
19 | this.input = this.container.appendChild(document.createElement("input"));
20 | this.input.type = "range";
21 | this.input.classList.add("guify-range");
22 | // Add ARIA attribute to input based on label text
23 | if(opts.label) this.input.setAttribute("aria-label", opts.label + " input");
24 |
25 | // Get initial value:
26 | if (opts.scale === "log") {
27 | // If logarithmic, we're going to set the slider to a known linear range. Then we'll
28 | // map that linear range to the user-set range using a log scale.
29 |
30 | // Check if all signs are valid
31 | if (opts.min * opts.max <= 0) {
32 | throw new Error("Log range min/max must have the same sign and not equal zero. Got min = " + opts.min + ", max = " + opts.max);
33 | }
34 |
35 | // Step is invalid for log scale slider
36 | if (isnumeric(opts.step)) {
37 | console.warn("Step is unused for log scale sliders.");
38 | }
39 |
40 | // Warn that `steps` was removed
41 | if (isnumeric(opts.steps)) {
42 | console.warn("\"steps\" option for log scale sliders has been removed.");
43 | }
44 |
45 | // Min/max are forced to a known range, and log value will be derived from slider position within.
46 | this.minPos = 0;
47 | this.maxPos = 1000000;
48 |
49 | this.min = Math.log( (isnumeric(opts.min)) ? opts.min : 0.000001 ); // This cannot be 0
50 | this.max = Math.log( (isnumeric(opts.max)) ? opts.max : 100 );
51 |
52 | this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
53 | this.logScale = (this.max - this.min) / (this.maxPos - this.minPos);
54 |
55 | this.initial = isnumeric(opts.initial) ? opts.initial : this.min;
56 |
57 | if (opts.initial < 0) {
58 | throw new Error(`Log range initial value must be > 0. Got initial value = ${opts.initial}`);
59 | }
60 | } else {
61 | // If linear, this is much simpler. Pos and value can directly match.
62 | this.minPos = (isnumeric(opts.min)) ? opts.min : 0;
63 | this.maxPos = (isnumeric(opts.max)) ? opts.max : 100;
64 | this.min = this.minPos;
65 | this.max = this.maxPos;
66 |
67 | this.precision = (isnumeric(opts.precision)) ? opts.precision : 3;
68 | this.step = (isnumeric(opts.step)) ? opts.step : (10 / Math.pow(10, 3)); // Default is the lowest possible number given the precision. When precision = 3, step = 0.01.
69 |
70 | this.initial = isnumeric(opts.initial) ? opts.initial : this.min;
71 |
72 | // Quantize the initial value to the nearest step:
73 | if (this.step != 0) {
74 | var initialStep = Math.round((this.initial - this.min) / this.step);
75 | this.initial = this.min + this.step * initialStep;
76 | }
77 | }
78 |
79 | // Set value on the this.input itself:
80 | this.input.min = this.minPos;
81 | this.input.max = this.maxPos;
82 | if (isnumeric(this.step)) {
83 | this.input.step = this.step;
84 | }
85 | this.input.value = this._Position(this.initial);
86 |
87 | css(this.input, {
88 | width: `calc(100% - ${theme.sizing.labelWidth} - 16% - 0.5em)`
89 | });
90 |
91 | this.valueComponent = ValuePartial(this.container, this.initial, theme, "16%");
92 | // Add ARIA attribute to input based on label text
93 | if(opts.label) this.valueComponent.setAttribute("aria-label", opts.label + " value");
94 |
95 | setTimeout(() => {
96 | this.emit("initialized", parseFloat(this.input.value));
97 | });
98 |
99 | this.userIsModifying = false;
100 |
101 | // Gain focus
102 | this.input.addEventListener("focus", () => {
103 | this.focused = true;
104 | });
105 |
106 | // Lose focus
107 | this.input.addEventListener("blur", () => {
108 | this.focused = false;
109 | });
110 |
111 | // Defocus on mouse up (for non-accessibility users)
112 | this.input.addEventListener("mouseup", () => {
113 | this.input.blur();
114 | });
115 |
116 | this.input.oninput = (data) => {
117 | let position = parseFloat(data.target.value);
118 | var scaledValue = this._Value(position);
119 | this.valueComponent.value = this._FormatNumber(scaledValue, this.precision);
120 | this.emit("input", scaledValue);
121 | };
122 |
123 | this.valueComponent.onchange = () => {
124 | let rawValue = this.valueComponent.value;
125 | if(Number(parseFloat(rawValue)) == rawValue){
126 | // Input number is valid
127 | var value = parseFloat(rawValue);
128 |
129 | // Ensure number fits slider properties
130 | value = this._ValidatedInputValue(value);
131 |
132 | this.valueComponent.value = value;
133 | this.emit("input", value);
134 | this.lastValue = value;
135 | } else {
136 | // Input number is invalid
137 | // Go back to before input change
138 | this.valueComponent.value = this.lastValue;
139 | }
140 | };
141 | }
142 |
143 | /**
144 | * Calculate value from slider position
145 | */
146 | _Value(position) {
147 | if (this.scale === "log") {
148 | // Map from slider position range to log value range
149 |
150 | // Map from slider range to min-max value range
151 | let rangePos = (position - this.minPos) * this.logScale + this.min;
152 | // Now convert to log space
153 | return Math.exp(rangePos);
154 | } else {
155 | // Position and value are equivalent
156 | return position;
157 | }
158 | }
159 |
160 | /**
161 | * Calculate slider position from value
162 | */
163 | _Position(value) {
164 | if (this.scale === "log") {
165 | // Map from log value range to the slider's position range
166 | return this.minPos + (Math.log(value) - this.min) / this.logScale;
167 | } else {
168 | // Value and position are equivalent
169 | return value;
170 | }
171 | }
172 |
173 | _ValidatedInputValue(value) {
174 | var newValue;
175 | if (this.scale === "log") {
176 | // Clamp to input range, turning logmin and logmax back into min/max in linear space
177 | newValue = Math.min(Math.max(value, Math.exp(this.min)), Math.exp(this.max));
178 | } else {
179 | // Clamp to input range
180 | newValue = Math.min(Math.max(value, this.min), this.max);
181 | // Quantize to step
182 | newValue = Math.ceil((newValue - this.min) / this.step) * this.step + this.min;
183 | }
184 | return this._FormatNumber(newValue, this.precision);
185 | }
186 |
187 | SetValue(value) {
188 | let validated = this._ValidatedInputValue(value);
189 | if(this.focused !== true) {
190 | this.valueComponent.value = this._FormatNumber(validated, this.precision);
191 | this.input.value = this._Position(validated);
192 | this.lastValue = validated;
193 | }
194 | }
195 |
196 | GetValue() {
197 | return this._Value(this.input.value);
198 | }
199 |
200 | // Formats the number for display.
201 | // `opts.precision` lets you customize how many decimal places you want here.
202 | // The default is 3.
203 | _FormatNumber(value, precision) {
204 | // https://stackoverflow.com/a/29249277
205 | return +parseFloat(value).toFixed(precision);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/components/public/select.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-select-dropdown {
4 | display: inline-block;
5 | position: absolute;
6 | width: calc(100% - var(--size-label-width));
7 | padding-left: 1.5%;
8 | height: var(--size-component-height);
9 | border: none;
10 | border-radius: 0;
11 | -webkit-appearance: none;
12 | -moz-appearance: none;
13 | -o-appearance:none;
14 | appearance: none;
15 | font-family: inherit;
16 | background-color: var(--color-component-background);
17 | color: var(--color-text-secondary);
18 | box-sizing: border-box !important;
19 | -moz-box-sizing: border-box !important;
20 | -webkit-box-sizing: border-box !important;
21 | }
22 |
23 | /* Disable default outline since we're providing our own */
24 | .guify-select-dropdown:focus {
25 | outline: none;
26 | }
27 | .guify-select-dropdown::-moz-focus-inner {
28 | border: 0;
29 | }
30 |
31 |
32 | .guify-select-dropdown:focus,
33 | .guify-select-dropdown:hover {
34 | color: var(--color-text-hover);
35 | background-color: var(--color-component-foreground);
36 | }
37 |
38 | .guify-select-dropdown::-ms-expand {
39 | display:none;
40 | }
41 | .guify-select-triangle {
42 | content: ' ';
43 | border-right: 3px solid transparent;
44 | border-left: 3px solid transparent;
45 | line-height: var(--size-component-height);
46 | position: absolute;
47 | right: 2.5%;
48 | z-index: 1;
49 | pointer-events: none;
50 | }
51 |
52 | .guify-select-triangle--up {
53 | bottom: 55%;
54 | border-bottom: 5px solid var(--color-text-secondary);
55 | border-top: 0px transparent;
56 | }
57 |
58 | .guify-select-triangle--down {
59 | top: 55%;
60 | border-top: 5px solid var(--color-text-secondary);
61 | border-bottom: 0px transparent;
62 | }
63 |
64 | .guify-select-triangle--up-highlight {
65 | border-bottom-color: var(--color-text-hover);
66 | }
67 |
68 | .guify-select-triangle--down-highlight {
69 | border-top-color: var(--color-text-hover);
70 | }
71 |
72 | /* Disabled styles */
73 |
74 | .disabled .guify-select-dropdown {
75 | pointer-events: none;
76 | color: var(--color-text-disabled);
77 | background-color: var(--color-component-background-disabled);
78 | }
79 |
80 | .disabled *[class^="guify-select-triangle"] {
81 | border-color: var(--color-text-disabled);
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/public/select.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import "./select.css";
4 |
5 | import { default as LabelPartial } from "../partials/label";
6 |
7 | export default class Select extends ComponentBase {
8 | constructor (root, opts, theme) {
9 | super(root, opts, theme);
10 |
11 | var i, downTriangle, upTriangle, key, option, el, keys;
12 |
13 | this.label = LabelPartial(this.container, opts.label, theme);
14 |
15 | this.input = document.createElement("select");
16 | this.input.classList.add("guify-select-dropdown");
17 | // Add ARIA attribute to input based on label text
18 | if(opts.label) this.input.setAttribute("aria-label", opts.label);
19 |
20 | downTriangle = document.createElement("span");
21 | downTriangle.classList.add("guify-select-triangle", "guify-select-triangle--down");
22 |
23 | upTriangle = document.createElement("span");
24 | upTriangle.classList.add("guify-select-triangle", "guify-select-triangle--up");
25 |
26 | this.container.appendChild(downTriangle);
27 | this.container.appendChild(upTriangle);
28 |
29 | if (Array.isArray(opts.options)) {
30 | for (i = 0; i < opts.options.length; i++) {
31 | option = opts.options[i];
32 | el = document.createElement("option");
33 | el.value = el.textContent = option;
34 | if (opts.initial === option) {
35 | el.selected = "selected";
36 | }
37 | this.input.appendChild(el);
38 | }
39 | } else {
40 | keys = Object.keys(opts.options);
41 | for (i = 0; i < keys.length; i++) {
42 | key = keys[i];
43 | el = document.createElement("option");
44 | el.value = key;
45 | if (opts.initial === key) {
46 | el.selected = "selected";
47 | }
48 | el.textContent = opts.options[key];
49 | this.input.appendChild(el);
50 | }
51 | }
52 |
53 | this.container.appendChild(this.input);
54 |
55 | this.input.onchange = (data) => {
56 | this.emit("input", data.target.value);
57 | };
58 |
59 | // Style the arrows based on mouse / focus behavior (and unfocus on mouse leave).
60 | // I'd like to do this through CSS :focus/:hover selectors but I just couldn't figure it out.
61 | // It could be done easily if CSS had a "general previous sibling" selector.
62 | let StyleFocus = () => {
63 | downTriangle.classList.add("guify-select-triangle--down-highlight");
64 | upTriangle.classList.add("guify-select-triangle--up-highlight");
65 | };
66 |
67 | let StyleUnfocus = () => {
68 | downTriangle.classList.remove("guify-select-triangle--down-highlight");
69 | upTriangle.classList.remove("guify-select-triangle--up-highlight");
70 | };
71 | let focused = false;
72 |
73 | this.input.addEventListener("mouseover", StyleFocus);
74 | this.input.addEventListener("focus", () => { focused = true; StyleFocus(); });
75 | this.input.addEventListener("blur", () => { focused = false; StyleUnfocus(); });
76 | this.input.addEventListener("mouseleave", () => { if (!focused) StyleUnfocus(); });
77 |
78 | }
79 |
80 | SetValue(value) {
81 | this.input.value = value;
82 | }
83 |
84 | GetValue() {
85 | return this.input.value;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/public/text.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-text-input {
4 | position: absolute;
5 | padding-left: 6px;
6 | height: var(--size-component-height);
7 | width: calc(100% - var(--size-label-width));
8 | border: none;
9 | background: var(--color-component-background);
10 | color: var(--color-text-secondary);
11 | font-family: inherit;
12 | box-sizing: border-box !important;
13 | resize: vertical;
14 |
15 | /* Fixes for Safari iOS */
16 | border-radius: 0;
17 | }
18 |
19 | .guify-text-input:focus {
20 | outline: none;
21 | }
22 |
23 | .disabled .guify-text-input {
24 | pointer-events: none;
25 | color: var(--color-text-disabled);
26 | background-color: var(--color-component-background-disabled);
27 | }
--------------------------------------------------------------------------------
/src/components/public/text.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import "./text.css";
4 |
5 | import { default as LabelPartial } from "../partials/label";
6 |
7 | export default class Text extends ComponentBase {
8 | static #supportedInputListenModes = ["input", "change"];
9 |
10 | constructor (root, opts, theme) {
11 | super(root, opts, theme);
12 |
13 | this.listenMode = opts.listenMode || "input";
14 | if (!Text.#supportedInputListenModes.includes(this.listenMode)) {
15 | console.error(`listenMode "${this.listenMode}" is not supported for text component "${opts.label}"! Falling back on "input".`);
16 | }
17 |
18 | this.label = LabelPartial(this.container, opts.label, theme);
19 |
20 | this.input = this.container.appendChild(document.createElement("input"));
21 | this.input.type = "text";
22 | this.input.classList.add("guify-text-input");
23 | if (opts.initial) this.input.value = opts.initial;
24 | // Add ARIA attribute to input based on label text
25 | if(opts.label) this.input.setAttribute("aria-label", opts.label);
26 |
27 | setTimeout(() => {
28 | this.emit("initialized", this.input.value);
29 | });
30 |
31 | this.input.addEventListener(this.listenMode, (data) => {
32 | console.log(data);
33 | this.emit("input", data.target.value);
34 | });
35 |
36 | // Gain focus
37 | this.input.addEventListener("focus", () => {
38 | this.focused = true;
39 | });
40 |
41 | // Lose focus
42 | this.input.addEventListener("blur", () => {
43 | this.focused = false;
44 | });
45 | }
46 |
47 | SetValue(value) {
48 | if(this.focused !== true)
49 | this.input.value = value;
50 | }
51 |
52 | GetValue() {
53 | return this.input.value;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/public/title.css:
--------------------------------------------------------------------------------
1 | @import "../variables.css";
2 |
3 | .guify-title {
4 | box-sizing: border-box;
5 | width: 100%;
6 | display: inline-block;
7 | height: var(--size-component-height);
8 | vertical-align: top;
9 | }
10 |
11 | .guify-title-text {
12 | display: inline-block;
13 | height: var(--size-component-height);
14 | line-height: var(--size-component-height);
15 | padding-left: 5px;
16 | padding-right: 5px;
17 | background-color: var(--color-text-primary);
18 | color: var(--color-panel-background);
19 | }
20 |
21 | /* Disabled style */
22 |
23 | .disabled .guify-title-text {
24 | background-color: var(--color-text-disabled);
25 | }
26 |
27 | /* Add a bit of top margin if immediately after another component */
28 |
29 | .guify-component-container > .guify-title {
30 | margin-top: 0.5em;
31 | }
--------------------------------------------------------------------------------
/src/components/public/title.js:
--------------------------------------------------------------------------------
1 | import ComponentBase from "../component-base.js";
2 |
3 | import "./title.css";
4 |
5 | export default class Title extends ComponentBase {
6 | constructor (root, opts, theme) {
7 | super(root, opts, theme);
8 |
9 | var background = this.container.appendChild(document.createElement("div"));
10 | background.classList.add("guify-title");
11 |
12 | var label = background.appendChild(document.createElement("div"));
13 | label.classList.add("guify-title-text");
14 | label.innerHTML = `■ ${opts.label} ■`;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-menu-bar-background: "black";
3 | --color-menu-bar-text: "black";
4 | --color-panel-background: "black";
5 |
6 | --color-component-background: "black";
7 | --color-component-background-hover: "black";
8 | --color-component-background-disabled: "black";
9 | --color-component-foreground: "black";
10 | --color-component-active: "black";
11 |
12 | --color-text-primary: "black";
13 | --color-text-secondary: "black";
14 | --color-text-hover: "black";
15 | --color-text-active: "black";
16 | --color-text-disabled: "black";
17 |
18 | --size-menu-bar-height: 25px;
19 | --size-component-height: 2rem;
20 | --size-component-spacing: 5px;
21 | --size-label-width: 42%;
22 |
23 | --font-family: ui-monospace, monospace;
24 | --font-height: 11px;
25 | --font-weight: 400;
26 | }
27 |
--------------------------------------------------------------------------------
/src/gui.css:
--------------------------------------------------------------------------------
1 | @import "./components/variables.css";
2 |
3 | .guify-container {
4 | position: absolute;
5 | left: 0;
6 | width: 100%;
7 | height: 100%;
8 | }
9 |
10 | /* Sub-elements of guify-container should appear over anything else. */
11 | .guify-container > * {
12 | z-index: 9999;
13 | }
14 |
15 | .guify-container, .guify-container * {
16 | font-family: var(--font-family);
17 | font-size: var(--font-size);
18 | font-weight: var(--font-weight);
19 | }
20 |
21 | .guify-container-overlay {
22 | height: 100%;
23 | }
24 |
25 | .guify-container-above {
26 | height: calc(100% + var(--size-menu-bar-height));
27 | bottom: 0;
28 | }
29 |
30 | /* Overlay container when fullscreen */
31 | .guify-fullscreen .guify-container {
32 | position: fixed;
33 | left: 0;
34 | top: 0;
35 | width: 100%;
36 | height: 100%;
37 | }
--------------------------------------------------------------------------------
/src/gui.js:
--------------------------------------------------------------------------------
1 | import css from "dom-css";
2 |
3 | import { default as Theme } from "./theme";
4 |
5 | import "./gui.css";
6 |
7 | import { ComponentManager } from "./component-manager";
8 |
9 | import { MenuBar } from "./components/internal/menu-bar";
10 | import { Panel } from "./components/internal/panel";
11 | import { ToastArea } from "./components/internal/toast-area";
12 | import screenfull from "screenfull";
13 |
14 | export default class GUI {
15 | constructor(opts) {
16 | this.opts = opts;
17 |
18 | this.hasRoot = opts.root !== undefined;
19 |
20 | opts.width = opts.width || 300;
21 | opts.root = opts.root || document.body;
22 | opts.align = opts.align || "left"; // Can be 'left' or 'right'
23 | opts.opacity = opts.opacity || 1.0;
24 | opts.barMode = opts.barMode || "offset"; // Can be 'none', 'above', 'offset', or 'overlay'
25 | opts.panelMode = opts.panelMode || "inner";
26 | opts.panelOverflowBehavior = opts.panelOverflowBehavior || "scroll";
27 | opts.pollRateMS = opts.pollRateMS || 100;
28 | opts.open = opts.open || false;
29 |
30 | // Set theme from opts
31 | let themeName = opts.theme || "dark";
32 | this.theme = new Theme(themeName);
33 | this.theme.Apply();
34 |
35 | this._ConstructElements();
36 | this._LoadStyles();
37 |
38 | if (screenfull.isEnabled) {
39 | screenfull.on("change", () => {
40 | this.opts.root.classList.toggle("guify-fullscreen", screenfull.isFullscreen);
41 | });
42 | }
43 |
44 | this.componentManager = new ComponentManager(this.theme);
45 |
46 | this.loadedComponents = [];
47 |
48 | // Begin component update loop
49 | this._UpdateComponents();
50 |
51 | }
52 |
53 | /**
54 | * Load any runtime styling information needed here.
55 | */
56 | _LoadStyles() {
57 | // Loads a font and appends it to the head
58 | let AppendFont = (href) => {
59 | var elem = document.createElement("style");
60 | elem.setAttribute("type", "text/css");
61 | elem.setAttribute("rel", "stylesheet");
62 | elem.setAttribute("href", href);
63 | document.getElementsByTagName("head")[0].appendChild(elem);
64 | };
65 | // Load the fonts we'll be using
66 | if(this.theme.font && this.theme.font.fontURL) {
67 | // Load theme font
68 | AppendFont(this.theme.font.fontURL);
69 | } else {
70 | // Fall back on "bundled" font
71 | AppendFont("//cdn.jsdelivr.net/font-hack/2.019/css/hack.min.css");
72 | }
73 | }
74 |
75 | /**
76 | * Create container, MenuBar, Panel, and ToastArea
77 | */
78 | _ConstructElements() {
79 | // Create the container that all the other elements will be contained within
80 | this.container = document.createElement("div");
81 | this.container.classList.add("guify-container");
82 |
83 | // Position the container relative to the root based on `opts`
84 | if (this.hasRoot && this.opts.barMode == "above") {
85 | this.container.classList.add("guify-container-above");
86 | } else if (this.hasRoot && this.opts.barMode == "overlay") {
87 | this.container.classList.add("guify-container-overlay");
88 | } else if (this.hasRoot && this.opts.barMode == "offset") {
89 | // Acts like "above", but adds top margin to the root to offset the title bar.
90 | this.container.classList.add("guify-container-above");
91 | // Add top margin to the root to offset for the menu bar.
92 | console.log(window.getComputedStyle(this.opts.root).getPropertyValue("margin-top"));
93 | let topMargin = window.getComputedStyle(this.opts.root).getPropertyValue("margin-top") || "0px";
94 | css(this.opts.root, {
95 | marginTop: `calc(${topMargin} + var(--size-menu-bar-height))`,
96 | });
97 | }
98 |
99 | // Insert the container into the root as the first element
100 | this.opts.root.insertBefore(this.container, this.opts.root.childNodes[0]);
101 |
102 | // Create a menu bar if specified in `opts`
103 | if(this.opts.barMode !== "none") {
104 | this.bar = new MenuBar(this.container, this.opts, this.theme);
105 | this.bar.addListener("ontogglepanel", () => {
106 | this.panel.ToggleVisible();
107 | });
108 | this.bar.addListener("onfullscreenrequested", () => {
109 | this.ToggleFullscreen();
110 | });
111 | }
112 |
113 | // Create panel
114 | this.panel = new Panel(this.container, this.opts, this.theme);
115 |
116 | // Show the panel by default if there's no menu bar or it's requested
117 | if(this.opts.barMode === "none" || this.opts.open === true) {
118 | this.panel.SetVisible(true);
119 | } else {
120 | // Otherwise hide it by default
121 | this.panel.SetVisible(false);
122 | }
123 |
124 | // Create toast area
125 | this.toaster = new ToastArea(this.container, this.opts, this.theme);
126 |
127 | }
128 |
129 | /**
130 | * Polling loop that allows our components to update themselves.
131 | * You can set the frequency of this using `this.opts.pollRateMS`.
132 | */
133 | _UpdateComponents() {
134 | this.loadedComponents.forEach((component) => {
135 | if(component.binding) {
136 | // Update the component from its bound value if the value has changed
137 | if(component.binding.object[component.binding.property] != component.oldValue) {
138 | component.SetValue(component.binding.object[component.binding.property]);
139 | component.oldValue = component.binding.object[component.binding.property];
140 | }
141 | }
142 | });
143 |
144 | setTimeout(() => {
145 | window.requestAnimationFrame(() => {
146 | this._UpdateComponents();
147 | });
148 | }, this.opts.pollRateMS);
149 |
150 | }
151 |
152 |
153 | /**
154 | * Creates a new component in the panel based on the provided options.
155 | * @param {*} [obj] Either an opts object or an array of opts objects
156 | * @param {Object} [applyToAll] Each property of this object will be applied to all opts objects
157 | */
158 | Register(obj, applyToAll = {}) {
159 | if (Array.isArray(obj)) {
160 | obj.forEach((item) => {
161 | let merged = Object.assign(item, applyToAll);
162 | this._Register(merged);
163 | });
164 | }
165 | else {
166 | let merged = Object.assign(obj, applyToAll);
167 | return this._Register(merged);
168 | }
169 | }
170 |
171 | Remove(obj) {
172 | obj.Remove();
173 | this.loadedComponents = this.loadedComponents.filter((item) => {
174 | return item !== obj;
175 | });
176 | }
177 |
178 | /**
179 | * Creates new component in the panel based on the options provided.
180 | *
181 | * @param {Object} [opts] Options for the component
182 | */
183 | _Register(opts) {
184 |
185 | if (opts.object && opts.property)
186 | if (opts.object[opts.property] === undefined)
187 | throw new Error(`Object ${opts.object} has no property '${opts.property}'`);
188 |
189 | // Set opts properties from the input
190 | if(opts.object && opts.property) {
191 | opts.initial = opts.object[opts.property];
192 | // If no label is specified, generate it from property name
193 | //opts.label = opts.label || property;
194 | }
195 |
196 | let root = this.panel.panel;
197 |
198 | // If a folder was specified, try to find a folder component with that name
199 | // and get its folderContainer.
200 | if(opts.folder) {
201 | let folderComp = this.loadedComponents.find((cmp) => {
202 | return cmp === opts.folder || (cmp.opts.type === "folder" && cmp.opts.label === opts.folder);
203 | });
204 |
205 | if(folderComp) root = folderComp.folderContainer;
206 | else throw new Error(`No folder exists with the name ${opts.folder}`);
207 | }
208 |
209 | let component = this.componentManager.Create(root, opts);
210 |
211 | // Add binding properties if specified
212 | if(opts.object && opts.property) {
213 | component["binding"] = { object: opts.object, property: opts.property };
214 | }
215 |
216 | // If the component has events, add listeners for those events.
217 | if(component.on) {
218 | component.on("initialized", function (data) {
219 | if(opts.onInitialize)
220 | opts.onInitialize(data);
221 | });
222 |
223 | component.on("input", (data) => {
224 | if(opts.object && opts.property)
225 | opts.object[opts.property] = data;
226 |
227 | if(opts.onChange) {
228 | opts.onChange(data);
229 | }
230 | });
231 | }
232 |
233 | this.loadedComponents.push(component);
234 |
235 | return component;
236 |
237 |
238 | }
239 |
240 | /**
241 | * Displays a toast notification under the MenuBar at the top of the root.
242 | *
243 | * @param {String} [message] The string you want displayed in the notification.
244 | * @param {Integer} [stayMS] The number of milliseconds to display the notification for
245 | * @param {Integer} [transitionMS] The number of milliseconds it takes for the notification to transition into disappearing
246 | */
247 | Toast(message, stayMS = 5000, transitionMS = 0) {
248 | this.toaster.CreateToast(message, stayMS, transitionMS);
249 | }
250 |
251 |
252 | ToggleFullscreen() {
253 | let isFullscreen = screenfull.isFullscreen;
254 | if (isFullscreen) {
255 | screenfull.exit();
256 | } else {
257 | console.log("Request fullscreen");
258 | screenfull.request(this.opts.root);
259 | }
260 | }
261 |
262 | // Just for debugging.
263 | _SetAllEnabled(enabled) {
264 | this.loadedComponents.forEach((item) => {
265 | item.SetEnabled(enabled);
266 | });
267 | }
268 |
269 | }
270 |
--------------------------------------------------------------------------------
/src/guify.js:
--------------------------------------------------------------------------------
1 | // Export GUI class as 'guify'.
2 | // Make sure Webpack is assigning the export to the global scope (don't set library.name)
3 | import { default as gui } from "./gui.js";
4 | let guify = gui;
5 | export { guify };
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import themes from "./themes";
2 |
3 | export default class Theme {
4 | constructor(themeName) {
5 |
6 | var theme = themes[themeName];
7 | if(theme === undefined) {
8 | console.error(`There is no theme preset with the name '${themeName}'! Defaulting to dark theme.`);
9 | theme = themes.dark;
10 | }
11 |
12 | // Merge the base theme with the theme parameters and make
13 | // the result properties of this object
14 | Object.assign(this, baseTheme, theme);
15 | }
16 |
17 | /**
18 | * Takes the values from the theme object and applies them as CSS variables to the page.
19 | */
20 | Apply() {
21 | console.log(this);
22 |
23 | let root = document.documentElement;
24 | root.style.setProperty("--color-menu-bar-background", this.colors.menuBarBackground);
25 | root.style.setProperty("--color-menu-bar-text", this.colors.menuBarText);
26 | root.style.setProperty("--color-panel-background", this.colors.panelBackground);
27 |
28 | root.style.setProperty("--color-component-background", this.colors.componentBackground);
29 | root.style.setProperty("--color-component-background-hover", this.colors.componentBackgroundHover);
30 | root.style.setProperty("--color-component-background-disabled", this.colors.componentBackgroundDisabled);
31 | root.style.setProperty("--color-component-foreground", this.colors.componentForeground);
32 | root.style.setProperty("--color-component-active", this.colors.componentActive);
33 |
34 | root.style.setProperty("--color-text-primary", this.colors.textPrimary);
35 | root.style.setProperty("--color-text-secondary", this.colors.textSecondary);
36 | root.style.setProperty("--color-text-hover", this.colors.textHover);
37 | root.style.setProperty("--color-text-active", this.colors.textActive);
38 | root.style.setProperty("--color-text-disabled", this.colors.textDisabled);
39 |
40 | root.style.setProperty("--size-menu-bar-height", this.sizing.menuBarHeight);
41 | root.style.setProperty("--size-component-height", this.sizing.componentHeight);
42 | root.style.setProperty("--size-component-spacing", this.sizing.componentSpacing);
43 | root.style.setProperty("--size-label-width", this.sizing.labelWidth);
44 |
45 | root.style.setProperty("--font-family", this.font.fontFamily);
46 | root.style.setProperty("--font-size", this.font.fontSize);
47 | root.style.setProperty("--font-weight", this.font.fontWeight);
48 |
49 | root.style.setProperty("--font-family-for-input", this.font.inputFontFamily);
50 | }
51 | }
52 |
53 | const baseTheme = {
54 | name: "BaseTheme",
55 |
56 | colors: {
57 | menuBarBackground: "black",
58 | menuBarText: "black",
59 | panelBackground: "black",
60 |
61 | componentBackground: "black",
62 | componentBackgroundHover: "black",
63 | componentBackgroundDisabled: "black",
64 | componentForeground: "black",
65 | componentActive: "black",
66 |
67 | textPrimary: "black",
68 | textSecondary: "black",
69 | textHover: "black",
70 | textActive: "black",
71 | textDisabled: "black",
72 | },
73 |
74 | sizing: {
75 | menuBarHeight: "25px",
76 | componentHeight: "20px",
77 | componentSpacing: "5px",
78 | labelWidth: "42%",
79 | },
80 |
81 | font: {
82 | fontFamily: "'Hack', ui-monospace, monospace",
83 | fontSize: "11px",
84 | fontWeight: "400",
85 |
86 | // The font family used for `value` components.
87 | inputFontFamily: "'Hack', ui-monospace, monospace",
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/src/themes.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | light: {
4 | name: "Light",
5 |
6 | colors: {
7 | menuBarBackground: "rgb(227, 227, 227)",
8 | menuBarText: "rgb(36, 36, 36)",
9 | panelBackground: "rgb(227, 227, 227)",
10 |
11 | componentBackground: "rgb(204, 204, 204)",
12 | componentBackgroundHover: "rgb(190, 190, 190)",
13 | componentBackgroundDisabled: "rgb(200, 200, 200)",
14 | componentForeground: "rgb(105, 105, 105)",
15 | componentActive: "rgb(36, 36, 36)",
16 |
17 | textPrimary: "rgb(36, 36, 36)",
18 | textSecondary: "rgb(87, 87, 87)",
19 | textHover: "rgb(204, 204, 204)",
20 | textActive: "rgb(204, 204, 204)",
21 | textDisabled: "rgb(180, 180, 180)",
22 | },
23 |
24 | },
25 |
26 | dark: {
27 | name: "Dark",
28 |
29 | colors: {
30 | menuBarBackground: "rgb(35, 35, 35)",
31 | menuBarText: "rgb(235, 235, 235)",
32 | panelBackground: "rgb(35, 35, 35)",
33 |
34 | componentBackground: "rgb(54, 54, 54)",
35 | componentBackgroundHover: "rgb(76, 76, 76)",
36 | componentBackgroundDisabled: "rgb(24, 24, 24)",
37 | componentForeground: "rgb(112, 112, 112)",
38 | componentActive: "rgb(202, 202, 202)",
39 |
40 | textPrimary: "rgb(235, 235, 235)",
41 | textSecondary: "rgb(181, 181, 181)",
42 | textHover: "rgb(235, 235, 235)",
43 | textActive: "rgb(54, 54, 54)",
44 | textDisabled: "rgb(54, 54, 54)",
45 | },
46 |
47 | },
48 |
49 | // Color scheme from https://metakirby5.github.io/yorha/
50 | yorha: {
51 | name: "YoRHa",
52 |
53 | colors: {
54 | menuBarBackground: "#CCC8B1",
55 | menuBarText: "#454138",
56 | panelBackground: "#CCC8B1",
57 |
58 | componentBackground: "#BAB5A1",
59 | componentBackgroundHover: "#877F6E",
60 | componentBackgroundDisabled: "#DED8C7",
61 | componentForeground: "#454138",
62 | componentActive: "#978F7E",
63 |
64 | textPrimary: "#454138",
65 | textSecondary: "#454138",
66 | textHover: "#CCC8B1",
67 | textActive: "#CCC8B1",
68 | textDisabled: "#BAB5A6",
69 | },
70 |
71 | //Optional
72 | font: {
73 | fontFamily: "helvetica, sans-serif",
74 | fontSize: "14px",
75 | fontWeight: "100",
76 | inputFontFamily: "ui-monospace, monospace",
77 | },
78 | },
79 |
80 |
81 | };
82 |
--------------------------------------------------------------------------------
/src/utils/math-utils.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Linearly interpolates `t` between `v0` and `v1`.
4 | * https://en.wikipedia.org/wiki/Linear_interpolation
5 | * @param {*} v0 Lower bound
6 | * @param {*} v1 Upper bound
7 | * @param {*} t Progress (value between 0 and 1)
8 | * @returns Mapped number
9 | */
10 | function lerp(v0, v1, t) {
11 | return (1 - t) * v0 + t * v1;
12 | }
13 |
14 | export { lerp };
--------------------------------------------------------------------------------
/test/library.spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, before */
2 |
3 | // import chai from 'chai';
4 | // import {GUI} from '../lib/guify.js';
5 |
6 | // chai.expect();
7 |
8 | // const expect = chai.expect;
9 |
10 | // let gui;
11 |
12 | // describe('Given an instance of my GUI library', () => {
13 | // before(() => {
14 | // gui = new GUI();
15 | // });
16 | // describe('when I need the name', () => {
17 | // it('should return the name', () => {
18 | // expect(gui.name).to.be.equal('Cat');
19 | // });
20 | // });
21 | // });
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* global __dirname, require, module*/
2 |
3 | const webpack = require('webpack');
4 | const ESLintPlugin = require('eslint-webpack-plugin');
5 | const path = require('path');
6 |
7 | let libraryName = 'guify';
8 |
9 | let plugins = [], outputFile;
10 |
11 | module.exports = (env, argv) => {
12 |
13 | console.log(`Current mode=${argv.mode}\n`)
14 |
15 | if (argv.mode === 'production') { // Uses --mode argument to webpack, or NODE_ENV if not defined.
16 | outputFile = libraryName + '.min.js';
17 | } else if (argv.mode === 'development') {
18 | let linter = new ESLintPlugin();
19 | plugins.push(linter);
20 | outputFile = libraryName + '.js';
21 | } else {
22 | throw new Error(`Invalid development mode ${argv.mode}!`)
23 | }
24 |
25 | let config = {
26 | entry: __dirname + '/src/guify.js',
27 | devtool: 'source-map',
28 | output: {
29 | path: __dirname + '/lib',
30 | filename: outputFile,
31 | library: {
32 | type: 'umd',
33 | },
34 | },
35 | module: {
36 | rules: [
37 | { // Process js files
38 | test: /\.js$/i,
39 | loader: 'babel-loader',
40 | exclude: /(node_modules|bower_components)/
41 | },
42 | {
43 | test: /\.css$/i,
44 | use: ["style-loader", "css-loader", "postcss-loader"],
45 | },
46 | // { // Lint all js files with eslint-loader
47 | // test: /(\.jsx|\.js)$/,
48 | // loader: 'eslint-loader',
49 | // exclude: /node_modules/
50 | // }
51 | ]
52 | },
53 | resolve: {
54 | modules: [path.resolve('./node_modules'), path.resolve('./src')],
55 | extensions: ['.json', '.js'],
56 | },
57 | plugins: plugins,
58 | devServer: {
59 | compress: true,
60 | port: 9000,
61 | static: {
62 | directory: path.join(__dirname, ''),
63 | serveIndex: true,
64 | },
65 | open: {
66 | target: ['/example'],
67 | }
68 | }
69 | }
70 |
71 | return config
72 | }
73 |
--------------------------------------------------------------------------------