feat(context-menu): add page context menu for copy/view Markdown (#908)

* feat(context-menu): implement page context menu for copy Markdown

- Added a context menu to Markdown pages allowing users to copy content as Markdown or view it in a new tab.
- Introduced new layout files for Markdown rendering and updated existing layouts to include the context menu.
- Enhanced configuration options to enable or disable the context menu globally or per page.
- Updated internationalization files to support new context menu actions.
- Improved documentation to guide users on the new features and their usage.

* feat(context-menu): enhance dropdown positioning and responsiveness

- Updated the context menu dropdown to use responsive positioning classes for better alignment on different screen sizes.
- Added new CSS classes to ensure the dropdown appears correctly on smaller devices, improving user experience.

* refactor(context-menu): adjust button styles for improved layout and usability

- Modified button classes in the page context menu to enhance spacing and alignment.
- Reduced padding and gap sizes for a more compact design, improving overall user experience.

* refactor(styles): update CSS classes for improved layout and consistency

- Removed outdated ring classes and adjusted padding and border styles in various components for a cleaner design.
- Enhanced hover effects and gap sizes for better user experience across language switcher, navbar, and theme toggle elements.
- Standardized rounded styles to maintain visual consistency throughout the interface.

* feat(context-menu): add custom links to page context menu

- Introduced new configuration options for custom links in the page context menu, allowing users to open documentation in ChatGPT and Claude.
- Updated the context menu layout to include a separator and display the new links with appropriate styling.
- Enhanced the CSS classes for better visual integration with existing context menu elements.

* feat(icons): add new AI icons and update context menu links

- Introduced new icons for AI tools including ChatGPT, Claude, Gemini, and others in the icons.yaml file.
- Enhanced the page context menu in multiple languages to include links for opening documentation in ChatGPT and Claude, improving user accessibility to AI resources.
- Updated internationalization files to reflect new context menu options and additional copy functionalities.

* refactor(layouts): simplify page structure in glossary and section layouts

- Removed the conditional rendering of page links in the glossary and section layouts to streamline the markup.
- Adjusted the blog and docs list layouts to ensure consistent closing of HTML tags and improved formatting.

* feat(context-menu): add outbound icons to external links

Add arrow-up-right icons to "View as Markdown" and custom links
in the page context menu to indicate they open in new tabs.

* Revert "feat(context-menu): add outbound icons to external links"

This reverts commit 670175e200f091ed89b15bd16f44a585355db57d.

* chore: rebuild css

* chore: update stats json

* chore: rename project and update context menu structure

- Changed project name from "wizardly-wing" to "hextra" in package-lock.json.
- Refactored context menu structure in hugo.yaml to improve organization and consistency across multiple languages.
- Updated context menu links to ensure proper functionality and accessibility.

* feat(context-menu): enhance clipboard functionality and pre-fetching

- Implemented pre-fetching of markdown content for copy buttons to improve performance and avoid clipboard access issues in Safari.
- Updated click event handlers to utilize cached content for clipboard operations, with a fallback to fetch content if not pre-fetched.
- Added checks to ensure elements exist before performing actions, enhancing robustness of the context menu interactions.

* fix(context-menu): improve hover effects and border styles

- Enhanced the context menu's border styles with transition effects for better visual feedback on hover.
- Updated CSS classes to ensure consistent styling across different themes, improving user experience.

* feat(context-menu): add page context menu functionality across multiple languages

- Introduced a page context menu that allows users to copy content as Markdown or view the raw Markdown source, enhancing usability for documentation sites.
- Added configuration options to enable the context menu globally and control it on a per-page basis.
- Implemented support for custom links in the context menu, allowing integration with external services.
- Updated documentation in Persian, Japanese, Chinese, and English to reflect these new features.

* fix(icons): update SVG definitions for Gemini and add fill attribute

- Updated the SVG definition for the Gemini icon to include the 'fill' attribute for better rendering.
- Ensured consistency in the SVG structure for the Gemini icon while maintaining the existing definitions for ChatGPT and Claude.

* chore: rebuild css

* chore: remove Claude links from context menu in multiple languages

- Removed the "Open in Claude" option from the context menu for Persian, Japanese, and Simplified Chinese languages to streamline the user experience.

* fix(context-menu): update cursor styles for buttons in context menu

- Changed cursor style from default to pointer for buttons in the page context menu to enhance user interaction feedback.
This commit is contained in:
Xin
2026-01-23 23:40:29 +00:00
committed by GitHub
parent e81447e7ae
commit fa8f8554ea
27 changed files with 612 additions and 23 deletions

View File

@@ -0,0 +1,155 @@
document.addEventListener('DOMContentLoaded', () => {
// Pre-fetch markdown content for all copy buttons to avoid Safari NotAllowedError
// Safari requires clipboard writes to happen synchronously within user gesture
const copyButtons = document.querySelectorAll('.hextra-page-context-menu-copy');
const contentCache = new Map();
// Pre-fetch content for each button on page load
copyButtons.forEach(button => {
const url = button.dataset.url;
if (url) {
fetch(url)
.then(response => {
if (response.ok) return response.text();
throw new Error('Failed to fetch');
})
.then(markdown => contentCache.set(url, markdown))
.catch(error => console.error('Failed to pre-fetch markdown:', error));
}
});
// Initialize copy buttons with synchronous clipboard access
copyButtons.forEach(button => {
button.addEventListener('click', () => {
const url = button.dataset.url;
const markdown = contentCache.get(url);
if (markdown) {
// Synchronous clipboard write initiation - works in Safari
navigator.clipboard.writeText(markdown)
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
} else {
// Fallback: fetch and copy (may fail in Safari if content not pre-fetched)
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.text();
})
.then(text => {
contentCache.set(url, text);
return navigator.clipboard.writeText(text);
})
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
}
});
});
// Initialize dropdown toggles
const dropdownToggles = document.querySelectorAll('.hextra-page-context-menu-toggle');
dropdownToggles.forEach(toggle => {
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = toggle.dataset.state === 'open';
// Close all other dropdowns first
dropdownToggles.forEach(t => {
if (t !== toggle) {
t.dataset.state = 'closed';
const otherContainer = t.closest('.hextra-page-context-menu');
const otherMenu = otherContainer.querySelector('.hextra-page-context-menu-dropdown');
const otherChevron = t.querySelector('[data-chevron]');
otherMenu.classList.add('hx:hidden');
if (otherChevron) {
otherChevron.style.transform = '';
}
}
});
// Toggle current
toggle.dataset.state = isOpen ? 'closed' : 'open';
menu.classList.toggle('hx:hidden', isOpen);
// Rotate chevron icon
if (chevron) {
chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
// Check if click is outside any dropdown container
const isOutside = !e.target.closest('.hextra-page-context-menu');
if (isOutside) {
dropdownToggles.forEach(toggle => {
toggle.dataset.state = 'closed';
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
});
}
});
// Helper to close dropdown
const closeDropdown = (container) => {
if (!container) return;
const toggle = container.querySelector('.hextra-page-context-menu-toggle');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
if (!toggle || !menu) return;
const chevron = toggle.querySelector('[data-chevron]');
toggle.dataset.state = 'closed';
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
};
// Handle dropdown menu copy action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="copy"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const copyBtn = container.querySelector('.hextra-page-context-menu-copy');
if (!copyBtn) return;
closeDropdown(container);
copyBtn.click();
});
});
// Handle dropdown menu view action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="view"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const url = btn.dataset.url;
if (!url) return;
closeDropdown(container);
window.open(url, '_blank', 'noopener,noreferrer');
});
});
});