diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 5ac254132..ebd9b9210 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -1,4 +1,4 @@ -/* eslint max-lines: ["error", { "max": 405 }] */ +/* eslint max-lines: ["error", { "max": 455 }] */ import './codeEditor.less'; import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; @@ -42,6 +42,7 @@ import cm5Themes from 'codemirror-5-themes'; const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themeCompartment = new Compartment(); const highlightCompartment = new Compartment(); +const settingsCompartment = new Compartment(); import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js'; import foldOnPages from './extensions/customFolding.js'; @@ -78,6 +79,20 @@ const programmaticCursorLineField = StateField.define({ provide : (decorationSet)=>EditorView.decorations.from(decorationSet) }); +const createSettingsExtensions = (settings)=>[ + ...(settings.autoCloseBrackets ? [autoCloseBrackets] : []), + ...(settings.lineNumbers ? [lineNumbers()] : []), + ...(settings.activeLineShading ? [highlightActiveLine(), + highlightActiveLineGutter()] : []), + ...(settings.fontSize + ? [EditorView.theme({ + '&, .cm-content' : { + fontSize : `${settings.fontSize || 1}em`, + }, + })] + : []), +]; + const CodeEditor = forwardRef( ( { @@ -91,6 +106,7 @@ const CodeEditor = forwardRef( editorTheme = 'default', style, renderer, + settings = {}, ...props }, ref, @@ -163,8 +179,7 @@ const CodeEditor = forwardRef( EditorView.lineWrapping, setEventListeners, languageExtension, - autoCloseBrackets, - lineNumbers(), + settingsCompartment.of(createSettingsExtensions(settings)), scrollPastEnd(), search(), history(), //allows for undo and redo @@ -178,10 +193,8 @@ const CodeEditor = forwardRef( }), //highlights - highlightCompartment.of([customHighlightPlugin(renderer, tab), highlightExtension]), + highlightCompartment.of([customHighlightPlugin(renderer, tab, settings), highlightExtension]), themeCompartment.of(themeExtension), - highlightActiveLine(), - highlightActiveLineGutter(), //keyboard shortcut keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]), @@ -271,6 +284,12 @@ const CodeEditor = forwardRef( } view.setState(nextState); + view.dispatch({ + effects : settingsCompartment.reconfigure( + createSettingsExtensions(settings) + ), + }); + restoreFolds(view, foldsRef.current[tab]); const savedScroll = scrollRef.current[tab]; @@ -320,10 +339,21 @@ const CodeEditor = forwardRef( : syntaxHighlighting(legacyCustomHighlightStyle); view.dispatch({ - effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab), highlightExtension]), + effects : highlightCompartment.reconfigure([customHighlightPlugin(renderer, tab, settings), highlightExtension]) }); }, [renderer, tab]); + useEffect(()=>{ + const view = viewRef.current; + if(!view) return; + + view.dispatch({ + effects : settingsCompartment.reconfigure( + createSettingsExtensions(settings) + ), + }); + }, [settings]); + useImperativeHandle(ref, ()=>({ injectText : (text)=>{ diff --git a/client/components/codeEditor/extensions/customHighlight.js b/client/components/codeEditor/extensions/customHighlight.js index cb44d3fd9..b245694ff 100644 --- a/client/components/codeEditor/extensions/customHighlight.js +++ b/client/components/codeEditor/extensions/customHighlight.js @@ -366,7 +366,7 @@ class ImageWidget extends WidgetType { } } -export function customHighlightPlugin(renderer, tab) { +export function customHighlightPlugin(renderer, tab, settings) { //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them //it also creates page number and snippet number widgets @@ -398,7 +398,7 @@ export function customHighlightPlugin(renderer, tab) { const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); tree.iterate({ enter : (node)=>{ - if(node.name === 'Image') { + if(node.name === 'Image' && settings.showImagePreviews) { const url = getUrl(node, view.state.doc); const widgetPosition = node.node.lastChild.from; @@ -431,7 +431,7 @@ export function customHighlightPlugin(renderer, tab) { const to = line.from + token.to; const attrs = {}; - if(token.type === 'Image' && token.url) { + if(token.type === 'Image' && token.url && settings.showImagePreviews) { attrs['data-url'] = token.url; } diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 70db74b21..777f7bcf1 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -6,8 +6,10 @@ import dedent from 'dedent'; import CodeEditor from '@components/codeEditor/codeEditor.jsx'; import SnippetBar from './snippetbar/snippetbar.jsx'; import MetadataEditor from './metadataEditor/metadataEditor.jsx'; +import SettingsEditor from './settingsEditor/settingsEditor.jsx'; const EDITOR_THEME_KEY = 'HB_editor_theme'; +const EDITOR_SETTINGS_KEY = 'HB_edit_settings'; import defaultCM5Theme from '@themes/codeMirror/default.js'; import darkbrewery from '@themes/codeMirror/darkbrewery.js'; @@ -19,6 +21,20 @@ const EditorThemes = Object.entries(themes) .filter(([name, value])=>Array.isArray(value) && !name.endsWith('Init') && !name.endsWith('Style')) .map(([name])=>name); +const themeNames = Object.entries(themes) + .filter(([name, value])=>Array.isArray(value) && + !name.endsWith('Init') && + !name.endsWith('Style') + ) + .map(([name])=>name); + +const EditorThemeNameList = [ + 'default', + ...themeNames + .filter((name)=>name !== 'default') + .sort((a, b)=>a.localeCompare(b)) +]; + //const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const DEFAULT_STYLE_TEXT = dedent` @@ -50,7 +66,6 @@ const Editor = forwardRef( onCursorPageChange = ()=>{}, onViewPageChange = ()=>{}, - editorTheme = 'default', renderer = 'legacy', moveBrew, @@ -69,9 +84,16 @@ const Editor = forwardRef( }, ref, )=>{ - const [currentEditorTheme, setEditorTheme] = useState(editorTheme); const [view, setView] = useState('text'); // 'text', 'style', 'meta', 'snippet' const [snippetBarHeight, setSnippetBarHeight] = useState(26); + const [editorSettings, setEditorSettings] = useState({ + autoCloseBrackets : true, + showImagePreviews : true, + activeLineShading : true, + lineNumbers : true, + fontSize : 1, + editorTheme : 'default', + }); const editor = useRef(null); const codeEditor = useRef(null); @@ -81,6 +103,7 @@ const Editor = forwardRef( const isStyle = ()=>isView('style'); const isMeta = ()=>isView('meta'); const isSnip = ()=>isView('snippet'); + const isSettings = ()=>isView('settings'); const isView = (name)=>view === name; @@ -89,8 +112,12 @@ const Editor = forwardRef( brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys); - const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); - if(editorTheme && EditorThemes.includes(editorTheme)) setEditorTheme(editorTheme); else setEditorTheme('default'); + const localEditorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); + if(localEditorTheme && EditorThemes.includes(localEditorTheme)) { + setEditorSettings({ ...editorSettings, editorTheme: localEditorTheme }); + } else setEditorSettings({ ...editorSettings, editorTheme: 'default' }); + const localEditorSettings = window.localStorage.getItem(EDITOR_SETTINGS_KEY); + if(localEditorSettings) setEditorSettings(JSON.parse(localEditorSettings)); const snippetBar = document.querySelector('.editor > .snippetBar'); if(!snippetBar) return; @@ -111,7 +138,7 @@ const Editor = forwardRef( useEffect(()=>{ if(liveScroll) brewJump(currentEditorViewPageNum, false); }, [currentEditorViewPageNum, liveScroll]); useEffect(()=>{ if(liveScroll) brewJump(currentEditorCursorPageNum, false); }, [currentEditorCursorPageNum, liveScroll]); - const handleFormatCode = () => { + const handleFormatCode = ()=>{ codeEditor.current?.formatCode(); }; @@ -211,7 +238,12 @@ const Editor = forwardRef( const updateEditorTheme = (newTheme)=>{ window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); - setEditorTheme(newTheme); + setEditorSettings({ ...editorSettings, editorTheme: newTheme }); + }; + + const updateEditorSettings = (newEditorSettings)=>{ + window.localStorage.setItem(EDITOR_SETTINGS_KEY, JSON.stringify(newEditorSettings)); + setEditorSettings(newEditorSettings); }; const renderEditor = ()=>{ @@ -228,9 +260,10 @@ const Editor = forwardRef( onChange={onBrewChange('text')} onCursorChange={(page)=>updateCurrentCursorPage(page)} onViewChange={(page)=>updateCurrentViewPage(page)} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} renderer={brew.renderer} style={{ height: `calc(100% - ${snippetBarHeight}px)` }} + settings={editorSettings} /> ); @@ -246,9 +279,10 @@ const Editor = forwardRef( view={view} value={brew.style ?? DEFAULT_STYLE_TEXT} onChange={onBrewChange('style')} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} renderer={brew.renderer} style={{ height: `calc(100% - ${snippetBarHeight}px)` }} + settings={editorSettings} /> ); @@ -268,9 +302,10 @@ const Editor = forwardRef( value={brew.snippets} onChange={onBrewChange('snippets')} enableFolding={true} - editorTheme={currentEditorTheme} + editorTheme={editorSettings.editorTheme} renderer={brew.renderer} style={{ height: `calc(100% - 25px)` }} + settings={editorSettings} /> ); @@ -278,7 +313,7 @@ const Editor = forwardRef( if(isMeta()) { return ( <> - + ); } + if(isSettings()){ + return ( + <> + + + + ); + } }; const redo = ()=>codeEditor.current?.redo(); @@ -308,7 +361,6 @@ const Editor = forwardRef( unfoldCode, historySize, })); - return (
+ return

Properties Editor

diff --git a/client/homebrew/editor/settingsEditor/settingsEditor.jsx b/client/homebrew/editor/settingsEditor/settingsEditor.jsx new file mode 100644 index 000000000..9ebc685e8 --- /dev/null +++ b/client/homebrew/editor/settingsEditor/settingsEditor.jsx @@ -0,0 +1,139 @@ +import '../uiEditor.less'; +import React from 'react'; + +const SettingsEditor = ({ settings, updateSettings = ()=>{}, EditorThemeNameList })=>{ + + const validations = { + + }; + + const handleFieldChange = (setting, e)=>{ + const value = + e.target.type === 'checkbox' + ? e.target.checked + : e.target.value; + + const inputRules = validations[setting] ?? []; + + const validationErrors = inputRules + .map((rule)=>rule(value)) + .filter(Boolean); + + if(validationErrors.length > 0) { + e.target.setCustomValidity(validationErrors.join('\n')); + e.target.reportValidity(); + return; + } + + e.target.setCustomValidity(''); + + const updatedSettings = { + ...settings, + [setting] : e.target.type === 'number' + ? Number(value) + : value, + }; + + updateSettings(updatedSettings); + }; + + return ( +
+

Editor Settings

+ +
+ +
+ +
+
+ +
+ +
+ handleFieldChange('autoCloseBrackets', e)} + /> +
+
+ +
+ +
+ handleFieldChange('showImagePreviews', e)} + /> +
+
+ +
+ +
+ handleFieldChange('activeLineShading', e)} + /> +
+ +
+ +
+ +
+ handleFieldChange('lineNumbers', e)} + /> +
+
+ +
+ + +
+ from 9px to 30px + handleFieldChange('fontSize', e)} + /> +
+
+
+ ); +}; + +export default SettingsEditor; \ No newline at end of file diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 4bc46723c..9aa6b5ab7 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -10,6 +10,7 @@ import cx from 'classnames'; import { loadHistory } from '../../utils/versionHistory.js'; import { brewSnippetsToJSON } from '@shared/helpers.js'; +/*eslint-disable camelcase*/ 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'; @@ -23,26 +24,7 @@ const ThemeSnippets = { V3_Journal : V3_Journal, V3_Blank : V3_Blank, }; - -import defaultCM5Theme from '@themes/codeMirror/default.js'; -import darkbrewery from '@themes/codeMirror/darkbrewery.js'; -import cm5Themes from 'codemirror-5-themes'; - -const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; - -const themeNames = Object.entries(themes) - .filter(([name, value])=>Array.isArray(value) && - !name.endsWith('Init') && - !name.endsWith('Style') - ) - .map(([name])=>name); - -const EditorThemes = [ - 'default', - ...themeNames - .filter((name)=>name !== 'default') - .sort((a, b)=>a.localeCompare(b)) -]; +/*eslint-enable camelcase */ const execute = function(val, props){ if(_.isFunction(val)) return val(props); @@ -53,30 +35,28 @@ const Snippetbar = createReactClass({ displayName : 'SnippetBar', getDefaultProps : function() { return { - brew : {}, - view : 'text', - onViewChange : ()=>{}, - onInject : ()=>{}, - onToggle : ()=>{}, - showEditButtons : true, - renderer : 'legacy', - undo : ()=>{}, - redo : ()=>{}, - historySize : ()=>{}, - foldCode : ()=>{}, - unfoldCode : ()=>{}, - formatCode : ()=>{}, - updateEditorTheme : ()=>{}, - cursorPos : {}, - themeBundle : [], - updateBrew : ()=>{} + brew : {}, + view : 'text', + onViewChange : ()=>{}, + onInject : ()=>{}, + onToggle : ()=>{}, + showEditButtons : true, + renderer : 'legacy', + undo : ()=>{}, + redo : ()=>{}, + historySize : ()=>{}, + foldCode : ()=>{}, + unfoldCode : ()=>{}, + formatCode : ()=>{}, + cursorPos : {}, + themeBundle : [], + updateBrew : ()=>{} }; }, getInitialState : function() { return { renderer : this.props.renderer, - themeSelector : false, snippets : [], showHistory : false, historyExists : false, @@ -93,7 +73,6 @@ const Snippetbar = createReactClass({ componentDidUpdate : async function(prevProps, prevState) { if(prevProps.renderer != this.props.renderer || - prevProps.theme != this.props.theme || prevProps.themeBundle != this.props.themeBundle || prevProps.brew.snippets != this.props.brew.snippets) { this.setState({ @@ -158,33 +137,6 @@ const Snippetbar = createReactClass({ this.props.onInject(injectedText); }, - toggleThemeSelector : function(e){ - if(e.target.tagName != 'SELECT'){ - this.setState({ - themeSelector : !this.state.themeSelector - }); - } - }, - - changeTheme : function(e){ - if(e.target.value == this.props.currentEditorTheme) return; - this.props.updateEditorTheme(e.target.value); - - this.setState({ - themeSelector : false, - }); - }, - - renderThemeSelector : function(){ - return
- -
; - }, - renderSnippetGroups : function(){ const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view); if(snippets.length === 0) return null; @@ -246,7 +198,7 @@ const Snippetbar = createReactClass({ return (
- {this.props.view !== 'meta' && <>
+ {this.props.view !== 'meta' && this.props.view !== 'settings' && <>
-
}
@@ -298,6 +245,10 @@ const Snippetbar = createReactClass({ onClick={()=>this.props.onViewChange('meta')}> +
@@ -346,7 +297,7 @@ const SnippetGroup = createReactClass({ {this.renderSnippets(snippet.subsnippets)} - ) + ); } }); diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 7b96fa523..99a68504c 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -3,27 +3,27 @@ @import (less) '@themes/fonts/5e/fonts.less'; .snippetBar { - --activeTriggerColor: inherit; - --menuColor : #DDDDDD; + --activeTriggerColor : inherit; + --menuColor : #DDDDDD; @menuHeight : 25px; - position : relative; - display : flex; - flex-wrap : wrap-reverse; - reading-flow : flex-visual; - justify-content : space-between; - height : auto; - color : black; - background-color : #DDDDDD; - font-size : .65rem; - font-family: 'Open Sans', sans-serif; - text-transform: uppercase; - font-weight: 800; + position : relative; + display : flex; + flex-wrap : wrap-reverse; + justify-content : space-between; + height : auto; + font-family : 'Open Sans', sans-serif; + font-size : 0.65rem; + font-weight : 800; + color : black; + text-transform : uppercase; + background-color : #DDDDDD; + reading-flow : flex-visual; .editors { display : flex; justify-content : flex-end; min-width : 275px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied - font-size: .85rem; + font-size : 0.85rem; &:only-child {min-width : unset; margin-left : auto;} reading-order : 2; @@ -138,36 +138,32 @@ } // removed caret for top level items, by request (makes buttons too wide). - .menu-wrapper .menu-item:is(.snippets > .menu-wrapper > .menu-item):first-child .caret { display: none; } + .menu-wrapper .menu-item:is(.snippets > .menu-wrapper > .menu-item):first-child .caret { display : none; } .menu-item { - position : relative; - display : flex; - justify-content: space-between; - align-items : center; - min-width : max-content; - padding : 5px; - cursor : pointer; - width: 100%; - &:is(.menu-list .menu-item) [class*="name"] { - padding-inline: 8px; // additional space between icon and name (helpful in Fonts menu especially). + position : relative; + display : flex; + align-items : center; + justify-content : space-between; + width : 100%; + min-width : max-content; + padding : 5px; + cursor : pointer; + &:is(.menu-list .menu-item) [class*='name'] { + padding-inline : 8px; // additional space between icon and name (helpful in Fonts menu especially). } .menu-name { - flex: 1; - text-align: left; - text-box-trim: trim-end; + flex : 1; + text-align : left; + text-box-trim : trim-end; } i { min-width : 25px; - height : .85rem; + height : 0.85rem; font-size : 1.2em; text-align : center; - &.caret { - margin-right: 0; - } - &.caret:is(.menu-wrapper .menu-wrapper * ) { - text-align: right; - } + &.caret { margin-right : 0; } + &.caret:is(.menu-wrapper .menu-wrapper *) { text-align : right; } /* Fonts */ &.font { height : auto; @@ -206,18 +202,16 @@ background : grey; border-radius : 12px; } - &:hover { - background-color : #999999; - } + &:hover { background-color : #999999; } &:disabled { - color: gray; - cursor: not-allowed; - &:hover { background-color: unset; } + color : gray; + cursor : not-allowed; + &:hover { background-color : unset; } } } } @container editor (width < 841px) { - .snippetBar { + .snippetBar { .editors { flex : 1; justify-content : space-between; @@ -232,4 +226,3 @@ .editors > div.history > .dropdown { right : unset; } } } - diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/uiEditor.less similarity index 83% rename from client/homebrew/editor/metadataEditor/metadataEditor.less rename to client/homebrew/editor/uiEditor.less index 3d3267f80..0b7c004c7 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/uiEditor.less @@ -5,7 +5,7 @@ padding-left : 10px; } -.metadataEditor { +.ui-editor { position : absolute; box-sizing : border-box; width : 100%; @@ -17,6 +17,7 @@ h1 { margin : 0 0 40px; + font-size : 15px; font-weight : bold; text-transform : uppercase; } @@ -63,10 +64,17 @@ &[data-tooltip-right] { max-width : 380px; } &:invalid { background : #FFB9B9; } small { - display : block; - font-size : 0.9em; - font-style : italic; - line-height : 1.4em; + display : block; + width : fit-content; + margin-inline : 5px; + font-size : 0.9em; + font-style : italic; + line-height : 1.4em; + } + + input[type='checkbox'], input[type='number'] { + float : right; + text-align : end; } } input[type='text'], textarea { @@ -175,47 +183,41 @@ } .authors.field { .tag { - font-weight:300; - transition:background-color 0.2s; + font-weight : 300; + transition : background-color 0.2s; &.owner { - position: relative; - background-color:@silverLight; - min-width:25px; - display:grid; - place-items:center; - font-weight: 900; + position : relative; + display : grid; + place-items : center; + min-width : 25px; + font-weight : 900; + background-color : @silverLight; &::after { - content: "\f521"; - font-family: "Font Awesome 6 Free"; - color:gold; - position: absolute; - top: 0; - left: 0; - width:15px; - height:15px; - rotate:-25deg; - translate:-30% -50%; - transform: scaleY(0.7); + position : absolute; + top : 0; + left : 0; + width : 15px; + height : 15px; + font-family : 'Font Awesome 6 Free'; + color : gold; + content : '\f521'; + transform : scaleY(0.7); + rotate : -25deg; + translate : -30% -50%; } } - &:has(button) a { - padding-right:5px; - } - &:has(button:hover) { - background:#d97d7d; - } + &:has(button) a { padding-right : 5px; } + &:has(button:hover) { background : #D97D7D; } - button { - color:@red; - } + button { color : @red; } } a { - color:black; - text-underline-offset:0.2em; + color : black; + text-underline-offset : 0.2em; } } @@ -327,9 +329,7 @@ .icon { #groupedIcon; } - button { - cursor : pointer; - } + button { cursor : pointer; } } .input-group { @@ -371,4 +371,21 @@ } } } +} + + +.settingsEditor .field { + padding-bottom : 10px; + border-bottom : 1px solid black; + + .value { + + display : flex; + justify-content : end; + } + + > label { + width : fit-content; + max-width : calc(100% - 200px); + } } \ No newline at end of file