Compare commits

...

2 Commits

Author SHA1 Message Date
evilchili
ce8042759e checkpoint 2026-04-28 16:18:22 -07:00
evilchili
d28b31ed14 fix editor layout 2026-01-30 14:59:00 -08:00
9 changed files with 621 additions and 54 deletions

View File

@ -13,14 +13,17 @@
<input name='title' id="data_form__title" type='text' value="{{ page.title }}"> <input name='title' id="data_form__title" type='text' value="{{ page.title }}">
<textarea name="body" id="data_form__body">{{ page.body }}</textarea> <textarea name="body" id="data_form__body">{{ page.body }}</textarea>
</form> </form>
<div id='masthead'>
{% block nav %} {% block nav %}
{% include "nav.html" %} {% include "nav.html" %}
{% include "breadcrumbs.html" %} {% include "breadcrumbs.html" %}
{% endblock %} {% endblock %}
{% if user.can_write(page) %}
{% include "toolbar.html" %}
{% endif %}
</div>
<div class='main-aligned'> <div class='main-aligned'>
<div class='content'> <main id='main'>
<main>
{% for message in g.messages %} {% for message in g.messages %}
<dialog class="alert"> <dialog class="alert">
{{ message }} {{ message }}
@ -28,7 +31,6 @@
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
</div>
</div> </div>
<footer> <footer>
{% block footer %} {% block footer %}

View File

@ -1,4 +1,4 @@
<nav> <nav id='pagenav'>
<ul class="container content-aligned"> <ul class="container content-aligned">
<li><a href='{{ root.uri }}'>Home</a></li> <li><a href='{{ root.uri }}'>Home</a></li>
{% for subpage in root.members %} {% for subpage in root.members %}

View File

@ -1,6 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block styles %} {% block styles %}
{% if user.can_write(page) %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat-editor.css' ) }}">
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -0,0 +1,192 @@
:root {
--toolbar-background: transparent;
--toolbar-border-radius: var(--content-border-radius);
--toolbar-enabled-background: var(--content-background);
--toolbar-height: 32px !important;
--toolbar-spacing: 5px !important;
--toolbar-button-size: 32px;
--toolbar-button-enabled-background: rgba(128, 192, 128);
--toolbar-button-active-background: rgba(192, 255, 192);
--toolbar-button-enabled-border: 1px solid #000;
--toolbar-icon-bold: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/></svg>');
--toolbar-icon-italic: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/></svg>');
/*
--toolbar-icon-underline: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57s-2.687-1.08-2.687-2.57zM12.5 15h-9v-1h9z"/></svg>');
*/
--toolbar-icon-h1: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.648 13V3H6.3v4.234H1.348V3H0v10h1.348V8.421H6.3V13zM14 13V3h-1.333l-2.381 1.766V6.12L12.6 4.443h.066V13z"/></svg>');
--toolbar-icon-h2: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13zm3.174-7.071v-.05c0-.934.66-1.752 1.801-1.752 1.005 0 1.76.639 1.76 1.651 0 .898-.582 1.58-1.12 2.19l-3.69 4.2V13h6.331v-1.149h-4.458v-.079L13.9 8.786c.919-1.048 1.666-1.874 1.666-3.101C15.565 4.149 14.35 3 12.499 3 10.46 3 9.384 4.393 9.384 5.879v.05z"/></svg>');
--toolbar-icon-h3: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11.07 8.4h1.049c1.174 0 1.99.69 2.004 1.724s-.802 1.786-2.068 1.779c-1.11-.007-1.905-.605-1.99-1.357h-1.21C8.926 11.91 10.116 13 12.028 13c1.99 0 3.439-1.188 3.404-2.87-.028-1.553-1.287-2.221-2.096-2.313v-.07c.724-.127 1.814-.935 1.772-2.293-.035-1.392-1.21-2.468-3.038-2.454-1.927.007-2.94 1.196-2.981 2.426h1.23c.064-.71.732-1.336 1.744-1.336 1.027 0 1.744.64 1.744 1.568.007.95-.738 1.639-1.744 1.639h-.991V8.4ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-h4: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M13.007 3H15v10h-1.29v-2.051H8.854v-1.18C10.1 7.513 11.586 5.256 13.007 3m-2.82 6.777h3.524v-5.62h-.074a95 95 0 0 0-3.45 5.554zM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-h5: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M9 10.516h1.264c.193.976 1.112 1.364 2.01 1.364 1.005 0 2.067-.782 2.067-2.247 0-1.292-.983-2.082-2.089-2.082-1.012 0-1.658.596-1.924 1.077h-1.12L9.646 3h5.535v1.141h-4.415L10.5 7.28h.072c.201-.316.883-.84 1.967-.84 1.709 0 3.13 1.177 3.13 3.158 0 2.025-1.407 3.403-3.475 3.403-1.809 0-3.1-1.048-3.194-2.484ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.512h4.854V13z"/></svg>');
--toolbar-icon-h6: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M15.596 5.178H14.3c-.106-.444-.62-1.072-1.706-1.072-1.332 0-2.325 1.269-2.325 3.947h.07c.268-.67 1.043-1.445 2.445-1.445 1.494 0 3.017 1.064 3.017 3.073C15.8 11.795 14.37 13 12.48 13c-1.036 0-2.093-.36-2.77-1.452C9.276 10.836 9 9.808 9 8.37 9 4.656 10.494 3 12.636 3c1.812 0 2.883 1.113 2.96 2.178m-5.151 4.566c0 1.367.944 2.15 2.043 2.15 1.128 0 2.037-.684 2.037-2.136 0-1.41-1-2.065-2.03-2.065-1.19 0-2.05.853-2.05 2.051M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-unordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>');
--toolbar-icon-ordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/><path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/></svg>');
--toolbar-icon-line: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8"/></svg>');
--toolbar-icon-quote: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm5 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm.79-5.373q.168-.117.444-.275L3.524 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562Q2 7.587 2 7.969q0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282q.036-.305.123-.498a1.4 1.4 0 0 1 .252-.37 2 2 0 0 1 .346-.298zm2.167 0q.17-.117.445-.275L5.692 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562q-.165.31-.164.692 0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282a1.8 1.8 0 0 1 .118-.492q.087-.194.257-.375a2 2 0 0 1 .346-.3z"/></svg>');
--toolbar-icon-link: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>');
--toolbar-icon-table: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z"/></svg>');
--toolbar-icon-macro: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.114 8.063V7.9c1.005-.102 1.497-.615 1.497-1.6V4.503c0-1.094.39-1.538 1.354-1.538h.273V2h-.376C3.25 2 2.49 2.759 2.49 4.352v1.524c0 1.094-.376 1.456-1.49 1.456v1.299c1.114 0 1.49.362 1.49 1.456v1.524c0 1.593.759 2.352 2.372 2.352h.376v-.964h-.273c-.964 0-1.354-.444-1.354-1.538V9.663c0-.984-.492-1.497-1.497-1.6M13.886 7.9v.163c-1.005.103-1.497.616-1.497 1.6v1.798c0 1.094-.39 1.538-1.354 1.538h-.273v.964h.376c1.613 0 2.372-.759 2.372-2.352v-1.524c0-1.094.376-1.456 1.49-1.456V7.332c-1.114 0-1.49-.362-1.49-1.456V4.352C13.51 2.759 12.75 2 11.138 2h-.376v.964h.273c.964 0 1.354.444 1.354 1.538V6.3c0 .984.492 1.497 1.497 1.6"/></svg>');
--toolbar-icon-macro_user: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/><path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/></svg>');
--toolbar-icon-macro_toc: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"/><path d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8m0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0M4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/></svg>');
--toolbar-icon-macro_style: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/><path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/></svg>');
--toolbar-icon-markdown: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0m6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0"/></svg>');
--toolbar-icon-save: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/><path fill-rule="evenodd" d="M7.646 4.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V14.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708z"/></svg>');
--toolbar-icon-toggle: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="m22.81,9.08c.807-.642,1.25-1.642,1.183-2.676-.067-1.05-.646-2.001-1.547-2.546-.772-.466-1.946-.892-3.574-1.297-.411-1.476-1.767-2.562-3.372-2.562-1.493,0-2.77.94-3.272,2.259C3.12,6.628.56,12.888.025,17.398c-.195,1.646.332,3.311,1.448,4.567,1.149,1.293,2.799,2.035,4.527,2.035h9c.552,0,1-.448,1-1s-.448-1-1-1h-2v-3c0-1.428.193-3.121.398-4.513l5.464,8.587c.369.579,1,.925,1.687.925h1.451c.552,0,1-.448,1-1s-.448-1-1-1h-1.451s-6.342-9.966-6.342-9.966c.116-.058.245-.095.383-.108,3.276-.294,6.349-1.358,8.22-2.846Zm-7.31-7.08c.827,0,1.5.673,1.5,1.5s-.673,1.5-1.5,1.5-1.5-.673-1.5-1.5.673-1.5,1.5-1.5Zm-3.783,10.367c-.143.797-.297,1.747-.426,2.745-1.189-1.297-2.897-2.112-4.791-2.112h-.5c-.552,0-1,.448-1,1s.448,1,1,1h.5c2.481,0,4.5,2.019,4.5,4.5v2.5h-5c-1.157,0-2.263-.497-3.032-1.363-.747-.841-1.087-1.908-.957-3.003.647-5.452,4.145-9.959,10.144-13.101.442,1.427,1.774,2.467,3.344,2.467,1.541,0,2.853-1.001,3.319-2.387,1.191.315,2.061.636,2.594.958.346.209.56.56.585.962.025.386-.133.744-.433.982-1.857,1.477-4.913,2.218-7.154,2.419-1.351.122-2.459,1.123-2.694,2.433Z" /></svg>');
}
#froghat[contenteditable] {
outline: 0px solid transparent;
}
#froghat {
box-sizing: border-box;
height: -webkit-fill-available;
}
#froghat.edit {
font-family: monospace;
white-space: break-spaces;
}
#froghat.wysiwyg {
span {
display: inline;
border-bottom: 1px solid green;
}
}
main.editing {
border: 1px solid green;
border-top: 1px solid transparent;
border-radius: 0px;
}
#toolbar {
background: var(--toolbar-background);
border-radius: var(--toolbar-border-radius);
height: var(--toolbar-height);
border: 1px solid transparent;
margin: 0;
margin-bottom: 0px;
padding: var(--toolbar-spacing);
ul {
display: flex;
margin: 0px;
padding: 0px;
border-radius: 5px;
li {
padding: 0px;
margin: 2px;
text-align: center;
line-height: var(--toolbar-button-size);
width: var(--toolbar-button-size);
height: var(--toolbar-button-size);
button {
opacity: 0.3;
display: block;
background-repeat: no-repeat;
background-attachment: local;
background-position: center;
background-size: 1.5rem 1.5rem;
border-radius: 5px;
border: 1px solid transparent;
width: var(--toolbar-button-size);
height: var(--toolbar-button-size);
}
button.enabled {
opacity: 1.0;
cursor: pointer;
}
button.on {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-enabled-background);
}
button:hover {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-active-background);
}
.dropdown-menu {
position-area: bottom;
margin: 0;
border: 0;
width: var(--toolbar-button-size);
min-height: var(--toolbar-button-size);
}
#bold { background-image: var(--toolbar-icon-bold); }
#italic { background-image: var(--toolbar-icon-italic); }
#underline { background-image: var(--toolbar-icon-underline); }
#header { background-image: var(--toolbar-icon-h1); anchor-name: "header"; }
#h1 { background-image: var(--toolbar-icon-h1); }
#h2 { background-image: var(--toolbar-icon-h2); }
#h3 { background-image: var(--toolbar-icon-h3); }
#h4 { background-image: var(--toolbar-icon-h4); }
#h5 { background-image: var(--toolbar-icon-h5); }
#h6 { background-image: var(--toolbar-icon-h6); }
#list { background-image: var(--toolbar-icon-unordered_list); anchor-name: "list"; }
#unordered_list { background-image: var(--toolbar-icon-unordered_list); }
#ordered_list { background-image: var(--toolbar-icon-ordered_list); }
#line { background-image: var(--toolbar-icon-line); }
#quote { background-image: var(--toolbar-icon-quote); }
#link { background-image: var(--toolbar-icon-link); }
#table { background-image: var(--toolbar-icon-table); }
#macro { background-image: var(--toolbar-icon-macro); anchor-name: "macro"}
#macro_user { background-image: var(--toolbar-icon-macro_user); }
#macro_toc { background-image: var(--toolbar-icon-macro_toc); }
#macro_style { background-image: var(--toolbar-icon-macro_style); }
#markdown { background-image: var(--toolbar-icon-markdown); }
#save { background-image: var(--toolbar-icon-save); }
#toggle {
background-color: var(--toolbar-button-enabled-background);
background-image: var(--toolbar-icon-toggle);
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
filter: FlipH;
-ms-filter: "FlipH";
opacity: 1.0;
cursor: pointer;
}
#toggle:hover {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-active-background);
}
#header-menu { position-anchor: "header"; }
#list-menu { position-anchor: "list"; }
}
li:last-child {
display: block;
margin-left: auto;
}
}
}
#toolbar.enabled {
background: var(--toolbar-enabled-background);
border-radius: 0px;
border: 1px solid green;
border-bottom: 1px solid transparent;
}

View File

@ -1,3 +1,221 @@
function reEscape(string) {
return string.replaceAll('*', '\\*');
}
class ToolbarButton {
constructor(settings) {
this.id = settings.id;
this.element = settings.element || document.getElementById(this.id);
this.toolbar = settings.toolbar;
this.isMenu = settings.isMenu || false;
this.open = settings.open || '';
this.close = settings.close || '';
this.tag = settings.tag || null;
this.onclick = settings.onclick;
if (!this.isMenu) {
if (this.open) {
var open = reEscape(this.open);
var close = this.close ? reEscape(this.close) : '';
var leading = '(?<leading>^(?:(?!' + open + ').)*)';
var middle = '(?<middle>(?:(?!' + open;
if (this.close && this.close != this.open) {
middle += '|' + close + ').)+)';
} else {
middle += ').)*)';
}
this.pattern = RegExp(
leading +
'(?<open>' + open + ')' +
middle +
(close ? '(?<closed>(?:' + close + ')(\\s.*?|$))' : '') +
'$'
);
console.log(this.id, this.pattern);
}
this.element.addEventListener('click', (e) => {
if (this.element.enabled) {
if (this.onclick) {
this.onclick({clickEvent: e, button: this });
} else if (this.toolbar.editor.isWysiwyg()) {
this.#applyHTMLFormatting();
} else if (this.toolbar.editor.isMarkdown()) {
this.#applyMarkdownFormatting();
}
}
document.querySelectorAll(".dropdown-menu:popover-open").forEach(el => {
el.hidePopover();
});
this.toolbar.editor.element.focus();
});
}
if (settings.enabled) {
this.enable();
}
}
#applyHTMLFormatting() {
if (!this.tag) {
return;
}
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var node = document.createElement(this.tag);
try {
range.surroundContents(node);
range.setStartAfter(node);
} catch(e) {
console.log(e);
}
selection.removeAllRanges();
selection.addRange(range);
this.toolbar.editor.moveCursorAfter(node);
}
#applyMarkdownFormatting() {
if (!this.open) {
return;
}
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var node = document.createTextNode(this.open + range.toString() + this.close);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
this.toolbar.editor.moveCursorAfter(node);
}
click() {
if (this.element.enabled) {
this.element.click();
}
}
enable() {
this.element.enabled = true;
this.element.classList.add('enabled');
}
disable() {
this.element.enabled = false;
this.element.classList.remove('enabled');
}
on() {
this.element.classList.add('on');
}
off() {
this.element.classList.remove('on');
}
toggle() {
this.element.classList.toggle('on');
}
}
class FroghatToolbar {
constructor(settings) {
this.editor = settings.editor;
this.element = settings.toolbar || document.getElementById('toolbar');
this.currentContext = null;
this.buttons = {
'line': new ToolbarButton({ toolbar: this, id: 'line', open: '***', close: '', tag: 'HR'}),
'bold': new ToolbarButton({ toolbar: this, id: 'bold', open: '**', close: '**', tag: 'STRONG'}),
'italic': new ToolbarButton({ toolbar: this, id: 'italic', open: '*', close: '*', tag: 'EM'}),
'header': new ToolbarButton({ toolbar: this, id: 'header', isMenu: true}),
'h1': new ToolbarButton({ toolbar: this, id: 'h1', markdown: '# ', close: "", tag: 'H1' }),
'h2': new ToolbarButton({ toolbar: this, id: 'h2', markdown: '## ', close: "", tag: 'H2' }),
'h3': new ToolbarButton({ toolbar: this, id: 'h3', markdown: '### ', close: "", tag: 'H3' }),
'h4': new ToolbarButton({ toolbar: this, id: 'h4', markdown: '#### ', close: "", tag: 'H4' }),
'h5': new ToolbarButton({ toolbar: this, id: 'h5', markdown: '##### ', close: "", tag: 'H5' }),
'h6': new ToolbarButton({ toolbar: this, id: 'h6', markdown: '###### ', close: "", tag: 'H6' }),
'list': new ToolbarButton({ toolbar: this, id: 'list', isMenu: true}),
'unordered_list': new ToolbarButton({ toolbar: this, id: 'unordered_list', }),
'ordered_list': new ToolbarButton({ toolbar: this, id: 'ordered_list', }),
'link': new ToolbarButton({ toolbar: this, id: 'link', }),
'quote': new ToolbarButton({ toolbar: this, id: 'quote', }),
'table': new ToolbarButton({ toolbar: this, id: 'table', }),
'macro': new ToolbarButton({ toolbar: this, id: 'macro', isMenu: true}),
'macro_user': new ToolbarButton({ toolbar: this, id: 'macro_user', }),
'macro_toc': new ToolbarButton({ toolbar: this, id: 'macro_toc', }),
'macro_style': new ToolbarButton({ toolbar: this, id: 'macro_style', }),
'save': new ToolbarButton({ toolbar: this, id: 'save', onclick: this.#click_save }),
'markdown': new ToolbarButton({ toolbar: this, id: 'markdown', onclick: this.#click_markdown }),
'toggle': new ToolbarButton({ toolbar: this, id: 'toggle', onclick: this.#click_toggle, enabled: true })
}
}
getContext() {
var context = null;
var node = window.getSelection().baseNode;
console.log({node});
Object.values(this.buttons).forEach(button => {
if (button.pattern) {
var closed = false;
var leading = "";
var matched = false;
matched = node.textContent.match(button.pattern);
if (matched && matched.groups) {
closed = matched.groups.closed;
leading = matched.groups.leading;
}
if (closed && matched.groups.middle) {
context = {
button: button,
closed: closed,
leading: leading,
node: node,
element: null
}
}
if (node.parentElement && node.parentElement.nodeName == button.tag) {
if (!context) {
context = {
button: button,
element: null
};
}
context.element = node.parentElement
}
if (context) {
return;
}
}
});
return context;
}
#click_save({clickEvent, button}) {
}
#click_markdown({clickEvent, button}) {
button.toolbar.editor.toggleMarkdown();
}
#click_toggle({clickEvent, button}) {
button.toolbar.editor.toggleView();
}
enable() {
this.element.classList.add("enabled");
Object.values(this.buttons).forEach(button => { button.enable() });
}
disable() {
this.element.classList.remove("enabled");
Object.values(this.buttons).forEach(button => { button.disable() });
this.buttons.toggle.enable();
}
}
class FroghatEditor extends Froghat { class FroghatEditor extends Froghat {
run() { run() {
@ -7,9 +225,13 @@ class FroghatEditor extends Froghat {
WYSIWYG: 'wysiwyg' WYSIWYG: 'wysiwyg'
} }
this.toolbar = new FroghatToolbar({editor: this});
this.turndown = new TurndownService({ this.turndown = new TurndownService({
headingStyle: 'atx', headingStyle: 'atx',
codeBlockStyle: 'fenced', codeBlockStyle: 'fenced',
emDelimiter: '*',
strongDelimiter: '**',
}); });
this.turndown.use([turndownPluginGfm.gfm, turndownPluginGfm.tables]); this.turndown.use([turndownPluginGfm.gfm, turndownPluginGfm.tables]);
this.turndown.keep(['pre']); this.turndown.keep(['pre']);
@ -20,18 +242,89 @@ class FroghatEditor extends Froghat {
this.view(); this.view();
} }
#replaceWysiwygNode(context) {
var offset = context.leading.length || 0;
var slice = context.node.textContent.slice(offset);
var html = this.markdownToHTML(slice);
html = html.replace("<p>", "").replace("</p>", "");
var el = document.createElement("span");
el.innerHTML = context.leading + html;
console.log(el.innerHTML);
if (el.innerHTML != context.node.innerHTML) {
context.node.replaceWith(el);
}
return el;
}
#refreshMarkdownCache() {
if (this.cachedMarkdown != this.element.textContent) {
this.changed = true;
this.toolbar.buttons.save.enable();
this.cachedMarkdown = this.element.textContent;
}
}
#handleEditorChanges(evt) {
var context = this.toolbar.getContext();
if (!context) {
if (this.toolbar.currentContext) {
this.toolbar.currentContext.button.off();
this.toolbar.currentContext = null;
}
return;
}
if (this.toolbar.currentContext) {
this.toolbar.currentContext.button.off();
}
context.button.on();
this.toolbar.currentContext = context;
if (this.isWysiwyg()) {
if (context.closed) {
context.node = this.#replaceWysiwygNode(context);
context.button.off();
this.toolbar.currentContext = null;
this.toolbar.editor.moveCursorAfter(context.node);
}
} else if (context.button) {
context.button.on();
}
}
#bindEvents() { #bindEvents() {
this.element.addEventListener('keydown', (evt) => { this.element.addEventListener('keydown', (evt) => {
if (this.state === this.states.VIEW) { if (! this.isEditing()) {
return; return;
} }
if (this.cachedMarkdown != this.element.textContent) { this.#refreshMarkdownCache();
this.changed = true; this.#handleEditorChanges(evt);
this.cachedMarkdown = this.element.textContent;
}
}); });
}; };
toggleMarkdown() {
if (this.getState() === this.states.EDIT) {
this.wysiwyg();
} else {
this.edit();
}
}
toggleView() {
if (this.getState() === this.states.VIEW) {
this.toolbar.enable();
this.wysiwyg();
this.element.focus();
} else {
this.toolbar.disable();
this.view();
}
}
htmlToMarkdown(html) { htmlToMarkdown(html) {
return this.turndown.turndown(html || this.element.innerHTML); return this.turndown.turndown(html || this.element.innerHTML);
} }
@ -60,7 +353,6 @@ class FroghatEditor extends Froghat {
if (this.getState() === this.states.WYSIWYG) { if (this.getState() === this.states.WYSIWYG) {
return; return;
} }
this.changed = false;
this.element.contentEditable = true; this.element.contentEditable = true;
this.element.innerHTML = this.getHTML(); this.element.innerHTML = this.getHTML();
Array.from(this.element.querySelectorAll('.macro')).forEach(el => { Array.from(this.element.querySelectorAll('.macro')).forEach(el => {
@ -70,6 +362,9 @@ class FroghatEditor extends Froghat {
} }
}); });
this.setState(this.states.WYSIWYG); this.setState(this.states.WYSIWYG);
this.toolbar.buttons.markdown.off();
this.toolbar.buttons.markdown.enable();
document.getElementById("main").classList.add("editing");
} }
edit() { edit() {
@ -79,21 +374,48 @@ class FroghatEditor extends Froghat {
if (this.state === this.states.EDIT) { if (this.state === this.states.EDIT) {
return; return;
} }
this.changed = false;
this.element.contentEditable = true; this.element.contentEditable = true;
this.element.innerHTML = encodeHtmlEntities(this.getMarkdown()); this.element.innerHTML = encodeHtmlEntities(this.getMarkdown());
this.setState(this.states.EDIT); this.setState(this.states.EDIT);
this.toolbar.buttons.markdown.on();
document.getElementById("main").classList.add("editing");
} }
insertAtCursor(node) { moveCursorAfter(node) {
var sel, range, html; var dummyElement = null;
sel = window.getSelection(); if (!node.nextElementSibling) {
range = sel.getRangeAt(0); // workaround for https://issues.chromium.org/issues/41239578
range.deleteContents(); var dummyElement = document.createElement('a');
range.insertNode(node); dummyElement.innerHTML="&#x200B;";
range.setStartAfter(node); dummyElement.className = 'bugfix';
this.element.focus(); node.parentNode.appendChild(dummyElement);
sel.removeAllRanges(); }
sel.addRange(range); var nextElement = node.nextElementSibling;
nextElement.tabIndex=0;
nextElement.focus();
var range = document.createRange();
range.setStart(nextElement.childNodes[0], 1);
range.setEnd(nextElement.childNodes[0], 1);
var selection = window.getSelection();
console.log(range);
console.log(selection);
selection.removeAllRanges();
selection.addRange(range);
} }
isWysiwyg() {
return this.state === this.states.WYSIWYG;
}
isMarkdown() {
return this.state === this.states.EDIT;
}
isEditing() {
return this.isWysiwyg() || this.isMarkdown();
}
} }

View File

@ -10,7 +10,7 @@ body {
font-family: var(--default-font-family); font-family: var(--default-font-family);
font-size: var(--default-font-size); font-size: var(--default-font-size);
background: var(--body-background); background: var(--body-background);
height: inherit; height: auto;
width: 100%; width: 100%;
margin: 0px; margin: 0px;
box-sizing: border-box; box-sizing: border-box;
@ -125,12 +125,21 @@ nav ul.container {
margin: auto; margin: auto;
} }
#masthead {
background: var(--body-background);
padding-bottom: var(--masthead-spacing);
display: block;
height: fit-content;
position:sticky;
top: 0px;
z-index: 100;
}
nav { nav {
display: block; display: block;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
height: var(--nav-height); height: var(--nav-height);
background: var(--blue);
} }
nav > ul > li { nav > ul > li {
@ -144,13 +153,21 @@ nav > ul > li:first-child {
} }
nav > ul > li > a { nav > ul > li > a {
color: var(--nav-color);
display: inline-block; display: inline-block;
} }
nav > ul > li:hover > a {
#pagenav > ul > li > a {
color: var(--nav-color);
}
#pagenav > ul > li:hover > a {
color: var(--nav-hover-color); color: var(--nav-hover-color);
} }
#pagenav {
background: var(--blue);
}
.dropdown {position: static;} .dropdown {position: static;}
#breadcrumbs { #breadcrumbs {
@ -180,21 +197,17 @@ nav > ul > li:hover > a {
padding-right: var(--content-padding); padding-right: var(--content-padding);
} }
.content { main {
position: relative; position: relative;
display: table; display: block;
width: calc(100% - (2 * var(--content-padding))); width: calc(100% - (2 * var(--content-padding)) - 2px);
min-height: var(--main-height);
height: fit-content;
padding: var(--content-padding); padding: var(--content-padding);
background: var(--content-background); background: var(--content-background);
border: var(--content-border);
border-radius: var(--content-border-radius); border-radius: var(--content-border-radius);
} }
main {
display: table-row;
height: var(--main-height);
}
footer { footer {
display: block; display: block;
position: relative; position: relative;
@ -350,14 +363,4 @@ div.macro {
#froghat.view { #froghat.view {
} }
#froghat.edit {
font-family: monospace;
white-space: pre;
}
#froghat.wysiwyg {
}
#froghat.wysiwyg .md {
opacity: 0.5;
}

View File

@ -143,6 +143,7 @@ class Froghat {
this.element.innerHTML = this.getHTML(); this.element.innerHTML = this.getHTML();
this.setState(this.states.VIEW); this.setState(this.states.VIEW);
this.element.contentEditable = false; this.element.contentEditable = false;
document.getElementById("main").classList.remove("editing");
} }
} }

View File

@ -18,23 +18,24 @@
--breadcrumbs-height: 50px; --breadcrumbs-height: 50px;
--footer-height: 50px; --footer-height: 50px;
--footer-spacing: var(--breadcrumbs-height); --footer-spacing: var(--breadcrumbs-height);
--toolbar-height: 0px;
--toolbar-spacing: 0px;
--masthead-spacing: 0px;
--max-width: 1024px; --max-width: 1024px;
--min-width: calc(710px + (2 * var(--content-padding))); --min-width: calc(710px + (2 * var(--content-padding)));
--main-height: calc(100vh - var(--nav-height) - var(--masthead-spacing) - var(--toolbar-height) - (3 * var(--toolbar-spacing)) - var(--footer-height) - var(--breadcrumbs-height) - calc(2 * var(--content-padding)) - var(--footer-spacing));
--main-height: calc(100vh - var(--nav-height) - var(--footer-height) - var(--breadcrumbs-height) - calc(2 * var(--content-padding)) - var(--footer-spacing));
--content-border-radius: calc(0.5 * var(--content-padding)); --content-border-radius: calc(0.5 * var(--content-padding));
--content-border: 1px solid transparent;
/* colors */ /* colors */
--blue: #0ca0d6; --blue: #0ca0d6;
--white: #FFF; --white: #FFF;
--body-background: #EEE; --body-background: #EEE;
--nav-color: #000055; --nav-color: #000055;
--nav-hover-color: #FFF; --nav-hover-color: #FFF;
--content-background: #FFF; --content-background: #FFF;
--footer-background-color: #DDD; --footer-background-color: #DDD;
} }

View File

@ -0,0 +1,43 @@
<div class='main-aligned'>
<nav id='toolbar'>
<ul>
<li><button id='bold'></button></li>
<li><button id='italic'></button></li>
<li>
<button id='header' popovertarget='header-menu'></button>
<div id='header-menu' class='dropdown-menu' popover>
<button id='h1'></button>
<button id='h2'></button>
<button id='h3'></button>
<button id='h4'></button>
<button id='h5'></button>
<button id='h6'></button>
</div>
</li>
<li><button id='link'></button></li>
<li class='spacer'></li>
<li><button id='line'></button></li>
<li>
<button id='list' popovertarget='list-menu'></button>
<div id='list-menu' class='dropdown-menu' popover>
<button id='unordered_list'></button>
<button id='ordered_list'></button>
</div>
</li>
<li><button id='quote'></button></li>
<li><button id='table'></button></li>
<li>
<button id='macro' popovertarget='macro-menu'></button>
<div id='macro-menu' class='dropdown-menu' popover>
<button id='macro_user'></button>
<button id='macro_toc'></button>
<button id='macro_style'></button>
</div>
</li>
<li class='spacer'></li>
<li><button id='markdown' alt='toggle markdown editor'></button></li>
<li><button id='save' alt='save'></button></li>
<li><button id='toggle' alt='toggle edit mode'></button></li>
</ul>
</nav>
</div>