Compare commits

..

3 Commits

Author SHA1 Message Date
evilchili
51795c8f7c ui wip 2025-10-18 11:23:03 -07:00
evilchili
fb391f9f4a only load the viewer when the user cannot write the page 2025-10-17 20:14:25 -07:00
evilchili
83527b85f4 working UI 2025-10-17 19:33:29 -07:00
15 changed files with 405 additions and 198 deletions

View File

@ -16,8 +16,10 @@
{% block nav %}
{% include "nav.html" %}
{% include "breadcrumbs.html" %}
{% endblock %}
<div class='table-wrapper'>
<div class='main-aligned'>
<div class='content'>
<main>
{% for message in g.messages %}
<div class="alert">
@ -27,6 +29,7 @@
{% block content %}{% endblock %}
</main>
</div>
</div>
<footer>
{% block footer %}
fnord

View File

@ -1,7 +1,3 @@
<div id='breadcrumbs'>
<div id='breadcrumbs' class='content-aligned'>
<a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %}
<div id='actions'>
<a id='action__edit' data-state='view'>edit</a>
<a id='action__save' style='display: none;'>save</a>
</div>
</div>

View File

@ -1,5 +1,5 @@
<nav>
<ul class="container">
<ul class="container content-aligned">
<li><a href='{{ root.uri }}'>Home</a></li>
{% for subpage in root.members %}
{% if user.can_read(subpage) %}
@ -14,24 +14,6 @@
[<a href="{{ url_for('logout') }}">Logout</a>]
{% endif %}
</li>
<!--
<li class='dropdown'>
<a href='#'>Pages <i class="fa fa-angle-down"></i></a>
<div class='mega-menu'>
<div class="container">
<div class="item">
<h3>Home Page</h3>
<ul>
<li><a href='#'>Home Page No #1</a></li>
<li><a href='#'>Home Page No #2</a></li>
<li><a href='#'>Home Page No #3</a></li>
<li><a href='#'>Home Page No #4</a></li>
</ul>
</div>
<</div>
</li>
-->
</ul>
</nav>
<menu>

View File

@ -1,60 +1,26 @@
{% extends "base.html" %}
{% block styles %}
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
{% if user.can_write(page) %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='editor/editor.css' ) }}">
{% else %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='viewer/viewer.css' ) }}">
{% endif %}
{% endblock %}
{% block content %}
{% include "breadcrumbs.html" %}
<div id='viewer'></div>
<div id='editor'></div>
<div id='{% if user.can_write(page) %}editor{% else %}viewer{% endif %}' class='read-only'></div>
{% endblock %}
{% block scripts %}
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<script>
const viewerEl = document.querySelector("#viewer");
const editorEl = document.querySelector("#editor");;
const initialValue = (
"# " + document.getElementById("data_form__title").value +
"\n" +
document.getElementById("data_form__body").value
);
const editor = new toastui.Editor({
el: editorEl,
initialEditType: 'wysiwyg',
initialValue: initialValue,
minHeight: '500px',
previewStyle: 'tab',
usageStatistics: false
});
const edit_btn = document.querySelector("#action__edit");
const save_btn = document.querySelector("#action__save");
toggleEditor = function() {
if (edit_btn.dataset.state == 'view') {
edit_btn.dataset.state = 'edit';
edit_btn.innerText = "discard draft";
viewerEl.style['display'] = "none";
editorEl.style['display'] = "inline";
save_btn.style['display'] = 'inline-block';
} else {
edit_btn.dataset.state = 'view';
edit_btn.innerText = "edit";
editorEl.style['display'] = "none";
viewerEl.style['display'] = "inline";
save_btn.style['display'] = 'none';
}
};
edit_btn.addEventListener('click', toggleEditor)
viewerEl.innerHTML = editor.getHTML();
</script>
{% if user.can_write(page) %}
<script src="{{ url_for('static', filename='editor/toastui-editor-all.min.js' ) }}"></script>
<script src="{{ url_for('static', filename='editor/editor.js' ) }}"></script>
<script>initialize();</script>
{% else %}
<script src="{{ url_for('static', filename='viewer/toastui-editor-viewer.min.js' ) }}"></script>
<script src="{{ url_for('static', filename='viewer/viewer.js' ) }}"></script>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,106 @@
@import 'toastui-editor.min.css';
#editor {
display: inline;
}
.toastui-editor-defaultUI-toolbar {
padding: 0px !important;
margin: 0px !important;
border: 0px !important;
background: transparent !important;
}
.toastui-editor-toolbar {
box-shadow: 0px 3px 5px #CCC;
margin-bottom: 20px !important;
}
.toastui-editor-main-container {
display: contents;
}
.toastui-editor,
.toastui-editor-main,
.toastui-editor-md-container,
.toastui-editor-defaultUI,
.toastui-editor-contents,
.toastui-editor-ww-mode,
.toastui-editor-md-preview,
.toastui-editor-md,
.ProseMirror {
border: 0px !important;
padding: 0px !important;
margin: 0px !important;
font-size: var(--default-font-size) !important;
font-family: var(--default-font-family) !important;
}
#editor .toastui-editor-md-container > div > div,
#editor .toastui-editor-ww-container > div > div {
border: 1px dotted #ccc;
}
#editor .toastui-editor-defaultUI-toolbar {
margin-bottom: 20px;
}
#editor .toastui-editor-toolbar {
position: sticky !important;
top: 0 !important;
z-index: 99 !important;
width: 100% !important;
background: #FFF !important;
}
/* applied to #editor */
.read-only {
}
#editor.read-only .toastui-editor-toolbar {
box-shadow: none;
}
#editor.read-only .toastui-editor-ww-container > div > div {
background: #FFF;
}
div.toastui-editor-md-splitter,
div.toastui-editor-md-preview {
display: none !Important;
}
div.toastui-editor.md-mode {
width: 100% !important;
}
#editor button.toastui-editor-toolbar-icons {
filter:unset;
pointer-events: all;
opacity: 1.0;
}
#editor.read-only button.toastui-editor-toolbar-icons {
filter:saturate(0) !Important;
pointer-events: none;
opacity: 0.3;
}
#editor button.actions {
background-image: none;
margin: 0;
}
#editor.read-only button.actions {
display: none;
}
#editor.read-only #toggleButton {
display: block !important;
}
.toastui-editor-defaultUI-toolbar > div:nth-child(5) {
flex: auto;
justify-content: flex-end;
}

View File

@ -0,0 +1,119 @@
var editor = document.querySelector("#editor");
var toolBar = null;
var contents = null;
var pageContent = null;
var saveButton = null;
var editorUI = null;
isReadOnly = function() {
if (editor) {
return editor.className == 'read-only';
}
return undefined;
}
setReadOnly = function() {
pageContent.innerHTML = editorUI.getHTML();
editorUI.changeMode('wysiwig');
editorUI.blur();
editor.classList.add('read-only');
pageContent.contentEditable = false;
}
setEditable = function() {
editor.classList.remove('read-only');
pageContent.contentEditable = true;
editorUI.moveCursorToStart();
editorUI.focus();
}
makeMarkdownButton = function() {
button = document.createElement('button');
button.classList.add("toastui-editor-toolbar-icons");
button.classList.add("last");
button.ariaLabel = "Toggle Markdown";
button.style.backgroundImage = 'none';
button.innerHTML = 'MD';
button.style.margin = '0';
button.addEventListener('click', () => {
if (editorUI.isMarkdownMode()) {
editorUI.changeMode("wysiwig");
} else {
editorUI.changeMode("markdown");
}
});
return button;
};
makeSaveButton = function() {
const button = document.createElement('button');
button.className = 'actions';
button.innerHTML = 'save';
button.id = 'saveButton';
button.addEventListener('click', () => {
});
saveButton = button;
return button;
};
toggleButton = function() {
const button = document.createElement('button');
button.className = 'actions';
button.id = 'toggleButton';
button.innerHTML = 'edit';
button.addEventListener('click', () => {
if (isReadOnly()) {
setEditable();
} else {
setReadOnly();
}
});
return button;
}
handleContentChange = function() {
}
initialize = function() {
return new toastui.Editor({
el: editor,
initialEditType: 'wysiwyg',
initialValue: "",
hideModeSwitch : true,
previewStyle: 'vertical',
usageStatistics: false,
autofocus: false,
toolbarItems: [
['heading', 'bold', 'italic' ],
['ul', 'ol', 'indent', 'outdent'],
['table', 'image', 'link'],
[
{ el: makeMarkdownButton(), tooltip: 'Toggle MD' },
],
[
{ el: makeSaveButton(), command: 'save', tooltip: 'Save Changes' },
{ el: toggleButton(), tooltip: 'Toggle Edit Mode' }
],
],
events: {
'loadUI': function(e) {
editorUI = e;
pageContent = document.querySelector(".toastui-editor-ww-container > div > div");
toolBar = document.querySelector('.toastui-editor-toolbar');
e.setMarkdown(document.getElementById("data_form__body").value);
setReadOnly();
},
'changeMode': function() {
if (editor && Array(editor.classList).includes('read-only')) {
setReadOnly();
}
},
'change': handleContentChange,
}
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,16 +9,17 @@ html {
body {
font-family: var(--default-font-family);
font-size: var(--default-font-size);
background: var(--body-background);
height: inherit;
width: 100%;
margin: 0px;
box-sizing: border-box;
overflow-wrap: break-word;
overflow-y: scroll;
overflow-y: auto;
scrollbar-gutter: stable;
}
h1, .toastui-editor-contents h1 {
h1 {
font-size: var(--h1-size) !important;
font-weight: var(--header-weight) !important;
font-family: var(--header-font) !important;
@ -31,7 +32,7 @@ h1, .toastui-editor-contents h1 {
}
h2, .toastui-editor-contents h2 {
h2 {
font-size: var(--h2-size) !important;
font-weight: var(--header2-weight) !important;
font-family: var(--header-font) !important;
@ -44,7 +45,7 @@ h2, .toastui-editor-contents h2 {
}
h3, .toastui-editor-contents h3 {
h3 {
font-size: var(--h3-size) !important;
font-weight: var(--header3-weight) !important;
font-family: var(--header-font) !important;
@ -57,7 +58,7 @@ h3, .toastui-editor-contents h3 {
}
h4, .toastui-editor-contents h4 {
h4 {
font-size: var(--h4-size) !important;
font-weight: var(--header4-weight) !important;
font-family: var(--header-font) !important;
@ -70,7 +71,7 @@ h4, .toastui-editor-contents h4 {
}
h5, .toastui-editor-contents h5 {
h5 {
font-size: var(--h5-size) !important;
font-weight: var(--header5-weight) !important;
font-family: var(--header-font) !important;
@ -83,7 +84,7 @@ h5, .toastui-editor-contents h5 {
}
h6, .toastui-editor-contents h6 {
h6 {
font-size: var(--h6-size) !important;
font-weight: var(--header6-weight) !important;
font-family: var(--header-font) !important;
@ -99,14 +100,15 @@ h6, .toastui-editor-contents h6 {
a {text-decoration: none;}
nav ul.container {
height: 100%;
align-content: center;
margin: auto;
}
ul {
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
list-style: none;
padding-left: 0;
margin:0;
}
nav {
@ -114,113 +116,64 @@ nav {
margin: 0px;
padding: 0px;
height: var(--nav-height);
background: #0ca0d6;
background: var(--blue);
}
nav > ul > li {
display: inline-block;
font-size: 14px;
padding: 0 15px;
padding: 0 var(--nav-spacing);
position: relative;
}
nav > ul > li:first-child {
padding-left: 0px;
}
nav > ul > li > a {
color: #fff;
color: var(--nav-color);
display: inline-block;
padding: 20px 0;
border-bottom: 3px solid transparent;
transition: all .3s ease;
}
nav > ul > li:hover > a {
color: #444;
border-bottom: 3px solid #444;
color: var(--nav-hover-color);
}
.mega-menu {
background: #eee;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.5s linear;
position: absolute;
left: 0;
width: 100%;
padding-bottom: 20px;
}
.mega-menu h3 {color: #444;}
.mega-menu .container {
display: flex;
}
.mega-menu .item {
flex-grow: 1;
margin: 0 10px;
}
.mega-menu .item img {
width: 100%;
}
.mega-menu a {
border-bottom: 1px solid #ddd;
color: #4ea3d8;
display: block;
padding: 10px 0;
}
.mega-menu a:hover {color: #2d6a91;}
.dropdown {position: static;}
.dropdown:hover .mega-menu {
visibility: visible;
opacity: 1;
}
#breadcrumbs {
display: block;
align-content: center;
height: var(--breadcrumbs-height);
border-bottom: 1px dotted #DEDEDE;
width: 100%;
}
#data_form {
display: none;
}
#editor {
display: none;
}
#viewer {
display: inline;
}
#actions {
display: flex;
float: right;
}
#actions > a {
display: flex;
border: 1px solid black;
border-radius: 5px;
padding: 5px;
background: #FFF;
color: blue;
cursor: pointer;
margin-left: 5px;
}
#actions a:hover {
color: white;
background: blue;
}
.table-wrapper {
padding: 0px;
padding-top: var(--wrapper-padding);
display: table;
.main-aligned {
display: block;
margin: auto;
width: var(--max-width);
max-width: 960px;
max-width: var(--max-width);
min-width: var(--min-width);
}
.content-aligned {
display: block;
margin: auto;
max-width: calc(var(--max-width) - (2 * var(--content-padding)));
min-width: var(--min-width);
padding-left: var(--content-padding);
padding-right: var(--content-padding);
}
.content {
position: relative;
display: table;
width: calc(100% - (2 * var(--content-padding)));
padding: var(--content-padding);
background: var(--content-background);
border: var(--content-border);
border-radius: var(--content-border-radius);
}
main {
@ -231,29 +184,10 @@ main {
footer {
display: block;
position: relative;
bottom: 100vh - var(--footer-height);
bottom: 0;
margin-top: var(--footer-spacing);
height: var(--footer-height);
background-color: #DEDEDE;
}
.toastui-editor-main-container {
display: contents;
}
.toastui-editor,
.toastui-editor-main,
.toastui-editor-md-container,
.toastui-editor-defaultUI,
.toastui-editor-contents,
.toastui-editor-ww-mode,
.toastui-editor-md-preview,
.toastui-editor-md,
.ProseMirror {
border: 0px !important;
padding: 0px !important;
margin: 0px !important;
font-size: var(--default-font-size) !important;
font-family: var(--default-font-family) !important;
background-color: var(--footer-background-color);
}
menu {

View File

@ -12,10 +12,29 @@
--h6-size: 10.72px;
/* Layout */
--content-padding: 20px;
--nav-height: 50px;
--footer-height: 150px;
--nav-spacing: 15px;
--breadcrumbs-height: 50px;
--wrapper-padding: 20px;
--main-height: calc(100vh - var(--nav-height) - var(--footer-height) - var(--breadcrumbs-height) + var(--wrapper-padding));
--max-width: calc(100vw - 20px);
--footer-height: 50px;
--footer-spacing: var(--breadcrumbs-height);
--max-width: 1024px;
--min-width: calc(710px + (2 * var(--content-padding)));
--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));
/* colors */
--blue: #0ca0d6;
--white: #FFF;
--body-background: #EEE;
--nav-color: #000055;
--nav-hover-color: #FFF;
--content-background: #FFF;
--footer-background-color: #DDD;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
@import 'toastui-editor-viewer.min.css';
#viewer {
display: inline;
}
.toastui-editor-contents {
font-size: var(--default-font-size);
}

View File

@ -0,0 +1,6 @@
var viewer = new toastui.Editor({
viewer: true,
el: document.querySelector("#viewer"),
usageStatistics: false,
});
viewer.setMarkdown(document.getElementById("data_form__body").value);

View File

@ -28,15 +28,19 @@ def get_page(path: str, table: str = "Page", create_okay: bool = False):
uri = relative_uri(path)
if table not in app.db.tables():
app.web.logger.debug(f"Table {table} does not exist in {app.db.tables()}.")
return None
page = app.db.table(table).get(where("uri") == uri, recurse=False)
if not page:
app.web.logger.debug("Page does not exist.")
if not create_okay:
app.web.logger.debug("Page does not exist and creating is not okay.")
return None
parent = get_parent(table, uri)
if not app.authorize(g.user, parent, schema.Permissions.READ):
if not app.authorize(g.user, parent, schema.Permissions.WRITE):
app.web.logger.debug(f"User {g.user} is not authorized to write {parent}")
return None
return getattr(schema, table)(name=uri.split("/")[-1], body="This page does not exist", parent=parent)
@ -111,6 +115,8 @@ def logout():
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
def view(table, path):
parent = get_parent(table, relative_uri())
if table not in app.db.tables():
table = parent.__class__.__name__ if parent else "Page"
page = get_page(request.path, table=table, create_okay=(parent and parent.doc_id is not None))
return rendered(page)
@ -142,6 +148,7 @@ def edit(table, path):
@app.web.before_request
def before_request():
g.messages = []
if not request.path.startswith('/static'):
user_id = session.get("user_id", 1)
g.user = app.db.User.get(doc_id=user_id)
session["user_id"] = user_id