ckeditor-math-chem-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/package.json +46 -0
- package/src/Ckeditor.jsx +6393 -0
- package/src/CustomMathEditor.css +1932 -0
- package/src/SpecialCharacterModal.css +140 -0
- package/src/SpecialCharacterModal.jsx +158 -0
- package/src/index.js +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
Special Character Modal — MathType Style
|
|
3
|
+
============================================================ */
|
|
4
|
+
|
|
5
|
+
.scm-overlay { position: fixed; inset: 0; z-index: 100000; }
|
|
6
|
+
|
|
7
|
+
.scm-modal {
|
|
8
|
+
position: absolute;
|
|
9
|
+
width: 270px;
|
|
10
|
+
height: 274px;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
border: 1px solid #8fa4b3;
|
|
15
|
+
border-radius: 4px;
|
|
16
|
+
background: linear-gradient(180deg, #e3ebf1 0%, #d0dbe4 100%);
|
|
17
|
+
box-shadow: 0 12px 28px rgba(15, 26, 35, 0.28);
|
|
18
|
+
font-family: "Segoe UI", Tahoma, sans-serif;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.scm-toolbar {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 6px;
|
|
25
|
+
padding: 5px 8px;
|
|
26
|
+
background: linear-gradient(180deg, #e8eef3 0%, #d8e2ea 100%);
|
|
27
|
+
border-bottom: 1px solid #a3b4c0;
|
|
28
|
+
box-sizing: border-box;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.scm-code-label {
|
|
32
|
+
font-size: 12px;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
color: #6d88a2;
|
|
35
|
+
flex-shrink: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.scm-code-input {
|
|
39
|
+
width: 68px;
|
|
40
|
+
height: 21px;
|
|
41
|
+
border: 1px solid #657887;
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
font-size: 11px;
|
|
44
|
+
outline: none;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
background: #fdfefe;
|
|
47
|
+
color: #22343d;
|
|
48
|
+
padding: 0 7px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.scm-code-input:focus {
|
|
52
|
+
border-color: #597086;
|
|
53
|
+
box-shadow: 0 0 0 2px rgba(125, 152, 170, 0.18);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.scm-category-select {
|
|
57
|
+
flex: 1;
|
|
58
|
+
height: 21px;
|
|
59
|
+
min-width: 96px;
|
|
60
|
+
padding: 0 7px;
|
|
61
|
+
border: 1px solid #657887;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
font-size: 11px;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
outline: none;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
background: linear-gradient(180deg, #ffffff 0%, #edf1f4 100%);
|
|
68
|
+
color: #22343d;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.scm-category-select:focus {
|
|
72
|
+
border-color: #597086;
|
|
73
|
+
box-shadow: 0 0 0 2px rgba(125, 152, 170, 0.18);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.scm-category-select option {
|
|
77
|
+
background: #f6f8fa;
|
|
78
|
+
color: #22343d;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.scm-body {
|
|
82
|
+
flex: 1;
|
|
83
|
+
padding: 8px 8px 10px;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
background: linear-gradient(180deg, #d9e2e8 0%, #c9d5de 100%);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.scm-grid-container {
|
|
89
|
+
height: 100%;
|
|
90
|
+
overflow-y: auto;
|
|
91
|
+
overflow-x: hidden;
|
|
92
|
+
padding: 0;
|
|
93
|
+
border: 1px solid #b2bdc6;
|
|
94
|
+
background: #f7f8fa;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.scm-grid-container::-webkit-scrollbar { width: 16px; }
|
|
98
|
+
.scm-grid-container::-webkit-scrollbar-track { background: #3d4044; }
|
|
99
|
+
.scm-grid-container::-webkit-scrollbar-thumb { background: #a8adb3; border-radius: 8px; border: 3px solid #3d4044; }
|
|
100
|
+
.scm-grid-container::-webkit-scrollbar-thumb:hover { background: #bcc2c7; }
|
|
101
|
+
|
|
102
|
+
.scm-category-group {
|
|
103
|
+
width: 100%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.scm-char-grid {
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
109
|
+
gap: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.scm-char-btn {
|
|
113
|
+
height: 24px;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
padding: 0;
|
|
119
|
+
border: 1px solid #d0d5da;
|
|
120
|
+
border-right: none;
|
|
121
|
+
border-bottom: none;
|
|
122
|
+
border-radius: 0;
|
|
123
|
+
background: #fbfbfc;
|
|
124
|
+
color: #6f89a3;
|
|
125
|
+
font-size: 17px;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
transition: background 0.1s, color 0.1s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.scm-char-btn:hover {
|
|
131
|
+
background: #eef4f8;
|
|
132
|
+
color: #38566f;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.scm-no-results {
|
|
136
|
+
text-align: center;
|
|
137
|
+
padding: 22px 10px;
|
|
138
|
+
color: #6b7f90;
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import './SpecialCharacterModal.css';
|
|
3
|
+
|
|
4
|
+
const CATEGORIES = [
|
|
5
|
+
{ id: 'All', name: 'All' },
|
|
6
|
+
{ id: 'Symbol', name: 'Symbol', ranges: [[0x2200, 0x22FF], [0x2600, 0x26FF], [0x2190, 0x21FF], [0x2700, 0x27BF]] },
|
|
7
|
+
{ id: 'Punctuation', name: 'Punctuation', ranges: [[0x0021, 0x002F], [0x003A, 0x0040], [0x005B, 0x0060], [0x007B, 0x007E], [0x2010, 0x2027], [0x2030, 0x205E], [0x2E00, 0x2E7F]] },
|
|
8
|
+
{ id: 'Letter', name: 'Letter', ranges: [[0x0041, 0x005A], [0x0061, 0x007A], [0x0370, 0x03FF], [0x0400, 0x04FF]] },
|
|
9
|
+
{ id: 'Mark', name: 'Mark', ranges: [[0x0300, 0x036F], [0x20D0, 0x20FF]] },
|
|
10
|
+
{ id: 'Number', name: 'Number', ranges: [[0x0030, 0x0039], [0x2150, 0x218F], [0x2070, 0x209F]] },
|
|
11
|
+
{ id: 'Phonetic', name: 'Phonetic', ranges: [[0x0250, 0x02AF], [0x1D00, 0x1D7F]] },
|
|
12
|
+
{ id: 'Other', name: 'Other', ranges: [[0x20A0, 0x20CF], [0x2100, 0x214F], [0x25A0, 0x25FF]] }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const generateCharacters = () => {
|
|
16
|
+
const chars = [];
|
|
17
|
+
CATEGORIES.slice(1).forEach(cat => {
|
|
18
|
+
cat.ranges.forEach(range => {
|
|
19
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
20
|
+
if (i >= 0x007F && i <= 0x009F) continue;
|
|
21
|
+
chars.push({
|
|
22
|
+
code: i.toString(16).toUpperCase().padStart(4, '0'),
|
|
23
|
+
char: String.fromCodePoint(i),
|
|
24
|
+
category: cat.id
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return chars;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ALL_CHARACTERS = generateCharacters();
|
|
33
|
+
|
|
34
|
+
export default function SpecialCharacterModal({ isOpen, onClose, onInsert, position, contained = false }) {
|
|
35
|
+
const [searchCode, setSearchCode] = useState('');
|
|
36
|
+
const [selectedCategory, setSelectedCategory] = useState('All');
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isOpen) {
|
|
40
|
+
setSearchCode('');
|
|
41
|
+
setSelectedCategory('All');
|
|
42
|
+
}
|
|
43
|
+
}, [isOpen]);
|
|
44
|
+
|
|
45
|
+
const filteredCharacters = useMemo(() => {
|
|
46
|
+
let filtered = ALL_CHARACTERS;
|
|
47
|
+
if (selectedCategory !== 'All') {
|
|
48
|
+
filtered = filtered.filter(c => c.category === selectedCategory);
|
|
49
|
+
}
|
|
50
|
+
if (searchCode.trim()) {
|
|
51
|
+
const codeUpper = searchCode.trim().toUpperCase();
|
|
52
|
+
filtered = filtered.filter(c => c.code.includes(codeUpper));
|
|
53
|
+
}
|
|
54
|
+
return filtered;
|
|
55
|
+
}, [selectedCategory, searchCode]);
|
|
56
|
+
|
|
57
|
+
const groupedCharacters = useMemo(() => {
|
|
58
|
+
if (selectedCategory !== 'All' && !searchCode.trim()) {
|
|
59
|
+
return { [selectedCategory]: filteredCharacters };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const groups = {};
|
|
63
|
+
filteredCharacters.forEach(c => {
|
|
64
|
+
if (!groups[c.category]) groups[c.category] = [];
|
|
65
|
+
groups[c.category].push(c);
|
|
66
|
+
});
|
|
67
|
+
return groups;
|
|
68
|
+
}, [filteredCharacters, selectedCategory, searchCode]);
|
|
69
|
+
|
|
70
|
+
const handleKeyDown = (e) => {
|
|
71
|
+
if (e.key === 'Escape') {
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (isOpen) {
|
|
78
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
79
|
+
}
|
|
80
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
81
|
+
}, [isOpen]);
|
|
82
|
+
|
|
83
|
+
if (!isOpen) return null;
|
|
84
|
+
|
|
85
|
+
let modalStyle = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' };
|
|
86
|
+
if (position) {
|
|
87
|
+
let x = position.x;
|
|
88
|
+
let y = position.y;
|
|
89
|
+
|
|
90
|
+
if (!contained) {
|
|
91
|
+
x -= 120; // Shift left so it aligns better under the button
|
|
92
|
+
|
|
93
|
+
if (x < 10) {
|
|
94
|
+
x = 10;
|
|
95
|
+
}
|
|
96
|
+
if (x + 230 > window.innerWidth) {
|
|
97
|
+
x = window.innerWidth - 240;
|
|
98
|
+
}
|
|
99
|
+
if (y + 200 > window.innerHeight) {
|
|
100
|
+
y = window.innerHeight - 210;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
modalStyle = { top: `${y}px`, left: `${x}px` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={`scm-overlay${contained ? ' scm-overlay--contained' : ''}`}
|
|
110
|
+
onMouseDown={(e) => { e.stopPropagation(); onClose(); }}
|
|
111
|
+
>
|
|
112
|
+
<div className="scm-modal" style={modalStyle} onMouseDown={(e) => e.stopPropagation()}>
|
|
113
|
+
<div className="scm-toolbar">
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
placeholder="Code"
|
|
117
|
+
value={searchCode}
|
|
118
|
+
onChange={(e) => setSearchCode(e.target.value)}
|
|
119
|
+
className="scm-code-input"
|
|
120
|
+
/>
|
|
121
|
+
<select
|
|
122
|
+
value={selectedCategory}
|
|
123
|
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
|
124
|
+
className="scm-category-select"
|
|
125
|
+
>
|
|
126
|
+
{CATEGORIES.map(cat => (
|
|
127
|
+
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
|
128
|
+
))}
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="scm-body">
|
|
133
|
+
<div className="scm-grid-container">
|
|
134
|
+
{Object.entries(groupedCharacters).map(([catName, chars]) => (
|
|
135
|
+
<div key={catName} className="scm-category-group">
|
|
136
|
+
<div className="scm-char-grid">
|
|
137
|
+
{chars.map(c => (
|
|
138
|
+
<button
|
|
139
|
+
key={c.code}
|
|
140
|
+
className="scm-char-btn"
|
|
141
|
+
onClick={() => { onInsert(c.char); onClose(); }}
|
|
142
|
+
title={`U+${c.code}`}
|
|
143
|
+
>
|
|
144
|
+
{c.char}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
{filteredCharacters.length === 0 && (
|
|
151
|
+
<div className="scm-no-results">No characters found.</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Ckeditor.jsx';
|