mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 13:53:09 +00:00
add image selector field type, modal component, and new snippet widgets
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -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 <img key={i} className={className} src={preview(v)} alt={`${name} image ${v}`} onClick={()=>this.select(v)}/>;
|
||||
});
|
||||
|
||||
this.state.modalRoot?.render(<Modal ref={this.modalRef} header={_.startCase(name)} save={this.save}>
|
||||
<div className={'images'}>
|
||||
{images}
|
||||
</div>
|
||||
</Modal>);
|
||||
},
|
||||
|
||||
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 <React.Fragment>
|
||||
<button onClick={()=>this.modalRef.current.setVisible(true)}>Select Image</button>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ImageSelector;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 <React.Fragment>
|
||||
{visible ? <div className={'bg-cover'}>
|
||||
<div className={'modal'}>
|
||||
<h1>{header}</h1>
|
||||
<hr/>
|
||||
{children}
|
||||
<div className={'action-row'}>
|
||||
<button id={'save'} onClick={()=>this.save()}>Save</button>
|
||||
<button id={'cancel'} onClick={()=>this.setVisible(false)}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> : null}
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Modal;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) : []);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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 <Checkbox key={genKey(widget.name, n, field.name)} cm={cm} CodeMirror={CodeMirror} n={n} prefix={widget.name} value={field.name}/>;
|
||||
return <Checkbox key={key} cm={cm} CodeMirror={CodeMirror} n={n} prefix={widget.name} value={field.name}/>;
|
||||
} else if(field.type === FIELD_TYPE.TEXT) {
|
||||
return <Text key={genKey(widget.name, n, field.name)} cm={cm} CodeMirror={CodeMirror} field={field} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints}/>;
|
||||
return <Text key={key} cm={cm} CodeMirror={CodeMirror} field={field} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints}/>;
|
||||
} else if(field.type === FIELD_TYPE.IMAGE_SELECTOR) {
|
||||
return <ImageSelector key={key} field={field} cm={cm} n={n}/>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -105,10 +113,12 @@ module.exports = function(widgets, cm, setHints) {
|
||||
return <Text key={genKey(widget.name, n, style)} cm={cm} CodeMirror={CodeMirror} field={field} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints}/>;
|
||||
}).filter(Boolean);
|
||||
|
||||
ReactDOM.render(<React.Fragment>
|
||||
const root = roots[n][id] ?? ReactDOMClient.createRoot(node || parent);
|
||||
root.render(<React.Fragment>
|
||||
{fields}
|
||||
{styles}
|
||||
</React.Fragment>, node || parent);
|
||||
</React.Fragment>);
|
||||
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 : ()=>{
|
||||
|
||||
Reference in New Issue
Block a user