generic collapsable sections
This commit is contained in:
@@ -56,6 +56,6 @@
|
||||
@apply flex items-center border-b border-slate-200 px-2 py-1 text-sm group-hover:bg-slate-50;
|
||||
}
|
||||
|
||||
#rich-text-content blockquote {
|
||||
.rich-text-content blockquote {
|
||||
@apply my-4 border-s-4 border-gray-300 bg-slate-200 p-4 italic leading-relaxed;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ module Domain::DomainsHelper
|
||||
furaffinity.net
|
||||
boosty.to
|
||||
hipolink.me
|
||||
archiveofourown.org
|
||||
].freeze
|
||||
|
||||
DOMAIN_TO_ICON_PATH =
|
||||
|
||||
@@ -304,12 +304,17 @@ module LogEntriesHelper
|
||||
},
|
||||
transformers: [
|
||||
quote_transformer,
|
||||
is_plain_text ? remove_empty_newline_transformer : nil,
|
||||
# is_plain_text ? remove_empty_newline_transformer : nil,
|
||||
is_plain_text ? plain_text_transformer : nil,
|
||||
is_plain_text ? remove_multiple_br_transformer : nil,
|
||||
# is_plain_text ? remove_multiple_br_transformer : nil,
|
||||
].compact,
|
||||
)
|
||||
|
||||
raw sanitizer.fragment(document_html).strip
|
||||
fragment = sanitizer.fragment(document_html).strip
|
||||
if is_plain_text
|
||||
fragment.gsub!("<br>", "")
|
||||
fragment.gsub!("<br />", "")
|
||||
end
|
||||
raw fragment
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,21 +8,34 @@ module UiHelper
|
||||
sig do
|
||||
params(
|
||||
title: String,
|
||||
has_collapse_widget: T::Boolean,
|
||||
collapsible: T::Boolean,
|
||||
initially_collapsed: T::Boolean,
|
||||
font_size_adjustable: T::Boolean,
|
||||
kwargs: T.untyped,
|
||||
block: T.proc.void,
|
||||
).returns(String)
|
||||
end
|
||||
def sky_section_tag(title, has_collapse_widget: false, **kwargs, &block)
|
||||
def sky_section_tag(
|
||||
title,
|
||||
collapsible: false,
|
||||
initially_collapsed: false,
|
||||
font_size_adjustable: false,
|
||||
**kwargs,
|
||||
&block
|
||||
)
|
||||
content = capture(&block)
|
||||
kwargs[:class] ||= "bg-slate-100 p-4"
|
||||
content_tag(:div, class: "sky-section") do
|
||||
concat content_tag(:div, title, class: "sky-section-header")
|
||||
concat(
|
||||
content_tag(:div, class: "sky-section-body #{kwargs[:class]}") do
|
||||
concat content
|
||||
end,
|
||||
)
|
||||
end
|
||||
|
||||
render(
|
||||
partial: "shared/section_controls/sky_section",
|
||||
locals: {
|
||||
title: title,
|
||||
content: content,
|
||||
collapsible: collapsible,
|
||||
initially_collapsed: initially_collapsed,
|
||||
font_size_adjustable: font_size_adjustable,
|
||||
container_class: kwargs[:class],
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
181
app/javascript/bundles/UI/collapsibleSections.ts
Normal file
181
app/javascript/bundles/UI/collapsibleSections.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Collapsible Section Controls
|
||||
*
|
||||
* This module contains functions to control collapsible sections with font size controls.
|
||||
* Features include:
|
||||
* - Toggling between expanded and collapsed view
|
||||
* - Increasing and decreasing font size
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class representing a collapsible section with font size controls
|
||||
*/
|
||||
class CollapsibleSection {
|
||||
private readonly section: Element;
|
||||
private readonly content: HTMLElement;
|
||||
private readonly toggleTextElement: Element | null;
|
||||
private readonly toggleIconElement: HTMLElement | null;
|
||||
private readonly fontDisplayElement: Element | null;
|
||||
private readonly baseFontSize: number;
|
||||
private readonly initiallyCollapsed: boolean;
|
||||
|
||||
/**
|
||||
* Create a new collapsible section
|
||||
* @param rootElement The root DOM element for this section
|
||||
*/
|
||||
constructor(rootElement: Element) {
|
||||
this.section = rootElement;
|
||||
this.initiallyCollapsed = rootElement.hasAttribute(
|
||||
'data-initially-collapsed',
|
||||
);
|
||||
|
||||
// Find and cache all necessary DOM elements
|
||||
const contentElement = rootElement.querySelector(
|
||||
'[data-collapsable="content"]',
|
||||
);
|
||||
if (!(contentElement instanceof HTMLElement)) {
|
||||
throw new Error(`Section is missing a content element`);
|
||||
}
|
||||
|
||||
this.content = contentElement;
|
||||
|
||||
// Get single elements instead of arrays
|
||||
this.toggleTextElement = rootElement.querySelector(
|
||||
'[data-collapsable="toggle-text"]',
|
||||
);
|
||||
|
||||
const iconElement = rootElement.querySelector(
|
||||
'[data-collapsable="toggle-icon"]',
|
||||
);
|
||||
this.toggleIconElement =
|
||||
iconElement instanceof HTMLElement ? iconElement : null;
|
||||
|
||||
this.fontDisplayElement = rootElement.querySelector(
|
||||
'[data-collapsable="font-display"]',
|
||||
);
|
||||
|
||||
// Initialize font size info
|
||||
this.baseFontSize = parseFloat(
|
||||
window.getComputedStyle(this.content).fontSize,
|
||||
);
|
||||
this.updateFontSizeDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize event listeners for this section
|
||||
*/
|
||||
initEventListeners(): void {
|
||||
// Setup handlers for all interactive elements
|
||||
this.section
|
||||
.querySelectorAll('[data-collapsable="toggle"]')
|
||||
.forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
});
|
||||
});
|
||||
|
||||
this.section
|
||||
.querySelectorAll('[data-collapsable="decrease"]')
|
||||
.forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.decreaseFontSize();
|
||||
});
|
||||
});
|
||||
|
||||
this.section
|
||||
.querySelectorAll('[data-collapsable="increase"]')
|
||||
.forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.increaseFontSize();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between expanded and collapsed view
|
||||
*/
|
||||
toggle(): void {
|
||||
// Determine current state from DOM
|
||||
const isCollapsed = this.content.classList.contains('line-clamp-6');
|
||||
|
||||
// Toggle line-clamp class
|
||||
this.content.classList.toggle('line-clamp-6');
|
||||
|
||||
// Update toggle text
|
||||
if (this.toggleTextElement) {
|
||||
this.toggleTextElement.textContent = isCollapsed
|
||||
? 'Show Less'
|
||||
: 'Show More';
|
||||
}
|
||||
|
||||
// Update toggle icon
|
||||
if (this.toggleIconElement) {
|
||||
this.toggleIconElement.style.transform = isCollapsed
|
||||
? 'rotate(180deg)'
|
||||
: 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust font size of content within min/max bounds
|
||||
*/
|
||||
adjustFontSize(delta: number, min = 12, max = 22): void {
|
||||
const currentSize = parseFloat(
|
||||
window.getComputedStyle(this.content).fontSize,
|
||||
);
|
||||
const newSize = Math.min(Math.max(currentSize + delta, min), max);
|
||||
|
||||
this.content.style.fontSize = `${newSize}px`;
|
||||
this.updateFontSizeDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase font size (max 22px)
|
||||
*/
|
||||
increaseFontSize(): void {
|
||||
this.adjustFontSize(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease font size (min 12px)
|
||||
*/
|
||||
decreaseFontSize(): void {
|
||||
this.adjustFontSize(-2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the font size percentage display
|
||||
*/
|
||||
updateFontSizeDisplay(): void {
|
||||
if (!this.fontDisplayElement) return;
|
||||
|
||||
const currentSize = parseFloat(
|
||||
window.getComputedStyle(this.content).fontSize,
|
||||
);
|
||||
const percentage = Math.round((currentSize / this.baseFontSize) * 100);
|
||||
|
||||
this.fontDisplayElement.textContent = `${percentage}%`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all collapsible sections on the page
|
||||
*/
|
||||
export function initCollapsibleSections(): void {
|
||||
document
|
||||
.querySelectorAll('[data-collapsable="root"]')
|
||||
.forEach((sectionElement) => {
|
||||
try {
|
||||
// Create a new section instance
|
||||
const section = new CollapsibleSection(sectionElement);
|
||||
|
||||
// Set up event handlers
|
||||
section.initEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize section:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,9 +2,15 @@ import ReactOnRails from 'react-on-rails';
|
||||
|
||||
import UserSearchBar from '../bundles/Main/components/UserSearchBar';
|
||||
import { UserMenu } from '../bundles/Main/components/UserMenu';
|
||||
import { initCollapsibleSections } from '../bundles/UI/collapsibleSections';
|
||||
|
||||
// This is how react_on_rails can see the components in the browser.
|
||||
ReactOnRails.register({
|
||||
UserSearchBar,
|
||||
UserMenu,
|
||||
});
|
||||
|
||||
// Initialize collapsible sections
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initCollapsibleSections();
|
||||
});
|
||||
|
||||
@@ -95,6 +95,11 @@ class Domain::Post::InkbunnyPost < Domain::Post
|
||||
"inkbunny.net"
|
||||
end
|
||||
|
||||
sig { override.returns(T.nilable(String)) }
|
||||
def description_html_for_view
|
||||
self.description
|
||||
end
|
||||
|
||||
sig { override.returns(T.nilable(String)) }
|
||||
def title
|
||||
super
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% description_html = sanitize_description_html(post) %>
|
||||
<%= sky_section_tag("Description", class: description_html ? "bg-slate-700 p-4 text-slate-200" : nil) do %>
|
||||
<%= sky_section_tag("Description", collapsible: true, class: description_html ? "bg-slate-700 p-4 text-slate-200" : nil) do %>
|
||||
<% if description_html %>
|
||||
<%= description_html %>
|
||||
<% else %>
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
<% if log_entry %>
|
||||
<section class="sky-section">
|
||||
<div class="section-header">File Details</div>
|
||||
<div class="bg-slate-100 p-4">
|
||||
<div class="flex flex-wrap gap-x-4 text-sm text-slate-600">
|
||||
<span>
|
||||
<i class="fa-regular fa-file mr-1"></i>
|
||||
<% ct = log_entry.content_type %>
|
||||
<% ct = ct.split(";").first if ct %>
|
||||
Type: <%= ct %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-weight-hanging mr-1"></i>
|
||||
Size: <%= number_to_human_size(log_entry.response_size) %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-clock mr-1"></i>
|
||||
Response Time: <%= log_entry.response_time_ms == -1 ? "(unknown)" : "#{log_entry.response_time_ms}ms" %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-signal mr-1"></i>
|
||||
Status: <span class="<%= log_entry.status_code == 200 ? 'text-green-600' : 'text-red-600' %>"><%= log_entry.status_code %></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
<%= sky_section_tag("File Details") do %>
|
||||
<div class="flex flex-wrap gap-x-4 text-sm text-slate-600">
|
||||
<span>
|
||||
<i class="fa-regular fa-file mr-1"></i>
|
||||
<% ct = log_entry.content_type %>
|
||||
<% ct = ct.split(";").first if ct %>
|
||||
Type: <%= ct %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-weight-hanging mr-1"></i>
|
||||
Size: <%= number_to_human_size(log_entry.response_size) %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-clock mr-1"></i>
|
||||
Response Time: <%= log_entry.response_time_ms == -1 ? "(unknown)" : "#{log_entry.response_time_ms}ms" %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-signal mr-1"></i>
|
||||
Status: <span class="<%= log_entry.status_code == 200 ? 'text-green-600' : 'text-red-600' %>"><%= log_entry.status_code %></span>
|
||||
</span>
|
||||
</div>
|
||||
<% end if log_entry %>
|
||||
|
||||
@@ -1,86 +1,17 @@
|
||||
<section class="sky-section w-full">
|
||||
<% rich_text_content = render_rich_text_content(log_entry) %>
|
||||
<div class="section-header flex justify-between items-center sticky z-10 py-2 border-b">
|
||||
<span><%= pretty_content_type(log_entry.content_type) %></span>
|
||||
<div class="flex items-center gap-3">
|
||||
<% if rich_text_content.present? %>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="font-size-percentage" class="text-sm text-gray-600 font-medium">100%</span>
|
||||
<button onclick="decreaseFontSize()" class="text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center bg-gray-100 border border-gray-300 rounded-md h-6 w-6 shadow-sm hover:bg-gray-200">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</button>
|
||||
<button onclick="increaseFontSize()" class="text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center bg-gray-100 border border-gray-300 rounded-md h-6 w-6 shadow-sm hover:bg-gray-200">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button onclick="toggleRichTextContent()" id="rich-text-toggle" class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1 bg-gray-100 border border-gray-300 rounded-md px-2 py-1 shadow-sm hover:bg-gray-200">
|
||||
<span>Show More</span>
|
||||
<i id="rich-text-toggle-icon" class="fa-solid fa-chevron-down w-4 h-4"></i>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% rich_text_content = render_rich_text_content(log_entry) %>
|
||||
<%= sky_section_tag(
|
||||
pretty_content_type(log_entry.content_type),
|
||||
collapsible: rich_text_content.present?,
|
||||
initially_collapsed: rich_text_content.present?,
|
||||
font_size_adjustable: rich_text_content.present?,
|
||||
) do %>
|
||||
<% if rich_text_content.present? %>
|
||||
<div class="bg-slate-100 p-4 indent-4 font-medium font-serif">
|
||||
<div id="rich-text-content" class="line-clamp-6 leading-loose">
|
||||
<%= rich_text_content %>
|
||||
</div>
|
||||
<script>
|
||||
// Store the base font size when page loads
|
||||
let baseFontSize;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const content = document.getElementById('rich-text-content');
|
||||
baseFontSize = parseFloat(window.getComputedStyle(content).fontSize);
|
||||
updateFontSizePercentage(baseFontSize);
|
||||
});
|
||||
|
||||
function toggleRichTextContent() {
|
||||
const content = document.getElementById('rich-text-content');
|
||||
const button = document.getElementById('rich-text-toggle');
|
||||
const icon = document.getElementById('rich-text-toggle-icon');
|
||||
if (content.classList.contains('line-clamp-6')) {
|
||||
content.classList.remove('line-clamp-6');
|
||||
button.firstElementChild.textContent = 'Show Less';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
} else {
|
||||
content.classList.add('line-clamp-6');
|
||||
button.firstElementChild.textContent = 'Show More';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
function increaseFontSize() {
|
||||
const content = document.getElementById('rich-text-content');
|
||||
const currentSize = window.getComputedStyle(content).fontSize;
|
||||
const newSize = Math.min(parseFloat(currentSize) + 2, 22); // Don't allow larger than 22px
|
||||
content.style.fontSize = `${newSize}px`;
|
||||
updateFontSizePercentage(newSize);
|
||||
}
|
||||
|
||||
function decreaseFontSize() {
|
||||
const content = document.getElementById('rich-text-content');
|
||||
const currentSize = window.getComputedStyle(content).fontSize;
|
||||
const newSize = Math.max(parseFloat(currentSize) - 2, 12); // Don't allow smaller than 12px
|
||||
content.style.fontSize = `${newSize}px`;
|
||||
updateFontSizePercentage(newSize);
|
||||
}
|
||||
|
||||
function updateFontSizePercentage(currentSize) {
|
||||
// If baseFontSize isn't set yet, set it
|
||||
if (!baseFontSize) {
|
||||
baseFontSize = currentSize;
|
||||
}
|
||||
|
||||
const percentageElement = document.getElementById('font-size-percentage');
|
||||
const percentage = Math.round((currentSize / baseFontSize) * 100);
|
||||
percentageElement.textContent = `${percentage}%`;
|
||||
}
|
||||
</script>
|
||||
<div class="bg-slate-100 p-4 indent-4 font-medium font-serif rich-text-content">
|
||||
<%= rich_text_content %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-slate-100 p-4 text-slate-500 italic text-center">
|
||||
<span>Error rendering document</span>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-600 font-medium" data-collapsable="font-display">100%</span>
|
||||
<button class="text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center bg-gray-100 border border-gray-300 rounded-md h-6 w-6 shadow-sm hover:bg-gray-200"
|
||||
data-collapsable="decrease">
|
||||
<i class="fa-solid fa-minus"></i>
|
||||
</button>
|
||||
<button class="text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center bg-gray-100 border border-gray-300 rounded-md h-6 w-6 shadow-sm hover:bg-gray-200"
|
||||
data-collapsable="increase">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
24
app/views/shared/section_controls/_sky_section.html.erb
Normal file
24
app/views/shared/section_controls/_sky_section.html.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="sky-section w-full" <%= 'data-collapsable=root' if collapsible %><%= ' data-initially-collapsed=true' if initially_collapsed %>>
|
||||
<%# Section header with controls %>
|
||||
<div class="section-header flex justify-between items-center sticky z-10 py-2 border-b">
|
||||
<span><%= title %></span>
|
||||
<% if collapsible || font_size_adjustable %>
|
||||
<div class="flex items-center gap-3">
|
||||
<% if font_size_adjustable %>
|
||||
<%= render partial: 'shared/section_controls/font_size_controls' %>
|
||||
<% end %>
|
||||
<% if collapsible %>
|
||||
<%= render partial: 'shared/section_controls/toggle_button', locals: { initially_collapsed: initially_collapsed } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%# Content container %>
|
||||
<div class="<%= container_class %>">
|
||||
<% content_classes = "leading-loose" %>
|
||||
<% content_classes += " line-clamp-6" if collapsible && initially_collapsed %>
|
||||
<div class="<%= content_classes %>"<%= ' data-collapsable=content' if collapsible %>>
|
||||
<%= content.html_safe %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<%
|
||||
button_text = initially_collapsed ? "Show More" : "Show Less"
|
||||
icon_style = initially_collapsed ? "" : "style=\"transform: rotate(180deg)\""
|
||||
%>
|
||||
<button class="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1 bg-gray-100 border border-gray-300 rounded-md px-2 py-1 shadow-sm hover:bg-gray-200"
|
||||
data-collapsable="toggle">
|
||||
<span data-collapsable="toggle-text"><%= button_text %></span>
|
||||
<i class="fa-solid fa-chevron-down w-4 h-4" data-collapsable="toggle-icon" <%= icon_style.html_safe %>></i>
|
||||
</button>
|
||||
Reference in New Issue
Block a user