PluginProbe
Code Block Pro – Beautiful Syntax Highlighting / 1.28.0
Code Block Pro – Beautiful Syntax Highlighting v1.28.0
1.27.1 1.27.2 1.27.3 1.27.4 1.27.5 1.27.6 1.27.7 1.28.0 1.3.0 1.4.0 1.5.0 1.5.1 1.5.2 1.6.0 1.7.0 1.8.0 1.9.0 1.9.1 1.9.2 1.9.3 trunk 1.1.0 1.10.0 1.11.0 1.11.1 All 63 releases
← All changes | src/front/front.js +223 -77 1.9.21.28.0 View file →
@@ -1,84 +1,230 @@
1 1 import copy from 'copy-to-clipboard';
2 2
3 -// Handle copy button
4 -addEventListener('DOMContentLoaded', () => {
5 - const buttons = Array.from(
6 - document.querySelectorAll('.code-block-pro-copy-button'),
7 - );
8 - buttons.forEach((button) => {
9 - // Setting it to block here lets users deactivate the plugin safely
10 - button.style.display = 'block';
11 - button.addEventListener('click', (event) => {
12 - const b = event.target?.closest('span');
13 - copy(b?.dataset?.code ?? '', {
14 - format: 'text/plain',
15 - });
16 - b.classList.add('copying');
17 - setTimeout(() => {
18 - b.classList.remove('copying');
19 - }, 2000);
20 - });
21 - });
22 -});
3 +const containerClass = '.wp-block-kevinbatdorf-code-block-pro';
23 4
24 -// Handle highlighter
25 -addEventListener('DOMContentLoaded', () => {
26 - const codeBlocks = Array.from(
27 - document.querySelectorAll('.wp-block-kevinbatdorf-code-block-pro pre'),
28 - );
5 +const sanitize = (str) =>
6 + str
7 + .replace(/[\u2018\u2019]/g, "'") // single quotes
8 + .replace(/[\u201C\u201D]/g, '"') // double quotes
9 + .replace(/[\u2013\u2014]/g, '-') // en/em dash
10 + .replace(/\u2026/g, '...') // ellipsis
11 + .replace(/[\u2039\u203A\u00AB\u00BB]/g, (c) =>
12 + c === '‹' || c === '«' ? '<' : '>',
13 + )
14 + .replace(/\u00A0/g, ' '); // non-breaking space
29 15
30 - codeBlocks.forEach((codeBlock) => {
31 - // search for highlights
32 - const highlighters = codeBlock.querySelectorAll('.cbp-line-highlight');
33 - if (!highlighters.length) return;
16 +const handleCopyButton = () => {
17 + const buttons = Array.from(
18 + document.querySelectorAll(
19 + '.code-block-pro-copy-button:not(.cbp-cb-loaded)',
20 + ),
21 + );
22 + buttons.forEach((button) => {
23 + button.classList.add('cbp-cb-loaded');
24 + // Setting it to block here lets users deactivate the plugin safely
25 + button.style.display = 'block';
26 + const handler = (event) => {
27 + const { type, key, currentTarget: b } = event;
28 + // if keydown event, make sure it's enter or space
29 + if (type === 'keydown' && !['Enter', ' '].includes(key)) return;
30 + event.preventDefault();
31 + const t = b?.querySelector('textarea');
32 + const source = t ? t?.value : b?.dataset?.code;
33 + const code = b?.dataset?.encoded
34 + ? decodeURIComponent(decodeURIComponent(source))
35 + : source;
36 + const content = sanitize(window.cbpCopyOverride?.(code, button) ?? code);
37 + copy(content ?? '', {
38 + format: 'text/plain',
39 + onCopy: (code) => {
40 + window.cbpCopyCallback?.(code, button);
41 + b.classList.add('cbp-copying');
42 + // Check if there is a data-text-copied attribute
43 + const hasTextCopied = b.dataset.copiedText;
44 + const innerSpan = b.querySelector('span');
45 + if (hasTextCopied) innerSpan.innerText = hasTextCopied;
46 + setTimeout(() => {
47 + b.classList.remove('cbp-copying');
48 + if (hasTextCopied) {
49 + innerSpan.innerText = b.getAttribute('aria-label');
50 + }
51 + }, 2_000);
52 + },
53 + });
54 + };
55 + ['click', 'keydown'].forEach((evt) =>
56 + button.addEventListener(evt, handler),
57 + );
58 + });
59 +};
34 60
35 - // We need to track the block width so we can adjust the
36 - // highlighter width in case of overflow
37 - const resizeObserver = new ResizeObserver((entries) => {
38 - entries.forEach((entry) => {
39 - // get width of inner code block
40 - codeBlock.style.setProperty('--cbp-block-width', '0');
41 - const codeWidth =
42 - entry.target.querySelector('code').offsetWidth;
43 - const computed =
44 - codeWidth + 16 === entry.target.scrollWidth
45 - ? codeWidth + 16
46 - : entry.target.scrollWidth + 16;
47 - codeBlock.style.setProperty(
48 - '--cbp-block-width',
49 - `${computed}px`,
50 - );
51 - });
52 - });
53 - resizeObserver.observe(codeBlock);
61 +const handleHighlighter = () => {
62 + const codeBlocks = Array.from(
63 + document.querySelectorAll(`${containerClass}:not(.cbp-hl-loaded)`),
64 + );
54 65
55 - // add the highlighter
56 - highlighters.forEach((highlighter) => {
57 - highlighter.insertAdjacentHTML(
58 - 'beforeend',
59 - '<span aria-hidden="true" class="cbp-line-highlighter"></span>',
60 - );
61 - });
62 - });
63 -});
66 + codeBlocks.forEach((codeBlock) => {
67 + codeBlock.classList.add('cbp-hl-loaded');
68 + // Search for highlights
69 + const highlighters = new Set(
70 + codeBlock.querySelectorAll('.cbp-line-highlight'),
71 + );
72 + // If the codeblock has .cbp-highlight-hover, then get all lines
73 + if (codeBlock.classList.contains('cbp-highlight-hover')) {
74 + codeBlock
75 + .querySelectorAll('span.line')
76 + .forEach((line) => highlighters.add(line));
77 + }
64 78
65 -// Handle font loading
66 -addEventListener('DOMContentLoaded', () => {
67 - if (!window.codeBlockPro?.pluginUrl) return;
68 - const fontsToLoad = new Set(
69 - Array.from(
70 - document.querySelectorAll('[data-code-block-pro-font-family]') ??
71 - [],
72 - )
73 - .map((f) => f.dataset.codeBlockProFontFamily)
74 - .filter(Boolean),
75 - );
76 - [...fontsToLoad].forEach(async (fontName) => {
77 - const font = new FontFace(
78 - fontName,
79 - `url(${window.codeBlockPro.pluginUrl}/build/fonts/${fontName}.woff2)`,
80 - );
81 - await font.load();
82 - document.fonts.add(font);
83 - });
84 -});
79 + if (!highlighters.size) return;
80 +
81 + // If the code block expands, we need to recalculate the width
82 + new ResizeObserver(() => {
83 + // find the longest line
84 + const lines = codeBlock.querySelectorAll('span.line');
85 + codeBlock.style.setProperty('--cbp-block-width', 'unset');
86 + const longestLine = Array.from(lines).reduce((a, b) =>
87 + a.offsetWidth > b.offsetWidth ? a : b,
88 + );
89 + const highestLineHeight = Array.from(lines).reduce((a, b) =>
90 + a.offsetHeight > b.offsetHeight ? a : b,
91 + );
92 + codeBlock.style.setProperty(
93 + '--cbp-block-height',
94 + `${highestLineHeight.offsetHeight}px`,
95 + );
96 + codeBlock.style.setProperty(
97 + '--cbp-block-width',
98 + `${longestLine.offsetWidth}px`,
99 + );
100 + }).observe(codeBlock);
101 +
102 + // Add the highlighter if not already there
103 + highlighters.forEach((highlighter) => {
104 + if (highlighter.querySelector('.cbp-line-highlighter')) return;
105 + highlighter.insertAdjacentHTML(
106 + 'beforeend',
107 + '<span aria-hidden="true" class="cbp-line-highlighter"></span>',
108 + );
109 + });
110 + });
111 +};
112 +
113 +const handleFontLoading = () => {
114 + if (!window.codeBlockPro?.pluginUrl) return;
115 + const elements = Array.from(
116 + document.querySelectorAll(
117 + '[data-code-block-pro-font-family]:not(.cbp-ff-loaded)',
118 + ) || [],
119 + );
120 + elements.forEach((e) => e.classList.add('cbp-ff-loaded'));
121 + const fontsToLoad = new Set(
122 + elements.map((f) => f.dataset.codeBlockProFontFamily).filter(Boolean),
123 + );
124 + [...fontsToLoad].forEach(async (fontName) => {
125 + const [name, ext] = fontName.split('.');
126 + const url = `url(${window.codeBlockPro.pluginUrl}/build/fonts/${name}.${
127 + ext || 'woff2'
128 + })`;
129 + const font = new FontFace(name, url);
130 + await font.load().catch((e) => console.error(e));
131 + document.fonts.add(font);
132 + });
133 +};
134 +
135 +const handleSeeMore = () => {
136 + const seeMoreLines = Array.from(
137 + document.querySelectorAll(
138 + `${containerClass}:not(.cbp-see-more-loaded) .cbp-see-more-line`,
139 + ),
140 + );
141 + seeMoreLines.forEach((line) => {
142 + const currentContainer = line.closest(containerClass);
143 + currentContainer.classList.add('cbp-see-more-loaded');
144 + const pre = line.closest('pre');
145 + const initialHeight = pre.offsetHeight;
146 + let animationSpeed = 0;
147 + const transition = line.classList.contains('cbp-see-more-transition');
148 +
149 + if (transition) {
150 + const lineCount = pre.querySelectorAll('code > *').length;
151 + const linesBeforeCurrent = Array.from(
152 + line.closest('code').children,
153 + ).filter((l) => l.offsetTop < line.offsetTop)?.length;
154 + animationSpeed = 0.5 + (lineCount - linesBeforeCurrent) * 0.01;
155 + pre.style.transition = `max-height ${animationSpeed}s ease-out`;
156 + }
157 +
158 + // if the first child it a span then get the height of that span
159 + const headerHeight =
160 + currentContainer.children[0].tagName === 'SPAN'
161 + ? currentContainer.children[0].offsetHeight
162 + : 0;
163 + const lineHeight = parseFloat(window.getComputedStyle(line).lineHeight);
164 + pre.style.maxHeight = `${line.offsetTop + lineHeight - headerHeight}px`;
165 +
166 + const buttonContainer = line
167 + .closest(containerClass)
168 + .querySelector('.cbp-see-more-container');
169 + if (!buttonContainer) return;
170 + buttonContainer.style.display = 'flex';
171 + const button = buttonContainer.querySelector('.cbp-see-more-simple-btn');
172 + if (!button) return;
173 + // Starts off collapsed
174 + button.setAttribute('aria-expanded', 'false');
175 + pre.id = `cbp-see-more-${Math.random().toString(36).slice(2)}`;
176 + button.setAttribute('aria-controls', pre.id);
177 + if (currentContainer.classList.contains('padding-disabled')) {
178 + button.classList.remove('cbp-see-more-simple-btn-hover');
179 + }
180 +
181 + const handle = (event) => {
182 + event.preventDefault();
183 + // disable scrolling
184 + pre.style.setProperty('overflow', 'hidden', 'important');
185 + setTimeout(() => {
186 + pre.style.overflow = 'auto';
187 + }, animationSpeed * 1000);
188 +
189 + pre.style.maxHeight = `${initialHeight}px`;
190 + // If there is data-see-more-collapse-string then we toggle
191 + if (!buttonContainer.dataset?.seeMoreCollapseString) {
192 + buttonContainer.remove();
193 + return;
194 + }
195 + // We're letting them collapse it
196 + if (button.getAttribute('aria-expanded') === 'true') {
197 + button.setAttribute('aria-expanded', 'false');
198 + button.innerText = buttonContainer.dataset.seeMoreString;
199 + pre.style.maxHeight = `${line.offsetTop + lineHeight - headerHeight}px`;
200 +
201 + // Move the scroll so the button is in center view
202 + line.scrollIntoView({
203 + behavior: transition ? 'smooth' : 'auto',
204 + block: 'center',
205 + });
206 + return;
207 + }
208 + button.setAttribute('aria-expanded', 'true');
209 + button.innerText = buttonContainer.dataset.seeMoreCollapseString;
210 + };
211 + button.addEventListener('click', handle);
212 + button.addEventListener('keydown', (event) => {
213 + if (event.key === 'Enter') handle(event);
214 + });
215 + });
216 +};
217 +
218 +const init = () => {
219 + handleFontLoading();
220 + handleSeeMore();
221 + handleCopyButton();
222 + handleHighlighter();
223 +};
224 +
225 +// Functions are idempotent, so we can run them on load, DOMContentLoaded, et al.
226 +init();
227 +// Useful for when the DOM is modified or loaded in late
228 +window.codeBlockProInit = init;
229 +window.addEventListener('DOMContentLoaded', init);
230 +window.addEventListener('load', init);