Starting work on new dynamic widgets using HTMX and Alpine.
This commit is contained in:
parent
109176573c
commit
c8190be21c
5 changed files with 174 additions and 3 deletions
|
|
@ -36,6 +36,10 @@ def coffee():
|
|||
matrix, clicked = generate_solvable_matrix(level)
|
||||
return render_template("coffee.html", matrix=matrix, level=level, clicked=clicked)
|
||||
|
||||
@main.route("/playground")
|
||||
def playground():
|
||||
return render_template("playground.html")
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
worklog_query = eager_load_worklog_relationships(
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container border border-black rounded bg-success-subtle">
|
||||
<div class="container border border-black bg-success-subtle">
|
||||
{% for x in range(level + 3) %}
|
||||
<div class="row" style="min-height: {{ 80 / (level + 3) }}vh;">
|
||||
{% for y in range(level + 3) %}
|
||||
<div class="col m-2 p-0 align-items-center d-flex justify-content-center border border-{% if (x, y) in clicked and false %}danger border-2{% else %}black{% endif %} rounded light shadow" id="light-{{ x }}-{{ y }}">
|
||||
<div class="col m-0 p-0 align-items-center d-flex justify-content-center outline outline-{% if (x, y) in clicked and false %}danger border-2{% else %}black{% endif %} light" id="light-{{ x }}-{{ y }}">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input d-none" id="checkbox-{{ x }}-{{ y }}"{% if matrix[x][y] %} checked{% endif %}>
|
||||
</div>
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
// Check if all checkboxes are checked
|
||||
const allChecked = Array.from(document.querySelectorAll('.form-check-input')).every(cb => cb.checked);
|
||||
const allUnchecked = Array.from(document.querySelectorAll('.form-check-input')).every(cb => !cb.checked);
|
||||
if (allChecked && !window.__alreadyNavigated && {{ level }} < 28) {
|
||||
if (allChecked && !window.__alreadyNavigated && {{ level }} < 51) {
|
||||
window.__alreadyNavigated = true;
|
||||
location.href = "{{ url_for('main.coffee', level=level + 1) }}";
|
||||
} else if (allUnchecked && !window.__alreadyNavigated && {{ level }} > -2) {
|
||||
|
|
|
|||
|
|
@ -42,3 +42,150 @@
|
|||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dynamic_combobox(
|
||||
id, options, label = none, placeholder = none, data_attributes = none,
|
||||
create_url = none, edit_url = none, delete_url = none, refresh_url = none
|
||||
) %}
|
||||
{% if label %}
|
||||
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||
{% endif %}
|
||||
<div id="{{ id }}-container"
|
||||
x-data="ComboBox({
|
||||
id: '{{ id }}',
|
||||
createUrl: {{ create_url|tojson if create_url else 'null' }},
|
||||
editUrl: {{ edit_url|tojson if edit_url else 'null' }},
|
||||
deleteUrl: {{ dekete_url|tojson if delete_url else 'null' }},
|
||||
refreshUrl: {{ refresh_url|tojson if refresh_url else 'null' }},
|
||||
})"
|
||||
hx-preserve
|
||||
class="combo-box-widget">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
id="{{ id }}-input"
|
||||
x-model.trim="query"
|
||||
@keydown.enter.prevent="submitAddOrEdit()"
|
||||
@keydown.escape="cancelEdit()"
|
||||
class="form-control rounded-bottom-0">
|
||||
|
||||
<button id="{{ id }}-add"
|
||||
:disabled="!query"
|
||||
@click="submitAddOrEdit()"
|
||||
class="btn btn-primary rounded-bottom-0">
|
||||
<i class="bi icon-state" :class="isEditing ? 'bi-pencil' : 'bi-plus-lg'"></i>
|
||||
</button>
|
||||
|
||||
<button id="{{ id }}-remove"
|
||||
:disabled="!hasSelection"
|
||||
@click="removeSelected()"
|
||||
class="btn btn-danger rounded-bottom-0">
|
||||
{{ icons.render_icon('dash-lg', 16) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select id="{{ id }}-list" multiple
|
||||
x-ref="list"
|
||||
@change="onListChange"
|
||||
class="form-select border-top-0 rounded-top-0"
|
||||
name="{{ id }}" size="10">
|
||||
{% for option in options %}
|
||||
<option value="{{ option.id }}">{{ option.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if refresh_url %}
|
||||
<div id="{{ id }}-htmx-refresh" class="d-none"
|
||||
hx-get="{{ refresh_url }}"
|
||||
hx-trigger="combobox:refresh from:#{{ id }}-container"
|
||||
hx-target="#{{ id }}-list"
|
||||
hx-swap="innerHTML"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function ComboBox(cfg) {
|
||||
return {
|
||||
id: cfg.id,
|
||||
createUrl: cfg.createUrl,
|
||||
editUrl: cfg.editUrl,
|
||||
deleteUrl: cfg.deleteUrl,
|
||||
refreshUrl: cfg.refreshUrl,
|
||||
|
||||
query: '',
|
||||
isEditing: false,
|
||||
editingOption: null,
|
||||
|
||||
get hasSelection() { return this.$refs.list?.selectedOptions.length > 0 },
|
||||
|
||||
onListChange() {
|
||||
const sel = this.$refs.list.selectedOptions;
|
||||
if (sel.length === 1) {
|
||||
this.query = sel[0].textContent.trim();
|
||||
this.isEditing = true;
|
||||
this.editingOption = sel[0];
|
||||
} else {
|
||||
this.cancelEdit();
|
||||
}
|
||||
},
|
||||
|
||||
cancelEdit() { this.isEditing = false; this.editingOption = null; },
|
||||
|
||||
async submitAddOrEdit() {
|
||||
const name = (this.query || '').trim();
|
||||
if (!name) return;
|
||||
|
||||
if (this.isEditing && this.editingOption && this.editUrl) {
|
||||
// EDIT
|
||||
const id = this.editingOption.value;
|
||||
await this._post(this.editUrl, { id, name });
|
||||
this.editingOption.textContent = name;
|
||||
} else if (this.createUrl) {
|
||||
// CREATE
|
||||
const data = await this._post(this.createUrl, { name });
|
||||
const id = (data && data.id) ? data.id : ('temp-' + Math.random().toString(36).slice(2));
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id; opt.textContent = name;
|
||||
this.$refs.list.appendChild(opt);
|
||||
this._sortOptions();
|
||||
}
|
||||
|
||||
this.query = '';
|
||||
this.cancelEdit();
|
||||
this._maybeRefresh();
|
||||
},
|
||||
|
||||
async removeSelected() {
|
||||
const opts = Array.from(this.$refs.list.selectedOptions);
|
||||
if (!opts.length) return;
|
||||
|
||||
if (!confirm(`Delete ${opts.length} item(s)?`)) return;
|
||||
|
||||
const ids = opts.map(o => o.remove());
|
||||
|
||||
this.query = '';
|
||||
this.cancelEdit();
|
||||
this._maybeRefresh();
|
||||
},
|
||||
|
||||
_sortOptions() {
|
||||
const list = this.$refs.list;
|
||||
const sorted = Array.from(list.options).sort((a,b)=>a.text.localeCompare(b.text));
|
||||
list.innerHTML = ''; sorted.forEach(o => list.appendChild(o));
|
||||
},
|
||||
|
||||
_maybeRefresh() { if (this.refreshUrl) this.$dispatch('combobox:refresh'); },
|
||||
|
||||
async _post(url, payload) {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const msg = await res.text().catch(()=> 'Error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@
|
|||
integrity="sha384-zqgMe4cx+N3TuuqXt4kWWDluM5g1CiRwqWBm3vpvY0GcDoXTwU8d17inavaLy3p3"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/combobox.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/csv.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/dropdown.js') }}"></script>
|
||||
|
|
|
|||
18
inventory/templates/playground.html
Normal file
18
inventory/templates/playground.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{% extends 'layout.html' %}
|
||||
|
||||
{% block content %}
|
||||
{{ combos.dynamic_combobox(
|
||||
'play',
|
||||
[
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Beans'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Toast'
|
||||
},
|
||||
],
|
||||
'Breakfast!'
|
||||
) }}
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue