blob: 2299d4757c4ab253c15202c98ec1249591795027 [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
Marc Kupietz09b75752023-10-07 09:32:19 +02006 // The minimum number of milliseconds that must pass between
7 // calls to history.replaceState
8 MAX_REPLACE_STATE_FREQUENCY = 1000
9
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020010 constructor( Reveal ) {
11
12 this.Reveal = Reveal;
13
14 // Delays updates to the URL due to a Chrome thumbnailer bug
15 this.writeURLTimeout = 0;
16
Marc Kupietz09b75752023-10-07 09:32:19 +020017 this.replaceStateTimestamp = 0;
18
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020019 this.onWindowHashChange = this.onWindowHashChange.bind( this );
20
21 }
22
23 bind() {
24
25 window.addEventListener( 'hashchange', this.onWindowHashChange, false );
26
27 }
28
29 unbind() {
30
31 window.removeEventListener( 'hashchange', this.onWindowHashChange, false );
32
33 }
34
35 /**
Christophe Dervieux8afae132021-12-06 15:16:42 +010036 * Returns the slide indices for the given hash link.
37 *
38 * @param {string} [hash] the hash string that we want to
39 * find the indices for
40 *
41 * @returns slide indices or null
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020042 */
Marc Kupietz09b75752023-10-07 09:32:19 +020043 getIndicesFromHash( hash=window.location.hash, options={} ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020044
45 // Attempt to parse the hash as either an index or name
Christophe Dervieux8afae132021-12-06 15:16:42 +010046 let name = hash.replace( /^#\/?/, '' );
47 let bits = name.split( '/' );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020048
49 // If the first bit is not fully numeric and there is a name we
50 // can assume that this is a named link
51 if( !/^[0-9]*$/.test( bits[0] ) && name.length ) {
Marc Kupietz09b75752023-10-07 09:32:19 +020052 let slide;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020053
54 let f;
55
56 // Parse named links with fragments (#/named-link/2)
57 if( /\/[-\d]+$/g.test( name ) ) {
58 f = parseInt( name.split( '/' ).pop(), 10 );
59 f = isNaN(f) ? undefined : f;
60 name = name.split( '/' ).shift();
61 }
62
63 // Ensure the named link is a valid HTML ID attribute
64 try {
Marc Kupietz09b75752023-10-07 09:32:19 +020065 slide = document
66 .getElementById( decodeURIComponent( name ) )
Marc Kupietz9c036a42024-05-14 13:17:25 +020067 .closest('.slides section');
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020068 }
69 catch ( error ) { }
70
Marc Kupietz09b75752023-10-07 09:32:19 +020071 if( slide ) {
72 return { ...this.Reveal.getIndices( slide ), f };
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020073 }
74 }
75 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +010076 const config = this.Reveal.getConfig();
Marc Kupietz09b75752023-10-07 09:32:19 +020077 let hashIndexBase = config.hashOneBasedIndex || options.oneBasedIndex ? 1 : 0;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020078
79 // Read the index components of the hash
80 let h = ( parseInt( bits[0], 10 ) - hashIndexBase ) || 0,
81 v = ( parseInt( bits[1], 10 ) - hashIndexBase ) || 0,
82 f;
83
84 if( config.fragmentInURL ) {
85 f = parseInt( bits[2], 10 );
86 if( isNaN( f ) ) {
87 f = undefined;
88 }
89 }
90
Christophe Dervieux8afae132021-12-06 15:16:42 +010091 return { h, v, f };
92 }
93
94 // The hash couldn't be parsed or no matching named link was found
95 return null
96
97 }
98
99 /**
100 * Reads the current URL (hash) and navigates accordingly.
101 */
102 readURL() {
103
104 const currentIndices = this.Reveal.getIndices();
105 const newIndices = this.getIndicesFromHash();
106
107 if( newIndices ) {
108 if( ( newIndices.h !== currentIndices.h || newIndices.v !== currentIndices.v || newIndices.f !== undefined ) ) {
109 this.Reveal.slide( newIndices.h, newIndices.v, newIndices.f );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200110 }
111 }
Christophe Dervieux8afae132021-12-06 15:16:42 +0100112 // If no new indices are available, we're trying to navigate to
113 // a slide hash that does not exist
114 else {
115 this.Reveal.slide( currentIndices.h || 0, currentIndices.v || 0 );
116 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200117
118 }
119
120 /**
121 * Updates the page URL (hash) to reflect the current
122 * state.
123 *
124 * @param {number} delay The time in ms to wait before
125 * writing the hash
126 */
127 writeURL( delay ) {
128
129 let config = this.Reveal.getConfig();
130 let currentSlide = this.Reveal.getCurrentSlide();
131
132 // Make sure there's never more than one timeout running
133 clearTimeout( this.writeURLTimeout );
134
135 // If a delay is specified, timeout this call
136 if( typeof delay === 'number' ) {
137 this.writeURLTimeout = setTimeout( this.writeURL, delay );
138 }
139 else if( currentSlide ) {
140
141 let hash = this.getHash();
142
143 // If we're configured to push to history OR the history
Marc Kupietz09b75752023-10-07 09:32:19 +0200144 // API is not available.
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200145 if( config.history ) {
146 window.location.hash = hash;
147 }
148 // If we're configured to reflect the current slide in the
149 // URL without pushing to history.
150 else if( config.hash ) {
151 // If the hash is empty, don't add it to the URL
152 if( hash === '/' ) {
Marc Kupietz09b75752023-10-07 09:32:19 +0200153 this.debouncedReplaceState( window.location.pathname + window.location.search );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200154 }
155 else {
Marc Kupietz09b75752023-10-07 09:32:19 +0200156 this.debouncedReplaceState( '#' + hash );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200157 }
158 }
159 // UPDATE: The below nuking of all hash changes breaks
160 // anchors on pages where reveal.js is running. Removed
161 // in 4.0. Why was it here in the first place? ¯\_(ツ)_/¯
162 //
163 // If history and hash are both disabled, a hash may still
164 // be added to the URL by clicking on a href with a hash
165 // target. Counter this by always removing the hash.
166 // else {
167 // window.history.replaceState( null, null, window.location.pathname + window.location.search );
168 // }
169
170 }
171
172 }
173
Marc Kupietz09b75752023-10-07 09:32:19 +0200174 replaceState( url ) {
175
176 window.history.replaceState( null, null, url );
177 this.replaceStateTimestamp = Date.now();
178
179 }
180
181 debouncedReplaceState( url ) {
182
183 clearTimeout( this.replaceStateTimeout );
184
185 if( Date.now() - this.replaceStateTimestamp > this.MAX_REPLACE_STATE_FREQUENCY ) {
186 this.replaceState( url );
187 }
188 else {
189 this.replaceStateTimeout = setTimeout( () => this.replaceState( url ), this.MAX_REPLACE_STATE_FREQUENCY );
190 }
191
192 }
193
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200194 /**
195 * Return a hash URL that will resolve to the given slide location.
196 *
197 * @param {HTMLElement} [slide=currentSlide] The slide to link to
198 */
199 getHash( slide ) {
200
201 let url = '/';
202
203 // Attempt to create a named link based on the slide's ID
204 let s = slide || this.Reveal.getCurrentSlide();
205 let id = s ? s.getAttribute( 'id' ) : null;
206 if( id ) {
207 id = encodeURIComponent( id );
208 }
209
210 let index = this.Reveal.getIndices( slide );
211 if( !this.Reveal.getConfig().fragmentInURL ) {
212 index.f = undefined;
213 }
214
215 // If the current slide has an ID, use that as a named link,
216 // but we don't support named links with a fragment index
217 if( typeof id === 'string' && id.length ) {
218 url = '/' + id;
219
220 // If there is also a fragment, append that at the end
221 // of the named link, like: #/named-link/2
222 if( index.f >= 0 ) url += '/' + index.f;
223 }
224 // Otherwise use the /h/v index
225 else {
226 let hashIndexBase = this.Reveal.getConfig().hashOneBasedIndex ? 1 : 0;
227 if( index.h > 0 || index.v > 0 || index.f >= 0 ) url += index.h + hashIndexBase;
228 if( index.v > 0 || index.f >= 0 ) url += '/' + (index.v + hashIndexBase );
229 if( index.f >= 0 ) url += '/' + index.f;
230 }
231
232 return url;
233
234 }
235
236 /**
237 * Handler for the window level 'hashchange' event.
238 *
239 * @param {object} [event]
240 */
241 onWindowHashChange( event ) {
242
243 this.readURL();
244
245 }
246
247}