Replace jQuery toggle animations with CSS-based animations

- Use CSS keyframes (slideDown/slideUp) for 800ms door-like animations
- JS only toggles classes and updates button text at correct timing
- Preview toggle uses native <details> with animated open/close
- Arrow indicators on right side (hide preview ▲ / show preview ▼)
- No-JS fallback preserved (links navigate to dedicated pages)
This commit is contained in:
Russell Ballestrini 2025-12-19 19:32:11 -05:00
parent 9c74e6256a
commit ef8f382976
7 changed files with 157 additions and 156 deletions

View file

@ -411,120 +411,112 @@ form.node-action {
opacity: 0.8 !important;
}
/* Pure CSS toggle using <details> element with animations */
.toggle-summary {
.remark-box-div-main {
margin-bottom: 35px;
}
.remark-box-div {
display: none;
overflow: hidden;
margin-top: 10px;
}
.edit-box-div {
display: none;
overflow: hidden;
margin-top: 15px;
}
/* CSS animation when toggled open via JS */
.remark-box-div.toggle-open,
.edit-box-div.toggle-open {
display: block;
overflow: hidden;
max-height: 1000px;
animation: slideDown 0.8s ease-out forwards;
}
.remark-box-div.toggle-closing,
.edit-box-div.toggle-closing {
display: block;
max-height: 1000px;
animation: slideUp 0.8s ease-out forwards;
}
@keyframes slideDown {
from {
max-height: 0;
}
to {
max-height: 1000px;
}
}
.my-namespaces-div {
display: none;
position: absolute;
background-color: #ffffff;
z-index: 1;
overflow: hidden;
padding-top: 10px;
padding-bottom: 10px;
}
.my-namespaces-div.toggle-open {
display: block;
animation: slideDown 0.8s ease-out;
}
/* Preview toggle with CSS animation */
.preview-details {
overflow: hidden;
}
.preview-details[open] > .preview {
overflow: hidden;
animation: slideDown 0.8s ease-out forwards;
}
.preview-details.closing > .preview {
overflow: hidden;
max-height: 1000px;
animation: slideUp 0.8s ease-out forwards;
}
@keyframes slideUp {
from {
max-height: 1000px;
}
to {
max-height: 0;
}
}
.preview-toggle {
cursor: pointer;
list-style: none;
}
.toggle-summary::-webkit-details-marker {
.preview-toggle::-webkit-details-marker {
display: none;
}
.toggle-summary::marker {
display: none;
content: "";
}
/* Toggle text switching: show/hide different text based on open state */
.toggle-summary .when-open {
display: none;
}
.toggle-summary .when-closed {
.preview-toggle .when-open {
display: inline;
}
details[open] > .toggle-summary .when-open {
.preview-toggle .when-closed {
display: none;
}
.preview-details:not([open]) .when-open {
display: none;
}
.preview-details:not([open]) .when-closed {
display: inline;
}
details[open] > .toggle-summary .when-closed {
display: none;
}
/* Hidden summary for main remark box (always open) */
.toggle-summary-hidden {
display: none;
}
/* Fallback links hidden when JS not needed */
.toggle-fallback {
display: none;
}
/* Animated details content using CSS grid technique */
.edit-box-details,
.remark-box-details,
.node-children-details {
--details-transition-duration: 0.3s;
}
.edit-box-details > .details-content,
.remark-box-details > .details-content,
.node-children-details > .details-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows var(--details-transition-duration) ease-out;
}
.edit-box-details[open] > .details-content,
.remark-box-details[open] > .details-content,
.node-children-details[open] > .details-content {
grid-template-rows: 1fr;
}
.edit-box-details > .details-content > .details-content-inner,
.remark-box-details > .details-content > .details-content-inner,
.node-children-details > .details-content > .details-content-inner {
overflow: hidden;
}
/* Edit box details styling */
.edit-box-details {
margin-top: 15px;
}
/* Remark box details styling */
.remark-box-details {
margin-top: 10px;
}
.remark-box-details-main {
margin-bottom: 35px;
}
/* Namespace switcher dropdown */
.my-namespaces-details {
display: inline;
position: relative;
}
.my-namespaces-content {
position: absolute;
background-color: #ffffff;
z-index: 1;
padding: 10px;
border: 1px solid #ddd;
min-width: 120px;
/* Fade in animation for dropdown */
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
/* Node children collapse/expand */
.node-children-details {
margin-top: 5px;
}
.node-children-details > .toggle-summary {
margin-bottom: 5px;
}
#remarkbox-footer {
font-size: 0.8em;
font-weight: bold;

View file

@ -37,19 +37,39 @@ function sendPreview(textarea, div, mathjax=false){
}
}
// CSS-based toggle for smoother animations.
function toggle(target, button, off_text, on_text="hide"){
var el = document.getElementById(target);
var btn = document.getElementById(button);
if (el.classList.contains('toggle-open')) {
// Animate close, then update text
el.classList.add('toggle-closing');
setTimeout(function() {
el.classList.remove('toggle-open');
el.classList.remove('toggle-closing');
btn.textContent = off_text;
}, 800);
} else {
// Update text immediately when opening
btn.textContent = on_text;
el.classList.add('toggle-open');
}
}
// Auto-focus textarea when details element opens (progressive enhancement).
// Falls back gracefully if browser doesn't support the required APIs.
// Animate <details> close for preview-details elements.
if (typeof document.addEventListener === 'function') {
document.addEventListener('toggle', function(e) {
var details = e.target;
if (details.tagName !== 'DETAILS' || !details.open) return;
// Find textarea inside the details element and focus it.
var textarea = details.querySelector('textarea');
if (textarea && typeof textarea.focus === 'function') {
// Small delay to let the CSS transition start.
setTimeout(function() { textarea.focus(); }, 50);
document.addEventListener('click', function(e) {
var summary = e.target.closest('.preview-toggle');
if (!summary) return;
var details = summary.parentElement;
if (!details || !details.classList.contains('preview-details')) return;
if (details.open && !details.classList.contains('closing')) {
e.preventDefault();
details.classList.add('closing');
setTimeout(function() {
details.open = false;
details.classList.remove('closing');
}, 800);
}
}, true);
}

View file

@ -35,9 +35,10 @@
{{ snippets.namespace_home_uri(request.namespace) }} &nbsp;
{%- if request.mode != 'embed' and request.user.authenticated and request.user.namespaces %}
<details id="my-namespaces-details" class="my-namespaces-details">
<summary class="toggle-summary" id="my-namespaces-link">(switch)</summary>
<div class="my-namespaces-content">
<a href="{{ domain_app }}/u/namespaces" onclick="toggle('my-namespaces-div', 'my-namespaces-link', '(switch)', '(switch)'); return false;" id="my-namespaces-link">(switch)</a>
<div id="my-namespaces-div" class="my-namespaces-div">
{% for namespace in request.user.namespaces %}
{% if namespace != request.namespace %}
{{ snippets.namespace_home_uri(namespace) }}
@ -50,7 +51,6 @@
<a href="{{ domain_app }}/setup" class="button">setup</a>
</center>
</div>
</details>
&nbsp;
{%- endif %}

View file

@ -46,19 +46,13 @@ page: {{ request.page_number }}
{{ snippets.actions(node) }}
</div>
<details id="edit-box-{{ node.id }}" class="edit-box-details">
<summary class="action toggle-summary"><span class="when-closed">edit</span><span class="when-open">hide</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="edit-box-{{ node.id }}" class="edit-box-div">
{{ forms.edit(node) }}
</div></div>
</details>
</div>
<details id="remark-box-{{ node.id }}" class="remark-box-details">
<summary class="remark toggle-summary"><span class="when-closed">remark</span><span class="when-open">hide</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="remark-box-{{ node.id }}" class="remark-box-div">
{{ forms.reply(node, node) }}
</div></div>
</details>
</div>
{% endfor -%}

View file

@ -75,19 +75,13 @@
</div>
{% endif %}
<details id="edit-box-{{ request.node.id }}" class="edit-box-details">
<summary class="action toggle-summary"><span class="when-closed">edit</span><span class="when-open">hide</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="edit-box-{{ request.node.id }}" class="edit-box-div">
{{ forms.edit(request.node) }}
</div></div>
</details>
</div>
<details id="remark-box-{{ request.node.id }}" class="remark-box-details remark-box-details-main" open>
<summary class="toggle-summary toggle-summary-hidden"></summary>
<div class="details-content"><div class="details-content-inner">
<div id="remark-box-{{ request.node.id }}" class="remark-box-div-main not-hidden-remark-box-div">
{{ forms.reply(request.node, request.root_node) }}
</div></div>
</details>
</div>
{%- if request.node.id and request.node_graph[request.node.id] -%}
@ -154,29 +148,20 @@
<a href="{{ request.link_prefix }}/{{ parent.id }}#{{ parent.id }}" class="load-more shown-on-tablet">load more <span style="color: #292929; font-weight: normal;">({{children_ids | length}} remarks)</span></a>
{% endif %}
<details id="edit-box-{{ parent.id }}" class="edit-box-details">
<summary class="action toggle-summary"><span class="when-closed">edit</span><span class="when-open">hide</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="edit-box-{{ parent.id }}" class="edit-box-div">
{{ forms.edit(parent) }}
</div></div>
</details>
</div>
<details id="remark-box-{{ parent.id }}" class="remark-box-details">
<summary class="remark toggle-summary"><span class="when-closed">remark</span><span class="when-open">hide</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="remark-box-{{ parent.id }}" class="remark-box-div">
{{ forms.reply(parent, request.root_node) }}
</div></div>
</details>
</div>
{#- nest children in this node's div for convo collapsing. -#}
<details id="node-children-{{ parent.id }}" class="node-children-details" open>
<summary class="action link toggle-summary"><span class="when-closed">expand [+]</span><span class="when-open">collapse [-]</span></summary>
<div class="details-content"><div class="details-content-inner">
<div id="node-children-{{ parent.id }}">
{%- if children_ids %}
{{ loop(children_ids) }}
{% endif -%}
</div></div>
</details>
</div>
{# close the class="node" div #}
</div>

View file

@ -1,4 +1,5 @@
{% macro reply(node, root) %}
{% if root.locked %}
<br>
<p>This thread was locked to prevent additional comments.</p>
@ -25,8 +26,8 @@
{% set submit_button_value = 'save message' %}
{% include 'submit.j2' %}
<details open>
<summary class="action link" float="right">hide preview</summary>
<details class="preview-details" open>
<summary class="action link preview-toggle"><span class="when-open">hide preview ▲</span><span class="when-closed">show preview ▼</span></summary>
<div id='preview-{{ node.id }}' name='preview' class='preview js-only'></div>
</details>
@ -38,9 +39,11 @@
</form>
{% endif %}
{% endmacro %}
{% macro edit(node) %}
<form method="post" action="{{ request.link_prefix }}/{{ node.id }}/edit" onsubmit="submit.disabled = true; return true;">
{% if node.title %}
@ -67,6 +70,7 @@
{% set submit_button_value = 'save message' %}
{% include 'submit.j2' %}
</form>
{% endmacro %}
{% macro pay_what_you_can() %}

View file

@ -84,7 +84,9 @@
{% macro button_remark(node, root_node) %}
{% if root_node and not root_node.locked %}
<a href="{{ request.link_prefix }}/{{ node.id }}" id="remark-link-{{ node.id }}" class="remark toggle-fallback">remark</a>
<a href="{{ request.link_prefix }}/{{ node.id }}"
onclick="toggle('remark-box-{{ node.id }}', 'remark-link-{{ node.id }}', 'remark', 'hide'); document.getElementById('textarea-{{ node.id }}').focus(); return false;"
id="remark-link-{{ node.id }}" class="remark">remark</a>
{% endif %}
{% endmacro %}
@ -97,6 +99,9 @@
{% endmacro %}
{% macro button_collapse(node) %}
<button
onclick="toggle('node-children-{{ node.id }}', 'collapse-link-{{ node.id }}', 'expand [+]', 'collapse [-]');"
id="collapse-link-{{ node.id }}" class="action link js-only">collapse [-]</button>
{% endmacro %}
{% macro permalinks(node, parent_node=None, root_node=None) %}
@ -206,7 +211,8 @@
{{ enable_node(node=node) }} &nbsp;
{% else %}
<a href="{{ request.link_prefix }}/{{ node.id }}/edit"
id="edit-link-{{ node.id }}" class="action toggle-fallback">edit</a>
onclick="toggle('edit-box-{{ node.id }}', 'edit-link-{{ node.id }}', 'edit', 'hide'); document.getElementById('edit-textarea-{{ node.id }}').focus(); return false;"
id="edit-link-{{ node.id }}" class="action">edit</a>
{{ disable_node(node=node) }} &nbsp;