PluginProbe
WP Coder – Insert & Manage Code Snippets / 4.5
WP Coder – Insert & Manage Code Snippets v4.5
4.5.1 1.1 2.3.1 2.3.2 2.4.1 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.1 3.1.1 3.2 3.2.1 3.3 3.4 3.5 3.5.1 All 39 releases
wp-coder / assets / js / codemirror / editor.js

editor.js in WP Coder – Insert & Manage Code Snippets 4.5, at assets/js/codemirror/editor.js

361 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 'use strict';
2
3 jQuery(function ($) {
4
5 // Init CodeMirror for a given textarea id (e.g., 'content')
6 function initCM(editorId) {
7 const ta = document.getElementById(editorId);
8 if (!ta || !window.wp || !wp.codeEditor) return null;
9
10 const settings = wp.codeEditor.defaultSettings ? _.clone(wp.codeEditor.defaultSettings) : {};
11 settings.codemirror = _.extend({}, settings.codemirror, {
12 mode: 'gfm', // GitHub Flavored Markdown
13 lineNumbers: false,
14 lineWrapping: true,
15 viewportMargin: Infinity, // render all content (no inner scroll)
16 styleActiveLine: true,
17 matchBrackets: true,
18 autoCloseBrackets: true,
19 indentUnit: 2,
20 tabSize: 2,
21 // Bind toggleComment only if addon is loaded
22 extraKeys: (function () {
23 const ok = window.CodeMirror && CodeMirror.commands && CodeMirror.commands.toggleComment;
24 return ok ? {'Ctrl-/': 'toggleComment', 'Cmd-/': 'toggleComment'} : {};
25 })()
26 });
27
28 const editor = wp.codeEditor.initialize(ta, settings);
29 return editor.codemirror;
30 }
31
32 // When QuickTags is ready, toolbar exists and editorId is known
33 $(document).on('quicktags-init', function (_e, qt) {
34 ['del', 'ins', 'li', 'more', 'close'].forEach(id => {
35 const btn = document.getElementById('qt_' + qt.id + '_' + id);
36 if (btn) btn.remove();
37 });
38
39 const editorId = qt.id; // e.g., 'content' or custom id
40
41 if (editorId !== 'content') return;
42
43 // 1) Ensure CodeMirror is initialized
44 let cm = wp.codeEditor?.editors?.[editorId]?.codemirror;
45 if (!cm) cm = initCM(editorId);
46 if (!cm) return;
47
48 // 2) Give space for QuickTags toolbar
49 const toolbar = document.getElementById('ed_toolbar');
50 const cmEl = document.querySelector('#wp-' + editorId + '-editor-container .CodeMirror');
51 if (cmEl && toolbar) cmEl.style.marginTop = toolbar.offsetHeight + 'px';
52
53 // 3) Rebind QuickTags buttons to Markdown actions (operate via CodeMirror)
54 function wrapSel(before, after = '') {
55 const d = cm.getDoc(), sel = d.getSelection();
56 d.replaceSelection(before + sel + after);
57 cm.focus();
58 }
59
60 function toggleQuote() {
61 const d = cm.getDoc(), from = d.getCursor('from'), to = d.getCursor('to');
62 for (let l = from.line; l <= to.line; l++) {
63 const txt = d.getLine(l);
64 const next = txt.startsWith('> ') ? txt.slice(2) : '> ' + txt;
65 d.replaceRange(next, {line: l, ch: 0}, {line: l, ch: txt.length});
66 }
67 cm.focus();
68 }
69
70 const btn = id => document.getElementById('qt_' + editorId + '_' + id);
71
72 const strong = btn('strong');
73 if (strong) {
74 // prevent default WP <b>…</b>
75 strong.removeAttribute('onclick');
76 strong.addEventListener('click', ev => {
77 ev.preventDefault();
78 ev.stopImmediatePropagation();
79 wrapSel('**', '**');
80 }, true);
81 }
82
83 const italic = btn('em');
84 if (italic) {
85 italic.removeAttribute('onclick');
86 italic.addEventListener('click', ev => {
87 ev.preventDefault();
88 ev.stopImmediatePropagation();
89 wrapSel('*', '*');
90 }, true);
91 }
92
93 const link = btn('link');
94 if (link) {
95 link.removeAttribute('onclick');
96 link.addEventListener('click', ev => {
97 ev.preventDefault();
98 ev.stopImmediatePropagation();
99 wrapSel('[', '](https://)');
100 }, true);
101 }
102
103 const block = btn('block'); // quote
104 if (block) {
105 block.removeAttribute('onclick');
106 block.addEventListener('click', ev => {
107 ev.preventDefault();
108 ev.stopImmediatePropagation();
109 toggleQuote();
110 }, true);
111 }
112
113 const code = btn('code');
114 if (code) {
115 code.removeAttribute('onclick');
116 code.addEventListener('click', ev => {
117 ev.preventDefault();
118 ev.stopImmediatePropagation();
119 wrapSel('`', '`');
120 }, true);
121 }
122
123 // Усередині quicktags-init
124 const ul = btn('ul');
125 if (ul) {
126 ul.removeAttribute('onclick');
127 ul.addEventListener('click', ev => {
128 ev.preventDefault();
129 ev.stopImmediatePropagation();
130 const d = cm.getDoc();
131 const from = d.getCursor('from'), to = d.getCursor('to');
132 for (let l = from.line; l <= to.line; l++) {
133 const txt = d.getLine(l);
134 // toggle: якщо вже починається з "- " → забрати
135 const newTxt = txt.startsWith('* ') ? txt.slice(2) : '* ' + txt;
136 d.replaceRange(newTxt, {line: l, ch: 0}, {line: l, ch: txt.length});
137 }
138 cm.focus();
139 }, true);
140 }
141
142 const ol = btn('ol');
143 if (ol) {
144 ol.removeAttribute('onclick');
145 ol.addEventListener('click', ev => {
146 ev.preventDefault();
147 ev.stopImmediatePropagation();
148 const d = cm.getDoc();
149 const from = d.getCursor('from'), to = d.getCursor('to');
150 let num = 1;
151 for (let l = from.line; l <= to.line; l++) {
152 const txt = d.getLine(l);
153 const match = txt.match(/^(\d+)\.\s+/);
154 let newTxt;
155 if (match) {
156 newTxt = txt.replace(/^\d+\.\s+/, ''); // remove numbering
157 } else {
158 newTxt = (num++) + '. ' + txt;
159 }
160 d.replaceRange(newTxt, {line: l, ch: 0}, {line: l, ch: txt.length});
161 }
162 cm.focus();
163 }, true);
164 }
165
166 const img = btn('img');
167 if (img) {
168 img.removeAttribute('onclick');
169 img.addEventListener('click', ev => {
170 ev.preventDefault();
171 ev.stopImmediatePropagation();
172
173 const d = cm.getDoc();
174 const sel = d.getSelection();
175
176 // якщо виділено текст → підставимо як alt
177 const alt = sel || 'alt text';
178 const url = 'https://example.com/image.jpg';
179
180 d.replaceSelection('![' + alt + '](' + url + ')');
181 cm.focus();
182 }, true);
183 }
184
185 // не дублювати кнопку
186 if (document.getElementById('qt_' + editorId + '_h')) {
187 return;
188 }
189
190 // helper: per-line cycle heading level (0..5)
191 function cycleHeading() {
192 const d = cm.getDoc();
193 const from = d.getCursor('from'), to = d.getCursor('to');
194
195 for (let l = from.line; l <= to.line; l++) {
196 const txt = d.getLine(l);
197
198 // detect current level: 1..5 if "##### " present; else 0
199 const m = txt.match(/^(#{1,5})\s+(.*)$/);
200 const currLevel = m ? m[1].length : 0;
201 const content = m ? m[2] : txt;
202
203 // next: 0→1, 1→2, 2→3, 3→4, 4→5, 5→0
204 const nextLevel = (currLevel === 5) ? 0 : (currLevel + 1);
205
206 const nextText = nextLevel === 0 ? content : ('#'.repeat(nextLevel) + ' ' + content);
207
208 d.replaceRange(nextText, {line: l, ch: 0}, {line: l, ch: txt.length});
209 }
210 cm.focus();
211 }
212
213 const hBtn = document.createElement('input');
214 hBtn.type = 'button';
215 hBtn.id = 'qt_' + editorId + '_h';
216 hBtn.className = 'ed_button button button-small';
217 hBtn.value = 'H';
218 hBtn.ariaLabel = 'Heading cycle';
219
220 hBtn.addEventListener('click', function (ev) {
221 ev.preventDefault();
222 ev.stopImmediatePropagation();
223 cycleHeading();
224 }, true);
225
226 toolbar.appendChild(hBtn);
227
228 if (!document.getElementById('qt_' + editorId + '_strike')) {
229 const strike = document.createElement('input');
230 strike.type = 'button';
231 strike.id = 'qt_' + editorId + '_strike';
232 strike.className = 'ed_button button button-small';
233 strike.value = 'S';
234 strike.ariaLabel = 'Strikethrough';
235 strike.addEventListener('click', function (ev) {
236 ev.preventDefault();
237 ev.stopImmediatePropagation();
238 wrapSel('~~', '~~');
239 }, true);
240 toolbar.appendChild(strike);
241 }
242
243 // --- Multi-line code block: ``` ---
244 if (!document.getElementById('qt_' + editorId + '_codeblock')) {
245 const b = document.createElement('input');
246 b.type = 'button';
247 b.id = 'qt_' + editorId + '_codeblock';
248 b.className = 'ed_button button button-small';
249 b.value = '```';
250 b.ariaLabel = 'Code block';
251 b.addEventListener('click', function (ev) {
252 ev.preventDefault();
253 ev.stopImmediatePropagation();
254 const d = cm.getDoc();
255 const sel = d.getSelection();
256 const trimmed = sel.trim();
257 const isFenced = /^```/.test(trimmed) && /```$/.test(trimmed);
258 if (sel && isFenced) {
259 // strip fences
260 const un = trimmed.replace(/^```[a-z0-9-]*\n?/i, '').replace(/\n?```$/, '');
261 d.replaceSelection(un);
262 } else {
263 const lang = ''; // set e.g. 'php', 'js' if needed
264 d.replaceSelection('```' + lang + '\n' + (sel || '') + '\n```');
265 }
266 cm.focus();
267 }, true);
268 toolbar.appendChild(b);
269 }
270
271 // --- Horizontal rule: --- ---
272 if (!document.getElementById('qt_' + editorId + '_hr')) {
273 const b = document.createElement('input');
274 b.type = 'button';
275 b.id = 'qt_' + editorId + '_hr';
276 b.className = 'ed_button button button-small';
277 b.value = 'hr';
278 b.ariaLabel = 'Horizontal rule';
279 b.addEventListener('click', function (ev) {
280 ev.preventDefault();
281 ev.stopImmediatePropagation();
282 const d = cm.getDoc();
283 const pos = d.getCursor();
284 d.replaceRange('\n\n---\n\n', pos);
285 cm.focus();
286 }, true);
287 toolbar.appendChild(b);
288 }
289
290 // --- Table template ---
291 if (!document.getElementById('qt_' + editorId + '_table')) {
292 const b = document.createElement('input');
293 b.type = 'button';
294 b.id = 'qt_' + editorId + '_table';
295 b.className = 'ed_button button button-small';
296 b.value = 'tbl';
297 b.ariaLabel = 'Table';
298 b.addEventListener('click', function (ev) {
299 ev.preventDefault();
300 ev.stopImmediatePropagation();
301 const d = cm.getDoc();
302 const tpl = [
303 '| Header 1 | Header 2 |',
304 '|----------|----------|',
305 '| Cell 1 | Cell 2 |'
306 ].join('\n');
307 const pos = d.getCursor();
308 d.replaceRange('\n\n' + tpl + '\n\n', pos);
309 cm.focus();
310 }, true);
311 toolbar.appendChild(b);
312 }
313
314 // EN: Keep WP word-count in sync with CodeMirror; also show char stats in title
315 function hookCounters(editorId, cm){
316 const ta = document.getElementById(editorId);
317 const cell = document.getElementById('wp-word-count');
318 const span = cell ? cell.querySelector('.word-count') : null;
319
320 // EN: word counter (Intl.Segmenter if available, else unicode regex)
321 const wordCount = (s) => {
322 if (window.Intl?.Segmenter) {
323 let n = 0;
324 for (const seg of new Intl.Segmenter('uk', {granularity:'word'}).segment(s)) {
325 if (seg.isWordLike) n++;
326 }
327 return n;
328 }
329 const m = s.match(/[\p{L}\p{N}]+/gu); // letters+digits (unicode)
330 return m ? m.length : 0;
331 };
332
333 const update = () => {
334 const val = cm.getValue();
335
336 // sync textarea so autosave/revisions see it
337 if (ta) ta.value = val;
338
339 // update WP word-count cell
340 if (span) span.textContent = String(wordCount(val));
341
342 // optional: chars tooltip
343 const chars = val.length;
344 const charsNoWS = val.replace(/\s/g,'').length;
345 if (cell) cell.title = `Chars: ${chars} (no ws: ${charsNoWS})`;
346 };
347
348 // EN: debounce to avoid excessive work while typing
349 let t;
350 const debounced = () => { clearTimeout(t); t = setTimeout(update, 120); };
351
352 cm.off('change', debounced); // safety
353 cm.on('change', debounced);
354 update();
355 }
356
357 hookCounters(editorId, cm);
358
359
360 });
361 });