Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ All templates can be overridden by setting these config values to your own templ
| `WIKI_NOT_FOUND_TEMPLATE` | `'wiki/404.html'` |
| `WIKI_FORBIDDEN_TEMPLATE` | `'wiki/403.html'` |
| `WIKI_ICON_TEMPLATE` | `'wiki/icons/bootstrap.html'` |
| `WIKI_TOAST_TEMPLATE` | `'wiki/toast.html'` |

### Front-end assets

Expand All @@ -151,6 +152,56 @@ instead of a CDN, which is what makes the wiki work without internet access:
app.config["BOOTSTRAP_SERVE_LOCAL"] = True
```

#### Page layout

Every page — article, editor, file listing, search results, error — is laid out
by the same three class names, which an application overriding one of the page
templates has to keep:

| Class | Role |
|-------|------|
| `wiki-page` | the grid: one column on a narrow screen, article plus outline from 768px up |
| `wiki-toc` | the table of contents, first in the source, placed on the right on a wide screen |
| `wiki-content` | the main column: article, editor form or listing, with its header |

The TOC precedes the article in the source so a narrow screen shows it
first, and the grid moves it to the right-hand column on a wide one. It is
pinned with `position: sticky` and scrolls on its own once it outgrows the
viewport. A page without headings renders no `wiki-toc` at all, and the grid
falls back to a single centered column.

#### Feedback

Feedback shares one channel: the messages flashed by the server and the ones the
browser raises on its own are all toasts, stacked in a `wiki-toasts` container
fixed to the top right. Their markup comes from the `toast` macro of
`WIKI_TOAST_TEMPLATE`, and the toasts the wiki raises by itself live in
`wiki/toasts.html`.

An application overriding `WIKI_BASE_TEMPLATE` renders its own stack. It
includes `wiki/toasts.html` in it, so it never has to know the ids `wiki.js`
reveals nor repeat their messages, and it renders the flashed messages itself if
nothing else in the application already does:

```jinja
{% raw %}<div class="wiki-toasts">
{% for category, message in get_flashed_messages(with_categories=True) %}
{{ toast(message, category, autoshow=True) }}
{% endfor %}
{%- include "wiki/toasts.html" %}
</div>{% endraw %}
```

To render those toasts as the rest of the application does, point
`WIKI_TOAST_TEMPLATE` at a template of its own supplying a macro with the same
signature — `toast(message, category='message', id=None, autoshow=False)`, where
`id` marks a toast kept hidden until a script reveals it, and `autoshow` one
`wiki.js` reveals as soon as the page is ready:

```python
app.config["WIKI_TOAST_TEMPLATE"] = "myapp/macros/toast.html"
```

#### Icons

Templates never name a glyph directly. They ask for an *intent* — `search`,
Expand Down
2 changes: 2 additions & 0 deletions flask_wiki/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
WIKI_PAGE_TEMPLATE = "wiki/page.html"
# Supplies the icon() macro; swap for wiki/icons/fontawesome.html or your own
WIKI_ICON_TEMPLATE = "wiki/icons/bootstrap.html"
# Supplies the toast() macro; swap for the one of the embedding application
WIKI_TOAST_TEMPLATE = "wiki/toast.html"


# MISCS
Expand Down
48 changes: 43 additions & 5 deletions flask_wiki/static/css/wiki.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,50 @@ h1 {
color: #6c757d;
}

.wiki-page .toc ul {
/* Article and table of contents: one column on a small screen, two on a wide
one. */
.wiki-page {
display: grid;
Comment thread
PascalRepond marked this conversation as resolved.
grid-template-columns: minmax(0, 1fr);
gap: 1rem 2.5rem;
max-width: 75rem;
margin-inline: auto;
}

/* Anchors land a little under the top of the viewport, not flush against it. */
.content h1, .content h2, .content h3,
.content h4, .content h5, .content h6 {
scroll-margin-top: 1rem;
}

/* The TOC comes first in the source, so it comes first on a narrow screen,
where it must not push the article out of sight. */
.wiki-toc {
max-height: 30vh;
overflow-y: auto;
}

.wiki-toc ul {
padding-left: 1rem;
}

.wiki-page .toc {
z-index: 1000;
@media (min-width: 768px) {
/* The second column exists only on a page that has an outline. */
.wiki-page:has(.wiki-toc) {
grid-template-columns: minmax(0, 1fr) 15rem;
}

.wiki-content {
grid-area: 1 / 1;
}

.wiki-toc {
grid-area: 1 / 2;
align-self: start; /* a stretched item has no room to stick */
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
}
}

.wiki-editor textarea {
Expand All @@ -61,8 +99,8 @@ h1 {

.wiki-toasts {
position: fixed;
top: 50px;
left: 15px;
top: 1rem;
right: 1rem;
z-index: 1090;
}

Expand Down
58 changes: 28 additions & 30 deletions flask_wiki/static/js/wiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,66 @@
// SPDX-License-Identifier: BSD-3-Clause

$(document).ready(function () {
// reveal the messages flashed by the server
$('.toast[data-autoshow]').toast('show');

// ask the backend for the preview and render it
$('.wiki-editor #preview-tab').on('click', function () {
var url = $(this).data('preview-url');
var $form = $('form');
var $inputs = $form.find('input, textarea, button');
var $pre = $('#preview');
var bodycontent = 'title: preview\n\n' + $form.find('textarea').val();
$('#preview-tab').on('click', function () {
const $tab = $(this);
const $form = $tab.closest('form');
const $inputs = $form.find('input, textarea, button');
const $pre = $('#preview');
const bodycontent = 'title: preview\n\n' + $form.find('textarea').val();
$inputs.prop('disabled', true);
$pre
.removeClass('alert')
.removeClass('alert-error')
.html('Loading...');
$pre.removeClass('alert alert-danger').text($tab.data('loading-text'));
$.ajax({
url: url,
url: $tab.data('preview-url'),
type: 'POST',
data: { body: bodycontent },
success: function (msg) {
$pre.html(msg);
},
error: function (e) {
console.log('error: ', e);
$pre.addClass('alert').addClass('alert-error');
$pre.html('There was a problem with the preview.');
$pre.addClass('alert alert-danger').text($tab.data('error-text'));
},
complete: function () {
$inputs.prop('disabled', false);
}
});
});

// Add the following code if you want the name of the file appear on select
// selecting a file uploads it, no further click needed
$('.wiki-files .custom-file-input').on('change', function () {
var form = $('form');
form.submit();
$(this).closest('form').submit();
});

// copy the markdown code in the clip board
// copy the markdown code of a file to the clipboard
$('.wiki-files .copy-md-code').on('click', function () {
// function copy(name, link) {
var name = $(this).data('name');
var link = $(this).data('link');;
const name = $(this).data('name');
const link = $(this).data('link');
copyToClipboard(`![${name}](${link} "${name}")`);
});

// copy the url code in the clip board
// copy the link of a page to the clipboard
$('.copy-file-code').on('click', function () {
// function copy(name, link) {
var name = $(this).data('name');
var link = $(this).data('link');
const name = $(this).data('name');
const link = $(this).data('link');
copyToClipboard(`[${name}](${link})`);
});

// Change the target modal when an element is clicked
$('.delete-file').on('click', function (e) {
var file = e.currentTarget.id;
var confirm = document.getElementById("confirm");
var url = confirm.href.concat(file);
document.getElementById("confirm").href=url;
// point the confirmation button at the file the reader asked to delete
$('.delete-file').on('click', function () {
$('#confirm').attr('href', $(this).data('delete-url'));
});
});

function copyToClipboard(text) {
// the clipboard API is missing outside a secure context
if (!navigator.clipboard) {
$('#copy-error').toast('show');
return;
}
navigator.clipboard.writeText(text).then(
() => {
$('#copy-success').toast('show');
Expand Down
4 changes: 2 additions & 2 deletions flask_wiki/templates/wiki/403.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
{% endblock %}

{% block content %}
<main class="row mt-4 wiki-page">
<div class="col-md-7 offset-md-2">
<main class="wiki-page mt-4">
<div class="wiki-content">
<h1>{{ _('Forbidden') }}</h1>
<p>{{ _('You are not allowed to see this page.') }}</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions flask_wiki/templates/wiki/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
{% endblock %}

{% block content %}
<main class="row mt-4 wiki-page">
<div class="col-md-7 offset-md-2">
<main class="wiki-page mt-4">
<div class="wiki-content">
<h1>{{ _('Page does not exist.') }}</h1>
<p>{{ _('You did not create any content for this page yet.') }}</p>
<p>
Expand Down
33 changes: 10 additions & 23 deletions flask_wiki/templates/wiki/base.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{# SPDX-FileCopyrightText: Fondation RERO+ #}
{# SPDX-License-Identifier: BSD-3-Clause #}

{% from 'bootstrap4/utils.html' import render_messages %}
{% from config.get('WIKI_TOAST_TEMPLATE') import toast %}

<!DOCTYPE html>
<html lang="en">
Expand All @@ -27,29 +27,16 @@
<a class="navbar-brand" href="#">{{ _('Flask-Wiki') }}</a>
</nav>

<div class="container-fluid py-4">
{{ render_messages(dismissible=True, dismiss_animate=True) }}
<div class="wiki-toasts">
{% for category, message in get_flashed_messages(with_categories=True) %}
{{ toast(message, category, autoshow=True) }}
{% endfor %}
{%- include "wiki/toasts.html" %}
</div>

<div class="container-fluid pb-4">
{% block navigation %} {%- include "wiki/navigation.html" %} {% endblock %}
{% block content %}
<div class="wiki-toasts">
<div data-delay="2500" id="copy-success" class="toast hide">
<div class="toast-header text-primary font-weight-bold">
Markdown
</div>
<div class="toast-body">
{{ _('Link copied to your clipboard') }}
</div>
</div>
<div data-delay="2500" id="copy-error" class="toast hide">
<div class="toast-header text-primary font-weight-bold">
Markdown
</div>
<div class="toast-body">
{{ _('Failed to copy link to your clipboard') }}
</div>
</div>
</div>
{% endblock %}
{% block content %}{% endblock %}
</div>

<footer class="container py-5">
Expand Down
10 changes: 6 additions & 4 deletions flask_wiki/templates/wiki/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
{% set active_page = "editor" %}

{% block content %}
<main class="wiki-editor col-md-7 offset-md-2 mt-4">
<form class="editor-content" method="post">
<main class="wiki-page wiki-editor mt-4">
<form class="editor-content wiki-content" method="post">
<ul class="nav nav-tabs mb-2" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" id="edit-tab" href="#edit" role="tab" aria-controls="home"
aria-selected="true">{{ _('Edit') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" id="preview-tab" href="#preview" role="tab"
data-preview-url="{{ url_for('wiki.preview') }}" aria-controls="profile"
aria-selected="false">{{ _('Preview') }}</a>
data-preview-url="{{ url_for('wiki.preview') }}"
data-loading-text="{{ _('Loading...') }}"
data-error-text="{{ _('There was a problem with the preview.') }}"
aria-controls="profile" aria-selected="false">{{ _('Preview') }}</a>
</li>
<li class="nav-item d-flex align-items-center ml-3">
<span class="wiki-editor-language text-muted small">
Expand Down
13 changes: 6 additions & 7 deletions flask_wiki/templates/wiki/files.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
{% set active_page = "files" %}

{% block content %}
{{ super() }}
<main class="row mt-4 wiki-files">
<article class="offset-md-1 col-md-10">
<main class="wiki-page wiki-files mt-4">
<article class="wiki-content">
<header class="d-flex flex-column flex-lg-row justify-content-lg-between">
<h1>{{ _('List of Uploaded Files') }}</h1>
{% if can_edit_wiki %}
<form class="" method="post" enctype="multipart/form-data">
<div class="custom-file">
<button class="btn btn-outline-primary btn-sm">{{ icon('upload') }} {{ _('Upload a file') }}</button>
<button type="button" class="btn btn-outline-primary btn-sm">{{ icon('upload') }} {{ _('Upload a file') }}</button>
<input class="custom-file-input" type="file" name="file" accept="image/*" />
</div>
</form>
Expand All @@ -33,7 +32,8 @@ <h1>{{ _('List of Uploaded Files') }}</h1>
aria-label="{{ _('Copy Markdown code') }}">
{{ icon('copy') }}
</button>
<button data-toggle="modal" data-target="#deleteModal" id="{{file}}" class="delete-file btn btn-sm btn-outline-danger"
<button data-toggle="modal" data-target="#deleteModal" class="delete-file btn btn-sm btn-outline-danger"
data-delete-url="{{ url_for('wiki.delete_file', filename=file) }}"
aria-label="{{ _('Delete file') }}">
{{ icon('delete') }}
</button>
Expand All @@ -59,8 +59,7 @@ <h5 class="modal-title" id="deleteModalCenterTitle">{{ _('Delete file') }}</h5>
{{ _('Are you sure you want to delete this file? Be warned that links from existing pages will be broken.') }}
</div>
<div class="modal-footer">
<a id="confirm" href="{{ url_for('wiki.delete_file', filename=file) }}" type="button"
class="btn btn-danger">{{ _('Delete file') }}</a>
<a id="confirm" href="#" class="btn btn-danger">{{ _('Delete file') }}</a>
<button type="button" class="btn btn-outline-primary" data-dismiss="modal">{{ _('Back') }}</button>
</div>
</div>
Expand Down
Loading