From f22e5f85e8284aaca724c372e8f9efdfd9b11943 Mon Sep 17 00:00:00 2001 From: Hakim El Hattab Date: Mon, 8 Oct 2012 00:08:50 -0400 Subject: [PATCH] new paused mode feature (closes #144), controls and progress DOM elements are no longer required in HTML --- css/reveal.css | 38 ++++++++++-- index.html | 24 +++----- js/reveal.js | 154 +++++++++++++++++++++++++++++++++++++---------- js/reveal.min.js | 136 +++++++++++++++++++++-------------------- 4 files changed, 234 insertions(+), 118 deletions(-) diff --git a/css/reveal.css b/css/reveal.css index 2f280bc..d493b22 100644 --- a/css/reveal.css +++ b/css/reveal.css @@ -410,6 +410,7 @@ body { margin-top: -320px; padding: 20px 0px; overflow: visible; + z-index: 1; text-align: center; @@ -921,6 +922,33 @@ body { } +/********************************************* + * PAUSED MODE + *********************************************/ + +.reveal .pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: black; + visibility: hidden; + opacity: 0; + z-index: 100; + + -webkit-transition: all 1s ease; + -moz-transition: all 1s ease; + -ms-transition: all 1s ease; + -o-transition: all 1s ease; + transition: all 1s ease; +} +.reveal.paused .pause-overlay { + visibility: visible; + opacity: 1; +} + + /********************************************* * FALLBACK *********************************************/ @@ -945,10 +973,10 @@ body { /********************************************* - * DEFAULT STATES + * BACKGROUND STATES *********************************************/ -.state-background { +.reveal .state-background { position: absolute; width: 100%; height: 100%; @@ -960,13 +988,13 @@ body { -o-transition: background 800ms ease; transition: background 800ms ease; } -.alert .state-background { +.alert .reveal .state-background { background: rgba( 200, 50, 30, 0.6 ); } -.soothe .state-background { +.soothe .reveal .state-background { background: rgba( 50, 200, 90, 0.4 ); } -.blackout .state-background { +.blackout .reveal .state-background { background: rgba( 0, 0, 0, 0.6 ); } diff --git a/index.html b/index.html index 1ebea32..b5625c9 100644 --- a/index.html +++ b/index.html @@ -34,9 +34,6 @@
- -
-
@@ -278,6 +275,14 @@ function linkify( selector ) {
+
+

Take a Moment

+

+ Press b or period on your keyboard to enter the 'paused' mode. This mode is helpful when you want to take disctracting slides off the screen + during a presentaion. +

+
+

Stellar Links

    @@ -310,17 +315,6 @@ function linkify( selector ) {

    BY Hakim El Hattab / hakim.se

- - - - - -
@@ -346,7 +340,7 @@ function linkify( selector ) { { src: 'lib/js/showdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, { src: 'lib/js/data-markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, { src: '/socket.io/socket.io.js', async: true, condition: function() { return window.location.host === 'localhost:1947'; } }, - { src: 'plugin/speakernotes/client.js', async: true, condition: function() { return window.location.host === 'localhost:1947'; } }, + { src: 'plugin/speakernotes/client.js', async: true, condition: function() { return window.location.host === 'localhost:1947'; } } ] }); diff --git a/js/reveal.js b/js/reveal.js index 06ecde6..c9ba7b1 100644 --- a/js/reveal.js +++ b/js/reveal.js @@ -1,5 +1,5 @@ /*! - * reveal.js 2.1 r28 + * reveal.js 2.1 r29 * http://lab.hakim.se/reveal-js * MIT licensed * @@ -116,9 +116,61 @@ var Reveal = (function(){ // Copy options over to our config object extend( config, options ); - // Cache references to DOM elements + // Make sure we've got all the DOM elements we need + setupDOM(); + + // Hide the address bar in mobile browsers + hideAddressBar(); + + // Loads the dependencies and continues to #start() once done + load(); + + } + + /** + * Finds and stores references to DOM elements which are + * required by the presentation. If a required element is + * not found, it is created. + */ + function setupDOM() { + // Cache references to key DOM elements dom.theme = document.querySelector( '#theme' ); dom.wrapper = document.querySelector( '.reveal' ); + + // Progress bar + if( !dom.wrapper.querySelector( '.progress' ) && config.progress ) { + var progressElement = document.createElement( 'div' ); + progressElement.classList.add( 'progress' ); + progressElement.innerHTML = ''; + dom.wrapper.appendChild( progressElement ); + } + + // Arrow controls + if( !dom.wrapper.querySelector( '.controls' ) && config.controls ) { + var controlsElement = document.createElement( 'aside' ); + controlsElement.classList.add( 'controls' ); + controlsElement.innerHTML = '' + + '' + + '' + + ''; + dom.wrapper.appendChild( controlsElement ); + } + + // Presentation background element + if( !dom.wrapper.querySelector( '.state-background' ) ) { + var backgroundElement = document.createElement( 'div' ); + backgroundElement.classList.add( 'state-background' ); + dom.wrapper.appendChild( backgroundElement ); + } + + // Overlay graphic which is displayed during the paused mode + if( !dom.wrapper.querySelector( '.pause-overlay' ) ) { + var pausedElement = document.createElement( 'div' ); + pausedElement.classList.add( 'pause-overlay' ); + dom.wrapper.appendChild( pausedElement ); + } + + // Cache references to elements dom.progress = document.querySelector( '.reveal .progress' ); dom.progressbar = document.querySelector( '.reveal .progress span' ); @@ -129,11 +181,12 @@ var Reveal = (function(){ dom.controlsUp = document.querySelector( '.reveal .controls .up' ); dom.controlsDown = document.querySelector( '.reveal .controls .down' ); } + } - // Loads the dependencies and continues to #start() once done - load(); - - // Set up hiding of the browser address bar + /** + * Hides the address bar if we're on a mobile device. + */ + function hideAddressBar() { if( navigator.userAgent.match( /(iphone|ipod|android)/i ) ) { // Give the page some scrollable overflow document.documentElement.style.overflow = 'scroll'; @@ -143,7 +196,6 @@ var Reveal = (function(){ window.addEventListener( 'load', removeAddressBar, false ); window.addEventListener( 'orientationchange', removeAddressBar, false ); } - } /** @@ -378,9 +430,11 @@ var Reveal = (function(){ // end case 35: navigateTo( Number.MAX_VALUE ); break; // space - case 32: overviewIsActive() ? deactivateOverview() : navigateNext(); break; + case 32: isOverviewActive() ? deactivateOverview() : navigateNext(); break; // return - case 13: overviewIsActive() ? deactivateOverview() : triggered = false; break; + case 13: isOverviewActive() ? deactivateOverview() : triggered = false; break; + // b, period + case 66: case 190: togglePause(); break; default: triggered = false; } @@ -532,7 +586,7 @@ var Reveal = (function(){ function onOverviewSlideClicked( event ) { // TODO There's a bug here where the event listeners are not // removed after deactivating the overview. - if( overviewIsActive() ) { + if( isOverviewActive() ) { event.preventDefault(); deactivateOverview(); @@ -652,16 +706,66 @@ var Reveal = (function(){ } } + /** + * Toggles the slide overview mode on and off. + * + * @param {Boolean} override Optional flag which overrides the + * toggle logic and forcibly sets the desired state. True means + * overview is open, false means it's closed. + */ + function toggleOverview( override ) { + if( typeof override === 'boolean' ) { + override ? activateOverview() : deactivateOverview(); + } + else { + isOverviewActive() ? deactivateOverview() : activateOverview(); + } + } + /** * Checks if the overview is currently active. * * @return {Boolean} true if the overview is active, * false otherwise */ - function overviewIsActive() { + function isOverviewActive() { return dom.wrapper.classList.contains( 'overview' ); } + /** + * Enters the paused mode which fades everything on screen to + * black. + */ + function pause() { + dom.wrapper.classList.add( 'paused' ); + } + + /** + * Exits from the paused mode. + */ + function resume() { + dom.wrapper.classList.remove( 'paused' ); + } + + /** + * Toggles the paused mode on and off. + */ + function togglePause() { + if( isPaused() ) { + resume(); + } + else { + pause(); + } + } + + /** + * Checks if we are currently in the paused mode. + */ + function isPaused() { + return dom.wrapper.classList.contains( 'paused' ); + } + /** * Updates one dimension of slides by showing the slide * with the specified index. @@ -701,7 +805,7 @@ var Reveal = (function(){ // Optimization; hide all slides that are three or more steps // away from the present slide - if( overviewIsActive() === false ) { + if( isOverviewActive() === false ) { // The distance loops so that it measures 1 between the first // and last slides var distance = Math.abs( ( index - i ) % ( slidesLength - 3 ) ) || 0; @@ -801,7 +905,7 @@ var Reveal = (function(){ } // If the overview is active, re-activate it to update positions - if( overviewIsActive() ) { + if( isOverviewActive() ) { activateOverview(); } @@ -1024,28 +1128,28 @@ var Reveal = (function(){ function navigateLeft() { // Prioritize hiding fragments - if( overviewIsActive() || previousFragment() === false ) { + if( isOverviewActive() || previousFragment() === false ) { slide( indexh - 1, 0 ); } } function navigateRight() { // Prioritize revealing fragments - if( overviewIsActive() || nextFragment() === false ) { + if( isOverviewActive() || nextFragment() === false ) { slide( indexh + 1, 0 ); } } function navigateUp() { // Prioritize hiding fragments - if( overviewIsActive() || previousFragment() === false ) { + if( isOverviewActive() || previousFragment() === false ) { slide( indexh, indexv - 1 ); } } function navigateDown() { // Prioritize revealing fragments - if( overviewIsActive() || nextFragment() === false ) { + if( isOverviewActive() || nextFragment() === false ) { slide( indexh, indexv + 1 ); } } @@ -1088,22 +1192,6 @@ var Reveal = (function(){ // another timeout cueAutoSlide(); } - - /** - * Toggles the slide overview mode on and off. - * - * @param {Boolean} override Optional flag which overrides the - * toggle logic and forcibly sets the desired state. True means - * overview is open, false means it's closed. - */ - function toggleOverview( override ) { - if( typeof override === 'boolean' ) { - override ? activateOverview() : deactivateOverview(); - } - else { - overviewIsActive() ? deactivateOverview() : activateOverview(); - } - } // Expose some methods publicly return { diff --git a/js/reveal.min.js b/js/reveal.min.js index 1a84333..0b8f16f 100644 --- a/js/reveal.min.js +++ b/js/reveal.min.js @@ -1,72 +1,78 @@ /*! - * reveal.js 2.1 r28 + * reveal.js 2.1 r29 * http://lab.hakim.se/reveal-js * MIT licensed * * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se */ -var Reveal=(function(){var j=".reveal .slides>section",b=".reveal .slides>section.present>section",M={controls:true,progress:true,history:false,keyboard:true,overview:true,loop:false,autoSlide:0,mouseWheel:true,rollingLinks:true,theme:"default",transition:"default",dependencies:[]},k=0,c=0,v,D,ab=[],d={},O="WebkitPerspective" in document.body.style||"MozPerspective" in document.body.style||"msPerspective" in document.body.style||"OPerspective" in document.body.style||"perspective" in document.body.style,l="WebkitTransform" in document.body.style||"MozTransform" in document.body.style||"msTransform" in document.body.style||"OTransform" in document.body.style||"transform" in document.body.style,w=0,i=0,z=0,V={startX:0,startY:0,startSpan:0,startCount:0,handled:false,threshold:40}; -function g(ac){if((!l&&!O)){document.body.setAttribute("class","no-transforms");return;}q(M,ac);d.theme=document.querySelector("#theme");d.wrapper=document.querySelector(".reveal"); -d.progress=document.querySelector(".reveal .progress");d.progressbar=document.querySelector(".reveal .progress span");if(M.controls){d.controls=document.querySelector(".reveal .controls"); -d.controlsLeft=document.querySelector(".reveal .controls .left");d.controlsRight=document.querySelector(".reveal .controls .right");d.controlsUp=document.querySelector(".reveal .controls .up"); -d.controlsDown=document.querySelector(".reveal .controls .down");}Q();if(navigator.userAgent.match(/(iphone|ipod|android)/i)){document.documentElement.style.overflow="scroll"; -document.body.style.height="120%";window.addEventListener("load",W,false);window.addEventListener("orientationchange",W,false);}}function Q(){var ad=[],ah=[]; -for(var ae=0,ac=M.dependencies.length;aeV.threshold){V.handled=true;if(agV.threshold&&Math.abs(ad)>Math.abs(ac)){V.handled=true;y();}else{if(ad<-V.threshold&&Math.abs(ad)>Math.abs(ac)){V.handled=true;h();}else{if(ac>V.threshold){V.handled=true; -r();}else{if(ac<-V.threshold){V.handled=true;C();}}}}}}ah.preventDefault();}else{if(navigator.userAgent.match(/android/gi)){ah.preventDefault();}}}function R(ac){V.handled=false; -}function m(ac){clearTimeout(w);w=setTimeout(function(){var ad=ac.detail||-ac.wheelDelta;if(ad>0){u();}else{T();}},100);}function t(ac){G();}function A(ac){if(U()){ac.preventDefault(); -X();k=this.getAttribute("data-index-h");c=this.getAttribute("data-index-v");a();}}function I(){if(O&&!("msPerspective" in document.body.style)){var ad=document.querySelectorAll(".reveal .slides section a:not(.image)"); -for(var ae=0,ac=ad.length;ae'+af.innerHTML+"";}}}}function F(){if(M.overview){d.wrapper.classList.add("overview");var ac=document.querySelectorAll(j); -for(var ah=0,af=ac.length;ah3?"none":"block";}ah[ag].classList.remove("past");ah[ag].classList.remove("present");ah[ag].classList.remove("future");if(agaf){ah[ag].classList.add("future");}}if(ac.querySelector("section")){ah[ag].classList.add("stack");}}ah[af].classList.add("present");var ae=ah[af].getAttribute("data-state"); -if(ae){ab=ab.concat(ae.split(" "));}}else{af=0;}return af;}function a(ai,am){v=D;var af=ab.concat();ab.length=0;var al=k,ad=c;k=aa(j,ai===undefined?k:ai); -c=aa(b,am===undefined?c:am);stateLoop:for(var ag=0,aj=ab.length;ag0,right:k0,down:c0||c>0){ac+=k;}if(c>0){ac+="/"+c;}window.location.hash=ac;}}function s(){if(document.querySelector(b+".present")){var ad=document.querySelectorAll(b+".present .fragment:not(.visible)"); -if(ad.length){ad[0].classList.add("visible");o("fragmentshown",{fragment:ad[0]});return true;}}else{var ac=document.querySelectorAll(j+".present .fragment:not(.visible)"); -if(ac.length){ac[0].classList.add("visible");o("fragmentshown",{fragment:ac[0]});return true;}}return false;}function L(){if(document.querySelector(b+".present")){var ad=document.querySelectorAll(b+".present .fragment.visible"); -if(ad.length){ad[ad.length-1].classList.remove("visible");o("fragmenthidden",{fragment:ad[ad.length-1]});return true;}}else{var ac=document.querySelectorAll(j+".present .fragment.visible"); -if(ac.length){ac[ac.length-1].classList.remove("visible");o("fragmenthidden",{fragment:ac[ac.length-1]});return true;}}return false;}function J(){clearTimeout(i); -if(M.autoSlide){i=setTimeout(u,M.autoSlide);}}function K(ad,ac){a(ad,ac);}function y(){if(U()||L()===false){a(k-1,0);}}function h(){if(U()||s()===false){a(k+1,0); -}}function r(){if(U()||L()===false){a(k,c-1);}}function C(){if(U()||s()===false){a(k,c+1);}}function T(){if(L()===false){if(e().up){r();}else{var ac=document.querySelector(".reveal .slides>section.past:nth-child("+k+")"); -if(ac){c=(ac.querySelectorAll("section").length+1)||0;k--;a();}}}}function u(){if(s()===false){e().down?C():h();}J();}function S(ac){if(typeof ac==="boolean"){ac?F():X(); -}else{U()?X():F();}}return{initialize:g,navigateTo:K,navigateLeft:y,navigateRight:h,navigateUp:r,navigateDown:C,navigatePrev:T,navigateNext:u,toggleOverview:S,addEventListeners:B,removeEventListeners:P,getIndices:function(ac){var ag=k,ae=c; -if(ac){var ah=!!ac.parentNode.nodeName.match(/section/gi);var af=ah?ac.parentNode:ac;var ad=Array.prototype.slice.call(document.querySelectorAll(j));ag=Math.max(ad.indexOf(af),0); -if(ah){ae=Math.max(Array.prototype.slice.call(ac.parentNode.children).indexOf(ac),0);}}return{h:ag,v:ae};},getPreviousSlide:function(){return v;},getCurrentSlide:function(){return D; -},getQueryHash:function(){var ac={};location.search.replace(/[A-Z0-9]+?=(\w*)/gi,function(ad){ac[ad.split("=").shift()]=ad.split("=").pop();});return ac; -},addEventListener:function(ad,ae,ac){if("addEventListener" in window){(d.wrapper||document.querySelector(".reveal")).addEventListener(ad,ae,ac);}},removeEventListener:function(ad,ae,ac){if("addEventListener" in window){(d.wrapper||document.querySelector(".reveal")).removeEventListener(ad,ae,ac); +var Reveal=(function(){var l=".reveal .slides>section",b=".reveal .slides>section.present>section",R={controls:true,progress:true,history:false,keyboard:true,overview:true,loop:false,autoSlide:0,mouseWheel:true,rollingLinks:true,theme:"default",transition:"default",dependencies:[]},m=0,e=0,y,G,ah=[],f={},T="WebkitPerspective" in document.body.style||"MozPerspective" in document.body.style||"msPerspective" in document.body.style||"OPerspective" in document.body.style||"perspective" in document.body.style,n="WebkitTransform" in document.body.style||"MozTransform" in document.body.style||"msTransform" in document.body.style||"OTransform" in document.body.style||"transform" in document.body.style,z=0,k=0,C=0,aa={startX:0,startY:0,startSpan:0,startCount:0,handled:false,threshold:40}; +function i(ai){if((!n&&!T)){document.body.setAttribute("class","no-transforms");return;}t(R,ai);P();d();V();}function P(){f.theme=document.querySelector("#theme"); +f.wrapper=document.querySelector(".reveal");if(!f.wrapper.querySelector(".progress")&&R.progress){var al=document.createElement("div");al.classList.add("progress"); +al.innerHTML="";f.wrapper.appendChild(al);}if(!f.wrapper.querySelector(".controls")&&R.controls){var ak=document.createElement("aside");ak.classList.add("controls"); +ak.innerHTML=''; +f.wrapper.appendChild(ak);}if(!f.wrapper.querySelector(".state-background")){var aj=document.createElement("div");aj.classList.add("state-background"); +f.wrapper.appendChild(aj);}if(!f.wrapper.querySelector(".pause-overlay")){var ai=document.createElement("div");ai.classList.add("pause-overlay");f.wrapper.appendChild(ai); +}f.progress=document.querySelector(".reveal .progress");f.progressbar=document.querySelector(".reveal .progress span");if(R.controls){f.controls=document.querySelector(".reveal .controls"); +f.controlsLeft=document.querySelector(".reveal .controls .left");f.controlsRight=document.querySelector(".reveal .controls .right");f.controlsUp=document.querySelector(".reveal .controls .up"); +f.controlsDown=document.querySelector(".reveal .controls .down");}}function d(){if(navigator.userAgent.match(/(iphone|ipod|android)/i)){document.documentElement.style.overflow="scroll"; +document.body.style.height="120%";window.addEventListener("load",ab,false);window.addEventListener("orientationchange",ab,false);}}function V(){var aj=[],an=[]; +for(var ak=0,ai=R.dependencies.length;akaa.threshold){aa.handled=true;if(amaa.threshold&&Math.abs(aj)>Math.abs(ai)){aa.handled=true;B();}else{if(aj<-aa.threshold&&Math.abs(aj)>Math.abs(ai)){aa.handled=true;j();}else{if(ai>aa.threshold){aa.handled=true; +u();}else{if(ai<-aa.threshold){aa.handled=true;F();}}}}}}an.preventDefault();}else{if(navigator.userAgent.match(/android/gi)){an.preventDefault();}}}function W(ai){aa.handled=false; +}function p(ai){clearTimeout(z);z=setTimeout(function(){var aj=ai.detail||-ai.wheelDelta;if(aj>0){x();}else{Y();}},100);}function w(ai){J();}function D(ai){if(L()){ai.preventDefault(); +ac();m=this.getAttribute("data-index-h");e=this.getAttribute("data-index-v");a();}}function M(){if(T&&!("msPerspective" in document.body.style)){var aj=document.querySelectorAll(".reveal .slides section a:not(.image)"); +for(var ak=0,ai=aj.length;ak'+al.innerHTML+"";}}}}function I(){if(R.overview){f.wrapper.classList.add("overview");var ai=document.querySelectorAll(l); +for(var an=0,al=ai.length;an3?"none":"block";}an[am].classList.remove("past"); +an[am].classList.remove("present");an[am].classList.remove("future");if(amal){an[am].classList.add("future"); +}}if(ai.querySelector("section")){an[am].classList.add("stack");}}an[al].classList.add("present");var ak=an[al].getAttribute("data-state");if(ak){ah=ah.concat(ak.split(" ")); +}}else{al=0;}return al;}function a(ao,at){y=G;var al=ah.concat();ah.length=0;var ar=m,aj=e;m=ag(l,ao===undefined?m:ao);e=ag(b,at===undefined?e:at);stateLoop:for(var am=0,ap=ah.length; +am0,right:m0,down:e0||e>0){ai+=m;}if(e>0){ai+="/"+e;}window.location.hash=ai;}}function v(){if(document.querySelector(b+".present")){var aj=document.querySelectorAll(b+".present .fragment:not(.visible)"); +if(aj.length){aj[0].classList.add("visible");r("fragmentshown",{fragment:aj[0]});return true;}}else{var ai=document.querySelectorAll(l+".present .fragment:not(.visible)"); +if(ai.length){ai[0].classList.add("visible");r("fragmentshown",{fragment:ai[0]});return true;}}return false;}function Q(){if(document.querySelector(b+".present")){var aj=document.querySelectorAll(b+".present .fragment.visible"); +if(aj.length){aj[aj.length-1].classList.remove("visible");r("fragmenthidden",{fragment:aj[aj.length-1]});return true;}}else{var ai=document.querySelectorAll(l+".present .fragment.visible"); +if(ai.length){ai[ai.length-1].classList.remove("visible");r("fragmenthidden",{fragment:ai[ai.length-1]});return true;}}return false;}function N(){clearTimeout(k); +if(R.autoSlide){k=setTimeout(x,R.autoSlide);}}function O(aj,ai){a(aj,ai);}function B(){if(L()||Q()===false){a(m-1,0);}}function j(){if(L()||v()===false){a(m+1,0); +}}function u(){if(L()||Q()===false){a(m,e-1);}}function F(){if(L()||v()===false){a(m,e+1);}}function Y(){if(Q()===false){if(g().up){u();}else{var ai=document.querySelector(".reveal .slides>section.past:nth-child("+m+")"); +if(ai){e=(ai.querySelectorAll("section").length+1)||0;m--;a();}}}}function x(){if(v()===false){g().down?F():j();}N();}return{initialize:i,navigateTo:O,navigateLeft:B,navigateRight:j,navigateUp:u,navigateDown:F,navigatePrev:Y,navigateNext:x,toggleOverview:X,addEventListeners:E,removeEventListeners:U,getIndices:function(ai){var am=m,ak=e; +if(ai){var an=!!ai.parentNode.nodeName.match(/section/gi);var al=an?ai.parentNode:ai;var aj=Array.prototype.slice.call(document.querySelectorAll(l));am=Math.max(aj.indexOf(al),0); +if(an){ak=Math.max(Array.prototype.slice.call(ai.parentNode.children).indexOf(ai),0);}}return{h:am,v:ak};},getPreviousSlide:function(){return y;},getCurrentSlide:function(){return G; +},getQueryHash:function(){var ai={};location.search.replace(/[A-Z0-9]+?=(\w*)/gi,function(aj){ai[aj.split("=").shift()]=aj.split("=").pop();});return ai; +},addEventListener:function(aj,ak,ai){if("addEventListener" in window){(f.wrapper||document.querySelector(".reveal")).addEventListener(aj,ak,ai);}},removeEventListener:function(aj,ak,ai){if("addEventListener" in window){(f.wrapper||document.querySelector(".reveal")).removeEventListener(aj,ak,ai); }}};})(); \ No newline at end of file