<file_sep># Runway-React
The `runway-react` package defines a `Gallery` component, which renders a thumbnail gallery and [Lightbox](https://github.com/rcwbr/react-images) viewer to display images processed by the `runway-gallery` tool.

## Usage
Example usage of the `runway-react Gallery` component can be found in the [test-runway](../test-runway/README.md) package.

The simplest use case of the `runway-react Gallery` component passes only the config prop:
```JavaScript
import Runway from 'runway-react'
<Runway config={runwayConfig} />
```
In this case, the `Gallery` defaults to render only the thumbnail gallery, with no Lightbox. `runwayConfig` is an object representation of the output of the `runway-gallery` tool, usually passed through the build tool. Here is an example using the [`runway-webpack-loader`](../runway-webpack-loader/README.md):
```JavaScript
import Runway from 'runway-react'
import runwayConfig from 'runway-webpack-loader!./gallery.runwayconf.json'
<Runway config={runwayConfig} />
```
Here, `gallery.runwayconf.json` would be a config file used as input to the `runway-gallery` tool. It could have this content:
```JSON
{
	"name": "My Gallery",
	"images": [
		{
			"filename": "image.jpg"
		}
	]
}
```
The `.runwayconf` pre-extension is not required when using the inline `runway-webpack-loader!` loader directive. However, Webpack can also be directed, in the `webpack.config.js` file, to use the `runway-webpack-loader` for any files with a `.runwayconf` pre-extension, such that the import could instead be just
```JavaScript
import runwayConfig from './gallery.runwayconf.json'
```
(see [`runway-webpack-loader`](../runway-webpack-loader/README.md))

### Input
A `runway-react` component receives configuration for the component itself, defining details such as whether the gallery should render images in an overlay when clicked, as well as the configuration details for the gallery rendered by the component.
#### runway-react configuration
Example:
```JavaScript
import Runway from 'runway-react'
<Runway
	lightbox={true}
	thumbImageType={(props) => {
		return <img src={props.metadata.src} />
	}}
	lightboxImageType={(props) => {
		return <img src={props.metadata.src} />
	}}
/>
```

Property | Type | Default | Description
:--------|:----:|:-------:|:-----------
lightbox | boolean | `false` | Show image lightbox on image click.
thumbImageType | component | `ImagePlaceholder` | Optional. Must define image component for the thumbnail gallery based on the `props`. See `props` fields below.
lightboxImageType | component | `ScaledImagePlaceholder` | Optional. Must define image component for the lightbox based on the `props`. See `props` fields below.

`thumbImageType` and `lightboxImageType` components receive `props` with the following fields:

Property | Type | Description
:--------|:----:|:-----------
metadata.src | string | The filename of the image (or path and filename, if applicable)
metadata.width | integer | Width, in pixels, of the image file.
metadata.height | integer | Height, in pixels, of the image file.
width | string | (`thumbImageComponent` only) Width integer with CSS units, representing the exact width the image should be rendered to.
height | string | (`thumbImageComponent` only) Height integer with CSS units, representing the exact height the image should be rendered to.
maxWidth | string | (`lightboxImageComponent` only) Width integer with CSS units to which the image should scale down to fit within. The rendered image's width may be smaller if the scale factor to keep its height within the `maxHeight` parameter requires it.
maxHeight | string | (`lightboxImageComponent` only) Height integer with CSS units to which the image should scale down to fit within. The rendered image's height may be smaller if the scale factor to keep its width within the `maxWidth` parameter requires it.

#### Lightbox configuration
If using the lightbox with a `runway-react` component (i.e. with the `lightbox` property set to true), a configuration object will be passed through to the lightbox component. Documentation of this configuration can be found in the [`react-images` package](https://github.com/rcwbr/react-images). Some fields mentioned there cannot be set as props from the `runway-react` component because it sets them internally. These fields are `onClose`, `images`, `isOpen`, `onClickNext`, `onClickPrev` and `onClose`.
```JavaScript
import Runway from 'runway-react'
<Runway
	lightboxConfig = {{
		width: '70vw',
		height: '70vh',
		heightOffset: 120
	}}
/>
```
Additionally, `runway-react` components set the `spinner` property of their lightbox components to an empty `div` by default, because the default image placeholder effectively replaces the function of the spinner. The spinner can be overridden as a field of the `lightboxConfig` property, either to an arbitrary component, or back to the `react-images` default spinner via an `undefined` value:
```JavaScript
import Runway from 'runway-react'
<Runway
	lightboxConfig = {{
		spinner: undefined
	}}
/>
```

Property | Type | Default | Description
:--------|:----:|:-------:|:-----------
width | string | `'90vw'` | The maximum width of images rendered in the lightbox. Must include CSS units in string. Should not exceed `90vh`, or buttons will render on top of images.
height | string | `'100vh'` | The maximum height of images rendered in the lightbox, plus the `heightOffset` property.
heightOffset | integer | `90` | The height (in pixels) subtracted from the `height` property to determine the maximum height of lightbox images, accounting for the height of the close button and image count text.

#### Runway configuration
The configuration for the gallery rendered by a `runway-react` component is passed in through the `config` property. Usually, this property should be defined by the output of a Runway loader, rather than by direct assignment at `runway-react` component instantiation.

Example:
```JavaScript
import Runway from 'runway-react'
<Runway config={{
	name: 'gallery',
	imagesFolder: 'images',
	thumbsFolder: 'images/thumbs',
	width: 960,
	imagesPerRow: 4,
	imageMargins: {
		horiz: 10,
		vert: 10
	},
	images: []
}} />
```

Normally, this property should be read from a Runway loader, as in this example:
```JavaScript
import Runway from 'runway-react'
import runwayConfig from 'runway-webpack-loader!./gallery.runwayconf.json'
<Runway config={runwayConfig} />
```

See [`runway-gallery`](../runway-gallery) for documentation of how to use this configuration.

### Output
The `Gallery` React component is self-contained within a div with an explicit pixel width defined by the `config` prop's `width` field. The height is not self-constrained.

## Implementation details

The image [lightbox](https://github.com/rcwbr/react-images) is part of a very lightly modified version of [jossmac's react-images](https://github.com/jossmac/react-images). The only change is the addition of an optional `component` field of each obect in the Lightbox component's `images` array, which, if present, is rendered in place of the default `<img>` component in the lightbox. This allows the [`react-element-placeholder`](https://todo) to be rendered in the lightbox while the image files load.
<file_sep># Runway-Gallery
`runway-gallery` pre-processes gallery images for static websites. By processing images during site builds, it generates optimally sized thumbnails to minimize site load time without creating any request-time server load.

## Usage
The `runway-gallery` package exports an object with a single attribute, `default`, with a function as the value. That function has two parameters: a configuration object (see [Configuration](Configuration)), and the path in which the gallery photos should be processed (i.e. the directory containing the `imagesFolder` and `thumbsFolder`). It returns a Promise with a resolution parameter of an object that includes all the configuration object's values, and adds the filenames of the processed thumbnail files.

Example usage:
```JavaScript
var runwayGallery = require('runway-gallery')
var galleryConfig = // configuration options object
runwayGallery.default(
	galleryConfig,
	'.'
).then(gallery => {
	// gallery object includes all values from galleryConfig, and adds processed thumbnail filenames
}
```

### Configuration

Example configuration object:
```JSON
{
	"name": "My Gallery",
	"imagesFolder": "images",
	"thumbsFolder": "images/thumbs",
	"width": 960,
	"imagesPerRow": 4,
	"imageMargins": {
		"horiz": 10,
		"vert": 10
	},
	"images": [
		{
			"filename": "image1.jpg"
		}
	]
}
```
Property | Type | Default | Description
:--------|:----:|:-------:|:-----------
name | string | `'gallery'` | The name to be rendered labelling the gallery
imagesFolder | string | `'images'` | The path, relative to the file instantiating the `runway-react` component, to the full-size image files listed in the `images` property
thumbsFolder | string | `'images/thumbs'` | The path, relative to the file instantiating the `runway-react` component, to which `runway-gallery` will save the generated thumbnail image files
width | integer | `960`| The width, in pixels, of the gallery component
imagesPerRow | integer | `4` | The number of images to place in each row of the thumbnail gallery
imageMargins | object | `{ horiz: 10, vert: 10 }` | The horizontal and vertical margins, in pixels, around each gallery thumbnail
images | array | `[]` | The list of images to render in the gallery as represented by an object -- see below for the definition of this object

Each object in the Runway configuration `images` consist of the following fields:

Property | Type | Description
:--------|:----:|:-----------
filename | string | The filename of the image -- the image file must exist in the `imagesFolder` directory
caption | string | (Optional) The caption to be rendered with the image in the overlay

Example `images` array:
```JSON
[
	{
		"filename": "image1.jpg"
	},
	{
		"filename": "iamge2.jpg",
		"caption": "This photo is the second image in this gallery"
	}
]
```

## Use in a build tool

### Webpack
The [`runway-webpack-loader`](../runway-webpack-loader) package defines a loader that wraps `runway-gallery` for use with Webpack.

### Other build tools
Wrappers may be written for `runway-gallery` so that it can be used as a plugin/loader for other build tools used to generate static sites. Use the [`runway-webpack-loader`](../runway-webpack-loader) package as an example of the requirements of such a wrapper.
<file_sep>import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Route } from 'react-router-dom'
import Runway from 'runway-react'
import runwayConfig from 'runway-webpack-loader!./testGallery.runwayconf.json'

class Site extends React.Component {
	render () {
		return (
			<div>
				<Route
					exact path='/'
					component={() => (
						<div>
							<Runway
								lightbox={true}
								config={runwayConfig}
							/>
						</div>
					)}
				/>
			</div>
		)
	}
}

ReactDOM.render(
	<BrowserRouter>
		<Site />
	</BrowserRouter>,
	document.getElementById('app')
)
<file_sep>import fs from 'fs'
import sharp from 'sharp'
import defaultConfig from './galleryConfig.js'

function buildGallery (runwayConf, context, callback) {
	// apply default config to gallery config
	var galleryConfig = defaultConfig(runwayConf)
	context = (context !== '') ? context + '/' : context
	galleryConfig.imagesFolderFullPath = context + galleryConfig.imagesFolder
	galleryConfig.thumbsFolderFullPath = context + galleryConfig.thumbsFolder

	// prepare images folder
	try {
		fs.mkdirSync(galleryConfig.imagesFolderFullPath)
	} catch (err) {
		if (err.code !== 'EEXIST') throw err
	}
	// prepare thumbs folder
	try {
		fs.mkdirSync(galleryConfig.thumbsFolderFullPath)
	} catch (err) {
		if (err.code !== 'EEXIST') throw err
	}

	// read metadata from images
	var metadataPromises = []
	galleryConfig.images.forEach(image => {
		metadataPromises.push(sharp(
			galleryConfig.imagesFolderFullPath + '/' + image.filename
		).metadata())
	})
	// resolve metadata promises
	Promise.all(metadataPromises).then(metadata => {
		// copy image meta into images object
		galleryConfig.images.forEach((image, index) => {
			// image.metadata = metadata[index]
			image.metadata = {}
			image.metadata.width = metadata[index].width
			image.metadata.height = metadata[index].height
		})

		// distribute images into rows in gallery object
		var gallery = {
			rows: []
		}
		var row = {
			height: -1,
			width: 0,
			images: []
		}
		galleryConfig.images.forEach((image, index) => {
			if (row.height === -1 || image.metadata.height < row.height) {
				// scale the running row width total by the new image scale
				row.width *= (image.metadata.height / row.height)
				row.height = image.metadata.height
			}
			row.images.push(image)
			row.width += image.metadata.width * (row.height / image.metadata.height)

			// if row has enough images and enough horizontal pixels, push to gallery
			var marginWidth = (row.images.length - 1) * galleryConfig.imageMargins.horiz
			if (
				row.width + marginWidth >= galleryConfig.width
				&& row.images.length >= galleryConfig.imagesPerRow
			) {
				gallery.rows.push(row)
				row = {
					height: -1,
					width: 0,
					images: []
				}
			}
		})
		// add the last row if it had fewer images than the imagesPerRow setting
		if (row.images.length > 0) {
			gallery.rows.push(row)
		}

		gallery.rows.forEach((row) => {
			// images will be scaled to gallery width less the horizontal image margins
			const galleryImageWidth = (
				galleryConfig.width
				- galleryConfig.imageMargins.horiz * (row.images.length - 1)
			)
			row.images.forEach((image) => {
				// scale image height to row height
				image.scaleFactor = row.height / image.metadata.height
				// scale images to fit in row
				image.scaleFactor *= galleryImageWidth / row.width
				// round down to prevent row overflow
				image.width = Math.floor(image.metadata.width * image.scaleFactor)
				image.height = Math.floor(image.metadata.height * image.scaleFactor)

				// resize image
				sharp(galleryConfig.imagesFolderFullPath + '/' + image.filename)
					.resize(image.width, image.height)
					.toFile(galleryConfig.thumbsFolderFullPath + '/' + image.filename)
			})
		})
		delete galleryConfig.images
		galleryConfig.gallery = gallery
		callback(galleryConfig)
	})
}

export default (runwayConf, context = '') => {
	return new Promise(function (resolve, reject) {
		buildGallery(runwayConf, context, (gallery) => {
			resolve(gallery)
		})
	})
}
<file_sep>const imageDiv = (last, imageMargins) => {
	const allImageDivs = {
		float: 'left',
		cursor: 'pointer'
	}
	const imageDivsExceptLast = {
		marginRight: imageMargins.horiz + 'px'
	}
	return last ?
		allImageDivs
		: {...imageDivsExceptLast, ...allImageDivs}
}

export default {
	imageDiv: imageDiv
}
<file_sep>const rowDiv = (last, imageMargins) => {
	const allRowDivs = {
		clear: 'both',
		overflow: 'auto'
	}
	const rowDivsExceptLast = {
		marginBottom: imageMargins.vert + 'px'
	}
	return last ?
		allRowDivs
		: {...rowDivsExceptLast, ...allRowDivs}
}

export default {
	rowDiv: rowDiv
}
<file_sep>import React from 'react'
import GalleryImage from './components/GalleryImage/index.jsx'
import styles from './styles.js'

export default class GalleryRow extends React.Component {
	render () {
		const galleryConfig = this.props.galleryConfig
		const rowImages = this.props.row.images.map((image, index) => {
			const last = (index === this.props.row.images.length - 1)
			return (
				<GalleryImage
					key = {index}
					image = {image}
					galleryConfig = {galleryConfig}
					openOverlay = {this.props.openOverlay}
					imageComponentConfig = {this.props.imageComponentConfig}
					last = {last}
				/>
			)
		})
		return (
			<div style = {
				styles.rowDiv(this.props.last, this.props.galleryConfig.imageMargins)
			}>
				{rowImages}
			</div>
		)
	}
}
