mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-27 18:58:12 +00:00
Merge branch 'master' into codeMirror-skipExtraKeys
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <label key={val}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={_.includes(this.props.metadata.systems, val)}
|
||||
onChange={(e)=>this.handleSystem(val, e)} />
|
||||
{val}
|
||||
</label>;
|
||||
});
|
||||
},
|
||||
|
||||
renderPublish : function(){
|
||||
if(this.props.metadata.published){
|
||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
@@ -305,7 +280,7 @@ const MetadataEditor = createClass({
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
return <div className='field systems'>
|
||||
return <div className='field renderers'>
|
||||
<label>Renderer</label>
|
||||
<div className='value'>
|
||||
<label key='legacy'>
|
||||
@@ -364,19 +339,15 @@ const MetadataEditor = createClass({
|
||||
{this.renderThumbnail()}
|
||||
</div>
|
||||
|
||||
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||
<TagInput
|
||||
label='tags'
|
||||
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||
placeholder='add tag' unique={true}
|
||||
values={this.props.metadata.tags}
|
||||
smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||
/>
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
<div className='value'>
|
||||
{this.renderSystems()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderLanguageDropdown()}
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
@@ -387,11 +358,13 @@ const MetadataEditor = createClass({
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
<TagInput label='invited authors' valuePatterns={[/.+/]}
|
||||
<TagInput
|
||||
label='invited authors'
|
||||
valuePatterns={/.+/}
|
||||
validators={[(v)=>!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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
title : [
|
||||
(value)=>{
|
||||
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
210
client/homebrew/editor/tagInput/curatedTagSuggestionList.js
Normal file
@@ -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",
|
||||
];
|
||||
@@ -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 (
|
||||
<li key={index}
|
||||
data-value={context.value}
|
||||
className='tag'
|
||||
onClick={()=>editTag(index)}>
|
||||
{context.value}
|
||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
|
||||
</li>
|
||||
);
|
||||
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 (
|
||||
<input type='text'
|
||||
key={index}
|
||||
defaultValue={context.value}
|
||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
|
||||
autoFocus
|
||||
/>
|
||||
<div className={classes} key={`tag-${tag}`} value={tag} data={tag} title={tag}>
|
||||
{tag}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='field'>
|
||||
<label>{props.label}</label>
|
||||
<div className='field tags'>
|
||||
{label && <label>{label}</label>}
|
||||
|
||||
<div className='value'>
|
||||
<ul className='list'>
|
||||
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
||||
{tagList.map((t, i)=>t.editing ? (
|
||||
<input
|
||||
key={i}
|
||||
type='text'
|
||||
value={t.draft} // always use draft
|
||||
pattern={valuePatterns.source}
|
||||
onChange={(e)=>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
|
||||
/>
|
||||
) : (
|
||||
<li key={i} className='tag' onClick={()=>editTag(i)}>
|
||||
{t.value}
|
||||
<button
|
||||
type='button'
|
||||
onClick={(e)=>{
|
||||
e.stopPropagation();
|
||||
removeTag(i);
|
||||
}}>
|
||||
<i className='fa fa-times fa-fw' />
|
||||
</button>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className='value'
|
||||
placeholder={props.placeholder}
|
||||
value={tempInputText}
|
||||
onChange={(e)=>setTempInputText(e.target.value)}
|
||||
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
||||
<Combobox
|
||||
trigger='click'
|
||||
className='tagInput-dropdown'
|
||||
default=''
|
||||
placeholder={placeholder}
|
||||
options={label === 'tags' ? suggestionOptions : []}
|
||||
autoSuggest={
|
||||
label === 'tags'
|
||||
? {
|
||||
suggestMethod : 'startsWith',
|
||||
clearAutoSuggestOnClick : true,
|
||||
filterOn : ['value', 'title'],
|
||||
}
|
||||
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
|
||||
}
|
||||
valuePatterns={valuePatterns.source}
|
||||
onSelect={(value)=>submitTag(value)}
|
||||
onEntry={(e)=>{
|
||||
if(e.key === 'Enter') {
|
||||
console.log('submit');
|
||||
e.preventDefault();
|
||||
submitTag(e.target.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{smallText.length !== 0 && <small>{smallText}</small>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = TagInput;
|
||||
export default TagInput;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
1980
client/homebrew/editor/tagInput/tagSuggestionList.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user