/* global document */ const moment = require('moment-timezone'); const _ = require('underscore'); const dom = require('../lib/dom'); const dispose = require('../lib/dispose'); const kd = require('../lib/koDom'); const DateEditor = require('./DateEditor'); const gutil = require('app/common/gutil'); const { parseDate } = require('app/common/parseDate'); const TextEditor = require('./TextEditor'); /** * DateTimeEditor - Editor for DateTime type. Includes a dropdown datepicker. * See reference: http://bootstrap-datepicker.readthedocs.org/en/latest/index.html */ function DateTimeEditor(options) { // Get the timezone from the end of the type string. options.timezone = gutil.removePrefix(options.field.column().type(), "DateTime:"); // Adjust the command group. var origCommands = options.commands; // don't modify navigation for readonly mode if (!options.readonly) { options.commands = Object.assign({}, origCommands, { prevField: () => this._focusIndex() === 1 ? this._setFocus(0) : origCommands.prevField(), nextField: () => this._focusIndex() === 0 ? this._setFocus(1) : origCommands.nextField(), }); } // Call the superclass. DateEditor.call(this, options); this._timeFormat = options.field.widgetOptionsJson.peek().timeFormat; // To reuse code, this knows all about the DOM that DateEditor builds (using TextEditor), and // modifies that to be two side-by-side textareas. this._dateSizer = this.contentSizer; // For consistency with _timeSizer and _timeInput. this._dateInput = this.textInput; const isValid = _.isNumber(options.cellValue); const formatted = this.formatValue(options.cellValue, this._timeFormat); // Use a placeholder of 12:00am, since that is the autofill time value. const placeholder = moment.tz('0', 'H', this.timezone).format(this._timeFormat); // for readonly if (options.readonly) { if (!isValid) { // do nothing - DateEditor will show correct error } else { // append time format or a placeholder const time = (formatted || placeholder); const sep = time ? ' ' : ''; this.textInput.value = this.textInput.value + sep + time; } } else { dom(this.dom, kd.toggleClass('celleditor_datetime', true)); dom(this.dom.firstChild, kd.toggleClass('celleditor_datetime_editor', true)); this.dom.appendChild( dom('div.celleditor_cursor_editor.celleditor_datetime_editor', this._timeSizer = dom('div.celleditor_content_measure'), this._timeInput = dom('textarea.celleditor_text_editor', kd.attr('placeholder', placeholder), kd.value(formatted), this.commandGroup.attach(), dom.on('input', () => this.onChange()) ) ) ); } // If the edit value is encoded json, use those values as a starting point if (typeof options.state == 'string') { try { const { date, time } = JSON.parse(options.state); this._dateInput.value = date; this._timeInput.value = time; this.onChange(); } catch(e) { console.error("DateTimeEditor can't restore its previous state") } } } dispose.makeDisposable(DateTimeEditor); _.extend(DateTimeEditor.prototype, DateEditor.prototype); DateTimeEditor.prototype.setSizerLimits = function() { var maxSize = this.editorPlacement.calcSize({width: Infinity, height: Infinity}, {calcOnly: true}); if (this.options.readonly) { return; } this._dateSizer.style.maxWidth = this._timeSizer.style.maxWidth = Math.ceil(maxSize.width / 2 - 6) + 'px'; }; /** * Returns which element has focus: 0 if date, 1 if time, null if neither. */ DateTimeEditor.prototype._focusIndex = function() { return document.activeElement === this._dateInput ? 0 : (document.activeElement === this._timeInput ? 1 : null); }; /** * Sets focus to date if index is 0, or time if index is 1. */ DateTimeEditor.prototype._setFocus = function(index) { var elem = (index === 0 ? this._dateInput : (index === 1 ? this._timeInput : null)); if (elem) { elem.focus(); elem.selectionStart = 0; elem.selectionEnd = elem.value.length; } }; /** * Occurs when user types something into the editor */ DateTimeEditor.prototype.onChange = function() { this._resizeInput(); // store editor state as an encoded JSON string const date = this._dateInput.value; const time = this._timeInput.value; this.editorState.set(JSON.stringify({ date, time})); } DateTimeEditor.prototype.getCellValue = function() { let date = this._dateInput.value; let time = this._timeInput.value; let timestamp = parseDate(date, { dateFormat: this.safeFormat, time: time, timeFormat: this._timeFormat, timezone: this.timezone }); return timestamp !== null ? timestamp : (date && time ? `${date} ${time}` : date || time); }; /** * Overrides the resizing function in TextEditor. */ DateTimeEditor.prototype._resizeInput = function() { // for readonly field, we will use logic from a super class if (this.options.readonly) { TextEditor.prototype._resizeInput.call(this); return; } // Use the size calculation provided in options.calcSize (that takes into account cell size and // screen size), with both date and time parts as the input. The resulting size is applied to // the parent (containing date + time), with date and time each expanding or shrinking from the // measured sizes using flexbox logic. this._dateSizer.textContent = this._dateInput.value; this._timeSizer.textContent = this._timeInput.value; var dateRect = this._dateSizer.getBoundingClientRect(); var timeRect = this._timeSizer.getBoundingClientRect(); // Textboxes get 3px of padding on left/right/top (see TextEditor.css); we specify it manually // since editorPlacement can't do a good job figuring it out with the flexbox arrangement. var size = this.editorPlacement.calcSize({ width: dateRect.width + timeRect.width + 12, height: Math.max(dateRect.height, timeRect.height) + 3 }); this.dom.style.width = size.width + 'px'; this._dateInput.parentNode.style.flexBasis = (dateRect.width + 6) + 'px'; this._timeInput.parentNode.style.flexBasis = (timeRect.width + 6) + 'px'; this._dateInput.style.height = Math.ceil(size.height - 3) + 'px'; this._timeInput.style.height = Math.ceil(size.height - 3) + 'px'; }; module.exports = DateTimeEditor;