UNPKG

@jacquelinett/react-agenda

Version:

An advanced agenda / organizer built with react

948 lines (749 loc) 27.1 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment' import ReactAgendaItem from './reactAgendaItem'; import classNames from 'classnames'; import { guid, getUnique, getLast, getFirst, mapItems } from './helpers.js'; import * as DragDropHelper from './dragAndDropHelper.js'; var startSelect let test; var endSelect var isDragging = false; var isMouseDown = false; var draggedElement; var timeNow = moment(); var draggedItem; var ctrlKey = false; var DEFAULT_ITEM = { name: '', classes: '', cellRefs: [] }; var mouse = { x: 0, y: 0, startX: 0, startY: 0 }; var element = null; var helper = null; export default class ReactAgenda extends Component { constructor(props) { super(props); this.state = { date: moment(), items: {}, itemOverlayStyles: {}, highlightedCells: [], numberOfDays: 4, autoScaleNumber: 0, focusedCell: null, }; this.handleBeforeUpdate = this.handleBeforeUpdate.bind(this); this.handleOnNextButtonClick = this.handleOnNextButtonClick.bind(this); this.handleOnPrevButtonClick = this.handleOnPrevButtonClick.bind(this); this.handleMouseClick = this.handleMouseClick.bind(this); this.handleMouseOver = this.handleMouseOver.bind(this); this.removeSelection = this.removeSelection.bind(this); this.handleAllClickStarts = this.handleAllClickStarts.bind(this); this.handleAllClickEnds = this.handleAllClickEnds.bind(this); this.onDragStart = this.onDragStart.bind(this); this.onDragEnter = this.onDragEnter.bind(this); this.onDragOver = this.onDragOver.bind(this); this.onDragEnd = this.onDragEnd.bind(this); this.onDragHandlerStart = this.onDragHandlerStart.bind(this); this.onDragHandlerEnd = this.onDragHandlerEnd.bind(this); this.getSelection = this.getSelection.bind(this); this.editEvent = this.editEvent.bind(this); this.removeEvent = this.removeEvent.bind(this); this.dragEvent = this.dragEvent.bind(this); this.duplicateEvent = this.duplicateEvent.bind(this); this.resizeEvent = this.resizeEvent.bind(this); this.updateDimensions = this.updateDimensions.bind(this); this.agendaScrollContainer = React.createRef(); this.cellRefMaps = {}; this.columnRefMaps = {}; this.columnRefMaps["column-0"] = React.createRef(); this.itemIDRefMaps = {}; this.hourRefMaps = {}; } /********************/ /* Life Cycle */ /********************/ componentWillMount() { this.handleBeforeUpdate(this.props); if (this.props.autoScale) { window.removeEventListener("resize", this.updateDimensions); } if (this.props.locale && this.props.locale != "en") { moment.locale(this.props.locale); } } componentWillReceiveProps(props) { this.handleBeforeUpdate(props); } componentDidMount() { if (this.props.autoScale) { window.addEventListener("resize", this.updateDimensions); this.updateDimensions(); } } updateDimensions() { var width = Math.round((document.getElementById('agenda-wrapper').offsetWidth / 150) - 1) this.setState({ autoScaleNumber: width, numberOfDays: width }) } /********************/ /* Item Renderers */ /********************/ getHeaderColumns() { var cols = []; for (var i = 0; i < this.state.numberOfDays; i++) { cols.push(moment(this.state.date).add(i, 'days').toDate()); } return cols; } getBodyRows() { var rows = []; var interval = (60 / this.props.rowsPerHour); if (this.props.startAtTime && typeof this.props.startAtTime === "number") { for (var i = 0; i < 24 * this.props.rowsPerHour; i++) { if (this.props.endAtTime != 0 && (this.props.endAtTime - this.props.startAtTime) * this.props.rowsPerHour >= i) { rows.push(moment(this.state.date).hours(this.props.startAtTime).minutes(0).seconds(0).milliseconds(0).add(Math.floor(i * interval), 'minutes')); } } return rows; } for (var i = 0; i < 24 * this.props.rowsPerHour; i++) { rows.push(moment(this.state.date).hours(0).minutes(0).seconds(0).milliseconds(0).add(Math.floor(i * interval), 'minutes')); } return rows; } getMinuteCells(rowMoment) { var cells = []; for (var i = 0; i < this.state.numberOfDays; i++) { var cellRef = moment(rowMoment).add(i, 'days').format('YYYY-MM-DDTHH:mm:ss'); this.cellRefMaps[cellRef] = React.createRef(); cells.push({ cellRef: cellRef, item: this.state.items[cellRef] || DEFAULT_ITEM }); } return cells; } /********************/ /* Event Handlers */ /********************/ handleBeforeUpdate(props) { if (props.hasOwnProperty('startDate') && props.startDate !== this.state.date.toDate()) { this.setState({ date: moment(props.startDate) }); } if (props.hasOwnProperty('items')) { this.setState({ items: mapItems(props.items, props.rowsPerHour, props.timezone) }); } if (props.hasOwnProperty('numberOfDays') && props.numberOfDays !== this.state.numberOfDays && !this.props.autoScale) { this.setState({ numberOfDays: props.numberOfDays }); } if (props.hasOwnProperty('minDate') && (!this.state.hasOwnProperty('minDate') || props.minDate !== this.state.minDate.toDate())) { this.setState({ minDate: moment(props.minDate) }); } if (props.hasOwnProperty('maxDate') && (!this.state.hasOwnProperty('maxDate') || props.maxDate !== this.state.maxDate.toDate())) { this.setState({ maxDate: moment(props.maxDate) }); } } handleOnNextButtonClick() { var nextStartDate = moment(this.state.date).add(this.state.numberOfDays, 'days'); if (this.state.hasOwnProperty('maxDate')) { nextStartDate = moment.min(nextStartDate, this.state.maxDate); } var newStart = nextStartDate; var newEnd = moment(newStart).add(this.state.numberOfDays - 1, 'days'); if (nextStartDate !== this.state.date) { this.setState({ date: nextStartDate }); } if (this.props.onDateRangeChange) { this.props.onDateRangeChange(newStart.startOf('day').toDate(), newEnd.endOf('day').toDate()); } } handleOnPrevButtonClick() { var prevStartDate = moment(this.state.date).subtract(this.state.numberOfDays, 'days'); if (this.state.hasOwnProperty('minDate')) { prevStartDate = moment.max(prevStartDate, this.state.minDate); } var newStart = prevStartDate; var newEnd = moment(newStart).add(this.state.numberOfDays - 1, 'days'); if (prevStartDate !== this.state.date) { this.setState({ date: prevStartDate }); } if (this.props.onDateRangeChange) { this.props.onDateRangeChange(newStart.toDate(), newEnd.toDate()); } } handleMouseClick(cell, bypass) { if (typeof cell != "string" && cell.tagName) { var dt = moment(cell.innerText, ["h:mm A"]).format("HH"); var old = parseInt(dt) var now = new Date(); var newdate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), old + 1, 0) var mom = newdate.toISOString().substring(0, newdate.toISOString().length - 5) if (this.props.onCellSelect) { return this.props.onCellSelect(mom, bypass); } } if (this.props.onCellSelect) { this.props.onCellSelect(cell, bypass); } } setMousePosition(e) { var ev = e || window.event; //Moz || IE mouse.x = ev.pageX; mouse.y = ev.pageY; }; handleMouseOver(e) { this.setMousePosition(e) if (e.buttons === 0) { return false; } e.preventDefault ? e.preventDefault() : e.returnValue = false this.removeSelection() if (element) { element.style.width = Math.abs(mouse.x - mouse.startX) + 'px'; element.style.height = Math.abs(mouse.y - mouse.startY) + 'px'; element.style.left = (mouse.x - mouse.startX < 0) ? mouse.x + 'px' : mouse.startX + 'px'; element.style.top = (mouse.y - mouse.startY < 0) ? mouse.y + 'px' : mouse.startY + 'px'; } if (helper) { helper.style.left = mouse.x + 'px'; helper.style.top = (mouse.y) + 'px'; if (e.target.classList.contains("agenda__cell") && !e.target.classList.contains("--time")) { var strt = moment(startSelect) var endd = moment(e.target.id) helper.innerHTML = endd.diff(strt) > 0 ? strt.format('LT') + ' -- ' + endd.format('LT') : endd.format('LT') + ' -- ' + strt.format('LT') } } } removeSelection() { var old = document.getElementsByClassName('agenda__cell_selected') for (var i = old.length - 1; i >= 0; --i) { if (old[i]) { old[i].classList.remove('agenda__cell_selected'); } } } handleAllClickStarts(e, n) { isMouseDown = true; this.removeSelection() if (e.target.classList.contains("--time") || e.target.classList.contains("--time-now") && !isDragging) { return this.handleMouseClick(e.target) } if (e.target.classList.contains("agenda__cell") && !e.target.classList.contains("--time") && !isDragging) { this.removeSelection() e.target.classList.toggle('agenda__cell_selected'); startSelect = e.target.id if (e.buttons === 0) { return false; } this.handleMouseClick(e.target.id) mouse.startX = mouse.x; mouse.startY = mouse.y; element = document.createElement('div'); element.className = 'rectangle' element.style.left = mouse.x + 'px'; element.style.top = mouse.y + 'px'; document.body.appendChild(element) if (this.props.helper) { helper = document.createElement('div'); helper.className = 'helper-reactangle' document.body.appendChild(helper) } } } handleAllClickEnds(e, n) { // e.preventDefault ? e.preventDefault() : e.returnValue = false isMouseDown = false; isDragging = false; endSelect = e.target.id var old = document.getElementsByClassName('rectangle') var old2 = document.getElementsByClassName('helper-reactangle') for (var i = old.length - 1; i >= 0; --i) { if (old[i]) { old[i].remove(); } } for (var i = old2.length - 1; i >= 0; --i) { if (old2[i]) { old2[i].remove(); } } element = null helper = null if (startSelect && endSelect && startSelect != endSelect) { return this.getSelection(startSelect, endSelect) } } /**************** ****/ /* Drag Handlers */ /*******************/ onDragStart(e) { isDragging = true; isMouseDown = false; draggedItem = e.target.id; e.dataTransfer.setData('text/html', e.target); e.dataTransfer.dropEffect = "move"; e.dataTransfer.setDragImage(e.target, 0, 0); } onDragEnter(e) { e.preventDefault() if (!isDragging) { this.removeSelection() } e.dataTransfer.dropEffect = "move"; if (e.ctrlKey) { e.dataTransfer.effectAllowed = "copy"; e.dataTransfer.dropEffect = "copy"; } } onDragOver(e) { e.preventDefault() e.stopPropagation(); if (e.target.id === draggedElement) { return false; } if (e.ctrlKey) { e.dataTransfer.effectAllowed = "copy"; ctrlKey = true; } else { e.dataTransfer.dropEffect = "move"; } if (e.target.classList.contains("cell-item")) { return draggedElement = e.target.parentNode.parentNode.id } if (e.target.classList.contains("handler")) { return draggedElement = e.target.parentNode.id } if (e.target.classList.contains("dragDiv")) { return draggedElement = e.target.parentNode.id } draggedElement = e.target.id } dragEvent(id, d) { if (!this.props.onChangeEvent) { return; } var date = d; var itm; if (!this.refs[d]) { return; } if (this.refs[d].tagName !== 'TD') { // when user drag and drop an event into another we assign parent id date = this.refs[d].parentNode.id; } var items = this.props.items if (id && date && items) { for (var i in items) { if (items[i]._id === id) { var start = moment(items[i].startDateTime); var end = moment(items[i].endDateTime); var duration = moment.duration(end.diff(start)); let newdate = moment(date).subtract((duration % (60 / this.state.rowsPerHour))) let newEnddate = moment(newdate).add(duration) items[i].startDateTime = new Date(newdate) items[i].endDateTime = new Date(newEnddate) itm = items[i] break; } } this.props.onChangeEvent(items, itm); } } duplicateEvent(id, d) { var date = d; var itm; var oldItm; if (!this.refs[d]) { return; } if (this.refs[d].tagName !== 'TD') { // when user drag and drop an event into another we assign parent id date = this.refs[d].parentNode.id; } var items = this.props.items if (id && date && items) { for (var i in items) { if (items[i]._id === id) { itm = Object.assign({}, items[i], { _id: guid() }); var start = moment(itm.startDateTime); var end = moment(itm.endDateTime); var duration = moment.duration(end.diff(start)); let newdate = moment(date) let newEnddate = moment(newdate).add(duration) itm.startDateTime = new Date(newdate) itm.endDateTime = new Date(newEnddate) items.push(itm) if (this.props.onChangeEvent) { this.props.onChangeEvent(items, itm); } break; } } } } resizeEvent(id, date) { if (!this.props.onChangeDuration) { return; } var items = this.props.items; if (id && date && items) { for (var i in items) { if (items[i]._id === id) { var difference = new Date(date) - new Date(items[i].startDateTime) if (difference < 1) { let strt = new Date(items[i].startDateTime) items[i].endDateTime = new Date(strt.getFullYear(), strt.getMonth(), strt.getDate(), strt.getHours(), strt.getMinutes() + 15, 0); this.setState({ items: items }) return this.props.onChangeDuration(items, items[i]) } let newdate = moment(date) items[i].endDateTime = new Date(newdate) return this.props.onChangeDuration(items, items[i]) break; } } } } onDragEnd(e) { let newDate = draggedElement if (ctrlKey) { this.duplicateEvent(e.target.id, newDate) } else { this.dragEvent(e.target.id, newDate) } isDragging = false; isMouseDown = false; ctrlKey = false; draggedElement = ''; draggedItem = ''; } onDragHandlerStart(e) { isDragging = true; e.dataTransfer.setData('text/html', e.target); e.dataTransfer.dropEffect = "move"; //e.dataTransfer.setData("text/html", e.target); e.dataTransfer.effectAllowed = "all"; } onDragHandlerEnd(e, n) { if (typeof draggedElement === undefined || draggedElement === '') { return; } let item = e.target.id || e.target.offsetParent.id if (this.refs[draggedElement] && this.refs[e.target.id] && this.refs[e.target.id].tagName === "DIV" && this.refs[draggedElement].tagName === "DIV") {//detect if we are resizing an event item = e.target.id draggedElement = this.refs[draggedElement].parentNode.id; return this.resizeEvent(item, draggedElement) } if (draggedElement === '' && !this.refs[draggedElement] && this.refs[e.target.id].tagName === "DIV") { // when user drag and drop an event into another we assign parent id draggedElement = this.refs[e.target.id].parentNode.id; return; } if (!this.refs[draggedElement] && draggedElement) { //detect if we are dragging an event from its description panel (item component) var old = document.getElementById(draggedElement) draggedElement = old.parentNode.id; } this.resizeEvent(item, draggedElement) isDragging = false; isMouseDown = false; draggedElement = '' } /**************************/ /* selection Handlers */ /************************/ getSelection(start, end) { // var array = []; // var array2 = []; // var old = document.getElementsByClassName('agenda__cell_selected') // array = Object.keys(old).map(function(value, index) { // return old[value].id; // }) // var last = moment(getLast(array)); // var addon = last.add((60 / this.props.rowsPerHour), 'Minutes') // array.push(addon.format('YYYY-MM-DDTHH:mm:00')) // if (this.props.onRangeSelection) { // console.log('array' , array) // this.props.onRangeSelection(array); // } var strt = moment(start) var endd = moment(end) var arr = endd.diff(strt) > 0 ? [start, end] : [end, start]; this.props.onRangeSelection(arr); } /***************************/ /* EVENTS MODIFiCATION */ /*************************/ editEvent(props) { if (this.props.onItemEdit) { this.props.onItemEdit(props, true); } } removeEvent(item) { var items = this.props.items; var newItems = items.filter(function (el) { return el._id !== item._id; }); if (this.props.onItemRemove) { this.props.onItemRemove(newItems, item); } } render() { var renderHeaderColumns = function (col, i) { var headerLabel = moment(col); headerLabel.locale(this.props.locale); if (this.columnRefMaps["column-" + (i + 1)] == null) this.columnRefMaps["column-" + (i + 1)] = React.createRef(); return <th ref={this.columnRefMaps["column-" + (i + 1)]} key={"col-" + i} className="agenda__cell --head"> {this.props.headFormat ? headerLabel.format(this.props.headFormat) : headerLabel.format('dddd DD MMM YY')} </th> }; var renderBodyRows = function (row, i) { if (i % this.props.rowsPerHour === 0) { var ref = "hour-" + Math.floor(i / this.props.rowsPerHour); var timeLabel = moment(row); var differ = timeLabel.diff(timeNow, 'minutes') if (this.hourRefMaps[ref] == null) this.hourRefMaps[ref] = React.createRef(); timeLabel.locale(this.props.locale); return ( <tr key={"row-" + i} ref={this.hourRefMaps[ref]} draggable={false} className="agenda__row --hour-start"> <td className={differ <= 0 && differ >= -60 ? 'disable-select agenda__cell --time-now' : 'disable-select agenda__cell --time'} rowSpan={this.props.rowsPerHour}>{timeLabel.format('LT')} </td> {this.getMinuteCells(row).map(renderMinuteCells, this)} </tr> ); } else { return ( <tr key={"row-" + i}> {this.getMinuteCells(row).map(renderMinuteCells, this)} </tr> ); } }; var itmName var Colors = this.props.itemColors var ItemComponent = this.props.itemComponent ? this.props.itemComponent : ReactAgendaItem; // Render an individual item var renderItemCells = function (cell, i) { var cellClasses = { 'agenda__cell': true }; cell['item'].forEach(function (itm) { cellClasses[itm.classes] = true; }) var classSet = classNames(cellClasses); var splt = classSet.split(' '); splt = splt.filter(i => !i.includes('agenda__cell')) splt = splt.filter(i => !i.includes('undefined')) var styles = { height: this.props.cellHeight + 'px' } var itemElement = cell.item.map(function (item, idx) { var last1 = getLast(item.cellRefs); var first1 = getFirst(item.cellRefs); if (first1 === cell.cellRef) { if (this.cellRefMaps[cell.cellRef] == null) this.cellRefMaps[cell.cellRef] = React.createRef(); return <div id={item._id} ref={this.cellRefMaps[cell.cellRef]} key={idx} className="dragDiv" onDragStart={this.onDragStart} onDragEnd={this.onDragEnd} draggable="true"> {/* {first1 === cell.cellRef ? <i className="drag-handle-icon" aria-hidden="true"></i> : ''} */} {first1 === cell.cellRef ? <ItemComponent item={item} parent={cell.cellRef} rowsPerHour={this.props.rowsPerHour} edit={this.props.onItemEdit ? this.editEvent : null} remove={this.props.onItemRemove ? this.removeEvent : null} days={this.props.numberOfDays} index={idx} count={cell['item'].length}/> : ''} </div> } if (last1 === cell.cellRef && this.props.onChangeDuration) { return <div className="handler" style={{ marginLeft: 8 * (idx + 1) + 'px' }} id={item._id} key={item._id} onDragStart={this.onDragHandlerStart} onDragEnd={this.onDragHandlerEnd} draggable="true"> <i className="resize-handle-icon"></i> </div> } return ''; }.bind(this)); if (this.cellRefMaps[cell.cellRef] == null) this.cellRefMaps[cell.cellRef] = React.createRef(); return ( <td ref={this.cellRefMaps[cell.cellRef]} key={"cell-" + i} className={classSet} style={styles} id={cell.cellRef}> {itemElement} </td> ) }.bind(this); var renderMinuteCells = function (cell, i) { if (cell.item[0] && !cell.item._id) { return renderItemCells(cell, i) } var cellClasses = { 'agenda__cell': true }; cellClasses[cell.item.classes] = true; if (cell.item.cellRefs) { var last = getLast(cell.item.cellRefs); var first = getFirst(cell.item.cellRefs); } var classSet = classNames(cellClasses); var splt = classSet.split(' '); splt = splt.filter(i => !i.includes('agenda__cell')); splt = splt.filter(i => !i.includes('undefined')); var nwsplt = []; splt.forEach(function (value) { if (value.length > 0) { nwsplt.push(Colors[value]); } }); var styles = { height: this.props.cellHeight + 'px' } if (splt.length > 1) { nwsplt = nwsplt.join(' , ') styles = { "background": 'linear-gradient(to left,' + nwsplt + ')', height: this.props.cellHeight + 'px' } } if (splt.length == 1) { styles = { "background": nwsplt[0], height: this.props.cellHeight + 'px' } } if (this.cellRefMaps[cell.cellRef] == null) this.cellRefMaps[cell.cellRef] = React.createRef(); if (this.itemIDRefMaps[cell.item._id] == null) this.itemIDRefMaps[cell.item._id] = React.createRef(); return ( <td ref={this.cellRefMaps[cell.cellRef]} key={"cell-" + i} className={classSet} style={styles} id={cell.cellRef}> {first === cell.cellRef ? <div id={cell.item._id} ref={this.itemIDRefMaps[cell.item._id]} className="dragDiv" onDragStart={this.onDragStart} onDragEnd={this.onDragEnd} draggable="true"> {/* {first === cell.cellRef && this.props.onChangeEvent ? <i className="drag-handle-icon" aria-hidden="true"></i> : ''} */} {first === cell.cellRef ? <ItemComponent item={cell.item} parent={cell.cellRef} rowsPerHour={this.props.rowsPerHour} edit={this.props.onItemEdit ? this.editEvent : null} remove={this.props.onItemRemove ? this.removeEvent : null} days={this.props.numberOfDays} /> : ''} </div> : ''} {last === cell.cellRef && this.props.onChangeDuration ? <div className="handler" id={cell.item._id} onDragStart={this.onDragHandlerStart} onDragEnd={this.onDragHandlerEnd} draggable="true"> <i className="resize-handle-icon"></i> </div> : ''} </td> ) }; var disablePrev = function (state) { if (!state.hasOwnProperty('minDate')) { return false; } return state.date.toDate().getTime() === state.minDate.toDate().getTime(); }; var disableNext = function (state) { if (!state.hasOwnProperty('maxDate')) { return false; } return state.date.toDate().getTime() === state.maxDate.toDate().getTime(); }; return ( <div className="agenda" id="agenda-wrapper" style={{ height: 'inherit' }}> { this.props.showHeader ? <div className="agenda__table --header"> <table> <thead> <tr> <th ref={this.columnRefMaps['column-0']} className="agenda__cell --controls"> </th> {this.getHeaderColumns(this.props.view).map(renderHeaderColumns, this)} </tr> </thead> </table> </div> : ""} <div ref={this.agendaScrollContainer} className="agenda__table --body" style={{ position: 'relative' }}> <table cellSpacing="0" cellPadding="0"> <tbody onMouseDown={this.handleAllClickStarts} onDragEnter={this.onDragEnter} onDragOver={this.onDragOver} onMouseUp={this.handleAllClickEnds} onMouseOver={this.handleMouseOver}> {this.getBodyRows().map(renderBodyRows, this)} </tbody> </table> </div> </div> ); } }; ReactAgenda.propTypes = { minDate: PropTypes.instanceOf(Date), maxDate: PropTypes.instanceOf(Date), startDate: PropTypes.instanceOf(Date), startAtTime: PropTypes.number, endAtTime: PropTypes.number, cellHeight: PropTypes.number, view: PropTypes.string, locale: PropTypes.string, items: PropTypes.array, helper: PropTypes.bool, itemComponent: PropTypes.element, numberOfDays: PropTypes.number, headFormat: PropTypes.string, rowsPerHour: PropTypes.number, itemColors: PropTypes.object, fixedHeader: PropTypes.bool, autoScaleNumber: PropTypes.bool, showHeader: PropTypes.bool }; ReactAgenda.defaultProps = { minDate: new Date(), maxDate: new Date(new Date().getFullYear(), new Date().getMonth() + 3), startDate: new Date(), startAtTime: 0, endAtTime: 0, cellHeight: 15, view: "agenda", locale: "en", helper: true, items: [], autoScale: false, itemComponent: ReactAgendaItem, numberOfDays: 4, headFormat: "ddd DD MMM", rowsPerHour: 4, itemColors: { 'color-1': "rgba(102, 195, 131 , 1)", "color-2": "rgba(242, 177, 52, 1)", "color-3": "rgba(235, 85, 59, 1)", "color-4": "rgba(70, 159, 213, 1)" }, fixedHeader: true, showHeader: true, }