From bef6b94dc42ab09986d1d8346516a007b8cc6643 Mon Sep 17 00:00:00 2001 From: Charlie Humphreys Date: Sun, 16 Jul 2023 02:14:43 -0500 Subject: [PATCH] add image selector field type, modal component, and new snippet widgets --- shared/naturalcrit/codeEditor/codeEditor.jsx | 4 +- .../helpers/widget-elements/constants.js | 8 +- .../image-selector/image-selector.jsx | 86 +++++++++++++++++++ .../image-selector/image-selector.less | 21 +++++ .../helpers/widget-elements/index.js | 6 +- .../helpers/widget-elements/modal/modal.jsx | 49 +++++++++++ .../helpers/widget-elements/modal/modal.less | 58 +++++++++++++ .../helpers/widget-elements/text/text.jsx | 2 +- .../naturalcrit/codeEditor/helpers/widgets.js | 29 +++++-- themes/V3/5ePHB/widgets.js | 55 +++++++++++- 10 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.less create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx create mode 100644 shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.less diff --git a/shared/naturalcrit/codeEditor/codeEditor.jsx b/shared/naturalcrit/codeEditor/codeEditor.jsx index c4762b20c..b763c2a30 100644 --- a/shared/naturalcrit/codeEditor/codeEditor.jsx +++ b/shared/naturalcrit/codeEditor/codeEditor.jsx @@ -165,7 +165,7 @@ const CodeEditor = createClass({ const { line } = cm.getCursor(); for (const key in this.state.widgets) { if(key != line) { - this.state.widgets[key]?.clear(); + this.state.widgetUtils.removeLineWidget(key, this.state.widgets[key]); } } const { widgets } = this.codeMirror.lineInfo(line); @@ -196,7 +196,7 @@ const CodeEditor = createClass({ } } else { for (const widget of widgets) { - widget.clear(); + this.state.widgetUtils.removeLineWidget(n, widget); } } } diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js b/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js index 99436ea69..0dbbab867 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/constants.js @@ -12,8 +12,9 @@ export const SNIPPET_TYPE = { }; export const FIELD_TYPE = { - TEXT : 0, - CHECKBOX : 1 + TEXT : 0, + CHECKBOX : 1, + IMAGE_SELECTOR : 2, }; export const PATTERNS = { @@ -23,7 +24,8 @@ 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] : (name)=>new RegExp(`[{,;](${name}):([^};,"\\(]*\\((?!,)[^};"\\)]*\\)|"[^},;"]*"|[^},;]*)`), + [FIELD_TYPE.IMAGE_SELECTOR] : (name)=>new RegExp(`{{(${name})(\\d*)`), }, collectStyles : new RegExp(`(?:([a-zA-Z-]+):(?!\\/))+`, 'g'), }; 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 new file mode 100644 index 000000000..162352c52 --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.jsx @@ -0,0 +1,86 @@ +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 { PATTERNS } = require('../constants.js'); +const CodeMirror = require('../../../code-mirror.js'); +const _ = require('lodash'); + +const ImageSelector = createClass({ + modalRef : React.createRef(), + + getDefaultProps : function () { + return { + field : {}, + cm : {}, + n : undefined + }; + }, + + getInitialState : function() { + return { + selected : undefined, + }; + }, + + componentDidMount : function() { + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + document.querySelector('body').append(el); + this.setState({ + el, + modalRoot : root + }); + }, + + componentDidUpdate : function() { + const { name, preview, values } = this.props.field; + const { selected } = this.state; + + const images = values.map((v, i)=>{ + const className = selected === v ? 'selected' : ''; + return {`${name}this.select(v)}/>; + }); + + this.state.modalRoot?.render( +
+ {images} +
+
); + }, + + componentWillUnmount : function() { + this.state.el.remove(); + }, + + save : function() { + const { cm, field, n } = this.props; + const { text } = cm.lineInfo(n); + const pattern = PATTERNS.field[field.type](field.name); + const [fullmatch, label, current] = text.match(pattern); + if(!fullmatch) { + console.warn('something is wrong... please report this warning with a screenshot'); + return; + } + const currentText = `${label}${current ?? ''}`; + const index = 2; + const value = label + this.state.selected; + console.log(text, pattern, currentText, value, fullmatch, label, current); + cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + currentText.length), '+insert'); + }, + + select : function(value) { + this.setState({ + selected : value + }); + }, + + render : function () { + return + + ; + } +}); + +module.exports = ImageSelector; \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.less b/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.less new file mode 100644 index 000000000..92a5ef83f --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/image-selector/image-selector.less @@ -0,0 +1,21 @@ +.images { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + img { + flex: 0 0 auto; + max-width: 10vw; + max-height: 20vh; + border-radius: 10px; + + &:hover { + background-color: rgba(0, 0, 0, .1); + } + + &.selected { + background-color: rgba(0, 0, 0, .175); + } + } +} + diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js b/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js index 8fbc04aa2..9c916d515 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/index.js @@ -1,7 +1,9 @@ const Text = require('./text/text.jsx'); const Checkbox = require('./checkbox/checkbox.jsx'); +const ImageSelector = require('./image-selector/image-selector.jsx'); module.exports = { - Text : Text, - Checkbox : Checkbox, + Text : Text, + Checkbox : Checkbox, + ImageSelector : ImageSelector }; diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx new file mode 100644 index 000000000..dfb145183 --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.jsx @@ -0,0 +1,49 @@ +require('./modal.less'); +const React = require('react'); +const createClass = require('create-react-class'); + +const Modal = createClass({ + getDefaultProps : function () { + return { + header : '', + save : ()=>{}, + }; + }, + + getInitialState : function() { + return { + visible : false, + }; + }, + + setVisible : function(visible) { + this.setState({ + visible + }); + }, + + save : function() { + this.props.save(); + this.setVisible(false); + }, + + render : function () { + const { children, header } = this.props; + const { visible } = this.state; + return + {visible ?
+
+

{header}

+
+ {children} +
+ + +
+
+
: null} +
; + } +}); + +module.exports = Modal; \ No newline at end of file diff --git a/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.less b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.less new file mode 100644 index 000000000..23b7ccdea --- /dev/null +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/modal/modal.less @@ -0,0 +1,58 @@ +@import 'naturalcrit/styles/colors.less'; + +.bg-cover { + width: 100vw; + height: 100vh; + position: absolute; + z-index: 10000000; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, .5); +} + +.modal { + position: absolute; + top: 10vh; + left: 25vw; + width: 50vw; + min-height: 50vh; + max-height: 80vh; + background-color: #fff; + border-radius: 10px; + box-shadow: 5px 5px 50px black; + display: flex; + flex-direction: column; + justify-content: space-between; + + h1 { + margin-top: 5px; + margin-left: 5px; + font-size: 2em; + } + + >* { + flex: 0 0 auto; + } + + .action-row { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: left; + + >* { + flex: 0 0 auto; + margin-left: 5px; + } + } + + button { + &#cancel { + background-color: @redLight; + + &:hover { + background-color: @red; + } + } + } +} \ 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 3f3e64ca3..55185fd8e 100644 --- a/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx +++ b/shared/naturalcrit/codeEditor/helpers/widget-elements/text/text.jsx @@ -47,7 +47,7 @@ const Text = createClass({ if(this.state.value !== value) { const { field } = this.props; - this.props.setHints(this, this.props.getStyleHints(field, field.hints ? this.state.value : [])); + this.props.setHints(this, field.hints ? this.props.getStyleHints(field, this.state.value) : []); } }, diff --git a/shared/naturalcrit/codeEditor/helpers/widgets.js b/shared/naturalcrit/codeEditor/helpers/widgets.js index 306131581..e64b7db13 100644 --- a/shared/naturalcrit/codeEditor/helpers/widgets.js +++ b/shared/naturalcrit/codeEditor/helpers/widgets.js @@ -1,8 +1,8 @@ const React = require('react'); -const ReactDOM = require('react-dom'); +const ReactDOMClient = require('react-dom/client'); const { PATTERNS, FIELD_TYPE, HINT_TYPE, UNITS } = require('./widget-elements/constants'); require('./widget-elements/hints/hints.jsx'); -const { Text, Checkbox } = require('./widget-elements'); +const { Text, Checkbox, ImageSelector } = require('./widget-elements'); const CodeMirror = require('../code-mirror.js'); // See https://codemirror.net/5/addon/hint/css-hint.js for code reference @@ -19,6 +19,7 @@ const pseudoClasses = { 'active' : 1, 'after' : 1, 'before' const genKey = (...args)=>args.join('-'); module.exports = function(widgets, cm, setHints) { + const roots = {}; const spec = CodeMirror.resolveMode('text/css'); const headless = CodeMirror(()=>{}); @@ -80,15 +81,22 @@ module.exports = function(widgets, cm, setHints) { name : widget.name, pattern : PATTERNS.snippet[widget.type](widget.name), renderWidget : (n, node)=>{ + roots[n] = roots[n] ?? {}; const parent = document.createElement('div'); + const id = `${widget.name}-${n}`; + parent.id = id; + const textFieldNames = (widget.fields || []).filter((f)=>f.type === FIELD_TYPE.TEXT).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}/>; + } else if(field.type === FIELD_TYPE.IMAGE_SELECTOR) { + return ; } else { return null; } @@ -105,10 +113,12 @@ module.exports = function(widgets, cm, setHints) { return setHints(h, f)} getStyleHints={getStyleHints}/>; }).filter(Boolean); - ReactDOM.render( + const root = roots[n][id] ?? ReactDOMClient.createRoot(node || parent); + root.render( {fields} {styles} - , node || parent); + ); + roots[n][id] = root; return node || parent; } @@ -133,8 +143,11 @@ module.exports = function(widgets, cm, setHints) { }; return { - removeLineWidget : (widget)=>{ - cm.removeLineWidget(widget); + roots, + removeLineWidget : (n, widget)=>{ + roots[n][widget.node.id]?.unmount(); + delete roots[n][widget.node.id]; + widget?.clear(); }, updateLineWidgets, updateAllLineWidgets : ()=>{ diff --git a/themes/V3/5ePHB/widgets.js b/themes/V3/5ePHB/widgets.js index 9513d0000..fc0ab922d 100644 --- a/themes/V3/5ePHB/widgets.js +++ b/themes/V3/5ePHB/widgets.js @@ -1,3 +1,4 @@ +const _ = require('lodash'); const { SNIPPET_TYPE, FIELD_TYPE } = require('../../../shared/naturalcrit/codeEditor/helpers/widget-elements/constants'); module.exports = [{ @@ -36,4 +37,56 @@ module.exports = [{ increment : 5, hints : true }] -}]; \ No newline at end of file +}, { + name : 'watercolor', + type : SNIPPET_TYPE.INLINE, + fields : [{ + name : 'watercolor', + type : FIELD_TYPE.IMAGE_SELECTOR, + preview : (value)=>`/assets/watercolor/watercolor${value}.png`, + values : _.range(1, 13) + }, { + name : 'top', + type : FIELD_TYPE.TEXT, + increment : 5, + hints : true + }, { + name : 'left', + type : FIELD_TYPE.TEXT, + increment : 5, + hints : true + }, { + name : 'width', + type : FIELD_TYPE.TEXT, + increment : 5, + hints : true + }, { + name : 'opacity', + type : FIELD_TYPE.TEXT, + increment : 5 + }] +}, { + name : 'imageMaskCenter', + type : SNIPPET_TYPE.INLINE, + fields : [{ + name : 'imageMaskCenter', + type : FIELD_TYPE.IMAGE_SELECTOR, + preview : (value)=>`/assets/waterColorMasks/center/${typeof value === 'number' ? (()=>{ + const str = String(value); + return _.range(0, 4 - str.length).map(()=>'0').join('') + str; + })() : value}.webp`, + values : _.range(1, 17) + }, { + name : '--offsetX', + type : FIELD_TYPE.TEXT, + increment : 5, + }, { + name : '--offsetY', + type : FIELD_TYPE.TEXT, + increment : 5, + }, { + name : '--rotation', + type : FIELD_TYPE.TEXT, + increment : 5, + }] +}];