Compare commits

..

13 Commits

Author SHA1 Message Date
koehr e259186d4c adds extended menu 5 years ago
koehr e7b5cdcbde SPA redirect config for netlify 5 years ago
koehr 58d3970e8e some extras for the live preview 5 years ago
koehr 4482fa2fcf Merge branch 'master' into own-rich-text-solution 5 years ago
koehr 35ef3505ce select whole title on focus 5 years ago
koehr 2085e22688 Fixes problems with bogus root level text nodes
Depending on the browser in different situations the root node itself is
selected and new text ends up in a text node on root level instead of a new
paragraph. This happens in:

 * Firefox: after inserting a closed block like a horizontal rule
 * Chromium: after inserting or selecting such a closed block

Now instead of inserting a paragraph directly after inserting an HR, the editor
simply checks for normal text input inside the root node and wraps the newly
written text with a paragraph (and moves the caret to the end of the paragraph
because chromium moves it to the beginning of the line)
5 years ago
koehr 18e043baad add home/end/pageup/pagedown keys to menu sync 5 years ago
koehr 8668838238 fix menu sync
this loops the search for marks until a block element is found, because marks
are nested elements like for example `<p><b><i>bold and italic</i></b></p>`.
5 years ago
koehr bfa4ef8ba0 add marks to editor actions (breaks menu sync) 5 years ago
koehr 5986e599fd implements basic menu functionalities
this includes standard block elements (headers, paragraphs, lists, rulers)
but not marks (bold, italic)
and for sure not special elements like the stat block
5 years ago
koehr 42dfa704d7 remove tiptap, start simple homegrown richtext functionalities
* menu now sends command events and button states
* menu state is synced with the elements at caret postion
* menu functionalities are not yet implemented
5 years ago
koehr e33adfe230 trying to extend that editor 5 years ago
koehr 591530246b add tiptap rich text editor and lots of small improvements 5 years ago

@ -14,8 +14,6 @@
"vue-router": "^3.1.5"
},
"devDependencies": {
"@editorjs/editorjs": "^2.17.0",
"@editorjs/list": "^1.4.0",
"@typescript-eslint/eslint-plugin": "^2.18.0",
"@typescript-eslint/parser": "^2.18.0",
"@vue/cli-plugin-babel": "^4.2.0",
@ -32,7 +30,6 @@
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.1.2",
"lint-staged": "^9.5.0",
"raw-loader": "^4.0.0",
"typescript": "~3.7.5",
"vue-property-decorator": "^8.4.0",
"vue-template-compiler": "^2.6.11"

@ -132,7 +132,3 @@ button.action-close {
border-radius: 1em;
cursor: pointer;
}
.codex-editor--narrow .codex-editor__redactor {
margin-right: 0;
}

@ -1 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle r="7" cx="7" cy="7" stroke="black" stroke-width="2" fill="none" /></svg>

Before

Width:  |  Height:  |  Size: 164 B

@ -1 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><rect width="14" height="14" stroke="black" stroke-width="2" fill="none" /></svg>

Before

Width:  |  Height:  |  Size: 165 B

@ -1 +0,0 @@
<svg width="19" height="4" viewBox="0 0 19 4" xmlns="http://www.w3.org/2000/svg"><path d="M1.25 0H7a1.25 1.25 0 1 1 0 2.5H1.25a1.25 1.25 0 1 1 0-2.5zM11 0h5.75a1.25 1.25 0 0 1 0 2.5H11A1.25 1.25 0 0 1 11 0z"/></svg>

Before

Width:  |  Height:  |  Size: 216 B

@ -1 +0,0 @@
<svg width="19" height="4" viewBox="0 0 19 4" xmlns="http://www.w3.org/2000/svg"><path d="m 16.61577,0 -5.820501,0.28200371 c -1.6647133,0.0806555 -1.6633064,1.74209559 0,1.84786639 L 16.61577,2.5 c 1.663306,0.1057709 1.664713,-2.58065548 0,-2.5 z M 6.9186458,0.51113173 1.2612438,0.8454868 c -1.66376299,0.0983292 -1.68672099,0.7081403 -0.0221,0.7906147 l 5.697126,0.2822659 c 1.664625,0.082474 1.6461385,-1.50556481 -0.017624,-1.40723567 z" /></svg>

Before

Width:  |  Height:  |  Size: 452 B

@ -1 +0,0 @@
<svg width="19" height="4" viewBox="0 0 19 4" xmlns="http://www.w3.org/2000/svg"><path d="m 1.25,0 5.8205009,0.28200371 c 1.6647132,0.0806555 1.6633063,1.74209559 0,1.84786639 L 1.25,2.5 c -1.66330631,0.1057709 -1.66471321,-2.58065548 0,-2.5 z m 9.697124,0.51113173 5.657402,0.33435507 c 1.663763,0.0983292 1.686721,0.7081403 0.0221,0.7906147 L 10.9295,1.9183674 C 9.264875,2.0008418 9.2833615,0.41280259 10.947124,0.51113173 Z" /></svg>

Before

Width:  |  Height:  |  Size: 438 B

@ -1 +0,0 @@
<svg width="10" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="M7.6 8.15H2.25v4.525a1.125 1.125 0 0 1-2.25 0V1.125a1.125 1.125 0 1 1 2.25 0V5.9H7.6V1.125a1.125 1.125 0 0 1 2.25 0v11.55a1.125 1.125 0 0 1-2.25 0V8.15z"/></svg>

Before

Width:  |  Height:  |  Size: 254 B

@ -1 +0,0 @@
<svg width="16" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.14 1.494V4.98h4.62V1.494c0-.498.098-.871.293-1.12A.927.927 0 0 1 7.82 0c.322 0 .583.123.782.37.2.246.3.62.3 1.124v9.588c0 .503-.101.88-.303 1.128a.957.957 0 0 1-.779.374.921.921 0 0 1-.77-.378c-.193-.251-.29-.626-.29-1.124V6.989H2.14v4.093c0 .503-.1.88-.302 1.128a.957.957 0 0 1-.778.374.921.921 0 0 1-.772-.378C.096 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.285.374A.922.922 0 0 1 1.06 0c.321 0 .582.123.782.37.199.246.299.62.299 1.124zm11.653 9.985V5.27c-1.279.887-2.14 1.33-2.583 1.33a.802.802 0 0 1-.563-.228.703.703 0 0 1-.245-.529c0-.232.08-.402.241-.511.161-.11.446-.25.854-.424.61-.259 1.096-.532 1.462-.818a5.84 5.84 0 0 0 .97-.962c.282-.355.466-.573.552-.655.085-.082.246-.123.483-.123.267 0 .481.093.642.28.161.186.242.443.242.77v7.813c0 .914-.345 1.371-1.035 1.371-.307 0-.554-.093-.74-.28-.187-.186-.28-.461-.28-.825z"/></svg>

Before

Width:  |  Height:  |  Size: 918 B

@ -1 +0,0 @@
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm10.99 9.288h3.527c.351 0 .62.072.804.216.185.144.277.34.277.588 0 .22-.073.408-.22.56-.146.154-.368.23-.665.23h-4.972c-.338 0-.601-.093-.79-.28a.896.896 0 0 1-.284-.659c0-.162.06-.377.182-.645s.255-.478.399-.631a38.617 38.617 0 0 1 1.621-1.598c.482-.444.827-.735 1.034-.875.369-.261.676-.523.922-.787.245-.263.432-.534.56-.81.129-.278.193-.549.193-.815 0-.288-.069-.546-.206-.773a1.428 1.428 0 0 0-.56-.53 1.618 1.618 0 0 0-.774-.19c-.59 0-1.054.26-1.392.777-.045.068-.12.252-.226.554-.106.302-.225.534-.358.696-.133.162-.328.243-.585.243a.76.76 0 0 1-.56-.223c-.149-.148-.223-.351-.223-.608 0-.31.07-.635.21-.972.139-.338.347-.645.624-.92a3.093 3.093 0 0 1 1.054-.665c.426-.169.924-.253 1.496-.253.69 0 1.277.108 1.764.324.315.144.592.343.83.595.24.252.425.544.558.875.133.33.2.674.2 1.03 0 .558-.14 1.066-.416 1.523-.277.457-.56.815-.848 1.074-.288.26-.771.666-1.45 1.22-.677.554-1.142.984-1.394 1.29a3.836 3.836 0 0 0-.331.44z"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@ -1 +0,0 @@
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm11.61 4.919c.418 0 .778-.123 1.08-.368.301-.245.452-.597.452-1.055 0-.35-.12-.65-.36-.902-.241-.252-.566-.378-.974-.378-.277 0-.505.038-.684.116a1.1 1.1 0 0 0-.426.306 2.31 2.31 0 0 0-.296.49c-.093.2-.178.388-.255.565a.479.479 0 0 1-.245.225.965.965 0 0 1-.409.081.706.706 0 0 1-.5-.22c-.152-.148-.228-.345-.228-.59 0-.236.071-.484.214-.745a2.72 2.72 0 0 1 .627-.746 3.149 3.149 0 0 1 1.024-.568 4.122 4.122 0 0 1 1.368-.214c.44 0 .842.06 1.205.18.364.12.679.294.947.52.267.228.47.49.606.79.136.3.204.622.204.967 0 .454-.099.843-.296 1.168-.198.324-.48.64-.848.95.354.19.653.408.895.653.243.245.426.516.548.813.123.298.184.619.184.964 0 .413-.083.812-.248 1.198-.166.386-.41.73-.732 1.031a3.49 3.49 0 0 1-1.147.708c-.443.17-.932.256-1.467.256a3.512 3.512 0 0 1-1.464-.293 3.332 3.332 0 0 1-1.699-1.64c-.142-.314-.214-.573-.214-.777 0-.263.085-.475.255-.636a.89.89 0 0 1 .637-.242c.127 0 .25.037.367.112a.53.53 0 0 1 .232.27c.236.63.489 1.099.759 1.405.27.306.65.46 1.14.46a1.714 1.714 0 0 0 1.46-.824c.17-.273.256-.588.256-.947 0-.53-.145-.947-.436-1.249-.29-.302-.694-.453-1.212-.453-.09 0-.231.01-.422.028-.19.018-.313.027-.367.027-.25 0-.443-.062-.579-.187-.136-.125-.204-.299-.204-.521 0-.218.081-.394.245-.528.163-.134.406-.2.728-.2h.28z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@ -1 +0,0 @@
<svg width="20" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm13.003 10.09v-1.252h-3.38c-.427 0-.746-.097-.96-.29-.213-.193-.32-.456-.32-.788 0-.085.016-.171.048-.259.031-.088.078-.18.141-.276.063-.097.128-.19.195-.28.068-.09.15-.2.25-.33l3.568-4.774a5.44 5.44 0 0 1 .576-.683.763.763 0 0 1 .542-.212c.682 0 1.023.39 1.023 1.171v5.212h.29c.346 0 .623.047.832.142.208.094.313.3.313.62 0 .26-.086.45-.256.568-.17.12-.427.179-.768.179h-.41v1.252c0 .346-.077.603-.23.771-.152.168-.356.253-.612.253a.78.78 0 0 1-.61-.26c-.154-.173-.232-.427-.232-.764zm-2.895-2.76h2.895V4.91L12.26 8.823z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@ -1 +0,0 @@
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm14.16 2.645h-3.234l-.388 2.205c.644-.344 1.239-.517 1.783-.517.436 0 .843.082 1.222.245.38.164.712.39.998.677.286.289.51.63.674 1.025.163.395.245.82.245 1.273 0 .658-.148 1.257-.443 1.797-.295.54-.72.97-1.276 1.287-.556.318-1.197.477-1.923.477-.813 0-1.472-.15-1.978-.45-.506-.3-.865-.643-1.076-1.031-.21-.388-.316-.727-.316-1.018 0-.177.073-.345.22-.504a.725.725 0 0 1 .556-.238c.381 0 .665.22.85.66.182.404.427.719.736.943.309.225.654.337 1.035.337.35 0 .656-.09.919-.272.263-.182.466-.431.61-.749.142-.318.214-.678.214-1.082 0-.436-.078-.808-.232-1.117a1.607 1.607 0 0 0-.62-.69 1.674 1.674 0 0 0-.864-.229c-.39 0-.67.048-.837.143-.168.095-.41.262-.725.5-.316.239-.576.358-.78.358a.843.843 0 0 1-.592-.242c-.173-.16-.259-.344-.259-.548 0-.022.025-.177.075-.463l.572-3.26c.063-.39.181-.675.354-.852.172-.177.454-.265.844-.265h3.595c.708 0 1.062.27 1.062.81a.711.711 0 0 1-.26.572c-.172.145-.426.218-.762.218z"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@ -1 +0,0 @@
<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zM12.53 7.058a3.093 3.093 0 0 1 1.004-.814 2.734 2.734 0 0 1 1.214-.264c.43 0 .827.08 1.19.24.365.161.684.39.957.686.274.296.485.645.635 1.048a3.6 3.6 0 0 1 .223 1.262c0 .637-.145 1.216-.437 1.736-.292.52-.699.926-1.221 1.218-.522.292-1.114.438-1.774.438-.76 0-1.416-.186-1.967-.557-.552-.37-.974-.919-1.265-1.645-.292-.726-.438-1.613-.438-2.662 0-.855.088-1.62.265-2.293.176-.674.43-1.233.76-1.676.33-.443.73-.778 1.2-1.004.47-.226 1.006-.339 1.608-.339.579 0 1.089.113 1.53.34.44.225.773.506.997.84.224.335.335.656.335.964 0 .185-.07.354-.21.505a.698.698 0 0 1-.536.227.874.874 0 0 1-.529-.18 1.039 1.039 0 0 1-.36-.498 1.42 1.42 0 0 0-.495-.655 1.3 1.3 0 0 0-.786-.247c-.24 0-.479.069-.716.207a1.863 1.863 0 0 0-.6.56c-.33.479-.525 1.333-.584 2.563zm1.832 4.213c.456 0 .834-.186 1.133-.56.298-.373.447-.862.447-1.468 0-.412-.07-.766-.21-1.062a1.584 1.584 0 0 0-.577-.678 1.47 1.47 0 0 0-.807-.234c-.28 0-.548.074-.804.224-.255.149-.461.365-.617.647a2.024 2.024 0 0 0-.234.994c0 .61.158 1.12.475 1.527.316.407.714.61 1.194.61z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -1 +0,0 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>

Before

Width:  |  Height:  |  Size: 488 B

@ -1 +0,0 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"><path d="M5.819 4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0-4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0 9.357h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 0 1 0-2.137zM1.468 4.155V1.33c-.554.404-.926.606-1.118.606a.338.338 0 0 1-.244-.104A.327.327 0 0 1 0 1.59c0-.107.035-.184.105-.234.07-.05.192-.114.369-.192.264-.118.475-.243.633-.373.158-.13.298-.276.42-.438a3.94 3.94 0 0 1 .238-.298C1.802.019 1.872 0 1.975 0c.115 0 .208.042.277.127.07.085.105.202.105.351v3.556c0 .416-.15.624-.448.624a.421.421 0 0 1-.32-.127c-.08-.085-.121-.21-.121-.376zm-.283 6.664h1.572c.156 0 .275.03.358.091a.294.294 0 0 1 .123.25.323.323 0 0 1-.098.238c-.065.065-.164.097-.296.097H.629a.494.494 0 0 1-.353-.119.372.372 0 0 1-.126-.28c0-.068.027-.16.081-.273a.977.977 0 0 1 .178-.268c.267-.264.507-.49.722-.678.215-.188.368-.312.46-.371.165-.11.302-.222.412-.334.109-.112.192-.226.25-.344a.786.786 0 0 0 .085-.345.6.6 0 0 0-.341-.553.75.75 0 0 0-.345-.08c-.263 0-.47.11-.62.329-.02.029-.054.107-.101.235a.966.966 0 0 1-.16.295c-.059.069-.145.103-.26.103a.348.348 0 0 1-.25-.094.34.34 0 0 1-.099-.258c0-.132.031-.27.093-.413.063-.143.155-.273.279-.39.123-.116.28-.21.47-.282.189-.072.411-.107.666-.107.307 0 .569.045.786.137a1.182 1.182 0 0 1 .618.623 1.18 1.18 0 0 1-.096 1.083 2.03 2.03 0 0 1-.378.457c-.128.11-.344.282-.646.517-.302.235-.509.417-.621.547a1.637 1.637 0 0 0-.148.187z"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -1 +0,0 @@
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>

Before

Width:  |  Height:  |  Size: 488 B

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.2 -0.3 9 11.4" width="12" height="14">
<path d="M0 2.77V.92A1 1 0 01.2.28C.35.1.56 0 .83 0h7.66c.28.01.48.1.63.28.14.17.21.38.21.64v1.85c0 .26-.08.48-.23.66-.15.17-.37.26-.66.26-.28 0-.5-.09-.64-.26a1 1 0 01-.21-.66V1.69H5.6v7.58h.5c.25 0 .45.08.6.23.17.16.25.35.25.6s-.08.45-.24.6a.87.87 0 01-.62.22H3.21a.87.87 0 01-.61-.22.78.78 0 01-.24-.6c0-.25.08-.44.24-.6a.85.85 0 01.61-.23h.5V1.7H1.73v1.08c0 .26-.08.48-.23.66-.15.17-.37.26-.66.26-.28 0-.5-.09-.64-.26A1 1 0 010 2.77z"/>
</svg>

Before

Width:  |  Height:  |  Size: 540 B

@ -0,0 +1,45 @@
<template>
<ol>
<li :key="n" v-for="n in amount" :style="style"> </li>
</ol>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardBoxes extends Vue {
@Prop() public readonly params!: string[]
private get amount () {
return parseInt(this.params[0], 10)
}
private get style () {
const size = parseFloat(this.params[1])
return {
width: `calc(${size}em - 0.25em)`,
height: `calc(${size}em - 0.25em)`
}
}
}
</script>
<style scoped>
ol {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
list-style: none;
margin: 0 0 1em 0;
padding: 0;
}
ol > li {
display: inline-block;
width: 1rem;
height: 1rem;
font-size: 1rem;
margin: 0 .3em .3em 0;
border: 0.25em solid var(--highlight-color);
}
</style>

@ -0,0 +1,51 @@
<template>
<ul>
<li v-for="(param, i) in params"
:key="`param${i}`"
v-editable:[i]="editable"
@keydown="handleKey(i, $event)">
{{ param }}
</li>
</ul>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardBulletList extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
private addEntry (index: number) {
const newParams = [...this.params]
newParams.splice(index + 1, 0, '')
this.$emit('replace', newParams)
}
private removeEntry (index: number) {
const newParams = [...this.params]
newParams.splice(index, 1)
this.$emit('replace', newParams)
}
private handleKey (index: number, event: KeyboardEvent) {
const { key, shiftKey } = event
if (key === 'Enter' && shiftKey) {
event.preventDefault()
this.addEntry(index)
} else if (key === 'Backspace') {
const text = (event.target as HTMLElement).innerText
if (text.trim() === '') this.removeEntry(index)
}
}
}
</script>
<style scoped>
ul {
list-style-position: inside;
margin: 0;
padding-left: .5em;
}
</style>

@ -0,0 +1,22 @@
<template>
<p>
<span v-editable:0="editable" class="title">{{ params[0] }}</span>
<span v-editable:1="editable">{{ params[1] }}</span>
</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardDescription extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
p { margin: 0; line-height: 1.2; }
p > .title { font-weight: bold; font-style: italic; }
p > .title::after { content: ' '; }
</style>

@ -0,0 +1,73 @@
<template>
<ol>
<li :key="titles[i]" v-for="(v, i) in params">
<span class="title">{{ titles[i] }}</span>
<span class="description">{{ v }} ({{ mod(v) }})</span>
</li>
</ol>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardDndstats extends Vue {
@Prop() public readonly params!: string[]
private titles = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
private mod (v: number): string {
switch (v) {
case 1: return '-5'
case 2:
case 3: return '-4'
case 4:
case 5: return '-3'
case 6:
case 7: return '-2'
case 8:
case 9: return '-1'
case 10:
case 11: return '0'
case 12:
case 13: return '+1'
case 14:
case 15: return '+2'
case 16:
case 17: return '+3'
case 18:
case 19: return '+4'
case 20:
case 21: return '+5'
case 22:
case 23: return '+6'
case 24:
case 25: return '+7'
case 26:
case 27: return '+8'
case 28:
case 29: return '+9'
default: return '+10'
}
}
}
</script>
<style scoped>
ol {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
list-style: none;
margin: 0;
padding: 0;
}
ol > li {
display: flex;
flex-flow: column nowrap;
justify-content: center;
font-size: 1rem;
text-align: center;
}
.title { font-weight: bold; }
</style>

@ -0,0 +1,136 @@
<template>
<menu class="menu-bar" :class="{ active }">
<button class="editor-button-bold" :class="{ active: value.bold }" @click="menuAction('bold')" />
<button class="editor-button-italic" :class="{ active: value.italic }" @click="menuAction('italic')" />
<button class="editor-button-paragraph" :class="{ active: value.paragraph }" @click="menuAction('paragraph')" />
<button class="editor-button-heading2" :class="{ active: value.heading2 }" @click="menuAction('heading2')" />
<button class="editor-button-heading3" :class="{ active: value.heading3 }" @click="menuAction('heading3')" />
<button class="editor-button-bullet-list" :class="{ active: value.bulletList }" @click="menuAction('bulletList')" />
<button class="editor-button-horizontal-rule" :class="{ active: value.separator}" @click="menuAction('separator')" />
<button class="editor-button-dropdown" :class="{ active: dropdownOpen }" @click="toggleDropdown" />
<div class="extended-menu" v-show="dropdownOpen">
<button class="extended-menu-button" @click="extMenuAction('statBlock')">Stat Block (DnD5e)</button>
<button class="extended-menu-button" @click="extMenuAction('boxes')">Empty Boxes</button>
</div>
</menu>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { blocks, marks, State } from '@/editor'
@Component
export default class DeckCardEditorMenu extends Vue {
@Prop() public readonly active!: boolean
@Prop() public readonly value!: State
private dropdownOpen = false
private menuAction (name: string) {
const newState = { ...this.value }
if (blocks.indexOf(name) >= 0) { // blocks behave like radio buttons
blocks.forEach(block => {
newState[block] = false
})
newState[name] = true
} else if (marks.indexOf(name)) { // marks behave like checkboxes
newState[name] = !newState[name]
}
this.$emit('input', newState)
this.$emit('action', name)
}
private toggleDropdown () {
this.dropdownOpen = !this.dropdownOpen
this.$emit('action', 'refocus')
}
private extMenuAction (name: string) {
this.menuAction(name)
this.dropdownOpen = false
}
}
</script>
<style scoped>
.card-front > main > .menu-bar {
position: absolute;
width: 70%;
margin: -3rem 0 0 -1rem;
padding: .2rem 1rem;
visibility: hidden;
opacity: 0;
transition: opacity .3s .2s, visibility .3s .2s;
background-color: var(--highlight-color);
z-index: 2;
}
.card-front > main > .menu-bar.active {
opacity: 1.0;
visibility: visible;
}
.menu-bar > button {
position: relative;
width: 1.6rem;
height: 1.6rem;
margin: 0 .1rem;
background-color: #EEE;
background-position: center;
background-repeat: no-repeat;
background-size: 75%;
border: none;
border-radius: 0;
}
.menu-bar > button:after {
position: absolute;
top: 0;
left: 0;
height: 1.6rem;
width: 1.6rem;
font-size: 1.2rem;
color: black;
}
.menu-bar > button.active {
background-color: #FF0;
}
.editor-button-bold { background-image: url(../assets/zondicons/format-bold.svg); }
.editor-button-italic { background-image: url(../assets/zondicons/format-italic.svg); }
.editor-button-bullet-list { background-image: url(../assets/zondicons/list-bullet.svg); }
.editor-button-heading2:after { content: 'H2'; }
.editor-button-heading3:after { content: 'H3'; }
.editor-button-paragraph:after { content: 'P'; }
.editor-button-horizontal-rule:after { content: '—'; }
.editor-button-stat-block:after { content: 'ST'; }
.menu-bar > button.editor-button-dropdown {
width: 3.6rem;
}
.menu-bar > button.editor-button-dropdown:after {
content: ' more ';
width: 90%;
font-size: 1rem;
}
.extended-menu {
width: 100%;
height: 4rem;
padding-top: .5rem;
background: var(--highlight-color);
}
.extended-menu-button {
width: 97%;
height: 1.6rem;
margin: 0 .1rem;
background-color: #EEE;
color: black;
font-size: 1rem;
border: none;
}
</style>

@ -1,61 +1,151 @@
<template>
<main ref="cardEl" class="card-content"></main>
<main>
<deck-card-editor-menu
:active="contentInFocus"
@action="editorAction"
v-model="menuState"
/>
<div
ref="content"
class="card-content"
:contenteditable="active"
@focus="start"
@click="syncMenuStateIfFocussed"
@keyup="syncMenuStateOnKeyPress"
@blur="stop"
>
<h2>card content</h2>
<hr />
<p><b>foo:</b> boom</p>
<p><b>bar:</b> blam</p>
<hr />
<p>Some description maybe?</p>
</div>
</main>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import DeckCardEditorMenu from '@/components/deck-card-editor-menu.vue'
import {
menuActionToCommand,
getActiveMarksAndBlocks,
State,
movementKeys,
controlSequenceKeys,
isRootNode,
isTextNode,
moveCaretToEOL
} from '@/editor'
import Editor from '@editorjs/editorjs'
import List from '@editorjs/list'
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
@Component
@Component({
components: { DeckCardEditorMenu }
})
export default class DeckCardEditor extends Vue {
@Prop() public readonly cardId!: string
@Prop() public readonly active!: boolean
@Prop() public readonly content!: Card['content']
private editor!: Editor
private contentInFocus = false
private defaultMenuState (): State {
return {
bold: false,
italic: false,
paragraph: true,
heading1: false,
heading2: false,
heading3: false,
bulletList: false,
spacer: false,
separator: false,
statBlock: false
}
}
private menuState = this.defaultMenuState()
private get id () {
return `${this.cardId}-editor`
private resetMenuState () {
this.menuState = this.defaultMenuState()
}
private mounted () {
this.editor = new Editor({
holder: this.$refs.cardEl as HTMLElement,
autofocus: false,
tools: {
// header: Heading,
list: { class: List, inlineToolbar: true },
heading: { class: Heading, inlineToolbar: true },
delimiter: { class: Delimiter, inlineToolbar: false },
charges: { class: Charges, inlineToolbar: false },
dndstats: { class: DnDStats, inlineToolbar: false }
},
data: this.content,
placeholder: 'Click here to write your card.',
onChange: () => {
console.log('editor change, saving')
this.editor.save().then(value => {
this.$emit('change', { field: 'content', value })
}).catch(error => {
console.error('error saving data', error)
})
private setMenuState (marks: string[], block: string) {
this.resetMenuState()
marks.forEach(mark => { this.menuState[mark] = true })
if (block !== 'paragraph') {
this.menuState.paragraph = false
this.menuState[block] = true
}
}
private editorAction (action: string) {
const content = this.$refs.content as HTMLElement
content.focus()
const cmd = menuActionToCommand[action]
if (cmd) cmd()
this.$nextTick(() => this.syncMenuState())
}
private syncMenuState () {
const sel = window.getSelection()?.focusNode
if (!sel) return
const { marks, block } = getActiveMarksAndBlocks(sel as HTMLElement)
this.setMenuState(marks, block)
}
private syncMenuStateIfFocussed () {
if (this.contentInFocus) this.syncMenuState()
}
private syncMenuStateOnKeyPress (event: KeyboardEvent) {
// undo/redo/cut/paste
const isCtrlSq = event.ctrlKey && controlSequenceKeys.indexOf(event.key) >= 0
// arrow keys, enter, delete, etc
const isMove = movementKeys.indexOf(event.key) >= 0
if (isCtrlSq || isMove) {
return this.syncMenuState()
} else if (!event.ctrlKey && event.key.length === 1) {
// this should capture all normal typable letters and numbers
// TODO: this needs to be done on text pasting as well
// some browsers create bogus root level text nodes, so whenever
// something is typed in such a root level node, we simply wrap it with
// a paragraph
const sel = window.getSelection()?.focusNode
if (sel && isTextNode(sel) && isRootNode(sel.parentElement as HTMLElement)) {
console.debug(`Typed letter "${event.key} into root node, throwing a <p> at it!"`)
document.execCommand('formatblock', false, 'P')
// Firefox behaves nicely and leaves the caret alone after surrounding
// the text node with a <p>. Unlike Chromium that moves the caret to
// the beginning of the new paragraph. To mitigate that, we set the
// caret to end-of-line manually.
moveCaretToEOL()
}
})
}
}
private start () {
this.contentInFocus = true
this.syncMenuState()
// insert paragraphs instead of DIVs on enter
document.execCommand('defaultParagraphSeparator', false, 'p')
}
private stop () {
this.contentInFocus = false
}
}
</script>
<style>
.card-content .cdx-block {
padding: 0;
}
.card-content .ce-paragraph, .card-content p {
.card-content p {
margin: 0;
line-height: 1.3;
line-height: 1.2;
}
.card-content ul {
@ -84,80 +174,24 @@ export default class DeckCardEditor extends Vue {
border-bottom: 1px solid var(--highlight-color);
}
.card-content .card-delimiter {
.card-content hr {
height: 0;
margin: .2em 0;
padding: 0;
border: 2px solid var(--highlight-color);
}
.card-content .card-delimiter.pointing-right {
.card-content hr.pointing-right {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 0 2px 220px;
border-color: transparent transparent transparent var(--highlight-color);
}
.card-content .card-delimiter.pointing-left {
.card-content hr.pointing-left {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 220px 2px 0;
border-color: transparent var(--highlight-color) transparent transparent;
}
.card-content .cdx-list__item {
padding: 0;
line-height: 1.3;
}
.card-content .card-charges-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
min-height: 1em;
}
.card-content .card-charges-wrapper.card-charges-stretch { justify-content: space-around; }
.card-content .card-charges-wrapper > .card-charge {
width: 1.0em;
height: 1.0em;
border: 2px solid var(--highlight-color);
margin: .5em .2em;
}
.card-content .card-charges-wrapper > .card-charge-circle { border-radius: 100%; }
.card-content .card-charges-wrapper > .card-charge-size-1 { width: 1.0em; height: 1.0em; }
.card-content .card-charges-wrapper > .card-charge-size-2 { width: 1.2em; height: 1.2em; }
.card-content .card-charges-wrapper > .card-charge-size-3 { width: 1.4em; height: 1.4em; }
.card-content .card-charges-wrapper > .card-charge-size-4 { width: 1.6em; height: 1.6em; }
.card-content .card-charges-wrapper > .card-charge-size-5 { width: 1.8em; height: 1.8em; }
.card-content .card-dnd-stats {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
align-items: center;
color: var(--highlight-color);
}
.card-content .dnd-stat-block {
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
font-size: .8em;
}
.card-content .dnd-stat-block > .dnd-stat-title {
width: 100%;
font-weight: bold;
text-align: center;
}
.card-content .dnd-stat-block > input {
width: 50%;
background: white;
color: var(--highlight-color);
border: none;
padding: 0;
margin: 0;
font-size: 1em;
text-align: center;
}
.card-content .dnd-stat-block {
}
[contenteditable="true"] { outline: none; }
</style>

@ -0,0 +1,18 @@
<template>
<div :style="{flex: params[0]}"> </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardFill extends Vue {
@Prop() public readonly params!: string[]
}
</script>
<style scoped>
div {
flex: 1;
}
</style>

@ -0,0 +1,17 @@
<template>
<p v-editable:0="editable">{{ params[0] }}</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardNote extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
p { margin: 0 0 .5em 0; line-height: 1.2; font-style: italic; }
</style>

@ -0,0 +1,22 @@
<template>
<p>
<span class="title" v-editable:0="editable">{{ params[0] }}</span>
<span class="description" v-editable:1="editable">{{ params[1] }}</span>
</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardProperty extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
p { margin: 0 0 0 1em; line-height: 1.2; text-indent: -1em; }
p > .title { font-weight: bold; }
p > .title::after { content: ' '; }
</style>

@ -0,0 +1,34 @@
<template>
<hr :class="params[0]" />
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardRule extends Vue {
@Prop() public readonly params!: string[]
}
</script>
<style scoped>
hr {
height: 0;
margin: .2em 0;
border: 2px solid var(--highlight-color);
}
hr.pointing-right {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 0 2px 220px;
border-color: transparent transparent transparent var(--highlight-color);
}
hr.pointing-left {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 220px 2px 0;
border-color: transparent var(--highlight-color) transparent transparent;
}
</style>

@ -0,0 +1,25 @@
<template>
<h4 v-editable:0="editable">{{ params[0] }}</h4>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardSection extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
h4 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0 0 .2em 0;
font-weight: normal;
font-variant: small-caps;
line-height: .9em;
border-bottom: 1px solid var(--highlight-color);
}
</style>

@ -0,0 +1,22 @@
<template>
<h3 v-editable:0="editable">{{ params[0] }}</h3>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardSubtitle extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
h3 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0;
font-weight: normal;
}
</style>

@ -0,0 +1,17 @@
<template>
<p v-editable:0="editable">{{ params[0] }}</p>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class DeckCardText extends Vue {
@Prop() public readonly params!: string[]
@Prop() public readonly editable!: boolean
}
</script>
<style scoped>
p { margin: 0 0 .5em 0; line-height: 1.2; }
</style>

@ -10,18 +10,14 @@
<section name="card-front" class="card-front">
<header>
<h1 :contenteditable="isSelection"
@focus="selectLine"
@blur="editField('name', $event)"
@keypress.enter.prevent="editField('name', $event)">
{{ card.name }}
</h1>
<img :src="icon" />
</header>
<deck-card-editor
:card-id="card.id"
:active="isSelection"
:content="card.content"
@change="$emit('edit', $event)"
/>
<deck-card-editor :active="isSelection" :content="card.content" @input="$emit('edit', $event)" />
</section>
<section name="card-back" class="card-back">
<div class="icon-wrapper">
@ -37,6 +33,7 @@
import { Component, Prop, Vue } from 'vue-property-decorator'
import { cardWHtoStyle, iconPath } from '@/lib'
import DeckCardEditor from '@/components/deck-card-editor.vue'
import { selectLine } from '@/editor'
@Component({
components: { DeckCardEditor }
@ -107,6 +104,10 @@ export default class DeckCard extends Vue {
return style
}
private selectLine () {
selectLine()
}
}
</script>
@ -200,7 +201,6 @@ export default class DeckCard extends Vue {
border-radius: 1rem;
font-size: 1.2rem;
color: black;
overflow: hidden;
}
.card-back {

@ -0,0 +1,33 @@
import { getFocussedNode } from './node'
function applyRange (callback: (range: Range) => void) {
const range = document.createRange()
callback(range)
const sel = window.getSelection()
if (sel) {
sel.removeAllRanges()
sel.addRange(range)
}
}
function collapseRange (node: Node, toStart = false) {
applyRange(range => {
range.selectNode(node)
range.collapse(toStart)
})
}
export function moveCaretToBOL () {
const node = getFocussedNode()
if (node) collapseRange(node, true)
}
export function moveCaretToEOL () {
const node = getFocussedNode()
if (node) collapseRange(node, false)
}
export function selectLine () {
const node = getFocussedNode()
if (node) {
applyRange(range => range.selectNodeContents(node))
}
}

@ -1,137 +0,0 @@
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
import icon from '../assets/editor/charges.svg.txt'
import iconCircle from '../assets/editor/charges-circle.svg.txt'
const title = 'Charges'
interface ChargesData {
variant: string;
amount: number;
size: number;
stretch: boolean;
}
class Charges extends ContentlessBlock {
static MIN_SIZE = 1
static MAX_SIZE = 5
private _variant: string
private _amount: number
private _size: number
private _stretch: boolean
constructor (args: BlockToolArgs) {
super(args)
console.log('new charges', args)
this._settingButtons = [
{ name: 'box', icon, action: (name: string) => this.setVariant(name) },
{ name: 'more', icon: icon, action: () => this.increaseAmount() },
{ name: 'bigger', icon: icon, action: () => this.increaseSize() },
{ name: 'circle', icon: iconCircle, action: (name: string) => this.setVariant(name) },
{ name: 'less', icon: icon, action: () => this.decreaseAmount() },
{ name: 'smaller', icon: icon, action: () => this.decreaseSize() },
{ name: 'toggle-stretch', icon: icon, action: () => this.toggleStretch() }
]
const { variant, amount, size, stretch } = (args.data || {}) as ChargesData
this._variant = variant || 'box'
this._amount = amount || 5
this._size = size || 1
this._stretch = !(stretch === false)
this._element = this._render()
}
private setVariant (variant: string) {
if (this._variant === variant) return
const charges = Array.from(this._element.children)
charges.forEach(charge => {
charge.classList.remove(`card-charge-${this._variant}`)
charge.classList.add(`card-charge-${variant}`)
})
this._variant = variant
}
private toggleStretch () {
if (this._stretch) this._element.classList.remove('card-charges-stretch')
else this._element.classList.add('card-charges-stretch')
this._stretch = !this._stretch
}
private createCharge (): HTMLElement {
const charge = document.createElement('DIV')
charge.classList.add('card-charge', `card-charge-${this._variant}`, `card-charge-size-${this._size}`)
return charge
}
private increaseAmount () {
this._element.appendChild(this.createCharge())
this._amount++
}
private decreaseAmount () {
const child = this._element.lastElementChild
if (child) {
this._element.removeChild(child)
this._amount--
}
}
private increaseSize () {
if (this._size >= Charges.MAX_SIZE) return
const charges = Array.from(this._element.children)
charges.forEach(charge => {
charge.classList.remove(`card-charge-size-${this._size}`)
charge.classList.add(`card-charge-size-${this._size + 1}`)
})
this._size++
}
private decreaseSize () {
if (this._size <= Charges.MIN_SIZE) return
const charges = Array.from(this._element.children)
charges.forEach(charge => {
charge.classList.remove(`card-charge-size-${this._size}`)
charge.classList.add(`card-charge-size-${this._size - 1}`)
})
this._size--
}
protected _render (): HTMLElement {
const el = document.createElement('DIV')
el.classList.add('card-charges-wrapper', this._CSS.block)
if (this._stretch) el.classList.add('card-charges-stretch')
for (let i = 0; i < this._amount; i++) {
el.appendChild(this.createCharge())
}
console.log('rendered', this._amount, 'charges', el)
return el
}
public save (): ChargesData {
return {
variant: this._variant,
amount: this._amount,
size: this._size,
stretch: this._stretch
}
}
static get toolbox () {
return { icon, title }
}
}
export default Charges

@ -0,0 +1,41 @@
export const movementKeys = [
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
'Delete',
'Backspace',
'Enter',
'Home',
'End',
'PageUp',
'PageDown'
]
export const controlSequenceKeys = ['p', 'x', 'y', 'z', 'Z']
export const elementNameToMenuState: KV<string> = {
B: 'bold',
STRONG: 'bold',
I: 'italic',
EM: 'italic',
P: 'paragraph',
H1: 'heading1',
H2: 'heading2',
H3: 'heading3',
UL: 'bulletList',
OL: 'numberedList',
HR: 'separator'
}
export const marks = ['bold', 'italic']
export const blocks = [
'paragraph',
'heading1',
'heading2',
'heading3',
'bulletList',
'spacer',
'separator',
'statBlock'
]

@ -1,222 +0,0 @@
import {
BlockTool,
BlockToolData,
ToolboxConfig,
API,
HTMLPasteEvent,
ToolSettings,
SanitizerConfig
} from '@editorjs/editorjs'
export { HTMLPasteEvent } from '@editorjs/editorjs'
interface PasteConfig {
tags: string[];
}
export interface ContentBlockConfig extends ToolSettings {
placeholder?: string;
}
export interface ContentBlockSettingButton {
name: string;
icon: string;
action: (name: string, event?: MouseEvent) => void; // action triggered by button
isActive?: (name: string) => boolean; // determine if current button is active
}
export type ContentBlockSettings = ContentBlockSettingButton[]
export interface ContentBlockArgs {
api: API;
config?: ContentBlockConfig;
data?: BlockToolData;
}
export interface CSSClasses {
[key: string]: string;
}
export interface ContentBlockData extends BlockToolData {
text?: string;
}
type importFunction = (str: string) => ContentBlockData
type exportFunction = (data: ContentBlockData) => string
export interface ConversionConfig {
import: string | importFunction;
export: string | exportFunction;
}
export class ContentBlock implements BlockTool {
// Default placeholder for Paragraph Tool
static get DEFAULT_PLACEHOLDER (): string {
return ''
}
static _supportedTags: string[] = []
static _toolboxConfig: ToolboxConfig = {
icon: '<svg></svg>',
title: 'UnnamedContentPlugin'
}
protected _defaultPlaceholder (): string {
return ContentBlock.DEFAULT_PLACEHOLDER
}
protected api: API
protected _element: HTMLElement
protected _data: ContentBlockData
protected _config: ContentBlockConfig
protected _placeholder: string
protected _CSS: CSSClasses = {}
protected onKeyUp: (event: KeyboardEvent) => void
protected _settingButtons: ContentBlockSettings = []
constructor ({ data, config, api }: ContentBlockArgs) {
this.api = api
this._config = config as ContentBlockConfig
this._CSS.block = this.api.styles.block
this.onKeyUp = (event: KeyboardEvent) => this._onKeyUp(event)
// Placeholder it is first Block
this._placeholder = config?.placeholder ? config.placeholder : this._defaultPlaceholder()
this._data = data as ContentBlockData
this._element = this._render()
}
// Check if text content is empty and set empty string to inner html.
// We need this because some browsers (e.g. Safari) insert <br> into empty contenteditanle elements
_onKeyUp (event: KeyboardEvent) {
if (event.code !== 'Backspace' && event.code !== 'Delete') return
if (this._element.textContent === '') {
this._element.innerHTML = ''
}
}
// render tool view
// whenever a redraw is needed the result is saved in this._element
protected _render (): HTMLElement {
const el = document.createElement('DIV')
el.classList.add(this._CSS.block)
el.dataset.placeholder = this._placeholder
el.addEventListener('keyup', this.onKeyUp)
el.innerHTML = this.data.text || ''
el.contentEditable = 'true'
return el
}
// Return Tool's view
public render (): HTMLElement {
return this._element
}
// Method that specified how to merge two Text blocks.
// Called by Editor.js by backspace at the beginning of the Block
public merge (data: ContentBlockData) {
this.data = {
text: (this.data.text || '') + data.text
}
}
// Validate Paragraph block data (by default checks for emptiness)
public validate (savedData: ContentBlockData): boolean {
if (!savedData.text) return false
return savedData.text.trim() !== ''
}
// Extract Tool's data from the view
public save (toolsContent: HTMLElement): ContentBlockData {
return {
text: toolsContent.innerHTML
}
}
public get CSS (): CSSClasses {
return this._CSS
}
/**
* Enable Conversion Toolbar. Paragraph can be converted to/from other tools
*/
static get conversionConfig (): ConversionConfig {
return {
export: 'text', // to convert Paragraph to other block, use 'text' property of saved data
import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data
}
}
// Sanitizer rules
static get sanitize (): SanitizerConfig {
return {
text: { br: true }
}
}
get data (): ContentBlockData {
const text = this._element?.innerHTML
if (text !== undefined) this._data.text = text
if (this._data.text === undefined) this._data.text = ''
return this._data
}
set data (data: ContentBlockData) {
this._data = data || {}
this._element.innerHTML = this._data.text || ''
}
public renderSettings (): HTMLElement {
const wrapper = document.createElement('DIV')
this._settingButtons.forEach(tune => {
// make sure the settings button does something
if (!tune.icon || typeof tune.action !== 'function') return
const { name, icon, action, isActive } = tune
const btn = document.createElement('SPAN')
btn.classList.add(this.api.styles.settingsButton)
if (typeof isActive === 'function' && isActive(name)) {
btn.classList.add(this.api.styles.settingsButtonActive)
}
btn.innerHTML = icon
btn.addEventListener('click', event => action(name, event))
wrapper.appendChild(btn)
})
return wrapper
}
// Used by Editor.js paste handling API.
// Provides configuration to handle the tools tags.
static get pasteConfig (): PasteConfig {
return {
tags: this._supportedTags
}
}
// overwrite this if you need special handling of paste data
protected pasteHandler (element: HTMLElement): ContentBlockData {
return { text: element.innerText }
}
// On paste callback fired from Editor.
public onPaste (event: HTMLPasteEvent) {
const element = event.detail.data
this.data = this.pasteHandler(element)
}
// Icon and title for displaying at the Toolbox
static get toolbox (): ToolboxConfig {
return this._toolboxConfig
}
}
export default ContentBlock

@ -1,73 +0,0 @@
import { BlockTool, BlockToolData, ToolSettings, ToolboxConfig, API } from '@editorjs/editorjs'
import { ContentBlockSettings, CSSClasses } from './content-block'
export interface BlockToolArgs {
api: API;
config?: ToolSettings;
data?: BlockToolData;
}
export class ContentlessBlock implements BlockTool {
static get contentless () {
return true
}
protected api: API
protected _element: HTMLElement
protected _data: object
protected _config: ToolSettings
protected _CSS: CSSClasses = {}
protected _settingButtons: ContentBlockSettings = []
constructor ({ data, config, api }: BlockToolArgs) {
this.api = api
this._config = config as ToolSettings
this._data = data || {}
this._CSS.block = this.api.styles.block
this._element = this._render()
}
protected _render (): HTMLElement {
const el = document.createElement('DIV')
el.classList.add(this._CSS.block)
return el
}
public render (): HTMLElement {
return this._element
}
public save (_toolsContent: HTMLElement): object {
return {}
}
public renderSettings (): HTMLElement {
const wrapper = document.createElement('DIV')
this._settingButtons.forEach(tune => {
// make sure the settings button does something
if (!tune.icon || typeof tune.action !== 'function') return
const { name, icon, action, isActive } = tune
const btn = document.createElement('SPAN')
btn.classList.add(this.api.styles.settingsButton)
if (typeof isActive === 'function' && isActive(name)) {
btn.classList.add(this.api.styles.settingsButtonActive)
}
btn.innerHTML = icon
btn.addEventListener('click', event => action(name, event))
wrapper.appendChild(btn)
})
return wrapper
}
static get toolbox (): ToolboxConfig {
return { icon: '<svg></svg>', title: 'UnnamedPlugin' }
}
}
export default ContentlessBlock

@ -1,53 +0,0 @@
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
import icon from '../assets/editor/delimiter.svg.txt'
import iconR from '../assets/editor/delimiter_r.svg.txt'
import iconL from '../assets/editor/delimiter_l.svg.txt'
const title = 'Delimiter'
interface DelimiterData {
variant: string;
}
class Delimiter extends ContentlessBlock {
private _variant = 'none'
constructor (args: BlockToolArgs) {
super(args)
this._settingButtons = [
{ name: 'straight', icon, action: (name: string) => this.setDelimiterType(name) },
{ name: 'pointing-left', icon: iconL, action: (name: string) => this.setDelimiterType(name) },
{ name: 'pointing-right', icon: iconR, action: (name: string) => this.setDelimiterType(name) }
]
const { variant } = (args.data || {}) as DelimiterData
if (variant) this.setDelimiterType(variant)
}
private setDelimiterType (name: string) {
this._element.classList.remove('pointing-left')
this._element.classList.remove('pointing-right')
this._variant = 'none'
if (name === 'pointing-left' || name === 'pointing-right') {
this._variant = name
this._element.classList.add(name)
}
}
protected _render (): HTMLElement {
const el = document.createElement('HR')
el.classList.add('card-delimiter', this._CSS.block)
return el
}
public save (): DelimiterData {
return {
variant: this._variant
}
}
static get toolbox () {
return { icon, title }
}
}
export default Delimiter

@ -1,106 +0,0 @@
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
import icon from '../assets/editor/charges-circle.svg.txt'
const title = 'DnDStats'
interface DnDStatsData {
text: string;
}
class DnDStats extends ContentlessBlock {
static _toolboxConfig = { icon, title }
private _stats = [10, 10, 10, 10, 10, 10]
constructor (args: BlockToolArgs) {
super(args)
this.data = args.data as DnDStatsData
this._element = this._render()
}
public get data () {
return {
text: this._stats.join(',')
}
}
public set data (data: DnDStatsData) {
if (data.text === undefined) data.text = ''
const newStats = data.text.split(',')
.map(x => parseInt(x, 10))
.filter(x => !Number.isNaN(x))
while (newStats.length < 6) newStats.push(10) // fill missing stats
this._stats = newStats
}
// creates a random four character long id
private randomId (): string {
const min = 46656 // '1000'
const max = 1679615 /* 'zzzz' */ - 46656 /* '1000' */
return (min + Math.floor(max * Math.random())).toString(36)
}
private renderStatMod (value: number): string {
const mod = Math.floor((value - 10) / 2.0)
const sign = mod < 0 ? '' : '+'
return ` (${sign}${mod})`
}
private createStatBlock (title: string, value: number, changeHandler: (newValue: number) => void): HTMLElement {
const id = `dnd-stat-${title}-${this.randomId()}`
const labelWrapper = document.createElement('label')
const titleEl = document.createElement('span')
const statInputEl = document.createElement('input')
const statModEl = document.createElement('span')
// should allow focussing block with tab
labelWrapper.setAttribute('z-index', '1')
labelWrapper.classList.add('dnd-stat-block')
labelWrapper.setAttribute('for', id)
titleEl.classList.add('dnd-stat-title')
titleEl.innerText = title
statInputEl.id = id
statInputEl.value = `${value}`
statInputEl.addEventListener('input', () => {
const value = parseInt(statInputEl.value, 10)
statModEl.innerText = this.renderStatMod(value)
changeHandler(value)
})
statModEl.innerText = this.renderStatMod(value)
labelWrapper.appendChild(titleEl)
labelWrapper.appendChild(statInputEl)
labelWrapper.appendChild(statModEl)
return labelWrapper
}
protected _render (): HTMLElement {
const el = document.createElement('div')
el.classList.add('card-dnd-stats')
const stats = this._stats || [10, 10, 10, 10, 10, 10]
const titles = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
stats.forEach((stat, i) => {
const title = titles[i]
const block = this.createStatBlock(title, stat, newValue => {
this._stats[i] = newValue
})
el.appendChild(block)
})
return el
}
public save (): DnDStatsData {
return this.data
}
}
export default DnDStats

@ -1,159 +0,0 @@
import {
ContentBlock,
ContentBlockArgs,
ContentBlockConfig,
ContentBlockData
} from './content-block'
import icon from '../assets/editor/header.svg.txt'
import icon1 from '../assets/editor/header1.svg.txt'
import icon2 from '../assets/editor/header2.svg.txt'
import icon3 from '../assets/editor/header3.svg.txt'
import icon4 from '../assets/editor/header4.svg.txt'
import icon5 from '../assets/editor/header5.svg.txt'
import icon6 from '../assets/editor/header6.svg.txt'
const title = 'Heading'
enum HeadingLevel {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6
}
const icons = [null, icon1, icon2, icon3, icon4, icon5, icon6]
interface HeadingConfig extends ContentBlockConfig {
placeholder?: string;
levels?: HeadingLevel[];
defaultLevel?: HeadingLevel;
}
interface HeadingData extends ContentBlockData {
text: string;
level?: HeadingLevel;
}
class Heading extends ContentBlock {
static _supportedTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
static _toolboxConfig = { icon, title }
protected _config: HeadingConfig
private defaultLevel: HeadingLevel
private currentLevel: HeadingLevel
constructor (args: ContentBlockArgs) {
super(args)
this._config = args.config as HeadingConfig
if (this._config.levels === undefined) {
this._config.levels = [HeadingLevel.Two, HeadingLevel.Three]
}
if (this._config.defaultLevel === undefined) {
this._config.defaultLevel = HeadingLevel.Two
}
if (this._config.levels.indexOf(this._config.defaultLevel) === -1) {
console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels')
}
this.defaultLevel = this._config.defaultLevel
this.currentLevel = this.defaultLevel
// setting data will rerender the element with the right settings
this.data = {
level: this.currentLevel,
text: (args.data as HeadingData).text || ''
}
this._settingButtons = this._config.levels.map(level => {
return {
name: `H${level}`,
icon: icons[level] || icon,
action: (name: string) => this.setLevel(name),
isActive: (name: string): boolean => this.isCurrentLevel(name)
}
})
}
public get data (): HeadingData {
return this._data as HeadingData
}
public set data (data: HeadingData) {
const currentData = this._data as HeadingData
if (data.level === undefined) data.level = currentData.level || this.defaultLevel
if (data.text === undefined) data.text = currentData.text || ''
this._data = data
this.currentLevel = data.level
const newHeader = this._render()
if (this._element.parentNode) {
this._element.parentNode.replaceChild(newHeader, this._element)
}
this._element = newHeader
}
private isCurrentLevel (name: string): boolean {
const currentLevel = `H${this.currentLevel}`
return name === currentLevel
}
private setLevel (name: string) {
const level = parseInt(name[1], 10)
this.data = { level, text: this._element.innerHTML }
}
protected _render (): HTMLElement {
const el = document.createElement(`H${this.currentLevel}`)
el.innerHTML = this.data.text || ''
el.classList.add(this._CSS.block)
el.contentEditable = 'true'
el.dataset.placeholder = this._config.placeholder || ''
return el
}
// Handle pasted H1-H6 tags to substitute with header tool
protected pasteHandler (element: HTMLHeadingElement): HeadingData {
const text = element.innerHTML
let level = this.defaultLevel
const tagMatch = element.tagName.match(/H(\d)/)
if (tagMatch) level = parseInt(tagMatch[1], 10)
// Fallback to nearest level when specified not available
if (this._config.levels) {
level = this._config.levels.reduce((prevLevel, currLevel) => {
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel
})
}
return { level, text }
}
// Method that specified how to merge two Text blocks.
// Called by Editor.js by backspace at the beginning of the Block
public merge (data: HeadingData) {
this.data = {
text: this.data.text + (data.text || ''),
level: this.data.level
}
}
// extract tools data from view
public save (toolsContent: HTMLElement): HeadingData {
return {
text: toolsContent.innerHTML,
level: this.currentLevel
}
}
static get sanitize () {
return { level: {} }
}
}
export default Heading

@ -1,4 +1,81 @@
export { default as Delimiter } from './delimiter'
export { default as Heading } from './heading'
export { default as Charges } from './charges'
export { default as DnDStats } from './dnd-stats'
import { elementNameToMenuState, marks, blocks } from './constants'
export {
isRootNode,
isRootChild,
isElementNode,
isTextNode,
isEmptyTextNode,
getFocussedNode
} from './node'
export {
moveCaretToBOL,
moveCaretToEOL,
selectLine
} from './caret'
export type State = KV<boolean>
export {
movementKeys,
controlSequenceKeys,
marks,
blocks
} from './constants'
function simpleAction (cmd: string, arg?: string): () => boolean {
return () => {
return document.execCommand(cmd, false, arg)
}
}
export const menuActionToCommand: KV<() => boolean> = {
paragraph: simpleAction('formatblock', 'P'),
heading1: simpleAction('formatblock', 'H1'),
heading2: simpleAction('formatblock', 'H2'),
heading3: simpleAction('formatblock', 'H3'),
bulletList: simpleAction('insertUnorderedList'),
numberedList: simpleAction('insertOrderedList'),
separator: simpleAction('insertHorizontalRule'),
bold: simpleAction('bold'),
italic: simpleAction('italic')
}
export function getActiveMarksAndBlocks (el: HTMLElement): {
marks: string[];
block: string;
} {
let activeBlock = 'paragraph'
const activeMarks: string[] = []
const focussedEl = el.nodeName === '#text' ? el.parentElement : el
if (!focussedEl) return { marks: activeMarks, block: activeBlock }
const focussedState = elementNameToMenuState[focussedEl.nodeName]
if (!focussedState) return { marks: activeMarks, block: activeBlock }
if (blocks.indexOf(focussedState) >= 0) {
activeBlock = focussedState
return { marks: activeMarks, block: activeBlock }
}
let wrappingEl = focussedEl.parentElement
let wrappingState: string
if (marks.indexOf(focussedState) >= 0) {
activeMarks.push(focussedState)
while (wrappingEl) {
wrappingState = elementNameToMenuState[wrappingEl.nodeName]
if (marks.indexOf(wrappingState) < 0) {
if (blocks.indexOf(wrappingState) >= 0) activeBlock = wrappingState
break
}
activeMarks.push(wrappingState)
wrappingEl = wrappingEl.parentElement
}
}
return { marks: activeMarks, block: activeBlock }
}

@ -0,0 +1,20 @@
const { TEXT_NODE, ELEMENT_NODE } = Node
export function getFocussedNode (): Node | null {
return window.getSelection()?.focusNode || null
}
export function isTextNode ({ nodeType }: Node): boolean {
return nodeType === TEXT_NODE
}
export function isElementNode ({ nodeType }: Node): boolean {
return nodeType === ELEMENT_NODE
}
export function isEmptyTextNode (node: Node): boolean {
return isTextNode(node) && (node as CharacterData).data === ''
}
export function isRootNode (node: Node): boolean {
return (node as HTMLElement).contentEditable === 'true'
}
export function isRootChild (node: Node): boolean {
// TODO: maybe use a data attribute or something for saver identification
return node.parentElement?.contentEditable === 'true'
}

@ -45,10 +45,6 @@ export function defaultCard (): Card {
count: 1,
tags: [],
icon: 'robe',
content: {
time: Date.now(),
blocks: [],
version: '2.17.0'
}
content: []
}
}

12
src/modules.d.ts vendored

@ -1,12 +0,0 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
declare module '*.txt' {
const content: string
export default content
}
declare module '@editorjs/paragraph'
declare module '@editorjs/list'

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}

@ -2,15 +2,19 @@ interface KV<V> {
[key: string]: V;
}
interface ContentBlock {
interface TextNode {
type: 'text';
text: string;
}
interface SimpleNode {
type: string;
data: object;
}
interface CardContent {
time: number;
blocks: ContentBlock[];
version: string;
interface ContentNode {
type: string;
content: (ContentNode | TextNode | SimpleNode)[];
attrs?: object;
}
interface Card {
@ -19,7 +23,7 @@ interface Card {
count: number;
tags: string[];
icon: string;
content: CardContent;
content: (ContentNode | SimpleNode)[];
backIcon?: string;
color?: string;
}

@ -86,29 +86,22 @@ export default class DeckView extends Vue {
if (this.deck === null) return
const newCard = defaultCard()
newCard.content = {
time: Date.now(),
blocks: [{
type: 'heading',
data: {
text: 'Next Level RPG Card',
level: 2
}
}, {
type: 'delimiter',
data: { variant: 'pointing-left' }
}, {
type: 'paragraph',
data: { text: 'This card is a rich text editor so you can basically do whatever you want.' }
}, {
type: 'paragraph',
data: { text: ' ' }
}, {
type: 'paragraph',
data: { text: 'You see that delimiter over there? It seems to be wrong, or maybe you like it that way. In any way you can change it by clicking on it and then on the little tool button on the right.' }
}],
version: '2.17.0'
}
newCard.content = [{
type: 'heading',
attrs: { level: 2 },
content: [{
type: 'text',
text: 'feel free to edit this card'
}]
}, {
type: 'horizontal_rule'
}, {
type: 'paragraph',
content: [{
type: 'text',
text: 'This is a rich-text editor, so you can basically do whatever you want.'
}]
}]
this.deck.cards.push(newCard)
this.$storage.persist()

@ -1,10 +0,0 @@
module.exports = {
chainWebpack: config => {
config.module
.rule('raw')
.test(/\.txt$/)
.use('raw-loader')
.loader('raw-loader')
.end()
}
}

@ -770,19 +770,6 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@editorjs/editorjs@^2.17.0":
version "2.17.0"
resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.17.0.tgz#38f20d7f99bc21868904b6b937905b6daad5a2a2"
integrity sha512-5rMjZLdiFOiUGESe5MZagvuVaLggORXBEolbbDLLVWHslR+r4+TACOXBcN8A6m9hMmnpHIJsC3442MZEWdNfQA==
dependencies:
codex-notifier "^1.1.2"
codex-tooltip "^1.0.0"
"@editorjs/list@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.4.0.tgz#e92459a8ac2305bc4385245e329c8b5c8437456a"
integrity sha512-iYDXGbVXvsAJbSxbjFMP4p7kS1zhQyNDqVNzkfMRhItulzKYlOMlFjTIGHqu5SxPy6NrcckhVFaWdfGDn5/gEA==
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -2372,16 +2359,6 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codex-notifier@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
codex-tooltip@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.0.tgz#720353b27fadc40f2d054d171479b016ffcb63ea"
integrity sha512-Wa/p/om166GVjg+q436BERBZZz3yvTnCDDzMV2kjKIzsUkj6vCWphTSTo+M0QJRfwODKzhXYaw8+S4EXPW6r0g==
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@ -6257,6 +6234,11 @@ ora@^3.4.0:
strip-ansi "^5.2.0"
wcwidth "^1.0.1"
orderedmap@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.1.1.tgz#c618e77611b3b21d0fe3edc92586265e0059c789"
integrity sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==
original@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
@ -7022,6 +7004,13 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
prosemirror-model@1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.8.2.tgz#c74eaacb0bbfea49b59a6d89fef5516181666a56"
integrity sha512-piffokzW7opZVCjf/9YaoXvTC0g7zMRWKJib1hpphPfC+4x6ZXe5CiExgycoWZJe59VxxP7uHX8aFiwg2i9mUQ==
dependencies:
orderedmap "^1.1.0"
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -7165,14 +7154,6 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-loader@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.0.tgz#d639c40fb9d72b5c7f8abc1fb2ddb25b29d3d540"
integrity sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==
dependencies:
loader-utils "^1.2.3"
schema-utils "^2.5.0"
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"

Loading…
Cancel
Save