blob: 42fff622c84e0c4a709434879e1be01a653801d1 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001/**
2 * Reads and writes the URL based on reveal.js' current state.
3 */
4export default class Location {
5
6 constructor( Reveal ) {
7
8 this.Reveal = Reveal;
9
10 // Delays updates to the URL due to a Chrome thumbnailer bug
11 this.writeURLTimeout = 0;
12
13 this.onWindowHashChange = this.onWindowHashChange.bind( this );
14
15 }
16
17 bind() {
18
19 window.addEventListener( 'hashchange', this.onWindowHashChange, false );
20
21 }
22
23 unbind() {
24
25 window.removeEventListener( 'hashchange', this.onWindowHashChange, false );
26
27 }
28
29 /**
30 * Reads the current URL (hash) and navigates accordingly.
31 */
32 readURL() {
33
34 let config = this.Reveal.getConfig();
35 let indices = this.Reveal.getIndices();
36 let currentSlide = this.Reveal.getCurrentSlide();
37
38 let hash = window.location.hash;
39
40 // Attempt to parse the hash as either an index or name
41 let bits = hash.slice( 2 ).split( '/' ),
42 name = hash.replace( /#\/?/gi, '' );
43
44 // If the first bit is not fully numeric and there is a name we
45 // can assume that this is a named link
46 if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
47 let element;
48
49 let f;
50
51 // Parse named links with fragments (#/named-link/2)
52 if( /\/[-\d]+$/g.test( name ) ) {
53 f = parseInt( name.split( '/' ).pop(), 10 );
54 f = isNaN(f) ? undefined : f;
55 name = name.split( '/' ).shift();
56 }
57
58 // Ensure the named link is a valid HTML ID attribute
59 try {
60 element = document.getElementById( decodeURIComponent( name ) );
61 }
62 catch ( error ) { }
63
64 // Ensure that we're not already on a slide with the same name
65 let isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute( 'id' ) === name : false;
66
67 if( element ) {
68 // If the slide exists and is not the current slide...
69 if ( !isSameNameAsCurrentSlide || typeof f !== 'undefined' ) {
70 // ...find the position of the named slide and navigate to it
71 let slideIndices = this.Reveal.getIndices( element );
72 this.Reveal.slide( slideIndices.h, slideIndices.v, f );
73 }
74 }
75 // If the slide doesn't exist, navigate to the current slide
76 else {
77 this.Reveal.slide( indices.h || 0, indices.v || 0 );
78 }
79 }
80 else {
81 let hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
82
83 // Read the index components of the hash
84 let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
85 v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
86 f;
87
88 if( config.fragmentInURL ) {
89 f = parseInt( bits[2], 10 );
90 if( isNaN( f ) ) {
91 f = undefined;
92 }
93 }
94
95 if( h !== indices.h || v !== indices.v || f !== undefined ) {
96 this.Reveal.slide( h, v, f );
97 }
98 }
99
100 }
101
102 /**
103 * Updates the page URL (hash) to reflect the current
104 * state.
105 *
106 * @param {number} delay The time in ms to wait before
107 * writing the hash
108 */
109 writeURL( delay ) {
110
111 let config = this.Reveal.getConfig();
112 let currentSlide = this.Reveal.getCurrentSlide();
113
114 // Make sure there's never more than one timeout running
115 clearTimeout( this.writeURLTimeout );
116
117 // If a delay is specified, timeout this call
118 if( typeof delay === 'number' ) {
119 this.writeURLTimeout = setTimeout( this.writeURL, delay );
120 }
121 else if( currentSlide ) {
122
123 let hash = this.getHash();
124
125 // If we're configured to push to history OR the history
126 // API is not avaialble.
127 if( config.history ) {
128 window.location.hash = hash;
129 }
130 // If we're configured to reflect the current slide in the
131 // URL without pushing to history.
132 else if( config.hash ) {
133 // If the hash is empty, don't add it to the URL
134 if( hash === '/' ) {
135 window.history.replaceState( null, null, window.location.pathname + window.location.search );
136 }
137 else {
138 window.history.replaceState( null, null, '#' + hash );
139 }
140 }
141 // UPDATE: The below nuking of all hash changes breaks
142 // anchors on pages where reveal.js is running. Removed
143 // in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
144 //
145 // If history and hash are both disabled, a hash may still
146 // be added to the URL by clicking on a href with a hash
147 // target. Counter this by always removing the hash.
148 // else {
149 // window.history.replaceState( null, null, window.location.pathname + window.location.search );
150 // }
151
152 }
153
154 }
155
156 /**
157 * Return a hash URL that will resolve to the given slide location.
158 *
159 * @param {HTMLElement} [slide=currentSlide] The slide to link to
160 */
161 getHash( slide ) {
162
163 let url = '/';
164
165 // Attempt to create a named link based on the slide's ID
166 let s = slide || this.Reveal.getCurrentSlide();
167 let id = s ? s.getAttribute( 'id' ) : null;
168 if( id ) {
169 id = encodeURIComponent( id );
170 }
171
172 let index = this.Reveal.getIndices( slide );
173 if( !this.Reveal.getConfig().fragmentInURL ) {
174 index.f = undefined;
175 }
176
177 // If the current slide has an ID, use that as a named link,
178 // but we don't support named links with a fragment index
179 if( typeof id === 'string' && id.length ) {
180 url = '/' + id;
181
182 // If there is also a fragment, append that at the end
183 // of the named link, like: #/named-link/2
184 if( index.f >= 0 ) url += '/' + index.f;
185 }
186 // Otherwise use the /h/v index
187 else {
188 let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
189 if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
190 if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
191 if( index.f >= 0 ) url += '/' + index.f;
192 }
193
194 return url;
195
196 }
197
198 /**
199 * Handler for the window level 'hashchange' event.
200 *
201 * @param {object} [event]
202 */
203 onWindowHashChange( event ) {
204
205 this.readURL();
206
207 }
208
209}