Skip to content

Commit b1d96aa

Browse files
committed
feat(editor): added source code snippet plugin for Jodit Editor (#3301)
1 parent 69f4f80 commit b1d96aa

File tree

9 files changed

+266
-188
lines changed

9 files changed

+266
-188
lines changed

phpmyfaq/admin/assets/src/content/editor.js

Lines changed: 12 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
import { Jodit } from 'jodit';
17+
1718
import 'jodit/esm/plugins/class-span/class-span.js';
1819
import 'jodit/esm/plugins/clean-html/clean-html.js';
1920
import 'jodit/esm/plugins/clipboard/clipboard.js';
@@ -28,6 +29,8 @@ import 'jodit/esm/plugins/indent/indent.js';
2829
import 'jodit/esm/plugins/justify/justify.js';
2930
import 'jodit/esm/plugins/line-height/line-height.js';
3031
import 'jodit/esm/plugins/media/media.js';
32+
import 'jodit/esm/plugins/paste-storage/paste-storage.js';
33+
import 'jodit/esm/plugins/paste-from-word/paste-from-word.js';
3134
import 'jodit/esm/plugins/preview/preview.js';
3235
import 'jodit/esm/plugins/print/print.js';
3336
import 'jodit/esm/plugins/resizer/resizer.js';
@@ -37,81 +40,8 @@ import 'jodit/esm/plugins/source/source.js';
3740
import 'jodit/esm/plugins/symbols/symbols.js';
3841
import 'jodit/esm/modules/uploader/uploader.js';
3942
import 'jodit/esm/plugins/video/video.js';
40-
41-
// Define the phpMyFAQ plugin
42-
Jodit.plugins.add('phpMyFAQ', (editor) => {
43-
// Register the button
44-
editor.registerButton({
45-
name: 'phpMyFAQ',
46-
});
47-
48-
// Register the command
49-
editor.registerCommand('phpMyFAQ', () => {
50-
const dialog = editor.dlg({ closeOnClickOverlay: true });
51-
52-
const content = `<form class="row row-cols-lg-auto g-3 align-items-center m-4">
53-
<div class="col-12">
54-
<label class="visually-hidden" for="pmf-search-internal-links">Search</label>
55-
<input type="text" class="form-control" id="pmf-search-internal-links" placeholder="Search">
56-
</div>
57-
</form>
58-
<div class="m-4" id="pmf-search-results"></div>
59-
<div class="m-4">
60-
<button type="button" class="btn btn-primary" id="select-faq-button">Select FAQ</button>
61-
</div>`;
62-
63-
dialog.setMod('theme', editor.o.theme).setHeader('phpMyFAQ Plugin').setContent(content);
64-
65-
dialog.open();
66-
67-
const searchInput = document.getElementById('pmf-search-internal-links');
68-
const resultsContainer = document.getElementById('pmf-search-results');
69-
const csrfToken = document.getElementById('pmf-csrf-token').value;
70-
const selectLink = document.getElementById('select-faq-button');
71-
72-
searchInput.addEventListener('keyup', () => {
73-
const query = searchInput.value;
74-
if (query.length > 0) {
75-
fetch('api/faq/search', {
76-
method: 'POST',
77-
headers: {
78-
'Content-Type': 'application/json',
79-
},
80-
body: JSON.stringify({
81-
search: query,
82-
csrf: csrfToken,
83-
}),
84-
})
85-
.then((response) => response.json())
86-
.then((data) => {
87-
resultsContainer.innerHTML = '';
88-
data.success.forEach((result) => {
89-
resultsContainer.innerHTML += `<label class="form-check-label">
90-
<input class="form-check-input" type="radio" name="faqURL" value="${result.url}">
91-
${result.question}
92-
</label><br>`;
93-
});
94-
})
95-
.catch((error) => console.error('Error:', error));
96-
} else {
97-
resultsContainer.innerHTML = '';
98-
}
99-
});
100-
101-
selectLink.addEventListener('click', () => {
102-
const selected = document.querySelector('input[name=faqURL]:checked');
103-
if (selected) {
104-
const url = selected.value;
105-
const question = selected.parentNode.textContent.trim();
106-
const anchor = `<a href="${url}">${question}</a>`;
107-
editor.selection.insertHTML(anchor);
108-
dialog.close();
109-
} else {
110-
alert('Please select an FAQ.');
111-
}
112-
});
113-
});
114-
});
43+
import '../plugins/phpmyfaq/phpmyfaq.js';
44+
import '../plugins/code-snippet/code-snippet.js';
11545

11646
export const renderEditor = () => {
11747
const editor = document.getElementById('editor');
@@ -134,13 +64,17 @@ export const renderEditor = () => {
13464
minHeight: 100,
13565
direction: '',
13666
language: 'auto',
137-
debugLanguage: false,
67+
debugLanguage: true,
13868
i18n: 'en',
13969
tabIndex: -1,
14070
toolbar: true,
14171
enter: 'P',
14272
defaultMode: Jodit.MODE_WYSIWYG,
73+
highlightMode: true,
14374
useSplitMode: false,
75+
askBeforePasteFromWord: true,
76+
processPasteFromWord: true,
77+
defaultActionOnPasteFromWord: 'insert_clear_html',
14478
colors: {
14579
greyscale: [
14680
'#000000',
@@ -233,7 +167,7 @@ export const renderEditor = () => {
233167
imageDefaultWidth: 300,
234168
removeButtons: [],
235169
disablePlugins: [],
236-
extraPlugins: ['phpMyFAQ'],
170+
extraPlugins: ['phpMyFAQ', 'codeSnippet'],
237171
extraButtons: [],
238172
buttons: [
239173
'source',
@@ -285,6 +219,7 @@ export const renderEditor = () => {
285219
'print',
286220
'|',
287221
'phpMyFAQ',
222+
'codeSnippet',
288223
],
289224
events: {},
290225
textIcons: false,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Jodit } from 'jodit';
2+
import codeSnippet from '../code-snippet/code-snippet.svg.js';
3+
4+
Jodit.modules.Icon.set('codeSnippet', codeSnippet);
5+
6+
Jodit.plugins.add('codeSnippet', (editor) => {
7+
// Register the button
8+
editor.registerButton({
9+
name: 'codeSnippet',
10+
group: 'insert',
11+
icon: 'codeSnippet',
12+
text: 'Insert Source Code Snippet',
13+
});
14+
15+
// Register the command
16+
editor.registerCommand('codeSnippet', () => {
17+
const dialog = editor.dlg({ closeOnClickOverlay: true });
18+
19+
const content = `<form class="row m-4">
20+
<div class="col-12 mb-2">
21+
<label class="visually-hidden" for="programming-language">Programming language</label>
22+
<select class="form-select" id="programming-language" name="programming-language">
23+
<option value="plaintext">Plain Text</option>
24+
<option value="bash">Bash</option>
25+
<option value="c">C</option>
26+
<option value="cpp">C++</option>
27+
<option value="css">CSS</option>
28+
<option value="html">HTML</option>
29+
<option value="java">Java</option>
30+
<option value="javascript">JavaScript</option>
31+
<option value="json">JSON</option>
32+
<option value="php">PHP</option>
33+
<option value="python">Python</option>
34+
<option value="ruby">Ruby</option>
35+
<option value="sql">SQL</option>
36+
<option value="typescript">TypeScript</option>
37+
<option value="xml">XML</option>
38+
</select>
39+
</div>
40+
<div class="col-12 mb-2">
41+
<label class="visually-hidden" for="code">Source code</label>
42+
<textarea class="form-control" id="code" rows="15" placeholder="Paste your source code snippet here"></textarea>
43+
</div>
44+
<div class="col-12">
45+
<button type="button" class="btn btn-primary text-end" id="add-code-snippet-button">
46+
Add source code snippet
47+
</button>
48+
</div>
49+
</form>`;
50+
51+
dialog
52+
.setMod('theme', editor.o.theme)
53+
.setHeader('Insert Source Code Snippet')
54+
.setContent(content)
55+
.setSize(Math.min(900, screen.width), Math.min(640, screen.width));
56+
57+
dialog.open();
58+
59+
const addCodeSnippetButton = document.getElementById('add-code-snippet-button');
60+
const language = document.getElementById('programming-language');
61+
const code = document.getElementById('code');
62+
63+
const encodeHTML = (str) => {
64+
return str
65+
.replace(/&/g, '&amp;')
66+
.replace(/</g, '&lt;')
67+
.replace(/>/g, '&gt;')
68+
.replace(/"/g, '&quot;')
69+
.replace(/'/g, '&#039;');
70+
};
71+
72+
addCodeSnippetButton.addEventListener('click', () => {
73+
const selectedLanguage = language.value;
74+
const selectedCode = code.value;
75+
const codeSnippet = `<pre><code class="language-${selectedLanguage}">${encodeHTML(selectedCode)}</code></pre>`;
76+
editor.selection.insertHTML(codeSnippet);
77+
dialog.close();
78+
});
79+
});
80+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const codeSnippet = `<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"
2+
preserveAspectRatio="xMidYMid meet">
3+
4+
<rect width="100%" height="100%" fill="none" />
5+
6+
<g transform="translate(0,128) scale(0.100000,-0.100000)"
7+
fill="#000000" stroke="none">
8+
<path d="M261 1173 c-24 -9 -63 -35 -86 -58 -60 -59 -75 -112 -75 -261 0 -67
9+
-3 -129 -6 -138 -3 -9 -23 -20 -44 -26 -41 -11 -56 -35 -45 -70 4 -12 21 -24
10+
45 -30 21 -6 41 -17 44 -26 3 -9 6 -71 6 -138 0 -149 15 -202 75 -261 53 -53
11+
115 -75 216 -75 82 0 109 12 109 50 0 37 -27 50 -106 50 -95 0 -145 23 -174
12+
80 -17 33 -20 59 -20 169 0 102 -4 138 -17 166 -16 33 -16 37 0 70 13 28 17
13+
64 17 166 0 110 3 136 20 169 29 57 79 80 174 80 79 0 106 13 106 50 0 10 -7
14+
26 -16 34 -21 21 -162 20 -223 -1z"/>
15+
<path d="M806 1174 c-9 -8 -16 -24 -16 -34 0 -37 27 -50 101 -50 89 0 141 -24
16+
169 -80 17 -33 20 -59 20 -169 0 -102 4 -138 17 -166 16 -33 16 -37 0 -70 -13
17+
-28 -17 -64 -17 -166 0 -110 -3 -136 -20 -169 -28 -56 -80 -80 -169 -80 -74 0
18+
-101 -13 -101 -50 0 -37 27 -50 104 -50 95 0 159 23 211 75 60 59 75 112 75
19+
261 0 67 3 129 6 138 3 9 23 20 44 26 67 18 67 82 0 100 -21 6 -41 17 -44 26
20+
-3 9 -6 71 -6 138 0 88 -5 136 -17 171 -21 63 -85 127 -148 148 -64 22 -188
21+
23 -209 1z"/>
22+
<path d="M316 974 c-9 -8 -16 -24 -16 -34 0 -36 27 -50 100 -50 73 0 100 14
23+
100 50 0 36 -27 50 -100 50 -49 0 -73 -4 -84 -16z"/>
24+
<path d="M616 974 c-9 -8 -16 -24 -16 -34 0 -44 22 -50 190 -50 131 0 161 3
25+
174 16 20 20 20 48 0 68 -23 24 -325 24 -348 0z"/>
26+
<path d="M316 774 c-9 -8 -16 -24 -16 -34 0 -44 22 -50 190 -50 131 0 161 3
27+
174 16 20 20 20 48 0 68 -23 24 -325 24 -348 0z"/>
28+
<path d="M796 774 c-9 -8 -16 -24 -16 -34 0 -36 27 -50 100 -50 73 0 100 14
29+
100 50 0 36 -27 50 -100 50 -49 0 -73 -4 -84 -16z"/>
30+
<path d="M316 574 c-9 -8 -16 -24 -16 -34 0 -36 27 -50 100 -50 73 0 100 14
31+
100 50 0 36 -27 50 -100 50 -49 0 -73 -4 -84 -16z"/>
32+
<path d="M616 574 c-9 -8 -16 -24 -16 -34 0 -44 22 -50 190 -50 131 0 161 3
33+
174 16 20 20 20 48 0 68 -23 24 -325 24 -348 0z"/>
34+
<path d="M316 374 c-20 -20 -20 -48 0 -68 23 -24 325 -24 348 0 20 20 20 48 0
35+
68 -23 24 -325 24 -348 0z"/>
36+
<path d="M796 374 c-9 -8 -16 -24 -16 -34 0 -36 27 -50 100 -50 73 0 100 14
37+
100 50 0 36 -27 50 -100 50 -49 0 -73 -4 -84 -16z"/>
38+
</g>
39+
</svg>`;
40+
41+
export default codeSnippet;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Jodit Editor plugin to fetch and insert internal links via REST
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public License,
5+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
6+
* obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* @package phpMyFAQ
9+
* @author Thorsten Rinne <[email protected]>
10+
* @copyright 2025 phpMyFAQ Team
11+
* @license http://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
12+
* @link https://www.phpmyfaq.de
13+
* @since 2025-01-04
14+
*/
15+
16+
import { Jodit } from 'jodit';
17+
import { fetchFaqsByAutocomplete } from '../../api/index.js';
18+
import phpmyfaq from './phpmyfaq.svg.js';
19+
20+
Jodit.modules.Icon.set('phpmyfaq', phpmyfaq);
21+
22+
Jodit.plugins.add('phpMyFAQ', (editor) => {
23+
// Register the button
24+
editor.registerButton({
25+
name: 'phpMyFAQ',
26+
group: 'insert',
27+
icon: 'phpmyfaq',
28+
});
29+
30+
// Register the command
31+
editor.registerCommand('phpMyFAQ', () => {
32+
const dialog = editor.dlg({ closeOnClickOverlay: true });
33+
34+
const content = `<form class="row row-cols-lg-auto g-3 align-items-center m-4">
35+
<div class="col-12">
36+
<label class="visually-hidden" for="pmf-search-internal-links">Search</label>
37+
<input type="text" class="form-control" id="pmf-search-internal-links" placeholder="Search">
38+
</div>
39+
</form>
40+
<div class="m-4" id="pmf-search-results"></div>
41+
<div class="m-4">
42+
<button type="button" class="btn btn-primary" id="select-faq-button">Select FAQ</button>
43+
</div>`;
44+
45+
dialog
46+
.setMod('theme', editor.o.theme)
47+
.setHeader('phpMyFAQ Plugin')
48+
.setContent(content)
49+
.setSize(Math.min(900, screen.width), Math.min(640, screen.width));
50+
51+
dialog.open();
52+
53+
const searchInput = document.getElementById('pmf-search-internal-links');
54+
const resultsContainer = document.getElementById('pmf-search-results');
55+
const csrfToken = document.getElementById('pmf-csrf-token').value;
56+
const selectLink = document.getElementById('select-faq-button');
57+
58+
searchInput.addEventListener('keyup', async () => {
59+
const query = searchInput.value;
60+
if (query.length > 0) {
61+
try {
62+
const response = await fetchFaqsByAutocomplete(query, csrfToken);
63+
64+
resultsContainer.innerHTML = '';
65+
response.success.forEach((result) => {
66+
resultsContainer.innerHTML += `<label class="form-check-label">
67+
<input class="form-check-input" type="radio" name="faqURL" value="${result.url}">
68+
${result.question}
69+
</label><br>`;
70+
});
71+
} catch (error) {
72+
console.error('Error:', error);
73+
}
74+
} else {
75+
resultsContainer.innerHTML = '';
76+
}
77+
});
78+
79+
selectLink.addEventListener('click', () => {
80+
const selected = document.querySelector('input[name=faqURL]:checked');
81+
if (selected) {
82+
const url = selected.value;
83+
const question = selected.parentNode.textContent.trim();
84+
const anchor = `<a href="${url}">${question}</a>`;
85+
editor.selection.insertHTML(anchor);
86+
dialog.close();
87+
} else {
88+
alert('Please select an FAQ.');
89+
}
90+
});
91+
});
92+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const phpmyfaq = `
2+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 1500"
3+
preserveAspectRatio="xMidYMid meet">
4+
5+
<g transform="translate(0.000000,1500) scale(0.100000,-0.100000)"
6+
fill="#000000" stroke="none">
7+
<path d="M7410 14534 c-19 -2 -98 -9 -175 -14 -1187 -91 -2412 -550 -3235
8+
-1214 -290 -234 -841 -728 -1156 -1038 l-141 -138 49 -193 c280 -1104 430
9+
-1570 632 -1972 102 -201 160 -289 261 -391 129 -130 230 -165 347 -121 101
10+
39 274 261 536 687 221 360 435 607 707 816 645 494 1413 764 2177 764 514 0
11+
978 -101 1383 -300 304 -150 501 -305 705 -555 272 -335 452 -802 520 -1355
12+
32 -259 41 -756 20 -1080 -71 -1102 -278 -1970 -617 -2593 -375 -690 -1018
13+
-1315 -1868 -1814 -66 -39 -128 -80 -139 -92 -18 -20 -18 -22 4 -83 121 -326
14+
425 -936 583 -1167 29 -42 31 -43 71 -37 92 14 533 258 871 481 757 500 1512
15+
1213 2092 1975 406 535 701 1009 1002 1610 454 910 671 1586 746 2330 48 475
16+
42 1030 -15 1430 -112 771 -449 1569 -924 2180 -124 160 -213 259 -395 444
17+
-733 744 -1648 1202 -2736 1372 -335 52 -506 65 -900 69 -203 2 -386 1 -405
18+
-1z"/>
19+
<path d="M2415 10278 c-15 -46 -48 -169 -75 -275 -224 -880 -299 -1807 -215
20+
-2649 120 -1204 554 -2193 1323 -3018 711 -761 1665 -1225 2767 -1346 55 -6
21+
255 -14 444 -17 l344 -6 -6 68 c-9 90 -38 147 -112 220 -68 67 -116 101 -486
22+
344 -1010 663 -1625 1302 -2033 2111 -228 450 -344 837 -402 1330 -14 121 -15
23+
204 -10 500 6 369 11 449 47 662 l21 128 -50 62 c-50 65 -535 702 -1097 1443
24+
-353 465 -400 525 -420 525 -9 0 -24 -32 -40 -82z"/>
25+
<path d="M10335 3160 c-317 -20 -646 -135 -932 -327 -381 -255 -653 -633 -750
26+
-1043 -30 -131 -42 -383 -24 -510 58 -392 310 -743 671 -934 112 -58 254 -110
27+
365 -133 75 -15 137 -18 370 -18 246 1 292 3 377 22 219 48 629 246 813 393
28+
77 62 231 218 290 295 153 199 273 458 327 709 16 77 22 136 22 251 1 167 -12
29+
254 -56 395 -156 493 -644 858 -1200 895 -46 3 -101 7 -123 9 -22 1 -89 -1
30+
-150 -4z"/>
31+
</g>
32+
</svg>
33+
`;
34+
35+
export default phpmyfaq;

0 commit comments

Comments
 (0)