new controller for speaker notes
This commit is contained in:
parent
2b02f3a1f9
commit
97ee72549b
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Constructor for the playback component, which displays
|
* UI component that lets the user control pause/resume
|
||||||
* play/pause/progress controls.
|
* and see the progress of auto-slide playback.
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} container The component will append
|
* @param {HTMLElement} container The component will append
|
||||||
* itself to this
|
* itself to this
|
||||||
|
|
|
@ -13,6 +13,20 @@ export default class Fragments {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the reveal.js config is updated.
|
||||||
|
*/
|
||||||
|
configure( config, oldConfig ) {
|
||||||
|
|
||||||
|
if( config.fragments === false ) {
|
||||||
|
this.disable();
|
||||||
|
}
|
||||||
|
else if( oldConfig.fragments === false ) {
|
||||||
|
this.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If fragments are disabled in the deck, they should all be
|
* If fragments are disabled in the deck, they should all be
|
||||||
* visible rather than stepped through.
|
* visible rather than stepped through.
|
||||||
|
|
|
@ -21,6 +21,32 @@ export default class Keyboard {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the reveal.js config is updated.
|
||||||
|
*/
|
||||||
|
configure( config, oldConfig ) {
|
||||||
|
|
||||||
|
if( config.navigationMode === 'linear' ) {
|
||||||
|
this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
|
||||||
|
this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.shortcuts['N , SPACE'] = 'Next slide';
|
||||||
|
this.shortcuts['P'] = 'Previous slide';
|
||||||
|
this.shortcuts['← , H'] = 'Navigate left';
|
||||||
|
this.shortcuts['→ , L'] = 'Navigate right';
|
||||||
|
this.shortcuts['↑ , K'] = 'Navigate up';
|
||||||
|
this.shortcuts['↓ , J'] = 'Navigate down';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shortcuts['Home , Shift ←'] = 'First slide';
|
||||||
|
this.shortcuts['End , Shift →'] = 'Last slide';
|
||||||
|
this.shortcuts['B , .'] = 'Pause';
|
||||||
|
this.shortcuts['F'] = 'Fullscreen';
|
||||||
|
this.shortcuts['ESC, O'] = 'Slide overview';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts listening for keyboard events.
|
* Starts listening for keyboard events.
|
||||||
*/
|
*/
|
||||||
|
@ -73,32 +99,6 @@ export default class Keyboard {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates our keyboard shortcuts based on current settings.
|
|
||||||
*/
|
|
||||||
refreshSortcuts() {
|
|
||||||
|
|
||||||
if( this.Reveal.getConfig().navigationMode === 'linear' ) {
|
|
||||||
this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
|
|
||||||
this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.shortcuts['N , SPACE'] = 'Next slide';
|
|
||||||
this.shortcuts['P'] = 'Previous slide';
|
|
||||||
this.shortcuts['← , H'] = 'Navigate left';
|
|
||||||
this.shortcuts['→ , L'] = 'Navigate right';
|
|
||||||
this.shortcuts['↑ , K'] = 'Navigate up';
|
|
||||||
this.shortcuts['↓ , J'] = 'Navigate down';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shortcuts['Home , Shift ←'] = 'First slide';
|
|
||||||
this.shortcuts['End , Shift →'] = 'Last slide';
|
|
||||||
this.shortcuts['B , .'] = 'Pause';
|
|
||||||
this.shortcuts['F'] = 'Fullscreen';
|
|
||||||
this.shortcuts['ESC, O'] = 'Slide overview';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Programmatically triggers a keyboard event
|
* Programmatically triggers a keyboard event
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { extend, toArray } from '../utils/util.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class Notes {
|
||||||
|
|
||||||
|
constructor( Reveal ) {
|
||||||
|
|
||||||
|
this.Reveal = Reveal;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
this.element = document.createElement( 'div' );
|
||||||
|
this.element.className = 'speaker-notes';
|
||||||
|
this.element.setAttribute( 'data-prevent-swipe', '' );
|
||||||
|
this.element.setAttribute( 'tabindex', '0' );
|
||||||
|
this.Reveal.getRevealElement().appendChild( this.element );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the reveal.js config is updated.
|
||||||
|
*/
|
||||||
|
configure( config, oldConfig ) {
|
||||||
|
|
||||||
|
if( config.showNotes ) {
|
||||||
|
this.element.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick up notes from the current slide and display them
|
||||||
|
* to the viewer.
|
||||||
|
*
|
||||||
|
* @see {@link config.showNotes}
|
||||||
|
*/
|
||||||
|
update() {
|
||||||
|
|
||||||
|
if( this.Reveal.getConfig().showNotes && this.element && this.Reveal.getCurrentSlide() && !this.Reveal.print.isPrintingPDF() ) {
|
||||||
|
|
||||||
|
this.element.innerHTML = this.getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of the speaker notes sidebar that
|
||||||
|
* is used to share annotated slides. The notes sidebar is
|
||||||
|
* only visible if showNotes is true and there are notes on
|
||||||
|
* one or more slides in the deck.
|
||||||
|
*/
|
||||||
|
updateVisibility() {
|
||||||
|
|
||||||
|
if( this.Reveal.getConfig().showNotes && this.hasNotes() ) {
|
||||||
|
this.Reveal.getRevealElement().classList.add( 'show-notes' );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.Reveal.getRevealElement().classList.remove( 'show-notes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are speaker notes for ANY slide in the
|
||||||
|
* presentation.
|
||||||
|
*/
|
||||||
|
hasNotes() {
|
||||||
|
|
||||||
|
return this.Reveal.getSlidesElement().querySelectorAll( '[data-notes], aside.notes' ).length > 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this presentation is running inside of the
|
||||||
|
* speaker notes window.
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isSpeakerNotes() {
|
||||||
|
|
||||||
|
return !!window.location.search.match( /receiver/gi );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the speaker notes from a slide. Notes can be
|
||||||
|
* defined in two ways:
|
||||||
|
* 1. As a data-notes attribute on the slide <section>
|
||||||
|
* 2. As an <aside class="notes"> inside of the slide
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} [slide=currentSlide]
|
||||||
|
* @return {(string|null)}
|
||||||
|
*/
|
||||||
|
getSlideNotes( slide = this.Reveal.getCurrentSlide() ) {
|
||||||
|
|
||||||
|
// Notes can be specified via the data-notes attribute...
|
||||||
|
if( slide.hasAttribute( 'data-notes' ) ) {
|
||||||
|
return slide.getAttribute( 'data-notes' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... or using an <aside class="notes"> element
|
||||||
|
let notesElement = slide.querySelector( 'aside.notes' );
|
||||||
|
if( notesElement ) {
|
||||||
|
return notesElement.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ export default class SlideNumber {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createElement() {
|
render() {
|
||||||
|
|
||||||
this.element = document.createElement( 'div' );
|
this.element = document.createElement( 'div' );
|
||||||
this.element.className = 'slide-number';
|
this.element.className = 'slide-number';
|
||||||
|
@ -18,12 +18,9 @@ export default class SlideNumber {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows or hides the slide number depending on the
|
* Called when the reveal.js config is updated.
|
||||||
* current config and state.
|
|
||||||
*/
|
*/
|
||||||
refreshVisibility() {
|
configure( config, oldConfig ) {
|
||||||
|
|
||||||
let config = this.Reveal.getConfig();
|
|
||||||
|
|
||||||
let slideNumberDisplay = 'none';
|
let slideNumberDisplay = 'none';
|
||||||
if( config.slideNumber && !this.Reveal.isPrintingPDF() ) {
|
if( config.slideNumber && !this.Reveal.isPrintingPDF() ) {
|
||||||
|
|
125
js/reveal.js
125
js/reveal.js
|
@ -9,6 +9,7 @@ import Location from './controllers/location.js'
|
||||||
import Plugins from './controllers/plugins.js'
|
import Plugins from './controllers/plugins.js'
|
||||||
import Print from './controllers/print.js'
|
import Print from './controllers/print.js'
|
||||||
import Touch from './controllers/touch.js'
|
import Touch from './controllers/touch.js'
|
||||||
|
import Notes from './controllers/notes.js'
|
||||||
import Playback from './components/playback.js'
|
import Playback from './components/playback.js'
|
||||||
import defaultConfig from './config.js'
|
import defaultConfig from './config.js'
|
||||||
import { isMobile, isChrome, isAndroid, supportsZoom } from './utils/device.js'
|
import { isMobile, isChrome, isAndroid, supportsZoom } from './utils/device.js'
|
||||||
|
@ -82,6 +83,7 @@ export default function( revealElement, options ) {
|
||||||
plugins = new Plugins( Reveal ),
|
plugins = new Plugins( Reveal ),
|
||||||
print = new Print( Reveal ),
|
print = new Print( Reveal ),
|
||||||
touch = new Touch( Reveal ),
|
touch = new Touch( Reveal ),
|
||||||
|
notes = new Notes( Reveal ),
|
||||||
|
|
||||||
// CSS transform that is currently applied to the slides container,
|
// CSS transform that is currently applied to the slides container,
|
||||||
// split into two groups
|
// split into two groups
|
||||||
|
@ -236,12 +238,10 @@ export default function( revealElement, options ) {
|
||||||
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>` );
|
<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>` );
|
||||||
|
|
||||||
// Slide number
|
// Slide number
|
||||||
slideNumber.createElement();
|
slideNumber.render();
|
||||||
|
|
||||||
// Element containing notes that are visible to the audience
|
// Slide notes
|
||||||
dom.speakerNotes = createSingletonNode( dom.wrapper, 'div', 'speaker-notes', null );
|
notes.render();
|
||||||
dom.speakerNotes.setAttribute( 'data-prevent-swipe', '' );
|
|
||||||
dom.speakerNotes.setAttribute( 'tabindex', '0' );
|
|
||||||
|
|
||||||
// Overlay graphic which is displayed during the paused mode
|
// Overlay graphic which is displayed during the paused mode
|
||||||
dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
|
dom.pauseOverlay = createSingletonNode( dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null );
|
||||||
|
@ -452,10 +452,6 @@ export default function( revealElement, options ) {
|
||||||
resume();
|
resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
if( config.showNotes ) {
|
|
||||||
dom.speakerNotes.setAttribute( 'data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline' );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( config.mouseWheel ) {
|
if( config.mouseWheel ) {
|
||||||
document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
|
document.addEventListener( 'DOMMouseScroll', onDocumentMouseScroll, false ); // FF
|
||||||
document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
|
document.addEventListener( 'mousewheel', onDocumentMouseScroll, false );
|
||||||
|
@ -506,14 +502,6 @@ export default function( revealElement, options ) {
|
||||||
autoSlidePaused = false;
|
autoSlidePaused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the state of our fragments
|
|
||||||
if( config.fragments === false ) {
|
|
||||||
fragments.disable();
|
|
||||||
}
|
|
||||||
else if( oldConfig.fragments === false ) {
|
|
||||||
fragments.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the navigation mode to the DOM so we can adjust styling
|
// Add the navigation mode to the DOM so we can adjust styling
|
||||||
if( config.navigationMode !== 'default' ) {
|
if( config.navigationMode !== 'default' ) {
|
||||||
dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
|
dom.wrapper.setAttribute( 'data-navigation-mode', config.navigationMode );
|
||||||
|
@ -522,8 +510,10 @@ export default function( revealElement, options ) {
|
||||||
dom.wrapper.removeAttribute( 'data-navigation-mode' );
|
dom.wrapper.removeAttribute( 'data-navigation-mode' );
|
||||||
}
|
}
|
||||||
|
|
||||||
slideNumber.refreshVisibility();
|
notes.configure( config, oldConfig );
|
||||||
keyboard.refreshSortcuts();
|
fragments.configure( config, oldConfig );
|
||||||
|
slideNumber.configure( config, oldConfig );
|
||||||
|
keyboard.configure( config, oldConfig );
|
||||||
|
|
||||||
sync();
|
sync();
|
||||||
|
|
||||||
|
@ -1409,11 +1399,10 @@ export default function( revealElement, options ) {
|
||||||
|
|
||||||
updateControls();
|
updateControls();
|
||||||
updateProgress();
|
updateProgress();
|
||||||
updateNotes();
|
|
||||||
|
|
||||||
|
notes.update();
|
||||||
backgrounds.update();
|
backgrounds.update();
|
||||||
backgrounds.updateParallax();
|
backgrounds.updateParallax();
|
||||||
|
|
||||||
slideNumber.update();
|
slideNumber.update();
|
||||||
fragments.update();
|
fragments.update();
|
||||||
|
|
||||||
|
@ -1475,9 +1464,9 @@ export default function( revealElement, options ) {
|
||||||
updateControls();
|
updateControls();
|
||||||
updateProgress();
|
updateProgress();
|
||||||
updateSlidesVisibility();
|
updateSlidesVisibility();
|
||||||
updateNotesVisibility();
|
|
||||||
updateNotes();
|
|
||||||
|
|
||||||
|
notes.update();
|
||||||
|
notes.updateVisibility();
|
||||||
backgrounds.update( true );
|
backgrounds.update( true );
|
||||||
slideNumber.update();
|
slideNumber.update();
|
||||||
slideContent.formatEmbeddedContent();
|
slideContent.formatEmbeddedContent();
|
||||||
|
@ -1514,7 +1503,7 @@ export default function( revealElement, options ) {
|
||||||
slideContent.load( slide );
|
slideContent.load( slide );
|
||||||
|
|
||||||
backgrounds.update();
|
backgrounds.update();
|
||||||
updateNotes();
|
notes.update();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1767,49 +1756,6 @@ export default function( revealElement, options ) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick up notes from the current slide and display them
|
|
||||||
* to the viewer.
|
|
||||||
*
|
|
||||||
* @see {@link config.showNotes}
|
|
||||||
*/
|
|
||||||
function updateNotes() {
|
|
||||||
|
|
||||||
if( config.showNotes && dom.speakerNotes && currentSlide && !print.isPrintingPDF() ) {
|
|
||||||
|
|
||||||
dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the visibility of the speaker notes sidebar that
|
|
||||||
* is used to share annotated slides. The notes sidebar is
|
|
||||||
* only visible if showNotes is true and there are notes on
|
|
||||||
* one or more slides in the deck.
|
|
||||||
*/
|
|
||||||
function updateNotesVisibility() {
|
|
||||||
|
|
||||||
if( config.showNotes && hasNotes() ) {
|
|
||||||
dom.wrapper.classList.add( 'show-notes' );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dom.wrapper.classList.remove( 'show-notes' );
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there are speaker notes for ANY slide in the
|
|
||||||
* presentation.
|
|
||||||
*/
|
|
||||||
function hasNotes() {
|
|
||||||
|
|
||||||
return dom.slides.querySelectorAll( '[data-notes], aside.notes' ).length > 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the progress bar to reflect the current slide.
|
* Updates the progress bar to reflect the current slide.
|
||||||
*/
|
*/
|
||||||
|
@ -2036,18 +1982,6 @@ export default function( revealElement, options ) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this presentation is running inside of the
|
|
||||||
* speaker notes window.
|
|
||||||
*
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
function isSpeakerNotes() {
|
|
||||||
|
|
||||||
return !!window.location.search.match( /receiver/gi );
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the h/v location and fragment of the current,
|
* Retrieves the h/v location and fragment of the current,
|
||||||
* or specified, slide.
|
* or specified, slide.
|
||||||
|
@ -2227,32 +2161,6 @@ export default function( revealElement, options ) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the speaker notes from a slide. Notes can be
|
|
||||||
* defined in two ways:
|
|
||||||
* 1. As a data-notes attribute on the slide <section>
|
|
||||||
* 2. As an <aside class="notes"> inside of the slide
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} [slide=currentSlide]
|
|
||||||
* @return {(string|null)}
|
|
||||||
*/
|
|
||||||
function getSlideNotes( slide = currentSlide ) {
|
|
||||||
|
|
||||||
// Notes can be specified via the data-notes attribute...
|
|
||||||
if( slide.hasAttribute( 'data-notes' ) ) {
|
|
||||||
return slide.getAttribute( 'data-notes' );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... or using an <aside class="notes"> element
|
|
||||||
let notesElement = slide.querySelector( 'aside.notes' );
|
|
||||||
if( notesElement ) {
|
|
||||||
return notesElement.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the current state of the presentation as
|
* Retrieves the current state of the presentation as
|
||||||
* an object. This state can then be restored at any
|
* an object. This state can then be restored at any
|
||||||
|
@ -2789,7 +2697,7 @@ export default function( revealElement, options ) {
|
||||||
// State checks
|
// State checks
|
||||||
isPaused,
|
isPaused,
|
||||||
isAutoSliding,
|
isAutoSliding,
|
||||||
isSpeakerNotes,
|
isSpeakerNotes: notes.isSpeakerNotes.bind( notes ),
|
||||||
isOverview: overview.isActive.bind( overview ),
|
isOverview: overview.isActive.bind( overview ),
|
||||||
isPrintingPDF: print.isPrintingPDF.bind( print ),
|
isPrintingPDF: print.isPrintingPDF.bind( print ),
|
||||||
|
|
||||||
|
@ -2832,7 +2740,7 @@ export default function( revealElement, options ) {
|
||||||
getSlideBackground,
|
getSlideBackground,
|
||||||
|
|
||||||
// Returns the speaker notes string for a slide, or null
|
// Returns the speaker notes string for a slide, or null
|
||||||
getSlideNotes,
|
getSlideNotes: notes.getSlideNotes.bind( notes ),
|
||||||
|
|
||||||
// Returns an array with all horizontal/vertical slides in the deck
|
// Returns an array with all horizontal/vertical slides in the deck
|
||||||
getHorizontalSlides,
|
getHorizontalSlides,
|
||||||
|
@ -2876,7 +2784,7 @@ export default function( revealElement, options ) {
|
||||||
// Helper method, retrieves query string as a key:value map
|
// Helper method, retrieves query string as a key:value map
|
||||||
getQueryHash,
|
getQueryHash,
|
||||||
|
|
||||||
// Returns the top-level DOM element
|
// Returns reveal.js DOM elements
|
||||||
getRevealElement: () => dom.wrapper || document.querySelector( '.reveal' ),
|
getRevealElement: () => dom.wrapper || document.querySelector( '.reveal' ),
|
||||||
getSlidesElement: () => dom.slides,
|
getSlidesElement: () => dom.slides,
|
||||||
getBackgroundsElement: () => dom.background,
|
getBackgroundsElement: () => dom.background,
|
||||||
|
@ -2892,6 +2800,7 @@ export default function( revealElement, options ) {
|
||||||
announceStatus,
|
announceStatus,
|
||||||
getStatusText,
|
getStatusText,
|
||||||
|
|
||||||
|
print,
|
||||||
location,
|
location,
|
||||||
overview,
|
overview,
|
||||||
fragments,
|
fragments,
|
||||||
|
|
Loading…
Reference in New Issue