diff --git a/changelog.md b/changelog.md index 7cfbfde8a..1e1ac70e2 100644 --- a/changelog.md +++ b/changelog.md @@ -83,12 +83,40 @@ pre { .page .exampleTable td,th { border:1px dashed #00000030; } + +.page .df { + font-size: 2em; + vertical-align: middle; +} ``` ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). -### Friday 11/14/2025 - v13.20.0 +### Friday 1/11/2026 - v3.20.1 + +{{taskList +##### calculuschild +* [x] Add D100 "ball" dice icons `:d100:` :df_d100_05: + +##### G-Ambatte +* [x] Fix transparent edge on back cover image + +Fixes issue [#4551](https://github.com/naturalcrit/homebrewery/issues/4551) + +* [x] Fix "Out of sync" error when document contains extended unicode characters + +Fixes issue [#4583](https://github.com/naturalcrit/homebrewery/issues/4583) + +##### 5e-Cleric +* [x] Fix page count error on Vault + +* [x] Fix cover page footnote set to all-caps + +Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559) +}} + +### Friday 11/14/2025 - v3.20.0 {{taskList ##### calculuschild diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx index 787c2a3eb..cc6eb72ca 100644 --- a/client/admin/admin.jsx +++ b/client/admin/admin.jsx @@ -1,7 +1,7 @@ import './admin.less'; import React, { useEffect, useState } from 'react'; -const BrewUtils = require('./brewUtils/brewUtils.jsx'); -const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); +import BrewUtils from './brewUtils/brewUtils.jsx'; +import NotificationUtils from './notificationUtils/notificationUtils.jsx'; import AuthorUtils from './authorUtils/authorUtils.jsx'; import LockTools from './lockTools/lockTools.jsx'; diff --git a/client/admin/authorUtils/authorLookup/authorLookup.jsx b/client/admin/authorUtils/authorLookup/authorLookup.jsx index abdece6f7..cdfe6274b 100644 --- a/client/admin/authorUtils/authorLookup/authorLookup.jsx +++ b/client/admin/authorUtils/authorLookup/authorLookup.jsx @@ -84,4 +84,4 @@ const authorLookup = ()=>{ ); }; -module.exports = authorLookup; +export default authorLookup; diff --git a/client/admin/authorUtils/authorUtils.jsx b/client/admin/authorUtils/authorUtils.jsx index a96eea528..ef717a1ec 100644 --- a/client/admin/authorUtils/authorUtils.jsx +++ b/client/admin/authorUtils/authorUtils.jsx @@ -10,4 +10,4 @@ const authorUtils = ()=>{ ); }; -module.exports = authorUtils; \ No newline at end of file +export default authorUtils; \ No newline at end of file diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index d4b17c570..6cec01178 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -1,9 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const request = require('superagent'); - -const BrewCleanup = createClass({ +const BrewCleanup = createReactClass({ displayName : 'BrewCleanup', getDefaultProps(){ return {}; @@ -69,4 +68,4 @@ const BrewCleanup = createClass({ } }); -module.exports = BrewCleanup; +export default BrewCleanup; diff --git a/client/admin/brewUtils/brewCompress/brewCompress.jsx b/client/admin/brewUtils/brewCompress/brewCompress.jsx index ccb59e027..181315fce 100644 --- a/client/admin/brewUtils/brewCompress/brewCompress.jsx +++ b/client/admin/brewUtils/brewCompress/brewCompress.jsx @@ -1,8 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const request = require('superagent'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const BrewCompress = createClass({ +const BrewCompress = createReactClass({ displayName : 'BrewCompress', getDefaultProps(){ return {}; @@ -85,4 +85,4 @@ const BrewCompress = createClass({ } }); -module.exports = BrewCompress; +export default BrewCompress; diff --git a/client/admin/brewUtils/brewLookup/brewLookup.jsx b/client/admin/brewUtils/brewLookup/brewLookup.jsx index fb780f29e..6b725198c 100644 --- a/client/admin/brewUtils/brewLookup/brewLookup.jsx +++ b/client/admin/brewUtils/brewLookup/brewLookup.jsx @@ -1,12 +1,11 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const cx = require('classnames'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; +import cx from 'classnames'; -const request = require('superagent'); -const Moment = require('moment'); +import Moment from 'moment'; - -const BrewLookup = createClass({ +const BrewLookup = createReactClass({ getDefaultProps() { return {}; }, @@ -110,4 +109,4 @@ const BrewLookup = createClass({ } }); -module.exports = BrewLookup; +export default BrewLookup; diff --git a/client/admin/brewUtils/brewUtils.jsx b/client/admin/brewUtils/brewUtils.jsx index bab2cb82f..dd663ddaa 100644 --- a/client/admin/brewUtils/brewUtils.jsx +++ b/client/admin/brewUtils/brewUtils.jsx @@ -1,15 +1,14 @@ -const React = require('react'); -const createClass = require('create-react-class'); -require('./brewUtils.less'); +import React from 'react'; +import './brewUtils.less'; -const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); -const BrewLookup = require('./brewLookup/brewLookup.jsx'); -const BrewCompress = require ('./brewCompress/brewCompress.jsx'); -const Stats = require('./stats/stats.jsx'); +import BrewCleanup from './brewCleanup/brewCleanup.jsx'; +import BrewLookup from './brewLookup/brewLookup.jsx'; +import BrewCompress from './brewCompress/brewCompress.jsx'; +import Stats from './stats/stats.jsx'; -const BrewUtils = createClass({ - render : function(){ - return <> +const BrewUtils = ()=>{ + return ( + <>
@@ -17,8 +16,7 @@ const BrewUtils = createClass({
- ; - } -}); - -module.exports = BrewUtils; + + ); +}; +export default BrewUtils; diff --git a/client/admin/brewUtils/stats/stats.jsx b/client/admin/brewUtils/stats/stats.jsx index 7f96618f9..b05d3afd5 100644 --- a/client/admin/brewUtils/stats/stats.jsx +++ b/client/admin/brewUtils/stats/stats.jsx @@ -1,9 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const request = require('superagent'); - -const Stats = createClass({ +const Stats = createReactClass({ displayName : 'Stats', getDefaultProps(){ return {}; @@ -43,4 +42,4 @@ const Stats = createClass({ } }); -module.exports = Stats; +export default Stats; diff --git a/client/admin/lockTools/lockTools.jsx b/client/admin/lockTools/lockTools.jsx index 9a28d330f..8144d5d11 100644 --- a/client/admin/lockTools/lockTools.jsx +++ b/client/admin/lockTools/lockTools.jsx @@ -1,11 +1,11 @@ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ -require('./lockTools.less'); -const React = require('react'); -const createClass = require('create-react-class'); +import './lockTools.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; import request from '../../homebrew/utils/request-middleware.js'; -const LockTools = createClass({ +const LockTools = createReactClass({ displayName : 'LockTools', getInitialState : function() { return { @@ -55,7 +55,7 @@ const LockTools = createClass({ } }); -const LockBrew = createClass({ +const LockBrew = createReactClass({ displayName : 'LockBrew', getInitialState : function() { // Default values @@ -183,7 +183,7 @@ const LockBrew = createClass({ } }); -const LockTable = createClass({ +const LockTable = createReactClass({ displayName : 'LockTable', getDefaultProps : function() { return { @@ -273,7 +273,7 @@ const LockTable = createClass({ } }); -const LockLookup = createClass({ +const LockLookup = createReactClass({ displayName : 'LockLookup', getDefaultProps : function() { return { @@ -339,4 +339,4 @@ const LockLookup = createClass({ } }); -module.exports = LockTools; \ No newline at end of file +export default LockTools; \ No newline at end of file diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx index 0cca1047e..3a64d4bf7 100644 --- a/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx +++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx @@ -1,7 +1,6 @@ -require('./notificationAdd.less'); -const React = require('react'); -const { useState, useRef } = require('react'); -const request = require('superagent'); +import './notificationAdd.less'; +import React, { useState, useRef } from 'react'; +import request from 'superagent'; const NotificationAdd = ()=>{ const [notificationResult, setNotificationResult] = useState(null); @@ -106,4 +105,4 @@ const NotificationAdd = ()=>{ ); }; -module.exports = NotificationAdd; +export default NotificationAdd; diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx index 05f81b776..063b3e908 100644 --- a/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx +++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx @@ -1,9 +1,7 @@ -require('./notificationLookup.less'); - -const React = require('react'); -const { useState } = require('react'); -const request = require('superagent'); -const Moment = require('moment'); +import './notificationLookup.less'; +import React, { useState } from 'react'; +import request from 'superagent'; +import Moment from 'moment'; const NotificationDetail = ({ notification, onDelete })=>( <> @@ -102,4 +100,4 @@ const NotificationLookup = ()=>{ ); }; -module.exports = NotificationLookup; +export default NotificationLookup; diff --git a/client/admin/notificationUtils/notificationUtils.jsx b/client/admin/notificationUtils/notificationUtils.jsx index 22ea21328..00db44a37 100644 --- a/client/admin/notificationUtils/notificationUtils.jsx +++ b/client/admin/notificationUtils/notificationUtils.jsx @@ -1,7 +1,6 @@ -const React = require('react'); - -const NotificationLookup = require('./notificationLookup/notificationLookup.jsx'); -const NotificationAdd = require('./notificationAdd/notificationAdd.jsx'); +import React from 'react'; +import NotificationLookup from './notificationLookup/notificationLookup.jsx'; +import NotificationAdd from './notificationAdd/notificationAdd.jsx'; const NotificationUtils = ()=>{ return ( @@ -12,4 +11,4 @@ const NotificationUtils = ()=>{ ); }; -module.exports = NotificationUtils; +export default NotificationUtils; diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/autocompleteEmoji.js index ae7f2628f..d5a3a71aa 100644 --- a/client/components/codeEditor/autocompleteEmoji.js +++ b/client/components/codeEditor/autocompleteEmoji.js @@ -79,6 +79,6 @@ const showAutocompleteEmoji = function(CodeMirror, editor) { }); }; -module.exports = { +export default { showAutocompleteEmoji }; \ No newline at end of file diff --git a/client/components/codeEditor/close-tag.js b/client/components/codeEditor/close-tag.js index 728b63a5c..84cf62169 100644 --- a/client/components/codeEditor/close-tag.js +++ b/client/components/codeEditor/close-tag.js @@ -38,11 +38,11 @@ const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) { } }; -module.exports = { +export default { autoCloseCurlyBraces : function(CodeMirror, codeMirror) { const map = { name: 'autoCloseCurlyBraces' }; map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); }; map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); }; - codeMirror.addKeyMap(map); + codeMirror?.addKeyMap(map); } }; \ No newline at end of file diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 0d402ae9d..cd140ad07 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -1,51 +1,13 @@ /* eslint-disable max-lines */ -require('./codeEditor.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -const closeTag = require('./close-tag'); -const autoCompleteEmoji = require('./autocompleteEmoji'); - +import './codeEditor.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import closeTag from './close-tag'; +import autoCompleteEmoji from './autocompleteEmoji'; let CodeMirror; -if(typeof window !== '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'); - //Autocompletion - require('codemirror/addon/hint/show-hint.js'); - - const foldPagesCode = require('./fold-pages'); - foldPagesCode.registerHomebreweryHelper(CodeMirror); - const foldCSSCode = require('./fold-css'); - foldCSSCode.registerHomebreweryHelper(CodeMirror); -} - -const CodeEditor = createClass({ +const CodeEditor = createReactClass({ displayName : 'CodeEditor', getDefaultProps : function() { return { @@ -67,23 +29,54 @@ const CodeEditor = createClass({ editor : React.createRef(null), - componentDidMount : function() { + async componentDidMount() { + CodeMirror = (await import('codemirror')).default; + this.CodeMirror = CodeMirror; + + await import('codemirror/mode/gfm/gfm.js'); + await import('codemirror/mode/css/css.js'); + await import('codemirror/mode/javascript/javascript.js'); + + // addons + await import('codemirror/addon/fold/foldcode.js'); + await import('codemirror/addon/fold/foldgutter.js'); + await import('codemirror/addon/fold/xml-fold.js'); + await import('codemirror/addon/search/search.js'); + await import('codemirror/addon/search/searchcursor.js'); + await import('codemirror/addon/search/jump-to-line.js'); + await import('codemirror/addon/search/match-highlighter.js'); + await import('codemirror/addon/search/matchesonscrollbar.js'); + await import('codemirror/addon/dialog/dialog.js'); + await import('codemirror/addon/scroll/scrollpastend.js'); + await import('codemirror/addon/edit/closetag.js'); + await import('codemirror/addon/hint/show-hint.js'); + // import 'codemirror/addon/selection/active-line.js'; + // import 'codemirror/addon/edit/trailingspace.js'; + + + // register helpers dynamically as well + const foldPagesCode = (await import('./fold-pages')).default; + const foldCSSCode = (await import('./fold-css')).default; + foldPagesCode.registerHomebreweryHelper(CodeMirror); + foldCSSCode.registerHomebreweryHelper(CodeMirror); + this.buildEditor(); - const newDoc = CodeMirror.Doc(this.props.value, this.props.language); - this.codeMirror.swapDoc(newDoc); + const newDoc = CodeMirror?.Doc(this.props.value, this.props.language); + this.codeMirror?.swapDoc(newDoc); }, + componentDidUpdate : function(prevProps) { if(prevProps.view !== this.props.view){ //view changed; swap documents let newDoc; if(!this.state.docs[this.props.view]) { - newDoc = CodeMirror.Doc(this.props.value, this.props.language); + newDoc = CodeMirror?.Doc(this.props.value, this.props.language); } else { newDoc = this.state.docs[this.props.view]; } - const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) }; + const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) }; this.setState((prevState)=>({ docs : _.merge({}, prevState.docs, oldDoc) @@ -91,17 +84,17 @@ const CodeEditor = createClass({ this.props.rerenderParent(); } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside - this.codeMirror.setValue(this.props.value); + this.codeMirror?.setValue(this.props.value); } if(this.props.enableFolding) { - this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror)); + this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror)); } else { - this.codeMirror.setOption('foldOptions', false); + this.codeMirror?.setOption('foldOptions', false); } if(prevProps.editorTheme !== this.props.editorTheme){ - this.codeMirror.setOption('theme', this.props.editorTheme); + this.codeMirror?.setOption('theme', this.props.editorTheme); } }, @@ -189,12 +182,12 @@ const CodeEditor = createClass({ closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror); autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); - // 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());}); + // 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.updateSize(); }, - // Use for GFM tabs that use common hotkeys + // Use for GFM tabs that use common hot-keys isGFM : function() { if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true; return false; @@ -220,94 +213,94 @@ const CodeEditor = createClass({ }, dedent : function () { - this.codeMirror.execCommand('indentLess'); + this.codeMirror?.execCommand('indentLess'); }, makeHeader : function (number) { if(!this.isGFM()) return; - const selection = this.codeMirror.getSelection(); + const selection = this.codeMirror?.getSelection(); const header = Array(number).fill('#').join(''); - this.codeMirror.replaceSelection(`${header} ${selection}`, 'around'); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); + this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around'); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); }, makeBold : function() { if(!this.isGFM()) return; - const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); + const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeItalic : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*'; - this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); } }, makeSuper : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^'; - this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); } }, makeSub : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeNbsp : function() { if(!this.isGFM()) return; - this.codeMirror.replaceSelection(' ', 'end'); + this.codeMirror?.replaceSelection(' ', 'end'); }, makeSpace : function() { if(!this.isGFM()) return; - const selection = this.codeMirror.getSelection(); + const selection = this.codeMirror?.getSelection(); const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; if(t){ const percent = parseInt(selection.slice(8, -4)) + 10; - this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); + this.codeMirror?.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); } else { - this.codeMirror.replaceSelection(`{{width:10% }}`, 'around'); + this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around'); } }, removeSpace : function() { if(!this.isGFM()) return; - const selection = this.codeMirror.getSelection(); + const selection = this.codeMirror?.getSelection(); const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; if(t){ const percent = parseInt(selection.slice(8, -4)) - 10; - this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); + this.codeMirror?.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); } }, newColumn : function() { if(!this.isGFM()) return; - this.codeMirror.replaceSelection('\n\\column\n\n', 'end'); + this.codeMirror?.replaceSelection('\n\\column\n\n', 'end'); }, newPage : function() { if(!this.isGFM()) return; - this.codeMirror.replaceSelection('\n\\page\n\n', 'end'); + this.codeMirror?.replaceSelection('\n\\page\n\n', 'end'); }, injectText : function(injectText, overwrite=true) { @@ -322,30 +315,30 @@ const CodeEditor = createClass({ makeUnderline : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '' && selection.slice(-4) === ''; - this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `${selection}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `${selection}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); } }, makeSpan : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeDiv : function() { if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection } }, @@ -353,7 +346,7 @@ const CodeEditor = createClass({ let regex; let cursorPos; let newComment; - const selection = this.codeMirror.getSelection(); + const selection = this.codeMirror?.getSelection(); if(this.isGFM()){ regex = /^\s*()\s*$/gs; cursorPos = 4; @@ -363,46 +356,46 @@ const CodeEditor = createClass({ cursorPos = 3; newComment = `/* ${selection} */`; } - this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); + this.codeMirror?.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); }; }, makeLink : function() { if(!this.isGFM()) return; const isLink = /^\[(.*)\]\((.*)\)$/; - const selection = this.codeMirror.getSelection().trim(); + const selection = this.codeMirror?.getSelection().trim(); let match; if(match = isLink.exec(selection)){ const altText = match[1]; const url = match[2]; - this.codeMirror.replaceSelection(`${altText} ${url}`); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); + this.codeMirror?.replaceSelection(`${altText} ${url}`); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); } else { - this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); + this.codeMirror?.replaceSelection(`[${selection || 'alt text'}](url)`); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); } }, makeList : function(listType) { if(!this.isGFM()) return; const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to'); - this.codeMirror.setSelection( + this.codeMirror?.setSelection( { line: selectionStart.line, ch: 0 }, - { line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length } + { line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length } ); - const newSelection = this.codeMirror.getSelection(); + const newSelection = this.codeMirror?.getSelection(); const regex = /^\d+\.\s|^-\s/gm; if(newSelection.match(regex) != null){ // if selection IS A LIST - this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around'); + this.codeMirror?.replaceSelection(newSelection.replace(regex, ''), 'around'); } else { // if selection IS NOT A LIST - listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : - this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{ + listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : + this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{ let n = 1; return ()=>{ return `${n++}. `; @@ -412,39 +405,39 @@ const CodeEditor = createClass({ }, foldAllCode : function() { - this.codeMirror.execCommand('foldAll'); + this.codeMirror?.execCommand('foldAll'); }, unfoldAllCode : function() { - this.codeMirror.execCommand('unfoldAll'); + this.codeMirror?.execCommand('unfoldAll'); }, //=-- Externally used -==// setCursorPosition : function(line, char){ setTimeout(()=>{ - this.codeMirror.focus(); - this.codeMirror.doc.setCursor(line, char); + this.codeMirror?.focus(); + this.codeMirror?.doc.setCursor(line, char); }, 10); }, getCursorPosition : function(){ - return this.codeMirror.getCursor(); + return this.codeMirror?.getCursor(); }, getTopVisibleLine : function(){ - const rect = this.codeMirror.getWrapperElement().getBoundingClientRect(); - const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window'); + const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect(); + const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window'); return topVisibleLine; }, updateSize : function(){ - this.codeMirror.refresh(); + this.codeMirror?.refresh(); }, redo : function(){ - return this.codeMirror.redo(); + return this.codeMirror?.redo(); }, undo : function(){ - return this.codeMirror.undo(); + return this.codeMirror?.undo(); }, historySize : function(){ - return this.codeMirror.doc.historySize(); + return this.codeMirror?.doc.historySize(); }, foldOptions : function(cm){ @@ -458,7 +451,7 @@ const CodeEditor = createClass({ let foldPreviewText = ''; while (currentLine <= to.line && text.length <= maxLength) { - const currentText = this.codeMirror.getLine(currentLine); + const currentText = this.codeMirror?.getLine(currentLine); currentLine++; if(currentText[0] == '#'){ foldPreviewText = currentText; @@ -493,5 +486,5 @@ const CodeEditor = createClass({ } }); -module.exports = CodeEditor; +export default CodeEditor; diff --git a/client/components/codeEditor/fold-css.js b/client/components/codeEditor/fold-css.js index 338cab176..06bfd96a4 100644 --- a/client/components/codeEditor/fold-css.js +++ b/client/components/codeEditor/fold-css.js @@ -1,4 +1,4 @@ -module.exports = { +export default { registerHomebreweryHelper : function(CodeMirror) { CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) { diff --git a/client/components/codeEditor/fold-pages.js b/client/components/codeEditor/fold-pages.js index cff1c64e0..1d8d19f6b 100644 --- a/client/components/codeEditor/fold-pages.js +++ b/client/components/codeEditor/fold-pages.js @@ -1,4 +1,4 @@ -module.exports = { +export default { registerHomebreweryHelper : function(CodeMirror) { CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) { const matcher = /^\\page.*/; diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index ae9f1d7f8..16122eafd 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -require('./combobox.less'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import './combobox.less'; -const Combobox = createClass({ +const Combobox = createReactClass({ displayName : 'Combobox', getDefaultProps : function() { return { @@ -16,6 +16,7 @@ const Combobox = createClass({ suggestMethod : 'includes', filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, + valuePatterns: /.+/ }; }, getInitialState : function() { @@ -74,6 +75,7 @@ const Combobox = createClass({ type='text' onChange={(e)=>this.handleInput(e)} value={this.state.value || ''} + pattern={this.props.valuePatterns} placeholder={this.props.placeholder} onBlur={(e)=>{ if(!e.target.checkValidity()){ @@ -82,6 +84,12 @@ const Combobox = createClass({ }); } }} + onKeyDown={(e)=>{ + if (e.key === "Enter") { + e.preventDefault(); + this.props.onEntry(e); + } + }} /> @@ -126,4 +134,4 @@ const Combobox = createClass({ } }); -module.exports = Combobox; +export default Combobox; diff --git a/client/components/renderWarnings/renderWarnings.jsx b/client/components/renderWarnings/renderWarnings.jsx index 6b8ea8184..75fdb8ff4 100644 --- a/client/components/renderWarnings/renderWarnings.jsx +++ b/client/components/renderWarnings/renderWarnings.jsx @@ -1,11 +1,11 @@ -require('./renderWarnings.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); +import './renderWarnings.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; import Dialog from '../dialog.jsx'; -const RenderWarnings = createClass({ +const RenderWarnings = createReactClass({ displayName : 'RenderWarnings', getInitialState : function() { return { @@ -57,4 +57,4 @@ const RenderWarnings = createClass({ } }); -module.exports = RenderWarnings; +export default RenderWarnings; diff --git a/client/components/splitPane/splitPane.jsx b/client/components/splitPane/splitPane.jsx index 78ba59ed3..7cbfe2066 100644 --- a/client/components/splitPane/splitPane.jsx +++ b/client/components/splitPane/splitPane.jsx @@ -1,6 +1,5 @@ -require('./splitPane.less'); -const React = require('react'); -const { useState, useEffect } = React; +import './splitPane.less'; +import React, { useEffect, useState } from 'react'; const PANE_WIDTH_KEY = 'HB_editor_splitWidth'; const LIVE_SCROLL_KEY = 'HB_editor_liveScroll'; @@ -108,4 +107,4 @@ const Pane = ({ width, children, isDragging, moveBrew, moveSource, liveScroll, s ); }; -module.exports = SplitPane; +export default SplitPane; diff --git a/client/components/svg/cauldron.svg.jsx b/client/components/svg/cauldron.svg.jsx index 49405783b..c396dd05e 100644 --- a/client/components/svg/cauldron.svg.jsx +++ b/client/components/svg/cauldron.svg.jsx @@ -1,7 +1,6 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; -module.exports = function(props){ +export default function(props){ return diff --git a/client/components/svg/naturalcrit-d20.svg.jsx b/client/components/svg/naturalcrit-d20.svg.jsx index 1482b5559..9109d6e91 100644 --- a/client/components/svg/naturalcrit-d20.svg.jsx +++ b/client/components/svg/naturalcrit-d20.svg.jsx @@ -1,6 +1,5 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; -module.exports = function(props){ +export default function(props){ return ; }; \ No newline at end of file diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 7e6681c57..771a6aa31 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -1,20 +1,19 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ -require('./brewRenderer.less'); -const React = require('react'); -const { useState, useRef, useMemo, useEffect } = React; -const _ = require('lodash'); +import './brewRenderer.less'; +import React, { useState, useRef, useMemo, useEffect } from 'react'; +import _ from 'lodash'; -const MarkdownLegacy = require('markdownLegacy.js'); -import Markdown from 'markdown.js'; -const ErrorBar = require('./errorBar/errorBar.jsx'); -const ToolBar = require('./toolBar/toolBar.jsx'); +import MarkdownLegacy from '../../../shared/markdownLegacy.js'; +import Markdown from '../../../shared/markdown.js'; +import ErrorBar from './errorBar/errorBar.jsx'; +import ToolBar from './toolBar/toolBar.jsx'; //TODO: move to the brew renderer -const RenderWarnings = require('client/components/renderWarnings/renderWarnings.jsx'); -const NotificationPopup = require('./notificationPopup/notificationPopup.jsx'); -const Frame = require('react-frame-component').default; -const dedent = require('dedent-tabs').default; -const { printCurrentBrew } = require('../../../shared/helpers.js'); +import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx'; +import NotificationPopup from './notificationPopup/notificationPopup.jsx'; +import Frame from 'react-frame-component'; +import dedent from 'dedent'; +import { printCurrentBrew } from '../../../shared/helpers.js'; import HeaderNav from './headerNav/headerNav.jsx'; import { safeHTML } from './safeHTML.js'; @@ -345,4 +344,4 @@ const BrewRenderer = (props)=>{ ); }; -module.exports = BrewRenderer; +export default BrewRenderer; diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index bb4fe69c5..5769a13df 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -59,6 +59,12 @@ } &-corner { visibility : hidden; } } + + @supports (break-after:always) { + .columnSplit { + margin-bottom: 100vh; + } + } } .pane { position : relative; } @@ -81,4 +87,5 @@ } } .headerNav { visibility : hidden; } -} \ No newline at end of file + +} diff --git a/client/homebrew/brewRenderer/errorBar/errorBar.jsx b/client/homebrew/brewRenderer/errorBar/errorBar.jsx index 78b36d70c..b4522d759 100644 --- a/client/homebrew/brewRenderer/errorBar/errorBar.jsx +++ b/client/homebrew/brewRenderer/errorBar/errorBar.jsx @@ -1,5 +1,5 @@ -require('./errorBar.less'); -const React = require('react'); +import './errorBar.less'; +import React from 'react'; import Dialog from '../../../components/dialog.jsx'; @@ -50,4 +50,4 @@ const ErrorBar = (props)=>{ ); }; -module.exports = ErrorBar; +export default ErrorBar; diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.jsx b/client/homebrew/brewRenderer/headerNav/headerNav.jsx index 04ced2585..3b184aff0 100644 --- a/client/homebrew/brewRenderer/headerNav/headerNav.jsx +++ b/client/homebrew/brewRenderer/headerNav/headerNav.jsx @@ -1,7 +1,7 @@ -require('./headerNav.less'); +import './headerNav.less'; -import * as React from 'react'; -import * as _ from 'lodash'; +import React from 'react'; +import _ from 'lodash'; const MAX_TEXT_LENGTH = 40; diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx index ad8f9140a..dd05391f0 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -1,4 +1,4 @@ -require('./notificationPopup.less'); +import './notificationPopup.less'; import React, { useEffect, useState } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'markdown.js'; @@ -62,4 +62,4 @@ const NotificationPopup = ()=>{ ; }; -module.exports = NotificationPopup; +export default NotificationPopup; diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index 4aee3b6bd..be0842f33 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -1,8 +1,7 @@ /* eslint-disable max-lines */ -require('./toolBar.less'); -const React = require('react'); -const { useState, useEffect } = React; -const _ = require('lodash'); +import './toolBar.less'; +import React, { useState, useEffect } from 'react'; +import _ from 'lodash'; import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; @@ -259,4 +258,4 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa ); }; -module.exports = ToolBar; +export default ToolBar; diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 7bc37145d..9707fe84f 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -1,14 +1,14 @@ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ -require('./editor.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -const dedent = require('dedent-tabs').default; +import './editor.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import dedent from 'dedent'; import Markdown from '../../../shared/markdown.js'; -const CodeEditor = require('client/components/codeEditor/codeEditor.jsx'); -const SnippetBar = require('./snippetbar/snippetbar.jsx'); -const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); +import CodeEditor from '../../components/codeEditor/codeEditor.jsx'; +import SnippetBar from './snippetbar/snippetbar.jsx'; +import MetadataEditor from './metadataEditor/metadataEditor.jsx'; const EDITOR_THEME_KEY = 'HB_editor_theme'; @@ -31,7 +31,7 @@ const DEFAULT_SNIPPET_TEXT = dedent` `; let isJumping = false; -const Editor = createClass({ +const Editor = createReactClass({ displayName : 'Editor', getDefaultProps : function() { return { @@ -76,8 +76,8 @@ const Editor = createClass({ document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys); - this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());}); - this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); + this.codeEditor.current.codeMirror?.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());}); + this.codeEditor.current.codeMirror?.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); if(editorTheme) { @@ -86,9 +86,9 @@ const Editor = createClass({ }); } const snippetBar = document.querySelector('.editor > .snippetBar'); - if (!snippetBar) return; + if(!snippetBar) return; - this.resizeObserver = new ResizeObserver(entries => { + this.resizeObserver = new ResizeObserver(entries=>{ const height = document.querySelector('.editor > .snippetBar').offsetHeight; this.setState({ snippetBarHeight: height }); }); @@ -117,7 +117,7 @@ const Editor = createClass({ }, componentWillUnmount() { - if (this.resizeObserver) this.resizeObserver.disconnect(); + if(this.resizeObserver) this.resizeObserver.disconnect(); }, handleControlKeys : function(e){ @@ -156,21 +156,21 @@ const Editor = createClass({ this.setState({ view : newView }, ()=>{ - this.codeEditor.current?.codeMirror.focus(); + this.codeEditor.current?.codeMirror?.focus(); }); }, highlightCustomMarkdown : function(){ - if(!this.codeEditor.current) return; + if(!this.codeEditor.current?.codeMirror) return; if((this.state.view === 'text') ||(this.state.view === 'snippet')) { const codeMirror = this.codeEditor.current.codeMirror; - codeMirror.operation(()=>{ // Batch CodeMirror styling + codeMirror?.operation(()=>{ // Batch CodeMirror styling const foldLines = []; //reset custom text styles - const customHighlights = codeMirror.getAllMarks().filter((mark)=>{ + const customHighlights = codeMirror?.getAllMarks().filter((mark)=>{ // Record details of folded sections if(mark.__isFold) { const fold = mark.find(); @@ -191,10 +191,10 @@ const Editor = createClass({ const textOrSnip = this.state.view === 'text'; //reset custom line styles - codeMirror.removeLineClass(lineNumber, 'background', 'pageLine'); - codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine'); - codeMirror.removeLineClass(lineNumber, 'text'); - codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); + codeMirror?.removeLineClass(lineNumber, 'background', 'pageLine'); + codeMirror?.removeLineClass(lineNumber, 'background', 'snippetLine'); + codeMirror?.removeLineClass(lineNumber, 'text'); + codeMirror?.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); // Don't process lines inside folded text // If the current lineNumber is inside any folded marks, skip line styling @@ -210,19 +210,19 @@ const Editor = createClass({ else if(this.state.view !== 'text') userSnippetCount += 1; // add back the original class 'background' but also add the new class '.pageline' - codeMirror.addLineClass(lineNumber, 'background', tabHighlight); + codeMirror?.addLineClass(lineNumber, 'background', tabHighlight); const pageCountElement = Object.assign(document.createElement('span'), { className : 'editor-page-count', textContent : textOrSnip ? editorPageCount : userSnippetCount }); - codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); + codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); }; - // New Codemirror styling for V3 renderer + // New CodeMirror styling for V3 renderer if(this.props.renderer === 'V3') { if(line.match(/^\\column(?:break)?$/)){ - codeMirror.addLineClass(lineNumber, 'text', 'columnSplit'); + codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit'); } // definition lists @@ -231,14 +231,14 @@ const Editor = createClass({ const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error. let match; while ((match = regex.exec(line)) != null){ - codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' }); - codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' }); - codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' }); + codeMirror?.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' }); + codeMirror?.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' }); + codeMirror?.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' }); const ddIndex = match.indices[2][0]; const colons = /::/g; const colonMatches = colons.exec(match[2]); if(colonMatches !== null){ - codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' }); + codeMirror?.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' }); } } } @@ -255,7 +255,7 @@ const Editor = createClass({ const match = subRegex.exec(line) || superRegex.exec(line); if(match) { isSuper = !subRegex.lastIndex; - codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' }); + codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' }); } startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex)); } @@ -266,7 +266,7 @@ const Editor = createClass({ const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm; let match; while ((match = regex.exec(line)) != null) { - codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' }); + codeMirror?.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' }); } } // Highlight inline spans {{content}} @@ -284,7 +284,7 @@ const Editor = createClass({ blockCount = 0; continue; } - codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' }); + codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' }); } } else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){ // Highlight block divs {{\n Content \n}} @@ -293,7 +293,7 @@ const Editor = createClass({ const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/); if(match) endCh = match.index+match[0].length; - codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' }); + codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' }); } // Emojis @@ -314,11 +314,11 @@ const Editor = createClass({ const endPos = { line: lineNumber, ch: match.index + match[0].length }; // Iterate over conflicting marks and clear them - const marks = codeMirror.findMarks(startPos, endPos); + const marks = codeMirror?.findMarks(startPos, endPos); marks.forEach(function(marker) { if(!marker.__isFold) marker.clear(); }); - codeMirror.markText(startPos, endPos, { className: 'emoji' }); + codeMirror?.markText(startPos, endPos, { className: 'emoji' }); } startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex)); } @@ -337,7 +337,7 @@ const Editor = createClass({ const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const currentPos = brewRenderer.scrollTop; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; - + let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs @@ -378,44 +378,46 @@ const Editor = createClass({ const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit); const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1; - let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; - let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); + let currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top; + let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true); let scrollingTimeout; const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs scrollingTimeout = setTimeout(()=>{ isJumping = false; - this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete); + this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete); }, 150); // If 150 ms pass without a scroll event, assume scrolling is done }; isJumping = true; checkIfScrollComplete(); - this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete); + if(this.codeEditor.current?.codeMirror) { + this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete); + } if(smooth) { //Scroll 1/10 of the way every 10ms until 1px off. const incrementalScroll = setInterval(()=>{ currentY += (targetY - currentY) / 10; - this.codeEditor.current.codeMirror.scrollTo(null, currentY); + this.codeEditor.current.codeMirror?.scrollTo(null, currentY); // Update target: target height is not accurate until within +-10 lines of the visible window if(Math.abs(targetY - currentY > 100)) - targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); + targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true); // End when close enough if(Math.abs(targetY - currentY) < 1) { - this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference + this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); - this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); + this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); clearInterval(incrementalScroll); } }, 10); } else { - this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference + this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); - this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); + this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); } }, @@ -545,4 +547,4 @@ const Editor = createClass({ } }); -module.exports = Editor; +export default Editor; diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 338ad9360..721340079 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -1,19 +1,16 @@ /* eslint-disable max-lines */ -require('./metadataEditor.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); +import './metadataEditor.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; import request from '../../utils/request-middleware.js'; -const Combobox = require('client/components/combobox.jsx'); -const TagInput = require('../tagInput/tagInput.jsx'); +import Combobox from '../../../components/combobox.jsx'; +import TagInput from '../tagInput/tagInput.jsx'; +import Themes from 'themes/themes.json'; +import validations from './validations.js'; -const Themes = require('themes/themes.json'); -const validations = require('./validations.js'); - -const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; - -const homebreweryThumbnail = require('../../thumbnail.png'); +import homebreweryThumbnail from '../../thumbnail.png'; const callIfExists = (val, fn, ...args)=>{ if(val[fn]) { @@ -21,7 +18,7 @@ const callIfExists = (val, fn, ...args)=>{ } }; -const MetadataEditor = createClass({ +const MetadataEditor = createReactClass({ displayName : 'MetadataEditor', getDefaultProps : function() { return { @@ -34,7 +31,6 @@ const MetadataEditor = createClass({ tags : [], published : false, authors : [], - systems : [], renderer : 'legacy', theme : '5ePHB', lang : 'en' @@ -92,15 +88,6 @@ const MetadataEditor = createClass({ } }, - handleSystem : function(system, e){ - if(e.target.checked){ - this.props.metadata.systems.push(system); - } else { - this.props.metadata.systems = _.without(this.props.metadata.systems, system); - } - this.props.onChange(this.props.metadata); - }, - handleRenderer : function(renderer, e){ if(e.target.checked){ this.props.metadata.renderer = renderer; @@ -156,18 +143,6 @@ const MetadataEditor = createClass({ }); }, - renderSystems : function(){ - return _.map(SYSTEMS, (val)=>{ - return ; - }); - }, - renderPublish : function(){ if(this.props.metadata.published){ return
+ return
- this.handleFieldChange('tags', e)} /> -
- -
- {this.renderSystems()} -
-
- {this.renderLanguageDropdown()} {this.renderThemeDropdown()} @@ -387,11 +358,13 @@ const MetadataEditor = createClass({ {this.renderAuthors()} - !this.props.metadata.authors?.includes(v)]} placeholder='invite author' unique={true} values={this.props.metadata.invitedAuthors} - notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} + smallText='Invited author usernames are case sensitive. After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.' onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} /> @@ -411,4 +384,4 @@ const MetadataEditor = createClass({ } }); -module.exports = MetadataEditor; +export default MetadataEditor; diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less index fd04f07d9..304b85d30 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/metadataEditor/metadataEditor.less @@ -114,6 +114,11 @@ z-index : 200; max-width : 150px; } + + &.tags .tagInput-dropdown { + z-index : 201; + max-width : 200px; + } } @@ -129,7 +134,7 @@ background-color : #AAAAAA; } - .systems.field .value { + .renderers.field .value { label { display : inline-flex; align-items : center; diff --git a/client/homebrew/editor/metadataEditor/validations.js b/client/homebrew/editor/metadataEditor/validations.js index d0e052b07..2430534dc 100644 --- a/client/homebrew/editor/metadataEditor/validations.js +++ b/client/homebrew/editor/metadataEditor/validations.js @@ -1,4 +1,4 @@ -module.exports = { +export default { title : [ (value)=>{ return value?.length > 100 ? 'Max title length of 100 characters' : null; diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 5e2051a86..b6e977ea2 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,29 +1,36 @@ /*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ -require('./snippetbar.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -const cx = require('classnames'); +import './snippetbar.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; + +import _ from 'lodash'; +import cx from 'classnames'; import { loadHistory } from '../../utils/versionHistory.js'; import { brewSnippetsToJSON } from '../../../../shared/helpers.js'; -//Import all themes -const ThemeSnippets = {}; -ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); -ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js'); -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'); +import Legacy5ePHB from 'themes/Legacy/5ePHB/snippets.js'; +import V3_5ePHB from 'themes/V3/5ePHB/snippets.js'; +import V3_5eDMG from 'themes/V3/5eDMG/snippets.js'; +import V3_Journal from 'themes/V3/Journal/snippets.js'; +import V3_Blank from 'themes/V3/Blank/snippets.js'; -const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json'); +const ThemeSnippets = { + Legacy_5ePHB : Legacy5ePHB, + V3_5ePHB : V3_5ePHB, + V3_5eDMG : V3_5eDMG, + V3_Journal : V3_Journal, + V3_Blank : V3_Blank, +}; + +import EditorThemes from 'build/homebrew/codeMirror/editorThemes.json'; const execute = function(val, props){ if(_.isFunction(val)) return val(props); return val; }; -const Snippetbar = createClass({ +const Snippetbar = createReactClass({ displayName : 'SnippetBar', getDefaultProps : function() { return { @@ -281,9 +288,9 @@ const Snippetbar = createClass({ } }); -module.exports = Snippetbar; +export default Snippetbar; -const SnippetGroup = createClass({ +const SnippetGroup = createReactClass({ displayName : 'SnippetGroup', getDefaultProps : function() { return { diff --git a/client/homebrew/editor/tagInput/curatedTagSuggestionList.js b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js new file mode 100644 index 000000000..d433175ef --- /dev/null +++ b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js @@ -0,0 +1,210 @@ +export default [ + // ############################## Systems + // D&D + "system:D&D Original", + "system:D&D Basic", + "system:AD&D 1e", + "system:AD&D 2e", + "system:D&D 3e", + "system:D&D 3.5e", + "system:D&D 4e", + "system:D&D 5e", + "system:D&D 5e 2024", + "system:BD&D (B/X)", + "system:D&D Essentials", + + // Other Famous RPGs + "system:Pathfinder 1e", + "system:Pathfinder 2e", + "system:Vampire: The Masquerade", + "system:Werewolf: The Apocalypse", + "system:Mage: The Ascension", + "system:Call of Cthulhu", + "system:Shadowrun", + "system:Star Wars RPG (D6/D20/Edge of the Empire)", + "system:Warhammer Fantasy Roleplay", + "system:Cyberpunk 2020", + "system:Blades in the Dark", + "system:Daggerheart", + "system:Draw Steel", + "system:Mutants and Masterminds", + + // Meta + "meta:V3", + "meta:Legacy", + "meta:Template", + "meta:Theme", + "meta:free", + "meta:Character Sheet", + "meta:Documentation", + "meta:NPC", + "meta:Guide", + "meta:Resource", + "meta:Notes", + "meta:Example", + + // Book type + "type:Campaign", + "type:Campaign Setting", + "type:Adventure", + "type:One-Shot", + "type:Setting", + "type:World", + "type:Lore", + "type:History", + "type:Dungeon Master", + "type:Encounter Pack", + "type:Encounter", + "type:Session Notes", + "type:reference", + "type:Handbook", + "type:Manual", + "type:Manuals", + "type:Compendium", + "type:Bestiary", + + // ###################################### RPG Keywords + + // Classes / Subclasses / Archetypes + "Class", + "Subclass", + "Archetype", + "Martial", + "Half-Caster", + "Full Caster", + "Artificer", + "Barbarian", + "Bard", + "Cleric", + "Druid", + "Fighter", + "Monk", + "Paladin", + "Rogue", + "Sorcerer", + "Warlock", + "Wizard", + + // Races / Species / Lineages + "Race", + "Ancestry", + "Lineage", + "Aasimar", + "Beastfolk", + "Dragonborn", + "Dwarf", + "Elf", + "Goblin", + "Half-Elf", + "Half-Orc", + "Human", + "Kobold", + "Lizardfolk", + "Lycan", + "Orc", + "Tiefling", + "Vampire", + "Yuan-Ti", + + // Magic / Spells / Items + "Magic", + "Magic Item", + "Magic Items", + "Wondrous Item", + "Magic Weapon", + "Artifact", + "Spell", + "Spells", + "Cantrip", + "Cantrips", + "Eldritch", + "Eldritch Invocation", + "Invocation", + "Invocations", + "Pact boon", + "Pact Boon", + "Spellcaster", + "Spellblade", + "Magical Tattoos", + "Enchantment", + "Enchanted", + "Attunement", + "Requires Attunement", + "Rune", + "Runes", + "Wand", + "Rod", + "Scroll", + "Potion", + "Potions", + "Item", + "Items", + "Bag of Holding", + + // Monsters / Creatures / Enemies + "Monster", + "Creatures", + "Creature", + "Beast", + "Beasts", + "Humanoid", + "Undead", + "Fiend", + "Aberration", + "Ooze", + "Giant", + "Dragon", + "Monstrosity", + "Demon", + "Devil", + "Elemental", + "Construct", + "Constructs", + "Boss", + "BBEG", + + // ############################# Media / Pop Culture + "One Piece", + "Dragon Ball", + "Dragon Ball Z", + "Naruto", + "Jujutsu Kaisen", + "Fairy Tail", + "Final Fantasy", + "Kingdom Hearts", + "Elder Scrolls", + "Skyrim", + "WoW", + "World of Warcraft", + "Marvel Comics", + "DC Comics", + "Pokemon", + "League of Legends", + "Runeterra", + "Arcane", + "Yu-Gi-Oh", + "Minecraft", + "Don't Starve", + "Witcher", + "Witcher 3", + "Cyberpunk", + "Cyberpunk 2077", + "Fallout", + "Divinity Original Sin 2", + "Fullmetal Alchemist", + "Fullmetal Alchemist Brotherhood", + "Lobotomy Corporation", + "Bloodborne", + "Dragonlance", + "Shackled City Adventure Path", + "Baldurs Gate 3", + "Library of Ruina", + "Radiant Citadel", + "Ravenloft", + "Forgotten Realms", + "Exandria", + "Critical Role", + "Star Wars", + "SW5e", + "Star Wars 5e", +]; diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index d60e23b1b..9f7f71498 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,105 +1,213 @@ -require('./tagInput.less'); -const React = require('react'); -const { useState, useEffect } = React; -const _ = require('lodash'); +import './tagInput.less'; +import React, { useState, useEffect } from 'react'; +import Combobox from '../../../components/combobox.jsx'; -const TagInput = ({ unique = true, values = [], ...props })=>{ - const [tempInputText, setTempInputText] = useState(''); - const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false }))); +import tagSuggestionList from './curatedTagSuggestionList.js'; + +const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{ + const [tagList, setTagList] = useState( + values.map((value)=>({ + value, + editing : false, + draft : '', + })), + ); useEffect(()=>{ - handleChange(tagList.map((context)=>context.value)); + const incoming = values || []; + const current = tagList.map((t)=>t.value); + + const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]); + + if(changed) { + setTagList( + incoming.map((value)=>({ + value, + editing : false, + })), + ); + } + }, [values]); + + useEffect(()=>{ + onChange?.({ + target : { value: tagList.map((t)=>t.value) }, + }); }, [tagList]); - const handleChange = (value)=>{ - props.onChange({ - target : { value } - }); - }; + // substrings to be normalized to the first value on the array + const duplicateGroups = [ + ['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'], + ['5e', '5th Edition'], + ['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'], + ['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'], + ['P2e', 'p2e', 'P2E', 'Pathfinder 2e'], + ]; - const handleInputKeyDown = ({ evt, value, index, options = {} })=>{ - if(_.includes(['Enter', ','], evt.key)) { - evt.preventDefault(); - submitTag(evt.target.value, value, index); - if(options.clear) { - setTempInputText(''); + const normalizeValue = (input)=>{ + const lowerInput = input.toLowerCase(); + let normalizedTag = input; + + for (const group of duplicateGroups) { + for (const tag of group) { + if(!tag) continue; + + const index = lowerInput.indexOf(tag.toLowerCase()); + if(index !== -1) { + normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length); + break; + } } } + + if(normalizedTag.includes(':')) { + const [rawType, rawValue = ''] = normalizedTag.split(':'); + const tagType = rawType.trim().toLowerCase(); + const tagValue = rawValue.trim(); + + if(tagValue.length > 0) { + normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`; + } + //trims spaces around colon and capitalizes the first word after the colon + //this is preferred to users not understanding they can't put spaces in + } + + return normalizedTag; }; - const submitTag = (newValue, originalValue, index)=>{ - setTagList((prevContext)=>{ - // remove existing tag - if(newValue === null){ - return [...prevContext].filter((context, i)=>i !== index); + const submitTag = (newValue, index = null)=>{ + const trimmed = newValue?.trim(); + if(!trimmed) return; + if(!valuePatterns.test(trimmed)) return; + + const normalizedTag = normalizeValue(trimmed); + + setTagList((prev)=>{ + const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase()); + if(unique && existsIndex !== -1) return prev; + if(index !== null) { + return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t)); } - // add new tag - if(originalValue === null){ - return [...prevContext, { value: newValue, editing: false }]; - } - // update existing tag - return prevContext.map((context, i)=>{ - if(i === index) { - return { ...context, value: newValue, editing: false }; - } - return context; - }); + + return [...prev, { value: normalizedTag, editing: false }]; }); }; + const removeTag = (index)=>{ + setTagList((prev)=>prev.filter((_, i)=>i !== index)); + }; + const editTag = (index)=>{ - setTagList((prevContext)=>{ - return prevContext.map((context, i)=>{ - if(i === index) { - return { ...context, editing: true }; - } - return { ...context, editing: false }; - }); - }); + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t))); }; - const renderReadTag = (context, index)=>{ - return ( -
  • editTag(index)}> - {context.value} - -
  • - ); + const stopEditing = (index)=>{ + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t))); }; - const renderWriteTag = (context, index)=>{ + const suggestionOptions = tagSuggestionList.map((tag)=>{ + const tagType = tag.split(':'); + + let classes = 'item'; + switch (tagType[0]) { + case 'type': + classes = 'item type'; + break; + case 'group': + classes = 'item group'; + break; + case 'meta': + classes = 'item meta'; + break; + case 'system': + classes = 'item system'; + break; + default: + classes = 'item'; + break; + } + return ( - handleInputKeyDown({ evt, value: context.value, index: index })} - autoFocus - /> +
    + {tag} +
    ); - }; + }); return ( -
    - +
    + {label && } +
      - {tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} + {tagList.map((t, i)=>t.editing ? ( + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)), + ) + } + onKeyDown={(e)=>{ + if(e.key === 'Enter') { + e.preventDefault(); + submitTag(t.draft, i); // submit draft + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)), + ); + } + if(e.key === 'Escape') { + stopEditing(i); + e.target.blur(); + } + }} + autoFocus + /> + ) : ( +
    • editTag(i)}> + {t.value} + +
    • + ), + )}
    - setTempInputText(e.target.value)} - onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })} + submitTag(value)} + onEntry={(e)=>{ + if(e.key === 'Enter') { + console.log('submit'); + e.preventDefault(); + submitTag(e.target.value); + } + }} /> + {smallText.length !== 0 && {smallText}}
    ); }; -module.exports = TagInput; +export default TagInput; diff --git a/client/homebrew/editor/tagInput/tagInput.less b/client/homebrew/editor/tagInput/tagInput.less index e69de29bb..3165b3935 100644 --- a/client/homebrew/editor/tagInput/tagInput.less +++ b/client/homebrew/editor/tagInput/tagInput.less @@ -0,0 +1,22 @@ +.list input { + border-radius: 5px; +} + +.tagInput-dropdown { + .dropdown-options { + .item { + &.type { + background-color: #00800035; + } + &.group { + background-color: #50505035; + } + &.meta { + background-color: #00008035; + } + &.system { + background-color: #80000035; + } + } + } +} diff --git a/client/homebrew/editor/tagInput/tagSuggestionList.js b/client/homebrew/editor/tagInput/tagSuggestionList.js new file mode 100644 index 000000000..6b8e4060e --- /dev/null +++ b/client/homebrew/editor/tagInput/tagSuggestionList.js @@ -0,0 +1,1980 @@ +export default [ + "meta:Theme", + "5e", + "Subclass", + "meta:theme", + "subclass", + "Class", + "Homebrew", + "Race", + "Dungeons and Dragons", + "theme", + "Daggerheart", + "2024", + "One Piece", + "One Piece DND", + "Luffy", + "Dungeons and Devil Fruits", + "Strawhats", + "Template", + "Campaign Frame", + "class", + "Players Handbook", + "dnd", + "osr", + "Dungeon Masters Guide", + "shadowdark", + "dragonbane", + "PHB", + "example", + "Devil Fruits", + "system:pf2e", + "DnD", + "DMG", + "system:dnd5.5", + "Monster", + "homebrew", + "race", + "template", + "Warlock", + "monster", + "Fighter", + "warlock", + "druid", + "sorcerer", + "D&D", + "Magic Item", + "Barbarian", + "Artificer", + "2014", + "system:descent into avernus", + "Sorcerer", + "Adventure", + "Paladin", + "Ranger", + "user help", + "fighter", + "5th Edition", + "Spells", + "Monk", + "Spell", + "NPC", + "Cleric", + "spell", + "Rogue", + "css", + "Item", + "artificer", + "magic item", + "Rules", + "barbarian", + "wizard", + "russian", + "DnD5e", + "Wizard", + "paladin", + "bastionland", + "spells", + "Devil Fruit", + "Bard", + "5.5e", + "rogue", + "Tabletop System", + "Haki", + "Druid", + "mystic bastionland", + "item", + "Lore", + "bard", + "monk", + "system:dnd5e", + "world", + "ranger", + "WIP", + "cleric", + "Dragon", + "Naruto", + "Creature", + "snippet", + "npc", + "DeS", + "Magic", + "guide", + "v3", + "Beast", + "Classe", + "onering", + "Monsters", + "Races", + "Weapon", + "adventure", + "Subclasses", + "stat block", + "weapon", + "Species", + "DONE", + "archetype", + "RPG", + "Hollow Knight", + "5e'24", + "Martial", + "DND", + "Classe Nova", + "Curse of Strahd", + "Boss", + "Hollowed Kingdoms", + "baldurs mouth", + "5.24", + "Homewbrew", + "Encyclopedia", + "Revised", + "OneWorldHD", + "knight", + "DPS", + "srd", + "Undead", + "items", + "DnD 5e", + "Guide", + "Compendium", + "Feat", + "newspaper", + "magic", + "TTRPG", + "descent into avernus", + "reference", + "system:D&D 5e24", + "feat", + "Magic Items", + "Campaign", + "resource", + "Feats", + "Anime", + "dd5", + "races", + "Monstrosity", + "DM Screen", + "2024 Rules", + "Rework", + "Character Build", + "Done", + "5e 2024", + "Construct", + "myth", + "magic items", + "creature", + "Legendary", + "Strahd", + "background", + "Player", + "style", + "Legacy", + "Player Handbook", + "martial", + "Character", + "Dungeons & Dragons", + "Table", + "reddit", + "monsters", + "OneDND", + "dragon", + "Suporte", + "Soulbound", + "Expanded Handbook", + "bestiary", + "Humanoid", + "system:dnd", + "Oredell", + "system:class", + "V3", + "system:5e", + "5e'14", + "Age of Sigmar", + "Items", + "Eberron", + "horror", + "dnd5e", + "star wars", + "Background", + "compendium", + "revision", + "Elemental", + "character", + "Marcial", + "SW5e", + "notes", + "DM", + "Fey", + "one-shot", + "resources", + "NEW", + "campaign", + "wondrous item", + "Setting", + "Archive", + "NIMRE", + "Tanque", + "Jogavel", + "Dnd 5e", + "LotM", + "setting", + "revised", + "Warhammer", + "rework", + "Conjurador", + "GMBinder", + "ItemSet", + "NPCs", + "classes", + "CR 2", + "AetherSail", + "undead", + "Artifact", + "cards", + "Example", + "Guides", + "Theme", + "Swamp", + "CR 1", + "patron", + "Extra", + "concept", + "final fantasy", + "Aberration", + "Elder Scrolls", + "rules", + "meta:gratis", + "Dice Pool", + "Cue", + "dark", + "type:Style", + "rpg", + "meta:free", + "summoner", + "OC", + "rare", + "Fleshing Out", + "francais", + "Dnd", + "Creatures", + "Sims 4", + "sous-classe", + "ffxiv", + "Underdark", + "add-on", + "weapons", + "Subrace", + "dungeons and dragons", + "Naruto 5e", + "Lafari", + "Very Rare", + "d6", + "red", + "Equipment", + "CR 3", + "Patron", + "5E", + "objet magique", + "spellcaster", + "feats", + "5.24e", + "system:d&d5e", + "Rare", + "Thudnfer", + "daggerheart", + "CR 1/2", + "Archetype", + "dnd 5e", + "Pathfinder", + "lineage", + "Celestial", + "Support", + "species", + "ARCHIVED", + "Horror", + "humanoid", + "Subclase", + "book:PHB E&E", + "final fantasy xiv", + "Jungle", + "Feywild", + "Fiend", + "subclasses", + "HB", + "Legacy Challenge", + "Project Horizon", + "vampire", + "WoW", + "DND 5e", + "Weapons", + "system:book clone", + "CoS", + "N5e", + "Summon", + "Spellcaster", + "Koretra", + "Voidborn", + "one shot", + "Templates", + "tables", + "Iphexar", + "Shattered Obelisk", + "Sword Coast", + "elemental", + "lore", + "character sheet", + "discord", + "BetterMonsters", + "players", + "group:simple skans", + "Coastal", + "Forest", + "Unearthed Arcana", + "Old", + "Collection", + "Ancestry", + "Caevash", + "Strixhaven", + "Limbus Company", + "Daydreams & Deviants", + "Statblocks", + "Urban", + "Classes", + "familiar", + "Blood", + "oneshot", + "n5e", + "wip", + "masks", + "Planeshifted", + "Carrioss", + "avernus", + "object", + "1", + "Ruins", + "Anime Character", + "wild shape", + "mask", + "dj9 game", + "Handbook", + "Curse", + "oath", + "Midralis", + "Appendix", + "5.5", + "tales of the valiant", + "player classes", + "Caster", + "Large", + "Fateforge", + "Cursed", + "beast", + "Pathfinder 2e", + "Design", + "Uncommon", + "Endeur", + "Elemental Water", + "SCC", + "sci-fi", + "Dragons", + "human", + "Gods", + "Medium", + "System", + "Help", + "Handout", + "Skyrim", + "BnB", + "draft", + "PC", + "Variant", + "syntax", + "OneShot", + "Clase", + "UESTRPG", + "Savannah", + "Reef", + "CR 5", + "Forgotten Realms", + "collection", + "Combat", + "Historia", + "artifact", + "Expansion", + "Attunement", + "Variant Rules", + "d&d", + "Party Build", + "Evocation", + "homerule", + "Forbidden West", + "how-to", + "tov", + "Mystical Item", + "CR 4", + "Necromancer", + "General Rules", + "Needs Update", + "stat blocks", + "DC Comics", + "Sword", + "Armor", + "blood hunter", + "Blood Hunter", + "Meio Conjurador", + "Mydia", + "3rd Party", + "N5E", + "Delvebound", + "Ocean", + "Subterranean", + "half-caster", + "Demon", + "Fire", + "meta:Template", + "Character Sheet", + "PF2e", + "png", + "fantasy", + "Setting Guide", + "Style", + "Cor", + "legacy", + "Minion", + "meta:khaoz age", + "Book", + "magical item", + "immersion", + "reminder cards", + "Regras", + "Durnovar", + "2025", + "Camp1", + "Abyss", + "armor", + "construct", + "firearms", + "Backgrounds", + "Companion", + "Melee", + "fey", + "Incomplete", + "Vampire", + "Bestiary", + "Worldbuilding", + "History", + "Conjuration", + "necromancy", + "dragons", + "ancestry", + "Mech", + "PbtA", + "COS", + "One Shot", + "Factions", + "Transmutation", + "Spellcasting", + "card", + "Ardh", + "redveil", + "conditions", + "elturel", + "rhye", + "group:James Haeck", + "CaelYuu", + "system:5.24e", + "system:GM Binder", + "Grassland", + "melee", + "Potions", + "anime", + "D&D 5e", + "Healer", + "Gambling", + "Lightning", + "fighting style", + "support", + "Style Template", + "mtg", + "NSFW", + "aventura", + "Manuals", + "plant", + "Incarnate", + "Crafting", + "DnDBeyond", + "Monster Girl", + "Monster Girl Encyclopedia", + "pet", + "npcs", + "Stat Block", + "Styleguide", + "mitologia", + "Objeto maravilloso", + "vaesen rpg", + "dnd-2024", + "Class Handbook", + "Space", + "Taiga", + "CR 6", + "Pact Boon", + "race/ancestry", + "Necromancy", + "cantrip", + "LoL", + "Raza", + "handout", + "Mechanic", + "Conversion", + "Wondrous Item", + "Familiar", + "necromancer", + "uncommon", + "curse", + "Campaign Setting", + "Tetra", + "1e", + "module", + "Evolving", + "boiling sea", + "deck", + "OSR", + "RU", + "VL", + "Underdeep", + "Deep Ocean", + "Giant", + "statblock", + "combat", + "Time", + "5E24", + "session zero", + "elf", + "tank", + "sorcerous origin", + "Drakkenheim", + "Tavern", + "Domain", + "healer", + "Valenor", + "blood", + "Oath", + "spooky", + "fire", + "meta:5e24 Style", + "Notes", + "City", + "Conjurador Completo", + "prop", + "Dotherys", + "Rietuma 3.0", + "5e24", + "Library of Ruina", + "español", + "Project Echo", + "battle of Japan", + "Plant", + "Badlands", + "Neverwinter", + "Fantasy", + "Beastfolk", + "Unarmed", + "Cold", + "Damage", + "attunement", + "Hurthud", + "3rd Level", + "spelljammer", + "mostro", + "Custom", + "PT-BR", + "Alternative Realms", + "The Foot", + "boss", + "demo", + "Supplement", + "FitD", + "classe", + "5.14", + "Copy", + "DnD5e24", + "X-Men", + "TNA", + "CR 8", + "Desert", + "CR 7", + "arme", + "random", + "spellcasting", + "Deprecated", + "Cards", + "finished", + "Ben 10", + "equipment", + "Geography", + "Games", + "For Players", + "Faerun", + "scroll", + "Faction", + "Alchemist", + "drow", + "Lineage", + "mix-blend-mode", + "columns", + "User Help", + "Reami Dimenticati", + "Класс", + "D100", + "nsfw", + "hucaen", + "v1.0", + "Cortex", + "Fallout", + "ww5e", + "MAGIC", + "DnD2024", + "ToV", + "D&D2024", + "The Backrooms", + "Freshwater", + "D20", + "Dragonborn", + "custom", + "sword", + "Dungeons and Dragons 5e", + "Water", + "legendary", + "Dungeon", + "Ravenloft", + "aberration", + "Longsword", + "transmutation", + "Fairy Tail", + "Character background", + "Exandria", + "Updated", + "pitch", + "Half-Caster", + "Complete", + "Money", + "player", + "forgotten-realms", + "Festival", + "Casino", + "SCAG", + "Currency", + "North", + "Toril", + "Scourged Land of Valenor", + "Oota", + "parchment", + "Literature", + "Serrith", + "PNJ", + "Divinity Original Sin 2", + "Wild", + "videogame", + "magic the gathering", + "sweetblossom", + "GMscreen", + "MandM", + "D&D 5.24", + "Camp2", + "Remaster", + "riassunti", + "type:Resource", + "system:D&D", + "tag:Class", + "Excelsior", + "Stat Blocks", + "Sci-Fi", + "Ooze", + "CR 1/8", + "sublclass", + "chart", + "Mountains", + "guns", + "Nature", + "Orc", + "Poison", + "Devil", + "fiend", + "DC", + "pt-br", + "ABnB", + "One-Shot", + "strahd", + "Ring", + "Theme song", + "orc", + "summon", + "Psion", + "Psionics", + "Dungeon Master", + "vehicle", + "DM only", + "Demigod", + "Antica Energia", + "Pirates", + "Sourcebook", + "devil", + "Cantrip", + "mystery", + "MtG", + "conversion", + "Festivals", + "Casinos", + "Taverns", + "Betting", + "Drinking", + "phandelver", + "Warhammer 40k", + "mutant", + "styling", + "FATE", + "Lone Wolf", + "icon", + "New Dawn", + "Magic Set", + "Paladin Subclass", + "Alter Class", + "difficulty classses", + "combat tables", + "phb", + "Project Moon", + "Undertomes", + "EGO", + "Campagne 1", + "Constelação", + "Arvore I", + "Fim da Jornada", + "greek god", + "dwarf", + "Firearms", + "3.5e", + "generator", + "Elf", + "meta: Scenario", + "enchantment", + "buff", + "ITW", + "Tank", + "Archived", + "Martial Archetype", + "caster", + "BR", + "Knight", + "Utility", + "SWADE", + "Star Wars", + "pc", + "Mystic", + "Useful", + "Netherdeep", + "crafting", + "Sapient Undead", + "Maverick", + "Revision", + "Resource", + "Humblewood", + "one piece", + "Bag of Holding", + "medium", + "lightning", + "backgrounds", + "4th Level", + "path", + "BREAK-RPG", + "dark fantasy", + "Players", + "poison", + "psionic", + "gazook89", + "homebrew subclass", + "wild-wasteland", + "CWD", + "Paid", + "Tales of the Valiant", + "Dreadhold", + "arma", + "system:Mutants and Masterminds", + "#Tiefschlaf", + "Brew", + "Myra", + "Swashbuckler", + "dead by daylight", + "Exceptional", + "COD Zombies", + "Hills", + "Tundra", + "type:Campaign", + "wild magic", + "Food", + "Death", + "homebrew rules", + "Remake", + "Witcher", + "water", + "Pet", + "book", + "AAH", + "pact", + "Ice", + "Character Creation", + "animal", + "Pokemon", + "clase", + "5e14", + "DBZ", + "CLONE", + "Evil", + "Tarsere", + "Mythology", + "pf2e", + "Magical", + "type:race", + "Sorcerous Origin", + "Information", + "styles", + "Module", + "gish", + "frames", + "DeltaGreen", + "Magic item", + "food", + "chef", + "basics", + "giant", + "Brew Creation", + "One-shot", + "ttrpg", + "Path", + "Don't Starve", + "MGE", + "firearm", + "DnDBehindTheScreen", + "store", + "The Artisan", + "timeline", + "college", + "dev", + "dungeon of the dead three", + "Cradle", + "Dnd5e", + "dungeon", + "Amaranthine", + "Regno di Oltremare", + "bestia", + "rewrite", + "WiP", + "Subclasse", + "mutants and masterminds", + "The Embrace", + "meta:documentation", + "Mutants And Masterminds", + "khedoria", + "Encounter Pack", + "giorni", + "Statblock", + "Enemies", + "Goblinoids", + "Heavens", + "system:2e", + "Vaalbara", + "Dwarf", + "airos", + "table", + "Artificer Specialization", + "Buff", + "Book 1", + "Ranged", + "cypher", + "utilities", + "40k", + "Psychic", + "Fear", + "steampunk", + "shadow", + "subclase", + "Barbarian Subclass", + "Elements", + "pact boon", + "Clan", + "Fly", + "solo", + "sourcebook", + "Marvel Comics", + "compilation", + "Firearm", + "sidekick", + "infusions", + "Mechanics", + "Summoner", + "Aasimar", + "Human", + "Vehicle", + "Shadow", + "Clone", + "custom css", + "ocean", + "sotdl", + "bandit", + "Wind", + "Printer Friendly", + "Obsolete", + "mechanics", + "illusion", + "5th edition", + "League of Legends", + "Vestige", + "dungeons", + "Dungeons", + "and", + "Elden Ring", + "L5R", + "d20", + "Poisons", + "d15", + "Dungeons And Dragons", + "MTG", + "divine", + "characters", + "witch", + "Anime Homebrew", + "Zombie", + "thunder", + "Jujutsu Kaisen", + "campagne", + "Deadlands", + "spell list", + "1 Person", + "Ritual", + "screen", + "nature", + "Divination", + "Compattare", + "dtrpg", + "quick ref", + "Mago", + "Illivia", + "Shonen", + "Core Deities", + "green ronin", + "Bless", + "D&D5e", + "version:0.1.0", + "curato", + "system:Ord", + "Images", + "Sealed Artifact", + "Giants", + "CR 9", + "CR 11", + "CR 0", + "CR 14", + "Shadowfell", + "Tier 1", + "d100", + "Elemental Air", + "artificiel", + "Cultist", + "Cyberpunk", + "Huge", + "Warrior", + "Gun", + "quest", + "LYRA", + "Music", + "Tiefling", + "Master", + "Witch", + "Linnorm", + "1st-level", + "Mount", + "Animal", + "Comics", + "Superhero", + "creatures", + "Hunter", + "Control", + "Dragon Ball", + "Dragon Ball Z", + "Dagger", + "questingforamonster", + "ICRPG", + "Booklet", + "f and t", + "common", + "Chaos", + "spellblade", + "Constitution", + "artisan", + "arcane", + "Released", + "ring", + "runes", + "gun", + "Supportive Material", + "The Witcher", + "Desarmado", + "Monster Monday", + "Bleach", + "Demon Slayer", + "mice", + "worldbuilding", + "Necrotic", + "ability score", + "demon", + "Armybook Shivatiano", + "warrior", + "Fighter Subclass", + "system", + "whisperveil", + "psychic", + "warhammer", + "Aventura", + "Culture", + "Material", + "meta:npc", + "shops", + "magic weapon", + "nhera", + "Dark Fantasy", + "Regles", + "Wonderous Item", + "Features", + "pokemon", + "Ghosts of Saltmarsh", + "monstrosity", + "DL TWW", + "companion", + "alternate layout", + "Tutorials", + "Kitsune", + "don", + "heroique", + "mini campaign", + "drago", + "Aquatic", + "tool", + "handmade", + "released", + "Spellblade", + "pregen", + "level 2", + "Baldurs gate 3", + "My Hero", + "Technically a subclass", + "5.24e Remastered Subclasses", + "dinosaurs", + "5E.2024", + "Razas", + "Horizon", + "Clothing", + "+2", + "castellano", + "pentacle prophecy", + "tag:Spells", + "gruppo A", + "Rpg", + "razze", + "type:Adventure", + "unfinished", + "3.5", + "gunslinger", + "BBEG", + "Arcane", + "component", + "Bow", + "backstory", + "phandalin", + "Skills", + "Pact", + "Elemental Earth", + "Joke", + "invocation", + "martial class", + "Super Villain", + "Eldritch", + "Elemental Fire", + "Homebrew Class", + "eldritch", + "cyberpunk", + "Player Race", + "Class Mod", + "Heatcoast", + "meta:Guide", + "Yemao", + "evil", + "Named NPC", + "CLASS", + "Angel", + "vecna", + "PT", + "PTBR", + "Ancient", + "Small", + "WotC Style", + "5e Homebrew", + "1st Level", + "dagger", + "Brancalonia", + "encounter", + "cat", + "primal path", + "Ambientazione", + "Magie", + "candlekeep", + "Ongoing", + "Oneshot", + "Wondrous", + "Janbrewery", + "Tattoos", + "5e (2014)", + "concentration", + "very rare", + "Set", + "Kobold", + "martial archetype", + "God", + "blog", + "New Gate", + "Healing", + "OneDnD", + "Incantesimi", + "Player Options", + "contest", + "pirate", + "Manuel", + "Alchimie", + "Herboristerie", + "Ingredients", + "starlost", + "Campaign 1", + "Abandoned", + "Previous Editions", + "Enchantment", + "Tools", + "Oblivion", + "domain", + "5th Level", + "DnD Beyond", + "Reference", + "Sorcerer Subclass", + "Dragon Magazine", + "feature", + "german", + "conjuration", + "strixhaven", + "Sentient", + "JJK", + "10 Generations", + "character creation", + "LevelUp", + "pallid grove", + "primer", + "Requires Attunement", + "College", + "Aesthetic", + "critter", + "home game", + "spanish", + "stats", + "Lairon", + "Hunters Guild", + "original setting", + "Bosses", + "Radiant Citadel", + "actions", + "Reworked", + "Elystera", + "Wyvern", + "vikings", + "thief", + "enemies", + "Obsession", + "Yi Sang", + "aberrazione", + "Limbus", + "animals", + "minecraft", + "mice of legend", + "osric", + "20 Minutes Till Dawn", + "campaign frame", + "latigo", + "DH", + "Eldritch Invocation", + "system:daggerheart", + "100ni", + "meta:Sheet", + "fa-solid fa-sheet-plastic:Ficha", + "tag:Berean", + "AD&D", + "B/X", + "The Codex Of Anomalous Entities", + "monster manual", + "Polar Waters", + "CR 12", + "CR 10", + "blood magic", + "Gunslinger", + "grimoire", + "Drakes", + "Japanese", + "subrace", + "ooze", + "Stats", + "Half Caster", + "Sea", + "time", + "Brawler", + "Session 0", + "Halloween", + "Runeterra", + "Divine", + "Random", + "Lifestar", + "arcane trickster", + "Paddy4530", + "evocation", + "light", + "Steampunk", + "shaman", + "Primal Path", + "monk subclass", + "Full Caster", + "World", + "Planning", + "spirit", + "Nova Era", + "abjuration", + "Christmas", + "Critical Role", + "Gish", + "Bandit", + "Monster Manual", + "party member", + "mgazt", + "Playable Race", + "Donjon.bin.sh", + "Final Fantasy", + "Roleplay", + "monstre", + "fairy", + "frame", + "Minecraft", + "Stealth", + "Manual", + "half caster", + "Storm", + "Sorcery", + "format work", + "Kingdom Hearts", + "hexblade", + "block", + "page layouts", + "Monk Subclass", + "FinyaFluKaiKolja", + "Radiant", + "group:playtest", + "Korrahir", + "noble", + "exorcist", + "xapien", + "Raven Queen", + "markdown", + "damage", + "Alchemy", + "morrigan", + "genasi", + "ZNH", + "folklore", + "Fate", + "hechicero", + "Air", + "Magic Weapon", + "Anime DND 5e", + "Dragon Ball Z TTRPG", + "Dragon Ball Z RPG", + "Dragon Ball Z DND", + "Dragon Ball Z 5e", + "samurai", + "Goblin", + "Base Sheet", + "Shackled City Adventure Path", + "Natureza", + "control", + "Normarch", + "Reddit", + "Genshin Impact", + "Abjuration", + "Myr", + "Flight", + "Vampyre", + "nightmare", + "Lycan", + "Occult", + "circle", + "Christmas Special", + "DoDD", + "Character Options", + "traduction", + "Characters", + "Gear", + "system:sf2e", + "drakkenheim", + "downtime", + "amulet", + "Feiticeiros e Maldicoes", + "Tecnica amaldicoada", + "prorpg", + "enemy", + "No Mercy", + "rain world", + "slugcat", + "fly", + "meta:User Guide", + "Fallout TTRPG", + "regles", + "Ill Tides", + "Light-hearted", + "Vastria", + "school", + "Fillible Online", + "Mezgarr", + "Berserk", + "invocations", + "Classe Refeita", + "Auroboros", + "bosses", + "fabula ultima", + "Shagya", + "wild", + "DnD 2024", + "KaiburrKathHound", + "Barbarian Path", + "fauna", + "5E.2014", + "system:curse of strahd", + "Unofficial", + "how to", + "Glaive", + "A5E", + "pt", + "Consumible", + "Realmers'", + "Versatile Lineage", + "Shichibukai", + "2024e", + "Rencontre", + "tag:Spell List", + "elementalist", + "noncaster", + "blasphemous", + "Mordhiem", + "Wildfrost", + "#Regelwerk", + "Rewrite", + "Maldición de Strahd", + "Scion", + "Entities", + "Hoarwyrm", + "Player utility", + "CR 1/4", + "Temperate Forest", + "Demons", + "Drow", + "type:rules", + "fay", + "2e", + "familier", + "supplement", + "Amberwar", + "slime", + "Lycanthropy", + "meta: Terres de Leyt", + "Strong", + "AAH Vol. 1", + "Force", + "Jump", + "Aboleths", + "lol", + "location", + "small", + "customizable", + "Modern", + "Sky", + "portugues", + "Hero", + "Villain", + "element", + "Tyranny of Dragons", + "Adventure Guide", + "New Class", + "Witchlight", + "Shardblade", + "Plateaux", + "WOTC", + "Snippet", + "Terra", + "Otherworldly Patron", + "ritual", + "hag", + "Cyberpunk 2077", + "tavern", + "Artificer Specialist", + "Werewolf", + "Boesia", + "vampiric", + "monastic tradition", + "Gothic", + "celestial", + "Unfinished", + "Core", + "Arcane Tradition", + "Troll", + "Origin", + "Draconic", + "dj9 member", + "test", + "Hag", + "gem", + "Invocations", + "Dark Sun", + "aarkhen", + "How to", + "ravenloft", + "faerie", + "Playtest", + "Shaman", + "dead", + "Tomba Aniquilacio", + "Pacto", + "fullcaster", + "Electric", + "Ability Score", + "4D", + "pathfinder", + "insect", + "hook", + "page layout", + "healing", + "Lineages", + "Flying", + "Martial Arts", + "journal", + "Aide de jeu", + "hunter", + "headers", + "Dark Souls", + "courtyard", + "crossroads", + "Quest", + "CotF", + "defense", + "Semryss", + "invoked class", + "Session Notes", + "goblin", + "infernal", + "fate", + "oni", + "spellbook", + "Summoning", + "slut", + "whore", + "Greyhawk", + "Mobility", + "Reddit Remake", + "Guild", + "Cosmic Mart", + "7th Level", + "dragonborn", + "curse of strahd", + "Ranger Subclass", + "dossier", + "dossie", + "de", + "pnpde", + "Plane Shift", + "halloween", + "group:aventura", + "9th Level", + "tome", + "cold", + "acid", + "deprecated", + "mind flayer", + "MECHA", + "EssentialsKit", + "2d6", + "ToD", + "Work In Progress", + "Bond", + "Versatile", + "Dead", + "SYWTBAGM", + "summoning", + "english", + "Eilistraee", + "Draft", + "DoD", + "map", + "Frightened", + "Psychic Damage", + "eberron", + "recompensa", + "wizard subclass", + "teiran", + "Saltmarsh", + "jp setting", + "Illithid", + "Longbow", + "hell", + "Monarch", + "type:feat", + "reglas", + "cooking", + "Abenteuer", + "reloaded", + "incompleto", + "mecanica", + "Location", + "Grimlores Grimoire", + "2024 Subclass", + "Chiesa di Toleno", + "finalfantasy", + "The Undertomes", + "Lobotomy Corporation", + "SDHTA", + "D&D 2024", + "other", + "ally", + "images", + "Player's Guide", + "Avalon Sword", + "Cael'Yuu", + "dnd-2014", + "Regelwerk", + "Español", + "br", + "dnd 5.0", + "monstro", + "grand cemetery", + "Phoenix", + "dnd 2024", + "Bloodhunter", + "Sintonizacion", + "dungeons & dragons", + "Fix", + "Rulebook", + "Shadowdark", + "heroic", + "HFW", + "Earthdawn", + "24e", + "cormyr", + "suzail", + "dc20", + "tag:Rules", + "The Griffon's Saddlebag", + "LOTM", + "tag:Adventure", + "drunken master", + "eldritch invocation", + "Персонаж", + "Orcs", + "Lizardfolk", + "Frostfell", + "CR 17", + "Shapechanger", + "Farmland", + "Mages", + "Any", + "CR 13", + "Earth", + "Mountain", + "Drake", + "transformation", + "GM", + "Lich", + "lovecraft", + "unique", + "Optional Rules", + "int", + "creator", + "Primal", + "simple", + "golem", + "Void", + "Armour", + "spellsword", + "General", + "Asian", + "Bringers of chaos", + "Optional Feature", + "subraces", + "Galanoth", + "barbarian subclass", + "felhearth", + "modular", + "Vampires", + "wysteria", + "adaptation", + "beasts", + "naruto", + "ninja", + "Psionic", + "Guns", + "Crystal", + "Guardian", + "NonProfit", + "Mimic", + "languages", + "Epic Boons", + "Primer", + "Icewind Dale", + "joke", + "lycan", + "CR3", + "Armors", + "ff7", + "materia", + "final fantasy 7 remake", + "esper", + "ff7 remake", + "gargantuan", + "Frog", + "CR5", + "blank", + "monster hunter", + "league of legends", + "french", + "Pokémon", + "kobold", + "soul", + "ffxi", + "d10", + "Roman", + "Cute", + "DD5", + "variant", + "tree", + "fr", + "Scenario", + "lycanthrope", + "druide", + "staff", + "eios", + "arkheneios", + "Runic", + "Work", + "Ukrainian", + "cover-page", + "mage", + "deities", + "gods", + "Boss Fight", + "Lair", + "WBTW", + "roguish archetype", + "Character Option", + "Shortsword", + "Illrigger", + "Bloodborne", + "cr6", + "Priest", + "Hamon", + "Toonkind", + "rol", + "Strength", + "forgotten realms", + "Spanish", + "Conclave", + "Electro", + "Magical Tattoos", + "Matt Mercer", + "Wildemount", + "Mighty Nien", + "Campaign 2", + "Resistances", + "Bug", + "impression", + "PF", + "Magnus Archives", + "ice", + "speed", + "Generic NPC", + "Titanic", + "Ink Friendly", + "bleed", + "elder scrolls", + "Immortal", + "LMOP", + "Travel", + "Olphus", + "3d6", + "heist", + "World History", + "ghost", + "genie", + "kids on bikes", + "Russia", + "conclave", + "overhaul", + "manual", + "Adventures In Eden", + "Downtime", + "hamon", + "cloak", + "shadowfell", + "Hellfire", + "Paladin Oath", + "Genshin", + "Nation", + "air", + "Magical Item", + "War", + "Original", + "Monstrous Compendium", + "Calamity", + "Warden", + "Apocalypse", + "shield", + "AC", + "expansion", + "Concentration", + "charm", + "Weave", + "lycanthropy", + "raza", + "far realm", + "fighter subclass", + "ita", + "Pirate", + "Laranja", + "Grapple", + "EastByForce", + "hobgoblin", + "oneshot-notes", + "Holy", + "optional", + "type:cenario", + "group:core", + "The Brewery", + "Alcance", + "Morrowind", + "Indigo", + "Divino", + "2nd Level", + "Sub-Class", + "cantrips", + "Cloak", + "battle master", + "Dark", + "Puzzle", + "Lucky", + "consumable", + "rebalance", + "Shove", + "Area Control", + "Vanguard", + "funny", + "e5", + "Dragonlance", + "psion", + "initiative", + "Tactician", + "Inspiration", + "artificier", + "way", + "inspired", + "historia", + "Medusa", + "2 part", + "holy", + "gift", + "Nimble", + "mostri", + "phoenix", + "travel", + "Class Template", + "Intimidation", + "constructs", + "P666", + "Formatting", + "Divinity", + "Rod", + "Language", + "yokai", + "rune", + "western", + "vampires", + "flying", + "cute", + "Enemy", + "boon", + "Tables", + "ShadowFight", + "meta: Theme", + "SCS", + "vanthampur villa", + "CoA", + "shop", + "destiny", + "magical weapon", + "Arcane Arcade", + "XP to Level 3", + "Dice Average RPG", + "Pip-Boy", + "Dragon Heist", + "session notes", + "tattoo", + "flick", + "P6:66", + "Comic Character", + "experiment", + "Minerva", + "type:Spellbook", + "Realmfall", + "Wand", + "halfling", + "sw5e", + "implementar AP", + "Mask", + "Gazook89", + "Weltenrauch-Chroniken", + "MiA", + "Made in Abyss", + "français", + "fae", + "Lemuria", + "Mork Borg", + "guerrier", + "prunus", + "condition", + "pf2", + "tr", + "costrutto", + "German", + "project moon", + "5r", + "galles", + "Project moon", + "Yisang", + "Spicebush", + "player-accessible", + "Especie", + "Westmarch", + "a", + "Cart", + "Magus", + "group:Mchael Galvis", + "tip", + "werewolf", + "mundane", + "garrett", + "unarmed", + "Arcane Odyssey", + "Tomb of Divinity", + "pets", + "Video Game", + "4 part", + "pbta", + "Druids", + "multiclass", + "manuale", + "mimic", + "plane shift", + "Dotes", + "Hechizos", + "Infernal", + "Enhanced", + "done", + "Mission report", + "Blanks", + "Masks", + "Ultimate Ability", + "shadow-slave", + "Advertising", + "transform", + "Fullmetal Alchemist", + "Fullmetal Alchemist Brotherhood", + "tag:TAoF&F", + "Dwarves", + "Humans", + "Nine Hells", + "Devils", + "Archons", + "CR 15", + "Troglodytes", + "Goliath", + "retired", + "boots", + "ranged", + "shields", + "Zhentarim", + "World of Warcraft", + "Frontline", + "Guildmaster's Guide to Ravnica", + "Dungeons & Dragons 5e", + "beholder", + "NEEDS FIXING", + "mechanic", + "Loot", + "champion", + "Runes", + "Shield", + "Punch", + "Sniper", + "Magical Girl", + "NotDND", + "story", + "Sleep", + "Bard College", + "Illusion", + "Thunder", + "Defender", + "Genasi", + "troll", + "Gehenna", + "Yugoloth", + "social", + "Player Class", + "homebrew class", + "CR 16", + "Ghost", + "Kobolds", + "Trolls", + "Yuan-Ti", + "Elder Scrolls Offline", + "armure", + "Mage", + "CR 18", + "Technology", + "Mystery", + "darkness", + "Airship", + "New Campaign", + "Warframe", + "Wizard Subclass", + "Gold", + "Candor", + "Overhaul", + "Dragon Knight", + "Enoreth", + "Artifacts", + "New", + "AMMO", + "Campagne", + "Valbise", + "Subclasseptember", + "Mecha", + "Yu-Gi-Oh", + "Goblinoid", + "underwater", + "SW5E", + "bardo" +] \ No newline at end of file diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index e38293bde..326287ee6 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,4 +1,4 @@ -/* eslint-disable camelcase */ + import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import './homebrew.less'; import React from 'react'; diff --git a/client/homebrew/navbar/account.navitem.jsx b/client/homebrew/navbar/account.navitem.jsx index bdc6d386a..8c0e92023 100644 --- a/client/homebrew/navbar/account.navitem.jsx +++ b/client/homebrew/navbar/account.navitem.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const Nav = require('client/homebrew/navbar/nav.jsx'); -const request = require('superagent'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; +import Nav from './nav.jsx'; -const Account = createClass({ +const Account = createReactClass({ displayName : 'AccountNavItem', getInitialState : function() { return { @@ -111,4 +111,4 @@ const Account = createClass({ } }); -module.exports = Account; +export default Account; diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index 6b739379b..e8802e184 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -1,6 +1,6 @@ -require('./error-navitem.less'); -const React = require('react'); -const Nav = require('client/homebrew/navbar/nav.jsx'); +import './error-navitem.less'; +import React from 'react'; +import Nav from './nav.jsx'; const ErrorNavItem = ({ error = '', clearError })=>{ const response = error.response; @@ -144,4 +144,4 @@ const ErrorNavItem = ({ error = '', clearError })=>{ ; }; -module.exports = ErrorNavItem; +export default ErrorNavItem; diff --git a/client/homebrew/navbar/help.navitem.jsx b/client/homebrew/navbar/help.navitem.jsx index 0b1ff6c3a..4e012d9a7 100644 --- a/client/homebrew/navbar/help.navitem.jsx +++ b/client/homebrew/navbar/help.navitem.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const dedent = require('dedent-tabs').default; +import React from 'react'; +import dedent from 'dedent'; -const Nav = require('client/homebrew/navbar/nav.jsx'); +import Nav from './nav.jsx'; -module.exports = function(props){ +export default function(props){ return need help? diff --git a/client/homebrew/navbar/metadata.navitem.jsx b/client/homebrew/navbar/metadata.navitem.jsx index 6eadac367..bfea2e81a 100644 --- a/client/homebrew/navbar/metadata.navitem.jsx +++ b/client/homebrew/navbar/metadata.navitem.jsx @@ -1,11 +1,11 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const Moment = require('moment'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import Moment from 'moment'; -const Nav = require('client/homebrew/navbar/nav.jsx'); +import Nav from './nav.jsx'; -const MetadataNav = createClass({ +const MetadataNav = createReactClass({ displayName : 'MetadataNav', getDefaultProps : function() { return { @@ -86,4 +86,4 @@ const MetadataNav = createClass({ }); -module.exports = MetadataNav; +export default MetadataNav; diff --git a/client/homebrew/navbar/nav.jsx b/client/homebrew/navbar/nav.jsx index 16f194716..9e065e0ee 100644 --- a/client/homebrew/navbar/nav.jsx +++ b/client/homebrew/navbar/nav.jsx @@ -1,14 +1,13 @@ -require('client/homebrew/navbar/navbar.less'); -const React = require('react'); -const { useState, useRef, useEffect } = React; -const createClass = require('create-react-class'); -const _ = require('lodash'); -const cx = require('classnames'); +import './navbar.less'; +import React, { useState, useRef, useEffect } from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import cx from 'classnames'; -const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx'); +import NaturalCritIcon from '../../components/svg/naturalcrit-d20.svg.jsx'; const Nav = { - base : createClass({ + base : createReactClass({ displayName : 'Nav.base', render : function(){ return