2020-10-02 15:10:00 +00:00
|
|
|
/* global $, document */
|
|
|
|
const moment = require('moment-timezone');
|
|
|
|
const _ = require('underscore');
|
|
|
|
const gutil = require('app/common/gutil');
|
|
|
|
const commands = require('../components/commands');
|
|
|
|
const dispose = require('../lib/dispose');
|
|
|
|
const dom = require('../lib/dom');
|
|
|
|
const kd = require('../lib/koDom');
|
|
|
|
const TextEditor = require('./TextEditor');
|
2021-08-20 18:18:34 +00:00
|
|
|
const { parseDate, TWO_DIGIT_YEAR_THRESHOLD } = require('app/common/parseDate');
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// DatePicker unfortunately requires an <input> (not <textarea>). But textarea is better for us,
|
|
|
|
// because sometimes it's taller than a line, and an <input> looks worse. The following
|
|
|
|
// unconsionable hack tricks Datepicker into thinking anything it's attached to is an input.
|
|
|
|
// It's more reasonable to just modify boostrap-datepicker, but that has its own downside (with
|
|
|
|
// upgrading and minification). This hack, however, is simpler than other workarounds.
|
|
|
|
var Datepicker = $.fn.datepicker.Constructor;
|
|
|
|
// datepicker.isInput can now be set to anything, but when read, always returns true. Tricksy.
|
|
|
|
Object.defineProperty(Datepicker.prototype, 'isInput', {
|
|
|
|
get: function() { return true; },
|
|
|
|
set: function(v) {},
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* DateEditor - Editor for Date type. Includes a dropdown datepicker.
|
|
|
|
* See reference: http://bootstrap-datepicker.readthedocs.org/en/latest/index.html
|
|
|
|
*
|
|
|
|
* @param {String} options.timezone: Optional timezone to use instead of UTC.
|
|
|
|
*/
|
|
|
|
function DateEditor(options) {
|
|
|
|
// A string that is always `UTC` in the DateEditor, eases DateTimeEditor inheritance.
|
|
|
|
this.timezone = options.timezone || 'UTC';
|
2021-08-26 16:35:11 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
this.dateFormat = options.field.widgetOptionsJson.peek().dateFormat;
|
2021-08-26 16:35:11 +00:00
|
|
|
this.locale = options.field.documentSettings.peek().locale;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-12-07 06:19:27 +00:00
|
|
|
// Update moment format string to represent a date unambiguously.
|
|
|
|
this.safeFormat = makeFullMomentFormat(this.dateFormat);
|
2021-06-17 16:41:07 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// Use the default local timezone to format the placeholder date.
|
2021-12-07 06:19:27 +00:00
|
|
|
const defaultTimezone = moment.tz.guess();
|
2020-10-02 15:10:00 +00:00
|
|
|
let placeholder = moment.tz(defaultTimezone).format(this.safeFormat);
|
2021-06-17 16:41:07 +00:00
|
|
|
if (options.readonly) {
|
|
|
|
// clear placeholder for readonly mode
|
|
|
|
placeholder = null;
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
TextEditor.call(this, _.defaults(options, { placeholder: placeholder }));
|
|
|
|
|
2021-12-07 06:19:27 +00:00
|
|
|
const cellValue = this.formatValue(options.cellValue, this.safeFormat, true);
|
2021-06-17 16:41:07 +00:00
|
|
|
|
2020-10-02 15:10:00 +00:00
|
|
|
// Set the edited value, if not explicitly given, to the formatted version of cellValue.
|
2021-06-17 16:41:07 +00:00
|
|
|
this.textInput.value = gutil.undef(options.state, options.editValue, cellValue);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-06-17 16:41:07 +00:00
|
|
|
if (!options.readonly) {
|
|
|
|
// Indicates whether keyboard navigation is active for the datepicker.
|
|
|
|
this._keyboardNav = false;
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-06-17 16:41:07 +00:00
|
|
|
// Attach the datepicker.
|
|
|
|
this._datePickerWidget = $(this.textInput).datepicker({
|
|
|
|
keyboardNavigation: false,
|
|
|
|
forceParse: false,
|
|
|
|
todayHighlight: true,
|
|
|
|
todayBtn: 'linked',
|
2021-08-20 18:18:34 +00:00
|
|
|
assumeNearbyYear: TWO_DIGIT_YEAR_THRESHOLD,
|
2021-08-26 16:35:11 +00:00
|
|
|
// Datepicker supports most of the languages. They just need to be included in the bundle
|
|
|
|
// or by script tag, i.e.
|
|
|
|
// <script src="bootstrap-datepicker/dist/locales/bootstrap-datepicker.pl.min.js"></script>
|
|
|
|
language : this.getLanguage(),
|
2021-12-07 06:19:27 +00:00
|
|
|
// Use the stripped format converted to one suitable for the datepicker.
|
|
|
|
format: {
|
|
|
|
toDisplay: (date, format, language) => moment.utc(date).format(this.safeFormat),
|
|
|
|
toValue: (date, format, language) => {
|
|
|
|
const timestampSec = parseDate(date, {
|
|
|
|
dateFormat: this.safeFormat,
|
|
|
|
timezone: this.timezone,
|
|
|
|
});
|
|
|
|
return (timestampSec === null) ? null : new Date(timestampSec * 1000);
|
|
|
|
},
|
|
|
|
},
|
2021-06-17 16:41:07 +00:00
|
|
|
});
|
2021-08-20 18:18:34 +00:00
|
|
|
this.autoDisposeCallback(() => this._datePickerWidget.datepicker('destroy'));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-06-17 16:41:07 +00:00
|
|
|
// NOTE: Datepicker interferes with normal enter and escape functionality. Add an event handler
|
|
|
|
// to the DatePicker to prevent interference with normal behavior.
|
|
|
|
this._datePickerWidget.on('keydown', e => {
|
|
|
|
// If enter or escape is pressed, destroy the datepicker and re-dispatch the event.
|
|
|
|
if (e.keyCode === 13 || e.keyCode === 27) {
|
2021-08-20 18:18:34 +00:00
|
|
|
this._datePickerWidget.datepicker('destroy');
|
2021-06-17 16:41:07 +00:00
|
|
|
// The current target of the event will be the textarea.
|
|
|
|
setTimeout(() => e.currentTarget.dispatchEvent(e.originalEvent), 0);
|
|
|
|
}
|
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-06-17 16:41:07 +00:00
|
|
|
// When the up/down arrow is pressed, modify the datepicker options to take control of
|
|
|
|
// the arrow keys for date selection.
|
|
|
|
let datepickerCommands = Object.assign({}, options.commands, {
|
|
|
|
datepickerFocus: () => { this._allowKeyboardNav(true); }
|
|
|
|
});
|
|
|
|
this._datepickerCommands = this.autoDispose(commands.createGroup(datepickerCommands, this, true));
|
2020-10-02 15:10:00 +00:00
|
|
|
|
2021-06-17 16:41:07 +00:00
|
|
|
this._datePickerWidget.on('show', () => {
|
|
|
|
// A workaround to allow clicking in the datepicker without losing focus.
|
|
|
|
dom(document.querySelector('.datepicker'),
|
|
|
|
kd.attr('tabIndex', 0), // allows datepicker to gain focus
|
|
|
|
kd.toggleClass('clipboard_focus', true) // tells clipboard to not steal focus from us
|
|
|
|
);
|
|
|
|
// Attach command group to the input to allow switching keyboard focus to the datepicker.
|
|
|
|
dom(this.textInput,
|
|
|
|
// If the user inputs text into the textbox, take keyboard focus from the datepicker.
|
|
|
|
dom.on('input', () => { this._allowKeyboardNav(false); }),
|
|
|
|
this._datepickerCommands.attach()
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
dispose.makeDisposable(DateEditor);
|
|
|
|
_.extend(DateEditor.prototype, TextEditor.prototype);
|
|
|
|
|
|
|
|
/** @inheritdoc */
|
|
|
|
DateEditor.prototype.getCellValue = function() {
|
|
|
|
let timestamp = parseDate(this.textInput.value, {
|
|
|
|
dateFormat: this.safeFormat,
|
|
|
|
timezone: this.timezone
|
|
|
|
});
|
|
|
|
return timestamp !== null ? timestamp : this.textInput.value;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Helper to allow/disallow keyboard navigation within the datepicker.
|
|
|
|
DateEditor.prototype._allowKeyboardNav = function(bool) {
|
|
|
|
if (this._keyboardNav !== bool) {
|
|
|
|
this._keyboardNav = bool;
|
|
|
|
$(this.textInput).data().datepicker.o.keyboardNavigation = bool;
|
|
|
|
// Force parse must be turned on with keyboard navigation, since it forces the highlighted date
|
|
|
|
// to be used when enter is pressed. Otherwise, keyboard date selection will have no effect.
|
|
|
|
$(this.textInput).data().datepicker.o.forceParse = bool;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Moment value formatting helper.
|
2021-12-07 06:19:27 +00:00
|
|
|
DateEditor.prototype.formatValue = function(value, formatString, shouldFallBackToValue) {
|
2020-10-02 15:10:00 +00:00
|
|
|
if (_.isNumber(value) && formatString) {
|
|
|
|
return moment.tz(value*1000, this.timezone).format(formatString);
|
|
|
|
} else {
|
2021-12-07 06:19:27 +00:00
|
|
|
// If value is AltText, return it unchanged. This way we can see it and edit in the editor.
|
|
|
|
return (shouldFallBackToValue && typeof value === 'string') ? value : "";
|
2020-10-02 15:10:00 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-08-26 16:35:11 +00:00
|
|
|
// Gets the language based on the current locale.
|
|
|
|
DateEditor.prototype.getLanguage = function() {
|
|
|
|
// this requires a polyfill, i.e. https://www.npmjs.com/package/@formatjs/intl-locale
|
|
|
|
// more info about ts: https://github.com/microsoft/TypeScript/issues/37326
|
|
|
|
// return new Intl.Locale(locale).language;
|
|
|
|
return this.locale.substr(0, this.locale.indexOf("-"));
|
|
|
|
}
|
|
|
|
|
2021-12-07 06:19:27 +00:00
|
|
|
// Updates the given Moment format to specify a complete date, so that the datepicker sees an
|
|
|
|
// unambiguous date in the textbox input. If the format is incomplete, fall back to YYYY-MM-DD.
|
|
|
|
function makeFullMomentFormat(mFormat) {
|
|
|
|
let safeFormat = mFormat;
|
|
|
|
if (!safeFormat.includes('Y')) {
|
|
|
|
safeFormat += " YYYY";
|
|
|
|
}
|
|
|
|
if (!safeFormat.includes('D') || !safeFormat.includes('M')) {
|
|
|
|
safeFormat = 'YYYY-MM-DD';
|
|
|
|
}
|
|
|
|
return safeFormat;
|
|
|
|
}
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
module.exports = DateEditor;
|