Issue #2445Opened December 6, 2019by kickbk1 reactions

Datetimepicker in a new component

Question

I apologize for the length of this question! It's a whole new component an I'm almost done writing it, I just need a little bit of help and I believe this could be useful for others.

I am creating a date select input that uses the daterangepicker lib. gatherDateOptions() gathers all date type fields via the options. I have date, datetime, daterange, and datetimerange fields. I then later use their name and split on "-" to get the field type.

I load the scripts one by one to avoid dependency issues (like "$ is not defined"). If you believe there's a more efficient way to do this, please advise. Once daterangepicker is loaded, I can run the initDateRange function and indeed it will run daterangepicker on the field and generate the date select dropdown in the correct place (under the input field), but I will explain later why this seems like a waste.

Since I have a select that allows to switch between fields and their type, I need a function to listen to that change and update the input field. So below defaults I added the init() function and I'm listening to the change event and running handleDateInputsOptions.

Problem No. 1: When executing daterangepicker initialization in default: script: the daterangepicker select pop up appears in the correct place - underneath the input field. However, when initializing daterangepicker inside handleDateInputsOptions it appends daterangepicker to the outer body and not below the input inside our grapesjs iframe. Looking at https://www.daterangepicker.com/#options you will see that we can add a parentEl, however setting it to iframe appends daterangepicker inside the iframe, but I'm unable to place it inside the body of the iframe. I have a feeling that the reason it appends the daterangepicker to the main body and not the iframe body is because I initialize daterangepicker outside the model? I have a feeling my init() function should happen inside default: script:? if so, how do I do that?

Problem No. 2: To properly initialize daterangepicker on the input on load, I can use default: script: but like I wrote inside my script below, if I want to initialize daterangepicker there, I will have to write most of the handleDateInputsOptions fucntion twice to check for what type of date field the input is. This seems like a waste.

Any help will be much appreciated!

function gatherDateOptions() {
	var customDateFieldInputs = c.customDateFields.split(',');
	var customDateRangeInputs = c.customDateRangeFields.split(',');
	var customDatetimeInputs = c.customDatetimeFields.split(',');
	var customDatetimeRangeInputs = c.customDatetimeRangeFields.split(',');

	const dateInputsOptions = []

	// Add date fields
	if (c.customDateFields.length){
		$.each(customDateFieldInputs, function( index, field ) {
			dateInputsOptions.push({
				value: field + " - Date", name: field + " - Date"
			})
		});
	}
	// Add dateRange fields
	if (c.customDateRangeFields.length){
		$.each(customDateRangeInputs, function( index, field ) {
			dateInputsOptions.push({
				value: field + " - Date Range", name: field + " - Date Range"
			})
		});
	}
	// Add datetime fields
	if (c.customDatetimeFields.length){
		$.each(customDatetimeInputs, function( index, field ) {
			dateInputsOptions.push({
				value: field + " - Datetime", name: field + " - Datetime"
			})
		});
	}
	// Add datetimeRange fields
	if (c.customDatetimeRangeFields.length){
		$.each(customDatetimeRangeInputs, function( index, field ) {
			dateInputsOptions.push({
				value: field + " - Datetime Range", name: field + " - Datetime Range"
			})
		});
	}

	return dateInputsOptions;
}

let dateInputsOptions = gatherDateOptions();

domc.addType('date', {
	model: defaultModel.extend({
		defaults: {
			...defaultModel.prototype.defaults,
			'custom-name': c.labelDateName,
			tagName: 'input',
			type: "date",
			draggable: 'form, form *', // Can be dropped only inside `form` elements
			droppable: false, // Can't drop other elements inside
			copyable: false, // Do not allow to duplicate the component
			traits: [
				'placeholder',
				{
					type: 'select',
					label: 'Select date field',
					name: 'dateInputsSelect',
					changeProp: 1,
					options: dateInputsOptions
				},
				{ type: 'checkbox', name: 'required' },
			],
			script: function () {
				var input = this;
				// console.log("Working input", input);
				var initDateRange = function() {
					// If I want to initialize daterangepicker here, I will have to write most of the handleDateInputsOptions fucntion twice to check for what type of date field the input is. This seems like a waste.
					// $(input).daterangepicker({
					// 	singleDatePicker: true,
					// 	showDropdowns: true,
					// });
				}

				if (typeof daterangepicker == 'undefined' || typeof jQuery == 'undefined' || typeof moment == 'undefined') {

					if (typeof jQuery == 'undefined'){
						var jquery = document.createElement('script');
						jquery.src = '//cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js';
						document.body.appendChild(jquery);

						jquery.onload = function(){
							loadMoment();
						}
					} else {
						loadMoment();
					}
					function loadMoment(){
						if (typeof moment == 'undefined'){
							var moment = document.createElement('script');
							moment.src = '//cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js';
							document.body.appendChild(moment);

							moment.onload = function(){
								loadDatePicker();
							}
						} else {
							loadDatePicker();
						}
					}
					function loadDatePicker(){
						if (typeof $ == 'undefined'){window.$ = jQuery};
						var link = document.createElement('link');
						link.rel = "stylesheet";
						link.href = "//cdnjs.cloudflare.com/ajax/libs/bootstrap-daterangepicker/3.0.5/daterangepicker.min.css";
						document.body.appendChild(link);

						var daterangepicker = document.createElement('script');
						daterangepicker.src = '//cdnjs.cloudflare.com/ajax/libs/bootstrap-daterangepicker/3.0.5/daterangepicker.min.js';
						document.body.appendChild(daterangepicker);

						daterangepicker.onload = initDateRange;
					}
				} else {
					// console.log("all libs present");
					initDateRange();
				}
			},
		},
		init() {
			// this.handleDateInputsOptions(); <<<< This will fail because it doesn't know what the view is at this stage. Maybe it's supposed to load something still?, which brings me to the next line:
			// this.listenTo(this, 'this.view.el is loaded?????', this.handleDateInputsOptions); <<<< What is that event to listen to the moment this.view.el is loaded?
			this.listenTo(this, 'change:dateInputsSelect', this.handleDateInputsOptions);
		},
		handleDateInputsOptions() {
			if (typeof $(this.view.el).data('daterangepicker') != 'undefined'){
				$(this.view.el).data('daterangepicker').remove();
			}
			function initDateRange(input, options) {
				$(input).daterangepicker(options);
			}
			let dateOption = this.get('dateInputsSelect');
			var val = dateOption.split(' - ');
			var fieldName = "Custom." + val[0];
			var type = val[1].trim().toLowerCase();
			var fieldOptions = {};
			this.set('attributes', {
				name: fieldName,
				id: val[0],
				type: "text",
			});

			if (type == "date"){
				fieldOptions = {
					singleDatePicker: true,
					showDropdowns: true,
				}
			} else if (type == "datetime"){
				fieldOptions = {
					singleDatePicker: true,
					showDropdowns: true,
					timePicker: true,
					locale: {
						format: 'MM/DD/YYYY hh:mm A'
					}
				}
			} else if (type == "datetimerange"){
				fieldOptions = {
					timePicker: true,
					locale: {
						format: 'MM/DD/YYYY hh:mm A'
					}
				}
			}

			initDateRange(this.view.el, fieldOptions);
		},

	}, {
		isComponent(el) {
			var dateType;
			if (typeof el.dataset != "undefined" && el.dataset.type == "date") {
				dateType = true;
			} else {
				dateType = false;
			}
			if(el.tagName == 'INPUT' && dateType){
				return {type: 'date'};
			}
		},
	}),
	view: defaultView
});

Answers (1)

artfDecember 10, 20191 reactions

Most of the logic you're doing in handleDateInputsOptions should be inside your component script. Your dateInputsSelect prop contains the input type, so use the interpolation in your script then apply the logic of handleDateInputsOptions. One other trick you'd need is to trigger script update in the canvas, so do this in your model.init

init() {
	this.on('change:dateInputsSelect', () => this.trigger('change:script'));
}

ps. do a favor to yourself... use the new Component API to define custom types, with the old one is quite easy to break stuff

Related Questions and Answers

Continue research with similar issue discussions.

Paid Plugins That Match This Issue

Curated by issue keywords and label relevance to help you ship faster.

View all plugins

Loading paid plugin recommendations...

Browse Plugin Categories

Jump directly to plugin category pages on the marketplace.