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