From 379f260de519cf2ce996339b01c050956cfb7b7a Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Mon, 17 Jul 2023 00:46:55 -0500 Subject: [PATCH] add color-selector component and colorings --- shared/naturalcrit/codeEditor/codeEditor.less | 27 ++++++ .../widget-elements/checkbox/checkbox.jsx | 17 ++-- .../color-selector/color-selector.jsx | 92 +++++++++++++++++++ .../color-selector/color-selector.less | 8 ++ .../helpers/widget-elements/constants.js | 19 +++- .../image-selector/image-selector.jsx | 31 ++++--- .../helpers/widget-elements/index.js | 4 +- .../helpers/widget-elements/modal/modal.jsx | 28 +++++- .../helpers/widget-elements/text/text.jsx | 33 ++++--- .../naturalcrit/codeEditor/helpers/widgets.js | 16 +++- themes/V3/5ePHB/widgets.js | 10 +- 11 files changed, 236 insertions(+), 49 deletions(-) create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.jsx create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.less diff --git a/shared/naturalcrit/codeEditor/codeEditor.less b/shared/naturalcrit/codeEditor/codeEditor.less index 81ef207cb..48fbf97b6 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.less +++ b/shared/naturalcrit/codeEditor/codeEditor.less @@ -3,6 +3,7 @@ @import (less) 'codemirror/addon/search/matchesonscrollbar.css'; @import (less) 'codemirror/addon/dialog/dialog.css'; @import (less) 'codemirror/addon/hint/show-hint.css'; +@import 'naturalcrit/styles/colors.less'; @keyframes sourceMoveAnimation { 50% {background-color: red; color: white;} @@ -52,4 +53,30 @@ max-width: 10vw; } } + + .widget-field { + border: 2px solid #ddd; + + &.default { + background-color: @purple; + border: 2px solid @purple; + color: white; + + >input { + background-color: @purple; + color: white; + } + } + + &.suggested { + background-color: #ddd; + border: 2px dashed grey; + color: grey; + + >input { + background-color: #ddd; + color: black; + } + } + } } diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/checkbox/checkbox.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/checkbox/checkbox.jsx index 5659d7df1..9b90a6009 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/checkbox/checkbox.jsx +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/checkbox/checkbox.jsx @@ -7,10 +7,11 @@ const CodeMirror = require('../../../code-mirror.js'); const Checkbox = createClass({ getDefaultProps : function() { return { - value : '', - prefix : '', - cm : {}, - n : -1 + value : '', + prefix : '', + cm : {}, + n : -1, + default : false }; }, @@ -28,11 +29,15 @@ const Checkbox = createClass({ }, render : function() { - const { cm, n, value, prefix } = this.props; + const { cm, n, value, prefix, def } = this.props; const { text } = cm.lineInfo(n); const id = [prefix, value, n].join('-'); + let className = 'widget-field widget-checkbox'; + if(def) { + className += ' default'; + } return -
+
diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.jsx new file mode 100644 index 000000000..eb1d9b607 --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.jsx @@ -0,0 +1,92 @@ +require('./color-selector.less'); +const React = require('react'); +const createClass = require('create-react-class'); +const { PATTERNS, STYLE_FN } = require('../constants'); +const CodeMirror = require('../../../code-mirror'); +const debounce = require('lodash.debounce'); + +const ColorSelector = createClass({ + getDefaultProps : function() { + return { + field : {}, + cm : {}, + n : undefined, + text : '', + def : false + }; + }, + getInitialState : function() { + return { + value : '' + }; + }, + componentDidMount : function() { + const { field, text } = this.props; + const pattern = PATTERNS.field[field.type](field.name); + const [_, __, value] = text.match(pattern) ?? []; + this.setState({ + value : value, + }); + }, + componentDidUpdate({ text }) { + const { field } = this.props; + if(this.props.text !== text) { + const pattern = PATTERNS.field[field.type](field.name); + const [_, __, value] = this.props.text.match(pattern) ?? []; + this.setState({ + value, + }); + } + }, + onChange : function(e) { + const { cm, text, field, n } = this.props; + const pattern = PATTERNS.field[field.type](field.name); + const [_, label, current] = text.match(pattern) ?? [null, field.name, '']; + let index = text.indexOf(`${label}:${current}`); + while (index !== -1 && text[index - 1] === '-') { + index = text.indexOf(`${label}:${current}`, index + 1); + } + let value = e.target.value; + if(index === -1) { + index = text.length; + value = `,${field.name}:${value}`; + } else { + index = index + 1 + field.name.length; + } + cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + current.length), '+insert'); + this.setState({ + value : e.target.value, + }); + }, + debounce : debounce((self, e)=>self.onChange(e), 300), + onChangeDebounce : function(e) { + this.setState({ + value : e.target.value, + }); + this.debounce(this, e); + }, + render : function() { + const { field, n, text, def } = this.props; + const { value } = this.state; + const style = STYLE_FN(value); + const id = `${field?.name}-${n}`; + const pattern = PATTERNS.field[field.type](field.name); + const [_, label, __] = text.match(pattern) ?? [null, undefined, '']; + let className = 'widget-field color-selector'; + if(!label) { + className += ' suggested'; + } + if(def) { + className += ' default'; + } + return +
+ + + +
+
; + } +}); + +module.exports = ColorSelector; \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.less b/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.less new file mode 100644 index 000000000..95c8b6909 --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/color-selector/color-selector.less @@ -0,0 +1,8 @@ +.color-selector { + .color { + height: 17px; + width: 13px; + padding: 0; + margin: 0; + } +} \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js b/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js index 0dbbab867..7840b2c5d 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js @@ -1,3 +1,5 @@ +const _ = require('lodash'); + export const UNITS = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']; export const HINT_TYPE = { @@ -15,8 +17,10 @@ export const FIELD_TYPE = { TEXT : 0, CHECKBOX : 1, IMAGE_SELECTOR : 2, + COLOR_SELECTOR : 3, }; +const textField = (name)=>new RegExp(`[{,;](${name}):([^};,"\\(]*\\((?!,)[^};"\\)]*\\)|"[^},;"]*"|[^},;]*)`); export const PATTERNS = { snippet : { [SNIPPET_TYPE.BLOCK] : (name)=>new RegExp(`^{{${name}(?:[^a-zA-Z].*)?`), @@ -24,10 +28,23 @@ export const PATTERNS = { [SNIPPET_TYPE.INJECTOR] : ()=>new RegExp(`^\\!\\[(?:[a-zA-Z -]+)?\\]\\(.*\\).*{[a-zA-Z0-9:, "'-]+}$`), }, field : { - [FIELD_TYPE.TEXT] : (name)=>new RegExp(`[{,;](${name}):([^};,"\\(]*\\((?!,)[^};"\\)]*\\)|"[^},;"]*"|[^},;]*)`), + [FIELD_TYPE.TEXT] : textField, [FIELD_TYPE.IMAGE_SELECTOR] : (name)=>new RegExp(`{{(${name})(\\d*)`), + [FIELD_TYPE.COLOR_SELECTOR] : textField }, collectStyles : new RegExp(`(?:([a-zA-Z-]+):(?!\\/))+`, 'g'), }; export const NUMBER_PATTERN = new RegExp(`([^-\\d]*)([-\\d]+)(${UNITS.join('|')})?(.*)`); + +export const fourDigitNumberFromValue = (value)=>typeof value === 'number' ? (()=>{ + const str = String(value); + return _.range(0, 4 - str.length).map(()=>'0').join('') + str; +})() : value; + +const DEFAULT_WIDTH = '30px'; + +export const STYLE_FN = (value, extras = {})=>({ + width : `calc(${value?.length ?? 0}ch + ${value?.length ? `${DEFAULT_WIDTH} / 2` : DEFAULT_WIDTH})`, + ...extras +}); \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx index 17e99f079..8e2e2a547 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx @@ -1,8 +1,7 @@ require('./image-selector.less'); const React = require('react'); -const ReactDOMClient = require('react-dom/client'); const createClass = require('create-react-class'); -const Modal = require('../modal/modal.jsx'); +const { Modal, modalHelpers } = require('../modal/modal.jsx'); const { PATTERNS } = require('../constants.js'); const CodeMirror = require('../../../code-mirror.js'); const _ = require('lodash'); @@ -25,13 +24,7 @@ const ImageSelector = createClass({ }, componentDidMount : function() { - const el = document.createElement('div'); - const root = ReactDOMClient.createRoot(el); - document.querySelector('body').append(el); - this.setState({ - el, - modalRoot : root - }); + modalHelpers.mount(this); }, componentDidUpdate : function() { @@ -39,7 +32,7 @@ const ImageSelector = createClass({ const { selected } = this.state; const images = values.map((v, i)=>{ - const className = selected === v ? 'selected' : ''; + const className = String(selected) === String(v) ? 'selected' : ''; return {`${name}this.select(v)}/>; }); @@ -51,7 +44,7 @@ const ImageSelector = createClass({ }, componentWillUnmount : function() { - this.state.el.remove(); + modalHelpers.unmount(this); }, save : function() { @@ -75,9 +68,23 @@ const ImageSelector = createClass({ }); }, + showModal : function() { + const { cm, field, n } = this.props; + const { text } = cm.lineInfo(n); + const pattern = PATTERNS.field[field.type](field.name); + const [fullmatch, _, current] = text.match(pattern); + if(!fullmatch) { + return; + } + this.setState({ + selected : current + }); + this.modalRef.current.setVisible(true); + }, + render : function () { return - + ; } }); diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js b/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js index 9c916d515..de42cda46 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js @@ -1,9 +1,11 @@ const Text = require('./text/text.jsx'); const Checkbox = require('./checkbox/checkbox.jsx'); const ImageSelector = require('./image-selector/image-selector.jsx'); +const ColorSelector = require('./color-selector/color-selector.jsx'); module.exports = { Text : Text, Checkbox : Checkbox, - ImageSelector : ImageSelector + ImageSelector : ImageSelector, + ColorSelector : ColorSelector, }; diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx index dfb145183..dcfe46f8f 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx @@ -1,5 +1,6 @@ require('./modal.less'); const React = require('react'); +const ReactDOMClient = require('react-dom/client'); const createClass = require('create-react-class'); const Modal = createClass({ @@ -46,4 +47,29 @@ const Modal = createClass({ } }); -module.exports = Modal; \ No newline at end of file +module.exports = { + /* + * Requirements: + * - modalRef member variable + * - should be re-rendered via `this.state.modalRoot?.render` in `componentDidUpdate` + */ + Modal, + modalHelpers : { + // should be called in `componentDidMount` + // `self` should be passed as the component instance (`this`) + mount : (self)=>{ + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + document.querySelector('body').append(el); + self.setState({ + el, + modalRoot : root + }); + }, + // should be called in `componentWillUnmount` + // `self` should be passed as the component instance (`this`) + unmount : (self)=>{ + self.state.el.remove(); + } + } +}; \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx index 7b3b4e3c1..64f2a2a2d 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx @@ -2,15 +2,9 @@ require('./text.less'); const React = require('react'); const createClass = require('create-react-class'); const _ = require('lodash'); -const { NUMBER_PATTERN, HINT_TYPE, PATTERNS } = require('../constants'); +const { NUMBER_PATTERN, HINT_TYPE, PATTERNS, STYLE_FN } = require('../constants'); const CodeMirror = require('../../../code-mirror.js'); -const DEFAULT_WIDTH = '30px'; - -const STYLE_FN = (value)=>({ - width : `calc(${value?.length ?? 0}ch + ${value?.length ? `${DEFAULT_WIDTH} / 2` : DEFAULT_WIDTH})` -}); - const Text = createClass({ fieldRef : {}, @@ -21,14 +15,14 @@ const Text = createClass({ n : 0, setHints : ()=>{}, onChange : ()=>{}, - getStyleHints : ()=>{} + getStyleHints : ()=>{}, + def : false }; }, getInitialState : function() { return { value : '', - style : STYLE_FN(), id : '' }; }, @@ -40,7 +34,6 @@ const Text = createClass({ const [_, __, value] = this.props.text.match(pattern) ?? []; this.setState({ value : value, - style : STYLE_FN(value), id : `${field?.name}-${n}` }); } @@ -58,7 +51,6 @@ const Text = createClass({ const [_, __, value] = text.match(pattern) ?? []; this.setState({ value : value, - style : STYLE_FN(value), id }); this.fieldRef[id] = React.createRef(); @@ -133,19 +125,26 @@ const Text = createClass({ } else { index = index + 1 + field.name.length; } - if(!value) return; cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + current.length), '+insert'); this.setState({ value : e.target.value, - style : STYLE_FN(e.target.value) }); }, render : function() { - const { value, id, style } = this.state; - const { field } = this.props; - + const { value, id } = this.state; + const { field, text, def } = this.props; + const style = STYLE_FN(value); + const pattern = PATTERNS.field[field.type](field.name); + const [_, label, __] = text.match(pattern) ?? [null, undefined, '']; + let className = 'widget-field'; + if(!label) { + className += ' suggested'; + } + if(def) { + className += ' default'; + } return -
+
f.type === FIELD_TYPE.TEXT).map((f)=>f.name); + const textFieldNames = (widget.fields || []).filter((f)=>f.type === FIELD_TYPE.TEXT || f.type === FIELD_TYPE.COLOR_SELECTOR).map((f)=>f.name); const { text } = cm.lineInfo(n); const fields = (widget.fields || []).map((field)=>{ const key = genKey(widget.name, n, field.name); if(field.type === FIELD_TYPE.CHECKBOX) { - return ; + return ; } else if(field.type === FIELD_TYPE.TEXT) { - return setHints(h, f)} getStyleHints={getStyleHints}/>; + return setHints(h, f)} getStyleHints={getStyleHints} def={true}/>; } else if(field.type === FIELD_TYPE.IMAGE_SELECTOR) { return ; + } else if(field.type === FIELD_TYPE.COLOR_SELECTOR) { + return ; } else { return null; } @@ -110,7 +112,11 @@ module.exports = function(widgets, cm, setHints) { increment : 5, hints : true, }; - return setHints(h, f)} getStyleHints={getStyleHints}/>; + const key = genKey(widget.name, n, style); + if(style.includes('color')) { + return ; + } + return setHints(h, f)} getStyleHints={getStyleHints}/>; }).filter(Boolean); const root = roots[n][id] ?? ReactDOMClient.createRoot(node || parent); diff --git a/themes/V3/5ePHB/widgets.js b/themes/V3/5ePHB/widgets.js index 5cc361baf..3f5709421 100644 --- a/themes/V3/5ePHB/widgets.js +++ b/themes/V3/5ePHB/widgets.js @@ -1,10 +1,5 @@ const _ = require('lodash'); -const { SNIPPET_TYPE, FIELD_TYPE } = require('../../../shared/naturalcrit/codeEditor/helpers/widget-elements/constants'); - -const fourDigitNumberFromValue = (value)=>typeof value === 'number' ? (()=>{ - const str = String(value); - return _.range(0, 4 - str.length).map(()=>'0').join('') + str; -})() : value; +const { SNIPPET_TYPE, FIELD_TYPE, fourDigitNumberFromValue } = require('../../../shared/naturalcrit/codeEditor/helpers/widget-elements/constants'); module.exports = [{ name : 'monster', @@ -93,6 +88,9 @@ module.exports = [{ name : 'opacity', type : FIELD_TYPE.TEXT, increment : 5 + }, { + name : 'background-color', + type : FIELD_TYPE.COLOR_SELECTOR, }] }, { name : 'imageMaskCenter',