PluginProbe
Code Block Pro – Beautiful Syntax Highlighting / 1.27.4
Code Block Pro – Beautiful Syntax Highlighting v1.27.4
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
code-block-pro / src / front / front.js

front.js in Code Block Pro – Beautiful Syntax Highlighting 1.27.4, at src/front/front.js

235 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import copy from 'copy-to-clipboard';
2
3 const containerClass = '.wp-block-kevinbatdorf-code-block-pro';
4
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
15
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(
37 window.cbpCopyOverride?.(code, button) ?? code,
38 );
39 copy(content ?? '', {
40 format: 'text/plain',
41 onCopy: (code) => {
42 window.cbpCopyCallback?.(code, button);
43 b.classList.add('cbp-copying');
44 // Check if there is a data-text-copied attribute
45 const hasTextCopied = b.dataset.copiedText;
46 const innerSpan = b.querySelector('span');
47 if (hasTextCopied) innerSpan.innerText = hasTextCopied;
48 setTimeout(() => {
49 b.classList.remove('cbp-copying');
50 if (hasTextCopied) {
51 innerSpan.innerText = b.getAttribute('aria-label');
52 }
53 }, 2_000);
54 },
55 });
56 };
57 ['click', 'keydown'].forEach((evt) =>
58 button.addEventListener(evt, handler),
59 );
60 });
61 };
62
63 const handleHighlighter = () => {
64 const codeBlocks = Array.from(
65 document.querySelectorAll(`${containerClass}:not(.cbp-hl-loaded)`),
66 );
67
68 codeBlocks.forEach((codeBlock) => {
69 codeBlock.classList.add('cbp-hl-loaded');
70 // Search for highlights
71 const highlighters = new Set(
72 codeBlock.querySelectorAll('.cbp-line-highlight'),
73 );
74 // If the codeblock has .cbp-highlight-hover, then get all lines
75 if (codeBlock.classList.contains('cbp-highlight-hover')) {
76 codeBlock
77 .querySelectorAll('span.line')
78 .forEach((line) => highlighters.add(line));
79 }
80
81 if (!highlighters.size) return;
82
83 // If the code block expands, we need to recalculate the width
84 new ResizeObserver(() => {
85 // find the longest line
86 const lines = codeBlock.querySelectorAll('span.line');
87 codeBlock.style.setProperty('--cbp-block-width', 'unset');
88 const longestLine = Array.from(lines).reduce((a, b) =>
89 a.offsetWidth > b.offsetWidth ? a : b,
90 );
91 const highestLineHeight = Array.from(lines).reduce((a, b) =>
92 a.offsetHeight > b.offsetHeight ? a : b,
93 );
94 codeBlock.style.setProperty(
95 '--cbp-block-height',
96 highestLineHeight.offsetHeight + 'px',
97 );
98 codeBlock.style.setProperty(
99 '--cbp-block-width',
100 longestLine.offsetWidth + 'px',
101 );
102 }).observe(codeBlock);
103
104 // Add the highlighter if not already there
105 highlighters.forEach((highlighter) => {
106 if (highlighter.querySelector('.cbp-line-highlighter')) return;
107 highlighter.insertAdjacentHTML(
108 'beforeend',
109 '<span aria-hidden="true" class="cbp-line-highlighter"></span>',
110 );
111 });
112 });
113 };
114
115 const handleFontLoading = () => {
116 if (!window.codeBlockPro?.pluginUrl) return;
117 const elements = Array.from(
118 document.querySelectorAll(
119 '[data-code-block-pro-font-family]:not(.cbp-ff-loaded)',
120 ) || [],
121 );
122 elements.forEach((e) => e.classList.add('cbp-ff-loaded'));
123 const fontsToLoad = new Set(
124 elements.map((f) => f.dataset.codeBlockProFontFamily).filter(Boolean),
125 );
126 [...fontsToLoad].forEach(async (fontName) => {
127 const [name, ext] = fontName.split('.');
128 const url = `url(${window.codeBlockPro.pluginUrl}/build/fonts/${name}.${
129 ext || 'woff2'
130 })`;
131 const font = new FontFace(name, url);
132 await font.load().catch((e) => console.error(e));
133 document.fonts.add(font);
134 });
135 };
136
137 const handleSeeMore = () => {
138 const seeMoreLines = Array.from(
139 document.querySelectorAll(
140 `${containerClass}:not(.cbp-see-more-loaded) .cbp-see-more-line`,
141 ),
142 );
143 seeMoreLines.forEach((line) => {
144 const currentContainer = line.closest(containerClass);
145 currentContainer.classList.add('cbp-see-more-loaded');
146 const pre = line.closest('pre');
147 const initialHeight = pre.offsetHeight;
148 let animationSpeed = 0;
149 const transition = line.classList.contains('cbp-see-more-transition');
150
151 if (transition) {
152 const lineCount = pre.querySelectorAll('code > *').length;
153 const linesBeforeCurrent = Array.from(
154 line.closest('code').children,
155 ).filter((l) => l.offsetTop < line.offsetTop)?.length;
156 animationSpeed = 0.5 + (lineCount - linesBeforeCurrent) * 0.01;
157 pre.style.transition = `max-height ${animationSpeed}s ease-out`;
158 }
159
160 // if the first child it a span then get the height of that span
161 const headerHeight =
162 currentContainer.children[0].tagName === 'SPAN'
163 ? currentContainer.children[0].offsetHeight
164 : 0;
165 const lineHeight = parseFloat(window.getComputedStyle(line).lineHeight);
166 pre.style.maxHeight = `${line.offsetTop + lineHeight - headerHeight}px`;
167
168 const buttonContainer = line
169 .closest(containerClass)
170 .querySelector('.cbp-see-more-container');
171 if (!buttonContainer) return;
172 buttonContainer.style.display = 'flex';
173 const button = buttonContainer.querySelector(
174 '.cbp-see-more-simple-btn',
175 );
176 if (!button) return;
177 // Starts off collapsed
178 button.setAttribute('aria-expanded', 'false');
179 pre.id = `cbp-see-more-${Math.random().toString(36).slice(2)}`;
180 button.setAttribute('aria-controls', pre.id);
181 if (currentContainer.classList.contains('padding-disabled')) {
182 button.classList.remove('cbp-see-more-simple-btn-hover');
183 }
184
185 const handle = (event) => {
186 event.preventDefault();
187 // disable scrolling
188 pre.style.setProperty('overflow', 'hidden', 'important');
189 setTimeout(() => {
190 pre.style.overflow = 'auto';
191 }, animationSpeed * 1000);
192
193 pre.style.maxHeight = `${initialHeight}px`;
194 // If there is data-see-more-collapse-string then we toggle
195 if (!buttonContainer.dataset?.seeMoreCollapseString) {
196 buttonContainer.remove();
197 return;
198 }
199 // We're letting them collapse it
200 if (button.getAttribute('aria-expanded') === 'true') {
201 button.setAttribute('aria-expanded', 'false');
202 button.innerText = buttonContainer.dataset.seeMoreString;
203 pre.style.maxHeight = `${line.offsetTop + lineHeight - headerHeight}px`;
204
205 // Move the scroll so the button is in center view
206 line.scrollIntoView({
207 behavior: transition ? 'smooth' : 'auto',
208 block: 'center',
209 });
210 return;
211 }
212 button.setAttribute('aria-expanded', 'true');
213 button.innerText = buttonContainer.dataset.seeMoreCollapseString;
214 };
215 button.addEventListener('click', handle);
216 button.addEventListener('keydown', (event) => {
217 if (event.key === 'Enter') handle(event);
218 });
219 });
220 };
221
222 const init = () => {
223 handleFontLoading();
224 handleSeeMore();
225 handleCopyButton();
226 handleHighlighter();
227 };
228
229 // Functions are idempotent, so we can run them on load, DOMContentLoaded, et al.
230 init();
231 // Useful for when the DOM is modified or loaded in late
232 window.codeBlockProInit = init;
233 window.addEventListener('DOMContentLoaded', init);
234 window.addEventListener('load', init);
235