mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 07:43:01 +00:00
Compare commits
284 Commits
editor-wid
...
v3.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
917b63722c | ||
|
|
c1cbbe0047 | ||
|
|
963d76add9 | ||
|
|
e055734d03 | ||
|
|
7c16805680 | ||
|
|
be52e0ecd9 | ||
|
|
175673cdf9 | ||
|
|
a279c75826 | ||
|
|
8be95e7d04 | ||
|
|
8e0c1d78dc | ||
|
|
5da73f8ff8 | ||
|
|
e20fc680e9 | ||
|
|
e821ddac93 | ||
|
|
fab2c2cead | ||
|
|
bd0e142999 | ||
|
|
673649abc4 | ||
|
|
b95a2189a5 | ||
|
|
f45547d899 | ||
|
|
f2c970fb79 | ||
|
|
6e6a6dd1e3 | ||
|
|
b2fc020d81 | ||
|
|
3bfa3bac3a | ||
|
|
00134c6c3d | ||
|
|
6a393a1437 | ||
|
|
115be2813d | ||
|
|
e4a46c84ec | ||
|
|
b994bf269a | ||
|
|
e7ce8d73bb | ||
|
|
c6ef051232 | ||
|
|
2c130d1943 | ||
|
|
f1e6a9a41e | ||
|
|
34edd436e7 | ||
|
|
0b13e90dd8 | ||
|
|
52dcc3b53c | ||
|
|
6803db268f | ||
|
|
99c45b6cc3 | ||
|
|
4cf8e620ee | ||
|
|
fb4c33545c | ||
|
|
68475a6aa0 | ||
|
|
e5140bd5b4 | ||
|
|
e3c05c83ba | ||
|
|
b374f06718 | ||
|
|
ea8c93d39d | ||
|
|
6dd8ccff90 | ||
|
|
ca90a22c64 | ||
|
|
abc4851375 | ||
|
|
a155c10d46 | ||
|
|
5247f5c87b | ||
|
|
e41c1ef0bd | ||
|
|
4d6b428f93 | ||
|
|
6928bd9fb4 | ||
|
|
f2855aca85 | ||
|
|
aeef595ea9 | ||
|
|
1a24709da0 | ||
|
|
8c11f47c1f | ||
|
|
486fbf32b2 | ||
|
|
8ac3cdcf9d | ||
|
|
f0fc0bcb6d | ||
|
|
63a5e9f817 | ||
|
|
5456f4f197 | ||
|
|
d75ea8943b | ||
|
|
07ae1539aa | ||
|
|
2125b8a026 | ||
|
|
b3522bddf1 | ||
|
|
9dc19d996d | ||
|
|
3e9bea3761 | ||
|
|
73e909c4c8 | ||
|
|
53fa6af5f9 | ||
|
|
4d2edf81a9 | ||
|
|
b10b33830a | ||
|
|
393ac69581 | ||
|
|
1098f6da70 | ||
|
|
a29fed3c89 | ||
|
|
7af3a629f9 | ||
|
|
b0a9765819 | ||
|
|
86d18ea0d9 | ||
|
|
bfe278c81c | ||
|
|
9e5103a0c7 | ||
|
|
7281b6e43c | ||
|
|
3f6eb7371f | ||
|
|
2b0bbfc2db | ||
|
|
e909bc8f35 | ||
|
|
e16110da6a | ||
|
|
fcd15e6d9c | ||
|
|
3f828c8649 | ||
|
|
bb66cffa13 | ||
|
|
da5a5631ad | ||
|
|
3e43b058a5 | ||
|
|
a30c2fa1f7 | ||
|
|
cb203c29c9 | ||
|
|
e50d7b8882 | ||
|
|
74d6aa7c8a | ||
|
|
e86686807b | ||
|
|
f4356025de | ||
|
|
73b141d08c | ||
|
|
9196ffc480 | ||
|
|
4e18bb047e | ||
|
|
e4b4e34216 | ||
|
|
d38c1b9ab2 | ||
|
|
deaaafd9d2 | ||
|
|
9b33bf9855 | ||
|
|
e5346d3a6e | ||
|
|
d9ca20e17d | ||
|
|
a5cb3f085f | ||
|
|
6d28948387 | ||
|
|
65c75b3282 | ||
|
|
a0f22e31b7 | ||
|
|
c750eebc11 | ||
|
|
93a93f1907 | ||
|
|
29428b81f6 | ||
|
|
a542953cec | ||
|
|
183ca51753 | ||
|
|
220816a172 | ||
|
|
840b075c8e | ||
|
|
3ba15a068a | ||
|
|
b99de1c6e1 | ||
|
|
73a48501e0 | ||
|
|
599d39b69d | ||
|
|
ed18ba3108 | ||
|
|
6d93291d5b | ||
|
|
3fef61cbf8 | ||
|
|
c5d9c3bdc0 | ||
|
|
6c7af2d968 | ||
|
|
6130d69906 | ||
|
|
8c975747c4 | ||
|
|
ef6dab24a2 | ||
|
|
1f09fff94b | ||
|
|
7bcd898c81 | ||
|
|
186809008c | ||
|
|
b6e11ba607 | ||
|
|
355b8ac78f | ||
|
|
a2c20a0f7a | ||
|
|
117e399c1d | ||
|
|
a3ac524308 | ||
|
|
1e9ba31644 | ||
|
|
751728d134 | ||
|
|
8e05cdbb43 | ||
|
|
a5483c549b | ||
|
|
746f3d35f8 | ||
|
|
94c902bf38 | ||
|
|
0da1c43dc3 | ||
|
|
32417e92ff | ||
|
|
540dee89dd | ||
|
|
1854080771 | ||
|
|
45207b8114 | ||
|
|
2b3c2c9fac | ||
|
|
0b953fcbf3 | ||
|
|
0a3453d228 | ||
|
|
6b49e720ca | ||
|
|
7feaa51de0 | ||
|
|
1729b13574 | ||
|
|
73832fabcc | ||
|
|
bac537244c | ||
|
|
54f8bb4b08 | ||
|
|
38f6929c1d | ||
|
|
64ce9ecfa6 | ||
|
|
858990c4bd | ||
|
|
785011cba4 | ||
|
|
50d3b503d9 | ||
|
|
2f8e2545c6 | ||
|
|
d8574e7045 | ||
|
|
b3497e14f1 | ||
|
|
d6e0047d4e | ||
|
|
8ebd5ccff9 | ||
|
|
4a927daff3 | ||
|
|
d28e85209e | ||
|
|
cca882869d | ||
|
|
9ae55c87a9 | ||
|
|
99ad96a584 | ||
|
|
f8b42031fb | ||
|
|
850b52d924 | ||
|
|
b2e7b28b65 | ||
|
|
3efa7dd0be | ||
|
|
677e27bb66 | ||
|
|
da71bd7a10 | ||
|
|
0869f6b29b | ||
|
|
0da5de494e | ||
|
|
bc9dc8dee9 | ||
|
|
e3e250255e | ||
|
|
787a23bdf8 | ||
|
|
7aca0f2f10 | ||
|
|
999f9c8f25 | ||
|
|
ee4b2d549b | ||
|
|
3eb7ce2775 | ||
|
|
4d4371f48c | ||
|
|
47666cc26d | ||
|
|
e8352d996e | ||
|
|
e6f792900c | ||
|
|
0ddeafd260 | ||
|
|
4cf54d6ae8 | ||
|
|
8d3329069a | ||
|
|
2e13eed2ef | ||
|
|
310faa449d | ||
|
|
8527a976a6 | ||
|
|
54460c52f6 | ||
|
|
e81bd2a0d2 | ||
|
|
6c0daa1e4d | ||
|
|
25d03faae2 | ||
|
|
378b2204da | ||
|
|
447b0939f2 | ||
|
|
0bde336226 | ||
|
|
73e44b8d7a | ||
|
|
53fbaf87c2 | ||
|
|
a8db7353b0 | ||
|
|
bda8037cd6 | ||
|
|
1f173814ec | ||
|
|
deb0e2f85b | ||
|
|
5425ae5d15 | ||
|
|
f03f2c36de | ||
|
|
a1e663bc32 | ||
|
|
16c842e08f | ||
|
|
1c0edce6f6 | ||
|
|
8ec6e66c92 | ||
|
|
330cdb35f4 | ||
|
|
f93fbab754 | ||
|
|
b9498e49fc | ||
|
|
4a434bb161 | ||
|
|
a339cb036f | ||
|
|
91eef51fb5 | ||
|
|
46b050ae7d | ||
|
|
b433691596 | ||
|
|
cef7f98176 | ||
|
|
b869d086ea | ||
|
|
64a361e06c | ||
|
|
9e7e646296 | ||
|
|
7274d788c5 | ||
|
|
7942f1caed | ||
|
|
343e176b83 | ||
|
|
3db5959cfa | ||
|
|
62f9781c8e | ||
|
|
9d53002874 | ||
|
|
4e7e6f8105 | ||
|
|
cea1157eb2 | ||
|
|
d535328eb8 | ||
|
|
627e9ec7d8 | ||
|
|
ab9b5b7487 | ||
|
|
957a8bf05c | ||
|
|
509390ae03 | ||
|
|
d3a70c3d75 | ||
|
|
1806854969 | ||
|
|
c2570fec6b | ||
|
|
cf3635bccc | ||
|
|
7aa374e529 | ||
|
|
9cb8b46930 | ||
|
|
467b6ff8de | ||
|
|
a41553637a | ||
|
|
636f2f9372 | ||
|
|
4ded080a58 | ||
|
|
a5885c8f4f | ||
|
|
4cb2a9ef76 | ||
|
|
661872f332 | ||
|
|
dbb4476eb4 | ||
|
|
65f55dfc12 | ||
|
|
95322595bf | ||
|
|
8c0ca988ae | ||
|
|
e1a22ed76c | ||
|
|
7e660aad45 | ||
|
|
c8df449aac | ||
|
|
7681be2e9c | ||
|
|
455b364160 | ||
|
|
56dbfc032c | ||
|
|
86823b43b1 | ||
|
|
0abfb23ef2 | ||
|
|
da5d4236b6 | ||
|
|
963236f961 | ||
|
|
2d4a3ec910 | ||
|
|
40afdf18d6 | ||
|
|
13944d3a76 | ||
|
|
3c168065ee | ||
|
|
cf42520305 | ||
|
|
d5ac237d40 | ||
|
|
c666d6acb9 | ||
|
|
2869726efd | ||
|
|
ae9a29c28c | ||
|
|
c660f87dff | ||
|
|
a86c728710 | ||
|
|
b28e183f95 | ||
|
|
dc55880544 | ||
|
|
5e9b451e29 | ||
|
|
977cbeed43 | ||
|
|
da6fcb297a | ||
|
|
b2546d908a | ||
|
|
7dd1368c09 | ||
|
|
51f657d2c5 |
@@ -5,12 +5,12 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@3.0.0
|
node: circleci/node@5.1.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
- image: mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: sudo npm install -g npm@8.10.0
|
- run: sudo npm install -g npm@10.2.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:20.8.0
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
|
|||||||
109
changelog.md
109
changelog.md
@@ -80,6 +80,104 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Friday 13/10/2023 - v3.10.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix user preferred save location being ignored
|
||||||
|
|
||||||
|
Fixes issue [#2993](https://github.com/naturalcrit/homebrewery/issues/2993)
|
||||||
|
|
||||||
|
* [x] Fix crash to white screen when starting new brews while not signed in
|
||||||
|
|
||||||
|
Fixes issue [#2999](https://github.com/naturalcrit/homebrewery/issues/2999)
|
||||||
|
|
||||||
|
* [x] Fix FreeBSD install script
|
||||||
|
|
||||||
|
Fixes issue [#3005](https://github.com/naturalcrit/homebrewery/issues/3005)
|
||||||
|
|
||||||
|
* [x] Fix *"This brew has been changed on another device"* triggering when manually saving during auto-save
|
||||||
|
|
||||||
|
Fixes issue [#2641](https://github.com/naturalcrit/homebrewery/issues/2641)
|
||||||
|
|
||||||
|
* [x] Fix Firefox different column-flow behavior
|
||||||
|
|
||||||
|
Fixes issue [#2982](https://github.com/naturalcrit/homebrewery/issues/2982)
|
||||||
|
|
||||||
|
* [x] Fix brew titles being mis-sorted on user page
|
||||||
|
|
||||||
|
Fixes issue [#2775](https://github.com/naturalcrit/homebrewery/issues/2775)
|
||||||
|
|
||||||
|
* [x] Text Editor themes now available via new drop-down
|
||||||
|
|
||||||
|
Fixes issue [#362](https://github.com/naturalcrit/homebrewery/issues/362)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fas,fa-quote-right}} QUOTE** }} snippet for V3!
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
* [x] Several updates and fixes to FAQ and Welcome page
|
||||||
|
|
||||||
|
Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
||||||
|
[#2787](https://github.com/naturalcrit/homebrewery/issues/2787)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Add syntax highlighting for Definition Lists <code>:\:</code>
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Thursday 17/08/2023 - v3.9.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix links to certain old Google Drive files
|
||||||
|
|
||||||
|
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Menus now open on click, and internally consistent
|
||||||
|
|
||||||
|
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
|
||||||
|
|
||||||
|
* [x] Add smarter footer snippet
|
||||||
|
|
||||||
|
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
|
||||||
|
|
||||||
|
* [x] Add sanitization in Style editor
|
||||||
|
|
||||||
|
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
|
||||||
|
|
||||||
|
* [x] Rework class table snippets to remove unnecessary randomness
|
||||||
|
|
||||||
|
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
|
||||||
|
|
||||||
|
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
|
||||||
|
|
||||||
|
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
|
||||||
|
|
||||||
|
* [x] Add default save location selection to Account Page
|
||||||
|
|
||||||
|
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
|
||||||
|
|
||||||
|
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Adjustments to improve mobile viewing
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Wednesday 28/06/2023 - v3.9.1
|
### Wednesday 28/06/2023 - v3.9.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -124,6 +222,8 @@ Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
|||||||
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 12/04/2023 - v3.8.0
|
### Wednesday 12/04/2023 - v3.8.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -185,8 +285,6 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Monday 13/03/2023 - v3.7.2
|
### Monday 13/03/2023 - v3.7.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -267,7 +365,11 @@ Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
|||||||
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||||
|
|
||||||
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
{{taskList
|
||||||
##### G-Ambatte
|
##### G-Ambatte
|
||||||
|
|
||||||
* [x] Auto-compile Themes CSS on development server
|
* [x] Auto-compile Themes CSS on development server
|
||||||
@@ -277,7 +379,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
|||||||
* [x] Fix cloned brews inheriting the parent view count
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Friday 23/12/2022 - v3.5.0
|
### Friday 23/12/2022 - v3.5.0
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -1361,4 +1462,4 @@ Massive changelog incoming:
|
|||||||
|
|
||||||
* Added `phb.standalone.css` plus a build system for creating it
|
* Added `phb.standalone.css` plus a build system for creating it
|
||||||
* Added page numbers and footer text
|
* Added page numbers and footer text
|
||||||
* Page accent now flips each page
|
* Page accent now flips each page
|
||||||
@@ -147,11 +147,11 @@ const BrewRenderer = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
renderPage : function(pageText, index){
|
||||||
const cleanPageText = this.sanitizeScriptTags(pageText);
|
let cleanPageText = this.sanitizeScriptTags(pageText);
|
||||||
if(this.props.renderer == 'legacy')
|
if(this.props.renderer == 'legacy')
|
||||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
||||||
else {
|
else {
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
cleanPageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
return (
|
return (
|
||||||
<div className='page' id={`p${index + 1}`} key={index} >
|
<div className='page' id={`p${index + 1}`} key={index} >
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
|||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||||
|
|
||||||
|
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
@@ -34,12 +36,14 @@ const Editor = createClass({
|
|||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
renderer : 'legacy'
|
editorTheme : 'default',
|
||||||
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
view : 'text' //'text', 'style', 'meta'
|
editorTheme : this.props.editorTheme,
|
||||||
|
view : 'text' //'text', 'style', 'meta'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,6 +55,13 @@ const Editor = createClass({
|
|||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
|
|
||||||
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
|
if(editorTheme) {
|
||||||
|
this.setState({
|
||||||
|
editorTheme : editorTheme
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -138,6 +149,17 @@ const Editor = createClass({
|
|||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// definition lists
|
||||||
|
if(line.includes('::')){
|
||||||
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null){
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
@@ -255,6 +277,13 @@ const Editor = createClass({
|
|||||||
this.refs.codeEditor?.updateSize();
|
this.refs.codeEditor?.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateEditorTheme : function(newTheme){
|
||||||
|
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
|
||||||
|
this.setState({
|
||||||
|
editorTheme : newTheme
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||||
rerenderParent : function (){
|
rerenderParent : function (){
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
@@ -269,7 +298,8 @@ const Editor = createClass({
|
|||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onTextChange}
|
||||||
rerenderParent={this.rerenderParent}/>
|
editorTheme={this.state.editorTheme}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
@@ -281,6 +311,7 @@ const Editor = createClass({
|
|||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onStyleChange}
|
||||||
enableFolding={false}
|
enableFolding={false}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
@@ -324,6 +355,8 @@ const Editor = createClass({
|
|||||||
undo={this.undo}
|
undo={this.undo}
|
||||||
redo={this.redo}
|
redo={this.redo}
|
||||||
historySize={this.historySize()}
|
historySize={this.historySize()}
|
||||||
|
currentEditorTheme={this.state.editorTheme}
|
||||||
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
|
|||||||
@@ -1,65 +1,73 @@
|
|||||||
|
@import 'themes/codeMirror/customEditorStyles.less';
|
||||||
.editor{
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.pageLine{
|
.pageLine {
|
||||||
background : #33333328;
|
background : #33333328;
|
||||||
border-top : #339 solid 1px;
|
border-top : #333399 solid 1px;
|
||||||
}
|
}
|
||||||
.editor-page-count{
|
.editor-page-count {
|
||||||
color : grey;
|
|
||||||
float : right;
|
float : right;
|
||||||
|
color : grey;
|
||||||
}
|
}
|
||||||
.columnSplit{
|
.columnSplit {
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : grey;
|
color : grey;
|
||||||
background-color : fade(#299, 15%);
|
background-color : fade(#229999, 15%);
|
||||||
border-bottom : #299 solid 1px;
|
border-bottom : #229999 solid 1px;
|
||||||
}
|
}
|
||||||
.block:not(.cm-comment){
|
.define {
|
||||||
color : purple;
|
&:not(.term):not(.definition) {
|
||||||
|
font-weight : bold;
|
||||||
|
color : #949494;
|
||||||
|
background : #E5E5E5;
|
||||||
|
border-radius : 3px;
|
||||||
|
}
|
||||||
|
&.term { color : rgb(96, 117, 143); }
|
||||||
|
&.definition { color : rgb(97, 57, 178); }
|
||||||
|
}
|
||||||
|
.block:not(.cm-comment) {
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
|
color : purple;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.inline-block:not(.cm-comment){
|
.inline-block:not(.cm-comment) {
|
||||||
color : red;
|
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
|
color : red;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.injection:not(.cm-comment){
|
.injection:not(.cm-comment) {
|
||||||
|
font-weight : bold;
|
||||||
color : green;
|
color : green;
|
||||||
font-weight : bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
background-color : @teal;
|
right : 20px;
|
||||||
cursor : pointer;
|
bottom : 20px;
|
||||||
width : 30px;
|
z-index : 1000000;
|
||||||
height : 30px;
|
display : flex;
|
||||||
display : flex;
|
align-items : center;
|
||||||
align-items : center;
|
justify-content : center;
|
||||||
bottom : 20px;
|
width : 30px;
|
||||||
right : 20px;
|
height : 30px;
|
||||||
z-index : 1000000;
|
cursor : pointer;
|
||||||
justify-content : center;
|
background-color : @teal;
|
||||||
.tooltipLeft("Jump to brew page");
|
.tooltipLeft('Jump to brew page');
|
||||||
}
|
}
|
||||||
|
|
||||||
.editorToolbar{
|
.editorToolbar {
|
||||||
position: absolute;
|
position : absolute;
|
||||||
top: 5px;
|
top : 5px;
|
||||||
left: 50%;
|
left : 50%;
|
||||||
color: black;
|
z-index : 9;
|
||||||
font-size: 13px;
|
font-size : 13px;
|
||||||
z-index: 9;
|
color : black;
|
||||||
span {
|
span { padding : 2px 5px; }
|
||||||
padding: 2px 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./snippetbar.less');
|
require('./snippetbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -15,6 +16,8 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
|||||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||||
|
|
||||||
|
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
|
||||||
|
|
||||||
const execute = function(val, props){
|
const execute = function(val, props){
|
||||||
if(_.isFunction(val)) return val(props);
|
if(_.isFunction(val)) return val(props);
|
||||||
return val;
|
return val;
|
||||||
@@ -24,24 +27,26 @@ const Snippetbar = createClass({
|
|||||||
displayName : 'SnippetBar',
|
displayName : 'SnippetBar',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {},
|
brew : {},
|
||||||
view : 'text',
|
view : 'text',
|
||||||
onViewChange : ()=>{},
|
onViewChange : ()=>{},
|
||||||
onInject : ()=>{},
|
onInject : ()=>{},
|
||||||
onToggle : ()=>{},
|
onToggle : ()=>{},
|
||||||
showEditButtons : true,
|
showEditButtons : true,
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
undo : ()=>{},
|
undo : ()=>{},
|
||||||
redo : ()=>{},
|
redo : ()=>{},
|
||||||
historySize : ()=>{},
|
historySize : ()=>{},
|
||||||
cursorPos : {}
|
updateEditorTheme : ()=>{},
|
||||||
|
cursorPos : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
snippets : []
|
themeSelector : false,
|
||||||
|
snippets : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,6 +100,31 @@ const Snippetbar = createClass({
|
|||||||
this.props.onInject(injectedText);
|
this.props.onInject(injectedText);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleThemeSelector : function(){
|
||||||
|
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({
|
||||||
|
showThemeSelector : false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderThemeSelector : function(){
|
||||||
|
return <div className='themeSelector'>
|
||||||
|
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} onMouseDown={(this.changeTheme)}>
|
||||||
|
{EditorThemes.map((theme, key)=>{
|
||||||
|
return <option key={key} value={theme}>{theme}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
|
||||||
@@ -124,6 +154,12 @@ const Snippetbar = createClass({
|
|||||||
<i className='fas fa-redo' />
|
<i className='fas fa-redo' />
|
||||||
</div>
|
</div>
|
||||||
<div className='divider'></div>
|
<div className='divider'></div>
|
||||||
|
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
|
onClick={this.toggleThemeSelector} >
|
||||||
|
<i className='fas fa-palette' />
|
||||||
|
</div>
|
||||||
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
|
<div className='divider'></div>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
<i className='fa fa-beer' />
|
<i className='fa fa-beer' />
|
||||||
@@ -196,5 +232,4 @@ const SnippetGroup = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,15 @@
|
|||||||
color : black;
|
color : black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.editorTheme{
|
||||||
|
.tooltipLeft('Editor Themes');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : black;
|
||||||
|
&.active{
|
||||||
|
color : white;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
&.divider {
|
&.divider {
|
||||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
@@ -54,6 +63,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.themeSelector{
|
||||||
|
position: absolute;
|
||||||
|
left: -65px;
|
||||||
|
top: 30px;
|
||||||
|
z-index: 999;
|
||||||
|
width: 170px;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.snippetBarButton{
|
.snippetBarButton{
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|||||||
|
|
||||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
|
let SAVEKEY = '';
|
||||||
|
|
||||||
const AccountPage = createClass({
|
const AccountPage = createClass({
|
||||||
displayName : 'AccountPage',
|
displayName : 'AccountPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -29,6 +31,27 @@ const AccountPage = createClass({
|
|||||||
uiItems : this.props.uiItems
|
uiItems : this.props.uiItems
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
componentDidMount : function(){
|
||||||
|
if(!this.state.saveLocation && this.props.uiItems.username) {
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
|
||||||
|
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||||
|
saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||||
|
this.makeActive(saveLocation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeActive : function(newSelection){
|
||||||
|
if(this.state.saveLocation == newSelection) return;
|
||||||
|
window.localStorage.setItem(SAVEKEY, newSelection);
|
||||||
|
this.setState({
|
||||||
|
saveLocation : newSelection
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderButton : function(name, key, shouldRender=true){
|
||||||
|
if(!shouldRender) return;
|
||||||
|
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
|
||||||
|
},
|
||||||
|
|
||||||
renderNavItems : function() {
|
renderNavItems : function() {
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
@@ -61,6 +84,11 @@ const AccountPage = createClass({
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h4>Default Save Location</h4>
|
||||||
|
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
|
||||||
|
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
|
||||||
|
</div>
|
||||||
</>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const moment = require('moment');
|
|||||||
const request = require('../../../../utils/request-middleware.js');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -90,11 +91,17 @@ const BrewItem = createClass({
|
|||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderStorageIcon : function(){
|
||||||
if(!this.props.brew.googleId) return;
|
if(this.props.brew.googleId) {
|
||||||
|
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||||
|
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||||
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</a>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return <span>
|
return <span title='Homebrewery Storage'>
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
</span>;
|
</span>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ const BrewItem = createClass({
|
|||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
|
|||||||
@@ -98,4 +98,11 @@
|
|||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
|
.homebreweryIcon {
|
||||||
|
mix-blend-mode : darken;
|
||||||
|
height : 24px;
|
||||||
|
position : relative;
|
||||||
|
top : 5px;
|
||||||
|
left : -5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const ListPage = createClass({
|
|||||||
sortBrewOrder : function(brew){
|
sortBrewOrder : function(brew){
|
||||||
if(!brew.title){brew.title = 'No Title';}
|
if(!brew.title){brew.title = 'No Title';}
|
||||||
const mapping = {
|
const mapping = {
|
||||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||||
'created' : moment(brew.createdAt).format(),
|
'created' : moment(brew.createdAt).format(),
|
||||||
'updated' : moment(brew.updatedAt).format(),
|
'updated' : moment(brew.updatedAt).format(),
|
||||||
'views' : brew.views,
|
'views' : brew.views,
|
||||||
|
|||||||
@@ -16,6 +16,23 @@
|
|||||||
margin : 5px 0px;
|
margin : 5px 0px;
|
||||||
border : 2px solid black;
|
border : 2px solid black;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
|
button {
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
width : 125px;
|
||||||
|
color : black;
|
||||||
|
margin-right : 5px;
|
||||||
|
&.active {
|
||||||
|
background-color: #0007;
|
||||||
|
color: white;
|
||||||
|
&:before {
|
||||||
|
content: '\f00c';
|
||||||
|
font-family: 'FONT AWESOME 5 FREE';
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h1, h2, h3, h4 {
|
h1, h2, h3, h4 {
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const EditPage = createClass({
|
|||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
if(e.keyCode == S_KEY) this.trySave(true);
|
||||||
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -137,13 +137,14 @@ const EditPage = createClass({
|
|||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
this.debounceSave();
|
this.debounceSave();
|
||||||
} else {
|
} else {
|
||||||
this.debounceSave.cancel();
|
this.debounceSave.cancel();
|
||||||
}
|
}
|
||||||
|
if(immediate) this.debounceSave.flush();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
handleGoogleClick : function(){
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ The Homebrewery makes the creation and sharing of authentic looking Fifth-Editio
|
|||||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||||
|
|
||||||
### Editing and Sharing
|
### Editing and Sharing
|
||||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
When you create a new homebrew document ("brew"), your document will be given a *edit link* and a *share link*.
|
||||||
|
|
||||||
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
The *edit link* is where you write your brew. If you edit a brew while logged in, you are added as one of the brew's authors, and no one else can edit that brew until you add them as a new author via the {{fa,fa-info-circle}} **Properties** tab. Brews without any author can still be edited by anyone with the *edit link*, so be careful about who you share it with if you prefer to work without an account.
|
||||||
|
|
||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
@@ -48,57 +48,63 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
|||||||
|
|
||||||
\column
|
\column
|
||||||
|
|
||||||
## New in V3.0.0
|
## V3 vs Legacy
|
||||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
The Homebrewery has two renderers: Legacy and V3. The V3 renderer is recommended for all users because it is more powerful, more customizable, and continues to receive new feature updates while Legacy does not. However Legacy mode will remain available for older brews and veteran users.
|
||||||
|
|
||||||
|
At any time, any individual brew can be changed to your renderer of choice via the {{fa,fa-info-circle}} **Properties** tab on your brew. However, converting between Legacy and V3 may require heavily tweaking the document; while both renderers can use raw HTML, V3 prefers a streamlined curly bracket syntax that avoids the complex HTML structures required by Legacy.
|
||||||
|
|
||||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
|
||||||
|
|
||||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
|
||||||
|
|
||||||
|
Scroll down to the next page for a brief summary of the changes and features available in V3!
|
||||||
#### New Things All The Time!
|
#### New Things All The Time!
|
||||||
Check out the latest updates in the full changelog [here](/changelog).
|
Check out the latest updates in the full changelog [here](/changelog).
|
||||||
|
|
||||||
### Helping out
|
### Helping out
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
Like this tool? Head over to our [Patreon](https://www.patreon.com/Naturalcrit) to help us keep the servers running.
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
|
||||||
|
This tool will **always** be free, never have ads, and we will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
### Bugs, Issues, Suggestions?
|
||||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
- Check the [Frequently Asked Questions](/faq) page first for quick answers.
|
||||||
|
- Get help or the right look for your brew by posting on [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) or joining the [Discord Of Many Things](https://discord.gg/by3deKx).
|
||||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
- Report technical issues or provide feedback on the [GitHub Repo](https://github.com/naturalcrit/homebrewery/).
|
||||||
|
|
||||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
|
||||||
|
|
||||||
### Legal Junk
|
### Legal Junk
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
#### Crediting Us
|
||||||
#### Crediting Me
|
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
|
||||||
|
|
||||||
### More Homebrew Resources
|
### More Homebrew Resources
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
|
||||||
{{position:absolute;top:20px;right:20px;width:auto
|
{{position:absolute;top:20px;right:20px;width:auto
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
[{height:30px}](https://discord.gg/by3deKx)
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Markdown+
|
## Markdown+
|
||||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
From version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
|
||||||
|
|
||||||
### Curly Brackets
|
### Curly Brackets
|
||||||
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
Standard Markdown lacks several equivalences to HTML. Hence, we have introduced `{{ }}` as a replacement for `<span></span>` and `<div></div>` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as CSS properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||||
|
|
||||||
#### Span
|
#### Span
|
||||||
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||||
@@ -126,16 +132,17 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
Much nicer than `<br><br><br><br><br>`
|
Much nicer than `<br><br><br><br><br>`
|
||||||
|
|
||||||
### Definition Lists
|
### Definition Lists
|
||||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Column Breaks
|
### Column Breaks
|
||||||
Column and page breaks with `\column` and `\page`.
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
@@ -163,13 +170,13 @@ Using *Curly Injection* you can assign an id, classes, or inline CSS properties
|
|||||||
|
|
||||||
 {width:100px,border:"2px solid",border-radius:10px}
|
 {width:100px,border:"2px solid",border-radius:10px}
|
||||||
|
|
||||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
|
||||||
|
|
||||||
## Snippets
|
## Snippets
|
||||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||||
|
|
||||||
## Style Editor Panel
|
## Style Editor Panel
|
||||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
{{fa,fa-paint-brush}} Usually overlooked or unused by some users, the **Style Editor** tab is located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
{{pageNumber 2}}
|
{{pageNumber 2}}
|
||||||
{{footnote PART 2 | BORING STUFF}}
|
{{footnote PART 2 | BORING STUFF}}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
let SAVEKEY;
|
||||||
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
@@ -62,12 +63,16 @@ const NewPage = createClass({
|
|||||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||||
|
|
||||||
this.setState({
|
|
||||||
brew : brew
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||||
|
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
brew : brew,
|
||||||
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
|
});
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
|
|||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||||
|
|||||||
25
faq.md
25
faq.md
@@ -62,16 +62,13 @@ pre {
|
|||||||
```
|
```
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
{{wide Updated Oct. 11, 2021}}
|
{{wide Updated Apr. 15, 2023}}
|
||||||
|
|
||||||
|
|
||||||
### The site is down for me! Anyone else?
|
### The site is down for me! Anyone else?
|
||||||
|
|
||||||
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||||
|
|
||||||
### How do I log out?
|
|
||||||
|
|
||||||
Go to https://homebrewery.naturalcrit.com/login, and hit the "*logout*" link.
|
|
||||||
|
|
||||||
### Why am I getting an error when trying to save, and my account is linked to Google?
|
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||||
|
|
||||||
@@ -120,26 +117,6 @@ The fonts used were originally created for use with the English language, though
|
|||||||
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
||||||
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
||||||
|
|
||||||
### The preview window is suddenly gone, I can only see the editor side of the Homebrewery (or the other way around).
|
|
||||||
|
|
||||||
1. Press `CTRL`+`SHIFT`+`i` (or right-click and select "Inspect") while in the Homebrewery.
|
|
||||||
|
|
||||||
2. Expand...
|
|
||||||
```
|
|
||||||
- `body`
|
|
||||||
- `main`
|
|
||||||
- `div class="homebrew"`
|
|
||||||
- `div class="editPage page"`
|
|
||||||
- `div class="content"`
|
|
||||||
- `div class="splitPane"`
|
|
||||||
```
|
|
||||||
|
|
||||||
There you will find 3 divs: `div class="pane" [...]`, `div class="divider" [...]`, and `div class="pane" [...]`.
|
|
||||||
|
|
||||||
The `class="pane"` looks similar to this: `div class="pane" data-reactid="36" style="flex: 0 0 auto; width: 925px;"`.
|
|
||||||
|
|
||||||
Change whatever stands behind width: to something smaller than your display width.
|
|
||||||
|
|
||||||
### I have white borders on the bottom/sides of the print preview.
|
### I have white borders on the bottom/sides of the print preview.
|
||||||
|
|
||||||
The Homebrewery paper size and your print paper size do not match.
|
The Homebrewery paper size and your print paper size do not match.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ npm install
|
|||||||
npm audit fix
|
npm audit fix
|
||||||
npm run postinstall
|
npm run postinstall
|
||||||
|
|
||||||
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||||
chmod +x /usr/local/etc/rc.d/homebrewery
|
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||||
|
|
||||||
sysrc homebrewery_enable=YES
|
sysrc homebrewery_enable=YES
|
||||||
|
|||||||
5401
package-lock.json
generated
5401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.9.1",
|
"version": "3.10.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.16.x"
|
"npm": "^10.2.x",
|
||||||
|
"node": ">=20.8.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -78,11 +79,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.8",
|
"@babel/core": "^7.23.2",
|
||||||
"@babel/plugin-transform-runtime": "^7.22.7",
|
"@babel/plugin-transform-runtime": "^7.23.2",
|
||||||
"@babel/preset-env": "^7.22.7",
|
"@babel/preset-env": "^7.23.2",
|
||||||
"@babel/preset-react": "^7.22.5",
|
"@babel/preset-react": "^7.22.15",
|
||||||
"@googleapis/drive": "^5.1.0",
|
"@googleapis/drive": "^8.4.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
@@ -98,31 +99,30 @@
|
|||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "5.1.1",
|
"marked": "5.1.1",
|
||||||
"marked-extended-tables": "^1.0.6",
|
"marked-extended-tables": "^1.0.7",
|
||||||
"marked-gfm-heading-id": "^3.0.4",
|
"marked-gfm-heading-id": "^3.1.0",
|
||||||
"marked-smartypants-lite": "^1.0.0",
|
"marked-smartypants-lite": "^1.0.1",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^7.3.2",
|
"mongoose": "^7.6.1",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"npm": "^9.8.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.14.1",
|
"react-router-dom": "6.16.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^8.1.2",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.51.0",
|
||||||
"eslint-plugin-jest": "^27.2.2",
|
"eslint-plugin-jest": "^27.4.2",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"jest": "^29.6.1",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^15.10.3",
|
||||||
"stylelint-config-recess-order": "^4.3.0",
|
"stylelint-config-recess-order": "^4.3.0",
|
||||||
"stylelint-config-recommended": "^13.0.0",
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
"stylelint-stylistic": "^0.4.3",
|
"stylelint-stylistic": "^0.4.3",
|
||||||
|
|||||||
@@ -99,6 +99,24 @@ fs.emptyDirSync('./build');
|
|||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
await fs.copy('./client/icons', './build/icons');
|
await fs.copy('./client/icons', './build/icons');
|
||||||
|
|
||||||
|
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
|
||||||
|
|
||||||
|
editorThemeFiles = fs.readdirSync('./node_modules/codemirror/theme');
|
||||||
|
|
||||||
|
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||||
|
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||||
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
|
for (themeFile of editorThemeFiles) {
|
||||||
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
|
}
|
||||||
|
stream.write('\n]\n');
|
||||||
|
stream.end();
|
||||||
|
|
||||||
|
await fs.copy('./node_modules/codemirror/theme', './build/homebrew/cm-themes');
|
||||||
|
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
|
||||||
|
|
||||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||||
|
|
||||||
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
const bundles = await pack('./client/homebrew/homebrew.jsx', {
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
brew.pageCount = googleBrews[match].pageCount;
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
brew.renderer = googleBrews[match].renderer;
|
brew.renderer = googleBrews[match].renderer;
|
||||||
brew.version = googleBrews[match].version;
|
brew.version = googleBrews[match].version;
|
||||||
|
brew.webViewLink = googleBrews[match].webViewLink;
|
||||||
googleBrews.splice(match, 1);
|
googleBrews.splice(match, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +268,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.brews = _.map(brews, (brew)=>{
|
req.brews = _.map(brews, (brew)=>{
|
||||||
|
// Clean up brew data
|
||||||
|
brew.title = brew.title?.trim();
|
||||||
|
brew.description = brew.description?.trim();
|
||||||
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const GoogleActions = {
|
|||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 1000,
|
pageSize : 1000,
|
||||||
pageToken : NextPageToken || '',
|
pageToken : NextPageToken || '',
|
||||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties, webViewLink)',
|
||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -139,7 +139,8 @@ const GoogleActions = {
|
|||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : [],
|
systems : [],
|
||||||
lang : file.properties.lang,
|
lang : file.properties.lang,
|
||||||
thumbnail : file.properties.thumbnail
|
thumbnail : file.properties.thumbnail,
|
||||||
|
webViewLink : file.webViewLink
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ const api = {
|
|||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
_.defaults(brew, DEFAULT_BREW);
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim();
|
||||||
},
|
},
|
||||||
newGoogleBrew : async (account, brew, res)=>{
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
@@ -217,6 +220,8 @@ const api = {
|
|||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
|
brew.title = brew.title.trim();
|
||||||
|
brew.description = brew.description.trim() || '';
|
||||||
brew.text = api.mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
let CodeMirror;
|
|
||||||
if(typeof navigator !== 'undefined'){
|
|
||||||
CodeMirror = require('codemirror');
|
|
||||||
|
|
||||||
//Language Modes
|
|
||||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
|
||||||
require('codemirror/mode/css/css.js');
|
|
||||||
require('codemirror/mode/javascript/javascript.js');
|
|
||||||
|
|
||||||
//Addons
|
|
||||||
//Code folding
|
|
||||||
require('codemirror/addon/fold/foldcode.js');
|
|
||||||
require('codemirror/addon/fold/foldgutter.js');
|
|
||||||
//Search and replace
|
|
||||||
require('codemirror/addon/search/search.js');
|
|
||||||
require('codemirror/addon/search/searchcursor.js');
|
|
||||||
require('codemirror/addon/search/jump-to-line.js');
|
|
||||||
require('codemirror/addon/search/match-highlighter.js');
|
|
||||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
|
||||||
require('codemirror/addon/dialog/dialog.js');
|
|
||||||
//Trailing space highlighting
|
|
||||||
// require('codemirror/addon/edit/trailingspace.js');
|
|
||||||
//Active line highlighting
|
|
||||||
// require('codemirror/addon/selection/active-line.js');
|
|
||||||
//Scroll past last line
|
|
||||||
require('codemirror/addon/scroll/scrollpastend.js');
|
|
||||||
//Auto-closing
|
|
||||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
|
||||||
require('codemirror/addon/fold/xml-fold.js');
|
|
||||||
require('codemirror/addon/edit/closetag.js');
|
|
||||||
|
|
||||||
const foldCode = require('./helpers/fold-code');
|
|
||||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = CodeMirror;
|
|
||||||
@@ -4,15 +4,45 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const closeTag = require('./helpers/close-tag');
|
const closeTag = require('./close-tag');
|
||||||
const Hints = require('./helpers/widget-elements/hints/hints.jsx');
|
|
||||||
const CodeMirror = require('./code-mirror.js');
|
|
||||||
|
|
||||||
const themeWidgets = require('../../../themes/V3/5ePHB/widgets');
|
let CodeMirror;
|
||||||
|
if(typeof navigator !== 'undefined'){
|
||||||
|
CodeMirror = require('codemirror');
|
||||||
|
|
||||||
|
//Language Modes
|
||||||
|
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||||
|
require('codemirror/mode/css/css.js');
|
||||||
|
require('codemirror/mode/javascript/javascript.js');
|
||||||
|
|
||||||
|
//Addons
|
||||||
|
//Code folding
|
||||||
|
require('codemirror/addon/fold/foldcode.js');
|
||||||
|
require('codemirror/addon/fold/foldgutter.js');
|
||||||
|
//Search and replace
|
||||||
|
require('codemirror/addon/search/search.js');
|
||||||
|
require('codemirror/addon/search/searchcursor.js');
|
||||||
|
require('codemirror/addon/search/jump-to-line.js');
|
||||||
|
require('codemirror/addon/search/match-highlighter.js');
|
||||||
|
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||||
|
require('codemirror/addon/dialog/dialog.js');
|
||||||
|
//Trailing space highlighting
|
||||||
|
// require('codemirror/addon/edit/trailingspace.js');
|
||||||
|
//Active line highlighting
|
||||||
|
// require('codemirror/addon/selection/active-line.js');
|
||||||
|
//Scroll past last line
|
||||||
|
require('codemirror/addon/scroll/scrollpastend.js');
|
||||||
|
//Auto-closing
|
||||||
|
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||||
|
require('codemirror/addon/fold/xml-fold.js');
|
||||||
|
require('codemirror/addon/edit/closetag.js');
|
||||||
|
|
||||||
|
const foldCode = require('./fold-code');
|
||||||
|
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||||
|
}
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
displayName : 'CodeEditor',
|
displayName : 'CodeEditor',
|
||||||
hintsRef : React.createRef(),
|
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
language : '',
|
language : '',
|
||||||
@@ -20,16 +50,13 @@ const CodeEditor = createClass({
|
|||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
enableFolding : true,
|
enableFolding : true,
|
||||||
|
editorTheme : 'default'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
docs : {},
|
docs : {}
|
||||||
widgetUtils : {},
|
|
||||||
widgets : {},
|
|
||||||
hints : [],
|
|
||||||
hintsField : undefined,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -66,8 +93,9 @@ const CodeEditor = createClass({
|
|||||||
this.codeMirror.setOption('foldOptions', false);
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.widgetUtils.updateWidgetGutter();
|
if(prevProps.editorTheme !== this.props.editorTheme){
|
||||||
this.state.widgetUtils.updateAllLineWidgets();
|
this.codeMirror.setOption('theme', this.props.editorTheme);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildEditor : function() {
|
buildEditor : function() {
|
||||||
@@ -132,10 +160,11 @@ const CodeEditor = createClass({
|
|||||||
},
|
},
|
||||||
foldGutter : true,
|
foldGutter : true,
|
||||||
foldOptions : this.foldOptions(this.codeMirror),
|
foldOptions : this.foldOptions(this.codeMirror),
|
||||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'widget-gutter'],
|
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
autoCloseTags : true,
|
autoCloseTags : true,
|
||||||
styleActiveLine : true,
|
styleActiveLine : true,
|
||||||
showTrailingSpace : false,
|
showTrailingSpace : false,
|
||||||
|
theme : this.props.editorTheme
|
||||||
// specialChars : / /,
|
// specialChars : / /,
|
||||||
// specialCharPlaceholder : function(char) {
|
// specialCharPlaceholder : function(char) {
|
||||||
// const el = document.createElement('span');
|
// const el = document.createElement('span');
|
||||||
@@ -146,69 +175,9 @@ const CodeEditor = createClass({
|
|||||||
});
|
});
|
||||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||||
|
|
||||||
this.setState({
|
|
||||||
widgetUtils : require('./helpers/widgets')(themeWidgets, this.codeMirror, (hints, field)=>{
|
|
||||||
this.setState({
|
|
||||||
hints,
|
|
||||||
hintsField : field
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||||
this.codeMirror.on('change', (cm)=>{
|
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||||
this.props.onChange(cm.getValue());
|
|
||||||
this.state.widgetUtils.updateWidgetGutter();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.codeMirror.on('cursorActivity', (cm)=>{
|
|
||||||
const { line } = cm.getCursor();
|
|
||||||
for (const key in this.state.widgets) {
|
|
||||||
if(key != line) {
|
|
||||||
this.state.widgetUtils.removeLineWidget(key, this.state.widgets[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
hints : [],
|
|
||||||
hintsField : undefined
|
|
||||||
});
|
|
||||||
const { widgets } = this.codeMirror.lineInfo(line);
|
|
||||||
if(!widgets) {
|
|
||||||
const widget = this.state.widgetUtils.updateLineWidgets(line);
|
|
||||||
if(widget) {
|
|
||||||
this.setState({
|
|
||||||
widgets : {
|
|
||||||
[line] : widget
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateSize();
|
this.updateSize();
|
||||||
|
|
||||||
this.codeMirror.on('gutterClick', (cm, n)=>{
|
|
||||||
// Open line widgets when 'widget-gutter' marker clicked
|
|
||||||
if(this.codeMirror.lineInfo(n).gutterMarkers?.['widget-gutter']) {
|
|
||||||
const { widgets } = this.codeMirror.lineInfo(n);
|
|
||||||
if(!widgets) {
|
|
||||||
const widget = this.state.widgetUtils.updateLineWidgets(n);
|
|
||||||
if(widget) {
|
|
||||||
this.setState({
|
|
||||||
widgets : { ...this.state.widgets, [n]: widget }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const widget of widgets) {
|
|
||||||
this.state.widgetUtils.removeLineWidget(n, widget);
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
hints : [],
|
|
||||||
hintsField : undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
indent : function () {
|
indent : function () {
|
||||||
@@ -440,19 +409,13 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyDown : function(e) {
|
|
||||||
if(this.hintsRef.current) {
|
|
||||||
this.hintsRef.current.keyDown(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
//----------------------//
|
//----------------------//
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const { hints, hintsField } = this.state;
|
return <>
|
||||||
return <React.Fragment>
|
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
|
||||||
<div className='codeEditor' ref='editor' style={this.props.style} onKeyDown={this.keyDown}/>
|
<div className='codeEditor' ref='editor' style={this.props.style}/>
|
||||||
<Hints ref={this.hintsRef} hints={hints} field={hintsField}/>
|
</>;
|
||||||
</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||||
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||||
@import (less) 'codemirror/addon/dialog/dialog.css';
|
@import (less) 'codemirror/addon/dialog/dialog.css';
|
||||||
@import (less) 'codemirror/addon/hint/show-hint.css';
|
|
||||||
@import 'naturalcrit/styles/colors.less';
|
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% {background-color: red; color: white;}
|
||||||
@@ -11,12 +9,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
|
@media screen and (pointer : coarse) {
|
||||||
|
font-size : 16px;
|
||||||
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.sourceMoveFlash .CodeMirror-line{
|
||||||
animation-name: sourceMoveAnimation;
|
animation-name: sourceMoveAnimation;
|
||||||
@@ -32,51 +33,4 @@
|
|||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
.widget-gutter {
|
|
||||||
width: .7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snippet-options-widget {
|
|
||||||
padding: 2px 0;
|
|
||||||
|
|
||||||
>div {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0 2px 2px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
max-width: 10vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-field {
|
|
||||||
border: 2px solid #ddd;
|
|
||||||
|
|
||||||
&.default {
|
|
||||||
background-color: @purple;
|
|
||||||
border: 2px solid @purple;
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
>input {
|
|
||||||
background-color: @purple;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.suggested {
|
|
||||||
background-color: #ddd;
|
|
||||||
border: 2px dashed grey;
|
|
||||||
color: grey;
|
|
||||||
|
|
||||||
>input {
|
|
||||||
background-color: #ddd;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
require('./checkbox.less');
|
|
||||||
const CodeMirror = require('../../../code-mirror.js');
|
|
||||||
|
|
||||||
const Checkbox = createClass({
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
value : '',
|
|
||||||
prefix : '',
|
|
||||||
cm : {},
|
|
||||||
n : -1,
|
|
||||||
default : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChange : function (e) {
|
|
||||||
const { cm, n, value, prefix } = this.props;
|
|
||||||
const { text } = cm.lineInfo(n);
|
|
||||||
const updatedPrefix = `{{${prefix}`;
|
|
||||||
if(e.target?.checked)
|
|
||||||
cm.replaceRange(`,${value}`, CodeMirror.Pos(n, updatedPrefix.length), CodeMirror.Pos(n, updatedPrefix.length), '+insert');
|
|
||||||
else {
|
|
||||||
const start = text.indexOf(`,${value}`);
|
|
||||||
if(start > -1)
|
|
||||||
cm.replaceRange('', CodeMirror.Pos(n, start), CodeMirror.Pos(n, start + value.length + 1), '-delete');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function() {
|
|
||||||
const { cm, n, value, prefix, def } = this.props;
|
|
||||||
const { text } = cm.lineInfo(n);
|
|
||||||
const id = [prefix, value, n].join('-');
|
|
||||||
let className = 'widget-field widget-checkbox';
|
|
||||||
if(def) {
|
|
||||||
className += ' default';
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
|
||||||
<div className={className}>
|
|
||||||
<input type='checkbox' id={id} onChange={this.handleChange} checked={_.includes(text, `,${value}`)}/>
|
|
||||||
<label htmlFor={id}>{_.startCase(value)}</label>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Checkbox;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.widget-checkbox {
|
|
||||||
display: inline-block;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
background-color: #ddd;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 4px 2px;
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
require('./color-selector.less');
|
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const { PATTERNS, STYLE_FN, SNIPPET_TYPE } = require('../constants');
|
|
||||||
const CodeMirror = require('../../../code-mirror');
|
|
||||||
const debounce = require('lodash.debounce');
|
|
||||||
|
|
||||||
const ColorSelector = createClass({
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
field : {},
|
|
||||||
cm : {},
|
|
||||||
n : undefined,
|
|
||||||
text : '',
|
|
||||||
def : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
value : ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentDidMount : function() {
|
|
||||||
const { field, text } = this.props;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, __, value] = text.match(pattern) ?? [];
|
|
||||||
this.setState({
|
|
||||||
value : value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentDidUpdate({ text }) {
|
|
||||||
const { field } = this.props;
|
|
||||||
if(this.props.text !== text) {
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, __, value] = this.props.text.match(pattern) ?? [];
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChange : function(e) {
|
|
||||||
const { cm, text, field, n, snippetType } = this.props;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, label, current] = text.match(pattern) ?? [null, field.name, ''];
|
|
||||||
let index = text.indexOf(`${label}:${current}`);
|
|
||||||
while (index !== -1 && text[index - 1] === '-') {
|
|
||||||
index = text.indexOf(`${label}:${current}`, index + 1);
|
|
||||||
}
|
|
||||||
let value = e.target.value;
|
|
||||||
if(index === -1) {
|
|
||||||
if(snippetType === SNIPPET_TYPE.INLINE) {
|
|
||||||
index = text.indexOf('}');
|
|
||||||
}
|
|
||||||
index = index === -1 ? text.length : index;
|
|
||||||
value = `,${field.name}:${value}`;
|
|
||||||
} else {
|
|
||||||
index = index + 1 + field.name.length;
|
|
||||||
}
|
|
||||||
cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + current.length), '+insert');
|
|
||||||
this.setState({
|
|
||||||
value : e.target.value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
debounce : debounce((self, e)=>self.onChange(e), 300),
|
|
||||||
onChangeDebounce : function(e) {
|
|
||||||
this.setState({
|
|
||||||
value : e.target.value,
|
|
||||||
});
|
|
||||||
this.debounce(this, e);
|
|
||||||
},
|
|
||||||
render : function() {
|
|
||||||
const { field, n, text, def } = this.props;
|
|
||||||
const { value } = this.state;
|
|
||||||
const style = STYLE_FN(value);
|
|
||||||
const id = `${field?.name}-${n}`;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, label, __] = text.match(pattern) ?? [null, undefined, ''];
|
|
||||||
let className = 'widget-field color-selector';
|
|
||||||
if(!label) {
|
|
||||||
className += ' suggested';
|
|
||||||
}
|
|
||||||
if(def) {
|
|
||||||
className += ' default';
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
|
||||||
<div className={className}>
|
|
||||||
<label htmlFor={id}>{field.name}:</label>
|
|
||||||
<input className='color' type='color' value={value} onChange={this.onChangeDebounce}/>
|
|
||||||
<input id={id} className='text' type='text' style={style} value={value} onChange={this.onChange}/>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ColorSelector;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
.color-selector {
|
|
||||||
.color {
|
|
||||||
height: 17px;
|
|
||||||
width: 13px;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
export const UNITS = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'];
|
|
||||||
|
|
||||||
export const HINT_TYPE = {
|
|
||||||
VALUE : 0,
|
|
||||||
NUMBER_SUFFIX : 1
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SNIPPET_TYPE = {
|
|
||||||
BLOCK : 0,
|
|
||||||
INLINE : 1,
|
|
||||||
INJECTOR : 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FIELD_TYPE = {
|
|
||||||
TEXT : 0,
|
|
||||||
CHECKBOX : 1,
|
|
||||||
IMAGE_SELECTOR : 2,
|
|
||||||
COLOR_SELECTOR : 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const textField = (name)=>new RegExp(`[{,;](${name}):([^};,"\\(]*\\((?!,)[^};"\\)]*\\)|"[^},;"]*"|[^},;]*)`);
|
|
||||||
export const PATTERNS = {
|
|
||||||
snippet : {
|
|
||||||
[SNIPPET_TYPE.BLOCK] : (name)=>new RegExp(`^{{${name}(?:[^a-zA-Z].*)?`),
|
|
||||||
[SNIPPET_TYPE.INLINE] : (name)=>new RegExp(`{{${name}`),
|
|
||||||
[SNIPPET_TYPE.INJECTOR] : ()=>new RegExp(`^\\!\\[(?:[a-zA-Z -]+)?\\]\\(.*\\).*{[a-zA-Z0-9:, "'-]+}$`),
|
|
||||||
},
|
|
||||||
field : {
|
|
||||||
[FIELD_TYPE.TEXT] : textField,
|
|
||||||
[FIELD_TYPE.IMAGE_SELECTOR] : (name)=>new RegExp(`{{(${name})(\\d*)`),
|
|
||||||
[FIELD_TYPE.COLOR_SELECTOR] : textField
|
|
||||||
},
|
|
||||||
collectStyles : new RegExp(`(?:([a-zA-Z-]+):(?!\\/))+`, 'g'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NUMBER_PATTERN = new RegExp(`([^-\\d]*)([-\\d]+)(${UNITS.join('|')})?(.*)`);
|
|
||||||
|
|
||||||
export const fourDigitNumberFromValue = (value)=>typeof value === 'number' ? (()=>{
|
|
||||||
const str = String(value);
|
|
||||||
return _.range(0, 4 - str.length).map(()=>'0').join('') + str;
|
|
||||||
})() : value;
|
|
||||||
|
|
||||||
const DEFAULT_WIDTH = '30px';
|
|
||||||
|
|
||||||
export const STYLE_FN = (value, extras = {})=>({
|
|
||||||
width : `calc(${value?.length ?? 0}ch + ${value?.length ? `${DEFAULT_WIDTH} / 2` : DEFAULT_WIDTH})`,
|
|
||||||
...extras
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const { NUMBER_PATTERN } = require('../constants');
|
|
||||||
|
|
||||||
const Hints = createClass({
|
|
||||||
hintsRef : React.createRef(),
|
|
||||||
activeHintRef : React.createRef(),
|
|
||||||
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
hints : [],
|
|
||||||
field : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
activeHint : 0
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function({ hints }) {
|
|
||||||
const hintsLength = this.props.hints.length;
|
|
||||||
if(hintsLength - 1 < this.state.activeHint && hintsLength !== hints.length) {
|
|
||||||
this.setState({
|
|
||||||
activeHint : hintsLength === 0 ? 0 : hintsLength - 1
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hintsRef.current && this.activeHintRef.current) {
|
|
||||||
const offset = this.activeHintRef.current.offsetTop;
|
|
||||||
const scrollTop = this.hintsRef.current.scrollTop;
|
|
||||||
if(scrollTop + 50 < offset || scrollTop + 50 > offset) {
|
|
||||||
this.hintsRef.current.scrollTo({
|
|
||||||
top : offset - 50,
|
|
||||||
behavior : 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {},
|
|
||||||
|
|
||||||
keyDown : function(e) {
|
|
||||||
const { code } = e;
|
|
||||||
const { activeHint } = this.state;
|
|
||||||
const { hints, field } = this.props;
|
|
||||||
const match = field?.state?.value?.match(NUMBER_PATTERN);
|
|
||||||
if(code === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
if(!match || !match?.at(3)) {
|
|
||||||
this.setState({
|
|
||||||
activeHint : activeHint === hints.length - 1 ? 0 : activeHint + 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if(code === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
if(!match || !match?.at(3)) {
|
|
||||||
this.setState({
|
|
||||||
activeHint : activeHint === 0 ? hints.length - 1 : activeHint - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if(code === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if(!match || !match?.at(3)) {
|
|
||||||
field?.hintSelected(hints[activeHint]);
|
|
||||||
this.setState({
|
|
||||||
activeHint : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function() {
|
|
||||||
const { activeHint } = this.state;
|
|
||||||
const { hints, field } = this.props;
|
|
||||||
if(!field) return null;
|
|
||||||
const bounds = field.fieldRef[field.state.id]?.current?.getBoundingClientRect();
|
|
||||||
if(!bounds) return null;
|
|
||||||
|
|
||||||
const hintElements = hints
|
|
||||||
.filter((h)=>h.hint !== field.state.value)
|
|
||||||
.map((h, i)=>{
|
|
||||||
let className = 'CodeMirror-hint';
|
|
||||||
if(activeHint === i) {
|
|
||||||
className += ' CodeMirror-hint-active';
|
|
||||||
return <li key={i}
|
|
||||||
role={'option'}
|
|
||||||
className={className}
|
|
||||||
onMouseDown={(e)=>field.hintSelected(h, e)}
|
|
||||||
ref={this.activeHintRef}>
|
|
||||||
{h.hint}
|
|
||||||
</li>;
|
|
||||||
}
|
|
||||||
return <li key={i}
|
|
||||||
role={'option'}
|
|
||||||
className={className}
|
|
||||||
onMouseDown={(e)=>field.hintSelected(h, e)}>
|
|
||||||
{h.hint}
|
|
||||||
</li>;
|
|
||||||
});
|
|
||||||
|
|
||||||
let style = {
|
|
||||||
display : 'none'
|
|
||||||
};
|
|
||||||
if(hintElements.length > 0) {
|
|
||||||
style = {
|
|
||||||
...style,
|
|
||||||
display : 'block',
|
|
||||||
top : `${bounds.top - 5}px`,
|
|
||||||
left : `${bounds.left}px`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
|
||||||
<ul role={'listbox'}
|
|
||||||
id={'hints'}
|
|
||||||
aria-expanded={true}
|
|
||||||
className={'CodeMirror-hints default'}
|
|
||||||
style={style}
|
|
||||||
ref={this.hintsRef}>
|
|
||||||
{hintElements}
|
|
||||||
</ul>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Hints;
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
require('./image-selector.less');
|
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const { Modal, modalHelpers } = require('../modal/modal.jsx');
|
|
||||||
const { PATTERNS } = require('../constants.js');
|
|
||||||
const CodeMirror = require('../../../code-mirror.js');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ImageSelector = createClass({
|
|
||||||
modalRef : React.createRef(),
|
|
||||||
|
|
||||||
getDefaultProps : function () {
|
|
||||||
return {
|
|
||||||
field : {},
|
|
||||||
cm : {},
|
|
||||||
n : undefined
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
selected : undefined,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
modalHelpers.mount(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function() {
|
|
||||||
const { name, preview, values } = this.props.field;
|
|
||||||
const { selected } = this.state;
|
|
||||||
|
|
||||||
const images = values.map((v, i)=>{
|
|
||||||
const className = String(selected) === String(v) ? 'selected' : '';
|
|
||||||
return <img key={i} className={className} src={preview(v)} alt={`${name} image ${v}`} onClick={()=>this.select(v)}/>;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state.modalRoot?.render(<Modal ref={this.modalRef} header={_.startCase(name)} save={this.save}>
|
|
||||||
<div className={'images'}>
|
|
||||||
{images}
|
|
||||||
</div>
|
|
||||||
</Modal>);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
modalHelpers.unmount(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
save : function() {
|
|
||||||
const { cm, field, n } = this.props;
|
|
||||||
const { text } = cm.lineInfo(n);
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [fullmatch, label, current] = text.match(pattern);
|
|
||||||
if(!fullmatch) {
|
|
||||||
console.warn('something is wrong... please report this warning with a screenshot');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentText = `${label}${current ?? ''}`;
|
|
||||||
const index = 2;
|
|
||||||
const value = label + this.state.selected;
|
|
||||||
cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + currentText.length), '+insert');
|
|
||||||
},
|
|
||||||
|
|
||||||
select : function(value) {
|
|
||||||
this.setState({
|
|
||||||
selected : value
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showModal : function() {
|
|
||||||
const { cm, field, n } = this.props;
|
|
||||||
const { text } = cm.lineInfo(n);
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [fullmatch, _, current] = text.match(pattern);
|
|
||||||
if(!fullmatch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
selected : current
|
|
||||||
});
|
|
||||||
this.modalRef.current.setVisible(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function () {
|
|
||||||
return <React.Fragment>
|
|
||||||
<button onClick={this.showModal}>Select Image</button>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ImageSelector;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
.images {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
max-height: 60vh;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
img {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
max-width: 10vw;
|
|
||||||
max-height: 20vh;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, .1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: rgba(0, 0, 0, .175);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
const Text = require('./text/text.jsx');
|
|
||||||
const Checkbox = require('./checkbox/checkbox.jsx');
|
|
||||||
const ImageSelector = require('./image-selector/image-selector.jsx');
|
|
||||||
const ColorSelector = require('./color-selector/color-selector.jsx');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Text : Text,
|
|
||||||
Checkbox : Checkbox,
|
|
||||||
ImageSelector : ImageSelector,
|
|
||||||
ColorSelector : ColorSelector,
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
require('./modal.less');
|
|
||||||
const React = require('react');
|
|
||||||
const ReactDOMClient = require('react-dom/client');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
const Modal = createClass({
|
|
||||||
getDefaultProps : function () {
|
|
||||||
return {
|
|
||||||
header : '',
|
|
||||||
save : ()=>{},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
visible : false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
setVisible : function(visible) {
|
|
||||||
this.setState({
|
|
||||||
visible
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : function() {
|
|
||||||
this.props.save();
|
|
||||||
this.setVisible(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function () {
|
|
||||||
const { children, header } = this.props;
|
|
||||||
const { visible } = this.state;
|
|
||||||
return <React.Fragment>
|
|
||||||
{visible ? <div className={'bg-cover'}>
|
|
||||||
<div className={'modal'}>
|
|
||||||
<h1>{header}</h1>
|
|
||||||
<hr/>
|
|
||||||
{children}
|
|
||||||
<div className={'action-row'}>
|
|
||||||
<button id={'save'} onClick={()=>this.save()}>Save</button>
|
|
||||||
<button id={'cancel'} onClick={()=>this.setVisible(false)}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> : null}
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/*
|
|
||||||
* Requirements:
|
|
||||||
* - modalRef member variable
|
|
||||||
* - should be re-rendered via `this.state.modalRoot?.render` in `componentDidUpdate`
|
|
||||||
*/
|
|
||||||
Modal,
|
|
||||||
modalHelpers : {
|
|
||||||
// should be called in `componentDidMount`
|
|
||||||
// `self` should be passed as the component instance (`this`)
|
|
||||||
mount : (self)=>{
|
|
||||||
const el = document.createElement('div');
|
|
||||||
const root = ReactDOMClient.createRoot(el);
|
|
||||||
document.querySelector('body').append(el);
|
|
||||||
self.setState({
|
|
||||||
el,
|
|
||||||
modalRoot : root
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// should be called in `componentWillUnmount`
|
|
||||||
// `self` should be passed as the component instance (`this`)
|
|
||||||
unmount : (self)=>{
|
|
||||||
self.state.el.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
|
||||||
|
|
||||||
.bg-cover {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000000;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: rgba(0, 0, 0, .5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
position: absolute;
|
|
||||||
top: 10vh;
|
|
||||||
left: 25vw;
|
|
||||||
width: 50vw;
|
|
||||||
min-height: 50vh;
|
|
||||||
max-height: 80vh;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 5px 5px 50px black;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
>* {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: left;
|
|
||||||
|
|
||||||
>* {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
&#cancel {
|
|
||||||
background-color: @redLight;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
require('./text.less');
|
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { NUMBER_PATTERN, HINT_TYPE, PATTERNS, STYLE_FN, FIELD_TYPE, SNIPPET_TYPE } = require('../constants');
|
|
||||||
const CodeMirror = require('../../../code-mirror.js');
|
|
||||||
|
|
||||||
const Text = createClass({
|
|
||||||
fieldRef : {},
|
|
||||||
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
field : {},
|
|
||||||
text : '',
|
|
||||||
n : 0,
|
|
||||||
setHints : ()=>{},
|
|
||||||
onChange : ()=>{},
|
|
||||||
getStyleHints : ()=>{},
|
|
||||||
def : false,
|
|
||||||
snippetType : -1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
value : '',
|
|
||||||
id : ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate : function({ text }, { value }) {
|
|
||||||
if(this.props.text !== text) {
|
|
||||||
const { field, n } = this.props;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, __, value] = this.props.text.match(pattern) ?? [];
|
|
||||||
this.setState({
|
|
||||||
value : value,
|
|
||||||
id : `${field?.name}-${n}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.value !== value) {
|
|
||||||
const { field } = this.props;
|
|
||||||
this.props.setHints(this, field.hints ? this.props.getStyleHints(field, this.state.value) : []);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
const { field, text, n } = this.props;
|
|
||||||
const id = `${field?.name}-${n}`;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, __, value] = text.match(pattern) ?? [];
|
|
||||||
this.setState({
|
|
||||||
value : value,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
this.fieldRef[id] = React.createRef();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
this.fieldRef = undefined;
|
|
||||||
this.fieldRef = {};
|
|
||||||
this.fieldRef[this.state.id]?.remove();
|
|
||||||
},
|
|
||||||
|
|
||||||
setFocus : function(e) {
|
|
||||||
const { type } = e;
|
|
||||||
const { field } = this.props;
|
|
||||||
this.props.setHints(this, type === 'focus' && field.hints ? this.props.getStyleHints(field, this.state.value) : []);
|
|
||||||
},
|
|
||||||
|
|
||||||
hintSelected : function(h, e) {
|
|
||||||
let value;
|
|
||||||
if(h?.type === HINT_TYPE.VALUE) {
|
|
||||||
value = h.hint;
|
|
||||||
} else if(h?.type === HINT_TYPE.NUMBER_SUFFIX) {
|
|
||||||
const match = this.state.value.match(NUMBER_PATTERN);
|
|
||||||
let suffix = match?.at(4) ?? '';
|
|
||||||
for (const char of h.hint) {
|
|
||||||
if(suffix.at(0) === char) {
|
|
||||||
suffix = suffix.slice(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
value = `${match?.at(1) ?? ''}${match?.at(2) ?? ''}${h.hint}${suffix}`;
|
|
||||||
}
|
|
||||||
this.onChange({
|
|
||||||
target : {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
keyDown : function(e) {
|
|
||||||
const { code } = e;
|
|
||||||
const { field } = this.props;
|
|
||||||
const { value } = this.state;
|
|
||||||
const match = value?.match(NUMBER_PATTERN);
|
|
||||||
if(code === 'ArrowDown') {
|
|
||||||
if(match && CSS.supports(field.name, value)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.onChange({
|
|
||||||
target : {
|
|
||||||
value : `${match.at(1) ?? ''}${Number(match[2]) - field.increment}${match[3] ?? ''}${match.at(4) ?? ''}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if(code === 'ArrowUp') {
|
|
||||||
if(match && CSS.supports(field.name, value)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.onChange({
|
|
||||||
target : {
|
|
||||||
value : `${match.at(1) ?? ''}${Number(match[2]) + field.increment}${match[3] ?? ''}${match.at(4) ?? ''}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onChange : function (e){
|
|
||||||
const { cm, text, field, n, snippetType } = this.props;
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, label, current] = text.match(pattern) ?? [null, field.name, ''];
|
|
||||||
let index = text.indexOf(`${label}:${current}`);
|
|
||||||
let value = e.target.value;
|
|
||||||
if(index === -1) {
|
|
||||||
if(snippetType === SNIPPET_TYPE.INLINE) {
|
|
||||||
index = text.indexOf('}');
|
|
||||||
}
|
|
||||||
index = index === -1 ? text.length : index;
|
|
||||||
value = `,${field.name}:${value}`;
|
|
||||||
} else {
|
|
||||||
index = index + 1 + field.name.length;
|
|
||||||
}
|
|
||||||
cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + current.length), '+insert');
|
|
||||||
this.setState({
|
|
||||||
value : e.target.value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render : function() {
|
|
||||||
const { value, id } = this.state;
|
|
||||||
const { field, text, def } = this.props;
|
|
||||||
const style = STYLE_FN(value);
|
|
||||||
const pattern = PATTERNS.field[field.type](field.name);
|
|
||||||
const [_, label, __] = text.match(pattern) ?? [null, undefined, ''];
|
|
||||||
let className = 'widget-field';
|
|
||||||
if(!label) {
|
|
||||||
className += ' suggested';
|
|
||||||
}
|
|
||||||
if(def) {
|
|
||||||
className += ' default';
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
|
||||||
<div className={className}>
|
|
||||||
<label htmlFor={id}>{field.name}:</label>
|
|
||||||
<input id={id} type='text' value={value}
|
|
||||||
style={style}
|
|
||||||
ref={this.fieldRef[id]}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onFocus={this.setFocus}
|
|
||||||
onBlur={this.setFocus}
|
|
||||||
onKeyDown={this.keyDown}/>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Text;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
.widget-field {
|
|
||||||
display: inline-block;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
background-color: #ddd;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 4px 2px;
|
|
||||||
|
|
||||||
>label {
|
|
||||||
display: inline;
|
|
||||||
width: 50px;
|
|
||||||
margin: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
>input {
|
|
||||||
background-color: #ddd;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
>.hints {
|
|
||||||
position: relative;
|
|
||||||
left: 30px;
|
|
||||||
max-height: 100px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
background-color: white;
|
|
||||||
|
|
||||||
>.hint {
|
|
||||||
margin: 0 0;
|
|
||||||
padding: 2px;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const ReactDOMClient = require('react-dom/client');
|
|
||||||
const { PATTERNS, FIELD_TYPE, HINT_TYPE, UNITS } = require('./widget-elements/constants');
|
|
||||||
require('./widget-elements/hints/hints.jsx');
|
|
||||||
const { Text, Checkbox, ImageSelector, ColorSelector } = require('./widget-elements');
|
|
||||||
const CodeMirror = require('../code-mirror.js');
|
|
||||||
|
|
||||||
// See https://codemirror.net/5/addon/hint/css-hint.js for code reference
|
|
||||||
const pseudoClasses = { 'active' : 1, 'after' : 1, 'before' : 1, 'checked' : 1, 'default' : 1,
|
|
||||||
'disabled' : 1, 'empty' : 1, 'enabled' : 1, 'first-child' : 1, 'first-letter' : 1,
|
|
||||||
'first-line' : 1, 'first-of-type' : 1, 'focus' : 1, 'hover' : 1, 'in-range' : 1,
|
|
||||||
'indeterminate' : 1, 'invalid' : 1, 'lang' : 1, 'last-child' : 1, 'last-of-type' : 1,
|
|
||||||
'link' : 1, 'not' : 1, 'nth-child' : 1, 'nth-last-child' : 1, 'nth-last-of-type' : 1,
|
|
||||||
'nth-of-type' : 1, 'only-of-type' : 1, 'only-child' : 1, 'optional' : 1, 'out-of-range' : 1,
|
|
||||||
'placeholder' : 1, 'read-only' : 1, 'read-write' : 1, 'required' : 1, 'root' : 1,
|
|
||||||
'selection' : 1, 'target' : 1, 'valid' : 1, 'visited' : 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const genKey = (...args)=>args.join('-');
|
|
||||||
|
|
||||||
module.exports = function(widgets, cm, setHints) {
|
|
||||||
const roots = {};
|
|
||||||
const spec = CodeMirror.resolveMode('text/css');
|
|
||||||
const headless = CodeMirror(()=>{});
|
|
||||||
|
|
||||||
const makeTempCSSDoc = (value)=>CodeMirror.Doc(`.selector {\n${value}\n}`, 'text/css');
|
|
||||||
|
|
||||||
// See https://codemirror.net/5/addon/hint/css-hint.js for code reference
|
|
||||||
const getStyleHints = (field, value)=>{
|
|
||||||
const tempDoc = makeTempCSSDoc(`${field.name}:${value?.replaceAll(`'"`, '') ?? ''}`);
|
|
||||||
headless.swapDoc(tempDoc);
|
|
||||||
const pos = CodeMirror.Pos(1, field.name.length + 1 + (value?.length ?? 0), false);
|
|
||||||
const token = headless.getTokenAt(pos);
|
|
||||||
const inner = CodeMirror.innerMode(tempDoc.getMode(), token?.state);
|
|
||||||
|
|
||||||
if(inner.mode.name !== 'css') return;
|
|
||||||
|
|
||||||
if(token.type === 'keyword' && '!important'.indexOf(token.string) === 0)
|
|
||||||
return { list : ['!important'], from : CodeMirror.Pos(pos.line, token.start),
|
|
||||||
to : CodeMirror.Pos(pos.line, token.end) };
|
|
||||||
|
|
||||||
let start = token.start, end = pos.ch, word = token.string.slice(0, end - start);
|
|
||||||
if(/[^\w$_-]/.test(word)) {
|
|
||||||
word = ''; start = end = pos.ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = [];
|
|
||||||
const add = (keywords)=>{
|
|
||||||
for (const name in keywords)
|
|
||||||
if(!word || name.lastIndexOf(word, 0) === 0)
|
|
||||||
result.push(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const st = inner.state.state;
|
|
||||||
if(st === 'pseudo' || token.type === 'variable-3') {
|
|
||||||
add(pseudoClasses);
|
|
||||||
} else if(st === 'block' || st === 'maybeprop') {
|
|
||||||
add(spec.propertyKeywords);
|
|
||||||
} else if(st === 'prop' || st === 'parens' || st === 'at' || st === 'params') {
|
|
||||||
add(spec.valueKeywords);
|
|
||||||
add(spec.colorKeywords);
|
|
||||||
} else if(st === 'media' || st === 'media_parens') {
|
|
||||||
add(spec.mediaTypes);
|
|
||||||
add(spec.mediaFeatures);
|
|
||||||
}
|
|
||||||
result = result.map((h)=>({ hint: h, type: HINT_TYPE.VALUE }))
|
|
||||||
.filter((h)=>CSS.supports(field.name, h.hint) && h.hint.includes(value ?? ''));
|
|
||||||
|
|
||||||
const numberSuffix = word.slice(-4).replaceAll(/\d/g, '');
|
|
||||||
if(token.type === 'number' && !UNITS.includes(numberSuffix)) {
|
|
||||||
result.push(...UNITS
|
|
||||||
.filter((u)=>u.includes(numberSuffix) && CSS.supports(field.name, `${value.replaceAll(/\D/g, '') ?? ''}${u}`))
|
|
||||||
.map((u)=>({ hint: u, type: HINT_TYPE.NUMBER_SUFFIX }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const widgetOptions = widgets.map((widget)=>({
|
|
||||||
name : widget.name,
|
|
||||||
pattern : PATTERNS.snippet[widget.type](widget.name),
|
|
||||||
renderWidget : (n, node)=>{
|
|
||||||
roots[n] = roots[n] ?? {};
|
|
||||||
const parent = document.createElement('div');
|
|
||||||
const id = `${widget.name}-${n}`;
|
|
||||||
parent.id = id;
|
|
||||||
|
|
||||||
const textFieldNames = (widget.fields || []).filter((f)=>f.type === FIELD_TYPE.TEXT || f.type === FIELD_TYPE.COLOR_SELECTOR).map((f)=>f.name);
|
|
||||||
const { text } = cm.lineInfo(n);
|
|
||||||
|
|
||||||
const fields = (widget.fields || []).map((field)=>{
|
|
||||||
const key = genKey(widget.name, n, field.name);
|
|
||||||
if(field.type === FIELD_TYPE.CHECKBOX) {
|
|
||||||
return <Checkbox key={key} cm={cm} n={n} prefix={widget.name} value={field.name} def={true}/>;
|
|
||||||
} else if(field.type === FIELD_TYPE.TEXT) {
|
|
||||||
return <Text key={key} field={field} cm={cm} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints} def={true} snippetType={widget.type}/>;
|
|
||||||
} else if(field.type === FIELD_TYPE.IMAGE_SELECTOR) {
|
|
||||||
return <ImageSelector key={key} field={field} cm={cm} n={n}/>;
|
|
||||||
} else if(field.type === FIELD_TYPE.COLOR_SELECTOR) {
|
|
||||||
return <ColorSelector key={key} field={field} cm={cm} n={n} text={text} def={true} snippetType={widget.type}/>;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
const styles = [...text.matchAll(PATTERNS.collectStyles)].map(([_, style])=>{
|
|
||||||
if(textFieldNames.includes(style)) return false;
|
|
||||||
const field = {
|
|
||||||
name : style,
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
hints : true,
|
|
||||||
};
|
|
||||||
const key = genKey(widget.name, n, style);
|
|
||||||
if(style.includes('color')) {
|
|
||||||
return <ColorSelector key={key} field={field} cm={cm} n={n} text={text} snippetType={widget.type}/>;
|
|
||||||
}
|
|
||||||
return <Text key={key} field={field} cm={cm} n={n} text={text} setHints={(f, h)=>setHints(h, f)} getStyleHints={getStyleHints} snippetType={widget.type}/>;
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
const root = roots[n][id] ?? ReactDOMClient.createRoot(node || parent);
|
|
||||||
root.render(<React.Fragment>
|
|
||||||
{fields}
|
|
||||||
{styles}
|
|
||||||
</React.Fragment>);
|
|
||||||
roots[n][id] = root;
|
|
||||||
|
|
||||||
return node || parent;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const updateLineWidgets = (n)=>{
|
|
||||||
const { text, widgets } = cm.lineInfo(n);
|
|
||||||
const widgetOption = widgetOptions.find((option)=>!!text.match(option.pattern));
|
|
||||||
if(!widgetOption) return;
|
|
||||||
if(!!widgets) {
|
|
||||||
for (const widget of widgets) {
|
|
||||||
widgetOption.renderWidget(n, widget.node);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return cm.addLineWidget(n, widgetOption.renderWidget(n), {
|
|
||||||
above : false,
|
|
||||||
coverGutter : false,
|
|
||||||
noHScroll : true,
|
|
||||||
className : `snippet-options-widget ${widgetOption.name}-widget ${widgetOption.name}-widget-${n}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
roots,
|
|
||||||
removeLineWidget : (n, widget)=>{
|
|
||||||
roots[n][widget.node.id]?.unmount();
|
|
||||||
delete roots[n][widget.node.id];
|
|
||||||
widget?.clear();
|
|
||||||
},
|
|
||||||
updateLineWidgets,
|
|
||||||
updateAllLineWidgets : ()=>{
|
|
||||||
for (let i = 0; i < cm.lineCount(); i++) {
|
|
||||||
const { widgets } = cm.lineInfo(i);
|
|
||||||
if(!!widgets) {
|
|
||||||
updateLineWidgets(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateWidgetGutter : ()=>{
|
|
||||||
cm.operation(()=>{
|
|
||||||
for (let i = 0; i < cm.lineCount(); i++) {
|
|
||||||
const { text, widgets } = cm.lineInfo(i);
|
|
||||||
|
|
||||||
if(widgetOptions.some((option)=>text.match(option.pattern))) {
|
|
||||||
if(widgets) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const optionsMarker = document.createElement('div');
|
|
||||||
optionsMarker.style.color = '#822';
|
|
||||||
optionsMarker.style.cursor = 'pointer';
|
|
||||||
optionsMarker.innerHTML = '●';
|
|
||||||
cm.setGutterMarker(i, 'widget-gutter', optionsMarker);
|
|
||||||
} else {
|
|
||||||
cm.setGutterMarker(i, 'widget-gutter', null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -61,7 +61,8 @@ const SplitPane = createClass({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUp : function(){
|
handleUp : function(e){
|
||||||
|
e.preventDefault();
|
||||||
if(this.state.isDragging){
|
if(this.state.isDragging){
|
||||||
this.props.onDragFinish(this.state.currentDividerPos);
|
this.props.onDragFinish(this.state.currentDividerPos);
|
||||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||||
@@ -78,6 +79,7 @@ const SplitPane = createClass({
|
|||||||
handleMove : function(e){
|
handleMove : function(e){
|
||||||
if(!this.state.isDragging) return;
|
if(!this.state.isDragging) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
const newSize = this.limitPosition(e.pageX);
|
const newSize = this.limitPosition(e.pageX);
|
||||||
this.setState({
|
this.setState({
|
||||||
currentDividerPos : newSize,
|
currentDividerPos : newSize,
|
||||||
@@ -122,7 +124,7 @@ const SplitPane = createClass({
|
|||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <>
|
return <>
|
||||||
{this.renderMoveArrows()}
|
{this.renderMoveArrows()}
|
||||||
<div className='divider' onMouseDown={this.handleDown} >
|
<div className='divider' onPointerDown={this.handleDown} >
|
||||||
<div className='dots'>
|
<div className='dots'>
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
@@ -133,7 +135,7 @@ const SplitPane = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
|
||||||
<Pane
|
<Pane
|
||||||
ref='pane1'
|
ref='pane1'
|
||||||
width={this.state.currentDividerPos}
|
width={this.state.currentDividerPos}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
flex : 1;
|
flex : 1;
|
||||||
}
|
}
|
||||||
.divider{
|
.divider{
|
||||||
|
touch-action : none;
|
||||||
display : table;
|
display : table;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
width : 15px;
|
width : 15px;
|
||||||
|
|||||||
@@ -10,6 +10,21 @@
|
|||||||
background-image : url(/assets/DMG_background.png);
|
background-image : url(/assets/DMG_background.png);
|
||||||
background-size : cover;
|
background-size : cover;
|
||||||
|
|
||||||
|
/*TABLES WITHIN NOTES*/
|
||||||
|
.note table tbody tr:nth-child(odd) {
|
||||||
|
background:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*DROP CAP*/
|
||||||
|
h1 + p::first-letter {
|
||||||
|
background-image: unset;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote p:first-child::first-line {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
background-image : url(/assets/DMG_footerAccent.png);
|
background-image : url(/assets/DMG_footerAccent.png);
|
||||||
height: 58px;
|
height: 58px;
|
||||||
@@ -25,4 +40,4 @@
|
|||||||
.partCover {
|
.partCover {
|
||||||
background-image: @partCoverHeaderDMG;
|
background-image: @partCoverHeaderDMG;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const ClassFeatureGen = require('./snippets/classfeature.gen.js');
|
|||||||
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
const CoverPageGen = require('./snippets/coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
|
||||||
const indexGen = require('./snippets/index.gen.js');
|
const indexGen = require('./snippets/index.gen.js');
|
||||||
|
const QuoteGen = require('./snippets/quote.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +124,11 @@ module.exports = [
|
|||||||
icon : 'fas fa-mask',
|
icon : 'fas fa-mask',
|
||||||
gen : ClassFeatureGen,
|
gen : ClassFeatureGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Quote',
|
||||||
|
icon : 'fas fa-quote-right',
|
||||||
|
gen : QuoteGen,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Note',
|
name : 'Note',
|
||||||
icon : 'fas fa-sticky-note',
|
icon : 'fas fa-sticky-note',
|
||||||
@@ -220,34 +226,51 @@ module.exports = [
|
|||||||
view : 'text',
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table',
|
name : 'Class Tables',
|
||||||
icon : 'fas fa-table',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
},
|
subsnippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table (unframed)',
|
name : 'Martial Class Table',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.full('classTable,wide'),
|
gen : ClassTableGen.non('classTable,frame,decoration'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table',
|
name : 'Martial Class Table (unframed)',
|
||||||
icon : 'fas fa-list-alt',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
gen : ClassTableGen.non('classTable'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/2 Class Table (unframed)',
|
name : 'Full Caster Class Table',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-table',
|
||||||
gen : ClassTableGen.half('classTable'),
|
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table',
|
name : 'Full Caster Class Table (unframed)',
|
||||||
icon : 'fas fa-border-all',
|
icon : 'fas fa-border-none',
|
||||||
gen : ClassTableGen.third('classTable,frame'),
|
gen : ClassTableGen.full('classTable,wide'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : '1/3 Class Table (unframed)',
|
name : 'Half Caster Class Table',
|
||||||
icon : 'fas fa-border-none',
|
icon : 'fas fa-list-alt',
|
||||||
gen : ClassTableGen.third('classTable'),
|
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Half Caster Class Table (unframed)',
|
||||||
|
icon : 'fas fa-border-none',
|
||||||
|
gen : ClassTableGen.half('classTable,wide'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Third Caster Spell Table',
|
||||||
|
icon : 'fas fa-border-all',
|
||||||
|
gen : ClassTableGen.third('classTable,frame,decoration'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Third Caster Spell Table (unframed)',
|
||||||
|
icon : 'fas fa-border-none',
|
||||||
|
gen : ClassTableGen.third('classTable'),
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Rune Table',
|
name : 'Rune Table',
|
||||||
|
|||||||
@@ -1,132 +1,138 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
'Astrological Botany',
|
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
|
||||||
'Biochemical Sorcery',
|
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
|
||||||
'Civil Divination',
|
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
|
||||||
'Consecrated Augury',
|
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
|
||||||
'Demonic Anthropology',
|
'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
|
||||||
'Divinatory Mineralogy',
|
'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
|
||||||
'Exo Interfacer',
|
'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
|
||||||
'Genetic Banishing',
|
'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
|
||||||
'Gunpowder Torturer',
|
'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
|
||||||
'Gunslinger Corruptor',
|
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
|
||||||
'Hermetic Geography',
|
|
||||||
'Immunological Cultist',
|
const classnames = [
|
||||||
'Malefic Chemist',
|
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||||
'Mathematical Pharmacy',
|
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
|
||||||
'Nuclear Biochemistry',
|
|
||||||
'Orbital Gravedigger',
|
|
||||||
'Pharmaceutical Outlaw',
|
|
||||||
'Phased Linguist',
|
|
||||||
'Plasma Gunslinger',
|
|
||||||
'Police Necromancer',
|
|
||||||
'Ritual Astronomy',
|
|
||||||
'Sixgun Poisoner',
|
|
||||||
'Seismological Alchemy',
|
|
||||||
'Spiritual Illusionism',
|
|
||||||
'Statistical Occultism',
|
|
||||||
'Spell Analyst',
|
|
||||||
'Torque Interfacer'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
|
||||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
|
||||||
|
|
||||||
const levels = ['1st', '2nd', '3rd', '4th', '5th',
|
|
||||||
'6th', '7th', '8th', '9th', '10th',
|
|
||||||
'11th', '12th', '13th', '14th', '15th',
|
|
||||||
'16th', '17th', '18th', '19th', '20th'];
|
|
||||||
|
|
||||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
|
||||||
|
|
||||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
|
||||||
|
|
||||||
const drawSlots = function(Slots, rows, padding){
|
|
||||||
let slots = Number(Slots);
|
|
||||||
return _.times(rows, function(i){
|
|
||||||
const max = maxes[i];
|
|
||||||
if(slots < 1) return _.pad('—', padding);
|
|
||||||
const res = _.min([max, slots]);
|
|
||||||
slots -= res;
|
|
||||||
return _.pad(res.toString(), padding);
|
|
||||||
}).join(' | ');
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
full : function(classes){
|
non : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
let cantrips = 3;
|
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|
||||||
let spells = 1;
|
|:-----:|:-----------------:|:---------|:---------------------:|
|
||||||
let slots = 2;
|
| 1st | +2 | ${_.sample(features)} | 2 |
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| 2nd | +2 | ${_.sample(features)} | 2 |
|
||||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
| 3rd | +2 | ${_.sample(features)} | 3 |
|
||||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
| 4th | +2 | ${_.sample(features)} | 3 |
|
||||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
| 5th | +3 | ${_.sample(features)} | 3 |
|
||||||
_.map(levels, function(levelName, level){
|
| 6th | +3 | ${_.sample(features)} | 4 |
|
||||||
const res = [
|
| 7th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(levelName, 5),
|
| 8th | +3 | ${_.sample(features)} | 4 |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 9th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.padEnd(_.sample(features), 21),
|
| 10th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 11th | +4 | ${_.sample(features)} | 4 |
|
||||||
_.pad(spells.toString(), 6),
|
| 12th | +4 | ${_.sample(features)} | 5 |
|
||||||
drawSlots(slots, 9, 2),
|
| 13th | +5 | ${_.sample(features)} | 5 |
|
||||||
].join(' | ');
|
| 14th | +5 | ${_.sample(features)} | 5 |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 5 |
|
||||||
cantrips += _.random(0, 1);
|
| 16th | +5 | ${_.sample(features)} | 5 |
|
||||||
spells += _.random(0, 1);
|
| 17th | +6 | ${_.sample(features)} | 6 |
|
||||||
slots += _.random(0, 2);
|
| 18th | +6 | ${_.sample(features)} | 6 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 6 |
|
||||||
return `| ${res} |`;
|
| 20th | +6 | ${_.sample(features)} | unlimited |
|
||||||
}).join('\n')}\n}}\n\n`;
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
half : function(classes){
|
full : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
let featureScore = 1;
|
##### The ${_.sample(classnames)}
|
||||||
return `{{${classes}\n##### The ${classname}\n` +
|
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
|
||||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|
||||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
_.map(levels, function(levelName, level){
|
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
|
||||||
const res = [
|
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
|
||||||
_.pad(levelName, 5),
|
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
|
||||||
_.pad(`+${profBonus[level]}`, 2),
|
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
|
||||||
_.padEnd(_.sample(features), 23),
|
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
|
||||||
_.pad(`+${featureScore}`, 21),
|
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
|
||||||
].join(' | ');
|
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
|
||||||
featureScore += _.random(0, 1);
|
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
|
||||||
return `| ${res} |`;
|
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
third : function(classes){
|
half : function(snippetClasses){
|
||||||
const classname = _.sample(classnames);
|
return dedent`
|
||||||
|
{{${snippetClasses}
|
||||||
|
##### The ${_.sample(classnames)}
|
||||||
|
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
|
||||||
|
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|
||||||
|
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
|
||||||
|
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
|
||||||
|
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
|
||||||
|
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
|
||||||
|
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
|
||||||
|
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
|
||||||
|
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
|
||||||
|
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
|
||||||
|
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
|
||||||
|
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
|
||||||
|
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
|
||||||
|
}}\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
let cantrips = 3;
|
third : function(snippetClasses){
|
||||||
let spells = 1;
|
return dedent`
|
||||||
let slots = 2;
|
{{${snippetClasses}
|
||||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
##### ${_.sample(classnames)} Spellcasting
|
||||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
|
||||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|
||||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
|
||||||
_.map(levels, function(levelName, level){
|
| 3rd | 2 | 3 | 2 | — | — | — |
|
||||||
const res = [
|
| 4th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(levelName, 6),
|
| 5th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(cantrips.toString(), 8),
|
| 6th | 2 | 4 | 3 | — | — | — |
|
||||||
_.pad(spells.toString(), 7),
|
| 7th | 2 | 5 | 4 | 2 | — | — |
|
||||||
drawSlots(slots, 4, 7),
|
| 8th | 2 | 6 | 4 | 2 | — | — |
|
||||||
].join(' | ');
|
| 9th | 2 | 6 | 4 | 2 | — | — |
|
||||||
|
| 10th | 3 | 7 | 4 | 3 | — | — |
|
||||||
cantrips += _.random(0, 1);
|
| 11th | 3 | 8 | 4 | 3 | — | — |
|
||||||
spells += _.random(0, 1);
|
| 12th | 3 | 8 | 4 | 3 | — | — |
|
||||||
slots += _.random(0, 1);
|
| 13th | 3 | 9 | 4 | 3 | 2 | — |
|
||||||
|
| 14th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
return `| ${res} |`;
|
| 15th | 3 | 10 | 4 | 3 | 2 | — |
|
||||||
}).join('\n')}\n}}\n\n`;
|
| 16th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 17th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 18th | 3 | 11 | 4 | 3 | 3 | — |
|
||||||
|
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
|
||||||
|
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
|
||||||
|
}}\n\n`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
51
themes/V3/5ePHB/snippets/quote.gen.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
const quotes = [
|
||||||
|
"The sword glinted in the dim light, its edges keen and deadly. As the adventurer reached for it, he couldn't help but feel a surge of excitement mixed with fear. This was no ordinary blade.",
|
||||||
|
"The dragon's roar shook the ground beneath their feet, and the brave knight stood tall, his sword at the ready. He knew that this would be the battle of his life, but he was determined to emerge victorious.",
|
||||||
|
"The wizard's laboratory was a sight to behold, filled with bubbling cauldrons, ancient tomes, and strange artifacts from distant lands. As the apprentice gazed around in wonder, she knew that she was about to embark on a journey unlike any other.",
|
||||||
|
"The tavern was packed with rowdy patrons, their voices raised in song and laughter. The bard took center stage, strumming his lute and launching into a tale of adventure and heroism that had the crowd hanging on his every word.",
|
||||||
|
"The thief crept through the shadows, his eyes scanning the room for any sign of danger. He knew that one false move could mean the difference between success and failure, and he was determined to come out on top.",
|
||||||
|
"The elf queen stood atop her castle walls, surveying the kingdom below with a mix of pride and sadness. She knew that the coming war would be brutal, but she was determined to protect her people at all costs.",
|
||||||
|
"The necromancer's tower loomed in the distance, its dark spires piercing the sky. As the adventurers approached, they could feel the chill of death emanating from within",
|
||||||
|
"The ranger moved through the forest like a shadow, his senses attuned to every sound and movement around him. He knew that danger lurked behind every tree, but he was ready for whatever came his way.",
|
||||||
|
"The paladin knelt before the altar, his hands clasped in prayer. He knew that his faith would be tested in the days ahead, but he was ready to face whatever trials lay in store for him.",
|
||||||
|
"The druid communed with the spirits of nature, his mind merging with the trees, the animals, and the very earth itself. He knew that his power came with a great responsibility, and he was determined to use it for the greater good.",
|
||||||
|
];
|
||||||
|
|
||||||
|
const authors = [
|
||||||
|
"Unknown",
|
||||||
|
"James Wyatt",
|
||||||
|
"Eolande Blackwood",
|
||||||
|
"Ragnar Ironheart",
|
||||||
|
"Lyra Nightshade",
|
||||||
|
"Valtorius Darkstar",
|
||||||
|
"Isadora Fireheart",
|
||||||
|
"Theron Shadowbane",
|
||||||
|
"Lirien Starweaver",
|
||||||
|
"Drogathar Bonecrusher",
|
||||||
|
"Kaelen Frostblade",
|
||||||
|
];
|
||||||
|
|
||||||
|
const books = [
|
||||||
|
"The Blade of Destiny",
|
||||||
|
"Dragonfire and Steel",
|
||||||
|
"The Bard's Tale",
|
||||||
|
"Darkness Rising",
|
||||||
|
"The Sacred Quest",
|
||||||
|
"Shadows in the Forest",
|
||||||
|
"The Starweaver Chronicles",
|
||||||
|
"Beneath the Bones",
|
||||||
|
"Moonlit Magic",
|
||||||
|
"Frost and Fury",
|
||||||
|
|
||||||
|
];
|
||||||
|
module.exports = () => {
|
||||||
|
return `
|
||||||
|
{{quote
|
||||||
|
${_.sample(quotes)}
|
||||||
|
|
||||||
|
{{attribution ${_.sample(authors)}, *${_.sample(books)}*}}
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
};
|
||||||
@@ -29,21 +29,23 @@ const getTOC = (pages)=>{
|
|||||||
|
|
||||||
const res = [];
|
const res = [];
|
||||||
_.each(pages, (page, pageNum)=>{
|
_.each(pages, (page, pageNum)=>{
|
||||||
const lines = page.split('\n');
|
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
|
||||||
_.each(lines, (line)=>{
|
const lines = page.split('\n');
|
||||||
if(_.startsWith(line, '# ')){
|
_.each(lines, (line)=>{
|
||||||
const title = line.replace('# ', '');
|
if(_.startsWith(line, '# ')){
|
||||||
add1(title, pageNum);
|
const title = line.replace('# ', '');
|
||||||
}
|
add1(title, pageNum);
|
||||||
if(_.startsWith(line, '## ')){
|
}
|
||||||
const title = line.replace('## ', '');
|
if(_.startsWith(line, '## ')){
|
||||||
add2(title, pageNum);
|
const title = line.replace('## ', '');
|
||||||
}
|
add2(title, pageNum);
|
||||||
if(_.startsWith(line, '### ')){
|
}
|
||||||
const title = line.replace('### ', '');
|
if(_.startsWith(line, '### ')){
|
||||||
add3(title, pageNum);
|
const title = line.replace('### ', '');
|
||||||
}
|
add3(title, pageNum);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,154 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
const { SNIPPET_TYPE, FIELD_TYPE, fourDigitNumberFromValue } = require('../../../shared/naturalcrit/codeEditor/helpers/widget-elements/constants');
|
|
||||||
|
|
||||||
module.exports = [{
|
|
||||||
name : 'monster',
|
|
||||||
type : SNIPPET_TYPE.BLOCK,
|
|
||||||
fields : [{
|
|
||||||
name : 'frame',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'wide',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'classTable',
|
|
||||||
type : SNIPPET_TYPE.BLOCK,
|
|
||||||
fields : [{
|
|
||||||
name : 'frame',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'decoration',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'wide',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'runeTable',
|
|
||||||
type : SNIPPET_TYPE.BLOCK,
|
|
||||||
fields : [{
|
|
||||||
name : 'frame',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'wide',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'font-family',
|
|
||||||
type : FIELD_TYPE.TEXT
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'index',
|
|
||||||
type : SNIPPET_TYPE.BLOCK,
|
|
||||||
fields : [{
|
|
||||||
name : 'wide',
|
|
||||||
type : FIELD_TYPE.CHECKBOX
|
|
||||||
}, {
|
|
||||||
name : 'columns',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 1
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'image',
|
|
||||||
type : SNIPPET_TYPE.INJECTOR,
|
|
||||||
fields : []
|
|
||||||
}, {
|
|
||||||
name : 'artist',
|
|
||||||
type : SNIPPET_TYPE.BLOCK,
|
|
||||||
fields : [{
|
|
||||||
name : 'top',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
hints : true
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'watercolor',
|
|
||||||
type : SNIPPET_TYPE.INLINE,
|
|
||||||
fields : [{
|
|
||||||
name : 'watercolor',
|
|
||||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
|
||||||
preview : (value)=>`/assets/watercolor/watercolor${value}.png`,
|
|
||||||
values : _.range(1, 13)
|
|
||||||
}, {
|
|
||||||
name : 'top',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
hints : true
|
|
||||||
}, {
|
|
||||||
name : 'left',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
hints : true
|
|
||||||
}, {
|
|
||||||
name : 'width',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
hints : true
|
|
||||||
}, {
|
|
||||||
name : 'opacity',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5
|
|
||||||
}, {
|
|
||||||
name : 'background-color',
|
|
||||||
type : FIELD_TYPE.COLOR_SELECTOR,
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'imageMaskCenter',
|
|
||||||
type : SNIPPET_TYPE.INLINE,
|
|
||||||
fields : [{
|
|
||||||
name : 'imageMaskCenter',
|
|
||||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
|
||||||
preview : (value)=>`/assets/waterColorMasks/center/${fourDigitNumberFromValue(value)}.webp`,
|
|
||||||
values : _.range(1, 17)
|
|
||||||
}, {
|
|
||||||
name : '--offsetX',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}, {
|
|
||||||
name : '--offsetY',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}, {
|
|
||||||
name : '--rotation',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'imageMaskEdge',
|
|
||||||
type : SNIPPET_TYPE.INLINE,
|
|
||||||
fields : [{
|
|
||||||
name : 'imageMaskEdge',
|
|
||||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
|
||||||
preview : (value)=>`/assets/waterColorMasks/edge/${fourDigitNumberFromValue(value)}.webp`,
|
|
||||||
values : _.range(1, 9)
|
|
||||||
}, {
|
|
||||||
name : '--offset',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}, {
|
|
||||||
name : '--rotation',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
name : 'imageMaskCorner',
|
|
||||||
type : SNIPPET_TYPE.INLINE,
|
|
||||||
fields : [{
|
|
||||||
name : 'imageMaskCorner',
|
|
||||||
type : FIELD_TYPE.IMAGE_SELECTOR,
|
|
||||||
preview : (value)=>`/assets/waterColorMasks/corner/${fourDigitNumberFromValue(value)}.webp`,
|
|
||||||
values : _.range(1, 38)
|
|
||||||
}, {
|
|
||||||
name : '--offsetX',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}, {
|
|
||||||
name : '--offsetY',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}, {
|
|
||||||
name : '--rotation',
|
|
||||||
type : FIELD_TYPE.TEXT,
|
|
||||||
increment : 5,
|
|
||||||
}]
|
|
||||||
}];
|
|
||||||
@@ -33,7 +33,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
.useColumns(@multiplier : 1, @fillMode: auto){
|
||||||
column-fill : @fillMode;
|
column-fill : @fillMode;
|
||||||
column-count : 2;
|
column-count : 2;
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,7 @@ body {
|
|||||||
column-span : all;
|
column-span : all;
|
||||||
columns : inherit;
|
columns : inherit;
|
||||||
column-gap : inherit;
|
column-gap : inherit;
|
||||||
|
column-fill : inherit;
|
||||||
}
|
}
|
||||||
.page{
|
.page{
|
||||||
.useColumns();
|
.useColumns();
|
||||||
|
|||||||
88
themes/codeMirror/customEditorStyles.less
Normal file
88
themes/codeMirror/customEditorStyles.less
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.editor .codeEditor .CodeMirror {
|
||||||
|
// Themes with dark backgrounds
|
||||||
|
&.cm-s-3024-night,
|
||||||
|
&.cm-s-abbott,
|
||||||
|
&.cm-s-abcdef,
|
||||||
|
&.cm-s-ambiance,
|
||||||
|
&.cm-s-ayu-dark,
|
||||||
|
&.cm-s-ayu-mirage,
|
||||||
|
&.cm-s-base16-dark,
|
||||||
|
&.cm-s-bespin,
|
||||||
|
&.cm-s-blackboard,
|
||||||
|
&.cm-s-cobalt,
|
||||||
|
&.cm-s-colorforth,
|
||||||
|
&.cm-s-darcula,
|
||||||
|
&.cm-s-dracula,
|
||||||
|
&.cm-s-duotone-dark,
|
||||||
|
&.cm-s-erlang-dark,
|
||||||
|
&.cm-s-gruvbox-dark,
|
||||||
|
&.cm-s-hopscotch,
|
||||||
|
&.cm-s-icecoder,
|
||||||
|
&.cm-s-isotope,
|
||||||
|
&.cm-s-lesser-dark,
|
||||||
|
&.cm-s-liquibyte,
|
||||||
|
&.cm-s-lucario,
|
||||||
|
&.cm-s-material,
|
||||||
|
&.cm-s-material-darker,
|
||||||
|
&.cm-s-material-ocean,
|
||||||
|
&.cm-s-material-palenight,
|
||||||
|
&.cm-s-mbo,
|
||||||
|
&.cm-s-midnight,
|
||||||
|
&.cm-s-monokai,
|
||||||
|
&.cm-s-moxer,
|
||||||
|
&.cm-s-night,
|
||||||
|
&.cm-s-nord,
|
||||||
|
&.cm-s-oceanic-next,
|
||||||
|
&.cm-s-panda-syntax,
|
||||||
|
&.cm-s-paraiso-dark,
|
||||||
|
&.cm-s-pastel-on-dark,
|
||||||
|
&.cm-s-railscasts,
|
||||||
|
&.cm-s-rubyblue,
|
||||||
|
&.cm-s-seti,
|
||||||
|
&.cm-s-shadowfox,
|
||||||
|
&.cm-s-the-matrix,
|
||||||
|
&.cm-s-tomorrow-night-bright,
|
||||||
|
&.cm-s-tomorrow-night-eighties,
|
||||||
|
&.cm-s-twilight,
|
||||||
|
&.cm-s-vibrant-ink,
|
||||||
|
&.cm-s-xq-dark,
|
||||||
|
&.cm-s-yonce,
|
||||||
|
&.cm-s-zenburn
|
||||||
|
{
|
||||||
|
.CodeMirror-code {
|
||||||
|
.block:not(.cm-comment) {
|
||||||
|
color: magenta;
|
||||||
|
}
|
||||||
|
.columnSplit {
|
||||||
|
color: black;
|
||||||
|
background-color: rgba(35,153,153,0.5);
|
||||||
|
}
|
||||||
|
.pageLine {
|
||||||
|
background-color: rgba(255,255,255,0.75);
|
||||||
|
& ~ pre.CodeMirror-line {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Themes with light backgrounds
|
||||||
|
&.cm-s-default,
|
||||||
|
&.cm-s-3024-day,
|
||||||
|
&.cm-s-ambiance-mobile,
|
||||||
|
&.cm-s-base16-light,
|
||||||
|
&.cm-s-duotone-light,
|
||||||
|
&.cm-s-eclipse,
|
||||||
|
&.cm-s-elegant,
|
||||||
|
&.cm-s-juejin,
|
||||||
|
&.cm-s-neat,
|
||||||
|
&.cm-s-neo,
|
||||||
|
&.cm-s-paraiso-lightm
|
||||||
|
&.cm-s-solarized,
|
||||||
|
&.cm-s-ssms,
|
||||||
|
&.cm-s-ttcn,
|
||||||
|
&.cm-s-xq-light,
|
||||||
|
&.cm-s-yeti {
|
||||||
|
// Future styling for themes with light backgrounds
|
||||||
|
--dummyVar: 'currently unused';
|
||||||
|
}
|
||||||
|
}
|
||||||
68
themes/codeMirror/editorThemes.json
Normal file
68
themes/codeMirror/editorThemes.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
[
|
||||||
|
"default",
|
||||||
|
"3024-day",
|
||||||
|
"3024-night",
|
||||||
|
"abbott",
|
||||||
|
"abcdef",
|
||||||
|
"ambiance-mobile",
|
||||||
|
"ambiance",
|
||||||
|
"ayu-dark",
|
||||||
|
"ayu-mirage",
|
||||||
|
"base16-dark",
|
||||||
|
"base16-light",
|
||||||
|
"bespin",
|
||||||
|
"blackboard",
|
||||||
|
"cobalt",
|
||||||
|
"colorforth",
|
||||||
|
"darcula",
|
||||||
|
"dracula",
|
||||||
|
"duotone-dark",
|
||||||
|
"duotone-light",
|
||||||
|
"eclipse",
|
||||||
|
"elegant",
|
||||||
|
"erlang-dark",
|
||||||
|
"gruvbox-dark",
|
||||||
|
"hopscotch",
|
||||||
|
"icecoder",
|
||||||
|
"idea",
|
||||||
|
"isotope",
|
||||||
|
"juejin",
|
||||||
|
"lesser-dark",
|
||||||
|
"liquibyte",
|
||||||
|
"lucario",
|
||||||
|
"material-darker",
|
||||||
|
"material-ocean",
|
||||||
|
"material-palenight",
|
||||||
|
"material",
|
||||||
|
"mbo",
|
||||||
|
"mdn-like",
|
||||||
|
"midnight",
|
||||||
|
"monokai",
|
||||||
|
"moxer",
|
||||||
|
"neat",
|
||||||
|
"neo",
|
||||||
|
"night",
|
||||||
|
"nord",
|
||||||
|
"oceanic-next",
|
||||||
|
"panda-syntax",
|
||||||
|
"paraiso-dark",
|
||||||
|
"paraiso-light",
|
||||||
|
"pastel-on-dark",
|
||||||
|
"railscasts",
|
||||||
|
"rubyblue",
|
||||||
|
"seti",
|
||||||
|
"shadowfox",
|
||||||
|
"solarized",
|
||||||
|
"ssms",
|
||||||
|
"the-matrix",
|
||||||
|
"tomorrow-night-bright",
|
||||||
|
"tomorrow-night-eighties",
|
||||||
|
"ttcn",
|
||||||
|
"twilight",
|
||||||
|
"vibrant-ink",
|
||||||
|
"xq-dark",
|
||||||
|
"xq-light",
|
||||||
|
"yeti",
|
||||||
|
"yonce",
|
||||||
|
"zenburn"
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user