Skip to content

Commit bfd1e78

Browse files
from point APIs
1 parent 90a147b commit bfd1e78

File tree

1 file changed

+230
-0
lines changed

1 file changed

+230
-0
lines changed

from-point-apis/index.html

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Path from Point APIs</title>
8+
<style>
9+
body {
10+
font-family: system-ui;
11+
padding: 4rem;
12+
font-size: 11pt;
13+
display: grid;
14+
gap: 1rem;
15+
grid-template-columns: 1fr 1fr;
16+
max-width: 1000px;
17+
margin: 0 auto;
18+
}
19+
20+
.layout-block {
21+
padding: .5rem;
22+
border: 1px solid #ccc;
23+
border-radius: .5rem;
24+
}
25+
26+
.text-editor {
27+
line-height: 1.8;
28+
grid-column: span 2;
29+
}
30+
31+
.text-editor :first-child {
32+
margin-block-start: 0;
33+
}
34+
35+
.text-editor :last-child {
36+
margin-block-end: 0;
37+
}
38+
39+
.inspector-hover-box {
40+
position: relative;
41+
outline: 1px dotted #0008;
42+
}
43+
44+
.selected-box {
45+
background: rgba(255, 251, 0, 0.541);
46+
outline: 1px dashed orange;
47+
}
48+
49+
.highlighters {
50+
display: flex;
51+
gap: .25rem;
52+
}
53+
54+
.highlighters label {
55+
display: flex;
56+
align-items: center;
57+
}
58+
59+
.highlighters #cyan-wave span,
60+
::highlight(cyan-wave-highlight) {
61+
background-color: cyan;
62+
text-decoration: wavy underline blue;
63+
}
64+
65+
.highlighters #red-line span,
66+
::highlight(red-line-highlight) {
67+
background-color: pink;
68+
text-decoration: solid underline red;
69+
}
70+
</style>
71+
</head>
72+
73+
<body>
74+
75+
<div class="highlighters layout-block">
76+
Highlighter styles:
77+
<label id="cyan-wave"><input type="radio" name="highlighter" checked><span>Cyan wave</span></label>
78+
<label id="red-line"><input type="radio" name="highlighter"><span>Red line</span></label>
79+
</div>
80+
81+
<div class="inspector-output layout-block"></div>
82+
83+
<div class="text-editor layout-block" contenteditable spellcheck="false">
84+
<h1>Get elements, caret, and highlights from a point</h1>
85+
<p>This webpage is a demo of the following DOM APIs: <code>document.elementFromtPoint()</code>,
86+
<code>document.elementsFromPoint()</code>, <code>document.caretPositionFromPoint()</code>, and
87+
<code>CSS.highlights.highlightsFromPoint()</code>, which are useful for text editing scenarios.
88+
</p>
89+
<h2>Get the topmost or all elements at a point</h2>
90+
<p>Move your mouse around the text to see the elements under the cursor highlighted with a dotted outline. This is
91+
achieved by
92+
using <a
93+
href="https://developer.mozilla.org/docs/Web/API/Document/elementsFromPoint"><code>document.elementsFromPoint()</code></a>
94+
to get <strong>all elements under the cursor</strong> and applying
95+
a special CSS
96+
class to them. The elements are also displayed at the top of the page.</p>
97+
<p>Click on an element to select it. This highlights the element with a yellow background and dashed outline and
98+
displays the element at the top of the page. This is done using <a
99+
href="https://developer.mozilla.org/docs/Web/API/Document/elementFromPoint"><code>document.elementFromPoint()</code></a>
100+
to get
101+
<strong>the topmost element under the cursor</strong>.
102+
</p>
103+
<h2>Get the caret at a point</h2>
104+
<p>You can also select text by clicking and dragging. The selected text will be highlighted with the chosen
105+
highlighter style above.</p>
106+
<p>This works thanks to <a
107+
href="https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint"><code>document.caretPositionFromPoint()</code></a>
108+
which gives <strong>the caret position</strong> at the start
109+
and end of the
110+
selection. A <code>StaticRange</code> is then created from these two positions and highlighted by using the <a
111+
href="https://developer.mozilla.org/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a>.
112+
</p>
113+
<h2>Get highlights at a point</h2>
114+
<p>After you've created a few highlights, including overlapping highlights, you can click highlighted text to delete
115+
the highlights at a given point.
116+
This is done using <a
117+
href="https://developer.mozilla.org/docs/Web/API/HighlightRegistry/highlightsFromPoint"><code>CSS.highlights.highlightsFromPoint()</code></a>
118+
which returns <strong>all highlights at the given point</strong>.
119+
</p>
120+
</div>
121+
122+
<script>
123+
const INSPECTOR_BOX_CLASS = "inspector-hover-box";
124+
const SELECTED_BOX_CLASS = "selected-box";
125+
126+
const editor = document.querySelector('.text-editor');
127+
const inspectorOutput = document.querySelector('.inspector-output');
128+
const cyanWaveHighlightRadio = document.querySelector('#cyan-wave input');
129+
const redLineHighlightRadio = document.querySelector('#red-line input');
130+
131+
function drawInspectorBoxes(els) {
132+
editor.querySelectorAll(`.${INSPECTOR_BOX_CLASS}`).forEach(el => el.classList.toggle(INSPECTOR_BOX_CLASS, false));
133+
els.forEach(el => {
134+
el.dataset.metadata = `${el.tagName.toLowerCase()}${el.className ? `.${el.className}` : ''}${el.id ? `#${el.id}` : ''}`;
135+
el.classList.toggle(INSPECTOR_BOX_CLASS, true);
136+
});
137+
}
138+
139+
function drawSelectedBox(el) {
140+
editor.querySelectorAll(`.${SELECTED_BOX_CLASS}`).forEach(el => el.classList.toggle(SELECTED_BOX_CLASS, false));
141+
if (el) {
142+
el.classList.toggle(SELECTED_BOX_CLASS, true);
143+
}
144+
}
145+
146+
function displayInspectorOutput(str) {
147+
inspectorOutput.textContent = str;
148+
}
149+
150+
addEventListener("mousemove", e => {
151+
const elements = document.elementsFromPoint(e.clientX, e.clientY);
152+
// Find the index at which editor is found
153+
const editorIndex = elements.findIndex(el => el === editor);
154+
// If not found, the even occurred outside the editor.
155+
if (editorIndex === -1) {
156+
drawInspectorBoxes([]);
157+
displayInspectorOutput("");
158+
return;
159+
}
160+
161+
// Slice the array to get only elements within the editor
162+
const editorElements = elements.slice(0, editorIndex);
163+
if (editorElements.length === 0) {
164+
drawInspectorBoxes([]);
165+
displayInspectorOutput("");
166+
return;
167+
}
168+
169+
drawInspectorBoxes(editorElements);
170+
displayInspectorOutput(`Hovering ${editorElements.reverse().map(el => el.dataset.metadata).join(' > ')}`);
171+
});
172+
173+
addEventListener("click", e => {
174+
const selectedElement = document.elementFromPoint(e.clientX, e.clientY);
175+
if (!selectedElement || !editor.contains(selectedElement) || selectedElement === editor) {
176+
return;
177+
}
178+
179+
drawSelectedBox(selectedElement);
180+
displayInspectorOutput(`Selected ${selectedElement.dataset.metadata}`);
181+
});
182+
183+
const cyanWaveHighlights = new Highlight();
184+
CSS.highlights.set("cyan-wave-highlight", cyanWaveHighlights);
185+
186+
const redLineHighlights = new Highlight();
187+
CSS.highlights.set("red-line-highlight", redLineHighlights);
188+
189+
editor.addEventListener("mousedown", e => {
190+
let startCaret = document.caretPositionFromPoint(e.clientX, e.clientY);
191+
editor.addEventListener("mouseup", e => {
192+
let endCaret = document.caretPositionFromPoint(e.clientX, e.clientY);
193+
194+
if (startCaret && endCaret) {
195+
// Reverse if endCaret is before startCaret.
196+
if (startCaret.offsetNode === endCaret.offsetNode) {
197+
if (startCaret.offset > endCaret.offset) {
198+
[startCaret, endCaret] = [endCaret, startCaret];
199+
}
200+
} else {
201+
const position = startCaret.offsetNode.compareDocumentPosition(endCaret.offsetNode);
202+
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
203+
[startCaret, endCaret] = [endCaret, startCaret];
204+
}
205+
}
206+
207+
const range = new StaticRange({
208+
startContainer: startCaret.offsetNode,
209+
startOffset: startCaret.offset,
210+
endContainer: endCaret.offsetNode,
211+
endOffset: endCaret.offset
212+
});
213+
214+
const highlight = cyanWaveHighlightRadio.checked ? cyanWaveHighlights : redLineHighlights;
215+
highlight.add(range);
216+
}
217+
}, { once: true });
218+
});
219+
220+
editor.addEventListener("mouseup", e => {
221+
CSS.highlights.highlightsFromPoint(e.clientX, e.clientY).forEach(({ highlight, ranges }) => {
222+
for (const range of ranges) {
223+
highlight.delete(range);
224+
}
225+
});
226+
});
227+
</script>
228+
</body>
229+
230+
</html>

0 commit comments

Comments
 (0)