@riverflowpkg/riverflow 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 +51 -0
- package/bar/bar.js +90 -0
- package/bar/theCodes.html +144 -0
- package/editor/editor.js +312 -0
- package/editor/highlight.min.js +2441 -0
- package/editor/terminal.js +309 -0
- package/effects/typing.js +105 -0
- package/effects/underwater.js +117 -0
- package/package.json +32 -0
- package/scroll/scroll.css +91 -0
- package/scroll/scroll.js +244 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RiverFlowPkg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# riverflow
|
|
2
|
+
|
|
3
|
+
Drop-in vanilla JS browser widgets — no build step, no bundler. Grab a script,
|
|
4
|
+
drop it in a `<script>` tag, and it wires itself up.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npm install riverflow
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Files live in `node_modules/riverflow/`. Copy the ones you need into your
|
|
13
|
+
public/static folder, or reference them directly.
|
|
14
|
+
|
|
15
|
+
## Widgets
|
|
16
|
+
|
|
17
|
+
### Top loading bar (`bar/bar.js`)
|
|
18
|
+
```html
|
|
19
|
+
<script>window.BarConfig = { color: '#00c9ff' };</script>
|
|
20
|
+
<script src="bar/bar.js"></script>
|
|
21
|
+
```
|
|
22
|
+
Optional config: `color`, `gradient`, `height`, `duration`.
|
|
23
|
+
|
|
24
|
+
### Smooth scroll (`scroll/scroll.js`)
|
|
25
|
+
```html
|
|
26
|
+
<body scroll-speed="0.6">
|
|
27
|
+
<script src="scroll/scroll.js"></script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Typing effect (`effects/typing.js`)
|
|
31
|
+
```html
|
|
32
|
+
<div class="typing">Hello world</div>
|
|
33
|
+
<script src="effects/typing.js"></script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Underwater overlay (`effects/underwater.js`)
|
|
37
|
+
```html
|
|
38
|
+
<div class="effect-underwater"></div>
|
|
39
|
+
<script src="effects/underwater.js"></script>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Code editor (`editor/editor.js`)
|
|
43
|
+
Syntax-highlighted code editor built on highlight.js (bundled as
|
|
44
|
+
`editor/highlight.min.js`).
|
|
45
|
+
```html
|
|
46
|
+
<script src="editor/editor.js"></script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/bar/bar.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
|
|
3
|
+
/* ── Config ─────────────────────────────────────────────────── */
|
|
4
|
+
/* Developer sets window.BarConfig before loading this script.
|
|
5
|
+
Example:
|
|
6
|
+
window.BarConfig = { color: '#ff6b6b' }
|
|
7
|
+
window.BarConfig = { gradient: 'linear-gradient(to right, #f093fb, #f5576c)' }
|
|
8
|
+
window.BarConfig = { height: 3, color: '#00c9ff' }
|
|
9
|
+
*/
|
|
10
|
+
const cfg = window.BarConfig || {};
|
|
11
|
+
const HEIGHT = cfg.height || 3;
|
|
12
|
+
const COLOR = cfg.color || '#3b82f6';
|
|
13
|
+
const GRADIENT = cfg.gradient || null;
|
|
14
|
+
const DURATION = cfg.duration || 600; /* ms for the finish animation */
|
|
15
|
+
|
|
16
|
+
/* ── Create bar element ─────────────────────────────────────── */
|
|
17
|
+
const bar = document.createElement('div');
|
|
18
|
+
bar.id = 'bar-top';
|
|
19
|
+
bar.style.cssText = `
|
|
20
|
+
position: fixed;
|
|
21
|
+
top: 0;
|
|
22
|
+
left: 0;
|
|
23
|
+
width: 0%;
|
|
24
|
+
height: ${HEIGHT}px;
|
|
25
|
+
background: ${GRADIENT || COLOR};
|
|
26
|
+
z-index: 99999;
|
|
27
|
+
transition: width 0.1s linear;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
border-radius: 0 ${HEIGHT}px ${HEIGHT}px 0;
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
/* subtle glow under the bar */
|
|
33
|
+
bar.style.boxShadow = GRADIENT
|
|
34
|
+
? `0 0 8px 1px rgba(255,255,255,0.25)`
|
|
35
|
+
: `0 0 8px 1px ${COLOR}88`;
|
|
36
|
+
|
|
37
|
+
document.documentElement.appendChild(bar);
|
|
38
|
+
|
|
39
|
+
/* ── Animation ──────────────────────────────────────────────── */
|
|
40
|
+
let current = 0;
|
|
41
|
+
let rafId = null;
|
|
42
|
+
let finished = false;
|
|
43
|
+
|
|
44
|
+
/* Eased trickle — moves fast at first, then slows as it
|
|
45
|
+
approaches 90%, never quite reaching it until finish() */
|
|
46
|
+
function trickle() {
|
|
47
|
+
if (finished) return;
|
|
48
|
+
const remaining = 90 - current;
|
|
49
|
+
const step = remaining * 0.08 + 0.4;
|
|
50
|
+
current = Math.min(current + step, 90);
|
|
51
|
+
bar.style.width = current + '%';
|
|
52
|
+
rafId = setTimeout(trickle, 120 + Math.random() * 80);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function start() {
|
|
56
|
+
current = 0;
|
|
57
|
+
finished = false;
|
|
58
|
+
bar.style.transition = 'width 0.1s linear';
|
|
59
|
+
bar.style.opacity = '1';
|
|
60
|
+
bar.style.width = '0%';
|
|
61
|
+
trickle();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function finish() {
|
|
65
|
+
finished = true;
|
|
66
|
+
clearTimeout(rafId);
|
|
67
|
+
/* snap to 100% */
|
|
68
|
+
bar.style.transition = `width ${DURATION * 0.4}ms ease`;
|
|
69
|
+
bar.style.width = '100%';
|
|
70
|
+
/* then fade out */
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
bar.style.transition = `opacity ${DURATION * 0.6}ms ease`;
|
|
73
|
+
bar.style.opacity = '0';
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
bar.style.width = '0%';
|
|
76
|
+
bar.style.opacity = '1';
|
|
77
|
+
}, DURATION * 0.7);
|
|
78
|
+
}, DURATION * 0.4);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Hook into page load ────────────────────────────────────── */
|
|
82
|
+
start();
|
|
83
|
+
|
|
84
|
+
if (document.readyState === 'complete') {
|
|
85
|
+
finish();
|
|
86
|
+
} else {
|
|
87
|
+
window.addEventListener('load', finish, { once: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
})();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title>theCodes</title>
|
|
7
|
+
<style>
|
|
8
|
+
body{
|
|
9
|
+
background: #000;
|
|
10
|
+
color: #fff;
|
|
11
|
+
font-family: 'consolas', 'monospace';
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 20px;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
|
|
19
|
+
<div class="editor-wrap">
|
|
20
|
+
<div class="editor editor-dark editor-motion editor-number editor-read">
|
|
21
|
+
<pre><code><!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<title>Bar Test</title>
|
|
25
|
+
<style>
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0;
|
|
30
|
+
user-select: none;
|
|
31
|
+
-webkit-tap-highlight-color: transparent;
|
|
32
|
+
-moz-tap-highlight-color: transparent;
|
|
33
|
+
tap-highlight-color: transparent;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
color: #fff;
|
|
38
|
+
font-family: 'Segoe UI', sans-serif;
|
|
39
|
+
background: #111;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
h1{
|
|
47
|
+
font-weight: 400;
|
|
48
|
+
animation: appear 2s forwards;
|
|
49
|
+
position: relative;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
h1::before,
|
|
53
|
+
h1::after{
|
|
54
|
+
opacity: 0;
|
|
55
|
+
animation-duration: 1s;
|
|
56
|
+
animation-fill-mode: forwards;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
h1::before{
|
|
60
|
+
content: "“";
|
|
61
|
+
animation-name: showQuote;
|
|
62
|
+
animation-delay: 2s;
|
|
63
|
+
margin-right: 6px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
h1::after{
|
|
67
|
+
content: "”";
|
|
68
|
+
animation-name: showQuote;
|
|
69
|
+
animation-delay: 2s;
|
|
70
|
+
margin-left: 6px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@keyframes showQuote{
|
|
74
|
+
from { opacity: 0; }
|
|
75
|
+
to { opacity: 1; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@keyframes appear{
|
|
79
|
+
0%{
|
|
80
|
+
font-weight: bolder;
|
|
81
|
+
margin-top: -30px;
|
|
82
|
+
opacity: 0;
|
|
83
|
+
color: transparent;
|
|
84
|
+
-webkit-text-stroke: 0.1px #ffffff;
|
|
85
|
+
}
|
|
86
|
+
100%{
|
|
87
|
+
font-weight: 400;
|
|
88
|
+
margin-top: 0;
|
|
89
|
+
opacity: 1;
|
|
90
|
+
color: #ffffff;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
|
|
97
|
+
<body>
|
|
98
|
+
|
|
99
|
+
<h1>Bar Test (See the codes to learn more,<br> or see the Docs)</h1>
|
|
100
|
+
|
|
101
|
+
<!-- This Loading Bar Applies when you link the bar.js, for customization of loading bar, do this -->
|
|
102
|
+
<!-- an example of customized loading bar (solid color):<br>
|
|
103
|
+
<script>
|
|
104
|
+
window.BarConfig = { color: '#f43f5e' }
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
it changes the loading bar color to pink, or that #f43f5e hex color
|
|
108
|
+
|
|
109
|
+
but what if you wanted to use gradient? -->
|
|
110
|
+
<!-- an example of customized loading bar (linear gradient color):<br>
|
|
111
|
+
<script>
|
|
112
|
+
window.BarConfig = {
|
|
113
|
+
gradient: 'linear-gradient(to right, #f093fb, #f5576c, #4facfe)'
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
this script will changes the loading bar combined with magenta, pink, blue or we can say #f093fb, #f5576c, #4facfe
|
|
118
|
+
|
|
119
|
+
the end of the description... -->
|
|
120
|
+
|
|
121
|
+
<button onclick="theCodes()">See the Codes</button>
|
|
122
|
+
<button onclick="seeDocs()">See Docs</button>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
function theCodes(){
|
|
126
|
+
window.location.href = "bar/theCodes.html";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function seeDocs(){
|
|
130
|
+
window.location.href = "docs/index.html";
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<script src="bar/bar.js"></script>
|
|
135
|
+
</body>
|
|
136
|
+
</html></code></pre>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<script src="bar.js"></script>
|
|
141
|
+
<script src="../editor/highlight.min.js"></script>
|
|
142
|
+
<script src="../editor/editor.js"></script>
|
|
143
|
+
</body>
|
|
144
|
+
</html>
|
package/editor/editor.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
|
|
3
|
+
/* ── highlight.js does all the tokenising ───────────────────── */
|
|
4
|
+
/* Loaded from CDN once, then used for every editor on the page. */
|
|
5
|
+
|
|
6
|
+
let hlReady = false;
|
|
7
|
+
let hlQueue = [];
|
|
8
|
+
|
|
9
|
+
function loadHL(cb) {
|
|
10
|
+
if (hlReady) { cb(); return; }
|
|
11
|
+
hlQueue.push(cb);
|
|
12
|
+
if (document.getElementById('hl-script')) return;
|
|
13
|
+
const s = document.createElement('script');
|
|
14
|
+
s.id = 'hl-script';
|
|
15
|
+
s.src = '../editor/highlight.min.js';
|
|
16
|
+
s.onload = () => { hlReady = true; hlQueue.forEach(fn => fn()); hlQueue = []; };
|
|
17
|
+
document.head.appendChild(s);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Map hljs token classes → our e-* classes for theming + glow */
|
|
21
|
+
const CLASS_MAP = {
|
|
22
|
+
'hljs-keyword': 'e-kw',
|
|
23
|
+
'hljs-built_in': 'e-fn',
|
|
24
|
+
'hljs-string': 'e-str',
|
|
25
|
+
'hljs-number': 'e-num',
|
|
26
|
+
'hljs-comment': 'e-cmt',
|
|
27
|
+
'hljs-tag': 'e-tag',
|
|
28
|
+
'hljs-attr': 'e-attr',
|
|
29
|
+
'hljs-attribute': 'e-attr',
|
|
30
|
+
'hljs-value': 'e-val',
|
|
31
|
+
'hljs-punctuation': 'e-punc',
|
|
32
|
+
'hljs-selector-class': 'e-cls',
|
|
33
|
+
'hljs-selector-id': 'e-cls',
|
|
34
|
+
'hljs-selector-tag': 'e-sel',
|
|
35
|
+
'hljs-property': 'e-prop',
|
|
36
|
+
'hljs-title': 'e-fn',
|
|
37
|
+
'hljs-name': 'e-tag',
|
|
38
|
+
'hljs-literal': 'e-num',
|
|
39
|
+
'hljs-type': 'e-kw',
|
|
40
|
+
'hljs-symbol': 'e-str',
|
|
41
|
+
'hljs-meta': 'e-cmt',
|
|
42
|
+
'hljs-operator': 'e-punc',
|
|
43
|
+
'hljs-variable': 'e-str',
|
|
44
|
+
'hljs-params': 'e-num',
|
|
45
|
+
'hljs-class': 'e-fn',
|
|
46
|
+
'hljs-function': 'e-fn',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function remapClasses(html) {
|
|
50
|
+
return html.replace(/class="([^"]+)"/g, (_, cls) => {
|
|
51
|
+
const mapped = cls.trim().split(/\s+/).map(c => CLASS_MAP[c] || c).join(' ');
|
|
52
|
+
return `class="${mapped}"`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function highlight(text, lang) {
|
|
57
|
+
try {
|
|
58
|
+
const result = lang && window.hljs.getLanguage(lang)
|
|
59
|
+
? window.hljs.highlight(text, { language: lang, ignoreIllegals: true })
|
|
60
|
+
: window.hljs.highlightAuto(text);
|
|
61
|
+
return remapClasses(result.value);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── Utilities ──────────────────────────────────────────────── */
|
|
68
|
+
function buildLineNums(n) {
|
|
69
|
+
let s = '';
|
|
70
|
+
for (let i = 1; i <= n; i++) s += `<span>${i}</span>`;
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveCaret(root) {
|
|
75
|
+
const sel = window.getSelection();
|
|
76
|
+
if (!sel || !sel.rangeCount) return null;
|
|
77
|
+
const r = sel.getRangeAt(0).cloneRange();
|
|
78
|
+
r.selectNodeContents(root);
|
|
79
|
+
r.setEnd(sel.getRangeAt(0).startContainer, sel.getRangeAt(0).startOffset);
|
|
80
|
+
return r.toString().length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function restoreCaret(root, offset) {
|
|
84
|
+
if (offset == null) return;
|
|
85
|
+
const sel = window.getSelection();
|
|
86
|
+
if (!sel) return;
|
|
87
|
+
const r = document.createRange();
|
|
88
|
+
let count = 0, found = false;
|
|
89
|
+
function walk(n) {
|
|
90
|
+
if (found) return;
|
|
91
|
+
if (n.nodeType === 3) {
|
|
92
|
+
if (count + n.length >= offset) { r.setStart(n, offset - count); r.collapse(true); found = true; }
|
|
93
|
+
else count += n.length;
|
|
94
|
+
} else n.childNodes.forEach(walk);
|
|
95
|
+
}
|
|
96
|
+
walk(root);
|
|
97
|
+
if (found) { sel.removeAllRanges(); sel.addRange(r); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function placeFakeCursor(root, cursor) {
|
|
101
|
+
const sel = window.getSelection();
|
|
102
|
+
if (!sel || !sel.rangeCount) return;
|
|
103
|
+
const r = sel.getRangeAt(0).cloneRange();
|
|
104
|
+
r.collapse(true);
|
|
105
|
+
const rect = r.getBoundingClientRect();
|
|
106
|
+
const rootRect = root.getBoundingClientRect();
|
|
107
|
+
if (!rect.height) return;
|
|
108
|
+
cursor.style.left = (rect.left - rootRect.left + root.scrollLeft) + 'px';
|
|
109
|
+
cursor.style.top = (rect.top - rootRect.top + root.scrollTop) + 'px';
|
|
110
|
+
cursor.style.height = rect.height + 'px';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── Main initialiser ───────────────────────────────────────── */
|
|
114
|
+
function initEditor(el) {
|
|
115
|
+
if (el.dataset.editorInit) return;
|
|
116
|
+
el.dataset.editorInit = '1';
|
|
117
|
+
|
|
118
|
+
const rawText = el.textContent.trim();
|
|
119
|
+
const lang = el.dataset.lang || null; /* null = auto-detect */
|
|
120
|
+
const isRead = el.classList.contains('editor-read');
|
|
121
|
+
const hasNums = el.classList.contains('editor-number');
|
|
122
|
+
const isMotion = el.classList.contains('editor-motion');
|
|
123
|
+
|
|
124
|
+
el.innerHTML = '';
|
|
125
|
+
el.style.cssText = 'display:flex;flex-direction:column;border-radius:10px;overflow:hidden;position:relative;';
|
|
126
|
+
|
|
127
|
+
/* header */
|
|
128
|
+
const header = document.createElement('div');
|
|
129
|
+
header.className = 'e-header';
|
|
130
|
+
const langLabel = document.createElement('span');
|
|
131
|
+
langLabel.className = 'e-lang';
|
|
132
|
+
langLabel.textContent = lang ? lang.toUpperCase() : '...';
|
|
133
|
+
header.innerHTML = `<span class="e-dot e-dot-r"></span><span class="e-dot e-dot-y"></span><span class="e-dot e-dot-g"></span>`;
|
|
134
|
+
header.appendChild(langLabel);
|
|
135
|
+
el.appendChild(header);
|
|
136
|
+
|
|
137
|
+
/* body */
|
|
138
|
+
const body = document.createElement('div');
|
|
139
|
+
body.style.cssText = 'display:flex;flex:1;overflow:auto;';
|
|
140
|
+
|
|
141
|
+
const nums = document.createElement('div');
|
|
142
|
+
nums.className = 'e-nums';
|
|
143
|
+
if (!hasNums) nums.style.display = 'none';
|
|
144
|
+
|
|
145
|
+
const code = document.createElement('div');
|
|
146
|
+
code.className = 'e-code' + (isMotion ? ' e-motion' : '');
|
|
147
|
+
code.contentEditable = isRead ? 'false' : 'true';
|
|
148
|
+
code.spellcheck = false;
|
|
149
|
+
code.setAttribute('autocorrect', 'off');
|
|
150
|
+
code.setAttribute('autocapitalize', 'off');
|
|
151
|
+
|
|
152
|
+
body.appendChild(nums);
|
|
153
|
+
body.appendChild(code);
|
|
154
|
+
el.appendChild(body);
|
|
155
|
+
|
|
156
|
+
if (isRead) {
|
|
157
|
+
const b = document.createElement('div');
|
|
158
|
+
b.className = 'e-badge e-badge-read';
|
|
159
|
+
b.textContent = 'read-only';
|
|
160
|
+
el.appendChild(b);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* fake cursor for motion mode */
|
|
164
|
+
let fakeCursor = null;
|
|
165
|
+
if (isMotion && !isRead) {
|
|
166
|
+
fakeCursor = document.createElement('span');
|
|
167
|
+
fakeCursor.className = 'e-fake-cursor';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* render */
|
|
171
|
+
let prevText = rawText;
|
|
172
|
+
let detectedLang = lang;
|
|
173
|
+
|
|
174
|
+
function render(text, newCharOffset) {
|
|
175
|
+
const offset = saveCaret(code);
|
|
176
|
+
const html = highlight(text, detectedLang);
|
|
177
|
+
|
|
178
|
+
/* update auto-detected lang label */
|
|
179
|
+
if (!lang && window.hljs) {
|
|
180
|
+
try {
|
|
181
|
+
const r = window.hljs.highlightAuto(text);
|
|
182
|
+
if (r.language) {
|
|
183
|
+
detectedLang = r.language;
|
|
184
|
+
langLabel.textContent = r.language.toUpperCase();
|
|
185
|
+
}
|
|
186
|
+
} catch (_) {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isMotion && newCharOffset != null) {
|
|
190
|
+
const before = highlight(text.slice(0, newCharOffset), detectedLang);
|
|
191
|
+
const delta = highlight(text.slice(newCharOffset), detectedLang);
|
|
192
|
+
code.innerHTML = before + `<span class="e-new">${delta}</span>`;
|
|
193
|
+
} else {
|
|
194
|
+
code.innerHTML = html;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
nums.innerHTML = buildLineNums(text.split('\n').length);
|
|
198
|
+
if (!isRead) restoreCaret(code, offset);
|
|
199
|
+
if (fakeCursor && document.activeElement === code) placeFakeCursor(code, fakeCursor);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* wait for hljs then do first render */
|
|
203
|
+
loadHL(() => render(rawText));
|
|
204
|
+
|
|
205
|
+
/* input */
|
|
206
|
+
let busy = false;
|
|
207
|
+
code.addEventListener('input', () => {
|
|
208
|
+
if (busy) return;
|
|
209
|
+
busy = true;
|
|
210
|
+
const cur = code.innerText || '';
|
|
211
|
+
let diffAt = 0;
|
|
212
|
+
while (diffAt < prevText.length && diffAt < cur.length && prevText[diffAt] === cur[diffAt]) diffAt++;
|
|
213
|
+
const added = cur.length > prevText.length;
|
|
214
|
+
render(cur, isMotion && added ? diffAt : null);
|
|
215
|
+
prevText = cur;
|
|
216
|
+
busy = false;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/* fake cursor events */
|
|
220
|
+
if (fakeCursor) {
|
|
221
|
+
code.addEventListener('focus', () => { code.appendChild(fakeCursor); placeFakeCursor(code, fakeCursor); });
|
|
222
|
+
code.addEventListener('blur', () => fakeCursor.remove());
|
|
223
|
+
code.addEventListener('keyup', () => placeFakeCursor(code, fakeCursor));
|
|
224
|
+
code.addEventListener('mouseup',() => placeFakeCursor(code, fakeCursor));
|
|
225
|
+
code.addEventListener('click', () => placeFakeCursor(code, fakeCursor));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── Styles ─────────────────────────────────────────────────── */
|
|
230
|
+
function injectStyles() {
|
|
231
|
+
if (document.getElementById('editor-styles')) return;
|
|
232
|
+
const s = document.createElement('style');
|
|
233
|
+
s.id = 'editor-styles';
|
|
234
|
+
s.textContent = `
|
|
235
|
+
.editor{display:flex;flex-direction:column;font-family:monospace;font-size:13.5px;line-height:1.65;position:relative;}
|
|
236
|
+
.editor-dark{background:#1a1a2e;color:#e2e8f0;border:0.5px solid #333;}
|
|
237
|
+
.editor-light{background:#f8f8fc;color:#2d2d45;border:0.5px solid #ddd;}
|
|
238
|
+
|
|
239
|
+
.e-header{display:flex;align-items:center;gap:8px;padding:8px 14px;}
|
|
240
|
+
.editor-dark .e-header{border-bottom:0.5px solid rgba(255,255,255,0.07);}
|
|
241
|
+
.editor-light .e-header{border-bottom:0.5px solid rgba(0,0,0,0.08);}
|
|
242
|
+
.e-dot{width:11px;height:11px;border-radius:50%;display:inline-block;}
|
|
243
|
+
.e-dot-r{background:#ff5f57}.e-dot-y{background:#febc2e}.e-dot-g{background:#28c840}
|
|
244
|
+
.e-lang{margin-left:auto;font-size:11px;opacity:0.4;letter-spacing:0.04em;}
|
|
245
|
+
|
|
246
|
+
.e-nums{padding:12px 0;min-width:40px;text-align:right;user-select:none;font-size:12px;line-height:1.65;flex-shrink:0;}
|
|
247
|
+
.editor-dark .e-nums{color:#e2e8f0;opacity:0.3;}
|
|
248
|
+
.editor-light .e-nums{color:#2d2d45;opacity:0.3;}
|
|
249
|
+
.e-nums span{display:block;padding:0 10px 0 6px;}
|
|
250
|
+
|
|
251
|
+
.e-code{flex:1;padding:12px 16px;outline:none;white-space:pre;overflow-x:auto;caret-color:#7c85ff;}
|
|
252
|
+
.e-code[contenteditable="false"]{cursor:default;}
|
|
253
|
+
.e-motion{caret-color:transparent;position:relative;}
|
|
254
|
+
|
|
255
|
+
.e-fake-cursor{position:absolute;width:2px;pointer-events:none;background:#7c85ff;border-radius:1px;animation:e-blink 1s steps(1) infinite;z-index:10;}
|
|
256
|
+
@keyframes e-blink{0%,49%{opacity:1}50%,100%{opacity:0}}
|
|
257
|
+
.e-new{animation:e-fadeIn 0.22s ease both;}
|
|
258
|
+
@keyframes e-fadeIn{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}
|
|
259
|
+
|
|
260
|
+
.e-badge{position:absolute;top:8px;right:50px;font-size:10px;padding:2px 7px;border-radius:4px;letter-spacing:0.05em;pointer-events:none;}
|
|
261
|
+
.e-badge-read{background:rgba(255,150,50,0.12);color:#f78c6c;}
|
|
262
|
+
|
|
263
|
+
/* Dark tokens */
|
|
264
|
+
.editor-dark .e-kw{color:#c792ea}.editor-dark .e-fn{color:#82aaff}
|
|
265
|
+
.editor-dark .e-str{color:#c3e88d}.editor-dark .e-num{color:#f78c6c}
|
|
266
|
+
.editor-dark .e-cmt{color:#546e7a;font-style:italic}
|
|
267
|
+
.editor-dark .e-tag{color:#f07178}.editor-dark .e-attr{color:#ffcb6b}
|
|
268
|
+
.editor-dark .e-val{color:#c3e88d}.editor-dark .e-punc{color:#89ddff}
|
|
269
|
+
.editor-dark .e-cls{color:#ffcb6b}.editor-dark .e-sel{color:#82aaff}
|
|
270
|
+
.editor-dark .e-prop{color:#c792ea}.editor-dark .e-unit{color:#f78c6c}
|
|
271
|
+
|
|
272
|
+
/* Light tokens */
|
|
273
|
+
.editor-light .e-kw{color:#7c3aed}.editor-light .e-fn{color:#1d4ed8}
|
|
274
|
+
.editor-light .e-str{color:#15803d}.editor-light .e-num{color:#c2410c}
|
|
275
|
+
.editor-light .e-cmt{color:#94a3b8;font-style:italic}
|
|
276
|
+
.editor-light .e-tag{color:#b91c1c}.editor-light .e-attr{color:#b45309}
|
|
277
|
+
.editor-light .e-val{color:#15803d}.editor-light .e-punc{color:#0369a1}
|
|
278
|
+
.editor-light .e-cls{color:#b45309}.editor-light .e-sel{color:#1d4ed8}
|
|
279
|
+
.editor-light .e-prop{color:#7c3aed}.editor-light .e-unit{color:#c2410c}
|
|
280
|
+
|
|
281
|
+
/* Glow */
|
|
282
|
+
.editor-glow.editor-dark .e-kw{text-shadow:0 0 8px #c792ea88}
|
|
283
|
+
.editor-glow.editor-dark .e-fn{text-shadow:0 0 8px #82aaff88}
|
|
284
|
+
.editor-glow.editor-dark .e-str{text-shadow:0 0 8px #c3e88d88}
|
|
285
|
+
.editor-glow.editor-dark .e-num{text-shadow:0 0 8px #f78c6c88}
|
|
286
|
+
.editor-glow.editor-dark .e-tag{text-shadow:0 0 8px #f0717888}
|
|
287
|
+
.editor-glow.editor-dark .e-attr{text-shadow:0 0 8px #ffcb6b88}
|
|
288
|
+
.editor-glow.editor-dark .e-punc{text-shadow:0 0 8px #89ddff88}
|
|
289
|
+
.editor-glow.editor-dark .e-prop{text-shadow:0 0 8px #c792ea88}
|
|
290
|
+
.editor-glow.editor-light .e-kw{text-shadow:0 0 6px #7c3aed55}
|
|
291
|
+
.editor-glow.editor-light .e-fn{text-shadow:0 0 6px #1d4ed855}
|
|
292
|
+
.editor-glow.editor-light .e-str{text-shadow:0 0 6px #15803d55}
|
|
293
|
+
.editor-glow.editor-light .e-tag{text-shadow:0 0 6px #b91c1c55}
|
|
294
|
+
`;
|
|
295
|
+
document.head.appendChild(s);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function init() {
|
|
299
|
+
injectStyles();
|
|
300
|
+
document.querySelectorAll('.editor').forEach(initEditor);
|
|
301
|
+
new MutationObserver(muts => muts.forEach(m =>
|
|
302
|
+
m.addedNodes.forEach(n => {
|
|
303
|
+
if (n.nodeType !== 1) return;
|
|
304
|
+
if (n.classList.contains('editor')) initEditor(n);
|
|
305
|
+
n.querySelectorAll?.('.editor').forEach(initEditor);
|
|
306
|
+
})
|
|
307
|
+
)).observe(document.body, { childList: true, subtree: true });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
311
|
+
else init();
|
|
312
|
+
})();
|