lightning_scheduler-8.x-1.x-dev/js/TransitionSet.es6.js
js/TransitionSet.es6.js
import { Component, createElement } from 'react';
import dateFormat from 'dateformat';
export default class extends Component
{
/**
* Constructor.
*
* @param {object} props
* @param {object[]} props.transitions
* @param {object} props.states
* @param {string} props.input
* @param {bool} props.step
*/
constructor (props)
{
super (props);
const transitions = props.transitions || [];
const states = Object.entries( props.states );
this.state = {
transitions: transitions.map(t => {
// Special case: when node edit page is viewed after preview.
// Unix timestamp is stored in input of Drupal.
// Converting this so that this is readable by JS.
if (typeof t.when == 'number') {
t.when = t.when * 1000;
}
// The date and time of a transition is passed in as an ISO
// 8601 UTC string, which we need to convert to a date object.
t.when = new Date(t.when);
return t;
}),
};
// When creating a new transition, default to the first available
// moderation state.
this.defaultState = states[0][0];
// The moderation state options never change, so build them once now.
this.stateOptions = states.map(state => {
const [ id, label ] = state;
return <option key={ id } value={ id }>{ label }</option>
});
}
/**
* Renders a saved transition.
*
* @param {object} transition
* @param {string} transition.state
* @param {Date} transition.when
* @param {number} index
* @returns {*}
*/
renderSaved (transition, index)
{
// Render the human-friendly label of the moderation state.
const state = this.props.states[ transition.state ];
// Render the date and time in a human-friendly format.
const date = this.localizeDate(document.documentElement.lang, transition.when);
// Event handler invoked when the transition is removed.
const onRemove = (event) => {
event.preventDefault();
this.setState(prev => {
// Splice the transition out of the current set.
prev.transitions.splice(index, 1);
return prev;
});
};
// Provide a detailed title so we can remove specific transitions in
// testing.
const remove_title = Drupal.t('Remove transition to @state on @date', {
'@state': state,
'@date': date
});
const class_list = ['scheduled-transition'];
return (
<div className={ class_list.join(' ') }>
{ Drupal.t('Change to') } <b>{ state }</b> { Drupal.t('on') } { date }
<a title={ remove_title } href="#" onClick={ onRemove }>{ Drupal.t('remove') }</a>
</div>
);
}
/**
* Renders the date.
*
* @returns {*}
*/
localizeDate(langCode, currentDate) {
const formatter = new Intl.DateTimeFormat(langCode, {
year: 'numeric',
month: 'long',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
return formatter.format(currentDate);
}
/**
* Renders the transition edit form.
*
* @returns {*}
*/
renderForm ()
{
return createElement('div', { className: 'scheduled-transition' },
Drupal.t('Change to'),
this.renderStateControl(),
Drupal.t('on'),
this.renderDateControl(),
Drupal.t('at'),
this.renderTimeControl(),
this.renderFormActions()
);
}
/**
* Renders the moderation state select box for a transition.
*
* @returns {*}
*/
renderStateControl ()
{
const onChange = (event) => {
// The event target will not be available when updating state, so
// we need to bind it here.
const element = event.target;
this.setState(prev => {
prev.edit.state = element.options[element.selectedIndex].value;
return prev;
});
};
const value = this.state.edit.state;
return (
<label>
<span hidden>{ Drupal.t('Scheduled moderation state') }</span>
<select defaultValue={ value } onChange={ onChange } class="form-select form-element form-element--type-select">{ this.stateOptions }</select>
</label>
);
}
/**
* Renders the date input field for a transition.
*
* @returns {*}
*/
renderDateControl ()
{
const onChange = (event) => {
// The event target will not be available when updating state, so
// we need to bind it here.
const element = event.target;
// Only change the date in state if it's valid.
if (element.checkValidity())
{
this.setState(prev => {
const date = element.value.split('-');
// The month needs to be zero-based, not one-based.
date[1]--;
prev.edit.when.setFullYear(...date);
return prev;
});
}
};
const value = dateFormat(this.state.edit.when, 'isoDate');
let mindate = "";
if (!drupalSettings.lightning_scheduler.allow_past_dates) {
let today;
today = new Date();
mindate = today.getFullYear() + '-' + ("0" + (today.getMonth() + 1)).slice(-2) + '-' + ("0" + today.getDate()).slice(-2);
}
return (
<label>
<span hidden>{ Drupal.t('Scheduled transition date') }</span>
<input required min={ mindate } defaultValue={ value } type="date" onChange={ onChange } class="form-date form-element form-element--type-date form-element--api-date"/>
</label>
);
}
/**
* Renders the time input field for a transition.
*
* @returns {*}
*/
renderTimeControl ()
{
const onChange = (event) => {
// The event target will not be available when updating state, so
// we need to bind it here.
const element = event.target;
this.setState(prev => {
prev.edit.when.setHours(...element.value.split(':'));
return prev;
});
};
let format;
if (this.props.step >= 3600) {
format = 'HH:00';
}
else if (this.props.step >= 60) {
format = 'HH:MM';
}
else {
format = 'isoTime';
}
const value = dateFormat(this.state.edit.when, format);
return (
<label>
<span hidden>{ Drupal.t('Scheduled transition time') }</span>
<input required defaultValue={ value } type="time" onChange={ onChange } step={ this.props.step } class="form-time form-element form-element--type-time form-element--api-date"/>
</label>
);
}
/**
* Renders the form actions for a transition being edited.
*
* @returns {*[]}
*/
renderFormActions ()
{
// Event handler invoked when the form is cancelled.
const onCancel = (event) => {
// Don't actually click the link.
event.preventDefault();
this.setState(prev => {
// The form will not appear if edit is null. See render().
prev.edit = null;
return prev;
});
};
// Event handler invoked when the form is saved.
const onSave = (event) => {
// Don't actually click the link.
event.preventDefault();
this.setState(prev => {
prev.transitions.push(prev.edit);
// The form will not appear if edit is null. See render().
prev.edit = null;
return prev;
});
};
return (
<span>
<button className="button" title={ Drupal.t('Save transition') } onClick={ onSave }>{ Drupal.t('Save') }</button>
{ Drupal.t('or') }
<a title={ Drupal.t('Cancel transition') } href="#" onClick={ onCancel }>{ Drupal.t('cancel') }</a>
</span>
);
}
/**
* Renders the component.
*
* @returns {*[]}
*/
render ()
{
const elements = [
createElement('input', {
type: 'hidden',
name: this.props.input,
value: JSON.stringify(this),
}),
this.state.transitions.map((t, i) => this.renderSaved(t, i))
];
// If we're currently editing a transition, render the form. Otherwise,
// render an 'add another' link.
elements.push( this.state.edit ? this.renderForm() : this.renderAddLink() );
return elements;
}
toJSON ()
{
let propsStep = parseInt(this.props.step);
return this.state.transitions.map(t => {
// Trim additional seconds based on configured granularity.
let trimWhen = t.when.getTime() / 1000;
if (!isNaN(propsStep) && propsStep >= 60) {
trimWhen -= trimWhen % 60;
}
return {
// JavaScript returns time stamps in milliseconds, but PHP handles them
// in seconds. Divide by 1000 to convert to seconds, then round down to
// ensure we do not send a floating-point value back to the server. We
// always round down (instead of using Math.round()) to in order to
// prevent off-by-one-second timing failures in tests.
when: Math.floor(trimWhen),
state: t.state,
};
});
}
renderAddLink ()
{
// Event handler invoked to add another transition.
const onAdd = (event) => {
// Don't actually click the link.
event.preventDefault();
this.add();
};
const link_text = this.state.transitions.length
? Drupal.t('add another')
: Drupal.t('Schedule a status change');
return <a href="#" onClick={ onAdd }>{ link_text }</a>
}
/**
* Adds a new transition, for editing.
*/
add ()
{
let propsStep = parseInt(this.props.step);
this.setState(prev => {
let newWhen = new Date();
// JavaScript returns time stamps in milliseconds, but PHP handles them
// in seconds. Divide by 1000 to convert to seconds, then round down to
// ensure we do not send a floating-point value back to the server. We
// always round down (instead of using Math.round()) to in order to
// prevent off-by-one-second timing failures in tests.
let trimWhen = newWhen.getTime() / 1000;
// Trim additional seconds based on configured granularity.
if (!isNaN(propsStep) && propsStep >= 60) {
trimWhen -= trimWhen % 60;
}
prev.edit = {
state: this.defaultState,
when: new Date(Math.floor(trimWhen) * 1000),
};
// If there is an existing transition, use its date and time as
// the default for one we are creating.
const last = prev.transitions.slice(-1).shift();
if (last)
{
prev.edit.when.setTime( last.when.getTime() );
}
return prev;
});
}
}
