highlighter-pen 1.0.1
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/README.md +49 -0
- package/dist/highlighter-pen.js +216 -0
- package/package.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<a href="https://www.morganbkf.com/highlighter-pen/" target="_blank">
|
|
2
|
+
<img src="https://github.com/mimoklef/highlighter-pen/blob/main/assets/demotitle.png?raw=true" />
|
|
3
|
+
</a>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
A simple JavaScript library that replaces the native text selection with a marker-like highlight effect.
|
|
7
|
+
|
|
8
|
+
It behaves like a real highlighter: drag your mouse over text and see a custom overlay instead of the default selection.
|
|
9
|
+
|
|
10
|
+
<a href="https://www.morganbkf.com/highlighter-pen/" target="_blank">
|
|
11
|
+
<img width=40% src="https://github.com/mimoklef/highlighter-pen/blob/main/assets/subtitle.jpg?raw=true" />
|
|
12
|
+
</a>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## ✨ Features
|
|
17
|
+
|
|
18
|
+
- Marker-style highlight effect
|
|
19
|
+
- Works across multiple lines
|
|
20
|
+
- Keeps native selection inside inputs and textareas
|
|
21
|
+
- No dependencies
|
|
22
|
+
- Plug & play (1 script)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## 🚀 Demo
|
|
27
|
+
|
|
28
|
+
[You can access this demo and try it yourself](https://www.morganbkf.com/highlighter-pen/)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
### CDN (recommended)
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script src="https://cdn.jsdelivr.net/gh/mimoklef/highlighter-pen@v1.0.0/dist/highlighter-pen.js"></script>
|
|
38
|
+
<script>
|
|
39
|
+
HighlighterPen().init();
|
|
40
|
+
</script>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## 🙋🏻♂️ Author
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Made with ❤️ by [Morgan Bouyakhlef](https://www.morganbkf.com/)
|
|
49
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Highlighter pen v1.0.0
|
|
3
|
+
* Marker-like selection overlay using <marker> + optional native input selection yellow.
|
|
4
|
+
* https://github.com/mimoklef/highlighter-pen/
|
|
5
|
+
* © 2026 Morgan Bouyakhlef
|
|
6
|
+
* Released under the MIT License
|
|
7
|
+
*/
|
|
8
|
+
(function (global) {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
markerImage: "https://cdn.jsdelivr.net/gh/mimoklef/highlighter-pen@v1.0.1/assets/marker.png",
|
|
13
|
+
markerZIndex: 10, // marker overlays on text
|
|
14
|
+
hideNativeSelection: true, // hide ::selection (so only your marker is visible)
|
|
15
|
+
inputSelectionYellow: true, // keep native selection in inputs/textarea and tint it yellow
|
|
16
|
+
inputSelectionColor: "rgba(255,235,59,0.95)",
|
|
17
|
+
exclude: "input, textarea, select, button, label" // clicking these cancels selection
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function createStyle(id, cssText) {
|
|
21
|
+
let el = document.getElementById(id);
|
|
22
|
+
if (el) return el;
|
|
23
|
+
el = document.createElement("style");
|
|
24
|
+
el.id = id;
|
|
25
|
+
el.textContent = cssText;
|
|
26
|
+
document.head.appendChild(el);
|
|
27
|
+
return el;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isTextLikeInput(el) {
|
|
31
|
+
if (!el) return false;
|
|
32
|
+
if (el.tagName === "TEXTAREA") return true;
|
|
33
|
+
if (el.tagName !== "INPUT") return false;
|
|
34
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
35
|
+
return ["text", "email", "password", "search", "tel", "url", "number"].includes(type);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function HighlighterPen(userOptions) {
|
|
39
|
+
const opt = Object.assign({}, DEFAULTS, userOptions || {});
|
|
40
|
+
const STYLE_MAIN_ID = "hp-style-main";
|
|
41
|
+
const STYLE_INPUT_ID = "hp-style-input";
|
|
42
|
+
const STYLE_HIDESEL_ID = "hp-style-hidesel";
|
|
43
|
+
|
|
44
|
+
let overlays = [];
|
|
45
|
+
let padTop = 0;
|
|
46
|
+
let padBottom = 0;
|
|
47
|
+
let destroyed = false;
|
|
48
|
+
|
|
49
|
+
let onSelectionChange, onScroll, onResize, onPointerDown;
|
|
50
|
+
|
|
51
|
+
function injectStyles() {
|
|
52
|
+
createStyle(
|
|
53
|
+
STYLE_MAIN_ID,
|
|
54
|
+
`
|
|
55
|
+
marker{
|
|
56
|
+
border-image-source: url("${opt.markerImage}");
|
|
57
|
+
border-image-slice: 0 27 0 29 fill;
|
|
58
|
+
border-image-width: 0px 1ch 0px 1ch;
|
|
59
|
+
border-image-outset: 0px 1ch 0px 1ch;
|
|
60
|
+
border-image-repeat: round stretch;
|
|
61
|
+
background-color: transparent;
|
|
62
|
+
-webkit-box-decoration-break: clone;
|
|
63
|
+
box-decoration-break: clone;
|
|
64
|
+
padding: 0.8em 0;
|
|
65
|
+
mix-blend-mode: multiply;
|
|
66
|
+
opacity: 1;
|
|
67
|
+
z-index: ${opt.markerZIndex} !important;
|
|
68
|
+
}
|
|
69
|
+
`.trim()
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (opt.hideNativeSelection) {
|
|
73
|
+
createStyle(
|
|
74
|
+
STYLE_HIDESEL_ID,
|
|
75
|
+
`
|
|
76
|
+
::selection { background: none; }
|
|
77
|
+
::-moz-selection { background: none; }
|
|
78
|
+
`.trim()
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (opt.inputSelectionYellow) {
|
|
83
|
+
createStyle(
|
|
84
|
+
STYLE_INPUT_ID,
|
|
85
|
+
`
|
|
86
|
+
input::selection, textarea::selection { background: ${opt.inputSelectionColor}; color: inherit; }
|
|
87
|
+
input::-moz-selection, textarea::-moz-selection { background: ${opt.inputSelectionColor}; color: inherit; }
|
|
88
|
+
`.trim()
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readMarkerPaddingPx() {
|
|
94
|
+
const t = document.createElement("marker");
|
|
95
|
+
t.textContent = "A";
|
|
96
|
+
t.style.position = "fixed";
|
|
97
|
+
t.style.left = "-9999px";
|
|
98
|
+
t.style.top = "-9999px";
|
|
99
|
+
t.style.pointerEvents = "none";
|
|
100
|
+
document.body.appendChild(t);
|
|
101
|
+
|
|
102
|
+
const cs = getComputedStyle(t);
|
|
103
|
+
padTop = parseFloat(cs.paddingTop) || 0;
|
|
104
|
+
padBottom = parseFloat(cs.paddingBottom) || 0;
|
|
105
|
+
|
|
106
|
+
t.remove();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clearOverlays() {
|
|
110
|
+
overlays.forEach((el) => el.remove());
|
|
111
|
+
overlays = [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function draw() {
|
|
115
|
+
if (destroyed) return;
|
|
116
|
+
|
|
117
|
+
const sel = window.getSelection();
|
|
118
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) {
|
|
119
|
+
clearOverlays();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Keep native selection (yellow) inside text inputs/textarea
|
|
124
|
+
const ae = document.activeElement;
|
|
125
|
+
if (isTextLikeInput(ae)) {
|
|
126
|
+
clearOverlays();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const range = sel.getRangeAt(0);
|
|
131
|
+
|
|
132
|
+
// avoid nesting markers
|
|
133
|
+
const sp = range.startContainer.parentElement;
|
|
134
|
+
const ep = range.endContainer.parentElement;
|
|
135
|
+
if ((sp && sp.closest("marker")) || (ep && ep.closest("marker"))) return;
|
|
136
|
+
|
|
137
|
+
const rects = Array.from(range.getClientRects()).filter(
|
|
138
|
+
(r) => r.width > 1 && r.height > 1
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
clearOverlays();
|
|
142
|
+
if (!rects.length) return;
|
|
143
|
+
|
|
144
|
+
for (const r of rects) {
|
|
145
|
+
const m = document.createElement("marker");
|
|
146
|
+
|
|
147
|
+
m.style.position = "fixed";
|
|
148
|
+
m.style.left = `${r.left}px`;
|
|
149
|
+
m.style.top = `${Math.round(r.top - padTop)}px`;
|
|
150
|
+
m.style.width = `${Math.round(r.width)}px`;
|
|
151
|
+
m.style.height = `${Math.round(r.height + padTop + padBottom)}px`;
|
|
152
|
+
m.style.display = "block";
|
|
153
|
+
m.style.pointerEvents = "none";
|
|
154
|
+
m.style.margin = "0";
|
|
155
|
+
|
|
156
|
+
// avoid double-padding (already included in top/height)
|
|
157
|
+
m.style.padding = "0";
|
|
158
|
+
|
|
159
|
+
document.body.appendChild(m);
|
|
160
|
+
overlays.push(m);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cancelSelection() {
|
|
165
|
+
const sel = window.getSelection();
|
|
166
|
+
if (sel) sel.removeAllRanges();
|
|
167
|
+
clearOverlays();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function init() {
|
|
171
|
+
injectStyles();
|
|
172
|
+
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
if (destroyed) return;
|
|
175
|
+
readMarkerPaddingPx();
|
|
176
|
+
draw();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
onSelectionChange = () => requestAnimationFrame(draw);
|
|
180
|
+
onScroll = () => requestAnimationFrame(draw);
|
|
181
|
+
onResize = () => requestAnimationFrame(draw);
|
|
182
|
+
|
|
183
|
+
onPointerDown = (e) => {
|
|
184
|
+
const el = e.target && e.target.closest(opt.exclude);
|
|
185
|
+
if (!el) return;
|
|
186
|
+
|
|
187
|
+
// keep native selection inside text-like inputs/textarea
|
|
188
|
+
if (isTextLikeInput(el)) return;
|
|
189
|
+
|
|
190
|
+
cancelSelection();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
194
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
195
|
+
window.addEventListener("resize", onResize);
|
|
196
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
197
|
+
|
|
198
|
+
return api;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function destroy() {
|
|
202
|
+
destroyed = true;
|
|
203
|
+
clearOverlays();
|
|
204
|
+
|
|
205
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
206
|
+
window.removeEventListener("scroll", onScroll);
|
|
207
|
+
window.removeEventListener("resize", onResize);
|
|
208
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const api = { init, draw, clear: clearOverlays, cancelSelection, destroy, options: opt };
|
|
212
|
+
return api;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
global.HighlighterPen = HighlighterPen;
|
|
216
|
+
})(typeof window !== "undefined" ? window : this);
|
package/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "highlighter-pen",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Marker-like text selection overlay (live) using JavaScript + CSS injection",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/highlighter-pen.js",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"author": "Morgan Bouyakhlef",
|
|
9
|
+
"keywords": ["highlight", "highlight pen", "text-selection", "highlighter", "highlighter pen", "javascript", "css", "ui", "ux", "dom", "pen"]
|
|
10
|
+
}
|