mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-25 05:23:01 +00:00
Compare commits
212 Commits
v3.9.0
...
editor-wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b427ea21 | ||
|
|
379f260de5 | ||
|
|
46f413c656 | ||
|
|
77b0e93dd3 | ||
|
|
1b2d8d46a6 | ||
|
|
62aae96012 | ||
|
|
bef6b94dc4 | ||
|
|
c113b8dd1f | ||
|
|
e6055bd417 | ||
|
|
4c087e9aa5 | ||
|
|
9d6a9c4ebf | ||
|
|
18c94f95f3 | ||
|
|
23f2f1f53b | ||
|
|
d044229b49 | ||
|
|
8e40cec051 | ||
|
|
ebbf0ca88b | ||
|
|
51760e02e7 | ||
|
|
f52d42bef5 | ||
|
|
3af5d27e3e | ||
|
|
9d64740678 | ||
|
|
053aadeff4 | ||
|
|
d3763beb15 | ||
|
|
8281051797 | ||
|
|
5b0104fc10 | ||
|
|
b3fa902d85 | ||
|
|
a22d223927 | ||
|
|
5c08926576 | ||
|
|
797ca7e64e | ||
|
|
5b8f2d8e3c | ||
|
|
bd0ef5da48 | ||
|
|
fe324d6822 | ||
|
|
3140299d73 | ||
|
|
96d04ad75a | ||
|
|
62532f788e | ||
|
|
69a3d04bb7 | ||
|
|
21017e45fe | ||
|
|
48474c6f7b | ||
|
|
3d318f8863 | ||
|
|
2b798f4ecb | ||
|
|
938802e1a3 | ||
|
|
14f825f3b5 | ||
|
|
37241a70eb | ||
|
|
bcd86a7f0c | ||
|
|
cf54594a4c | ||
|
|
535fdeaf62 | ||
|
|
77bf1b5258 | ||
|
|
63da418b60 | ||
|
|
67f5e53160 | ||
|
|
c6103d51c5 | ||
|
|
a4a10783f6 | ||
|
|
5ed53f75c5 | ||
|
|
81b289923a | ||
|
|
c3d8364789 | ||
|
|
82fec9901d | ||
|
|
173d0a726b | ||
|
|
e064219ca0 | ||
|
|
ec339f2717 | ||
|
|
d9d27808a8 | ||
|
|
a4584dc78e | ||
|
|
6344eaa17d | ||
|
|
5c41110e50 | ||
|
|
085cb99562 | ||
|
|
568586541a | ||
|
|
0d44e1778f | ||
|
|
4a5269e1f3 | ||
|
|
62cf0a4483 | ||
|
|
07c7352aa2 | ||
|
|
cf6c8bce88 | ||
|
|
45d32ebfc3 | ||
|
|
314275122d | ||
|
|
5716e4fcfd | ||
|
|
b9aaee43c2 | ||
|
|
35a74b3e46 | ||
|
|
f4fe08f8fd | ||
|
|
65c0c81984 | ||
|
|
84496f51ba | ||
|
|
4cf659e711 | ||
|
|
712f0309e9 | ||
|
|
e0bfef5231 | ||
|
|
afb6962407 | ||
|
|
b7be2d6463 | ||
|
|
47c84d9f01 | ||
|
|
8d2945ee5c | ||
|
|
1dad009298 | ||
|
|
aadf663623 | ||
|
|
8685f32b49 | ||
|
|
678ac90cd0 | ||
|
|
3cb5e8ed42 | ||
|
|
273f0ca05d | ||
|
|
3c929870cb | ||
|
|
36df5a3212 | ||
|
|
cea5f2e43a | ||
|
|
046845885d | ||
|
|
9713cc4be9 | ||
|
|
8baf0fc849 | ||
|
|
a7040e554a | ||
|
|
ba43055f32 | ||
|
|
d0de7ca28c | ||
|
|
c0164dce6a | ||
|
|
9e2b8477a8 | ||
|
|
5a32ae5cd4 | ||
|
|
e88e7f852c | ||
|
|
a3b2c6987f | ||
|
|
3d47b5a0bc | ||
|
|
c5f4793c23 | ||
|
|
10e14bfcfd | ||
|
|
f3c36ffb0a | ||
|
|
cff4f8eae5 | ||
|
|
4799e8b443 | ||
|
|
fa38d5c892 | ||
|
|
04eb7d0556 | ||
|
|
f175323221 | ||
|
|
9f4de3c66e | ||
|
|
800bff611a | ||
|
|
e28b6e7a19 | ||
|
|
4c6de90d82 | ||
|
|
e5ef0aedd3 | ||
|
|
da8e7ec610 | ||
|
|
d1412abe03 | ||
|
|
9de4a82977 | ||
|
|
9ddae7bbea | ||
|
|
4fdc6b79ea | ||
|
|
0001cf16d9 | ||
|
|
438cb7f26d | ||
|
|
ffa240f78d | ||
|
|
782aa8e658 | ||
|
|
7efe8964f1 | ||
|
|
853515e09e | ||
|
|
f6c5354ce0 | ||
|
|
6353341738 | ||
|
|
66b9a792e7 | ||
|
|
2775614eab | ||
|
|
32229c6e6e | ||
|
|
37c88b83f1 | ||
|
|
2fa1b2bb8b | ||
|
|
949d763e35 | ||
|
|
46cb2e6b5b | ||
|
|
e7224e97ef | ||
|
|
e07d53aa5f | ||
|
|
1e004977be | ||
|
|
9110e7cf7e | ||
|
|
27e8b54528 | ||
|
|
aa31919563 | ||
|
|
7bf3295fc2 | ||
|
|
9fd3f47689 | ||
|
|
0ca7e43d73 | ||
|
|
b33b3cd49b | ||
|
|
71c384ee0b | ||
|
|
546b8d5725 | ||
|
|
4d6ac2b142 | ||
|
|
ce538ebbfd | ||
|
|
cf17e73dfa | ||
|
|
69ef4d7653 | ||
|
|
c98224f3e4 | ||
|
|
4f870de68f | ||
|
|
2cfee2e8ad | ||
|
|
9e1d53a30c | ||
|
|
1fe9f0c8d0 | ||
|
|
adc7233cab | ||
|
|
1b2fc746d3 | ||
|
|
b472fc1115 | ||
|
|
a7a47afaae | ||
|
|
509c7d8832 | ||
|
|
caff1d8e2b | ||
|
|
e06f5e17d9 | ||
|
|
ade61971d0 | ||
|
|
6451d79d92 | ||
|
|
9202f9c8eb | ||
|
|
097cc220f8 | ||
|
|
2e3c10c35b | ||
|
|
a5aeb7dccd | ||
|
|
b6d37dd825 | ||
|
|
92ff776270 | ||
|
|
bf1fb97789 | ||
|
|
2cddc2debe | ||
|
|
0d4a1a11c1 | ||
|
|
aa3cf1d9c1 | ||
|
|
5d0062f610 | ||
|
|
7976917bb9 | ||
|
|
023071c874 | ||
|
|
7da42d3742 | ||
|
|
3269e94757 | ||
|
|
c69f4289ed | ||
|
|
8752a32626 | ||
|
|
8735d1f222 | ||
|
|
21929e676d | ||
|
|
5ca61935a8 | ||
|
|
10143cec93 | ||
|
|
643c8503c0 | ||
|
|
e92d3ecd68 | ||
|
|
4f092828ac | ||
|
|
2d4c211483 | ||
|
|
7d30abc4d9 | ||
|
|
1d513f7a0e | ||
|
|
44922f5261 | ||
|
|
f695cc6948 | ||
|
|
3722387f1f | ||
|
|
8950cb944f | ||
|
|
66fb70a5f8 | ||
|
|
69c242425b | ||
|
|
9093f610bd | ||
|
|
d2b2e69123 | ||
|
|
052c255068 | ||
|
|
e6ad8aefde | ||
|
|
43ae80e80d | ||
|
|
e6e04ad21d | ||
|
|
7be6b913b0 | ||
|
|
c30eb9056a | ||
|
|
ee3cd21486 | ||
|
|
5de63799c2 | ||
|
|
dfa8aea1ec | ||
|
|
8e26161244 |
@@ -15,7 +15,7 @@ module.exports = {
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-array-constructor' : 'error',
|
||||
'no-iterator' : 'error',
|
||||
'no-nested-ternary' : 'error',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.13-alpine
|
||||
FROM node:18-alpine
|
||||
RUN apk --no-cache add git
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
14
changelog.md
14
changelog.md
@@ -80,6 +80,16 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 28/06/2023 - v3.9.1
|
||||
{{taskList
|
||||
|
||||
##### G-Ambatte
|
||||
|
||||
* [x] Better error pages with more useful information
|
||||
|
||||
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||
}}
|
||||
|
||||
### Friday 02/06/2023 - v3.9.0
|
||||
{{taskList
|
||||
|
||||
@@ -101,7 +111,7 @@ Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#
|
||||
|
||||
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
||||
|
||||
##### 5e-Clerc
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
||||
|
||||
@@ -145,7 +155,7 @@ Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
|
||||
|
||||
* [x] Add local Windows install script via Chocolatey
|
||||
|
||||
##### 5e-Clerc
|
||||
##### 5e-Cleric
|
||||
|
||||
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
||||
|
||||
|
||||
@@ -108,6 +108,12 @@ const BrewRenderer = createClass({
|
||||
return false;
|
||||
},
|
||||
|
||||
sanitizeScriptTags : function(content) {
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
},
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo' ref='main'>
|
||||
<div>
|
||||
@@ -135,18 +141,20 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.props.style) return;
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
|
||||
const cleanStyle = this.sanitizeScriptTags(this.props.style);
|
||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
const cleanPageText = this.sanitizeScriptTags(pageText);
|
||||
if(this.props.renderer == 'legacy')
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
||||
else {
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -185,6 +193,12 @@ const BrewRenderer = createClass({
|
||||
}, 100);
|
||||
},
|
||||
|
||||
emitClick : function(){
|
||||
// console.log('iFrame clicked');
|
||||
if(!window || !document) return;
|
||||
document.dispatchEvent(new MouseEvent('click'));
|
||||
},
|
||||
|
||||
render : function(){
|
||||
//render in iFrame so broken code doesn't crash the site.
|
||||
//Also render dummy page while iframe is mounting.
|
||||
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
|
||||
|
||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||
contentDidMount={this.frameDidMount}>
|
||||
contentDidMount={this.frameDidMount}
|
||||
onClick={()=>{this.emitClick();}}
|
||||
>
|
||||
<div className={'brewRenderer'}
|
||||
onScroll={this.handleScroll}
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
@@ -269,7 +269,7 @@ const Editor = createClass({
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}/>
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
@@ -323,7 +323,8 @@ const Editor = createClass({
|
||||
theme={this.props.brew.theme}
|
||||
undo={this.undo}
|
||||
redo={this.redo}
|
||||
historySize={this.historySize()} />
|
||||
historySize={this.historySize()}
|
||||
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,8 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
const execute = function(val, props){
|
||||
if(_.isFunction(val)) return val(props);
|
||||
return val;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ const Snippetbar = createClass({
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{}
|
||||
historySize : ()=>{},
|
||||
cursorPos : {}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -105,6 +106,7 @@ const Snippetbar = createClass({
|
||||
snippets={snippetGroup.snippets}
|
||||
key={snippetGroup.groupName}
|
||||
onSnippetClick={this.handleSnippetClick}
|
||||
cursorPos={this.props.cursorPos}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
@@ -165,7 +167,7 @@ const SnippetGroup = createClass({
|
||||
},
|
||||
handleSnippetClick : function(e, snippet){
|
||||
e.stopPropagation();
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||
},
|
||||
renderSnippets : function(snippets){
|
||||
return _.map(snippets, (snippet)=>{
|
||||
|
||||
@@ -9,7 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
@@ -78,6 +78,7 @@ const Homebrew = createClass({
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
|
||||
@@ -81,20 +81,70 @@
|
||||
color : pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
.recent.navDropdownContainer {
|
||||
position : relative;
|
||||
.dropdown {
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
top : 28px;
|
||||
left : 0;
|
||||
.navDropdown .navItem {
|
||||
overflow : hidden auto;
|
||||
width : 100%;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4 {
|
||||
font-size : 0.8em;
|
||||
|
||||
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
&.header {
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 5px 0;
|
||||
@@ -109,62 +159,6 @@
|
||||
background-color : darken(@purple, 30%);
|
||||
}
|
||||
}
|
||||
.item {
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
overflow : clip;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
text-decoration : none;
|
||||
color : white;
|
||||
border-top : 1px solid #888;
|
||||
background-color : #333;
|
||||
.clear {
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
right : 0;
|
||||
display : none;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
transform : translateY(-50%);
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
background-color : #333;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
font-size : 10px;
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
.clear {
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
display : inline-block;
|
||||
overflow : hidden;
|
||||
width : 100%;
|
||||
white-space : nowrap;
|
||||
text-overflow : ellipsis;
|
||||
}
|
||||
.time {
|
||||
font-size : 0.7em;
|
||||
position : absolute;
|
||||
right : 2px;
|
||||
bottom : 2px;
|
||||
color : #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.metadata.navItem {
|
||||
|
||||
@@ -121,6 +121,7 @@ const RecentItems = createClass({
|
||||
|
||||
removeItem : function(url, evt){
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
@@ -139,11 +140,11 @@ const RecentItems = createClass({
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
// if(!this.state.showDropdown) return null;
|
||||
|
||||
const makeItems = (brews)=>{
|
||||
return _.map(brews, (brew, i)=>{
|
||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||
@@ -151,25 +152,25 @@ const RecentItems = createClass({
|
||||
});
|
||||
};
|
||||
|
||||
return <div className='dropdown'>
|
||||
return <>
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>edited</h4> : null }
|
||||
<Nav.item className='header'>edited</Nav.item> : null }
|
||||
{this.props.showEdit ?
|
||||
makeItems(this.state.edit) : null }
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>viewed</h4> : null }
|
||||
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||
{this.props.showView ?
|
||||
makeItems(this.state.view) : null }
|
||||
</div>;
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
{this.props.text}
|
||||
return <Nav.dropdown className='recent'>
|
||||
<Nav.item icon='fas fa-history' color='grey' >
|
||||
{this.props.text}
|
||||
</Nav.item>
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>;
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
.uiPage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
width : 90vw;
|
||||
background-color: #f0f0f0;
|
||||
font-family: 'Open Sans';
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2% 4%;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8em;
|
||||
.dataGroup{
|
||||
padding: 6px 20px 15px;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
h1, h2, h3, h4{
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin: 0.5em 30% 0.25em 0;
|
||||
border-bottom: 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid darkslategrey;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
svg {
|
||||
width: 19px;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.content {
|
||||
width : ~"min(90vw, 1000px)";
|
||||
padding : 2% 4%;
|
||||
margin-top : 25px;
|
||||
margin-right : auto;
|
||||
margin-left : auto;
|
||||
overflow-y : scroll;
|
||||
font-family : 'Open Sans';
|
||||
font-size : 0.8em;
|
||||
line-height : 1.8em;
|
||||
background-color : #F0F0F0;
|
||||
.dataGroup {
|
||||
padding : 6px 20px 15px;
|
||||
margin : 5px 0px;
|
||||
border : 2px solid black;
|
||||
border-radius : 5px;
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
width : 100%;
|
||||
margin : 0.5em 30% 0.25em 0;
|
||||
font-weight : 900;
|
||||
text-transform : uppercase;
|
||||
border-bottom : 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
margin-right : 0;
|
||||
margin-bottom : 0.5em;
|
||||
font-size : 2em;
|
||||
border-bottom : 2px solid darkslategrey;
|
||||
}
|
||||
h2 { font-size : 1.75em; }
|
||||
h3 {
|
||||
font-size : 1.5em;
|
||||
svg { width : 19px; }
|
||||
}
|
||||
h4 { font-size : 1.25em; }
|
||||
strong { font-weight : bold; }
|
||||
em { font-style : italic; }
|
||||
ul {
|
||||
padding-left : 1.25em;
|
||||
list-style : square;
|
||||
}
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * { margin-top : 0; }
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
const ErrorIndex = require('./errors/errorIndex.js');
|
||||
|
||||
const ErrorPage = createClass({
|
||||
displayName : 'ErrorPage',
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
ver : '0.0.0',
|
||||
errorId : ''
|
||||
errorId : '',
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
error : {}
|
||||
};
|
||||
},
|
||||
|
||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||
|
||||
render : function(){
|
||||
return <div className='errorPage sitePage'>
|
||||
<Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
<Nav.item className='errorTitle'>
|
||||
Crit Fail!
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.text} />
|
||||
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||
<div className='dataGroup'>
|
||||
<div className='errorTitle'>
|
||||
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||
</div>
|
||||
<hr />
|
||||
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||
</div>
|
||||
</div>;
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
.errorPage{
|
||||
.errorTitle{
|
||||
background-color: @orange;
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.errorTitle {
|
||||
//background-color: @orange;
|
||||
color : #D02727;
|
||||
text-align : center;
|
||||
}
|
||||
.content {
|
||||
h1, h2, h3, h4 { border-bottom : none; }
|
||||
hr { border-bottom : 2px solid slategrey; }
|
||||
}
|
||||
}
|
||||
}
|
||||
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||
|
||||
const errorIndex = (props)=>{
|
||||
return {
|
||||
// Default catch all
|
||||
'00' : dedent`
|
||||
## An unknown error occurred!
|
||||
|
||||
We aren't sure what happened, but our server wasn't able to find what you
|
||||
were looking for.`,
|
||||
|
||||
// General Google load error
|
||||
'01' : dedent`
|
||||
## An error occurred while retrieving this brew from Google Drive!
|
||||
|
||||
Google reported an error while attempting to retrieve a brew from this link.`,
|
||||
|
||||
// Google Drive - 404 : brew deleted or access denied
|
||||
'02' : dedent`
|
||||
## We can't find this brew in Google Drive!
|
||||
|
||||
This file was saved on Google Drive, but this link doesn't work anymore.
|
||||
${ props.brew.authors?.length > 0
|
||||
? `Note that this brew belongs to the Homebrewery account **${ props.brew.authors[0] }**,
|
||||
${ props.brew.account
|
||||
? `which is
|
||||
${props.brew.authors[0] == props.brew.account
|
||||
? `your account.`
|
||||
: `not your account (you are currently signed in as **${props.brew.account}**).`
|
||||
}`
|
||||
: 'and you are not currently signed in to any account.'
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
The Homebrewery cannot delete files from Google Drive on its own, so there
|
||||
are three most likely possibilities:
|
||||
:
|
||||
- **The Google Drive files may have been accidentally deleted.** Look in
|
||||
the Google Drive account that owns this brew (or ask the owner to do so),
|
||||
and make sure the Homebrewery folder is still there, and that it holds your brews
|
||||
as text files.
|
||||
- **You may have changed the sharing settings for your files.** If the files
|
||||
are still on Google Drive, change all of them to be shared *with everyone who has
|
||||
the link* so the Homebrewery can access them.
|
||||
- **The Google Account may be closed.** Google may have removed the account
|
||||
due to inactivity or violating a Google policy. Make sure the owner can
|
||||
still access Google Drive normally and upload/download files to it.
|
||||
:
|
||||
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||
shows the recent activity on Google Drive. This can help you pin down the exact date
|
||||
the brew was deleted or moved, and by whom.
|
||||
:
|
||||
If the brew still isn't found, some people have had success asking Google to recover
|
||||
accidentally deleted files at this link:
|
||||
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
|
||||
At the bottom of the page there is a button that says *Send yourself an Email*
|
||||
and you will receive instructions on how to request the files be restored.
|
||||
:
|
||||
Also note, if you prefer not to use your Google Drive for storage, you can always
|
||||
change the storage location of a brew by clicking the Google drive icon by the
|
||||
brew title and choosing *transfer my brew to/from Google Drive*.`,
|
||||
|
||||
// User is not Authors list
|
||||
'03' : dedent`
|
||||
## Current signed-in user does not have editor access to this brew.
|
||||
|
||||
If you believe you should have access to this brew, ask one of its authors to invite you
|
||||
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
|
||||
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||
then try to access this document again.
|
||||
|
||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// User is not signed in; must be a user on the Authors List
|
||||
'04' : dedent`
|
||||
## Sign-in required to edit this brew.
|
||||
|
||||
You must be logged in to one of the accounts listed as an author of this brew.
|
||||
User is not logged in. Please log in [here](${loginUrl}).
|
||||
|
||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// Brew load error
|
||||
'05' : dedent`
|
||||
## No Homebrewery document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. It was likely deleted by
|
||||
its owner.
|
||||
|
||||
**Requested access:** ${props.brew.accessType}
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Brew save error
|
||||
'06' : dedent`
|
||||
## Unable to save Homebrewery document.
|
||||
|
||||
An error occurred wil attempting to save the Homebrewery document.`,
|
||||
|
||||
// Brew delete error
|
||||
'07' : dedent`
|
||||
## Unable to delete Homebrewery document.
|
||||
|
||||
An error occurred while attempting to remove the Homebrewery document.
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Author delete error
|
||||
'08' : dedent`
|
||||
## Unable to remove user from Homebrewery document.
|
||||
|
||||
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = errorIndex;
|
||||
35105
package-lock.json
generated
35105
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.9.0",
|
||||
"version": "3.9.1",
|
||||
"engines": {
|
||||
"node": ">=18.16.x"
|
||||
},
|
||||
@@ -78,10 +78,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.1",
|
||||
"@babel/plugin-transform-runtime": "^7.22.4",
|
||||
"@babel/preset-env": "^7.22.4",
|
||||
"@babel/preset-react": "^7.22.3",
|
||||
"@babel/core": "^7.22.8",
|
||||
"@babel/plugin-transform-runtime": "^7.22.7",
|
||||
"@babel/preset-env": "^7.22.7",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@googleapis/drive": "^5.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.3.2",
|
||||
@@ -97,34 +97,35 @@
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "5.0.4",
|
||||
"marked": "5.1.1",
|
||||
"marked-extended-tables": "^1.0.6",
|
||||
"marked-gfm-heading-id": "^3.0.3",
|
||||
"marked-gfm-heading-id": "^3.0.4",
|
||||
"marked-smartypants-lite": "^1.0.0",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^7.2.2",
|
||||
"mongoose": "^7.3.2",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"npm": "^9.6.7",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"npm": "^9.8.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.11.2",
|
||||
"react-router-dom": "6.14.1",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"jest": "^29.5.0",
|
||||
"jest": "^29.6.1",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^15.6.2",
|
||||
"stylelint-config-recess-order": "^4.0.0",
|
||||
"stylelint-config-recommended": "^12.0.0",
|
||||
"stylelint-stylistic": "^0.4.2",
|
||||
"stylelint": "^15.10.1",
|
||||
"stylelint-config-recess-order": "^4.3.0",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"stylelint-stylistic": "^0.4.3",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,12 +135,12 @@ fs.emptyDirSync('./build');
|
||||
|
||||
})().catch(console.error);
|
||||
|
||||
//In development set up a watch server and livereload
|
||||
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here
|
||||
ignore : ['./build'], // Ignore ./build or it will rebuild again
|
||||
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default)
|
||||
//watch: ['./client', './server', './themes'], // Watch additional folders if you want
|
||||
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
@@ -324,8 +324,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
||||
};
|
||||
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
const googleId = brew.googleId;
|
||||
const shareId = brew.shareId;
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
@@ -397,7 +397,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
@@ -414,8 +413,7 @@ if(isLocalEnvironment){
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
|
||||
const renderPage = async (req, res)=>{
|
||||
// Create configuration object
|
||||
const configuration = {
|
||||
local : isLocalEnvironment,
|
||||
@@ -424,7 +422,7 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
};
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
url : req.customUrl || req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
@@ -438,15 +436,20 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
return page;
|
||||
};
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
}));
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
//Format Errors as plain objects so all fields will appear in the string sent
|
||||
const formatErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
@@ -458,13 +461,30 @@ const replaceErrors = (key, value)=>{
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
app.use(async (err, req, res, next)=>{
|
||||
const status = err.status || err.code || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Error Page',
|
||||
description : 'Something went wrong!'
|
||||
};
|
||||
req.brew = {
|
||||
...err,
|
||||
title : 'Error - Something went wrong!',
|
||||
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
||||
status : status,
|
||||
HBErrorCode : err.HBErrorCode ?? '00',
|
||||
pureError : getPureError(err)
|
||||
};
|
||||
req.customUrl= '/error';
|
||||
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
});
|
||||
|
||||
app.use((req, res)=>{
|
||||
|
||||
@@ -100,12 +100,12 @@ const GoogleActions = {
|
||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||
|
||||
const fileList = [];
|
||||
let NextPageToken = "";
|
||||
let NextPageToken = '';
|
||||
|
||||
do {
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 1000,
|
||||
pageToken : NextPageToken || "",
|
||||
pageToken : NextPageToken || '',
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
@@ -243,9 +243,9 @@ const GoogleActions = {
|
||||
|
||||
if(obj) {
|
||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||
throw ('Edit ID does not match');
|
||||
throw ({ message: 'Edit ID does not match' });
|
||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||
throw ('Share ID does not match');
|
||||
throw ({ message: 'Share ID does not match' });
|
||||
}
|
||||
|
||||
const file = await drive.files.get({
|
||||
|
||||
@@ -27,8 +27,13 @@ const api = {
|
||||
|
||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||
if(id.length > 12) {
|
||||
googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||
googleId = id.slice(0, -12); // current editId is 12 chars
|
||||
} else { // old editIds used to be 10 chars;
|
||||
googleId = id.slice(0, -10); // if total string is too short, must be old brew
|
||||
console.log('Old brew, using 10-char Id');
|
||||
}
|
||||
id = id.slice(googleId.length);
|
||||
}
|
||||
return { id, googleId };
|
||||
},
|
||||
@@ -57,7 +62,14 @@ const api = {
|
||||
googleError = err;
|
||||
});
|
||||
// Throw any error caught while attempting to retrieve Google brew.
|
||||
if(googleError) throw googleError;
|
||||
if(googleError) {
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound') {
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
} else {
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
}
|
||||
}
|
||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
@@ -65,14 +77,16 @@ const api = {
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
throw `The current logged in user does not have editor access to this brew.
|
||||
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||
const accessError = { name: 'Access Error', status: 401 };
|
||||
if(req.account){
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
||||
}
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
if(!stub && !stubOnly) {
|
||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||
}
|
||||
|
||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||
@@ -181,7 +195,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err, err.toString(), err.stack);
|
||||
throw `Error while creating new brew, ${err.toString()}`;
|
||||
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
|
||||
});
|
||||
if(!saved) return;
|
||||
saved = saved.toObject();
|
||||
@@ -283,10 +297,13 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
try {
|
||||
await api.getBrew('edit')(req, res, ()=>{});
|
||||
} catch (err) {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
|
||||
if(err.HBErrorCode == '02') {
|
||||
const { id, googleId } = api.getId(req);
|
||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||
await HomebrewModel.deleteOne({ editId: id });
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
let brew = req.brew;
|
||||
@@ -308,7 +325,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
|
||||
});
|
||||
} else {
|
||||
if(shouldDeleteGoogleBrew) {
|
||||
@@ -320,7 +337,7 @@ If you believe you should have access to this brew, ask the file owner to invite
|
||||
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||
await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
|
||||
expect(googleId).toEqual('12345');
|
||||
});
|
||||
|
||||
it('should return id and google id from params', ()=>{
|
||||
it('should return 12-char id and google id from params', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : '123456789012abcdefghijkl'
|
||||
id : '123456789012345678901234567890123abcdefghijkl'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
expect(googleId).toEqual('123456789012');
|
||||
});
|
||||
|
||||
it('should return 10-char id and google id from params', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : '123456789012345678901234567890123abcdefghij'
|
||||
}
|
||||
});
|
||||
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
expect(id).toEqual('abcdefghij');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrew', ()=>{
|
||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
||||
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||
|
||||
it('returns middleware', ()=>{
|
||||
const getFn = api.getBrew('share');
|
||||
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws if invalid author', async ()=>{
|
||||
it('throws if not logged in as author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
||||
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||
});
|
||||
|
||||
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
||||
it('throws if logged in as invalid author', async ()=>{
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||
|
||||
const fn = api.getBrew('edit', true);
|
||||
const req = { brew: {}, account: { username: 'b' } };
|
||||
|
||||
let err;
|
||||
try {
|
||||
await fn(req, null, null);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||
});
|
||||
|
||||
it('does not throw if no authors', async ()=>{
|
||||
@@ -545,7 +571,7 @@ brew`);
|
||||
|
||||
describe('deleteBrew', ()=>{
|
||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||
model.deleteOne = jest.fn(async ()=>{});
|
||||
const next = jest.fn(()=>{});
|
||||
|
||||
36
shared/naturalcrit/codeEditor/code-mirror.js
Normal file
36
shared/naturalcrit/codeEditor/code-mirror.js
Normal file
@@ -0,0 +1,36 @@
|
||||
let CodeMirror;
|
||||
if(typeof navigator !== 'undefined'){
|
||||
CodeMirror = require('codemirror');
|
||||
|
||||
//Language Modes
|
||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||
require('codemirror/mode/css/css.js');
|
||||
require('codemirror/mode/javascript/javascript.js');
|
||||
|
||||
//Addons
|
||||
//Code folding
|
||||
require('codemirror/addon/fold/foldcode.js');
|
||||
require('codemirror/addon/fold/foldgutter.js');
|
||||
//Search and replace
|
||||
require('codemirror/addon/search/search.js');
|
||||
require('codemirror/addon/search/searchcursor.js');
|
||||
require('codemirror/addon/search/jump-to-line.js');
|
||||
require('codemirror/addon/search/match-highlighter.js');
|
||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||
require('codemirror/addon/dialog/dialog.js');
|
||||
//Trailing space highlighting
|
||||
// require('codemirror/addon/edit/trailingspace.js');
|
||||
//Active line highlighting
|
||||
// require('codemirror/addon/selection/active-line.js');
|
||||
//Scroll past last line
|
||||
require('codemirror/addon/scroll/scrollpastend.js');
|
||||
//Auto-closing
|
||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||
require('codemirror/addon/fold/xml-fold.js');
|
||||
require('codemirror/addon/edit/closetag.js');
|
||||
|
||||
const foldCode = require('./helpers/fold-code');
|
||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||
}
|
||||
|
||||
module.exports = CodeMirror;
|
||||
@@ -4,58 +4,32 @@ const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const closeTag = require('./close-tag');
|
||||
const closeTag = require('./helpers/close-tag');
|
||||
const Hints = require('./helpers/widget-elements/hints/hints.jsx');
|
||||
const CodeMirror = require('./code-mirror.js');
|
||||
|
||||
let CodeMirror;
|
||||
if(typeof navigator !== 'undefined'){
|
||||
CodeMirror = require('codemirror');
|
||||
|
||||
//Language Modes
|
||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||
require('codemirror/mode/css/css.js');
|
||||
require('codemirror/mode/javascript/javascript.js');
|
||||
|
||||
//Addons
|
||||
//Code folding
|
||||
require('codemirror/addon/fold/foldcode.js');
|
||||
require('codemirror/addon/fold/foldgutter.js');
|
||||
//Search and replace
|
||||
require('codemirror/addon/search/search.js');
|
||||
require('codemirror/addon/search/searchcursor.js');
|
||||
require('codemirror/addon/search/jump-to-line.js');
|
||||
require('codemirror/addon/search/match-highlighter.js');
|
||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||
require('codemirror/addon/dialog/dialog.js');
|
||||
//Trailing space highlighting
|
||||
// require('codemirror/addon/edit/trailingspace.js');
|
||||
//Active line highlighting
|
||||
// require('codemirror/addon/selection/active-line.js');
|
||||
//Scroll past last line
|
||||
require('codemirror/addon/scroll/scrollpastend.js');
|
||||
//Auto-closing
|
||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||
require('codemirror/addon/fold/xml-fold.js');
|
||||
require('codemirror/addon/edit/closetag.js');
|
||||
|
||||
const foldCode = require('./fold-code');
|
||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||
}
|
||||
const themeWidgets = require('../../../themes/V3/5ePHB/widgets');
|
||||
|
||||
const CodeEditor = createClass({
|
||||
displayName : 'CodeEditor',
|
||||
hintsRef : React.createRef(),
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true
|
||||
enableFolding : true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
docs : {}
|
||||
docs : {},
|
||||
widgetUtils : {},
|
||||
widgets : {},
|
||||
hints : [],
|
||||
hintsField : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -91,6 +65,9 @@ const CodeEditor = createClass({
|
||||
} else {
|
||||
this.codeMirror.setOption('foldOptions', false);
|
||||
}
|
||||
|
||||
this.state.widgetUtils.updateWidgetGutter();
|
||||
this.state.widgetUtils.updateAllLineWidgets();
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
@@ -155,7 +132,7 @@ const CodeEditor = createClass({
|
||||
},
|
||||
foldGutter : true,
|
||||
foldOptions : this.foldOptions(this.codeMirror),
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'widget-gutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
@@ -169,14 +146,74 @@ const CodeEditor = createClass({
|
||||
});
|
||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||
|
||||
this.setState({
|
||||
widgetUtils : require('./helpers/widgets')(themeWidgets, this.codeMirror, (hints, field)=>{
|
||||
this.setState({
|
||||
hints,
|
||||
hintsField : field
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||
this.codeMirror.on('change', (cm)=>{
|
||||
this.props.onChange(cm.getValue());
|
||||
this.state.widgetUtils.updateWidgetGutter();
|
||||
});
|
||||
|
||||
this.codeMirror.on('cursorActivity', (cm)=>{
|
||||
const { line } = cm.getCursor();
|
||||
for (const key in this.state.widgets) {
|
||||
if(key != line) {
|
||||
this.state.widgetUtils.removeLineWidget(key, this.state.widgets[key]);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
hints : [],
|
||||
hintsField : undefined
|
||||
});
|
||||
const { widgets } = this.codeMirror.lineInfo(line);
|
||||
if(!widgets) {
|
||||
const widget = this.state.widgetUtils.updateLineWidgets(line);
|
||||
if(widget) {
|
||||
this.setState({
|
||||
widgets : {
|
||||
[line] : widget
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updateSize();
|
||||
|
||||
this.codeMirror.on('gutterClick', (cm, n)=>{
|
||||
// Open line widgets when 'widget-gutter' marker clicked
|
||||
if(this.codeMirror.lineInfo(n).gutterMarkers?.['widget-gutter']) {
|
||||
const { widgets } = this.codeMirror.lineInfo(n);
|
||||
if(!widgets) {
|
||||
const widget = this.state.widgetUtils.updateLineWidgets(n);
|
||||
if(widget) {
|
||||
this.setState({
|
||||
widgets : { ...this.state.widgets, [n]: widget }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const widget of widgets) {
|
||||
this.state.widgetUtils.removeLineWidget(n, widget);
|
||||
}
|
||||
this.setState({
|
||||
hints : [],
|
||||
hintsField : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
indent : function () {
|
||||
const cm = this.codeMirror;
|
||||
if (cm.somethingSelected()) {
|
||||
if(cm.somethingSelected()) {
|
||||
cm.execCommand('indentMore');
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab');
|
||||
@@ -403,10 +440,19 @@ const CodeEditor = createClass({
|
||||
}
|
||||
};
|
||||
},
|
||||
keyDown : function(e) {
|
||||
if(this.hintsRef.current) {
|
||||
this.hintsRef.current.keyDown(e);
|
||||
}
|
||||
},
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
||||
const { hints, hintsField } = this.state;
|
||||
return <React.Fragment>
|
||||
<div className='codeEditor' ref='editor' style={this.props.style} onKeyDown={this.keyDown}/>
|
||||
<Hints ref={this.hintsRef} hints={hints} field={hintsField}/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||
@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;}
|
||||
@@ -14,7 +16,7 @@
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
@@ -30,4 +32,51 @@
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
|
||||
.widget-gutter {
|
||||
width: .7em;
|
||||
}
|
||||
|
||||
.snippet-options-widget {
|
||||
padding: 2px 0;
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0 2px 2px 2px;
|
||||
}
|
||||
|
||||
input {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
require('./checkbox.less');
|
||||
const CodeMirror = require('../../../code-mirror.js');
|
||||
|
||||
const Checkbox = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
value : '',
|
||||
prefix : '',
|
||||
cm : {},
|
||||
n : -1,
|
||||
default : false
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function (e) {
|
||||
const { cm, n, value, prefix } = this.props;
|
||||
const { text } = cm.lineInfo(n);
|
||||
const updatedPrefix = `{{${prefix}`;
|
||||
if(e.target?.checked)
|
||||
cm.replaceRange(`,${value}`, CodeMirror.Pos(n, updatedPrefix.length), CodeMirror.Pos(n, updatedPrefix.length), '+insert');
|
||||
else {
|
||||
const start = text.indexOf(`,${value}`);
|
||||
if(start > -1)
|
||||
cm.replaceRange('', CodeMirror.Pos(n, start), CodeMirror.Pos(n, start + value.length + 1), '-delete');
|
||||
}
|
||||
},
|
||||
|
||||
render : function() {
|
||||
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 <React.Fragment>
|
||||
<div className={className}>
|
||||
<input type='checkbox' id={id} onChange={this.handleChange} checked={_.includes(text, `,${value}`)}/>
|
||||
<label htmlFor={id}>{_.startCase(value)}</label>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Checkbox;
|
||||
@@ -0,0 +1,7 @@
|
||||
.widget-checkbox {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
background-color: #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
require('./color-selector.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { PATTERNS, STYLE_FN, SNIPPET_TYPE } = 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, snippetType } = 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) {
|
||||
if(snippetType === SNIPPET_TYPE.INLINE) {
|
||||
index = text.indexOf('}');
|
||||
}
|
||||
index = index === -1 ? text.length : index;
|
||||
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 <React.Fragment>
|
||||
<div className={className}>
|
||||
<label htmlFor={id}>{field.name}:</label>
|
||||
<input className='color' type='color' value={value} onChange={this.onChangeDebounce}/>
|
||||
<input id={id} className='text' type='text' style={style} value={value} onChange={this.onChange}/>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ColorSelector;
|
||||
@@ -0,0 +1,8 @@
|
||||
.color-selector {
|
||||
.color {
|
||||
height: 17px;
|
||||
width: 13px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
export const UNITS = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'];
|
||||
|
||||
export const HINT_TYPE = {
|
||||
VALUE : 0,
|
||||
NUMBER_SUFFIX : 1
|
||||
};
|
||||
|
||||
export const SNIPPET_TYPE = {
|
||||
BLOCK : 0,
|
||||
INLINE : 1,
|
||||
INJECTOR : 2,
|
||||
};
|
||||
|
||||
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].*)?`),
|
||||
[SNIPPET_TYPE.INLINE] : (name)=>new RegExp(`{{${name}`),
|
||||
[SNIPPET_TYPE.INJECTOR] : ()=>new RegExp(`^\\!\\[(?:[a-zA-Z -]+)?\\]\\(.*\\).*{[a-zA-Z0-9:, "'-]+}$`),
|
||||
},
|
||||
field : {
|
||||
[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
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { NUMBER_PATTERN } = require('../constants');
|
||||
|
||||
const Hints = createClass({
|
||||
hintsRef : React.createRef(),
|
||||
activeHintRef : React.createRef(),
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
hints : [],
|
||||
field : undefined,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
activeHint : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidUpdate : function({ hints }) {
|
||||
const hintsLength = this.props.hints.length;
|
||||
if(hintsLength - 1 < this.state.activeHint && hintsLength !== hints.length) {
|
||||
this.setState({
|
||||
activeHint : hintsLength === 0 ? 0 : hintsLength - 1
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.hintsRef.current && this.activeHintRef.current) {
|
||||
const offset = this.activeHintRef.current.offsetTop;
|
||||
const scrollTop = this.hintsRef.current.scrollTop;
|
||||
if(scrollTop + 50 < offset || scrollTop + 50 > offset) {
|
||||
this.hintsRef.current.scrollTo({
|
||||
top : offset - 50,
|
||||
behavior : 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount : function() {},
|
||||
|
||||
keyDown : function(e) {
|
||||
const { code } = e;
|
||||
const { activeHint } = this.state;
|
||||
const { hints, field } = this.props;
|
||||
const match = field?.state?.value?.match(NUMBER_PATTERN);
|
||||
if(code === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if(!match || !match?.at(3)) {
|
||||
this.setState({
|
||||
activeHint : activeHint === hints.length - 1 ? 0 : activeHint + 1
|
||||
});
|
||||
}
|
||||
} else if(code === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if(!match || !match?.at(3)) {
|
||||
this.setState({
|
||||
activeHint : activeHint === 0 ? hints.length - 1 : activeHint - 1
|
||||
});
|
||||
}
|
||||
} else if(code === 'Enter') {
|
||||
e.preventDefault();
|
||||
if(!match || !match?.at(3)) {
|
||||
field?.hintSelected(hints[activeHint]);
|
||||
this.setState({
|
||||
activeHint : 0
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render : function() {
|
||||
const { activeHint } = this.state;
|
||||
const { hints, field } = this.props;
|
||||
if(!field) return null;
|
||||
const bounds = field.fieldRef[field.state.id]?.current?.getBoundingClientRect();
|
||||
if(!bounds) return null;
|
||||
|
||||
const hintElements = hints
|
||||
.filter((h)=>h.hint !== field.state.value)
|
||||
.map((h, i)=>{
|
||||
let className = 'CodeMirror-hint';
|
||||
if(activeHint === i) {
|
||||
className += ' CodeMirror-hint-active';
|
||||
return <li key={i}
|
||||
role={'option'}
|
||||
className={className}
|
||||
onMouseDown={(e)=>field.hintSelected(h, e)}
|
||||
ref={this.activeHintRef}>
|
||||
{h.hint}
|
||||
</li>;
|
||||
}
|
||||
return <li key={i}
|
||||
role={'option'}
|
||||
className={className}
|
||||
onMouseDown={(e)=>field.hintSelected(h, e)}>
|
||||
{h.hint}
|
||||
</li>;
|
||||
});
|
||||
|
||||
let style = {
|
||||
display : 'none'
|
||||
};
|
||||
if(hintElements.length > 0) {
|
||||
style = {
|
||||
...style,
|
||||
display : 'block',
|
||||
top : `${bounds.top - 5}px`,
|
||||
left : `${bounds.left}px`
|
||||
};
|
||||
}
|
||||
return <React.Fragment>
|
||||
<ul role={'listbox'}
|
||||
id={'hints'}
|
||||
aria-expanded={true}
|
||||
className={'CodeMirror-hints default'}
|
||||
style={style}
|
||||
ref={this.hintsRef}>
|
||||
{hintElements}
|
||||
</ul>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Hints;
|
||||
@@ -0,0 +1,92 @@
|
||||
require('./image-selector.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { Modal, modalHelpers } = 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() {
|
||||
modalHelpers.mount(this);
|
||||
},
|
||||
|
||||
componentDidUpdate : function() {
|
||||
const { name, preview, values } = this.props.field;
|
||||
const { selected } = this.state;
|
||||
|
||||
const images = values.map((v, i)=>{
|
||||
const className = String(selected) === String(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() {
|
||||
modalHelpers.unmount(this);
|
||||
},
|
||||
|
||||
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;
|
||||
cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + currentText.length), '+insert');
|
||||
},
|
||||
|
||||
select : function(value) {
|
||||
this.setState({
|
||||
selected : value
|
||||
});
|
||||
},
|
||||
|
||||
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 <React.Fragment>
|
||||
<button onClick={this.showModal}>Select Image</button>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ImageSelector;
|
||||
@@ -0,0 +1,23 @@
|
||||
.images {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
max-height: 60vh;
|
||||
overflow-y: scroll;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +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,
|
||||
ColorSelector : ColorSelector,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
require('./modal.less');
|
||||
const React = require('react');
|
||||
const ReactDOMClient = require('react-dom/client');
|
||||
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 = {
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
require('./text.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const { NUMBER_PATTERN, HINT_TYPE, PATTERNS, STYLE_FN, FIELD_TYPE, SNIPPET_TYPE } = require('../constants');
|
||||
const CodeMirror = require('../../../code-mirror.js');
|
||||
|
||||
const Text = createClass({
|
||||
fieldRef : {},
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
field : {},
|
||||
text : '',
|
||||
n : 0,
|
||||
setHints : ()=>{},
|
||||
onChange : ()=>{},
|
||||
getStyleHints : ()=>{},
|
||||
def : false,
|
||||
snippetType : -1
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
value : '',
|
||||
id : ''
|
||||
};
|
||||
},
|
||||
|
||||
componentDidUpdate : function({ text }, { value }) {
|
||||
if(this.props.text !== text) {
|
||||
const { field, n } = this.props;
|
||||
const pattern = PATTERNS.field[field.type](field.name);
|
||||
const [_, __, value] = this.props.text.match(pattern) ?? [];
|
||||
this.setState({
|
||||
value : value,
|
||||
id : `${field?.name}-${n}`
|
||||
});
|
||||
}
|
||||
|
||||
if(this.state.value !== value) {
|
||||
const { field } = this.props;
|
||||
this.props.setHints(this, field.hints ? this.props.getStyleHints(field, this.state.value) : []);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
const { field, text, n } = this.props;
|
||||
const id = `${field?.name}-${n}`;
|
||||
const pattern = PATTERNS.field[field.type](field.name);
|
||||
const [_, __, value] = text.match(pattern) ?? [];
|
||||
this.setState({
|
||||
value : value,
|
||||
id
|
||||
});
|
||||
this.fieldRef[id] = React.createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
this.fieldRef = undefined;
|
||||
this.fieldRef = {};
|
||||
this.fieldRef[this.state.id]?.remove();
|
||||
},
|
||||
|
||||
setFocus : function(e) {
|
||||
const { type } = e;
|
||||
const { field } = this.props;
|
||||
this.props.setHints(this, type === 'focus' && field.hints ? this.props.getStyleHints(field, this.state.value) : []);
|
||||
},
|
||||
|
||||
hintSelected : function(h, e) {
|
||||
let value;
|
||||
if(h?.type === HINT_TYPE.VALUE) {
|
||||
value = h.hint;
|
||||
} else if(h?.type === HINT_TYPE.NUMBER_SUFFIX) {
|
||||
const match = this.state.value.match(NUMBER_PATTERN);
|
||||
let suffix = match?.at(4) ?? '';
|
||||
for (const char of h.hint) {
|
||||
if(suffix.at(0) === char) {
|
||||
suffix = suffix.slice(1);
|
||||
}
|
||||
}
|
||||
value = `${match?.at(1) ?? ''}${match?.at(2) ?? ''}${h.hint}${suffix}`;
|
||||
}
|
||||
this.onChange({
|
||||
target : {
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
keyDown : function(e) {
|
||||
const { code } = e;
|
||||
const { field } = this.props;
|
||||
const { value } = this.state;
|
||||
const match = value?.match(NUMBER_PATTERN);
|
||||
if(code === 'ArrowDown') {
|
||||
if(match && CSS.supports(field.name, value)) {
|
||||
e.preventDefault();
|
||||
this.onChange({
|
||||
target : {
|
||||
value : `${match.at(1) ?? ''}${Number(match[2]) - field.increment}${match[3] ?? ''}${match.at(4) ?? ''}`
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if(code === 'ArrowUp') {
|
||||
if(match && CSS.supports(field.name, value)) {
|
||||
e.preventDefault();
|
||||
this.onChange({
|
||||
target : {
|
||||
value : `${match.at(1) ?? ''}${Number(match[2]) + field.increment}${match[3] ?? ''}${match.at(4) ?? ''}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
onChange : function (e){
|
||||
const { cm, text, field, n, snippetType } = 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}`);
|
||||
let value = e.target.value;
|
||||
if(index === -1) {
|
||||
if(snippetType === SNIPPET_TYPE.INLINE) {
|
||||
index = text.indexOf('}');
|
||||
}
|
||||
index = index === -1 ? text.length : index;
|
||||
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,
|
||||
});
|
||||
},
|
||||
render : function() {
|
||||
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 <React.Fragment>
|
||||
<div className={className}>
|
||||
<label htmlFor={id}>{field.name}:</label>
|
||||
<input id={id} type='text' value={value}
|
||||
style={style}
|
||||
ref={this.fieldRef[id]}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.setFocus}
|
||||
onBlur={this.setFocus}
|
||||
onKeyDown={this.keyDown}/>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Text;
|
||||
@@ -0,0 +1,37 @@
|
||||
.widget-field {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
background-color: #ddd;
|
||||
border-radius: 10px;
|
||||
padding: 4px 2px;
|
||||
|
||||
>label {
|
||||
display: inline;
|
||||
width: 50px;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
>input {
|
||||
background-color: #ddd;
|
||||
border: none;
|
||||
}
|
||||
|
||||
>.hints {
|
||||
position: relative;
|
||||
left: 30px;
|
||||
max-height: 100px;
|
||||
overflow-y: scroll;
|
||||
background-color: white;
|
||||
|
||||
>.hint {
|
||||
margin: 0 0;
|
||||
padding: 2px;
|
||||
cursor: default;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
188
shared/naturalcrit/codeEditor/helpers/widgets.js
Normal file
188
shared/naturalcrit/codeEditor/helpers/widgets.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const React = require('react');
|
||||
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, ImageSelector, ColorSelector } = require('./widget-elements');
|
||||
const CodeMirror = require('../code-mirror.js');
|
||||
|
||||
// See https://codemirror.net/5/addon/hint/css-hint.js for code reference
|
||||
const pseudoClasses = { 'active' : 1, 'after' : 1, 'before' : 1, 'checked' : 1, 'default' : 1,
|
||||
'disabled' : 1, 'empty' : 1, 'enabled' : 1, 'first-child' : 1, 'first-letter' : 1,
|
||||
'first-line' : 1, 'first-of-type' : 1, 'focus' : 1, 'hover' : 1, 'in-range' : 1,
|
||||
'indeterminate' : 1, 'invalid' : 1, 'lang' : 1, 'last-child' : 1, 'last-of-type' : 1,
|
||||
'link' : 1, 'not' : 1, 'nth-child' : 1, 'nth-last-child' : 1, 'nth-last-of-type' : 1,
|
||||
'nth-of-type' : 1, 'only-of-type' : 1, 'only-child' : 1, 'optional' : 1, 'out-of-range' : 1,
|
||||
'placeholder' : 1, 'read-only' : 1, 'read-write' : 1, 'required' : 1, 'root' : 1,
|
||||
'selection' : 1, 'target' : 1, 'valid' : 1, 'visited' : 1
|
||||
};
|
||||
|
||||
const genKey = (...args)=>args.join('-');
|
||||
|
||||
module.exports = function(widgets, cm, setHints) {
|
||||
const roots = {};
|
||||
const spec = CodeMirror.resolveMode('text/css');
|
||||
const headless = CodeMirror(()=>{});
|
||||
|
||||
const makeTempCSSDoc = (value)=>CodeMirror.Doc(`.selector {\n${value}\n}`, 'text/css');
|
||||
|
||||
// See https://codemirror.net/5/addon/hint/css-hint.js for code reference
|
||||
const getStyleHints = (field, value)=>{
|
||||
const tempDoc = makeTempCSSDoc(`${field.name}:${value?.replaceAll(`'"`, '') ?? ''}`);
|
||||
headless.swapDoc(tempDoc);
|
||||
const pos = CodeMirror.Pos(1, field.name.length + 1 + (value?.length ?? 0), false);
|
||||
const token = headless.getTokenAt(pos);
|
||||
const inner = CodeMirror.innerMode(tempDoc.getMode(), token?.state);
|
||||
|
||||
if(inner.mode.name !== 'css') return;
|
||||
|
||||
if(token.type === 'keyword' && '!important'.indexOf(token.string) === 0)
|
||||
return { list : ['!important'], from : CodeMirror.Pos(pos.line, token.start),
|
||||
to : CodeMirror.Pos(pos.line, token.end) };
|
||||
|
||||
let start = token.start, end = pos.ch, word = token.string.slice(0, end - start);
|
||||
if(/[^\w$_-]/.test(word)) {
|
||||
word = ''; start = end = pos.ch;
|
||||
}
|
||||
|
||||
let result = [];
|
||||
const add = (keywords)=>{
|
||||
for (const name in keywords)
|
||||
if(!word || name.lastIndexOf(word, 0) === 0)
|
||||
result.push(name);
|
||||
};
|
||||
|
||||
const st = inner.state.state;
|
||||
if(st === 'pseudo' || token.type === 'variable-3') {
|
||||
add(pseudoClasses);
|
||||
} else if(st === 'block' || st === 'maybeprop') {
|
||||
add(spec.propertyKeywords);
|
||||
} else if(st === 'prop' || st === 'parens' || st === 'at' || st === 'params') {
|
||||
add(spec.valueKeywords);
|
||||
add(spec.colorKeywords);
|
||||
} else if(st === 'media' || st === 'media_parens') {
|
||||
add(spec.mediaTypes);
|
||||
add(spec.mediaFeatures);
|
||||
}
|
||||
result = result.map((h)=>({ hint: h, type: HINT_TYPE.VALUE }))
|
||||
.filter((h)=>CSS.supports(field.name, h.hint) && h.hint.includes(value ?? ''));
|
||||
|
||||
const numberSuffix = word.slice(-4).replaceAll(/\d/g, '');
|
||||
if(token.type === 'number' && !UNITS.includes(numberSuffix)) {
|
||||
result.push(...UNITS
|
||||
.filter((u)=>u.includes(numberSuffix) && CSS.supports(field.name, `${value.replaceAll(/\D/g, '') ?? ''}${u}`))
|
||||
.map((u)=>({ hint: u, type: HINT_TYPE.NUMBER_SUFFIX }))
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const widgetOptions = widgets.map((widget)=>({
|
||||
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 || 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 <Checkbox key={key} cm={cm} n={n} prefix={widget.name} value={field.name} def={true}/>;
|
||||
} else if(field.type === FIELD_TYPE.TEXT) {
|
||||
return <Text key={key} field={field} cm={cm} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints} def={true} snippetType={widget.type}/>;
|
||||
} else if(field.type === FIELD_TYPE.IMAGE_SELECTOR) {
|
||||
return <ImageSelector key={key} field={field} cm={cm} n={n}/>;
|
||||
} else if(field.type === FIELD_TYPE.COLOR_SELECTOR) {
|
||||
return <ColorSelector key={key} field={field} cm={cm} n={n} text={text} def={true} snippetType={widget.type}/>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
|
||||
const styles = [...text.matchAll(PATTERNS.collectStyles)].map(([_, style])=>{
|
||||
if(textFieldNames.includes(style)) return false;
|
||||
const field = {
|
||||
name : style,
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
hints : true,
|
||||
};
|
||||
const key = genKey(widget.name, n, style);
|
||||
if(style.includes('color')) {
|
||||
return <ColorSelector key={key} field={field} cm={cm} n={n} text={text} snippetType={widget.type}/>;
|
||||
}
|
||||
return <Text key={key} field={field} cm={cm} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints} snippetType={widget.type}/>;
|
||||
}).filter(Boolean);
|
||||
|
||||
const root = roots[n][id] ?? ReactDOMClient.createRoot(node || parent);
|
||||
root.render(<React.Fragment>
|
||||
{fields}
|
||||
{styles}
|
||||
</React.Fragment>);
|
||||
roots[n][id] = root;
|
||||
|
||||
return node || parent;
|
||||
}
|
||||
}));
|
||||
|
||||
const updateLineWidgets = (n)=>{
|
||||
const { text, widgets } = cm.lineInfo(n);
|
||||
const widgetOption = widgetOptions.find((option)=>!!text.match(option.pattern));
|
||||
if(!widgetOption) return;
|
||||
if(!!widgets) {
|
||||
for (const widget of widgets) {
|
||||
widgetOption.renderWidget(n, widget.node);
|
||||
}
|
||||
} else {
|
||||
return cm.addLineWidget(n, widgetOption.renderWidget(n), {
|
||||
above : false,
|
||||
coverGutter : false,
|
||||
noHScroll : true,
|
||||
className : `snippet-options-widget ${widgetOption.name}-widget ${widgetOption.name}-widget-${n}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
roots,
|
||||
removeLineWidget : (n, widget)=>{
|
||||
roots[n][widget.node.id]?.unmount();
|
||||
delete roots[n][widget.node.id];
|
||||
widget?.clear();
|
||||
},
|
||||
updateLineWidgets,
|
||||
updateAllLineWidgets : ()=>{
|
||||
for (let i = 0; i < cm.lineCount(); i++) {
|
||||
const { widgets } = cm.lineInfo(i);
|
||||
if(!!widgets) {
|
||||
updateLineWidgets(i);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateWidgetGutter : ()=>{
|
||||
cm.operation(()=>{
|
||||
for (let i = 0; i < cm.lineCount(); i++) {
|
||||
const { text, widgets } = cm.lineInfo(i);
|
||||
|
||||
if(widgetOptions.some((option)=>text.match(option.pattern))) {
|
||||
if(widgets) {
|
||||
continue;
|
||||
}
|
||||
const optionsMarker = document.createElement('div');
|
||||
optionsMarker.style.color = '#822';
|
||||
optionsMarker.style.cursor = 'pointer';
|
||||
optionsMarker.innerHTML = '●';
|
||||
cm.setGutterMarker(i, 'widget-gutter', optionsMarker);
|
||||
} else {
|
||||
cm.setGutterMarker(i, 'widget-gutter', null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
const _ = require('lodash');
|
||||
const Marked = require('marked');
|
||||
const MarkedExtendedTables = require('marked-extended-tables');
|
||||
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
|
||||
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
|
||||
const renderer = new Marked.Renderer();
|
||||
|
||||
@@ -237,49 +238,10 @@ const definitionLists = {
|
||||
}
|
||||
};
|
||||
|
||||
const MarkedSmartyPantsLite = ()=>{
|
||||
return {
|
||||
tokenizer : {
|
||||
inlineText(src) {
|
||||
// don't escape inlineText
|
||||
const cap = this.rules.inline.text.exec(src);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if(!cap) {
|
||||
// should never happen
|
||||
return;
|
||||
}
|
||||
|
||||
const text = cap[0]
|
||||
// em-dashes
|
||||
.replace(/---/g, '\u2014')
|
||||
// en-dashes
|
||||
.replace(/--/g, '\u2013')
|
||||
// opening singles
|
||||
.replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
|
||||
// closing singles & apostrophes
|
||||
.replace(/'/g, '\u2019')
|
||||
// opening doubles
|
||||
.replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
|
||||
// closing doubles
|
||||
.replace(/"/g, '\u201d')
|
||||
// ellipses
|
||||
.replace(/\.{3}/g, '\u2026');
|
||||
|
||||
return {
|
||||
type : 'text',
|
||||
raw : cap[0],
|
||||
text : text
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
||||
Marked.use(mustacheInjectBlock);
|
||||
Marked.use({ renderer: renderer, mangle: false });
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartyPantsLite());
|
||||
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
|
||||
|
||||
//Fix local links in the Preview iFrame to link inside the frame
|
||||
renderer.link = function (href, title, text) {
|
||||
@@ -351,12 +313,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -387,7 +343,7 @@ module.exports = {
|
||||
render : (rawBrewText)=>{
|
||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||
return Marked.parse(sanatizeScriptTags(rawBrewText));
|
||||
return Marked.parse(rawBrewText);
|
||||
},
|
||||
|
||||
validate : (rawBrewText)=>{
|
||||
|
||||
@@ -90,12 +90,6 @@ const escape = function (html, encode) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
_.map(tagTypes, (type)=>{
|
||||
@@ -113,7 +107,7 @@ module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
rawBrewText,
|
||||
{ renderer: renderer }
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require('./nav.less');
|
||||
const React = require('react');
|
||||
const { useState, useRef, useEffect } = React;
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
@@ -71,64 +72,49 @@ const Nav = {
|
||||
}
|
||||
}),
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
trigger : 'hover'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
handleClickOutside : function(e){
|
||||
// Close dropdown when clicked outside
|
||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
||||
this.handleDropdown(false);
|
||||
}
|
||||
},
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
});
|
||||
},
|
||||
renderDropdown : function(dropdownChildren){
|
||||
if(!this.state.showDropdown) return null;
|
||||
dropdown : function dropdown(props) {
|
||||
props = Object.assign({}, props, {
|
||||
trigger : 'hover click'
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='navDropdown'>
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render : function () {
|
||||
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
||||
// Ignore the first child
|
||||
if(i < 1) return;
|
||||
return child;
|
||||
});
|
||||
return (
|
||||
<div className={`navDropdownContainer ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
const myRef = useRef(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return ()=>{
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function handleClickOutside(e) {
|
||||
// Close dropdown when clicked outside
|
||||
if(!myRef.current?.contains(e.target)) {
|
||||
handleDropdown(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function handleDropdown(show) {
|
||||
setShowDropdown(show ?? !showDropdown);
|
||||
}
|
||||
|
||||
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
|
||||
if(i < 1) return;
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`navDropdownContainer ${props.className}`}
|
||||
ref={myRef}
|
||||
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
|
||||
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
|
||||
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
|
||||
>
|
||||
{props.children[0] || props.children /*children is not an array when only one child*/}
|
||||
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ nav{
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : calc(100vh - 28px);
|
||||
.navItem{
|
||||
animation-name: glideDropDown;
|
||||
animation-duration: 0.4s;
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Escapes <script> tag', function() {
|
||||
const source = '<script></script>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toMatch('<script></script>');
|
||||
});
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
|
||||
@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
|
||||
@@ -19,16 +19,6 @@ module.exports = [
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fas fa-book',
|
||||
|
||||
@@ -48,8 +48,8 @@ const getTOC = (pages)=>{
|
||||
return res;
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
const pages = brew.text.split('\\page');
|
||||
module.exports = function(props){
|
||||
const pages = props.brew.text.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
if(g1.title !== null) {
|
||||
|
||||
154
themes/V3/5ePHB/widgets.js
Normal file
154
themes/V3/5ePHB/widgets.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const _ = require('lodash');
|
||||
const { SNIPPET_TYPE, FIELD_TYPE, fourDigitNumberFromValue } = require('../../../shared/naturalcrit/codeEditor/helpers/widget-elements/constants');
|
||||
|
||||
module.exports = [{
|
||||
name : 'monster',
|
||||
type : SNIPPET_TYPE.BLOCK,
|
||||
fields : [{
|
||||
name : 'frame',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'wide',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}]
|
||||
}, {
|
||||
name : 'classTable',
|
||||
type : SNIPPET_TYPE.BLOCK,
|
||||
fields : [{
|
||||
name : 'frame',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'decoration',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'wide',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}]
|
||||
}, {
|
||||
name : 'runeTable',
|
||||
type : SNIPPET_TYPE.BLOCK,
|
||||
fields : [{
|
||||
name : 'frame',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'wide',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'font-family',
|
||||
type : FIELD_TYPE.TEXT
|
||||
}]
|
||||
}, {
|
||||
name : 'index',
|
||||
type : SNIPPET_TYPE.BLOCK,
|
||||
fields : [{
|
||||
name : 'wide',
|
||||
type : FIELD_TYPE.CHECKBOX
|
||||
}, {
|
||||
name : 'columns',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 1
|
||||
}]
|
||||
}, {
|
||||
name : 'image',
|
||||
type : SNIPPET_TYPE.INJECTOR,
|
||||
fields : []
|
||||
}, {
|
||||
name : 'artist',
|
||||
type : SNIPPET_TYPE.BLOCK,
|
||||
fields : [{
|
||||
name : 'top',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
hints : true
|
||||
}]
|
||||
}, {
|
||||
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 : 'background-color',
|
||||
type : FIELD_TYPE.COLOR_SELECTOR,
|
||||
}]
|
||||
}, {
|
||||
name : 'imageMaskCenter',
|
||||
type : SNIPPET_TYPE.INLINE,
|
||||
fields : [{
|
||||
name : 'imageMaskCenter',
|
||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
||||
preview : (value)=>`/assets/waterColorMasks/center/${fourDigitNumberFromValue(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,
|
||||
}]
|
||||
}, {
|
||||
name : 'imageMaskEdge',
|
||||
type : SNIPPET_TYPE.INLINE,
|
||||
fields : [{
|
||||
name : 'imageMaskEdge',
|
||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
||||
preview : (value)=>`/assets/waterColorMasks/edge/${fourDigitNumberFromValue(value)}.webp`,
|
||||
values : _.range(1, 9)
|
||||
}, {
|
||||
name : '--offset',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
}, {
|
||||
name : '--rotation',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
}]
|
||||
}, {
|
||||
name : 'imageMaskCorner',
|
||||
type : SNIPPET_TYPE.INLINE,
|
||||
fields : [{
|
||||
name : 'imageMaskCorner',
|
||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
||||
preview : (value)=>`/assets/waterColorMasks/corner/${fourDigitNumberFromValue(value)}.webp`,
|
||||
values : _.range(1, 38)
|
||||
}, {
|
||||
name : '--offsetX',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
}, {
|
||||
name : '--offsetY',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
}, {
|
||||
name : '--rotation',
|
||||
type : FIELD_TYPE.TEXT,
|
||||
increment : 5,
|
||||
}]
|
||||
}];
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||
const FooterGen = require('./snippets/footer.gen.js');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
module.exports = [
|
||||
@@ -21,6 +22,53 @@ module.exports = [
|
||||
icon : 'fas fa-file-alt',
|
||||
gen : '\n\\page\n'
|
||||
},
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fas fa-bookmark',
|
||||
gen : '{{pageNumber 1}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fas fa-sort-numeric-down',
|
||||
gen : '{{pageNumber,auto}}\n'
|
||||
},
|
||||
{
|
||||
name : 'Footer',
|
||||
icon : 'fas fa-shoe-prints',
|
||||
gen : FooterGen.createFooterFunc(),
|
||||
subsnippets : [
|
||||
{
|
||||
name : 'Footer from H1',
|
||||
icon : 'fas fa-dice-one',
|
||||
gen : FooterGen.createFooterFunc(1)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H2',
|
||||
icon : 'fas fa-dice-two',
|
||||
gen : FooterGen.createFooterFunc(2)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H3',
|
||||
icon : 'fas fa-dice-three',
|
||||
gen : FooterGen.createFooterFunc(3)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H4',
|
||||
icon : 'fas fa-dice-four',
|
||||
gen : FooterGen.createFooterFunc(4)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H5',
|
||||
icon : 'fas fa-dice-five',
|
||||
gen : FooterGen.createFooterFunc(5)
|
||||
},
|
||||
{
|
||||
name : 'Footer from H6',
|
||||
icon : 'fas fa-dice-six',
|
||||
gen : FooterGen.createFooterFunc(6)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name : 'Vertical Spacing',
|
||||
icon : 'fas fa-arrows-alt-v',
|
||||
|
||||
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
17
themes/V3/Blank/snippets/footer.gen.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||
|
||||
module.exports = {
|
||||
createFooterFunc : function(headerSize=1){
|
||||
return (props)=>{
|
||||
const cursorPos = props.cursorPos;
|
||||
|
||||
const markdownText = props.brew.text.split('\n').slice(0, cursorPos.line).join('\n');
|
||||
const markdownTokens = Markdown.marked.lexer(markdownText);
|
||||
const headerToken = markdownTokens.findLast((lexerToken)=>{ return lexerToken.type === 'heading' && lexerToken.depth === headerSize; });
|
||||
const headerText = headerToken?.tokens.map((token)=>{ return token.text; }).join('');
|
||||
const outputText = headerText || 'PART 1 | SECTION NAME';
|
||||
|
||||
return `\n{{footnote ${outputText}}}\n`;
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user