0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-02 15:02:38 +00:00

Merge branch 'master' into addEditorThemes-#362

This commit is contained in:
G.Ambatte
2023-07-11 07:38:13 +12:00
committed by GitHub
24 changed files with 947 additions and 889 deletions

View File

@@ -15,7 +15,7 @@ module.exports = {
rules : { rules : {
/** Errors **/ /** Errors **/
'camelcase' : ['error', { properties: 'never' }], 'camelcase' : ['error', { properties: 'never' }],
'func-style' : ['error', 'expression', { allowArrowFunctions: true }], //'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error', 'no-array-constructor' : 'error',
'no-iterator' : 'error', 'no-iterator' : 'error',
'no-nested-ternary' : 'error', 'no-nested-ternary' : 'error',

View File

@@ -108,6 +108,12 @@ const BrewRenderer = createClass({
return false; return false;
}, },
sanitizeScriptTags : function(content) {
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
},
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo' ref='main'> return <div className='pageInfo' ref='main'>
<div> <div>
@@ -135,18 +141,20 @@ const BrewRenderer = createClass({
renderStyle : function() { renderStyle : function() {
if(!this.props.style) return; if(!this.props.style) return;
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />; const cleanStyle = this.sanitizeScriptTags(this.props.style);
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</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){ renderPage : function(pageText, index){
const cleanPageText = this.sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy') 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 { else {
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return ( return (
<div className='page' id={`p${index + 1}`} key={index} > <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> </div>
); );
} }
@@ -185,6 +193,12 @@ const BrewRenderer = createClass({
}, 100); }, 100);
}, },
emitClick : function(){
// console.log('iFrame clicked');
if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click'));
},
render : function(){ render : function(){
//render in iFrame so broken code doesn't crash the site. //render in iFrame so broken code doesn't crash the site.
//Also render dummy page while iframe is mounting. //Also render dummy page while iframe is mounting.
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
<Frame id='BrewRenderer' initialContent={this.state.initialContent} <Frame id='BrewRenderer' initialContent={this.state.initialContent}
style={{ width: '100%', height: '100%', visibility: this.state.visibility }} style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
contentDidMount={this.frameDidMount}> contentDidMount={this.frameDidMount}
onClick={()=>{this.emitClick();}}
>
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={this.handleScroll} onScroll={this.handleScroll}
style={{ height: this.state.height }}> style={{ height: this.state.height }}>

View File

@@ -345,7 +345,8 @@ const Editor = createClass({
redo={this.redo} redo={this.redo}
historySize={this.historySize()} historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme} currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme} /> updateEditorTheme={this.updateEditorTheme}
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
{this.renderEditor()} {this.renderEditor()}
</div> </div>

View File

@@ -15,8 +15,8 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js'); ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js'); ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
const execute = function(val, brew){ const execute = function(val, props){
if(_.isFunction(val)) return val(brew); if(_.isFunction(val)) return val(props);
return val; return val;
}; };
@@ -34,7 +34,8 @@ const Snippetbar = createClass({
undo : ()=>{}, undo : ()=>{},
redo : ()=>{}, redo : ()=>{},
historySize : ()=>{}, historySize : ()=>{},
updateEditorTheme : ()=>{} updateEditorTheme : ()=>{},
cursorPos : {}
}; };
}, },
@@ -131,6 +132,7 @@ const Snippetbar = createClass({
snippets={snippetGroup.snippets} snippets={snippetGroup.snippets}
key={snippetGroup.groupName} key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick} onSnippetClick={this.handleSnippetClick}
cursorPos={this.props.cursorPos}
/>; />;
}); });
}, },
@@ -197,7 +199,7 @@ const SnippetGroup = createClass({
}, },
handleSnippetClick : function(e, snippet){ handleSnippetClick : function(e, snippet){
e.stopPropagation(); e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props.brew)); this.props.onSnippetClick(execute(snippet.gen, this.props));
}, },
renderSnippets : function(snippets){ renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{ return _.map(snippets, (snippet)=>{

View File

@@ -81,20 +81,70 @@
color : pink; color : pink;
} }
} }
.recent.navItem { .recent.navDropdownContainer {
position : relative; position : relative;
.dropdown { .navDropdown .navItem {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
overflow : hidden auto; overflow : hidden auto;
width : 100%;
max-height : ~"calc(100vh - 28px)"; max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333; scrollbar-color : #666 #333;
scrollbar-width : thin; 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; display : block;
box-sizing : border-box; box-sizing : border-box;
padding : 5px 0; padding : 5px 0;
@@ -109,62 +159,6 @@
background-color : darken(@purple, 30%); 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 { .metadata.navItem {

View File

@@ -121,6 +121,7 @@ const RecentItems = createClass({
removeItem : function(url, evt){ removeItem : function(url, evt){
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation();
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]'); let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]'); let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
@@ -139,11 +140,11 @@ const RecentItems = createClass({
}, },
renderDropdown : function(){ renderDropdown : function(){
if(!this.state.showDropdown) return null; // if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{ const makeItems = (brews)=>{
return _.map(brews, (brew, i)=>{ 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='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</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> <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) ? {(this.props.showEdit && this.props.showView) ?
<h4>edited</h4> : null } <Nav.item className='header'>edited</Nav.item> : null }
{this.props.showEdit ? {this.props.showEdit ?
makeItems(this.state.edit) : null } makeItems(this.state.edit) : null }
{(this.props.showEdit && this.props.showView) ? {(this.props.showEdit && this.props.showView) ?
<h4>viewed</h4> : null } <Nav.item className='header'>viewed</Nav.item> : null }
{this.props.showView ? {this.props.showView ?
makeItems(this.state.view) : null } makeItems(this.state.view) : null }
</div>; </>;
}, },
render : function(){ render : function(){
return <Nav.item icon='fas fa-history' color='grey' className='recent' return <Nav.dropdown className='recent'>
onMouseEnter={()=>this.handleDropdown(true)} <Nav.item icon='fas fa-history' color='grey' >
onMouseLeave={()=>this.handleDropdown(false)}> {this.props.text}
{this.props.text} </Nav.item>
{this.renderDropdown()} {this.renderDropdown()}
</Nav.item>; </Nav.dropdown>;
} }
}); });

1369
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,9 +78,9 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.22.8",
"@babel/plugin-transform-runtime": "^7.22.5", "@babel/plugin-transform-runtime": "^7.22.7",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.7",
"@babel/preset-react": "^7.22.5", "@babel/preset-react": "^7.22.5",
"@googleapis/drive": "^5.1.0", "@googleapis/drive": "^5.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
@@ -103,12 +103,12 @@
"marked-smartypants-lite": "^1.0.0", "marked-smartypants-lite": "^1.0.0",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^7.3.1", "mongoose": "^7.3.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^9.7.2", "npm": "^9.8.0",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.14.1", "react-router-dom": "6.14.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
@@ -119,12 +119,12 @@
"eslint": "^8.44.0", "eslint": "^8.44.0",
"eslint-plugin-jest": "^27.2.2", "eslint-plugin-jest": "^27.2.2",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"jest": "^29.5.0", "jest": "^29.6.1",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^15.9.0", "stylelint": "^15.10.1",
"stylelint-config-recess-order": "^4.2.0", "stylelint-config-recess-order": "^4.2.0",
"stylelint-config-recommended": "^12.0.0", "stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.2", "stylelint-stylistic": "^0.4.2",
"supertest": "^6.3.3" "supertest": "^6.3.3"
} }

View File

@@ -153,12 +153,12 @@ fs.emptyDirSync('./build');
})().catch(console.error); })().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){ if(isDev){
livereload('./build'); livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Rebuild when change detected to this file or any nested directory from here watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build'], // Ignore ./build or it will rebuild again ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'less', // Other extensions to watch (only .js/.json/.jsx by default) ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch: ['./client', './server', './themes'], // Watch additional folders if you want //watch : ['./server', './themes'], // Watch additional folders if needed
}); });
} }

View File

@@ -326,8 +326,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
}; };
if(req.params.id.length > 12 && !brew._id) { if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12); const googleId = brew.googleId;
const shareId = req.params.id.slice(-12); const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew) await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);}); .catch((err)=>{next(err);});
} else { } else {

View File

@@ -100,12 +100,12 @@ const GoogleActions = {
const drive = googleDrive.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
const fileList = []; const fileList = [];
let NextPageToken = ""; let NextPageToken = '';
do { do {
const obj = await drive.files.list({ const obj = await drive.files.list({
pageSize : 1000, pageSize : 1000,
pageToken : NextPageToken || "", pageToken : NextPageToken || '',
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)', fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false' q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
}) })
@@ -243,9 +243,9 @@ const GoogleActions = {
if(obj) { if(obj) {
if(accessType == 'edit' && obj.data.properties.editId != accessId){ 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){ } 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({ const file = await drive.files.get({

View File

@@ -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 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) { if(id.length > 12) {
googleId = id.slice(0, -12); if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
id = id.slice(-12); 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 }; return { id, googleId };
}, },

View File

@@ -111,15 +111,26 @@ describe('Tests for api', ()=>{
expect(googleId).toEqual('12345'); 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({ const { id, googleId } = api.getId({
params : { params : {
id : '123456789012abcdefghijkl' id : '123456789012345678901234567890123abcdefghijkl'
} }
}); });
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghijkl'); 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');
}); });
}); });

View File

@@ -182,7 +182,7 @@ const CodeEditor = createClass({
indent : function () { indent : function () {
const cm = this.codeMirror; const cm = this.codeMirror;
if (cm.somethingSelected()) { if(cm.somethingSelected()) {
cm.execCommand('indentMore'); cm.execCommand('indentMore');
} else { } else {
cm.execCommand('insertSoftTab'); cm.execCommand('insertSoftTab');

View File

@@ -313,12 +313,6 @@ const escape = function (html, encode) {
return html; return html;
}; };
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a']; const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${ const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{ _.map(tagTypes, (type)=>{
@@ -349,7 +343,7 @@ module.exports = {
render : (rawBrewText)=>{ render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`) rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`); .replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Marked.parse(sanatizeScriptTags(rawBrewText)); return Marked.parse(rawBrewText);
}, },
validate : (rawBrewText)=>{ validate : (rawBrewText)=>{

View File

@@ -90,12 +90,6 @@ const escape = function (html, encode) {
return html; return html;
}; };
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a']; const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${ const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{ _.map(tagTypes, (type)=>{
@@ -113,7 +107,7 @@ module.exports = {
marked : Markdown, marked : Markdown,
render : (rawBrewText)=>{ render : (rawBrewText)=>{
return Markdown( return Markdown(
sanatizeScriptTags(rawBrewText), rawBrewText,
{ renderer: renderer } { renderer: renderer }
); );
}, },

View File

@@ -1,5 +1,6 @@
require('./nav.less'); require('./nav.less');
const React = require('react'); const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -71,64 +72,49 @@ const Nav = {
} }
}), }),
dropdown : createClass({ dropdown : function dropdown(props) {
displayName : 'Nav.dropdown', props = Object.assign({}, props, {
getDefaultProps : function() { trigger : 'hover click'
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;
return ( const myRef = useRef(null);
<div className='navDropdown'> const [showDropdown, setShowDropdown] = useState(false);
{dropdownChildren}
</div> useEffect(()=>{
); document.addEventListener('click', handleClickOutside);
}, return ()=>{
render : function () { document.removeEventListener('click', handleClickOutside);
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{ };
// Ignore the first child }, []);
if(i < 1) return;
return child; function handleClickOutside(e) {
}); // Close dropdown when clicked outside
return ( if(!myRef.current?.contains(e.target)) {
<div className={`navDropdownContainer ${this.props.className}`} handleDropdown(false);
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>
);
} }
})
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>
);
}
}; };

View File

@@ -79,6 +79,8 @@ nav{
left : 0px; left : 0px;
z-index : 10000; z-index : 10000;
width : 100%; width : 100%;
overflow : hidden auto;
max-height : calc(100vh - 28px);
.navItem{ .navItem{
animation-name: glideDropDown; animation-name: glideDropDown;
animation-duration: 0.4s; animation-duration: 0.4s;

View File

@@ -2,12 +2,6 @@
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
test('Escapes <script> tag', function() {
const source = '<script></script>';
const rendered = Markdown.render(source);
expect(rendered).toMatch('<p>&lt;script>&lt;/script&gt;</p>\n');
});
test('Processes the markdown within an HTML block if its just a class wrapper', function() { test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>'; const source = '<div>*Bold text*</div>';
const rendered = Markdown.render(source); const rendered = Markdown.render(source);

View File

@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
return res; return res;
}; };
module.exports = function(brew){ module.exports = function(props){
const pages = brew.text.split('\\page'); const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`); r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);

View File

@@ -19,16 +19,6 @@ module.exports = [
icon : 'fas fa-pencil-alt', icon : 'fas fa-pencil-alt',
view : 'text', view : 'text',
snippets : [ 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', name : 'Table of Contents',
icon : 'fas fa-book', icon : 'fas fa-book',

View File

@@ -48,8 +48,8 @@ const getTOC = (pages)=>{
return res; return res;
}; };
module.exports = function(brew){ module.exports = function(props){
const pages = brew.text.split('\\page'); const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
if(g1.title !== null) { if(g1.title !== null) {

View File

@@ -2,6 +2,7 @@
const WatercolorGen = require('./snippets/watercolor.gen.js'); const WatercolorGen = require('./snippets/watercolor.gen.js');
const ImageMaskGen = require('./snippets/imageMask.gen.js'); const ImageMaskGen = require('./snippets/imageMask.gen.js');
const FooterGen = require('./snippets/footer.gen.js');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
module.exports = [ module.exports = [
@@ -21,6 +22,53 @@ module.exports = [
icon : 'fas fa-file-alt', icon : 'fas fa-file-alt',
gen : '\n\\page\n' 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', name : 'Vertical Spacing',
icon : 'fas fa-arrows-alt-v', icon : 'fas fa-arrows-alt-v',

View 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`;
};
}
};