new sortable list implementation

This commit is contained in:
2025-12-19 19:01:01 +03:00
parent 98bf430604
commit 46704df7d9
16 changed files with 581 additions and 395 deletions

View File

@@ -29,8 +29,10 @@ def sort_topics():
@bp.post("/sort-topics")
def sort_topics_post():
topics_list = request.form.getlist('topics[]')
print(topics_list)
with db.transaction():
for topic_id, new_order in request.form.items():
for new_order, topic_id in enumerate(topics_list):
db.execute("UPDATE topics SET sort_order = ? WHERE id = ?", new_order, topic_id)
return redirect(url_for(".sort_topics"))

View File

@@ -59,3 +59,9 @@
<path d="M5 11C9.41828 11 13 14.5817 13 19M5 5C12.732 5 19 11.268 19 19M7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% endmacro %}
{% macro icn_drag(width=24) %}
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 10H19M14 19L12 21L10 19M14 5L12 3L10 5M5 14H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% endmacro %}

View File

@@ -1,4 +1,8 @@
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_rss %}
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_rss, icn_drag %}
{%- macro dict_to_attr(attrs) -%}
{%- for key, value in attrs.items() if value is not none -%}{{' '}}{{key}}="{{value}}"{%- endfor -%}
{%- endmacro -%}
{% macro pager(current_page, page_count) %}
{% set left_start = [1, current_page - 5] | max %}
{% set right_end = [page_count, current_page + 5] | min %}
@@ -331,7 +335,9 @@
{% else %}
{% set selected_href = defaults[0].file_path %}
{% endif %}
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change" data-receive="deleteBadge">
<li class="sortable-item" data-sortable-list-key="" data-receive="deleteBadge"> {# breaking convention on purpose since this one is special #}
<span class="dragger" draggable="true">{{ icn_drag(24) }}</span>
<bitty-7-0 class="fg" data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorBadge" data-listeners="click input submit change">
<div class="settings-badge-container">
<div class="settings-badge-select">
<select data-send="badgeUpdatePreview badgeToggleFilePicker" name="badge_choice[]" required>
@@ -358,6 +364,7 @@
<button data-send="deleteBadge" type="button" class="critical" title="Delete">X</button>
</div>
</bitty-7-0>
</li>
{% endmacro %}
{% macro rss_html_content(html) %}
@@ -367,3 +374,20 @@
{% macro rss_button(feed) %}
<a class="linkbutton contain-svg inline icon rss-button" href="{{feed}}" title="it&#39;s actually atom, don&#39;t tell anyone &#59;&#41;">{{ icn_rss(20) }} Subscribe via RSS</a>
{% endmacro %}
{% macro sortable_list(attr=none) %}
<ol class="sortable-list" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
{% if caller %}
{{ caller() }}
{% endif %}
</ol>
{% endmacro %}
{% macro sortable_list_item(key, immovable=false, attr=none) %}
<li class="sortable-item{{' immovable' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
<span class="dragger" draggable="{{ 'true' if not immovable else 'false'}}">{{ icn_drag(24) }}</span>
<div class="sortable-item-inner">
{{ caller() }}
</div>
</li>
{% endmacro %}

View File

@@ -1,18 +1,20 @@
{% extends "base.html" %}
{% from 'common/macros.html' import sortable_list, sortable_list_item %}
{% block content %}
<div class="darkbg">
<h1>Change topics order</h1>
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
<form method="post" id=topics-container>
{% for topic in topics %}
<div draggable="true" class="draggable-topic" ondragover="dragOver(event)" ondragstart="dragStart(event)" ondragend="dragEnd()">
<div class="thread-title">{{ topic['name'] }}</div>
<div>{{ topic.description }}</div>
<input type="hidden" name="{{ topic['id'] }}" value="{{ topic['sort-order'] }}" class="topic-input">
</div>
{% endfor %}
<p>Drag topic titles to reoder them. Press "Save order" when done. The topics will appear to users in the order set here.</p>
<form method="post">
<input type=submit value="Save order">
{% call() sortable_list() %}
{% for topic in topics %}
{% call() sortable_list_item(key="topics") %}
<div class="thread-title">{{ topic.name }}</div>
<div>{{ topic.description }}</div>
<input type="hidden" name="topics[]" value="{{ topic.id }}" class="topic-input">
{% endcall %}
{% endfor %}
{% endcall %}
</form>
</div>
<script src="{{ "/static/js/sort-topics.js" | cachebust }}"></script>
{% endblock %}

View File

@@ -1,35 +1,41 @@
{% extends "base.html" %}
{% from 'common/macros.html' import sortable_list, sortable_list_item %}
{% block title %}managing bookmark collections{% endblock %}
{% block content %}
<div class="darkbg">
<h1>Manage bookmark collections</h1>
<p>Drag collections to reoder them. You cannot move or remove the default collection, but you can rename it.</p>
<div>
<button type="button" id="add-collection-button">Add new collection</button>
<div id="collections-container">
{% for collection in collections | sort(attribute='sort_order') %}
<div class="draggable-collection {{ "default" if collection.is_default else ""}}"
{% if not collection.is_default %}
draggable="true"
ondragover="dragOver(event)"
ondragstart="dragStart(event)"
ondragend="dragEnd()"
{% else %}
id="default-collection"
{% endif %}
data-collection-id="{{ collection.id }}">
<input type="text" class="collection-name" value="{{ collection.name }}" placeholder="Collection name" required autocomplete="off" maxlength="60"><br>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} CollectionsEditor">
<button type="button" data-send="addCollection">Add new collection</button>
{% set sorted_collections = collections | sort(attribute='sort_order') %}
{% macro collection_inner(collection) %}
<input type="text" class="collection-name" value="{{collection.name}}" placeholder="Collection name" required autocomplete="off" maxlength="60">
<div>{{ collection.get_threads_count() }} {{ "thread" | pluralize(num=collection.get_threads_count()) }}, {{ collection.get_posts_count() }} {{ "post" | pluralize(num=collection.get_posts_count()) }}</div>
{% if collection.is_default %}
<i>Default collection</i>
{% else %}
<button type="button" class="delete-button critical">Delete</button>
<button type="button" class="delete-button critical" data-send="deleteCollection">Delete</button>
{% endif %}
</div>
{% endmacro %}
{% call() sortable_list(attr={'data-receive': 'addCollection' }) %}
{% call() sortable_list_item(key='collections', immovable=true, attr={'data-collection-id': sorted_collections[0].id, 'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
{{ collection_inner(sorted_collections[0]) }}
{% endcall %}
{% for collection in sorted_collections[1:] %}
{% call() sortable_list_item(key='collections', attr={'data-collection-id': collection.id, 'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
{{ collection_inner(collection) }}
{% endcall %}
{% endfor %}
{% endcall %}
<button data-use="saveCollections" type="button" id="save-button" data-submit-href="{{ url_for('api.manage_bookmark_collections', user_id=active_user.id) }}">Save</button>
</div>
<button type="button" id="save-button" data-submit-href="{{ url_for('api.manage_bookmark_collections', user_id=active_user.id) }}">Save</button>
</div>
</div>
<script src="{{ "/static/js/manage-bookmark-collections.js" | cachebust }}"></script>
<template id="new-collection-template">
{% call() sortable_list_item(key='collections', attr={'data-receive': 'deleteCollection getCollectionData testValidity'}) %}
<input type="text" class="collection-name" value="" placeholder="Collection name" required autocomplete="off" maxlength="60">
<div>0 threads, 0 posts</div>
<button type="button" class="delete-button critical" data-send="deleteCollection">Delete</button>
{% endcall %}
</template>
</bitty-7-0>
{#<script src="{{ "/static/js/manage-bookmark-collections.js" | cachebust }}"></script>#}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import babycode_editor_component, badge_editor_single %}
{% from 'common/macros.html' import babycode_editor_component, badge_editor_single, sortable_list %}
{% extends 'base.html' %}
{% block title %}settings{% endblock %}
{% block content %}
@@ -57,7 +57,7 @@
<legend>Badges</legend>
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' data-receive='addBadge' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
<form data-use="badgeEditorPrepareSubmit" data-init='loadBadgeEditor' method='post' enctype='multipart/form-data' action='{{ url_for('users.save_badges', username=active_user.username) }}'>
<div>Loading badges&hellip;</div>
<div>If badges fail to load, JS may be disabled.</div>
</form>
@@ -72,4 +72,8 @@
<template id='badge-editor-template'>
{{ badge_editor_single(options=uploads) }}
</template>
<template id="badges-list-template">
{{ sortable_list(attr={'data-receive': 'addBadge'}) }}
</template>
{% endblock %}

View File

@@ -867,6 +867,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
background-color: rgb(230.2, 235.4, 223.8);
}
input:not(form input):invalid {
border: 2px dashed red;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
@@ -1069,35 +1073,6 @@ textarea {
background-color: none;
}
.draggable-topic {
cursor: pointer;
user-select: none;
background-color: #c1ceb1;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(217.26, 220.38, 213.42);
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
}
.draggable-topic.dragged {
background-color: rgb(177, 206, 204.5);
}
.draggable-collection {
cursor: pointer;
user-select: none;
background-color: #c1ceb1;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(217.26, 220.38, 213.42);
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
}
.draggable-collection.dragged {
background-color: rgb(177, 206, 204.5);
}
.draggable-collection.default {
background-color: #beb1ce;
}
.editing {
background-color: rgb(217.26, 220.38, 213.42);
}
@@ -1550,3 +1525,54 @@ img.badge-button {
padding-right: 20px;
}
}
ol.sortable-list {
list-style: none;
flex-grow: 1;
margin: 0;
}
ol.sortable-list li {
display: flex;
gap: 10px;
background-color: #c1ceb1;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(217.26, 220.38, 213.42);
border-bottom: 5px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
}
ol.sortable-list li.dragged {
background-color: rgb(177, 206, 204.5);
}
ol.sortable-list li.immovable {
background-color: #beb1ce;
}
ol.sortable-list li.immovable .dragger {
cursor: not-allowed;
}
.dragger {
display: flex;
align-items: center;
background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252);
padding: 5px 10px;
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: 10px;
flex-grow: 1;
flex-direction: column;
}
.sortable-item-inner > * {
flex-grow: 1;
}
.sortable-item-inner.row {
flex-direction: row;
}
.sortable-item-inner:not(.row) > * {
margin-right: auto;
}
.fg {
flex-grow: 1;
}

View File

@@ -867,6 +867,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
background-color: #514151;
}
input:not(form input):invalid {
border: 2px dashed #d53232;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
@@ -1069,35 +1073,6 @@ textarea {
background-color: #503250;
}
.draggable-topic {
cursor: pointer;
user-select: none;
background-color: #9b649b;
padding: 20px;
margin: 15px 0;
border-top: 5px outset #503250;
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
}
.draggable-topic.dragged {
background-color: #3c283c;
}
.draggable-collection {
cursor: pointer;
user-select: none;
background-color: #9b649b;
padding: 20px;
margin: 15px 0;
border-top: 5px outset #503250;
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
}
.draggable-collection.dragged {
background-color: #3c283c;
}
.draggable-collection.default {
background-color: #8a5584;
}
.editing {
background-color: #503250;
}
@@ -1458,7 +1433,7 @@ a.mention:hover, a.mention:visited:hover {
.settings-grid fieldset {
border: 1px solid black;
border-radius: 8px;
background-color: rgb(141.6, 79.65, 141.6);
background-color: #503250;
}
.hfc {
@@ -1479,10 +1454,10 @@ h1 {
margin: 10px 0;
}
.settings-badge-container:has(input:invalid) {
border: 2px dashed red;
border: 2px dashed #d53232;
}
.settings-badge-container input:invalid {
border: 2px dashed red;
border: 2px dashed #d53232;
}
.settings-badge-file-picker {
@@ -1550,6 +1525,58 @@ img.badge-button {
padding-right: 20px;
}
}
ol.sortable-list {
list-style: none;
flex-grow: 1;
margin: 0;
}
ol.sortable-list li {
display: flex;
gap: 10px;
background-color: #9b649b;
padding: 20px;
margin: 15px 0;
border-top: 5px outset #503250;
border-bottom: 5px outset rgb(96.95, 81.55, 96.95);
}
ol.sortable-list li.dragged {
background-color: #3c283c;
}
ol.sortable-list li.immovable {
background-color: #8a5584;
}
ol.sortable-list li.immovable .dragger {
cursor: not-allowed;
}
.dragger {
display: flex;
align-items: center;
background-color: rgb(96.95, 81.55, 96.95);
padding: 5px 10px;
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: 10px;
flex-grow: 1;
flex-direction: column;
}
.sortable-item-inner > * {
flex-grow: 1;
}
.sortable-item-inner.row {
flex-direction: row;
}
.sortable-item-inner:not(.row) > * {
margin-right: auto;
}
.fg {
flex-grow: 1;
}
#topnav {
margin-bottom: 10px;
border: 10px solid rgb(40, 40, 40);

View File

@@ -867,6 +867,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
background-color: rgb(249.8, 201.8, 189);
}
input:not(form input):invalid {
border: 2px dashed #f73030;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
@@ -1069,35 +1073,6 @@ textarea {
background-color: #f27a5a;
}
.draggable-topic {
cursor: pointer;
user-select: none;
background-color: #f27a5a;
padding: 12px;
margin: 8px 0;
border-top: 5px outset rgb(219.84, 191.04, 183.36);
border-bottom: 5px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
}
.draggable-topic.dragged {
background-color: #f27a5a;
}
.draggable-collection {
cursor: pointer;
user-select: none;
background-color: #f27a5a;
padding: 12px;
margin: 8px 0;
border-top: 5px outset rgb(219.84, 191.04, 183.36);
border-bottom: 5px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
}
.draggable-collection.dragged {
background-color: #f27a5a;
}
.draggable-collection.default {
background-color: #b54444;
}
.editing {
background-color: rgb(219.84, 191.04, 183.36);
}
@@ -1479,10 +1454,10 @@ h1 {
margin: 6px 0;
}
.settings-badge-container:has(input:invalid) {
border: 2px dashed red;
border: 2px dashed #f73030;
}
.settings-badge-container input:invalid {
border: 2px dashed red;
border: 2px dashed #f73030;
}
.settings-badge-file-picker {
@@ -1550,6 +1525,58 @@ img.badge-button {
padding-right: 12px;
}
}
ol.sortable-list {
list-style: none;
flex-grow: 1;
margin: 0;
}
ol.sortable-list li {
display: flex;
gap: 6px;
background-color: #f27a5a;
padding: 12px;
margin: 8px 0;
border-top: 5px outset rgb(219.84, 191.04, 183.36);
border-bottom: 5px outset rgb(155.8907865169, 93.2211235955, 76.5092134831);
}
ol.sortable-list li.dragged {
background-color: #f27a5a;
}
ol.sortable-list li.immovable {
background-color: #b54444;
}
ol.sortable-list li.immovable .dragger {
cursor: not-allowed;
}
.dragger {
display: flex;
align-items: center;
background-color: rgb(155.8907865169, 93.2211235955, 76.5092134831);
padding: 3px 6px;
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: 6px;
flex-grow: 1;
flex-direction: column;
}
.sortable-item-inner > * {
flex-grow: 1;
}
.sortable-item-inner.row {
flex-direction: row;
}
.sortable-item-inner:not(.row) > * {
margin-right: auto;
}
.fg {
flex-grow: 1;
}
#topnav {
border-top-left-radius: 16px;
border-top-right-radius: 16px;

View File

@@ -867,6 +867,10 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
background-color: rgb(235.4, 239.8, 248.2);
}
input:not(form input):invalid {
border: 2px dashed red;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
@@ -1069,35 +1073,6 @@ textarea {
background-color: none;
}
.draggable-topic {
cursor: pointer;
user-select: none;
background-color: #ced9ee;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(231.36, 234, 239.04);
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
}
.draggable-topic.dragged {
background-color: #eecee9;
}
.draggable-collection {
cursor: pointer;
user-select: none;
background-color: #ced9ee;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(231.36, 234, 239.04);
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
}
.draggable-collection.dragged {
background-color: #eecee9;
}
.draggable-collection.default {
background-color: #eee3ce;
}
.editing {
background-color: rgb(231.36, 234, 239.04);
}
@@ -1550,3 +1525,54 @@ img.badge-button {
padding-right: 20px;
}
}
ol.sortable-list {
list-style: none;
flex-grow: 1;
margin: 0;
}
ol.sortable-list li {
display: flex;
gap: 10px;
background-color: #ced9ee;
padding: 20px;
margin: 15px 0;
border-top: 5px outset rgb(231.36, 234, 239.04);
border-bottom: 5px outset rgb(136.0836363636, 149.3636363636, 174.7163636364);
}
ol.sortable-list li.dragged {
background-color: #eecee9;
}
ol.sortable-list li.immovable {
background-color: #eee3ce;
}
ol.sortable-list li.immovable .dragger {
cursor: not-allowed;
}
.dragger {
display: flex;
align-items: center;
background-color: rgb(136.0836363636, 149.3636363636, 174.7163636364);
padding: 5px 10px;
cursor: move;
}
.sortable-item-inner {
display: flex;
gap: 10px;
flex-grow: 1;
flex-direction: column;
}
.sortable-item-inner > * {
flex-grow: 1;
}
.sortable-item-inner.row {
flex-direction: row;
}
.sortable-item-inner:not(.row) > * {
margin-right: auto;
}
.fg {
flex-grow: 1;
}

View File

@@ -287,8 +287,7 @@ export class BadgeEditorForm {
return;
}
if (this.#badgeTemplate === undefined){
this.#badgeTemplate = document.getElementById('badge-editor-template').content;
console.log(this.#badgeTemplate);
this.#badgeTemplate = document.getElementById('badge-editor-template').content.firstElementChild.outerHTML;
}
el.replaceChildren();
const addButton = `<button data-disable-if-max="1" data-receive="updateBadgeCount" DISABLE_IF_MAX type="button" data-send="addBadge">Add badge</button>`;
@@ -300,14 +299,18 @@ export class BadgeEditorForm {
['DISABLE_IF_MAX', badgeCount === 10 ? 'disabled' : ''],
];
el.appendChild(this.api.makeHTML(controls, subs));
el.appendChild(badges.value);
const listTemplate = document.getElementById('badges-list-template').content.firstElementChild.outerHTML;
const list = this.api.makeHTML(listTemplate).firstElementChild;
list.appendChild(badges.value);
el.appendChild(list);
}
addBadge(ev, el) {
if (this.#badgeTemplate === undefined) {
return;
}
const badge = this.#badgeTemplate.cloneNode(true);
const badge = this.api.makeHTML(this.#badgeTemplate).firstElementChild;
el.appendChild(badge);
this.api.localTrigger('updateBadgeCount');
}
@@ -341,9 +344,7 @@ export class BadgeEditorForm {
noUploads.forEach(e => {
e.value = null;
})
// console.log(noUploads);
el.submit();
// console.log('would submit now');
}
}
@@ -463,3 +464,80 @@ export class BadgeEditorBadge {
el.setCustomValidity('');
}
}
const getCollectionDataForEl = el => {
const nameInput = el.querySelector(".collection-name");
const collectionId = el.dataset.collectionId;
return {
id: collectionId,
name: nameInput.value,
is_new: !('collectionId' in el.dataset),
};
}
export class CollectionsEditor {
#collectionTemplate = undefined;
#collectionsData = [];
#removedCollections = [];
#valid = true;
addCollection(ev, el) {
if (this.#collectionTemplate === undefined) {
this.#collectionTemplate = document.getElementById('new-collection-template').content;
}
// interesting
const newCollection = this.api.makeHTML(this.#collectionTemplate.firstElementChild.outerHTML);
el.appendChild(newCollection);
}
deleteCollection(ev, el) {
if (!el.contains(ev.sender)) {
return;
}
if ('collectionId' in el.dataset) {
this.#removedCollections.push(el.dataset.collectionId);
}
el.remove();
}
async saveCollections(ev, el) {
this.#valid = true;
this.api.localTrigger('testValidity');
if (!this.#valid) {
return;
}
this.#collectionsData = [];
this.api.localTrigger('getCollectionData');
const data = {
collections: this.#collectionsData,
removed_collections: this.#removedCollections,
};
const res = await this.api.getJSON(el.prop('submitHref'), [], {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (res.error) {
return;
}
window.location.reload();
}
getCollectionData(ev, el) {
this.#collectionsData.push(getCollectionDataForEl(el));
}
testValidity(ev, el) {
const input = el.querySelector('input');
if (!input.validity.valid) {
input.reportValidity();
this.#valid = false;
}
}
}

View File

@@ -1,128 +0,0 @@
let removedCollections = [];
document.getElementById("add-collection-button").addEventListener("click", () => {
const container = document.getElementById("collections-container");
const currentCount = container.querySelectorAll(".draggable-collection").length;
const newId = `new-${Date.now()}`
const collectionHtml = `
<div class="draggable-collection"
data-collection-id="${newId}"
draggable="true"
ondragover="dragOver(event)"
ondragstart="dragStart(event)"
ondragend="dragEnd()">
<input type="text" class="collection-name" value="" required placeholder="Enter collection name" autocomplete="off" maxlength="60"><br>
<div>0 threads, 0 posts</div>
<button type="button" class="delete-button critical">Delete</button>
</div>
`;
container.insertAdjacentHTML('beforeend', collectionHtml);
})
document.addEventListener("click", e => {
if (!e.target.classList.contains("delete-button")) {
return;
}
const collectionDiv = e.target.closest(".draggable-collection");
const collectionId = collectionDiv.dataset.collectionId;
if (!collectionId.startsWith("new-")) {
removedCollections.push(collectionId);
}
collectionDiv.remove();
})
document.getElementById("save-button").addEventListener("click", async () => {
const collections = [];
const collectionDivs = document.querySelectorAll(".draggable-collection");
let isValid = true;
collectionDivs.forEach((collection, index) => {
const collectionId = collection.dataset.collectionId;
const nameInput = collection.querySelector(".collection-name");
if (!nameInput.reportValidity()) {
isValid = false;
return;
}
collections.push({
id: collectionId,
name: nameInput.value,
is_new: collectionId.startsWith("new-"),
});
})
if (!isValid) {
return;
}
const data = {
collections: collections,
removed_collections: removedCollections,
};
try {
const saveHref = document.getElementById('save-button').dataset.submitHref;
const response = await fetch(saveHref, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (response.ok) {
window.location.reload();
} else {
console.error("Error saving collections");
}
} catch (error) {
console.error("Error saving collections: ", error);
}
})
// drag logic
// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap
let selected = null;
const container = document.getElementById("collections-container");
function isBefore(el1, el2) {
let cur;
if (el2.parentNode === el1.parentNode) {
for (cur = el1.previousSibling; cur; cur = cur.previousSibling) {
if (cur === el2) return true;
}
}
return false;
}
function dragOver(e) {
let target = e.target.closest(".draggable-collection")
if (!target || target === selected) {
return;
}
if (isBefore(selected, target)) {
container.insertBefore(selected, target)
} else {
container.insertBefore(selected, target.nextSibling)
}
}
function dragEnd() {
if (!selected) return;
selected.classList.remove("dragged")
selected = null;
}
function dragStart(e) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', "")
selected = e.target
selected.classList.add("dragged")
}

View File

@@ -1,45 +0,0 @@
// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap
let selected = null;
let container = document.getElementById("topics-container")
function isBefore(el1, el2) {
let cur
if (el2.parentNode === el1.parentNode) {
for (cur = el1.previousSibling; cur; cur = cur.previousSibling) {
if (cur === el2) return true
}
}
return false;
}
function dragOver(e) {
let target = e.target.closest(".draggable-topic")
if (!target || target === selected) {
return;
}
if (isBefore(selected, target)) {
container.insertBefore(selected, target)
} else {
container.insertBefore(selected, target.nextSibling)
}
}
function dragEnd() {
if (!selected) return;
selected.classList.remove("dragged")
selected = null;
for (let i = 0; i < container.childElementCount - 1; i++) {
let input = container.children[i].querySelector(".topic-input");
input.value = i + 1;
}
}
function dragStart(e) {
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', null)
selected = e.target
selected.classList.add("dragged")
}

View File

@@ -90,21 +90,16 @@ document.addEventListener("DOMContentLoaded", () => {
minWidth: origMinWidth,
minHeight: origMinHeight,
} = getComputedStyle(img);
console.log(img, img.naturalWidth, img.naturalHeight, origMinWidth, origMinHeight, origMaxWidth, origMaxHeight)
if (img.naturalWidth < parseInt(origMinWidth)) {
console.log(1)
img.style.minWidth = img.naturalWidth + "px";
}
if (img.naturalHeight < parseInt(origMinHeight)) {
console.log(2)
img.style.minHeight = img.naturalHeight + "px";
}
if (img.naturalWidth < parseInt(origMaxWidth)) {
console.log(3)
img.style.maxWidth = img.naturalWidth + "px";
}
if (img.naturalHeight < parseInt(origMaxHeight)) {
console.log(4)
img.style.maxHeight = img.naturalHeight + "px";
}
}
@@ -133,3 +128,108 @@ document.addEventListener("DOMContentLoaded", () => {
}
})
});
{
function isBefore(el1, el2) {
if (el2.parentNode === el1.parentNode) {
for (let cur = el1.previousSibling; cur; cur = cur.previousSibling) {
if (cur === el2) return true;
}
}
return false;
}
let draggedItem = null;
function sortableItemDragStart(e, item) {
const box = item.getBoundingClientRect();
const oX = e.clientX - box.left;
const oY = e.clientY - box.top;
draggedItem = item;
item.classList.add('dragged');
e.dataTransfer.setDragImage(item, oX, oY);
e.dataTransfer.effectAllowed = 'move';
}
function sortableItemDragEnd(e, item) {
draggedItem = null;
item.classList.remove('dragged');
}
function sortableItemDragOver(e, item) {
const target = e.target.closest('.sortable-item');
if (!target || target === draggedItem) {
return;
}
const inSameList = draggedItem.dataset.sortableListKey === target.dataset.sortableListKey;
if (!inSameList) {
return;
}
const targetList = draggedItem.closest('.sortable-list');
if (isBefore(draggedItem, target)) {
targetList.insertBefore(draggedItem, target);
} else {
targetList.insertBefore(draggedItem, target.nextSibling);
}
}
const listItemsHandled = new Map();
const getListItemsHandled = (list) => {
return listItemsHandled.get(list) || new Set();
}
function registerSortableList(list) {
list.querySelectorAll('li:not(.immovable)').forEach(item => {
const listItems = getListItemsHandled(list);
listItems.add(item);
listItemsHandled.set(list, listItems);
const dragger = item.querySelector('.dragger');
dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, item)});
dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, item)});
item.addEventListener('dragover', e => {sortableItemDragOver(e, item)});
});
const obs = new MutationObserver(records => {
for (const mutation of records) {
mutation.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) {
return;
}
if (!node.classList.contains('sortable-item')) {
return;
}
const listItems = getListItemsHandled(list)
if (listItems.has(node)) {
return;
}
const dragger = node.querySelector('.dragger');
dragger.addEventListener('dragstart', e => {sortableItemDragStart(e, node)});
dragger.addEventListener('dragend', e => {sortableItemDragEnd(e, node)});
node.addEventListener('dragover', e => {sortableItemDragOver(e, node)});
listItems.add(node);
listItemsHandled.set(list, listItems);
});
}
});
obs.observe(list, {childList: true});
}
document.querySelectorAll('.sortable-list').forEach(registerSortableList);
listsObs = new MutationObserver(records => {
for (const mutation of records) {
mutation.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) {
return;
}
if (!node.classList.contains('sortable-list')) {
return;
}
registerSortableList(node);
})
}
})
listsObs.observe(document.body, {childList: true, subtree: true});
}

View File

@@ -56,6 +56,7 @@ $PAGE_SIDE_MARGIN: 100px !default;
// BORDERS
// **************
$DEFAULT_BORDER: 1px solid black !default;
$DEFAULT_BORDER_INVALID: 2px dashed $BUTTON_COLOR_CRITICAL !default;
$DEFAULT_BORDER_RADIUS: 4px !default;
// other variables can be found before the rule that uses them. they are usually constructed from these basic variables.
@@ -709,6 +710,11 @@ input[type="text"], input[type="password"], textarea, select {
}
}
// lone required inputs managed by js
input:not(form input):invalid {
border: $DEFAULT_BORDER_INVALID;
}
textarea {
font-family: "Atkinson Hyperlegible Mono", monospace;
}
@@ -966,54 +972,6 @@ $topic_locked_background: none !default;
background-color: $topic_locked_background;
}
$draggable_topic_background: $ACCENT_COLOR !default;
$draggable_topic_dragged_color: $BUTTON_COLOR !default;
$draggable_topic_padding: $BIG_PADDING !default;
$draggable_topic_margin: $MEDIUM_BIG_PADDING 0 !default;
$draggable_topic_border: 5px outset !default;
$draggable_topic_border_top: $draggable_topic_border $LIGHT !default;
$draggable_topic_border_bottom: $draggable_topic_border $DARK_2 !default;
.draggable-topic {
cursor: pointer;
user-select: none;
background-color: $draggable_topic_background;
padding: $draggable_topic_padding;
margin: $draggable_topic_margin;
border-top: $draggable_topic_border_top;
border-bottom: $draggable_topic_border_bottom;
&.dragged {
background-color: $draggable_topic_dragged_color;
}
}
$draggable_collection_background: $ACCENT_COLOR !default;
$draggable_collection_dragged_color: $BUTTON_COLOR !default;
$draggable_collection_default_color: $BUTTON_COLOR_2 !default;
$draggable_collection_padding: $BIG_PADDING !default;
$draggable_collection_margin: $MEDIUM_BIG_PADDING 0 !default;
$draggable_collection_border: 5px outset !default;
$draggable_collection_border_top: $draggable_collection_border $LIGHT !default;
$draggable_collection_border_bottom: $draggable_collection_border $DARK_2 !default;
.draggable-collection {
cursor: pointer;
user-select: none;
background-color: $draggable_collection_background;
padding: $draggable_collection_padding;
margin: $draggable_collection_margin;
border-top: $draggable_collection_border_top;
border-bottom: $draggable_collection_border_bottom;
&.dragged {
background-color: $draggable_collection_dragged_color;
}
&.default {
background-color: $draggable_collection_default_color;
}
}
$post_editing_header_color: $LIGHT !default;
.editing {
background-color: $post_editing_header_color;
@@ -1415,6 +1373,7 @@ $settings_grid_gap: $MEDIUM_PADDING !default;
$settings_grid_item_min_width: 600px !default;
$settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default;
$settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
$settings_grid_fieldset_background_color: $DARK_1_LIGHTER !default;
.settings-grid {
display: grid;
gap: $settings_grid_gap;
@@ -1425,7 +1384,7 @@ $settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
& fieldset {
border: $settings_grid_fieldset_border;
border-radius: $settings_grid_fieldset_border_radius;
background-color: $DARK_1_LIGHTER;
background-color: $settings_grid_fieldset_background_color;
}
}
@@ -1440,7 +1399,7 @@ h1 {
$settings_badge_container_gap: $SMALL_PADDING !default;
$settings_badge_container_border: $DEFAULT_BORDER !default;
$settings_badge_container_border_invalid: 2px dashed red !default;
$settings_badge_container_border_invalid: $DEFAULT_BORDER_INVALID !default;
$settings_badge_container_border_radius: $DEFAULT_BORDER_RADIUS !default;
$settings_badge_container_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
$settings_badge_container_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
@@ -1552,3 +1511,73 @@ $rss_button_font_color_active: black !default;
padding-right: $guide_section_padding_right_portrait;
}
}
$sortable_item_background: $ACCENT_COLOR !default;
$sortable_item_dragged_color: $BUTTON_COLOR !default;
$sortable_item_immovable_color: $BUTTON_COLOR_2 !default;
$sortable_item_padding: $BIG_PADDING !default;
$sortable_item_margin: $MEDIUM_BIG_PADDING 0 !default;
$sortable_item_border: 5px outset !default;
$sortable_item_border_top: $sortable_item_border $LIGHT !default;
$sortable_item_border_bottom: $sortable_item_border $DARK_2 !default;
$sortable_item_gap: $MEDIUM_PADDING !default;
ol.sortable-list {
list-style: none;
flex-grow: 1;
margin: 0;
li {
display: flex;
gap: $sortable_item_gap;
background-color: $sortable_item_background;
padding: $sortable_item_padding;
margin: $sortable_item_margin;
border-top: $sortable_item_border_top;
border-bottom: $sortable_item_border_bottom;
}
li.dragged {
background-color: $sortable_item_dragged_color;
}
li.immovable {
background-color: $sortable_item_immovable_color;
}
li.immovable .dragger {
cursor: not-allowed;
}
}
$sortable_item_grabber_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
.dragger {
display: flex;
align-items: center;
background-color: $DARK_2;
padding: $sortable_item_grabber_padding;
cursor: move;
}
$sortable_item_inner_gap: $MEDIUM_PADDING !default;
.sortable-item-inner {
display: flex;
gap: $sortable_item_inner_gap;
flex-grow: 1;
flex-direction: column;
& > * {
flex-grow: 1;
}
&.row {
flex-direction: row;
}
&:not(.row) > * {
margin-right: auto;
}
}
.fg {
flex-grow: 1;
}

View File

@@ -83,6 +83,8 @@ $br: 8px;
$mention_font_color: $fc,
$settings_grid_fieldset_background_color: $lightish_accent,
// $settings_badge_container_border_invalid: 2px dashed $crit,
);