Skip to content

Commit

Permalink
Fix editable placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeSCahill committed Sep 4, 2024
1 parent e4eb8c0 commit d762323
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 90 deletions.
4 changes: 4 additions & 0 deletions preview-src/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Author Name

link:./cloud-api.html[API preview]

```bash
alias internal-rpk="kubectl --namespace <namespace> exec -i -t redpanda-0 -c redpanda -- rpk"
```

[.float-group]
--
image:multirepo-ssg.svg[Multirepo SSG,180,135,float=right,role=float-gap]
Expand Down
107 changes: 51 additions & 56 deletions src/js/11-editable-placeholders.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,59 @@ const REGEX_PREPROCESS_PUNCTUATION = /<span class="token punctuation">(\()<\/spa
const REGEX_CONUM_SPAN = /(\s\(<span class="token number">(\d+)<\/span>\)|(\s)\((\d+)\))$/gm;

function addPencilSpans() {
const editableSpans = document.querySelectorAll('[contenteditable="true"].editable');
// Get all code blocks that contain editable spans
const codeBlocks = document.querySelectorAll('pre code');

editableSpans.forEach(span => {
// Check if there's already a cursor after the current span
let nextSibling = span.nextElementSibling;
codeBlocks.forEach(codeBlock => {
const editableSpans = codeBlock.querySelectorAll('[contenteditable="true"].editable');

while (nextSibling && !nextSibling.textContent.trim() && !nextSibling.classList.contains('cursor')) {
const siblingToRemove = nextSibling;
nextSibling = nextSibling.nextElementSibling;
siblingToRemove.remove();
}
if (editableSpans.length === 0) return; // Skip if no editable spans found

// Add a pencil cursor if one doesn't exist
if (!nextSibling?.classList.contains('cursor')) {
const pencilSpan = document.createElement('span');
pencilSpan.className = 'fa fa-pencil cursor';
pencilSpan.setAttribute('aria-hidden', 'true');
span.insertAdjacentElement('afterend', pencilSpan);
}
});
}
// Create a DocumentFragment to batch DOM updates
const fragment = document.createDocumentFragment();

function processEditableSpans() {
const editableSpans = document.querySelectorAll('[contenteditable="true"].editable');
editableSpans.forEach(span => {
let parent = span.parentElement;

editableSpans.forEach(span => {
const codeParent = span.closest('code');
// Unnest if the contenteditable span is nested within other spans
while (parent && parent.tagName.toLowerCase() === 'span' && !parent.hasAttribute('contenteditable')) {
const grandParent = parent.parentElement;
grandParent.insertBefore(span, parent);

if (codeParent && span.parentElement !== codeParent) {
// Move the editable span to be a direct child of the code element
let currentParent = span.parentElement;
// If the parent becomes empty after unnesting, remove the parent element
if (parent.innerHTML.trim() === '' || parent.querySelector('.cursor')) {
parent.remove();
}

while (currentParent && currentParent !== codeParent) {
const grandParent = currentParent.parentElement;
parent = grandParent; // Move up the tree and repeat the check
}

// Insert the span directly into the codeParent
if (grandParent && grandParent === codeParent) {
grandParent.insertBefore(span, currentParent.nextSibling);
// Remove empty siblings that aren't cursors
let nextSibling = span.nextElementSibling;
while (nextSibling && !nextSibling.classList.contains('cursor')) {
if (nextSibling.innerHTML.trim() === '') {
const siblingToRemove = nextSibling;
nextSibling = nextSibling.nextElementSibling;
siblingToRemove.remove();
} else {
grandParent.insertBefore(span, currentParent);
}

// Remove the previous parent if it's empty and not needed
if (currentParent.textContent.trim() === '' && !currentParent.classList.contains('cursor')) {
currentParent.remove();
break;
}

// Move up the DOM tree
currentParent = span.parentElement;
}
}

// Remove any nested spans within the editable span itself
let textContent = '';
span.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
textContent += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SPAN') {
textContent += node.textContent;
}
// Check if the next sibling is already a cursor
if (nextSibling?.classList.contains('cursor')) return;

// Create and add the pencil cursor using the fragment
const pencilSpan = document.createElement('span');
pencilSpan.className = 'fa fa-pencil cursor';
pencilSpan.setAttribute('aria-hidden', 'true');
fragment.appendChild(pencilSpan);
span.insertAdjacentElement('afterend', pencilSpan);
});

// Clear all child nodes and set the cleaned text content
span.textContent = textContent.trim();
// Append the fragment to the DOM in one go
codeBlock.appendChild(fragment);
});

// Now, add pencil spans after ensuring the spans are properly nested
addPencilSpans();
}

(function () {
Expand Down Expand Up @@ -127,7 +112,7 @@ function processEditableSpans() {
makePlaceholdersEditable();
Prism && Prism.highlightAll();
// Remove any Prism markup injected inside editable spans.
processEditableSpans()
addPencilSpans()
} catch (error) {
console.error('An error occurred while making placeholders editable:', error);
}
Expand Down Expand Up @@ -172,8 +157,18 @@ function processEditableSpans() {
function unnestPlaceholders() {
const editables = document.querySelectorAll('[contenteditable="true"]');
editables.forEach(editable => {
if (editable.parentElement?.getAttribute('contenteditable') === 'true') {
editable.replaceWith(editable); // Unnest by replacing the parent with the child
let parent = editable.parentElement;
// If the parent is also contenteditable, move the child out of the nested structure
while (parent && parent.getAttribute('contenteditable') === 'true') {
const grandParent = parent.parentElement;
// Move the current editable element before the parent to "unnest" it
grandParent.insertBefore(editable, parent);
// If the parent becomes empty, remove the parent element
if (parent.childNodes.length === 0) {
parent.remove();
}
// Continue checking up the chain if the parent is also contenteditable
parent = grandParent;
}
});
}
Expand Down
73 changes: 39 additions & 34 deletions src/js/13-open-nested-tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,70 +11,75 @@
* - Update the URL when a tab is clicked to maintain the state of the selected tab.
* - Synchronize related tabs across the page.
* - Re-run Prism for syntax highlighting when tab content becomes visible.
* - Scroll to the selected tab or anchor smoothly.
* - Scroll to the selected tab.
*/

(function () {
'use strict'

function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}

const debouncedHighlightAll = debounce(() => {
const elementsToHighlight = document.querySelectorAll('.tabs.is-loaded pre.highlight');
elementsToHighlight.forEach((element) => {
Prism.plugins.lineNumbers.resize(element);
});
Prism.highlightAll();
}, 300); // Adjust debounce wait time as needed

const debouncedAddPencilSpans = debounce(addPencilSpans, 300);

window.addEventListener('DOMContentLoaded', function (event) {
// Get the current URL and extract the 'tab' query parameter or hash value
const url = new URL(window.location.href)
const tabId = url.searchParams.get('tab') || url.hash.replace('#', '')
const url = new URL(window.location.href);
const tabId = url.searchParams.get('tab') || url.hash.replace('#', '');

// Function to get the data-sync-group-id of the closest ancestor
function getClosestSyncGroupId(element) {
if (element) {
return element.closest('[data-sync-group-id]');
}
return null;
}

// If a tabId is present, simulate a click on the corresponding tab
if (tabId) {
// Set the hash anchor to the value of the 'tab' parameter
url.hash = `#${tabId}`;
// Remove the 'tab' parameter from the query string
url.searchParams.delete('tab');
// Update the browser's URL without reloading the page
window.history.replaceState({}, document.title, url.toString());
// Scroll to the element with the new hash anchor
const element = document.getElementById(tabId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
setTimeout(() => {
const tabToClick = document.getElementById(tabId)
const syncGroup = getClosestSyncGroupId(tabToClick)
const tabToClick = document.getElementById(tabId);
const syncGroup = getClosestSyncGroupId(tabToClick);
if (tabToClick) {
if (syncGroup && !syncGroup.classList.contains('is-loaded')) tabToClick.click() // Simulate the click event
if (syncGroup && !syncGroup.classList.contains('is-loaded')) tabToClick.click();
}
}, 0)
}, 0);
}
// Add click event listeners to all tabs
const tabs = document.querySelectorAll('li.tab')

const tabs = document.querySelectorAll('li.tab');
tabs.forEach(function (tab) {
tab.addEventListener('click', function (event) {
const currentTab = event.target.closest('li.tab')
const id = currentTab.id
const currentTab = event.target.closest('li.tab');
const id = currentTab.id;

// Update the hash fragment and the query parameter
const url = new URL(window.location.href)
url.hash = id
url.searchParams.set('tab', id)
window.history.pushState(null, null, url)
const url = new URL(window.location.href);
url.hash = id;
url.searchParams.set('tab', id);
window.history.pushState(null, null, url);
setTimeout(function() {
// Defer highlighting using requestIdleCallback
requestIdleCallback(function() {
const elementsToHighlight = document.querySelectorAll('.tabs.is-loaded pre.highlight');
elementsToHighlight.forEach((element) => {
Prism.plugins.lineNumbers.resize(element);
});
Prism.highlightAll()
addPencilSpans()
debouncedHighlightAll();
debouncedAddPencilSpans();
});
}, 0);
}, true)
})
})
})()
}, true);
});
});
})();

0 comments on commit d762323

Please sign in to comment.