|
<?php |
|
/** |
|
* Gutenberg Inline Toolbar: "THIN" Button (font-weight: 100) |
|
* |
|
* Adds a small "THIN" button to the contextual inline toolbar (the one with Bold/Italic) |
|
* in the block editor. When clicked (or via the shortcut), it wraps the current selection |
|
* inside <span data-fw="100" style='font-weight:100; font-variation-settings:"wght" 100;'>…</span>. |
|
* Clicking again on a selection that is fully wrapped will unwrap it (toggle behavior). |
|
* |
|
* Keyboard shortcut: Ctrl/Cmd + Alt + 1 |
|
* |
|
* Implementation notes |
|
* - Injects a button into every contextual inline toolbar instance (once per toolbar). |
|
* - Works only when the selection lies inside a contenteditable region in the editor. |
|
* - Keeps the selection after wrapping by reselecting the inserted span's contents. |
|
* - Also injects a minimal editor-side style so the weight is visible immediately. |
|
* |
|
* Usage |
|
* - Place this into a small plugin, mu-plugin, or use Code Snippets (PHP, admin only). |
|
*/ |
|
add_action('enqueue_block_editor_assets', function () { |
|
|
|
// Inject JS after the editor scripts to ensure editor globals are available. |
|
wp_add_inline_script('wp-block-editor', <<<'JS' |
|
(function(){ |
|
const BTN_LABEL = 'THIN'; // Button label shown in the inline toolbar |
|
// Attributes applied to the wrapping <span>. |
|
const WRAP_ATTR = { 'data-fw': '100', style: 'font-weight:100; font-variation-settings:"wght" 100;' }; |
|
|
|
// ---- Helpers --------------------------------------------------------------- |
|
// Safe accessor for the current DOM selection. |
|
const sel = () => (window.getSelection && window.getSelection()) || null; |
|
|
|
// Checks whether a Range is inside a contenteditable area of the editor. |
|
function insideEditable(range){ |
|
if (!range || !range.commonAncestorContainer) return false; |
|
const el = range.commonAncestorContainer.nodeType === 1 |
|
? range.commonAncestorContainer |
|
: range.commonAncestorContainer.parentElement; |
|
return !!(el && el.closest('[contenteditable="true"]')); |
|
} |
|
|
|
// Returns the closest <span data-fw="100"> wrapper for a node (or null). |
|
function nearestThinSpan(node){ |
|
if (!node) return null; |
|
const el = node.nodeType === 3 ? node.parentElement : node; |
|
return el ? el.closest('span[data-fw="100"]') : null; |
|
} |
|
|
|
// Removes a wrapper span but keeps its children in place. |
|
function unwrapSpan(span){ |
|
const parent = span.parentNode; |
|
while (span.firstChild) parent.insertBefore(span.firstChild, span); |
|
parent.removeChild(span); |
|
} |
|
|
|
// Wraps the current selection with a THIN span, or unwraps if already fully wrapped. |
|
function wrapSelectionThin(){ |
|
const s = sel(); |
|
if (!s || s.rangeCount === 0) return; |
|
const range = s.getRangeAt(0); |
|
if (!insideEditable(range) || range.collapsed) return; |
|
|
|
// Toggle behavior: if entire selection is within a single thin span -> unwrap it. |
|
const startThin = nearestThinSpan(s.anchorNode); |
|
const endThin = nearestThinSpan(s.focusNode); |
|
if (startThin && endThin && startThin === endThin && startThin.contains(range.cloneContents())) { |
|
unwrapSpan(startThin); |
|
return; |
|
} |
|
|
|
// Otherwise: wrap selection. |
|
const span = document.createElement('span'); |
|
Object.entries(WRAP_ATTR).forEach(([k,v])=> span.setAttribute(k,v)); |
|
const frag = range.extractContents(); |
|
span.appendChild(frag); |
|
range.insertNode(span); |
|
|
|
// Reselect the inserted content for a better UX. |
|
const r = document.createRange(); |
|
r.selectNodeContents(span); |
|
s.removeAllRanges(); |
|
s.addRange(r); |
|
} |
|
|
|
// ---- UI Injection ---------------------------------------------------------- |
|
// Inject a button into each inline toolbar (once per toolbar instance). |
|
const seen = new WeakSet(); |
|
function injectInto(toolbar){ |
|
if (seen.has(toolbar)) return; |
|
seen.add(toolbar); |
|
|
|
// Find the main group (next to Bold/Italic). Skip if button already added. |
|
const group = toolbar.querySelector('.components-toolbar-group, .components-toolbar'); |
|
if (!group || group.querySelector('[data-thin-btn]')) return; |
|
|
|
const btn = document.createElement('button'); |
|
btn.type = 'button'; |
|
btn.className = 'components-button has-icon'; |
|
btn.setAttribute('aria-label', 'Font weight 100'); |
|
btn.dataset.thinBtn = '1'; |
|
btn.textContent = BTN_LABEL; |
|
btn.style.fontSize = '11px'; |
|
btn.style.fontWeight = '600'; |
|
btn.style.letterSpacing = '0.6px'; |
|
btn.addEventListener('click', wrapSelectionThin); |
|
|
|
group.appendChild(btn); |
|
} |
|
|
|
// Observe DOM mutations to catch contextual toolbars appearing. |
|
const mo = new MutationObserver(mList => { |
|
mList.forEach(m => { |
|
m.addedNodes.forEach(n => { |
|
if (!(n instanceof HTMLElement)) return; |
|
// Contextual inline toolbar |
|
if (n.matches && n.matches('.block-editor-block-contextual-toolbar')) injectInto(n); |
|
n.querySelectorAll && n.querySelectorAll('.block-editor-block-contextual-toolbar').forEach(injectInto); |
|
}); |
|
}); |
|
}); |
|
mo.observe(document.body, { childList: true, subtree: true }); |
|
|
|
// Keyboard shortcut: Ctrl/Cmd + Alt + 1 |
|
document.addEventListener('keydown', (e) => { |
|
const mod = (e.ctrlKey || e.metaKey) && e.altKey && e.key === '1'; |
|
if (!mod) return; |
|
const s = sel(); |
|
if (!s || s.rangeCount === 0) return; |
|
if (!insideEditable(s.getRangeAt(0))) return; |
|
e.preventDefault(); |
|
wrapSelectionThin(); |
|
}); |
|
|
|
console.log('[THIN] inline toolbar button active'); |
|
})(); |
|
JS, 'after'); |
|
|
|
// Editor preview styles so the thin weight is visible immediately in the editor. |
|
wp_add_inline_style('wp-block-editor', |
|
'.editor-styles-wrapper [data-fw="100"]{font-weight:100 !important; font-variation-settings:"wght" 100 !important;}' |
|
); |
|
}); |