mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-22 08:58:11 +00:00
96
client/components/Anchored.jsx
Normal file
96
client/components/Anchored.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useRef, forwardRef, useEffect, cloneElement, Children } from 'react';
|
||||
import './Anchored.less';
|
||||
|
||||
// Anchored is a wrapper component that must have as children an <AnchoredTrigger> and a <AnchoredBox> component.
|
||||
// AnchoredTrigger must have a unique `id` prop, which is passed up to Anchored, saved in state on mount, and
|
||||
// then passed down through props into AnchoredBox. The `id` is used for the CSS Anchor Positioning properties.
|
||||
// **The Anchor Positioning API is not available in Firefox yet**
|
||||
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
||||
|
||||
// When Anchor Positioning is added to Firefox, this can also be rewritten using the Popover API-- add the `popover` attribute
|
||||
// to the container div, which will render the container in the *top level* and give it better interactions like
|
||||
// click outside to dismiss. **Do not** add without Anchor, though, because positioning is very limited with the `popover`
|
||||
// attribute.
|
||||
|
||||
|
||||
const Anchored = ({ children })=>{
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [anchorId, setAnchorId] = useState(null);
|
||||
const boxRef = useRef(null);
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
// promote trigger id to Anchored id (to pass it back down to the box as "anchorId")
|
||||
useEffect(()=>{
|
||||
if(triggerRef.current){
|
||||
setAnchorId(triggerRef.current.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// close box on outside click or Escape key
|
||||
useEffect(()=>{
|
||||
const handleClickOutside = (evt)=>{
|
||||
if(
|
||||
boxRef.current &&
|
||||
!boxRef.current.contains(evt.target) &&
|
||||
triggerRef.current &&
|
||||
!triggerRef.current.contains(evt.target)
|
||||
) {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscapeKey = (evt)=>{
|
||||
if(evt.key === 'Escape') setVisible(false);
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
window.addEventListener('keydown', handleEscapeKey);
|
||||
|
||||
return ()=>{
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
window.removeEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleVisibility = ()=>setVisible((prev)=>!prev);
|
||||
|
||||
// Map children to inject necessary props
|
||||
const mappedChildren = Children.map(children, (child)=>{
|
||||
if(child.type === AnchoredTrigger) {
|
||||
return cloneElement(child, { ref: triggerRef, toggleVisibility, visible });
|
||||
}
|
||||
if(child.type === AnchoredBox) {
|
||||
return cloneElement(child, { ref: boxRef, visible, anchorId });
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return <>{mappedChildren}</>;
|
||||
};
|
||||
|
||||
// forward ref for AnchoredTrigger
|
||||
const AnchoredTrigger = forwardRef(({ toggleVisibility, visible, children, className, ...props }, ref)=>(
|
||||
<button
|
||||
ref={ref}
|
||||
className={`anchored-trigger${visible ? ' active' : ''} ${className}`}
|
||||
onClick={toggleVisibility}
|
||||
style={{ anchorName: `--${props.id}` }} // setting anchor properties here allows greater recyclability.
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
));
|
||||
|
||||
// forward ref for AnchoredBox
|
||||
const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={`anchored-box${visible ? ' active' : ''} ${className}`}
|
||||
style={{ positionAnchor: `--${anchorId}` }} // setting anchor properties here allows greater recyclability.
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export { Anchored, AnchoredTrigger, AnchoredBox };
|
||||
11
client/components/Anchored.less
Normal file
11
client/components/Anchored.less
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
.anchored-box {
|
||||
position : absolute;
|
||||
visibility : hidden;
|
||||
justify-self : anchor-center;
|
||||
@supports (inset-block-start: anchor(bottom)) {
|
||||
inset-block-start : anchor(bottom);
|
||||
}
|
||||
&.active { visibility : visible; }
|
||||
}
|
||||
84
client/components/codeEditor/autocompleteEmoji.js
Normal file
84
client/components/codeEditor/autocompleteEmoji.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import diceFont from '../../../themes/fonts/iconFonts/diceFont.js';
|
||||
import elderberryInn from '../../../themes/fonts/iconFonts/elderberryInn.js';
|
||||
import fontAwesome from '../../../themes/fonts/iconFonts/fontAwesome.js';
|
||||
import gameIcons from '../../../themes/fonts/iconFonts/gameIcons.js';
|
||||
|
||||
const emojis = {
|
||||
...diceFont,
|
||||
...elderberryInn,
|
||||
...fontAwesome,
|
||||
...gameIcons
|
||||
};
|
||||
|
||||
const showAutocompleteEmoji = function(CodeMirror, editor) {
|
||||
CodeMirror.commands.autocomplete = function(editor) {
|
||||
editor.showHint({
|
||||
completeSingle : false,
|
||||
hint : function(editor) {
|
||||
const cursor = editor.getCursor();
|
||||
const line = cursor.line;
|
||||
const lineContent = editor.getLine(line);
|
||||
const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1;
|
||||
const end = cursor.ch;
|
||||
const currentWord = lineContent.slice(start, end);
|
||||
|
||||
|
||||
const list = Object.keys(emojis).filter(function(emoji) {
|
||||
return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0;
|
||||
}).sort((a, b)=>{
|
||||
const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string
|
||||
return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in
|
||||
}).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6"
|
||||
const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting
|
||||
return match.padStart(4, '0');
|
||||
}).toLowerCase();
|
||||
|
||||
if(lowerA < lowerB)
|
||||
return -1;
|
||||
return 1;
|
||||
}).map(function(emoji) {
|
||||
return {
|
||||
text : `${emoji}:`, // Text to output to editor when option is selected
|
||||
render : function(element, self, data) { // How to display the option in the dropdown
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `<i class="emojiPreview ${emojis[emoji]}"></i> ${emoji}`;
|
||||
element.appendChild(div);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
list : list.length ? list : [],
|
||||
from : CodeMirror.Pos(line, start),
|
||||
to : CodeMirror.Pos(line, end)
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('inputRead', function(instance, change) {
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
// Get the text from the start of the line to the cursor
|
||||
const textToCursor = line.slice(0, cursor.ch);
|
||||
|
||||
// Do not autosuggest emojis in curly span/div/injector properties
|
||||
if(line.includes('{')) {
|
||||
const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`));
|
||||
const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
|
||||
|
||||
if(curlySpanRegex.test(curlyToCursor))
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the text ends with ':xyz'
|
||||
if(/:[^\s:]+$/.test(textToCursor)) {
|
||||
CodeMirror.commands.autocomplete(editor);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
showAutocompleteEmoji
|
||||
};
|
||||
48
client/components/codeEditor/close-tag.js
Normal file
48
client/components/codeEditor/close-tag.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
|
||||
const ranges = cm.listSelections(), replacements = [];
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
if(!ranges[i].empty()) return CodeMirror.Pass;
|
||||
const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
|
||||
if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
|
||||
return CodeMirror.Pass;
|
||||
else if(typingClosingBrace) {
|
||||
let hasUnclosedBraces = false, index = -1;
|
||||
do {
|
||||
index = line.indexOf('{{', index + 1);
|
||||
if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
|
||||
hasUnclosedBraces = true;
|
||||
break;
|
||||
}
|
||||
} while (index !== -1);
|
||||
if(!hasUnclosedBraces) return CodeMirror.Pass;
|
||||
}
|
||||
|
||||
replacements[i] = typingClosingBrace ? {
|
||||
text : '}}',
|
||||
newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
|
||||
} : {
|
||||
text : '{}}',
|
||||
newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = ranges.length - 1; i >= 0; i--) {
|
||||
const info = replacements[i];
|
||||
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
|
||||
const sel = cm.listSelections().slice(0);
|
||||
sel[i] = {
|
||||
head : info.newPos,
|
||||
anchor : info.newPos
|
||||
};
|
||||
cm.setSelections(sel);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
458
client/components/codeEditor/codeEditor.jsx
Normal file
458
client/components/codeEditor/codeEditor.jsx
Normal file
@@ -0,0 +1,458 @@
|
||||
/* eslint-disable max-lines */
|
||||
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;
|
||||
|
||||
const CodeEditor = createReactClass({
|
||||
displayName : 'CodeEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true,
|
||||
editorTheme : 'default'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
docs : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
|
||||
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);
|
||||
} else {
|
||||
newDoc = this.state.docs[this.props.view];
|
||||
}
|
||||
|
||||
const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) };
|
||||
|
||||
this.setState((prevState)=>({
|
||||
docs : _.merge({}, prevState.docs, oldDoc)
|
||||
}));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if(this.props.enableFolding) {
|
||||
this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror));
|
||||
} else {
|
||||
this.codeMirror?.setOption('foldOptions', false);
|
||||
}
|
||||
|
||||
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||
this.codeMirror?.setOption('theme', this.props.editorTheme);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
this.codeMirror = CodeMirror(this.editor.current, {
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
indentWithTabs : false,
|
||||
tabSize : 2,
|
||||
smartIndent : false,
|
||||
historyEventDelay : 250,
|
||||
scrollPastEnd : true,
|
||||
extraKeys : {
|
||||
'Tab' : this.indent,
|
||||
'Shift-Tab' : this.dedent,
|
||||
'Ctrl-B' : this.makeBold,
|
||||
'Cmd-B' : this.makeBold,
|
||||
'Shift-Ctrl-=' : this.makeSuper,
|
||||
'Shift-Cmd-=' : this.makeSuper,
|
||||
'Ctrl-=' : this.makeSub,
|
||||
'Cmd-=' : this.makeSub,
|
||||
'Ctrl-I' : this.makeItalic,
|
||||
'Cmd-I' : this.makeItalic,
|
||||
'Ctrl-U' : this.makeUnderline,
|
||||
'Cmd-U' : this.makeUnderline,
|
||||
'Ctrl-.' : this.makeNbsp,
|
||||
'Cmd-.' : this.makeNbsp,
|
||||
'Shift-Ctrl-.' : this.makeSpace,
|
||||
'Shift-Cmd-.' : this.makeSpace,
|
||||
'Shift-Ctrl-,' : this.removeSpace,
|
||||
'Shift-Cmd-,' : this.removeSpace,
|
||||
'Ctrl-M' : this.makeSpan,
|
||||
'Cmd-M' : this.makeSpan,
|
||||
'Shift-Ctrl-M' : this.makeDiv,
|
||||
'Shift-Cmd-M' : this.makeDiv,
|
||||
'Ctrl-/' : this.makeComment,
|
||||
'Cmd-/' : this.makeComment,
|
||||
'Ctrl-K' : this.makeLink,
|
||||
'Cmd-K' : this.makeLink,
|
||||
'Ctrl-L' : ()=>this.makeList('UL'),
|
||||
'Cmd-L' : ()=>this.makeList('UL'),
|
||||
'Shift-Ctrl-L' : ()=>this.makeList('OL'),
|
||||
'Shift-Cmd-L' : ()=>this.makeList('OL'),
|
||||
'Shift-Ctrl-1' : ()=>this.makeHeader(1),
|
||||
'Shift-Ctrl-2' : ()=>this.makeHeader(2),
|
||||
'Shift-Ctrl-3' : ()=>this.makeHeader(3),
|
||||
'Shift-Ctrl-4' : ()=>this.makeHeader(4),
|
||||
'Shift-Ctrl-5' : ()=>this.makeHeader(5),
|
||||
'Shift-Ctrl-6' : ()=>this.makeHeader(6),
|
||||
'Shift-Cmd-1' : ()=>this.makeHeader(1),
|
||||
'Shift-Cmd-2' : ()=>this.makeHeader(2),
|
||||
'Shift-Cmd-3' : ()=>this.makeHeader(3),
|
||||
'Shift-Cmd-4' : ()=>this.makeHeader(4),
|
||||
'Shift-Cmd-5' : ()=>this.makeHeader(5),
|
||||
'Shift-Cmd-6' : ()=>this.makeHeader(6),
|
||||
'Shift-Ctrl-Enter' : this.newColumn,
|
||||
'Shift-Cmd-Enter' : this.newColumn,
|
||||
'Ctrl-Enter' : this.newPage,
|
||||
'Cmd-Enter' : this.newPage,
|
||||
'Ctrl-F' : 'findPersistent',
|
||||
'Cmd-F' : 'findPersistent',
|
||||
'Shift-Enter' : 'findPersistentPrevious',
|
||||
'Ctrl-[' : this.foldAllCode,
|
||||
'Cmd-[' : this.foldAllCode,
|
||||
'Ctrl-]' : this.unfoldAllCode,
|
||||
'Cmd-]' : this.unfoldAllCode
|
||||
},
|
||||
foldGutter : true,
|
||||
foldOptions : this.foldOptions(this.codeMirror),
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
theme : this.props.editorTheme
|
||||
// specialChars : / /,
|
||||
// specialCharPlaceholder : function(char) {
|
||||
// const el = document.createElement('span');
|
||||
// el.className = 'cm-space';
|
||||
// el.innerHTML = ' ';
|
||||
// return el;
|
||||
// }
|
||||
});
|
||||
|
||||
// Add custom behaviors (auto-close curlies and auto-complete emojis)
|
||||
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());});
|
||||
this.updateSize();
|
||||
},
|
||||
|
||||
indent : function () {
|
||||
const cm = this.codeMirror;
|
||||
if(cm.somethingSelected()) {
|
||||
cm.execCommand('indentMore');
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab');
|
||||
}
|
||||
},
|
||||
|
||||
dedent : function () {
|
||||
this.codeMirror?.execCommand('indentLess');
|
||||
},
|
||||
|
||||
makeHeader : function (number) {
|
||||
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 });
|
||||
},
|
||||
|
||||
makeBold : function() {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeItalic : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeSuper : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeSub : function() {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
makeNbsp : function() {
|
||||
this.codeMirror?.replaceSelection(' ', 'end');
|
||||
},
|
||||
|
||||
makeSpace : function() {
|
||||
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');
|
||||
} else {
|
||||
this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around');
|
||||
}
|
||||
},
|
||||
|
||||
removeSpace : function() {
|
||||
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');
|
||||
}
|
||||
},
|
||||
|
||||
newColumn : function() {
|
||||
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end');
|
||||
},
|
||||
|
||||
newPage : function() {
|
||||
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end');
|
||||
},
|
||||
|
||||
injectText : function(injectText, overwrite=true) {
|
||||
const cm = this.codeMirror;
|
||||
if(!overwrite) {
|
||||
cm.setCursor(cm.getCursor('from'));
|
||||
}
|
||||
cm.replaceSelection(injectText, 'end');
|
||||
cm.focus();
|
||||
},
|
||||
|
||||
makeUnderline : function() {
|
||||
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
|
||||
this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
|
||||
if(selection.length === 0){
|
||||
const cursor = this.codeMirror?.getCursor();
|
||||
this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
|
||||
}
|
||||
},
|
||||
|
||||
makeSpan : function() {
|
||||
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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeDiv : function() {
|
||||
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');
|
||||
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
|
||||
}
|
||||
},
|
||||
|
||||
makeComment : function() {
|
||||
let regex;
|
||||
let cursorPos;
|
||||
let newComment;
|
||||
const selection = this.codeMirror?.getSelection();
|
||||
if(this.props.language === 'gfm'){
|
||||
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
|
||||
cursorPos = 4;
|
||||
newComment = `<!-- ${selection} -->`;
|
||||
} else {
|
||||
regex = /^\s*(\/\*\s?)(.*?)(\s?\*\/)\s*$/gs;
|
||||
cursorPos = 3;
|
||||
newComment = `/* ${selection} */`;
|
||||
}
|
||||
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 });
|
||||
};
|
||||
},
|
||||
|
||||
makeLink : function() {
|
||||
const isLink = /^\[(.*)\]\((.*)\)$/;
|
||||
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 });
|
||||
} 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 });
|
||||
}
|
||||
},
|
||||
|
||||
makeList : function(listType) {
|
||||
const selectionStart = this.codeMirror?.getCursor('from'), selectionEnd = this.codeMirror?.getCursor('to');
|
||||
this.codeMirror?.setSelection(
|
||||
{ line: selectionStart.line, ch: 0 },
|
||||
{ line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length }
|
||||
);
|
||||
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');
|
||||
} else { // if selection IS NOT A LIST
|
||||
listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
|
||||
this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{
|
||||
let n = 1;
|
||||
return ()=>{
|
||||
return `${n++}. `;
|
||||
};
|
||||
})()), 'around');
|
||||
}
|
||||
},
|
||||
|
||||
foldAllCode : function() {
|
||||
this.codeMirror?.execCommand('foldAll');
|
||||
},
|
||||
|
||||
unfoldAllCode : function() {
|
||||
this.codeMirror?.execCommand('unfoldAll');
|
||||
},
|
||||
|
||||
//=-- Externally used -==//
|
||||
setCursorPosition : function(line, char){
|
||||
setTimeout(()=>{
|
||||
this.codeMirror?.focus();
|
||||
this.codeMirror?.doc.setCursor(line, char);
|
||||
}, 10);
|
||||
},
|
||||
getCursorPosition : function(){
|
||||
return this.codeMirror?.getCursor();
|
||||
},
|
||||
getTopVisibleLine : function(){
|
||||
const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect();
|
||||
const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window');
|
||||
return topVisibleLine;
|
||||
},
|
||||
updateSize : function(){
|
||||
this.codeMirror?.refresh();
|
||||
},
|
||||
redo : function(){
|
||||
return this.codeMirror?.redo();
|
||||
},
|
||||
undo : function(){
|
||||
return this.codeMirror?.undo();
|
||||
},
|
||||
historySize : function(){
|
||||
return this.codeMirror?.doc.historySize();
|
||||
},
|
||||
|
||||
foldOptions : function(cm){
|
||||
return {
|
||||
scanUp : true,
|
||||
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
let maxLength = 50;
|
||||
|
||||
let foldPreviewText = '';
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
const currentText = this.codeMirror?.getLine(currentLine);
|
||||
currentLine++;
|
||||
if(currentText[0] == '#'){
|
||||
foldPreviewText = currentText;
|
||||
break;
|
||||
}
|
||||
if(!foldPreviewText && currentText != '\n') {
|
||||
foldPreviewText = currentText;
|
||||
}
|
||||
}
|
||||
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
||||
text = text.replace('{', '').trim();
|
||||
|
||||
// Truncate data URLs at `data:`
|
||||
const startOfData = text.indexOf('data:');
|
||||
if(startOfData > 0)
|
||||
maxLength = Math.min(startOfData + 5, maxLength);
|
||||
|
||||
if(text.length > maxLength)
|
||||
text = `${text.slice(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
};
|
||||
},
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
return <>
|
||||
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type='text/css' rel='stylesheet' />
|
||||
<div className='codeEditor' ref={this.editor} style={this.props.style}/>
|
||||
</>;
|
||||
}
|
||||
});
|
||||
|
||||
export default CodeEditor;
|
||||
|
||||
60
client/components/codeEditor/codeEditor.less
Normal file
60
client/components/codeEditor/codeEditor.less
Normal file
@@ -0,0 +1,60 @@
|
||||
@import (less) 'codemirror/lib/codemirror.css';
|
||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||
@import (less) 'codemirror/addon/dialog/dialog.css';
|
||||
@import (less) 'codemirror/addon/hint/show-hint.css';
|
||||
|
||||
//Icon fonts included so they can appear in emoji autosuggest dropdown
|
||||
@import (less) './themes/fonts/iconFonts/diceFont.less';
|
||||
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
|
||||
@import (less) './themes/fonts/iconFonts/gameIcons.less';
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@keyframes sourceMoveAnimation {
|
||||
50% { color : white;background-color : red;}
|
||||
100% { color : unset;background-color : unset;}
|
||||
}
|
||||
|
||||
.codeEditor {
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family : inherit;
|
||||
font-weight : 600;
|
||||
color : grey;
|
||||
text-shadow : none;
|
||||
}
|
||||
|
||||
.CodeMirror-foldgutter {
|
||||
cursor : pointer;
|
||||
border-left : 1px solid #EEEEEE;
|
||||
transition : background 0.1s;
|
||||
&:hover { background : #DDDDDD; }
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line {
|
||||
animation-name : sourceMoveAnimation;
|
||||
animation-duration : 0.4s;
|
||||
}
|
||||
|
||||
.CodeMirror-search-field {
|
||||
width:25em !important;
|
||||
outline:1px inset #00000055 !important;
|
||||
}
|
||||
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.emojiPreview {
|
||||
font-size : 1.5em;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
44
client/components/codeEditor/fold-css.js
Normal file
44
client/components/codeEditor/fold-css.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export default {
|
||||
registerHomebreweryHelper : function(CodeMirror) {
|
||||
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
|
||||
|
||||
// BRACE FOLDING
|
||||
const startMatcher = /\{[ \t]*$/;
|
||||
const endMatcher = /\}[ \t]*$/;
|
||||
const activeLine = cm.getLine(start.line);
|
||||
|
||||
|
||||
if(activeLine.match(startMatcher)) {
|
||||
const lastLineNo = cm.lastLine();
|
||||
let end = start.line + 1;
|
||||
let braceCount = 1;
|
||||
|
||||
while (end < lastLineNo) {
|
||||
const curLine = cm.getLine(end);
|
||||
if(curLine.match(startMatcher)) braceCount++;
|
||||
if(curLine.match(endMatcher)) braceCount--;
|
||||
if(braceCount == 0) break;
|
||||
++end;
|
||||
}
|
||||
|
||||
return {
|
||||
from : CodeMirror.Pos(start.line, 0),
|
||||
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
||||
};
|
||||
}
|
||||
|
||||
// @import and data-url folding
|
||||
const importMatcher = /^@import.*?;/;
|
||||
const dataURLMatcher = /url\(.*?data\:.*\)/;
|
||||
|
||||
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
|
||||
return {
|
||||
from : CodeMirror.Pos(start.line, 0),
|
||||
to : CodeMirror.Pos(start.line, activeLine.length)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
26
client/components/codeEditor/fold-pages.js
Normal file
26
client/components/codeEditor/fold-pages.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
registerHomebreweryHelper : function(CodeMirror) {
|
||||
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
|
||||
const matcher = /^\\page.*/;
|
||||
const prevLine = cm.getLine(start.line - 1);
|
||||
|
||||
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
|
||||
const lastLineNo = cm.lastLine();
|
||||
let end = start.line;
|
||||
|
||||
while (end < lastLineNo) {
|
||||
if(cm.getLine(end + 1).match(matcher))
|
||||
break;
|
||||
++end;
|
||||
}
|
||||
|
||||
return {
|
||||
from : CodeMirror.Pos(start.line, 0),
|
||||
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
129
client/components/combobox.jsx
Normal file
129
client/components/combobox.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import _ from 'lodash';
|
||||
import './combobox.less';
|
||||
|
||||
const Combobox = createReactClass({
|
||||
displayName : 'Combobox',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
className : '',
|
||||
trigger : 'hover',
|
||||
default : '',
|
||||
placeholder : '',
|
||||
autoSuggest : {
|
||||
clearAutoSuggestOnClick : true,
|
||||
suggestMethod : 'includes',
|
||||
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||
},
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false,
|
||||
value : '',
|
||||
options : [...this.props.options],
|
||||
inputFocused : false
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
this.setState({
|
||||
value : this.props.default
|
||||
});
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
if(this.props.trigger == 'click')
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
},
|
||||
handleClickOutside : function(e){
|
||||
// Close dropdown when clicked outside
|
||||
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
||||
this.handleDropdown(false);
|
||||
}
|
||||
},
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
value : show ? '' : this.props.default,
|
||||
showDropdown : show,
|
||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||
});
|
||||
},
|
||||
handleInput : function(e){
|
||||
e.persist();
|
||||
this.setState({
|
||||
value : e.target.value,
|
||||
inputFocused : false
|
||||
}, ()=>{
|
||||
this.props.onEntry(e);
|
||||
});
|
||||
},
|
||||
handleSelect : function(value, data=value){
|
||||
this.setState({
|
||||
value : value
|
||||
}, ()=>{this.props.onSelect(data);});
|
||||
;
|
||||
},
|
||||
renderTextInput : function(){
|
||||
return (
|
||||
<div className='dropdown-input item'
|
||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
|
||||
<input
|
||||
type='text'
|
||||
onChange={(e)=>this.handleInput(e)}
|
||||
value={this.state.value || ''}
|
||||
placeholder={this.props.placeholder}
|
||||
onBlur={(e)=>{
|
||||
if(!e.target.checkValidity()){
|
||||
this.setState({
|
||||
value : this.props.default
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<i className='fas fa-caret-down'/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
renderDropdown : function(dropdownChildren){
|
||||
if(!this.state.showDropdown) return null;
|
||||
if(this.props.autoSuggest && !this.state.inputFocused){
|
||||
const suggestMethod = this.props.autoSuggest.suggestMethod;
|
||||
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
||||
const filteredArrays = filterOn.map((attr)=>{
|
||||
const children = dropdownChildren.filter((item)=>{
|
||||
if(suggestMethod === 'includes')
|
||||
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
||||
if(suggestMethod === 'startsWith')
|
||||
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
||||
});
|
||||
return children;
|
||||
});
|
||||
dropdownChildren = _.uniq(filteredArrays.flat(1));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='dropdown-options'>
|
||||
{dropdownChildren}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render : function () {
|
||||
const dropdownChildren = this.state.options.map((child, i)=>{
|
||||
const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) });
|
||||
return clone;
|
||||
});
|
||||
return (
|
||||
<div className={`dropdown-container ${this.props.className}`}
|
||||
ref='dropdown'
|
||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||
{this.renderTextInput()}
|
||||
{this.renderDropdown(dropdownChildren)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Combobox;
|
||||
46
client/components/combobox.less
Normal file
46
client/components/combobox.less
Normal file
@@ -0,0 +1,46 @@
|
||||
.dropdown-container {
|
||||
position : relative;
|
||||
input { width : 100%; }
|
||||
.item i {
|
||||
position : absolute;
|
||||
right : 10px;
|
||||
color : black;
|
||||
}
|
||||
.dropdown-options {
|
||||
position : absolute;
|
||||
z-index : 100;
|
||||
width : 100%;
|
||||
max-height : 200px;
|
||||
overflow-y : auto;
|
||||
background-color : white;
|
||||
border : 1px solid gray;
|
||||
|
||||
&::-webkit-scrollbar { width : 14px; }
|
||||
&::-webkit-scrollbar-track { background : #FFFFFF; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color : #949494;
|
||||
border : 3px solid #FFFFFF;
|
||||
border-radius : 10px;
|
||||
}
|
||||
|
||||
.item {
|
||||
position : relative;
|
||||
padding : 5px;
|
||||
margin : 0 3px;
|
||||
font-family : 'Open Sans';
|
||||
font-size : 11px;
|
||||
cursor : default;
|
||||
&:hover {
|
||||
background-color : rgb(163, 163, 163);
|
||||
filter : brightness(120%);
|
||||
}
|
||||
.detail {
|
||||
width : 100%;
|
||||
font-size : 9px;
|
||||
font-style : italic;
|
||||
color : rgb(124, 124, 124);
|
||||
text-align : left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
client/components/dialog.jsx
Normal file
31
client/components/dialog.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
// Dialog box, for popups and modal blocking messages
|
||||
import React from 'react';
|
||||
const { useRef, useEffect } = React;
|
||||
|
||||
function Dialog({ dismisskeys = [], closeText = 'Close', blocking = false, ...rest }) {
|
||||
const dialogRef = useRef(null);
|
||||
|
||||
useEffect(()=>{
|
||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||
}, []);
|
||||
|
||||
const dismiss = ()=>{
|
||||
dismisskeys.forEach((key)=>{
|
||||
if(key) {
|
||||
localStorage.setItem(key, 'true');
|
||||
}
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
|
||||
{rest.children}
|
||||
<button className='dismiss' onClick={dismiss}>
|
||||
{closeText}
|
||||
</button>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
||||
60
client/components/renderWarnings/renderWarnings.jsx
Normal file
60
client/components/renderWarnings/renderWarnings.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import './renderWarnings.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Dialog from '../dialog.jsx';
|
||||
|
||||
const RenderWarnings = createReactClass({
|
||||
displayName : 'RenderWarnings',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
warnings : {}
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
this.checkWarnings();
|
||||
window.addEventListener('resize', this.checkWarnings);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.checkWarnings);
|
||||
},
|
||||
warnings : {
|
||||
chrome : function(){
|
||||
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
if(!isChrome){
|
||||
return <li key='chrome'>
|
||||
<em>Built for Chrome </em> <br />
|
||||
Other browsers have not been tested for compatibility. If you
|
||||
experience issues with your document not rendering or printing
|
||||
properly, please try using the latest version of Chrome before
|
||||
submitting a bug report.
|
||||
</li>;
|
||||
}
|
||||
},
|
||||
},
|
||||
checkWarnings : function(){
|
||||
this.setState({
|
||||
warnings : _.reduce(this.warnings, (r, fn, type)=>{
|
||||
const element = fn();
|
||||
if(element) r[type] = element;
|
||||
return r;
|
||||
}, {})
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
if(_.isEmpty(this.state.warnings)) return null;
|
||||
|
||||
const DISMISS_KEY = 'dismiss_render_warning';
|
||||
const DISMISS_TEXT = <i className='fas fa-times dismiss' />;
|
||||
|
||||
return <Dialog className='renderWarnings' dismissKey={DISMISS_KEY} closeText={DISMISS_TEXT}>
|
||||
<i className='fas fa-exclamation-triangle ohno' />
|
||||
<h3>Render Warnings</h3>
|
||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||
<ul>{_.values(this.state.warnings)}</ul>
|
||||
</Dialog>;
|
||||
}
|
||||
});
|
||||
|
||||
export default RenderWarnings;
|
||||
50
client/components/renderWarnings/renderWarnings.less
Normal file
50
client/components/renderWarnings/renderWarnings.less
Normal file
@@ -0,0 +1,50 @@
|
||||
@import './shared/naturalcrit/styles/colors.less';
|
||||
|
||||
.renderWarnings {
|
||||
position : relative;
|
||||
float : right;
|
||||
width : 350px;
|
||||
padding : 20px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 85px;
|
||||
margin-bottom : 10px;
|
||||
color : white;
|
||||
background-color : @yellow;
|
||||
border : none;
|
||||
a { font-weight : 800; }
|
||||
i.ohno {
|
||||
position : absolute;
|
||||
top : 24px;
|
||||
left : 24px;
|
||||
font-size : 2.5em;
|
||||
opacity : 0.8;
|
||||
}
|
||||
button.dismiss {
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
background-color : transparent;
|
||||
opacity : 0.6;
|
||||
&:hover { opacity : 1; }
|
||||
}
|
||||
small {
|
||||
font-size : 0.6em;
|
||||
opacity : 0.7;
|
||||
}
|
||||
h3 {
|
||||
font-size : 1.1em;
|
||||
font-weight : 800;
|
||||
}
|
||||
ul {
|
||||
margin-top : 15px;
|
||||
font-size : 0.8em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
li {
|
||||
font-size : 0.8em;
|
||||
line-height : 1.6em;
|
||||
em { font-weight : 800; }
|
||||
}
|
||||
}
|
||||
}
|
||||
111
client/components/splitPane/splitPane.jsx
Normal file
111
client/components/splitPane/splitPane.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
import './splitPane.less';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
|
||||
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
|
||||
|
||||
const SplitPane = (props)=>{
|
||||
const {
|
||||
onDragFinish = ()=>{},
|
||||
showDividerButtons = true
|
||||
} = props;
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dividerPos, setDividerPos] = useState(null);
|
||||
const [moveSource, setMoveSource] = useState(false);
|
||||
const [moveBrew, setMoveBrew] = useState(false);
|
||||
const [showMoveArrows, setShowMoveArrows] = useState(true);
|
||||
const [liveScroll, setLiveScroll] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
||||
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
||||
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return ()=>window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
||||
|
||||
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
||||
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
||||
|
||||
const handleUp =(e)=>{
|
||||
e.preventDefault();
|
||||
if(isDragging) {
|
||||
onDragFinish(dividerPos);
|
||||
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos);
|
||||
}
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDown = (e)=>{
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleMove = (e)=>{
|
||||
if(!isDragging) return;
|
||||
e.preventDefault();
|
||||
setDividerPos(limitPosition(e.pageX));
|
||||
};
|
||||
|
||||
const liveScrollToggle = ()=>{
|
||||
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll));
|
||||
setLiveScroll(!liveScroll);
|
||||
};
|
||||
|
||||
const renderMoveArrows = (showMoveArrows &&
|
||||
<>
|
||||
<div className='arrow left'
|
||||
onClick={()=>setMoveSource(!moveSource)} >
|
||||
<i className='fas fa-arrow-left' />
|
||||
</div>
|
||||
<div className='arrow right'
|
||||
onClick={()=>setMoveBrew(!moveBrew)} >
|
||||
<i className='fas fa-arrow-right' />
|
||||
</div>
|
||||
<div id='scrollToggleDiv' className={liveScroll ? 'arrow lock' : 'arrow unlock'}
|
||||
onClick={liveScrollToggle} >
|
||||
<i id='scrollToggle' className={liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderDivider = (
|
||||
<div className={`divider ${isDragging && 'dragging'}`} onPointerDown={handleDown}>
|
||||
{showDividerButtons && renderMoveArrows}
|
||||
<div className='dots'>
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
<i className='fas fa-circle' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='splitPane' onPointerMove={handleMove} onPointerUp={handleUp}>
|
||||
<Pane width={dividerPos} moveBrew={moveBrew} moveSource={moveSource} liveScroll={liveScroll} setMoveArrows={setShowMoveArrows}>
|
||||
{props.children[0]}
|
||||
</Pane>
|
||||
{renderDivider}
|
||||
<Pane isDragging={isDragging}>{props.children[1]}</Pane>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Pane = ({ width, children, isDragging, moveBrew, moveSource, liveScroll, setMoveArrows })=>{
|
||||
const styles = width
|
||||
? { flex: 'none', width: `${width}px` }
|
||||
: { pointerEvents: isDragging ? 'none' : 'auto' }; //Disable mouse capture in the right pane; else dragging into the iframe drops the divider
|
||||
|
||||
return (
|
||||
<div className='pane' style={styles}>
|
||||
{React.cloneElement(children, { moveBrew, moveSource, liveScroll, setMoveArrows })}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SplitPane;
|
||||
69
client/components/splitPane/splitPane.less
Normal file
69
client/components/splitPane/splitPane.less
Normal file
@@ -0,0 +1,69 @@
|
||||
@import './shared/naturalcrit/styles/core.less';
|
||||
|
||||
.splitPane {
|
||||
position : relative;
|
||||
display : flex;
|
||||
flex-direction : row;
|
||||
height : 100%;
|
||||
outline : none;
|
||||
.pane {
|
||||
flex : 1;
|
||||
overflow-x : hidden;
|
||||
overflow-y : hidden;
|
||||
}
|
||||
.divider {
|
||||
position : relative;
|
||||
display : table;
|
||||
width : 15px;
|
||||
height : 100%;
|
||||
text-align : center;
|
||||
touch-action : none;
|
||||
cursor : ew-resize;
|
||||
background-color : #BBBBBB;
|
||||
.dots {
|
||||
display : table-cell;
|
||||
vertical-align : middle;
|
||||
text-align : center;
|
||||
i {
|
||||
display : block !important;
|
||||
margin : 10px 0px;
|
||||
font-size : 6px;
|
||||
color : #666666;
|
||||
}
|
||||
}
|
||||
&:hover,&.dragging { background-color : #999999; }
|
||||
}
|
||||
.arrow {
|
||||
position : absolute;
|
||||
left : 50%;
|
||||
z-index : 999;
|
||||
width : 25px;
|
||||
height : 25px;
|
||||
font-size : 1.2em;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
background-color : #DDDDDD;
|
||||
border : 2px solid #BBBBBB;
|
||||
border-radius : 15px;
|
||||
box-shadow : 0 4px 5px #0000007F;
|
||||
translate : -50%;
|
||||
&.left {
|
||||
.tooltipLeft('Jump to location in Editor');
|
||||
top : 30px;
|
||||
}
|
||||
&.right {
|
||||
.tooltipRight('Jump to location in Preview');
|
||||
top : 60px;
|
||||
}
|
||||
&.lock {
|
||||
.tooltipRight('De-sync Editor and Preview locations.');
|
||||
top : 90px;
|
||||
background : #666666;
|
||||
}
|
||||
&.unlock {
|
||||
.tooltipRight('Sync Editor and Preview locations');
|
||||
top : 90px;
|
||||
}
|
||||
&:hover { background-color : #666666; }
|
||||
}
|
||||
}
|
||||
9
client/components/svg/cauldron.svg.jsx
Normal file
9
client/components/svg/cauldron.svg.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function(props){
|
||||
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' >
|
||||
<path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/>
|
||||
<circle cx='62.905' cy='10.089' r='3.595'/>
|
||||
<circle cx='52.616' cy='5.048' r='2.73'/>
|
||||
</svg>;
|
||||
};
|
||||
5
client/components/svg/naturalcrit-d20.svg.jsx
Normal file
5
client/components/svg/naturalcrit-d20.svg.jsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function(props){
|
||||
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'><path d='M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z'></path></svg>;
|
||||
};
|
||||
Reference in New Issue
Block a user