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
code-block-pro / src / front / front.js

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

231 lines 7.4 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(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 };
60
61 const handleHighlighter = () => {
62 const codeBlocks = Array.from(
63 document.querySelectorAll(`${containerClass}:not(.cbp-hl-loaded)`),
64 );
65
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 }
78
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);
231