diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/package.json b/package.json index 1a347b2..7f417fc 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,14 @@ }, "homepage": "https://github.com/webslides/webslides#readme", "devDependencies": { + "ava": "^0.19.1", "babel-cli": "^6.24.1", "babel-core": "^6.24.1", "babel-loader": "^6.4.1", "babel-preset-env": "^1.4.0", "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "browser-env": "^2.0.30", "eslint": "^3.19.0", "eslint-loader": "^1.7.1", "npm-run-all": "^4.0.2", @@ -46,7 +49,8 @@ "build": "npm-run-all --parallel build:*", "build:main": "webpack", "build:main.min": "webpack --output-filename [name].min.js -p", - "dev": "webpack-dev-server" + "dev": "webpack-dev-server", + "test": "ava test/*.js" }, "babel": { "presets": [ @@ -54,8 +58,23 @@ "es2015", { "modules": false - } + }, + "@ava/stage-4", + "@ava/transform-test-files" ] ] + }, + "ava": { + "babel": { + "presets": [ + "es2015", + "stage-0", + "react" + ] + }, + "require": [ + "babel-register", + "./test/helpers/setup-browser-env.js" + ] } } diff --git a/src/js/plugins/zoom.js b/src/js/plugins/zoom.js new file mode 100644 index 0000000..fbd1b3c --- /dev/null +++ b/src/js/plugins/zoom.js @@ -0,0 +1,190 @@ +import DOM from '../utils/dom'; +import Keys from '../utils/keys'; +import Slide from '../modules/slide'; + + +const CLASSES = { + ZOOM: 'grid', + DIV: 'column', + WRAP: 'wrap-zoom' +}; + +const ID = 'webslides-zoomed'; + +/** + * Zoom plugin. + */ +export default class Zoom { + /** + * @param {WebSlides} wsInstance The WebSlides instance + * @constructor + */ + constructor(wsInstance) { + /** + * @type {WebSlides} + * @private + */ + this.ws_ = wsInstance; + + /** + * @type {WebSlides} + * @private + */ + this.zws_ = {}; + + /** + * @type {boolean} + * @private + */ + this.isZoomed_ = false; + + this.preBuildZoom_(); + document.body.addEventListener('keydown', this.onKeyDown.bind(this)); + window.addEventListener('resize', this.onWindowResize.bind(this)); + } + + /** + * On key down handler. Will decide if Zoom in or out + * @param {Event} event Key down event + */ + onKeyDown(event) { + if ( !this.isZoomed_ && Keys.MINUS.includes( event.which ) ) { + this.zoomIn(); + } else if ( this.isZoomed_ && Keys.PLUS.includes( event.which ) ) { + this.zoomOut(); + } + } + + /** + * Prepare zoom structure, scales the slides and uses a grid layout + * to show them + */ + preBuildZoom_() { + // Clone #webslides element + this.zws_.el = this.ws_.el.cloneNode(); + this.zws_.el.id = ID; + this.zws_.el.className = CLASSES.ZOOM; + // Clone the slides + this.zws_.slides = [].map.call(this.ws_.slides, + (slide, i) => { + const s_ = slide.el.cloneNode(true); + this.zws_.el.appendChild(s_); + return new Slide(s_, i); + }); + DOM.hide(this.zws_.el); + DOM.after(this.zws_.el, this.ws_.el); + + // Creates the container for each slide + this.zws_.slides.forEach( elem => this.createSlideBlock_(elem)); + } + + /** + * Creates a block structure around the slide + * @param {Element} elem slide element + */ + createSlideBlock_(elem) { + // Wraps the slide around a container + const wrap = DOM.wrap(elem.el, 'div'); + wrap.className = CLASSES.WRAP; + // Slide container, need due to flexbox styles + const div = DOM.wrap(wrap, 'div'); + div.className = CLASSES.DIV; + // Adding some layer for controling click events + const divLayer = document.createElement('div'); + divLayer.className = 'zoom-layer'; + divLayer.addEventListener('click', e => { + this.zoomOut(); + this.ws_.goToSlide(elem.i); + }); + wrap.appendChild(divLayer); + // Slide number + const slideNumber = document.createElement('span'); + slideNumber.className = 'slide-number'; + slideNumber.textContent = `${elem.i+1}`; + div.appendChild(slideNumber); + // Zoom out when click in slide "border" + div.addEventListener('click', this.ws_.toggleZoom); + + this.setSizes_(div, wrap, elem); + } + + /** + * Sets layers size + * @param {Element} div flexbox element + * @param {Element} wrap wrapping element + * @param {Element} elem slide element + */ + setSizes_(div, wrap, elem) { + // Calculates the margins in relation to window width + const divCSS = window.getComputedStyle(div); + const marginW = DOM.parseSize(divCSS.paddingLeft) + + DOM.parseSize(divCSS.paddingRight); + const marginH = DOM.parseSize(divCSS.paddingTop) + + DOM.parseSize(divCSS.paddingBottom); + + // Sets element size: window size - relative margins + const scale = divCSS.width.includes('%') ? + 100 / DOM.parseSize(divCSS.width) : + window.innerWidth / DOM.parseSize(divCSS.width); + if (scale == 1) { + // If the scale is 100% means it is mobile + const wsW = this.ws_.el.clientWidth; + elem.el.style.width = `${(wsW - marginW) * 2}px`; + elem.el.style.height = `${(wsW - marginH) * 1.5}px`; + elem.el.style.minHeight = scale == 1? 'auto' : ''; + // Because of flexbox, wrap height is required + wrap.style.height = `${(wsW - marginH) * 1.5 / 2}px`; + } else { + elem.el.style.width = `${window.innerWidth - marginW * scale}px`; + elem.el.style.height = `${window.innerHeight - marginH * scale}px`; + // Because of flexbox, wrap height is required + wrap.style.height = `${window.innerHeight / scale}px`; + } + } + + /** + * Toggles zoom + */ + toggleZoom() { + if (this.isZoomed_) { + this.zoomOut(); + } else { + this.zoomIn(); + } + } + + /** + * Zoom In the slider, scales the slides and uses a grid layout to show them + */ + zoomIn() { + DOM.hide(this.ws_.el); + DOM.show(this.zws_.el); + this.isZoomed_ = true; + document.body.style.overflow = 'auto'; + } + + /** + * Zoom Out the slider, remove scale from the slides + */ + zoomOut() { + DOM.hide(this.zws_.el); + DOM.show(this.ws_.el); + this.isZoomed_ = false; + document.body.style.overflow = ''; + } + + /** + * When windows resize it is necessary to recalculate layers sizes + * @param {Event} ev + */ + onWindowResize(ev) { + if (this.isZoomed_) this.zoomOut(); + + this.zws_.slides.forEach( elem => { + const wrap = elem.el.parentElement; + const div = wrap.parentElement; + this.setSizes_(div, wrap, elem); + }); + } + +} diff --git a/test/dom.js b/test/dom.js new file mode 100644 index 0000000..a19dad1 --- /dev/null +++ b/test/dom.js @@ -0,0 +1,40 @@ +import test from 'ava'; +import DOM from '../src/js/utils/dom'; + +test('DOM.createNode', t => { + const div = DOM.createNode('div', 'my-id'); + t.is(div.tagName, 'DIV'); + t.is(div.id, 'my-id'); + t.is(div.innerHTML, ''); +}); + +test('DOM.once', t => { + const div = DOM.createNode('div'); + DOM.once(div, 'click', () => div.classList.toggle('ok')); + div.click(); + t.is(div.className, 'ok'); + div.click(); + t.is(div.className, 'ok'); +}); + +test('DOM.hide', t => { + const div = DOM.createNode('div'); + DOM.hide(div); + t.is(div.style.display, 'none'); +}); + +test('DOM.show', t => { + const div = DOM.createNode('div'); + DOM.hide(div); + DOM.show(div); + t.is(div.style.display, ''); +}); + +test('DOM.fireEvent', t => { + const div = DOM.createNode('div'); + div.addEventListener('toggle-class', () => div.classList.toggle('ok')); + DOM.fireEvent(div, 'toggle-class'); + t.is(div.className, 'ok'); + DOM.fireEvent(div, 'toggle-class'); + t.is(div.className, ''); +}); diff --git a/test/helpers/setup-browser-env.js b/test/helpers/setup-browser-env.js new file mode 100644 index 0000000..bf8cd07 --- /dev/null +++ b/test/helpers/setup-browser-env.js @@ -0,0 +1,2 @@ +import browserEnv from 'browser-env'; +browserEnv();