<commit_before>/**
 * ownCloud - ProtOn files plugin
 *
 * @author Antonio Espinosa
 * @copyright 2013 Protección Online, S.L. info@prot-on.com
 *
 *
 * 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.
 *
 *
 */

OC.Proton = {
    FILE_TYPE_UNPROTECTED:0,
    FILE_TYPE_PROTECTED_DND:1,
    FILE_TYPE_PROTECTED:2,
    FILE_TYPE_UNSUPPORTED:3,
	MIMETYPES: new Array(
		'application/msword',
		'application/msexcel',
		'application/mspowerpoint',
		'application/pdf',
		'image'),
    droppedDown:false,
	UNPROTECTED: /.*\.(docx|xlsx|pptx|pdf|jpg|png|gif|bmp|tiff)$/,
    PROTECTED_DND: /.*\.proton.*?\.(docx|xlsx|pptx|pdf|jpg|png|gif|bmp|tiff)$/,
    PROTECTED: /.*\.proton.*?\..+$/,
    fileTypeGet:function(fileName) {
        if (OC.Proton.PROTECTED_DND.test(fileName)) {
            return OC.Proton.FILE_TYPE_PROTECTED_DND;
        } else if (OC.Proton.PROTECTED.test(fileName)) {
            return OC.Proton.FILE_TYPE_PROTECTED;
        } else if (OC.Proton.UNPROTECTED.test(fileName)) {
            return OC.Proton.FILE_TYPE_UNPROTECTED;
        }
        return OC.Proton.FILE_TYPE_UNSUPPORTED;
    },
    register:function(fileName) {
        // Show drop down
        var $tr          = $('tr').filterAttr('data-file', fileName);
        var appendTo    = $tr.find('td.filename');
        if (OC.Proton.droppedDown) {
            if ($tr.attr('data-id') != $('#proton-dropdown').attr('data-item-source')) {
                OC.Proton.hideDropDown(function () {
                    $tr.addClass('mouseOver');
                    OC.Proton.showDropDown($tr, appendTo);
                });
            } else {
                OC.Proton.hideDropDown();
            }
        } else {
            $tr.addClass('mouseOver');
            OC.Proton.showDropDown($tr, appendTo);
        }
    },
    showDropDown:function($tr, appendTo) {
        var fileName    = $tr.attr('data-file');
        var itemSource  = $tr.attr('data-id');
        var fileType    = OC.Proton.fileTypeGet(fileName);
        var html = '
';
        html += '
';
        if (fileType == OC.Proton.FILE_TYPE_UNPROTECTED) {
            html += '
'+t('files_proton','Protect')+'
';
        } else {
            html += '
'+t('files_proton','Manage permision')+'
';
            html += '
'+t('files_proton','View activity log')+'
';
            html += '
'+t('files_proton','Unprotect')+'
';
        }
        html += '
';
        html += '
';
        $(html).appendTo(appendTo);

        $('#proton-dropdown').each(OC.Proton.setActions);
        $('#proton-dropdown').show('blind', function() {
            OC.Proton.droppedDown = true;
        });
    },
    hideDropDown:function(callback) {
        $('#proton-dropdown').hide('blind', function() {
            OC.Proton.droppedDown = false;
            $('#proton-dropdown').remove();
            if (typeof FileActions !== 'undefined') {
                $('tr').removeClass('mouseOver');
            }
            if (callback) {
                callback.call();
            }
        });
    },
    setActions:function() {
        $(this).find('.proton-action-protect').click(OC.Proton.modifyFileContents('protect', 'Error protecting file'));
        $(this).find('.proton-action-unprotect').click(OC.Proton.modifyFileContents('unprotect', 'Error unprotecting file'));

        $(this).find('.proton-action-view').click(OC.Proton.getUrlAndRedirect('view', 'Error processing file'));
        $(this).find('.proton-action-rights').click(OC.Proton.getUrlAndRedirect('rights', 'Error processing file'));
        $(this).find('.proton-action-activity').click(OC.Proton.getUrlAndRedirect('activity', 'Error processing file'));
    },
    modifyFileContents: function(action, title) {
    	return function(event) {
    		var $tr = $(this).closest('tr');
			var path = $('#dir').val()+"/"+$tr.attr('data-file');
    		var itemSource = $tr.attr('data-id');
	        $.get(OC.filePath('files_proton', 'ajax', 'action.php'),
	            { action: action, itemSource: itemSource, path: path },
	            OC.Proton.checkResult(title, function(result) {
	            	OC.Proton.renameAndRefresh($tr, result);
    			}));
       };
    },
    getUrlAndRedirect: function(action, title) {
    	return function(path, itemSource) {
    		if (!itemSource && typeof path == 'object') {
	    		var $tr = $(this).closest('tr');
				var path = $('#dir').val()+"/"+$tr.attr('data-file');
	    		var itemSource = $tr.attr('data-id');
    		}
	        $.get(OC.filePath('files_proton', 'ajax', 'action.php'),
	            { action: action, itemSource: itemSource, path: path },
	            OC.Proton.checkResult(title,
		            function(result) {
		                if (result && result.status === 'success') {
		                	$.fancybox({
		                		'href'				: result.redirect,
								'width'				: '75%',
								'height'			: '75%',
								'type'				: 'iframe',
								'autoScale'     	: false
							});
		                }
	        		}
	        ));
		};
    },
    renameAndRefresh: function(tr, result) {
    	var newname = result.name;
    	var name = tr.attr('data-file');
		tr.attr('data-file', newname);
		tr.attr('data-size', result.size);
		var td=tr.children('td.filename');
		var path = td.children('a.name').attr('href');
		td.children('a.name').attr('href', path.replace(encodeURIComponent(name), encodeURIComponent(newname)));
		if (newname.indexOf('.') > 0 && tr.attr('data-type') != 'dir') {
			var basename=newname.substr(0,newname.lastIndexOf('.'));
		} else {
			var basename=newname;
		}
		td.find('a.name span.nametext').text(basename);
		if (newname.indexOf('.') > 0 && tr.attr('data-type') != 'dir') {
			if (td.find('a.name span.extension').length == 0 ) {
				td.find('a.name span.nametext').append('');
			}
			td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.')));
		}
		OC.Proton.hideDropDown();
    },
    checkResult: function(title, func) {
    	return function(result) {
			if (!result || result.status == 'error') {
				OC.dialogs.alert(result.data.message, title);
			} else if (func){
				func(result);
			}
		};
    },
    hijackDefault: function () { //Overwrite defaults for our supported files with our filter function
		for (var i = 0; i < OC.Proton.MIMETYPES.length; ++i){
			var mime = OC.Proton.MIMETYPES[i];
			var name = FileActions.defaults[mime];
			if (name) {
				var oldDefault = FileActions.actions[mime][name]['action'];
				var newDefault = OC.Proton.newDefaultGenerator(oldDefault);
				FileActions.actions[mime][name]['action'] = newDefault;
			} else {
				var newDefault = OC.Proton.newDefaultGenerator();
				FileActions.register(mime,'View',OC.PERMISSION_READ,'',function(filename){
					newDefault(filename);
				});
				FileActions.setDefault(mime,'View');
			}
		}
	},
	newDefaultGenerator: function (oldDefault) { //This should show or downoad protected files and download or call existing default for not protected files
		return function(file) {
			var type = OC.Proton.fileTypeGet(file);
			if (type == OC.Proton.FILE_TYPE_PROTECTED_DND) {
				var itemSource = $('tr').filterAttr('data-file', file).attr('data-id');
				var path = $('#dir').val() + "/" + file;
				OC.Proton.getUrlAndRedirect('view', 'Error processing file')(path, itemSource);
			} else if (type != OC.Proton.FILE_TYPE_PROTECTED && oldDefault) {
				oldDefault(file);
			} else {//No Old and protected but no DnD
				window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(file) + '&dir=' + encodeURIComponent($('#dir').val());
			}
		};
	}
};

$(document).ready(function(){

    if (typeof OC.Proton !== 'undefined' && typeof FileActions !== 'undefined') {
		for (var i = 0; i < OC.Proton.MIMETYPES.length; ++i){
			var mime = OC.Proton.MIMETYPES[i];
	        FileActions.register(mime, 'Prot-On', OC.PERMISSION_UPDATE, OC.imagePath('files_proton', 'proton'), OC.Proton.register);
		}
    }

	$(this).click(function(event) {
		var target = $(event.target);
		var isMatched = !target.is('.drop, .ui-datepicker-next, .ui-datepicker-prev, .ui-icon')
			&& !target.closest('#ui-datepicker-div').length;
		if (OC.Proton.droppedDown && isMatched && $('#proton-dropdown').has(event.target).length === 0) {
			OC.Proton.hideDropDown();
		}
	});
	setTimeout(OC.Proton.hijackDefault,100);
});
<commit_msg>Update error titles<commit_after>/**
 * ownCloud - ProtOn files plugin
 *
 * @author Antonio Espinosa
 * @copyright 2013 Protección Online, S.L. info@prot-on.com
 *
 *
 * 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.
 *
 *
 */

OC.Proton = {
    FILE_TYPE_UNPROTECTED:0,
    FILE_TYPE_PROTECTED_DND:1,
    FILE_TYPE_PROTECTED:2,
    FILE_TYPE_UNSUPPORTED:3,
	MIMETYPES: new Array(
		'application/msword',
		'application/msexcel',
		'application/mspowerpoint',
		'application/pdf',
		'image'),
    droppedDown:false,
	UNPROTECTED: /.*\.(docx|xlsx|pptx|pdf|jpg|png|gif|bmp|tiff)$/,
    PROTECTED_DND: /.*\.proton.*?\.(docx|xlsx|pptx|pdf|jpg|png|gif|bmp|tiff)$/,
    PROTECTED: /.*\.proton.*?\..+$/,
    fileTypeGet:function(fileName) {
        if (OC.Proton.PROTECTED_DND.test(fileName)) {
            return OC.Proton.FILE_TYPE_PROTECTED_DND;
        } else if (OC.Proton.PROTECTED.test(fileName)) {
            return OC.Proton.FILE_TYPE_PROTECTED;
        } else if (OC.Proton.UNPROTECTED.test(fileName)) {
            return OC.Proton.FILE_TYPE_UNPROTECTED;
        }
        return OC.Proton.FILE_TYPE_UNSUPPORTED;
    },
    register:function(fileName) {
        // Show drop down
        var $tr          = $('tr').filterAttr('data-file', fileName);
        var appendTo    = $tr.find('td.filename');
        if (OC.Proton.droppedDown) {
            if ($tr.attr('data-id') != $('#proton-dropdown').attr('data-item-source')) {
                OC.Proton.hideDropDown(function () {
                    $tr.addClass('mouseOver');
                    OC.Proton.showDropDown($tr, appendTo);
                });
            } else {
                OC.Proton.hideDropDown();
            }
        } else {
            $tr.addClass('mouseOver');
            OC.Proton.showDropDown($tr, appendTo);
        }
    },
    showDropDown:function($tr, appendTo) {
        var fileName    = $tr.attr('data-file');
        var itemSource  = $tr.attr('data-id');
        var fileType    = OC.Proton.fileTypeGet(fileName);
        var html = '
';
        html += '
';
        if (fileType == OC.Proton.FILE_TYPE_UNPROTECTED) {
            html += '
'+t('files_proton','Protect')+'
';
        } else {
            html += '
'+t('files_proton','Manage permision')+'
';
            html += '
'+t('files_proton','View activity log')+'
';
            html += '
'+t('files_proton','Unprotect')+'
';
        }
        html += '
';
        html += '
';
        $(html).appendTo(appendTo);

        $('#proton-dropdown').each(OC.Proton.setActions);
        $('#proton-dropdown').show('blind', function() {
            OC.Proton.droppedDown = true;
        });
    },
    hideDropDown:function(callback) {
        $('#proton-dropdown').hide('blind', function() {
            OC.Proton.droppedDown = false;
            $('#proton-dropdown').remove();
            if (typeof FileActions !== 'undefined') {
                $('tr').removeClass('mouseOver');
            }
            if (callback) {
                callback.call();
            }
        });
    },
    setActions:function() {
        $(this).find('.proton-action-protect').click(OC.Proton.modifyFileContents('protect', 'Error protecting file'));
        $(this).find('.proton-action-unprotect').click(OC.Proton.modifyFileContents('unprotect', 'Error unprotecting file'));

        $(this).find('.proton-action-view').click(OC.Proton.getUrlAndRedirect('view', 'Error processing file'));
        $(this).find('.proton-action-rights').click(OC.Proton.getUrlAndRedirect('rights', 'Error processing file'));
        $(this).find('.proton-action-activity').click(OC.Proton.getUrlAndRedirect('activity', 'Error processing file'));
    },
    modifyFileContents: function(action, title) {
    	return function(event) {
    		var $tr = $(this).closest('tr');
			var path = $('#dir').val()+"/"+$tr.attr('data-file');
    		var itemSource = $tr.attr('data-id');
	        $.get(OC.filePath('files_proton', 'ajax', 'action.php'),
	            { action: action, itemSource: itemSource, path: path },
	            OC.Proton.checkResult(title, function(result) {
	            	OC.Proton.renameAndRefresh($tr, result);
    			}));
       };
    },
    getUrlAndRedirect: function(action, title) {
    	return function(path, itemSource) {
    		if (!itemSource && typeof path == 'object') {
	    		var $tr = $(this).closest('tr');
				var path = $('#dir').val()+"/"+$tr.attr('data-file');
	    		var itemSource = $tr.attr('data-id');
    		}
	        $.get(OC.filePath('files_proton', 'ajax', 'action.php'),
	            { action: action, itemSource: itemSource, path: path },
	            OC.Proton.checkResult(title,
		            function(result) {
		                if (result && result.status === 'success') {
		                	$.fancybox({
		                		'href'				: result.redirect,
								'width'				: '75%',
								'height'			: '75%',
								'type'				: 'iframe',
								'autoScale'     	: false
							});
		                }
	        		}
	        ));
		};
    },
    renameAndRefresh: function(tr, result) {
    	var newname = result.name;
    	var name = tr.attr('data-file');
		tr.attr('data-file', newname);
		tr.attr('data-size', result.size);
		var td=tr.children('td.filename');
		var path = td.children('a.name').attr('href');
		td.children('a.name').attr('href', path.replace(encodeURIComponent(name), encodeURIComponent(newname)));
		if (newname.indexOf('.') > 0 && tr.attr('data-type') != 'dir') {
			var basename=newname.substr(0,newname.lastIndexOf('.'));
		} else {
			var basename=newname;
		}
		td.find('a.name span.nametext').text(basename);
		if (newname.indexOf('.') > 0 && tr.attr('data-type') != 'dir') {
			if (td.find('a.name span.extension').length == 0 ) {
				td.find('a.name span.nametext').append('');
			}
			td.find('a.name span.extension').text(newname.substr(newname.lastIndexOf('.')));
		}
		OC.Proton.hideDropDown();
    },
    checkResult: function(title, func) {
    	return function(result) {
			if (!result || result.status == 'error') {
				OC.dialogs.alert(result.data.message, t('files_proton',title));
			} else if (func){
				func(result);
			}
		};
    },
    hijackDefault: function () { //Overwrite defaults for our supported files with our filter function
		for (var i = 0; i < OC.Proton.MIMETYPES.length; ++i){
			var mime = OC.Proton.MIMETYPES[i];
			var name = FileActions.defaults[mime];
			if (name) {
				var oldDefault = FileActions.actions[mime][name]['action'];
				var newDefault = OC.Proton.newDefaultGenerator(oldDefault);
				FileActions.actions[mime][name]['action'] = newDefault;
			} else {
				var newDefault = OC.Proton.newDefaultGenerator();
				FileActions.register(mime,'View',OC.PERMISSION_READ,'',function(filename){
					newDefault(filename);
				});
				FileActions.setDefault(mime,'View');
			}
		}
	},
	newDefaultGenerator: function (oldDefault) { //This should show or downoad protected files and download or call existing default for not protected files
		return function(file) {
			var type = OC.Proton.fileTypeGet(file);
			if (type == OC.Proton.FILE_TYPE_PROTECTED_DND) {
				var itemSource = $('tr').filterAttr('data-file', file).attr('data-id');
				var path = $('#dir').val() + "/" + file;
				OC.Proton.getUrlAndRedirect('view', 'Error processing file')(path, itemSource);
			} else if (type != OC.Proton.FILE_TYPE_PROTECTED && oldDefault) {
				oldDefault(file);
			} else {//No Old and protected but no DnD
				window.location = OC.filePath('files', 'ajax', 'download.php') + '?files=' + encodeURIComponent(file) + '&dir=' + encodeURIComponent($('#dir').val());
			}
		};
	}
};

$(document).ready(function(){

    if (typeof OC.Proton !== 'undefined' && typeof FileActions !== 'undefined') {
		for (var i = 0; i < OC.Proton.MIMETYPES.length; ++i){
			var mime = OC.Proton.MIMETYPES[i];
	        FileActions.register(mime, 'Prot-On', OC.PERMISSION_UPDATE, OC.imagePath('files_proton', 'proton'), OC.Proton.register);
		}
    }

	$(this).click(function(event) {
		var target = $(event.target);
		var isMatched = !target.is('.drop, .ui-datepicker-next, .ui-datepicker-prev, .ui-icon')
			&& !target.closest('#ui-datepicker-div').length;
		if (OC.Proton.droppedDown && isMatched && $('#proton-dropdown').has(event.target).length === 0) {
			OC.Proton.hideDropDown();
		}
	});
	setTimeout(OC.Proton.hijackDefault,100);
});
<|endoftext|>


<commit_before>import React, {Component, PropTypes} from 'react'
import pluralizeArea from '../lib/pluralize-area'
import capitalize from 'lodash/string/capitalize'

import {List} from 'immutable'

import AreaOfStudy from './area-of-study'
import AreaPicker from './area-picker'
import Button from './button'

import './area-of-study-group.scss'

export default class AreaOfStudyGroup extends Component {
	static propTypes = {
		addArea: PropTypes.func.isRequired,
		addOverride: PropTypes.func.isRequired,
		allAreas: PropTypes.instanceOf(List).isRequired,
		areas: PropTypes.instanceOf(List).isRequired,
		endAddArea: PropTypes.func.isRequired,
		initiateAddArea: PropTypes.func.isRequired,
		removeArea: PropTypes.func.isRequired,
		removeOverride: PropTypes.func.isRequired,
		showAreaPicker: PropTypes.bool.isRequired,
		toggleOverride: PropTypes.func.isRequired,
		type: PropTypes.oneOf(['degree', 'major', 'concentration', 'emphasis']).isRequired,
	}

	static defaultProps = {
		showAreaPicker: false,
	}

	render() {
		return (

					{capitalize(pluralizeArea(this.props.type))}
					{
						this.props.showAreaPicker
							? <Button className='add-area-of-study' type='flat' onClick={ev => this.props.endAddArea({ev, type: this.props.type})}>
								Close
							</Button>
							: <Button className='add-area-of-study' type='flat' onClick={ev => this.props.initiateAddArea({ev, type: this.props.type})}>
								Add/Edit
							</Button>
					}

				{this.props.areas.map(area =>
					<AreaOfStudy key={area.id}
						{...area}
						removeArea={this.props.removeArea}
						addOverride={this.props.addOverride}
						removeOverride={this.props.removeOverride}
						toggleOverride={this.props.toggleOverride}
						showCloseButton={this.props.showAreaPicker}
					/>).toArray()}

				{this.props.showAreaPicker
					? <AreaPicker
						currentAreas={this.props.areas}
						closePicker={ev => this.props.endAddArea({ev, type: this.props.type})}
						type={this.props.type}
						allAreas={this.props.allAreas}
						addArea={this.props.addArea}
						removeArea={this.props.removeArea} />
					: null}

		)
	}
}
<commit_msg>Reword the "Add/Edit" areas button<commit_after>import React, {Component, PropTypes} from 'react'
import pluralizeArea from '../lib/pluralize-area'
import capitalize from 'lodash/string/capitalize'

import {List} from 'immutable'

import AreaOfStudy from './area-of-study'
import AreaPicker from './area-picker'
import Button from './button'

import './area-of-study-group.scss'

export default class AreaOfStudyGroup extends Component {
	static propTypes = {
		addArea: PropTypes.func.isRequired,
		addOverride: PropTypes.func.isRequired,
		allAreas: PropTypes.instanceOf(List).isRequired,
		areas: PropTypes.instanceOf(List).isRequired,
		endAddArea: PropTypes.func.isRequired,
		initiateAddArea: PropTypes.func.isRequired,
		removeArea: PropTypes.func.isRequired,
		removeOverride: PropTypes.func.isRequired,
		showAreaPicker: PropTypes.bool.isRequired,
		toggleOverride: PropTypes.func.isRequired,
		type: PropTypes.oneOf(['degree', 'major', 'concentration', 'emphasis']).isRequired,
	}

	static defaultProps = {
		showAreaPicker: false,
	}

	render() {
		return (

					{capitalize(pluralizeArea(this.props.type))}
					{
						this.props.showAreaPicker
							? <Button className='add-area-of-study' type='flat' onClick={ev => this.props.endAddArea({ev, type: this.props.type})}>
								Close
							</Button>
							: <Button className='add-area-of-study' type='flat' onClick={ev => this.props.initiateAddArea({ev, type: this.props.type})}>
								{/*'Add/Edit'*/}
								{/*'Add ⋎ Edit'*/}
								{'Add ∨ Edit'}
							</Button>
					}

				{this.props.areas.map(area =>
					<AreaOfStudy key={area.id}
						{...area}
						removeArea={this.props.removeArea}
						addOverride={this.props.addOverride}
						removeOverride={this.props.removeOverride}
						toggleOverride={this.props.toggleOverride}
						showCloseButton={this.props.showAreaPicker}
					/>).toArray()}

				{this.props.showAreaPicker
					? <AreaPicker
						currentAreas={this.props.areas}
						closePicker={ev => this.props.endAddArea({ev, type: this.props.type})}
						type={this.props.type}
						allAreas={this.props.allAreas}
						addArea={this.props.addArea}
						removeArea={this.props.removeArea} />
					: null}

		)
	}
}
<|endoftext|>


<commit_before>const Router = require('koa-router')
const { generatePDF, generateSourceCode } = require('../generator')

const router = new Router({ prefix: '/api' })

/**
 * Generates a PDF from the request body which
 * is then sent to the client.
 */
router.post('/generate/resume', async ctx => {
  ctx.body = generatePDF(ctx.request.body)
  ctx.type = 'application/pdf'
})

/**
 * Generates resume source files from request body and
 * saves it to a zip which is then sent to the client.
 */
router.post('/generate/source', async ctx => {
  ctx.body = generateSourceCode(ctx.request.body)
  ctx.type = 'application/octet-stream'
})

module.exports = router
<commit_msg>use application/zip for source code mimetype<commit_after>const Router = require('koa-router')
const { generatePDF, generateSourceCode } = require('../generator')

const router = new Router({ prefix: '/api' })

/**
 * Generates a PDF from the request body which
 * is then sent to the client.
 */
router.post('/generate/resume', async ctx => {
  ctx.body = generatePDF(ctx.request.body)
  ctx.type = 'application/pdf'
})

/**
 * Generates resume source files from request body and
 * saves it to a zip which is then sent to the client.
 */
router.post('/generate/source', async ctx => {
  ctx.body = generateSourceCode(ctx.request.body)
  ctx.type = 'application/zip'
})

module.exports = router
<|endoftext|>


<commit_before>dojo.provide("dijit._base.popup");

dojo.require("dijit._base.focus");
dojo.require("dijit._base.place");
dojo.require("dijit._base.window");

dijit.popup = new function(){
	// summary:
	//		This class is used to show/hide widgets as popups.
	//

	var stack = [],
		beginZIndex=1000,
		idGen = 1;

	this.open = function(/*Object*/ args){
		// summary:
		//		Popup the widget at the specified position
		//
		// args: Object
		//		popup: Widget
		//			widget to display,
		//		parent: Widget
		//			the button etc. that is displaying this popup
		//		around: DomNode
		//			DOM node (typically a button); place popup relative to this node
		//		orient: Object
		//			structure specifying possible positions of popup relative to "around" node
		//		onCancel: Function
		//			callback when user has canceled the popup by
		//          	1. hitting ESC or
		//				2. by using the popup widget's proprietary cancel mechanism (like a cancel button in a dialog);
		//				   ie: whenever popupWidget.onCancel() is called, args.onCancel is called
		//		onClose: Function
		//			callback whenever this popup is closed (via close(), closeAll(), or closeTo())
		//		submenu: Boolean
		//			Is this a submenu off of the existing popup?
		//		onExecute: Function
		//			callback when user "executed" on the popup/sub-popup by selecting a menu choice, etc. (top menu only)
		//
		// examples:
		//		1. opening at the mouse position
		//			dijit.popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY});
		//		2. opening the widget as a dropdown
		//			dijit.popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}  });
		//
		//	Note that whatever widget called dijit.popup.open() should also listen to it's own _onBlur callback
		//	(fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed.

		var widget = args.popup,
			orient = args.orient || {'BL':'TL', 'TL':'BL'},
			around = args.around,
			id = (args.around && args.around.id) ? (args.around.id+"_dropdown") : ("popup_"+idGen++);

		if(!args.submenu){
			this.closeAll();
		}

		// make wrapper div to hold widget and possibly hold iframe behind it.
		// we can't attach the iframe as a child of the widget.domNode because
		// widget.domNode might be a <table>, <ul>, etc.
		var wrapper = dojo.doc.createElement("div");
		wrapper.id = id;
		wrapper.className="dijitPopup";
		wrapper.style.zIndex = beginZIndex + stack.length;
		if(args.parent){
			wrapper.dijitPopupParent=args.parent.id;
		}
		dojo.body().appendChild(wrapper);

		widget.domNode.style.display="";
		wrapper.appendChild(widget.domNode);

		var iframe = new dijit.BackgroundIframe(wrapper);

		// position the wrapper node
		var best = around ?
			dijit.placeOnScreenAroundElement(wrapper, around, orient, widget.orient ? dojo.hitch(widget, "orient") : null) :
			dijit.placeOnScreen(wrapper, args, orient == 'R' ? ['TR','BR','TL','BL'] : ['TL','BL','TR','BR']);

		// TODO: use effects to fade in wrapper

		var handlers = [];

		// provide default escape key handling
		handlers.push(dojo.connect(wrapper, "onkeypress", this, function(evt){
			if (evt.keyCode == dojo.keys.ESCAPE){
				args.onCancel();
			}
		}));

		// watch for cancel/execute events on the popup and notify the caller
		// (for a menu, "execute" means clicking an item)
		if(widget.onCancel){
			handlers.push(dojo.connect(widget, "onCancel", null, args.onCancel));
		}

		handlers.push(dojo.connect(widget, widget.onExecute ? "onExecute" : "onChange", null, function(){
			if(stack[0] && stack[0].onExecute){
				stack[0].onExecute();
			}
		}));

		stack.push({
			wrapper: wrapper,
			iframe: iframe,
			widget: widget,
			onExecute: args.onExecute,
			onCancel: args.onCancel,
 			onClose: args.onClose,
			handlers: handlers
		});

		if(widget.onOpen){
			widget.onOpen(best);
		}

		return best;
	};

	this.close = function(){
		// summary:
		//		Close popup on the top of the stack (the highest z-index popup)

		// this needs to happen before the stack is popped, because menu's
		// onClose calls closeTo(this)
		var widget = stack[stack.length-1].widget;
		if(widget.onClose){
			widget.onClose();
		}

		if(!stack.length){
			return;
		}
		var top = stack.pop();
		var wrapper = top.wrapper,
			iframe = top.iframe,
			widget = top.widget,
			onClose = top.onClose;

		dojo.forEach(top.handlers, dojo.disconnect);

		// #2685: check if the widget still has a domNode so ContentPane can change its URL without getting an error
		if(!widget||!widget.domNode){ return; }
		dojo.style(widget.domNode, "display", "none");
		dojo.body().appendChild(widget.domNode);
		iframe.destroy();
		dojo._destroyElement(wrapper);

		if(onClose){
			onClose();
		}
	};

	this.closeAll = function(){
		// summary: close every popup, from top of stack down to the first one
		while(stack.length){
			this.close();
		}
	};

	this.closeTo = function(/*Widget*/ widget){
		// summary: closes every popup above specified widget
		while(stack.length && stack[stack.length-1].widget.id != widget.id){
			this.close();
		}
	};

}();

dijit.BackgroundIframe = function(/* HTMLElement */node){
	//	summary:
	//		For IE z-index schenanigans. id attribute is required.
	//
	//	description:
	//		new dijit.BackgroundIframe(node)
	//			Makes a background iframe as a child of node, that fills
	//			area (and position) of node

	if(!node.id){ throw new Error("no id"); }

	if((dojo.isIE && dojo.isIE < 7) || (dojo.isFF && dojo.isFF < 3 && dojo.hasClass(dojo.body(), "dijit_a11y"))){
		var iframe;
		if(dojo.isIE){
			var html="<iframe src='javascript:\"\"'"
				+ " style='position: absolute; left: 0px; top: 0px;"
				+ " width: expression(document.getElementById(\"" + node.id + "\").offsetWidth);"
				+ " height: expression(document.getElementById(\"" + node.id + "\").offsetHeight); "
				+ "z-index: -1; filter:Alpha(Opacity=\"0\");'>";
			iframe = dojo.doc.createElement(html);
		}else{
		 	iframe = dojo.doc.createElement("iframe");
			iframe.src = 'javascript:""';
			iframe.className = "dijitBackgroundIframe";
		}
		iframe.tabIndex = -1; // Magic to prevent iframe from getting focus on tab keypress - as style didnt work.
		node.appendChild(iframe);
		this.iframe = iframe;
	}
};

dojo.extend(dijit.BackgroundIframe, {
	destroy: function(){
		//	summary: destroy the iframe
		if(this.iframe){
			dojo._destroyElement(this.iframe);
			delete this.iframe;
		}
	}
});
<commit_msg>InlineEditBox inherits the block/inline behavior from its parent, which effectively sets a minimum size on the edit widget of one line if the source node was block.  Refs #4093. Patch from Mark Hays.<commit_after>dojo.provide("dijit._base.popup");

dojo.require("dijit._base.focus");
dojo.require("dijit._base.place");
dojo.require("dijit._base.window");

dijit.popup = new function(){
	// summary:
	//		This class is used to show/hide widgets as popups.
	//

	var stack = [],
		beginZIndex=1000,
		idGen = 1;

	this.open = function(/*Object*/ args){
		// summary:
		//		Popup the widget at the specified position
		//
		// args: Object
		//		popup: Widget
		//			widget to display,
		//		parent: Widget
		//			the button etc. that is displaying this popup
		//		around: DomNode
		//			DOM node (typically a button); place popup relative to this node
		//		orient: Object
		//			structure specifying possible positions of popup relative to "around" node
		//		onCancel: Function
		//			callback when user has canceled the popup by
		//          	1. hitting ESC or
		//				2. by using the popup widget's proprietary cancel mechanism (like a cancel button in a dialog);
		//				   ie: whenever popupWidget.onCancel() is called, args.onCancel is called
		//		onClose: Function
		//			callback whenever this popup is closed (via close(), closeAll(), or closeTo())
		//		submenu: Boolean
		//			Is this a submenu off of the existing popup?
		//		onExecute: Function
		//			callback when user "executed" on the popup/sub-popup by selecting a menu choice, etc. (top menu only)
		//
		// examples:
		//		1. opening at the mouse position
		//			dijit.popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY});
		//		2. opening the widget as a dropdown
		//			dijit.popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}  });
		//
		//	Note that whatever widget called dijit.popup.open() should also listen to it's own _onBlur callback
		//	(fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed.

		var widget = args.popup,
			orient = args.orient || {'BL':'TL', 'TL':'BL'},
			around = args.around,
			id = (args.around && args.around.id) ? (args.around.id+"_dropdown") : ("popup_"+idGen++);

		if(!args.submenu){
			this.closeAll();
		}

		// make wrapper div to hold widget and possibly hold iframe behind it.
		// we can't attach the iframe as a child of the widget.domNode because
		// widget.domNode might be a <table>, <ul>, etc.
		var wrapper = dojo.doc.createElement("div");
		wrapper.id = id;
		wrapper.className="dijitPopup";
		wrapper.style.zIndex = beginZIndex + stack.length;
		if(args.parent){
			wrapper.dijitPopupParent=args.parent.id;
		}
		dojo.body().appendChild(wrapper);

		widget.domNode.style.display="";
		wrapper.appendChild(widget.domNode);

		var iframe = new dijit.BackgroundIframe(wrapper);

		// position the wrapper node
		var best = around ?
			dijit.placeOnScreenAroundElement(wrapper, around, orient, widget.orient ? dojo.hitch(widget, "orient") : null) :
			dijit.placeOnScreen(wrapper, args, orient == 'R' ? ['TR','BR','TL','BL'] : ['TL','BL','TR','BR']);

		// TODO: use effects to fade in wrapper

		var handlers = [];

		// provide default escape key handling
		handlers.push(dojo.connect(wrapper, "onkeypress", this, function(evt){
			if (evt.keyCode == dojo.keys.ESCAPE){
				args.onCancel();
			}
		}));

		// watch for cancel/execute events on the popup and notify the caller
		// (for a menu, "execute" means clicking an item)
		if(widget.onCancel){
			handlers.push(dojo.connect(widget, "onCancel", null, args.onCancel));
		}

		handlers.push(dojo.connect(widget, widget.onExecute ? "onExecute" : "onChange", null, function(){
			if(stack[0] && stack[0].onExecute){
				stack[0].onExecute();
			}
		}));

		stack.push({
			wrapper: wrapper,
			iframe: iframe,
			widget: widget,
			onExecute: args.onExecute,
			onCancel: args.onCancel,
 			onClose: args.onClose,
			handlers: handlers
		});

		if(widget.onOpen){
			widget.onOpen(best);
		}

		return best;
	};

	this.close = function(){
		// summary:
		//		Close popup on the top of the stack (the highest z-index popup)

		// this needs to happen before the stack is popped, because menu's
		// onClose calls closeTo(this)
		var widget = stack[stack.length-1].widget;
		if(widget.onClose){
			widget.onClose();
		}

		if(!stack.length){
			return;
		}
		var top = stack.pop();
		var wrapper = top.wrapper,
			iframe = top.iframe,
			widget = top.widget,
			onClose = top.onClose;

		dojo.forEach(top.handlers, dojo.disconnect);

		// #2685: check if the widget still has a domNode so ContentPane can change its URL without getting an error
		if(!widget||!widget.domNode){ return; }
		dojo.style(widget.domNode, "display", "none");
		dojo.body().appendChild(widget.domNode);
		iframe.destroy();
		dojo._destroyElement(wrapper);

		if(onClose){
			onClose();
		}
	};

	this.closeAll = function(){
		// summary: close every popup, from top of stack down to the first one
		while(stack.length){
			this.close();
		}
	};

	this.closeTo = function(/*Widget*/ widget){
		// summary: closes every popup above specified widget
		while(stack.length && stack[stack.length-1].widget.id != widget.id){
			this.close();
		}
	};

}();

/***
dijit._frames = function(){
	// summary: cache of iframes
	var queue = [];

	this.get = function(){
		var iframe;
		if(queue.length){
			iframe = queue.pop();
			iframe.style.display="";
		}else{
			if(dojo.isIE){
				var html="<iframe src='javascript:\"\"'"
					+ " style='position: absolute; left: 0px; top: 0px;"
					+ "z-index: -1; filter:Alpha(Opacity=\"0\");'>";
				iframe = dojo.doc.createElement(html);
			}else{
			 	var iframe = dojo.doc.createElement("iframe");
				iframe.src = 'javascript:""';
				iframe.className = "dijitBackgroundIframe";
			}
			iframe.tabIndex = -1; // Magic to prevent iframe from getting focus on tab keypress - as style didnt work.
		}
		return iframe;
	};

	this.release = function(iframe){
		iframe.style.display="";
		queue.push(iframe);
	}
}();
***/
dijit.BackgroundIframe = function(/* HTMLElement */node){
	//	summary:
	//		For IE z-index schenanigans. id attribute is required.
	//
	//	description:
	//		new dijit.BackgroundIframe(node)
	//			Makes a background iframe as a child of node, that fills
	//			area (and position) of node

	if(!node.id){ throw new Error("no id"); }

	if((dojo.isIE && dojo.isIE < 7) || (dojo.isFF && dojo.isFF < 3 && dojo.hasClass(dojo.body(), "dijit_a11y"))){
		var iframe = dijit._frames.get();
		if(dojo.isIE){
			var html="<iframe src='javascript:\"\"'"
				+ " style='position: absolute; left: 0px; top: 0px;"
				+ " width: expression(document.getElementById(\"" + node.id + "\").offsetWidth);"
				+ " height: expression(document.getElementById(\"" + node.id + "\").offsetHeight); "
				+ "z-index: -1; filter:Alpha(Opacity=\"0\");'>";
			iframe = dojo.doc.createElement(html);
		}else{
		 	iframe = dojo.doc.createElement("iframe");
			iframe.src = 'javascript:""';
			iframe.className = "dijitBackgroundIframe";
		}
		node.appendChild(iframe);
		this.iframe = iframe;
	}
};

dojo.extend(dijit.BackgroundIframe, {
	destroy: function(){
		//	summary: destroy the iframe
		if(this.iframe){
			dojo._destroyElement(this.iframe);
			delete this.iframe;
		}
	}
});
<|endoftext|>


<commit_before>const PDFDocument = require('pdfkit');
const PDFTable = require('voilab-pdf-table');
const content = require('../../data/content');
const Case = require('./textHelpers');
const moment = require('moment');
const logger = require('../../log');

module.exports = {
    createPdf,
    addSection,
    subjectContent,
    sentenceSummaryContent,
    sentenceHistoryContent,
    courtHearingsContent,
    movementContent,
    hdcInfoContent,
    hdcRecallContent,
    offenceContent,
    custodyOffenceContent,
    addressContent,
    twoColumnTable,
    aliasContent
};

function createPdf(res, printItems, data, availablePrintOptions, options = {}) {

    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename=hpa-download.pdf',
        Pragma: 'token'
    });

    const doc = new PDFDocument({
        autoFirstPage: false,
        size: 'A4',
        margin: 50,
        info: {
            Title: 'Historic Prisoner Application'
        }
    });

    if (options.type && options.type === 'searchPrint') {
        createSearchPrint(doc, data, options);
    } else {
        doc.fontSize(12);
        doc.moveDown(2);
    }

    printItems.forEach(item => {
        Object.keys(availablePrintOptions[item]).forEach(dataType => {
            addSection(doc, availablePrintOptions[item], data[dataType], dataType);
        });

    });

    doc.pipe(res);
    doc.end();
}

function createSearchPrint(doc, data) {
    const {firstName, lastName} = data.summary;
    const {prisonNumber} = data.summary;

    doc.on('pageAdded', () => {
        doc.moveTo(doc.x, doc.y).lineTo(545, doc.y).stroke('#ccc');
        doc.moveDown(2);
        doc.image('assets/images/HMPPS_logo_crop.png', {width: 160});
        doc.image('assets/images/icon-important-2x.png', doc.x + 200, doc.y - 80, {width: 10});
        doc.fontSize(13);
        doc.text('Notice', doc.x + 220, doc.y - 80, {align: 'justify'});
        doc.fontSize(10);
        doc.text(content.pdf.disclaimer, doc.x - 20, doc.y, {align: 'justify'});
        doc.fontSize(11);
        doc.moveDown();
        doc.moveTo(doc.x - 200, doc.y).lineTo(545, doc.y).stroke('#ccc');
        doc.moveDown(2);
        doc.fontSize(24);
        doc.text(`${Case.capital(firstName)} ${Case.capital(lastName)}, ${prisonNumber}`, doc.x - 200, doc.y);
        doc.moveDown();
        doc.fontSize(12);
    });
}

function addSection(doc, printOption, items, dataType) {

    const {title, addContent} = printOption[dataType];

    doc.addPage();

    doc.fontSize(20).text(title);
    doc.fontSize(12);

    if (!items) return emptySection(doc, title);

    addContent(doc, items);
}

function emptySection(doc, title) {
    doc.moveDown();
    doc.text(`Subject has no ${Case.lower(title)}.`);
}

function subSection(doc, title) {
    doc.moveDown();
    doc.fontSize(14).text(title);
    doc.fontSize(12);
}

function pad(doc, text) {
    doc.moveDown();
    doc.text(text);
    doc.moveDown();
}

function subjectContent(doc, items) {

    const excludedItems = ['initial', 'firstName', 'middleName', 'lastName'];

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'key', width: 150},
        {id: 'value', width: 300}
    ]);

    const tableBody = Object.keys(items).map(key => {
        if (items[key] && !excludedItems.includes(key)) {
            const value = getSubjectValue(items, key);
            return {key: `${content.pdf.subject2[key] || key}: `, value};
        }
    }).filter(n => n);
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function getSubjectValue(items, key) {
    const dates = ['dob', 'receptionDate'];

    if (dates.includes(key)) {
        return moment(items[key]).format('DD/MM/YYYY');
    }

    if (key === 'sex') {
        if (items[key] === 'M') {
            return 'Male';
        }

        if (items[key] === 'F') {
            return 'Female';
        }
    }
    return items[key];
}

function sentenceSummaryContent(doc, items) {

    subSection(doc, 'Last recorded establishment');
    pad(doc, Case.capital(items.establishment));

    subSection(doc, 'Last recorded category');
    categoryContent(doc, [items.category]);

    subSection(doc, 'Court Hearing');
    courtHearingsContent(doc, [items.courtHearing]);

    subSection(doc, 'Effective Sentence');
    sentenceHistoryContent(doc, [items.effectiveSentence]);
}

function sentenceHistoryContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'changeDate', width: 130},
        {id: 'length', width: 150},
        {id: 'dates', width: 150, align: 'left'}
    ]);

    const tableBody = items.map(item => {
        const {changeDate, lengthDays} = item;
        const keyDateKeys = ['CRD', 'HDCED', 'LED', 'SED'];

        const dateString = Object.keys(item).map(key => {
            if (keyDateKeys.includes(key)) {
                return `${key}: ${moment(item[key]).format('DD/MM/YYYY')}`;
            }
        })
            .filter(n => n)
            .join('\n');

        return {changeDate: moment(changeDate).format('DD/MM/YYYY'), length: `${lengthDays} days`, dates: dateString};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function courtHearingsContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'court', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, court} = item;
        return {date: moment(date).format('DD/MM/YYYY'), court: Case.capital(court)};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function categoryContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'category', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, category} = item;
        return {date: moment(date).format('DD/MM/YYYY'), category: Case.capital(category)};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function movementContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'establishment', width: 130},
        {id: 'detail', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, establishment, type, movement} = item;
        return {
            date: moment(date).format('DD/MM/YYYY'),
            establishment: Case.capital(establishment),
            detail: `${type === 'D' ? 'OUT' : 'IN'} - ${Case.sentenceWithAcronyms(movement)}`
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function hdcInfoContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'stage', width: 130},
        {id: 'detail', width: 190},
        {id: 'reason', width: 180}
    ]);

    const tableBody = items.map(item => {
        const {stage, date, status, reasons} = item;
        return {
            stage: Case.sentenceWithAcronyms(stage),
            detail: `${Case.sentenceWithAcronyms(status)}, ${moment(date).format('DD/MM/YYYY')}`,
            reasons: Case.capitalWithAcronyms(reasons)
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function hdcRecallContent(doc, items) {

    items.forEach(item => {
        const table = new PDFTable(doc, {bottomMargin: 30});
        table.addColumns([
            {id: 'key', width: 170},
            {id: 'value', width: 300}
        ]);
        const {createdDate, curfewEndDate, outcome, outcomeDate, reason} = item;

        const tableBody = [
            {key: 'Recall date', value: moment(createdDate).format('DD/MM/YYYY')},
            {key: 'Original curfew end date', value: moment(curfewEndDate).format('DD/MM/YYYY')},
            {key: 'Outcome', value: Case.sentenceWithAcronyms(outcome)},
            {key: 'Outcome date', value: moment(outcomeDate).format('DD/MM/YYYY')},
            {key: 'Reason', value: Case.sentenceWithAcronyms(reason)}
        ];

        table.setNewPageFn(table => table.pdf.addPage());
        table.addBody(tableBody);

        doc.moveDown();
    });
}

function offenceContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'offenceCode', width: 200},
        {id: 'establishment', width: 200}
    ]);

    const tableBody = items.map(item => {
        const {date, code, establishment, establishment_code} = item;
        return {
            date: moment(date).format('DD/MM/YYYY'),
            offenceCode: `Offence code ${code}`,
            establishment: establishment ? `${Case.sentenceWithAcronyms(establishment)}` : `${establishment_code}`
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function custodyOffenceContent(doc, items) {

    items.forEach(item => {

        subSection(doc, `${moment(item.date).format('DD/MM/YYYY')}, ${Case.sentenceWithAcronyms(item.establishment)}`);
        doc.text(`${Case.sentence(item.outcome)} - ${Case.sentenceWithAcronyms(item.charge)}`);

        if (item.punishments)
            punishmentContent(doc, item.punishments);
    });
}

function punishmentContent(doc, items) {
    items.forEach(item => {
        doc.text(` - ${Case.sentenceWithAcronyms(item.punishment)} (${item.duration} days)`);
    });
}

function addressContent(doc, items) {

    items.forEach(item => {
        doc.moveDown();
        const {type, person, street, town, county} = item;
        if (street) {
            if (type) Case.capital(doc.text(type));
            if (person) doc.text(Case.capital(person));
            if (street) doc.text(Case.capitalWithAcronyms(street));
            if (town) doc.text(Case.capital(town));
            if (county) doc.text(Case.capital(county));
        }
    });
}

function aliasContent(doc, items) {

    items.forEach(item => {
        doc.moveDown();
        const {first, middle, last, birthDate} = item;

        if (first || middle || last) {
            doc.text(Case.capital(first + ' ' + middle + ' ' + last));
        }
        if (birthDate) {
            doc.text(`Born on: ${moment(birthDate).format('DD/MM/YYYY')}`);
        }
    });
}

function twoColumnTable(doc, items) {
    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'item0', width: 400},
        {id: 'item1', width: 200}
    ]);

    const tableBody = items.map(item => {
        return Object.keys(item).reduce((obj, itemKey, index) => {
            obj[`item${index}`] = item[itemKey];
            return obj;
        }, {});
    });

    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}
<commit_msg>Protect against missing data items<commit_after>const PDFDocument = require('pdfkit');
const PDFTable = require('voilab-pdf-table');
const content = require('../../data/content');
const Case = require('./textHelpers');
const moment = require('moment');
const logger = require('../../log');

module.exports = {
    createPdf,
    addSection,
    subjectContent,
    sentenceSummaryContent,
    sentenceHistoryContent,
    courtHearingsContent,
    movementContent,
    hdcInfoContent,
    hdcRecallContent,
    offenceContent,
    custodyOffenceContent,
    addressContent,
    twoColumnTable,
    aliasContent
};

function createPdf(res, printItems, data, availablePrintOptions, options = {}) {

    res.writeHead(200, {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename=hpa-download.pdf',
        Pragma: 'token'
    });

    const doc = new PDFDocument({
        autoFirstPage: false,
        size: 'A4',
        margin: 50,
        info: {
            Title: 'Historic Prisoner Application'
        }
    });

    if (options.type && options.type === 'searchPrint') {
        createSearchPrint(doc, data, options);
    } else {
        doc.fontSize(12);
        doc.moveDown(2);
    }

    printItems.forEach(item => {
        Object.keys(availablePrintOptions[item]).forEach(dataType => {
            addSection(doc, availablePrintOptions[item], data[dataType], dataType);
        });

    });

    doc.pipe(res);
    doc.end();
}

function createSearchPrint(doc, data) {
    const {firstName, lastName} = data.summary;
    const {prisonNumber} = data.summary;

    doc.on('pageAdded', () => {
        doc.moveTo(doc.x, doc.y).lineTo(545, doc.y).stroke('#ccc');
        doc.moveDown(2);
        doc.image('assets/images/HMPPS_logo_crop.png', {width: 160});
        doc.image('assets/images/icon-important-2x.png', doc.x + 200, doc.y - 80, {width: 10});
        doc.fontSize(13);
        doc.text('Notice', doc.x + 220, doc.y - 80, {align: 'justify'});
        doc.fontSize(10);
        doc.text(content.pdf.disclaimer, doc.x - 20, doc.y, {align: 'justify'});
        doc.fontSize(11);
        doc.moveDown();
        doc.moveTo(doc.x - 200, doc.y).lineTo(545, doc.y).stroke('#ccc');
        doc.moveDown(2);
        doc.fontSize(24);
        doc.text(`${Case.capital(firstName)} ${Case.capital(lastName)}, ${prisonNumber}`, doc.x - 200, doc.y);
        doc.moveDown();
        doc.fontSize(12);
    });
}

function addSection(doc, printOption, items, dataType) {

    const {title, addContent} = printOption[dataType];

    doc.addPage();

    doc.fontSize(20).text(title);
    doc.fontSize(12);

    if (!items) return emptySection(doc, title);

    addContent(doc, items);
}

function emptySection(doc, title) {
    doc.moveDown();
    doc.text(`Subject has no ${Case.lower(title)}.`);
}

function subSection(doc, title) {
    doc.moveDown();
    doc.fontSize(14).text(title);
    doc.fontSize(12);
}

function pad(doc, text) {
    doc.moveDown();
    doc.text(text);
    doc.moveDown();
}

function subjectContent(doc, items) {

    const excludedItems = ['initial', 'firstName', 'middleName', 'lastName'];

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'key', width: 150},
        {id: 'value', width: 300}
    ]);

    const tableBody = Object.keys(items).map(key => {
        if (items[key] && !excludedItems.includes(key)) {
            const value = getSubjectValue(items, key);
            return {key: `${content.pdf.subject2[key] || key}: `, value};
        }
    }).filter(n => n);
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function getSubjectValue(items, key) {
    const dates = ['dob', 'receptionDate'];

    if (dates.includes(key)) {
        return moment(items[key]).format('DD/MM/YYYY');
    }

    if (key === 'sex') {
        if (items[key] === 'M') {
            return 'Male';
        }

        if (items[key] === 'F') {
            return 'Female';
        }
    }
    return items[key];
}

function sentenceSummaryContent(doc, items) {

    if (items.establishment) {
        subSection(doc, 'Last recorded establishment');
        pad(doc, Case.capital(items.establishment));
    }

    if (items.category) {
        subSection(doc, 'Last recorded category');
        categoryContent(doc, [items.category]);
    }

    if (items.courtHearing) {
        subSection(doc, 'Court Hearing');
        courtHearingsContent(doc, [items.courtHearing]);
    }

    if (items.effectiveSentence) {
        subSection(doc, 'Effective Sentence');
        sentenceHistoryContent(doc, [items.effectiveSentence]);
    }
}

function sentenceHistoryContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'changeDate', width: 130},
        {id: 'length', width: 150},
        {id: 'dates', width: 150, align: 'left'}
    ]);

    const tableBody = items.map(item => {
        const {changeDate, lengthDays} = item;
        const keyDateKeys = ['CRD', 'HDCED', 'LED', 'SED'];

        const dateString = Object.keys(item).map(key => {
            if (keyDateKeys.includes(key)) {
                return `${key}: ${moment(item[key]).format('DD/MM/YYYY')}`;
            }
        })
            .filter(n => n)
            .join('\n');

        return {changeDate: moment(changeDate).format('DD/MM/YYYY'), length: `${lengthDays} days`, dates: dateString};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function courtHearingsContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'court', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, court} = item;
        return {date: moment(date).format('DD/MM/YYYY'), court: Case.capital(court)};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function categoryContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'category', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, category} = item;
        return {date: moment(date).format('DD/MM/YYYY'), category: Case.capital(category)};
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function movementContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'establishment', width: 130},
        {id: 'detail', width: 300}
    ]);

    const tableBody = items.map(item => {
        const {date, establishment, type, movement} = item;
        return {
            date: moment(date).format('DD/MM/YYYY'),
            establishment: Case.capital(establishment),
            detail: `${type === 'D' ? 'OUT' : 'IN'} - ${Case.sentenceWithAcronyms(movement)}`
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function hdcInfoContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'stage', width: 130},
        {id: 'detail', width: 190},
        {id: 'reason', width: 180}
    ]);

    const tableBody = items.map(item => {
        const {stage, date, status, reasons} = item;
        return {
            stage: Case.sentenceWithAcronyms(stage),
            detail: `${Case.sentenceWithAcronyms(status)}, ${moment(date).format('DD/MM/YYYY')}`,
            reasons: Case.capitalWithAcronyms(reasons)
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function hdcRecallContent(doc, items) {

    items.forEach(item => {
        const table = new PDFTable(doc, {bottomMargin: 30});
        table.addColumns([
            {id: 'key', width: 170},
            {id: 'value', width: 300}
        ]);
        const {createdDate, curfewEndDate, outcome, outcomeDate, reason} = item;

        const tableBody = [
            {key: 'Recall date', value: moment(createdDate).format('DD/MM/YYYY')},
            {key: 'Original curfew end date', value: moment(curfewEndDate).format('DD/MM/YYYY')},
            {key: 'Outcome', value: Case.sentenceWithAcronyms(outcome)},
            {key: 'Outcome date', value: moment(outcomeDate).format('DD/MM/YYYY')},
            {key: 'Reason', value: Case.sentenceWithAcronyms(reason)}
        ];

        table.setNewPageFn(table => table.pdf.addPage());
        table.addBody(tableBody);

        doc.moveDown();
    });
}

function offenceContent(doc, items) {

    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'date', width: 130},
        {id: 'offenceCode', width: 200},
        {id: 'establishment', width: 200}
    ]);

    const tableBody = items.map(item => {
        const {date, code, establishment, establishment_code} = item;
        return {
            date: moment(date).format('DD/MM/YYYY'),
            offenceCode: `Offence code ${code}`,
            establishment: establishment ? `${Case.sentenceWithAcronyms(establishment)}` : `${establishment_code}`
        };
    });
    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}

function custodyOffenceContent(doc, items) {

    items.forEach(item => {

        subSection(doc, `${moment(item.date).format('DD/MM/YYYY')}, ${Case.sentenceWithAcronyms(item.establishment)}`);
        doc.text(`${Case.sentence(item.outcome)} - ${Case.sentenceWithAcronyms(item.charge)}`);

        if (item.punishments)
            punishmentContent(doc, item.punishments);
    });
}

function punishmentContent(doc, items) {
    items.forEach(item => {
        doc.text(` - ${Case.sentenceWithAcronyms(item.punishment)} (${item.duration} days)`);
    });
}

function addressContent(doc, items) {

    items.forEach(item => {
        doc.moveDown();
        const {type, person, street, town, county} = item;
        if (street) {
            if (type) Case.capital(doc.text(type));
            if (person) doc.text(Case.capital(person));
            if (street) doc.text(Case.capitalWithAcronyms(street));
            if (town) doc.text(Case.capital(town));
            if (county) doc.text(Case.capital(county));
        }
    });
}

function aliasContent(doc, items) {

    items.forEach(item => {
        doc.moveDown();
        const {first, middle, last, birthDate} = item;

        if (first || middle || last) {
            doc.text(Case.capital(first + ' ' + middle + ' ' + last));
        }
        if (birthDate) {
            doc.text(`Born on: ${moment(birthDate).format('DD/MM/YYYY')}`);
        }
    });
}

function twoColumnTable(doc, items) {
    const table = new PDFTable(doc, {bottomMargin: 30});
    table.addColumns([
        {id: 'item0', width: 400},
        {id: 'item1', width: 200}
    ]);

    const tableBody = items.map(item => {
        return Object.keys(item).reduce((obj, itemKey, index) => {
            obj[`item${index}`] = item[itemKey];
            return obj;
        }, {});
    });

    table.setNewPageFn(table => table.pdf.addPage());
    table.addBody(tableBody);
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import createSectionIterator from 'section-iterator';
import themeable from 'react-themeable';
import SectionTitle from './SectionTitle';
import ItemsList from './ItemsList';

const emptyObject = {};
const defaultRenderInputComponent = props => <input {...props} />;
const defaultRenderItemsContainer =
  ({ containerProps, children }) =>
{children}
;
const defaultTheme = {
  container: 'react-autowhatever__container',
  containerOpen: 'react-autowhatever__container--open',
  input: 'react-autowhatever__input',
  inputOpen: 'react-autowhatever__input--open',
  inputFocused: 'react-autowhatever__input--focused',
  itemsContainer: 'react-autowhatever__items-container',
  itemsContainerOpen: 'react-autowhatever__items-container--open',
  itemsList: 'react-autowhatever__items-list',
  item: 'react-autowhatever__item',
  itemFirst: 'react-autowhatever__item--first',
  itemHighlighted: 'react-autowhatever__item--highlighted',
  sectionContainer: 'react-autowhatever__section-container',
  sectionContainerFirst: 'react-autowhatever__section-container--first',
  sectionTitle: 'react-autowhatever__section-title'
};

export default class Autowhatever extends Component {
  static propTypes = {
    id: PropTypes.string,                 // Used in aria-* attributes. If multiple Autowhatever's are rendered on a page, they must have unique ids.
    multiSection: PropTypes.bool,         // Indicates whether a multi section layout should be rendered.
    renderInputComponent: PropTypes.func, // When specified, it is used to render the input element.
    renderItemsContainer: PropTypes.func, // Renders the items container.
    items: PropTypes.array.isRequired,    // Array of items or sections to render.
    renderItem: PropTypes.func,           // This function renders a single item.
    renderItemData: PropTypes.object,     // Arbitrary data that will be passed to renderItem()
    renderSectionTitle: PropTypes.func,   // This function gets a section and renders its title.
    getSectionItems: PropTypes.func,      // This function gets a section and returns its items, which will be passed into `renderItem` for rendering.
    inputProps: PropTypes.object,         // Arbitrary input props
    itemProps: PropTypes.oneOfType([      // Arbitrary item props
      PropTypes.object,
      PropTypes.func
    ]),
    highlightedSectionIndex: PropTypes.number, // Section index of the highlighted item
    highlightedItemIndex: PropTypes.number,    // Highlighted item index (within a section)
    theme: PropTypes.oneOfType([               // Styles. See: https://github.com/markdalgleish/react-themeable
      PropTypes.object,
      PropTypes.array
    ])
  };

  static defaultProps = {
    id: '1',
    multiSection: false,
    renderInputComponent: defaultRenderInputComponent,
    renderItemsContainer: defaultRenderItemsContainer,
    renderItem: () => {
      throw new Error('`renderItem` must be provided');
    },
    renderItemData: emptyObject,
    renderSectionTitle: () => {
      throw new Error('`renderSectionTitle` must be provided');
    },
    getSectionItems: () => {
      throw new Error('`getSectionItems` must be provided');
    },
    inputProps: emptyObject,
    itemProps: emptyObject,
    highlightedSectionIndex: null,
    highlightedItemIndex: null,
    theme: defaultTheme
  };

  constructor(props) {
    super(props);

    this.highlightedItem = null;

    this.state = {
      isInputFocused: false
    };

    this.setSectionsItems(props);
    this.setSectionIterator(props);
    this.setTheme(props);
  }

  componentDidMount() {
    this.ensureHighlightedItemIsVisible();
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.items !== this.props.items) {
      this.setSectionsItems(nextProps);
    }

    if (nextProps.items !== this.props.items || nextProps.multiSection !== this.props.multiSection) {
      this.setSectionIterator(nextProps);
    }

    if (nextProps.theme !== this.props.theme) {
      this.setTheme(nextProps);
    }
  }

  componentDidUpdate() {
    this.ensureHighlightedItemIsVisible();
  }

  setSectionsItems(props) {
    if (props.multiSection) {
      this.sectionsItems = props.items.map(section => props.getSectionItems(section));
      this.sectionsLengths = this.sectionsItems.map(items => items.length);
      this.allSectionsAreEmpty = this.sectionsLengths.every(itemsCount => itemsCount === 0);
    }
  }

  setSectionIterator(props) {
    this.sectionIterator = createSectionIterator({
      multiSection: props.multiSection,
      data: props.multiSection ? this.sectionsLengths : props.items.length
    });
  }

  setTheme(props) {
    this.theme = themeable(props.theme);
  }

  storeInputReference = input => {
    if (input !== null) {
      this.input = input;
    }
  };

  storeItemsContainerReference = itemsContainer => {
    if (itemsContainer !== null) {
      this.itemsContainer = itemsContainer;
    }
  };

  onHighlightedItemChange = highlightedItem => {
    this.highlightedItem = highlightedItem;
  };

  getItemId = (sectionIndex, itemIndex) => {
    if (itemIndex === null) {
      return null;
    }

    const { id } = this.props;
    const section = (sectionIndex === null ? '' : `section-${sectionIndex}`);

    return `react-autowhatever-${id}-${section}-item-${itemIndex}`;
  };

  renderSections() {
    if (this.allSectionsAreEmpty) {
      return null;
    }

    const { theme } = this;
    const {
      id, items, renderItem, renderItemData, renderSectionTitle,
      highlightedSectionIndex, highlightedItemIndex, itemProps
    } = this.props;

    return items.map((section, sectionIndex) => {
      const keyPrefix = `react-autowhatever-${id}-`;
      const sectionKeyPrefix = `${keyPrefix}section-${sectionIndex}-`;
      const isFirstSection = (sectionIndex === 0);

      // `key` is provided by theme()
      /* eslint-disable react/jsx-key */
      return (
        <div {...theme(`${sectionKeyPrefix}container`, 'sectionContainer', isFirstSection && 'sectionContainerFirst')}>
          <SectionTitle
            section={section}
            renderSectionTitle={renderSectionTitle}
            theme={theme}
            sectionKeyPrefix={sectionKeyPrefix}
          />
          <ItemsList
            items={this.sectionsItems[sectionIndex]}
            itemProps={itemProps}
            renderItem={renderItem}
            renderItemData={renderItemData}
            sectionIndex={sectionIndex}
            highlightedItemIndex={highlightedSectionIndex === sectionIndex ? highlightedItemIndex : null}
            onHighlightedItemChange={this.onHighlightedItemChange}
            getItemId={this.getItemId}
            theme={theme}
            keyPrefix={keyPrefix}
            ref={this.storeItemsListReference}
          />

      );
      /* eslint-enable react/jsx-key */
    });
  }

  renderItems() {
    const { items } = this.props;

    if (items.length === 0) {
      return null;
    }

    const { theme } = this;
    const {
      id, renderItem, renderItemData, highlightedSectionIndex,
      highlightedItemIndex, itemProps
    } = this.props;

    return (
      <ItemsList
        items={items}
        itemProps={itemProps}
        renderItem={renderItem}
        renderItemData={renderItemData}
        highlightedItemIndex={highlightedSectionIndex === null ? highlightedItemIndex : null}
        onHighlightedItemChange={this.onHighlightedItemChange}
        getItemId={this.getItemId}
        theme={theme}
        keyPrefix={`react-autowhatever-${id}-`}
      />
    );
  }

  onFocus = event => {
    const { inputProps } = this.props;

    this.setState({
      isInputFocused: true
    });

    inputProps.onFocus && inputProps.onFocus(event);
  };

  onBlur = event => {
    const { inputProps } = this.props;

    this.setState({
      isInputFocused: false
    });

    inputProps.onBlur && inputProps.onBlur(event);
  };

  onKeyDown = event => {
    const { inputProps, highlightedSectionIndex, highlightedItemIndex } = this.props;

    switch (event.key) {
      case 'ArrowDown':
      case 'ArrowUp': {
        const nextPrev = (event.key === 'ArrowDown' ? 'next' : 'prev');
        const [newHighlightedSectionIndex, newHighlightedItemIndex] =
          this.sectionIterator[nextPrev]([highlightedSectionIndex, highlightedItemIndex]);

        inputProps.onKeyDown(event, { newHighlightedSectionIndex, newHighlightedItemIndex });
        break;
      }

      default:
        inputProps.onKeyDown(event, { highlightedSectionIndex, highlightedItemIndex });
    }
  };

  ensureHighlightedItemIsVisible() {
    const { highlightedItem } = this;

    if (!highlightedItem) {
      return;
    }

    const { itemsContainer } = this;
    const itemOffsetRelativeToContainer =
      highlightedItem.offsetParent === itemsContainer
        ? highlightedItem.offsetTop
        : highlightedItem.offsetTop - itemsContainer.offsetTop;

    let { scrollTop } = itemsContainer; // Top of the visible area

    if (itemOffsetRelativeToContainer < scrollTop) {
      // Item is off the top of the visible area
      scrollTop = itemOffsetRelativeToContainer;
    } else if (itemOffsetRelativeToContainer + highlightedItem.offsetHeight > scrollTop + itemsContainer.offsetHeight) {
      // Item is off the bottom of the visible area
      scrollTop = itemOffsetRelativeToContainer + highlightedItem.offsetHeight - itemsContainer.offsetHeight;
    }

    if (scrollTop !== itemsContainer.scrollTop) {
      itemsContainer.scrollTop = scrollTop;
    }
  }

  render() {
    const { theme } = this;
    const {
      id, multiSection, renderInputComponent, renderItemsContainer,
      highlightedSectionIndex, highlightedItemIndex
    } = this.props;
    const { isInputFocused } = this.state;
    const renderedItems = multiSection ? this.renderSections() : this.renderItems();
    const isOpen = (renderedItems !== null);
    const ariaActivedescendant = this.getItemId(highlightedSectionIndex, highlightedItemIndex);
    const containerProps = theme(
      `react-autowhatever-${id}-container`,
      'container',
      isOpen && 'containerOpen'
    );
    const itemsContainerId = `react-autowhatever-${id}`;
    const inputComponent = renderInputComponent({
      type: 'text',
      value: '',
      autoComplete: 'off',
      role: 'combobox',
      'aria-autocomplete': 'list',
      'aria-owns': itemsContainerId,
      'aria-expanded': isOpen,
      'aria-haspopup': isOpen,
      'aria-activedescendant': ariaActivedescendant,
      ...theme(
        `react-autowhatever-${id}-input`,
        'input',
        isOpen && 'inputOpen',
        isInputFocused && 'inputFocused'
      ),
      ...this.props.inputProps,
      onFocus: this.onFocus,
      onBlur: this.onBlur,
      onKeyDown: this.props.inputProps.onKeyDown && this.onKeyDown,
      ref: this.storeInputReference
    });
    const itemsContainer = renderItemsContainer({
      containerProps: {
        id: itemsContainerId,
        ...theme(
          `react-autowhatever-${id}-items-container`,
          'itemsContainer',
          isOpen && 'itemsContainerOpen'
        ),
        ref: this.storeItemsContainerReference
      },
      children: renderedItems
    });

    return (

        {inputComponent}
        {itemsContainer}

    );
  }
}
<commit_msg>Closing 30: remove `aria-haspopup` (#36)<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import createSectionIterator from 'section-iterator';
import themeable from 'react-themeable';
import SectionTitle from './SectionTitle';
import ItemsList from './ItemsList';

const emptyObject = {};
const defaultRenderInputComponent = props => <input {...props} />;
const defaultRenderItemsContainer =
  ({ containerProps, children }) =>
{children}
;
const defaultTheme = {
  container: 'react-autowhatever__container',
  containerOpen: 'react-autowhatever__container--open',
  input: 'react-autowhatever__input',
  inputOpen: 'react-autowhatever__input--open',
  inputFocused: 'react-autowhatever__input--focused',
  itemsContainer: 'react-autowhatever__items-container',
  itemsContainerOpen: 'react-autowhatever__items-container--open',
  itemsList: 'react-autowhatever__items-list',
  item: 'react-autowhatever__item',
  itemFirst: 'react-autowhatever__item--first',
  itemHighlighted: 'react-autowhatever__item--highlighted',
  sectionContainer: 'react-autowhatever__section-container',
  sectionContainerFirst: 'react-autowhatever__section-container--first',
  sectionTitle: 'react-autowhatever__section-title'
};

export default class Autowhatever extends Component {
  static propTypes = {
    id: PropTypes.string,                 // Used in aria-* attributes. If multiple Autowhatever's are rendered on a page, they must have unique ids.
    multiSection: PropTypes.bool,         // Indicates whether a multi section layout should be rendered.
    renderInputComponent: PropTypes.func, // When specified, it is used to render the input element.
    renderItemsContainer: PropTypes.func, // Renders the items container.
    items: PropTypes.array.isRequired,    // Array of items or sections to render.
    renderItem: PropTypes.func,           // This function renders a single item.
    renderItemData: PropTypes.object,     // Arbitrary data that will be passed to renderItem()
    renderSectionTitle: PropTypes.func,   // This function gets a section and renders its title.
    getSectionItems: PropTypes.func,      // This function gets a section and returns its items, which will be passed into `renderItem` for rendering.
    inputProps: PropTypes.object,         // Arbitrary input props
    itemProps: PropTypes.oneOfType([      // Arbitrary item props
      PropTypes.object,
      PropTypes.func
    ]),
    highlightedSectionIndex: PropTypes.number, // Section index of the highlighted item
    highlightedItemIndex: PropTypes.number,    // Highlighted item index (within a section)
    theme: PropTypes.oneOfType([               // Styles. See: https://github.com/markdalgleish/react-themeable
      PropTypes.object,
      PropTypes.array
    ])
  };

  static defaultProps = {
    id: '1',
    multiSection: false,
    renderInputComponent: defaultRenderInputComponent,
    renderItemsContainer: defaultRenderItemsContainer,
    renderItem: () => {
      throw new Error('`renderItem` must be provided');
    },
    renderItemData: emptyObject,
    renderSectionTitle: () => {
      throw new Error('`renderSectionTitle` must be provided');
    },
    getSectionItems: () => {
      throw new Error('`getSectionItems` must be provided');
    },
    inputProps: emptyObject,
    itemProps: emptyObject,
    highlightedSectionIndex: null,
    highlightedItemIndex: null,
    theme: defaultTheme
  };

  constructor(props) {
    super(props);

    this.highlightedItem = null;

    this.state = {
      isInputFocused: false
    };

    this.setSectionsItems(props);
    this.setSectionIterator(props);
    this.setTheme(props);
  }

  componentDidMount() {
    this.ensureHighlightedItemIsVisible();
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.items !== this.props.items) {
      this.setSectionsItems(nextProps);
    }

    if (nextProps.items !== this.props.items || nextProps.multiSection !== this.props.multiSection) {
      this.setSectionIterator(nextProps);
    }

    if (nextProps.theme !== this.props.theme) {
      this.setTheme(nextProps);
    }
  }

  componentDidUpdate() {
    this.ensureHighlightedItemIsVisible();
  }

  setSectionsItems(props) {
    if (props.multiSection) {
      this.sectionsItems = props.items.map(section => props.getSectionItems(section));
      this.sectionsLengths = this.sectionsItems.map(items => items.length);
      this.allSectionsAreEmpty = this.sectionsLengths.every(itemsCount => itemsCount === 0);
    }
  }

  setSectionIterator(props) {
    this.sectionIterator = createSectionIterator({
      multiSection: props.multiSection,
      data: props.multiSection ? this.sectionsLengths : props.items.length
    });
  }

  setTheme(props) {
    this.theme = themeable(props.theme);
  }

  storeInputReference = input => {
    if (input !== null) {
      this.input = input;
    }
  };

  storeItemsContainerReference = itemsContainer => {
    if (itemsContainer !== null) {
      this.itemsContainer = itemsContainer;
    }
  };

  onHighlightedItemChange = highlightedItem => {
    this.highlightedItem = highlightedItem;
  };

  getItemId = (sectionIndex, itemIndex) => {
    if (itemIndex === null) {
      return null;
    }

    const { id } = this.props;
    const section = (sectionIndex === null ? '' : `section-${sectionIndex}`);

    return `react-autowhatever-${id}-${section}-item-${itemIndex}`;
  };

  renderSections() {
    if (this.allSectionsAreEmpty) {
      return null;
    }

    const { theme } = this;
    const {
      id, items, renderItem, renderItemData, renderSectionTitle,
      highlightedSectionIndex, highlightedItemIndex, itemProps
    } = this.props;

    return items.map((section, sectionIndex) => {
      const keyPrefix = `react-autowhatever-${id}-`;
      const sectionKeyPrefix = `${keyPrefix}section-${sectionIndex}-`;
      const isFirstSection = (sectionIndex === 0);

      // `key` is provided by theme()
      /* eslint-disable react/jsx-key */
      return (
        <div {...theme(`${sectionKeyPrefix}container`, 'sectionContainer', isFirstSection && 'sectionContainerFirst')}>
          <SectionTitle
            section={section}
            renderSectionTitle={renderSectionTitle}
            theme={theme}
            sectionKeyPrefix={sectionKeyPrefix}
          />
          <ItemsList
            items={this.sectionsItems[sectionIndex]}
            itemProps={itemProps}
            renderItem={renderItem}
            renderItemData={renderItemData}
            sectionIndex={sectionIndex}
            highlightedItemIndex={highlightedSectionIndex === sectionIndex ? highlightedItemIndex : null}
            onHighlightedItemChange={this.onHighlightedItemChange}
            getItemId={this.getItemId}
            theme={theme}
            keyPrefix={keyPrefix}
            ref={this.storeItemsListReference}
          />

      );
      /* eslint-enable react/jsx-key */
    });
  }

  renderItems() {
    const { items } = this.props;

    if (items.length === 0) {
      return null;
    }

    const { theme } = this;
    const {
      id, renderItem, renderItemData, highlightedSectionIndex,
      highlightedItemIndex, itemProps
    } = this.props;

    return (
      <ItemsList
        items={items}
        itemProps={itemProps}
        renderItem={renderItem}
        renderItemData={renderItemData}
        highlightedItemIndex={highlightedSectionIndex === null ? highlightedItemIndex : null}
        onHighlightedItemChange={this.onHighlightedItemChange}
        getItemId={this.getItemId}
        theme={theme}
        keyPrefix={`react-autowhatever-${id}-`}
      />
    );
  }

  onFocus = event => {
    const { inputProps } = this.props;

    this.setState({
      isInputFocused: true
    });

    inputProps.onFocus && inputProps.onFocus(event);
  };

  onBlur = event => {
    const { inputProps } = this.props;

    this.setState({
      isInputFocused: false
    });

    inputProps.onBlur && inputProps.onBlur(event);
  };

  onKeyDown = event => {
    const { inputProps, highlightedSectionIndex, highlightedItemIndex } = this.props;

    switch (event.key) {
      case 'ArrowDown':
      case 'ArrowUp': {
        const nextPrev = (event.key === 'ArrowDown' ? 'next' : 'prev');
        const [newHighlightedSectionIndex, newHighlightedItemIndex] =
          this.sectionIterator[nextPrev]([highlightedSectionIndex, highlightedItemIndex]);

        inputProps.onKeyDown(event, { newHighlightedSectionIndex, newHighlightedItemIndex });
        break;
      }

      default:
        inputProps.onKeyDown(event, { highlightedSectionIndex, highlightedItemIndex });
    }
  };

  ensureHighlightedItemIsVisible() {
    const { highlightedItem } = this;

    if (!highlightedItem) {
      return;
    }

    const { itemsContainer } = this;
    const itemOffsetRelativeToContainer =
      highlightedItem.offsetParent === itemsContainer
        ? highlightedItem.offsetTop
        : highlightedItem.offsetTop - itemsContainer.offsetTop;

    let { scrollTop } = itemsContainer; // Top of the visible area

    if (itemOffsetRelativeToContainer < scrollTop) {
      // Item is off the top of the visible area
      scrollTop = itemOffsetRelativeToContainer;
    } else if (itemOffsetRelativeToContainer + highlightedItem.offsetHeight > scrollTop + itemsContainer.offsetHeight) {
      // Item is off the bottom of the visible area
      scrollTop = itemOffsetRelativeToContainer + highlightedItem.offsetHeight - itemsContainer.offsetHeight;
    }

    if (scrollTop !== itemsContainer.scrollTop) {
      itemsContainer.scrollTop = scrollTop;
    }
  }

  render() {
    const { theme } = this;
    const {
      id, multiSection, renderInputComponent, renderItemsContainer,
      highlightedSectionIndex, highlightedItemIndex
    } = this.props;
    const { isInputFocused } = this.state;
    const renderedItems = multiSection ? this.renderSections() : this.renderItems();
    const isOpen = (renderedItems !== null);
    const ariaActivedescendant = this.getItemId(highlightedSectionIndex, highlightedItemIndex);
    const containerProps = theme(
      `react-autowhatever-${id}-container`,
      'container',
      isOpen && 'containerOpen'
    );
    const itemsContainerId = `react-autowhatever-${id}`;
    const inputComponent = renderInputComponent({
      type: 'text',
      value: '',
      autoComplete: 'off',
      role: 'combobox',
      'aria-autocomplete': 'list',
      'aria-owns': itemsContainerId,
      'aria-expanded': isOpen,
      'aria-activedescendant': ariaActivedescendant,
      ...theme(
        `react-autowhatever-${id}-input`,
        'input',
        isOpen && 'inputOpen',
        isInputFocused && 'inputFocused'
      ),
      ...this.props.inputProps,
      onFocus: this.onFocus,
      onBlur: this.onBlur,
      onKeyDown: this.props.inputProps.onKeyDown && this.onKeyDown,
      ref: this.storeInputReference
    });
    const itemsContainer = renderItemsContainer({
      containerProps: {
        id: itemsContainerId,
        ...theme(
          `react-autowhatever-${id}-items-container`,
          'itemsContainer',
          isOpen && 'itemsContainerOpen'
        ),
        ref: this.storeItemsContainerReference
      },
      children: renderedItems
    });

    return (

        {inputComponent}
        {itemsContainer}

    );
  }
}
<|endoftext|>