diff --git a/amanuensis/resources/editor.css b/amanuensis/resources/editor.css index 2ccbbaa..450fa08 100644 --- a/amanuensis/resources/editor.css +++ b/amanuensis/resources/editor.css @@ -1,76 +1,75 @@ html, body { - height: 100%; - margin: 0px; + height: 100%; + margin: 0px; } div#wrapper { - max-width: 1128px; - height: 100%; - display:flex; - flex-direction: row; - align-items: stretch; + height: 100%; + display: flex; + flex-direction: row; + align-items: stretch; } div.column { - width: 50%; + width: 50%; } div#editor-left { - display: flex; - align-items: stretch; + display: flex; + align-items: stretch; } div#editor-left section { - display: flex; - flex-direction: column; - margin: 10px 5px 10px 10px; - width: 100%; - padding: 5px; + display: flex; + flex-direction: column; + margin: 10px 5px 10px 10px; + width: 100%; + padding: 5px; } div#editor-left div#editor-header { - margin: 5px; - display: flex; - justify-content: space-between; + margin: 5px; + display: flex; + justify-content: space-between; } div#editor-left div#editor-charselect { - margin: 5px; + margin: 5px; } div#editor-left div#editor-buttons { - margin: 5px; + margin: 5px; } div#editor-left input#editor-title { - font-size: 2em; - margin: 5px; + font-size: 2em; + margin: 5px; } textarea#editor-content { - margin: 5px; - resize: none; - flex-grow: 1; - width: initial; + margin: 5px; + resize: none; + flex-grow: 1; + width: initial; } div#editor-right { - overflow-y: scroll; + overflow-y: scroll; } div#editor-right section { - margin: 10px 5px 10px 10px; + margin: 10px 5px 10px 10px; } span.message-warning { - color: #808000; + color: #808000; } span.message-error { - color: #ff0000; + color: #ff0000; } @media only screen and (max-width: 816px) { - div#wrapper { - max-width: 564px; - margin: 0 auto; - padding: inherit; - flex-direction: column; - } - div.column { - width: 100%; - } - div#editor-left { - height: 100%; - } - div#editor-right { - overflow-y: inherit; - } + div#wrapper { + max-width: 564px; + margin: 0 auto; + padding: inherit; + flex-direction: column; + } + div.column { + width: 100%; + } + div#editor-left { + height: 100%; + } + div#editor-right { + overflow-y: inherit; + } } diff --git a/amanuensis/resources/editor.js b/amanuensis/resources/editor.js index ef663bc..fcdbcf2 100644 --- a/amanuensis/resources/editor.js +++ b/amanuensis/resources/editor.js @@ -1,126 +1,152 @@ -// Reduce unnecessary requests by checking for no further changes being made -// before updating in response to a change. -var nonce = 0; +(function(){ + /** Article submission state. */ + const ArticleState = { + DRAFT: 0, + SUBMITTED: 1, + APPROVED: 2 + }; -function ifNoFurtherChanges(callback, timeout=2000) { - var nonce_local = Math.random(); - nonce = nonce_local; - setTimeout(() => { - if (nonce == nonce_local) { - callback(); - nonce = 0; - } - }, timeout); -} + /** Article state to be tracked in addition to the editable content. */ + var article = { + state: undefined, + ersatz: false + }; -// Read data out of params and initialize editor -window.onload = function() { - // Kill noscript message first - document.getElementById("preview").innerHTML = "

 

"; + /** Article content as last received from the server. */ + var preview = { + title: undefined, + rendered: undefined, + citations: [], + errors: [] + } - if (document.body.contains(document.getElementById("editor-content"))) { - onContentChange(0); - } -}; + /** The nonce of the last-made update request. */ + let nonce = 0; -function buildArticleObject() { - var title = document.getElementById("editor-title").value; - var contents = document.getElementById("editor-content").value; - return { - aid: params.article.aid, - title: title, - status: params.article.status, - contents: contents - }; -} + /** + * Update request debounce wrapper that executes the callback if no further + * calls are made during the timeout period. If a new call is made, any + * previous calls are skipped. + */ + function ifNoFurtherChanges(callback, timeout) + { + // Stake a claim on the nonce, potentially overwriting a previous + // nonce value. + const nonce_local = 1 + Math.random(); + nonce = nonce_local; + // Wait to see if this call is overwritten in turn. + setTimeout(() => { + if (nonce == nonce_local) + { + callback(); + nonce = 0; + } + }, timeout); + } -function update(article) { - var req = new XMLHttpRequest(); - req.open("POST", params.updateURL, true); - req.setRequestHeader("Content-type", "application/json"); - req.responseType = "json"; - req.onreadystatechange = function () { - if (req.readyState == 4 && req.status == 200) { - // Update internal state with the returned article object - params.status = req.response.status; - params.errors = req.response.error.length; - document.getElementById("editor-title").value = req.response.title; - // Set editor editability based on article status - updateEditorStatus(); - // Update the preview with the parse information - updatePreview(req.response); - } - }; - var payload = { article: article }; - req.send(JSON.stringify(payload)); -} + /** Update the editor controls and preview to match the current state. */ + function refreshEditor() + { + // Enable or disable controls + const isEditable = article.state == ArticleState.DRAFT; + const blocked = preview.errors.filter(err => err.severity == 2).length > 0; + document.getElementById("editor-title").disabled = !isEditable; + document.getElementById("editor-content").disabled = !isEditable; + document.getElementById("button-submit").innerText = isEditable ? "Submit article" : "Edit article"; + document.getElementById("button-submit").disabled = blocked; -function updateEditorStatus() { - var ready = !!params.status.ready || !!params.status.approved; - document.getElementById("editor-title").disabled = ready; - document.getElementById("editor-content").disabled = ready; - var hasErrors = params.errors > 0; - var submitButton = document.getElementById("button-submit"); - submitButton.innerText = ready ? "Edit article" : "Submit article"; - submitButton.disabled = hasErrors; -} + // Update the preview + const previewHtml = "

" + preview.title + "

\n" + preview.rendered; + document.getElementById("preview").innerHTML = previewHtml; -function updatePreview(response) { - var previewHtml = "

" + response.title + "

\n" + response.rendered; - document.getElementById("preview").innerHTML = previewHtml; + // Fill in the citation block + let citations = "
    "; + preview.citations.forEach(cit => citations += "
  1. " + JSON.stringify(cit) + "
  2. "); + citations += "
"; + document.getElementById("preview-citations").innerHTML = citations; - var citations = "
    "; - for (var i = 0; i < response.citations.length; i++) { - citations += "
  1. " + response.citations[i] + "
  2. "; - } - citations += "
"; - document.getElementById("preview-citations").innerHTML = citations; + // Fill in the status message block + let statuses = "
    "; + preview.errors.forEach(err => statuses += "
  1. " + JSON.stringify(err) + "
  2. "); + statuses += "
      "; + document.getElementById("preview-control").innerHTML = statuses; + } - var info = ""; - for (var i = 0; i < response.info.length; i++) { - info += "" + response.info[i] + "
      "; - } - var warning = ""; - for (var i = 0; i < response.warning.length; i++) { - warning += "" + - response.warning[i] + "
      "; - } - var error = ""; - for (var i = 0; i < response.error.length; i++) { - error += "" + response.error[i] + "
      "; - } - var control = info + warning + error; - document.getElementById("preview-control").innerHTML = control; -} + /** Update the current state with the given data and refresh the editor. */ + function updateState(data) + { + article.state = data.state; + article.ersatz = data.ersatz; + preview.title = data.title; + preview.rendered = data.rendered; + preview.citations = data.citations; + preview.errors = data.errors; + refreshEditor(); + } -function onContentChange(timeout=2000) { - ifNoFurtherChanges(() => { - var article = buildArticleObject(); - update(article); - }, timeout); -} + /** Send the article's current content to the server. */ + function update() + { + const updateUrl = document.body.dataset.amanuensisUpdateUrl; + const data = { + title: document.getElementById("editor-title").value, + body: document.getElementById("editor-content").value, + state: article.state + }; + fetch(updateUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }).then(response => response.json()).then(updateState); + } -function submitArticle() { - ifNoFurtherChanges(() => { - params.article.status.ready = !params.article.status.ready; - var article = buildArticleObject(); - update(article); - }, 0); -} + function onContentChange(e, timeout=2000) + { + ifNoFurtherChanges(update, timeout); + } -window.addEventListener("beforeunload", function(e) { - if (nonce != 0) { - e.returnValue = "Are you sure?"; - } -}); + function submitArticle() + { + ifNoFurtherChanges(() => { + article.state = ArticleState.SUBMITTED; + update(); + }, + /* timeout: */ 0); + } -window.addEventListener("keydown", function(event) { - if (event.ctrlKey || event.metaKey) - { - if (String.fromCharCode(event.which).toLowerCase() == 's') - { - event.preventDefault(); - onContentChange(0); - } - } -}); \ No newline at end of file + /** Initialize the editor on page load. */ + function initializeEditor() + { + // Kill the noscript message + document.getElementById("preview").innerHTML = "

      Loading...

      "; + document.getElementById("preview-citations").innerHTML = "

      Loading...

      "; + document.getElementById("preview-control").innerHTML = "

      Loading...

      "; + + // Hook up the controls + document.getElementById("button-submit").onclick = submitArticle; + document.getElementById("editor-title").oninput = onContentChange; + document.getElementById("editor-content").oninput = onContentChange; + window.addEventListener("beforeunload", e => + { + if (nonce > 0) + { + e.returnValue = "Are you sure?"; + } + }); + window.addEventListener("keydown", e => + { + if (e.ctrlKey && e.key == 's') + { + e.preventDefault(); + onContentChange(e, 0); + } + }); + + // Get the article status information. + const updateUrl = document.body.dataset.amanuensisUpdateUrl; + fetch(updateUrl).then(response => response.json()).then(updateState); + } + window.onload = initializeEditor; +})(); diff --git a/amanuensis/server/lexicon.jinja b/amanuensis/server/lexicon.jinja index 1c332bc..333f8f4 100644 --- a/amanuensis/server/lexicon.jinja +++ b/amanuensis/server/lexicon.jinja @@ -15,6 +15,9 @@ {% if current_page == "contents" %}class="current-page" {% else %}href="{{ url_for('lexicon.contents', lexicon_name=g.lexicon.name) }}" {% endif %}>Contents{% endblock %} +{% block sb_editor %}Editor{% endblock %} {% block sb_posts %}") +@lexicon_param +@player_required +def open(lexicon_name, article_id): + db: DbContext = g.db + article: Article = db( + select(Article).where(Article.public_id == article_id) + ).scalar_one() + return render_template( + "session.editor.jinja", + lexicon_name=lexicon_name, + article=article, + ) + + +@bp.get("//update") +@lexicon_param +@player_required +def load(lexicon_name, article_id): + db: DbContext = g.db + article: Article = db( + select(Article).where(Article.public_id == article_id) + ).scalar_one() + citations = [ + {'title': 'Citation Title', 'type': 'phantom'} + ] + errors = [ + {'severity': 0, 'message': "OK"}, + {'severity': 1, 'message': "Warning"}, + {'severity': 2, 'message': "Error"}, + ] + return { + 'title': article.title, + 'rendered': article.body, + 'state': article.state.value, + 'ersatz': article.ersatz, + 'citations': citations, + 'errors': errors, + } + + +@bp.post("//update") +def update(lexicon_name, article_id): + db: DbContext = g.db + article: Article = db( + select(Article).where(Article.public_id == article_id) + ).scalar_one() + citations = [ + {'title': 'Citation Title', 'type': 'phantom'} + ] + errors = [ + {'severity': 0, 'message': "OK"}, + {'severity': 1, 'message': "Warning"}, + ] + return { + 'title': article.title, + 'rendered': article.body, + 'state': article.state.value, + 'ersatz': article.ersatz, + 'citations': citations, + 'errors': errors, + } diff --git a/amanuensis/server/lexicon/editor/session.editor.jinja b/amanuensis/server/lexicon/editor/session.editor.jinja index 031deb9..6fdc810 100644 --- a/amanuensis/server/lexicon/editor/session.editor.jinja +++ b/amanuensis/server/lexicon/editor/session.editor.jinja @@ -1,121 +1,64 @@ -{% if character and not article %} -{% set characters = [character] %} -{% endif %} - - - Editor - - - - - + + + Editor + + + + - -
      -
      -
      - {# Thin header bar #} -
      - {# Header always includes backlink to lexicon #} - - {{ g.lexicon.title }} - - {# If article is not finalized, show button to submit and retract #} - {% if article and not article.status.approved %} - - {% endif %} - {# Header always includes character/player info #} - - - {% if character %} - {{ character.name }} / - {% endif %} - {{ current_user.cfg.username }} - - -
      - {# In load mode, `characters` is specified and `article` is #} - {# not, and the main body of the editor column contains a #} - {# list of articles that can be loaded. #} - {% for char in characters %} -
      - {{ char.name }} - -
      - {% endfor %} - {# In edit mode, `article` is specified and `characters` is #} - {# not, and the editor pane contains the article editor. #} - {% if article %} - {#
      - Character literals: - - - - - -
      #} - - - {% endif %} -
      -
      -
      -
      -

      This editor requires Javascript to function.

      -
      -
      -

       

      -
      -
      -

       

      - - - + +
      +
      +
      + {# Thin header bar #} +
      + {# Header always includes backlink to lexicon #} + + {{ g.lexicon.full_title }} + + {# If article is not finalized, show button to submit and retract #} + {# {% if article and not article.status.approved %} #} + {% if article %} + + {% endif %} + {# Header always includes character/player info #} + {{ article.character.name }} +
      + {% if article %} + {#
      + Character literals: + + + + + +
      #} + + + {% endif %} +
      +
      +
      +
      +

      This editor requires Javascript to function.

      +
      +
      +

       

      +
      +
      +

       

      + +
      +
      diff --git a/amanuensis/server/lexicon/lexicon.contents.jinja b/amanuensis/server/lexicon/lexicon.contents.jinja index 1415449..1830cb9 100644 --- a/amanuensis/server/lexicon/lexicon.contents.jinja +++ b/amanuensis/server/lexicon/lexicon.contents.jinja @@ -8,17 +8,9 @@ {{ message }}
      {% endfor %} -{% for index in indexed %} -{{ index }} -{% if indexed[index] %} - -{% endif %} +{% for article in current_lexicon.articles %} +

      {{ article.title }} - {{ article.public_id }}

      +

      {{ article.body }}

      {% endfor %}
      {% endblock %}