blob: 9a4955928f60f5c12f6e575da7b659f7aaca9ef4 [file] [log] [blame]
/**
* @license
* Copyright 2016 Leif Olsen. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This code is built with Google Material Design Lite,
* which is Licensed under the Apache License, Version 2.0
*/
/**
* A lightboard is a translucent surface illuminated from behind, used for situations
* where a shape laid upon the surface needs to be seen with high contrast. In the "old days" of photography
* photograpers used a lightboard to get a quick view of their slides. The goal is to create a responsive lightbox
* design, based on flex layout, similar to what is used in Adobe LightRoom to browse images.
*/
import {
VK_ENTER,
VK_SPACE,
VK_END,
VK_HOME,
VK_ARROW_LEFT,
VK_ARROW_UP,
VK_ARROW_RIGHT,
VK_ARROW_DOWN,
IS_UPGRADED,
MDL_RIPPLE,
MDL_RIPPLE_COMPONENT,
MDL_RIPPLE_EFFECT,
MDL_RIPPLE_EFFECT_IGNORE_EVENTS
} from '../utils/constants';
const MDL_RIPPLE_CONTAINER = 'mdlext-lightboard__slide__ripple-container';
(function() {
'use strict';
//const LIGHTBOARD = 'mdlext-lightboard';
const LIGHTBOARD_ROLE = 'grid';
const SLIDE = 'mdlext-lightboard__slide';
const SLIDE_ROLE = 'gridcell';
const SLIDE_TABSTOP = 'mdlext-lightboard__slide__frame';
/**
* @constructor
* @param {Element} element The element that will be upgraded.
*/
const MaterialExtLightboard = function MaterialExtLightboard(element) {
// Stores the element.
this.element_ = element;
// Initialize instance.
this.init();
};
window['MaterialExtLightboard'] = MaterialExtLightboard;
// Helpers
const getSlide = element => {
return element ? element.closest(`.${SLIDE}`) : null;
};
// Private methods.
/**
* Select a slide, i.e. set aria-selected="true"
* @param element
* @private
*/
MaterialExtLightboard.prototype.selectSlide_ = function(element) {
const slide = getSlide(element);
if( slide && !slide.hasAttribute('aria-selected') ) {
[...this.element_.querySelectorAll(`.${SLIDE}[aria-selected="true"]`)]
.forEach(selectedSlide => selectedSlide.removeAttribute('aria-selected'));
slide.setAttribute('aria-selected', 'true');
}
};
/**
* Dispatch select event
* @param {Element} slide The slide that caused the event
* @private
*/
MaterialExtLightboard.prototype.dispatchSelectEvent_ = function ( slide ) {
this.element_.dispatchEvent(
new CustomEvent('select', {
bubbles: true,
cancelable: true,
detail: { source: slide }
})
);
};
/**
* Handles custom command event, 'first', 'next', 'prev', 'last', 'select' or upgrade
* @param event. A custom event
* @private
*/
MaterialExtLightboard.prototype.commandHandler_ = function( event ) {
event.preventDefault();
event.stopPropagation();
if(event && event.detail) {
this.command(event.detail);
}
};
// Public methods
/**
* Initialize lightboard slides
* @public
*/
MaterialExtLightboard.prototype.upgradeSlides = function() {
const addRipple = slide => {
// Use slide frame as ripple container
if(!slide.querySelector(`.${MDL_RIPPLE_CONTAINER}`)) {
const a = slide.querySelector(`.${SLIDE_TABSTOP}`);
if(a) {
const rippleContainer = a;
rippleContainer.classList.add(MDL_RIPPLE_CONTAINER);
rippleContainer.classList.add(MDL_RIPPLE_EFFECT);
const ripple = document.createElement('span');
ripple.classList.add(MDL_RIPPLE);
rippleContainer.appendChild(ripple);
componentHandler.upgradeElement(rippleContainer, MDL_RIPPLE_COMPONENT);
}
}
};
const hasRippleEffect = this.element_.classList.contains(MDL_RIPPLE_EFFECT);
[...this.element_.querySelectorAll(`.${SLIDE}`)].forEach( slide => {
slide.setAttribute('role', SLIDE_ROLE);
if(!slide.querySelector('a')) {
slide.setAttribute('tabindex', '0');
}
if(hasRippleEffect) {
addRipple(slide);
}
});
};
MaterialExtLightboard.prototype['upgradeSlides'] = MaterialExtLightboard.prototype.upgradeSlides;
/**
* Execute command
* @param detail
* @public
*/
MaterialExtLightboard.prototype.command = function( detail ) {
const firstSlide = () => {
return this.element_.querySelector(`.${SLIDE}:first-child`);
};
const lastSlide = () => {
return this.element_.querySelector(`.${SLIDE}:last-child`);
};
const nextSlide = () => {
const slide = this.element_.querySelector(`.${SLIDE}[aria-selected="true"]`).nextElementSibling;
return slide ? slide : firstSlide();
};
const prevSlide = () => {
const slide = this.element_.querySelector(`.${SLIDE}[aria-selected="true"]`).previousElementSibling;
return slide ? slide : lastSlide();
};
if(detail && detail.action) {
const { action, target } = detail;
let slide;
switch (action.toLowerCase()) {
case 'select':
slide = getSlide(target);
this.dispatchSelectEvent_(slide);
break;
case 'first':
slide = firstSlide();
break;
case 'next':
slide = nextSlide();
break;
case 'prev':
slide = prevSlide();
break;
case 'last':
slide = lastSlide();
break;
case 'upgrade':
this.upgradeSlides();
break;
default:
throw new Error(`Unknown action "${action}". Action must be one of "first", "next", "prev", "last", "select" or "upgrade"`);
}
if (slide) {
const a = slide.querySelector('a');
if (a) {
a.focus();
}
else {
slide.focus();
}
// Workaround for JSDom testing:
// In JsDom 'element.focus()' does not trigger any focus event
if(!slide.hasAttribute('aria-selected')) {
this.selectSlide_(slide);
}
}
}
};
MaterialExtLightboard.prototype['command'] = MaterialExtLightboard.prototype.command;
/**
* Initialize component
*/
MaterialExtLightboard.prototype.init = function() {
const keydownHandler = event => {
if(event.target !== this.element_) {
let action;
let target;
switch (event.keyCode) {
case VK_HOME:
action = 'first';
break;
case VK_END:
action = 'last';
break;
case VK_ARROW_UP:
case VK_ARROW_LEFT:
action = 'prev';
break;
case VK_ARROW_DOWN:
case VK_ARROW_RIGHT:
action = 'next';
break;
case VK_ENTER:
case VK_SPACE:
action = 'select';
target = event.target;
break;
}
if(action) {
event.preventDefault();
event.stopPropagation();
this.command( { action: action, target: target } );
}
}
};
const clickHandler = event => {
event.preventDefault();
event.stopPropagation();
if(event.target !== this.element_) {
this.command( { action: 'select', target: event.target } );
}
};
const focusHandler = event => {
event.preventDefault();
event.stopPropagation();
if(event.target !== this.element_) {
this.selectSlide_(event.target);
}
};
if (this.element_) {
this.element_.setAttribute('role', LIGHTBOARD_ROLE);
if (this.element_.classList.contains(MDL_RIPPLE_EFFECT)) {
this.element_.classList.add(MDL_RIPPLE_EFFECT_IGNORE_EVENTS);
}
// Remove listeners, just in case ...
this.element_.removeEventListener('command', this.commandHandler_);
this.element_.removeEventListener('keydown', keydownHandler);
this.element_.removeEventListener('click', clickHandler);
this.element_.removeEventListener('focus', focusHandler);
this.element_.addEventListener('command', this.commandHandler_.bind(this), false);
this.element_.addEventListener('keydown', keydownHandler, true);
this.element_.addEventListener('click', clickHandler, true);
this.element_.addEventListener('focus', focusHandler, true);
this.upgradeSlides();
this.element_.classList.add(IS_UPGRADED);
}
};
// The component registers itself. It can assume componentHandler is available
// in the global scope.
/* eslint no-undef: 0 */
/* jshint undef:false */
componentHandler.register({
constructor: MaterialExtLightboard,
classAsString: 'MaterialExtLightboard',
cssClass: 'mdlext-js-lightboard',
widget: true
});
})();