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.
@@ -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';