Files
JQC_features/templates/admin/topic_form.html
T

225 lines
9.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "admin/base.html" %}
{% block title %}{{ 'Edit topic' if topic else 'New topic' }}{% endblock %}
{% block head_extra %}
<link href="{{ url_for('static', filename='vendor/quill.snow.css') }}" rel="stylesheet">
{% endblock %}
{% macro val(field, default='') -%}
{%- if form is not none -%}{{ form.get(field, default) }}
{%- elif topic is not none -%}{{ topic[field] if topic[field] is not none else default }}
{%- else -%}{{ default }}{%- endif -%}
{%- endmacro %}
{% block content %}
{% set current_section = (form.get('section_id') if form else (topic.section_id if topic else request.args.get('section'))) %}
{% set current_media = (form.get('media_type') if form else (topic.media_type if topic else 'none')) %}
<header class="page-head">
<div>
<h1>{{ 'Edit topic' if topic else 'New topic' }}</h1>
<p class="muted">{{ topic.title if topic else 'Add a new topic to a section.' }}</p>
</div>
<a class="btn btn--ghost" href="{{ url_for('admin.dashboard') }}">← Back</a>
</header>
<form method="post" class="form-card stack"
action="{{ url_for('admin.topic_form', topic_id=topic.id) if topic else url_for('admin.topic_form') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="grid-2">
<label class="field">
<span>Section *</span>
<select name="section_id" required>
{% for s in sections %}
<option value="{{ s.id }}" {{ 'selected' if current_section and s.id|string == current_section|string }}>
§{{ '%02d' % s.num }} — {{ s.title }}
</option>
{% endfor %}
</select>
</label>
<label class="field">
<span>Sort order</span>
<input type="number" name="sort_order" value="{{ val('sort_order', '10') }}">
</label>
</div>
<label class="field">
<span>Title *</span>
<input type="text" name="title" value="{{ val('title') }}" required>
</label>
<label class="field">
<span>Slug <small class="muted">(optional — auto-generated from the title, must be unique)</small></span>
<input type="text" name="slug" value="{{ val('slug') }}" placeholder="auto">
</label>
<div class="field">
<span>Body</span>
<div id="editor-toolbar" class="ql-tools">
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<button class="ql-header" value="2"></button>
<button class="ql-header" value="3"></button>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
<button class="ql-blockquote"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image" title="Insert image"></button>
<button class="ql-clean"></button>
</span>
<span class="ql-formats ql-table-tools">
<button type="button" class="ql-table-op" data-op="insert" title="Insert table"></button>
<button type="button" class="ql-table-op" data-op="row" title="Add row below">+Row</button>
<button type="button" class="ql-table-op" data-op="col" title="Add column right">+Col</button>
<button type="button" class="ql-table-op" data-op="delrow" title="Delete row">Row</button>
<button type="button" class="ql-table-op" data-op="delcol" title="Delete column">Col</button>
<button type="button" class="ql-table-op" data-op="deltable" title="Delete table">✕Table</button>
</span>
</div>
<div id="editor"></div>
<!-- Real field. Quill enhances it; if Quill fails to load, this stays a
usable raw-HTML textarea so a save never wipes the body. -->
<textarea name="body_html" id="body_src" rows="7">{{ val('body_html') }}</textarea>
</div>
{% set pub = (form.get('is_published') == '1') if form is not none else (topic.is_published if topic else True) %}
<label class="field field--check">
<input type="checkbox" name="is_published" value="1" {{ 'checked' if pub }}>
<span>Published <small class="muted">(uncheck to keep as a draft — hidden from the public site)</small></span>
</label>
<fieldset class="fieldset">
<legend>Media</legend>
<div class="grid-2">
<label class="field">
<span>Type</span>
<select name="media_type">
{% for mt in media_types %}
<option value="{{ mt }}" {{ 'selected' if mt == current_media }}>{{ mt }}</option>
{% endfor %}
</select>
</label>
<label class="field">
<span>Caption</span>
<input type="text" name="media_caption" value="{{ val('media_caption') }}">
</label>
</div>
<label class="field">
<span>Media URL</span>
<input type="text" name="media_url" value="{{ val('media_url') }}"
placeholder="/static/img/photo.jpg · /static/vid/demo.mp4 · https://www.youtube.com/embed/ID">
</label>
<p class="hint muted">image → &lt;img&gt; · video → &lt;video&gt; · embed → &lt;iframe&gt; (use a YouTube <code>/embed/</code> URL)</p>
</fieldset>
<fieldset class="fieldset">
<legend>Link button (optional)</legend>
<div class="grid-2">
<label class="field">
<span>Link URL</span>
<input type="text" name="link_url" value="{{ val('link_url') }}" placeholder="https://…">
</label>
<label class="field">
<span>Link label</span>
<input type="text" name="link_label" value="{{ val('link_label') }}" placeholder="Learn more">
</label>
</div>
</fieldset>
<div class="form-actions">
<a class="btn btn--ghost" href="{{ url_for('admin.dashboard') }}">Cancel</a>
<button class="btn btn--primary" type="submit">Save topic</button>
</div>
</form>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='vendor/quill.min.js') }}"></script>
<script>
(function () {
if (typeof Quill === 'undefined') return; // textarea stays usable
var ta = document.getElementById('body_src');
var toolbarEl = document.getElementById('editor-toolbar');
var editorEl = document.getElementById('editor');
var quill = new Quill(editorEl, {
theme: 'snow',
// `table: true` enables Quill 2's built-in table module.
modules: { toolbar: toolbarEl, table: true }
});
if (ta.value.trim()) quill.clipboard.dangerouslyPasteHTML(ta.value);
var csrf = document.querySelector('meta[name="csrf-token"]').content;
var tableModule = quill.getModule('table');
// --- Image button: upload the file, then embed the returned URL. This
// keeps the DB small (no base64) and the image is served as a static file.
quill.getModule('toolbar').addHandler('image', function () {
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg,image/gif,image/webp';
input.onchange = function () {
var file = input.files && input.files[0];
if (!file) return;
var fd = new FormData();
fd.append('file', file);
fetch('{{ url_for("admin.upload") }}', {
method: 'POST', headers: { 'X-CSRFToken': csrf }, body: fd
}).then(function (r) {
return r.json().then(function (j) { return { ok: r.ok, body: j }; });
}).then(function (res) {
if (!res.ok) { alert(res.body.error || 'Upload failed.'); return; }
var range = quill.getSelection(true);
quill.insertEmbed(range.index, 'image', res.body.url, 'user');
quill.setSelection(range.index + 1, 'silent');
}).catch(function () { alert('Upload failed.'); });
};
input.click();
});
// --- Table controls. Row/column ops act on the cell holding the cursor.
toolbarEl.querySelectorAll('.ql-table-op').forEach(function (btn) {
// Preventing the mousedown default keeps the editor's selection so the
// table module knows which cell the cursor is in.
btn.addEventListener('mousedown', function (e) { e.preventDefault(); });
btn.addEventListener('click', function () {
var op = btn.getAttribute('data-op');
try {
if (op === 'insert') {
if (!quill.getSelection()) quill.setSelection(quill.getLength() - 1, 0);
tableModule.insertTable(3, 3);
}
else if (op === 'row') tableModule.insertRowBelow();
else if (op === 'col') tableModule.insertColumnRight();
else if (op === 'delrow') tableModule.deleteRow();
else if (op === 'delcol') tableModule.deleteColumn();
else if (op === 'deltable') tableModule.deleteTable();
} catch (err) {
alert('Place the cursor inside a table cell first.');
}
});
});
// Switch the UI from textarea to Quill only once it's ready.
document.body.classList.add('quill-on');
ta.form.addEventListener('submit', function () {
// getText() is empty for an image/table-only body, so also check for
// embeds before treating the editor as blank (which would wipe the save).
var hasText = quill.getText().trim().length > 0;
var hasEmbed = quill.root.querySelector('img, table');
ta.value = (hasText || hasEmbed) ? quill.root.innerHTML : '';
});
})();
</script>
{% endblock %}