Customizing the interval between the start and end times of calendar events to 3 hours

Thank you as always for your advice.

The PortlandLabs support team suggested that I cross-post in English, so I would appreciate your help.

I would appreciate it if you could tell me how to customize the interval between the start and end times of calendar events to 3 hours in ConcreteCMS 9.4.7.

The default interval is 1 hour.

For example, if the start time is 10:00, I would like the end time to be 13:00.

Thank you in advance.

This has also been asked in Japanese below:

I can’t find the file you mentioned in the question above.

You’ll need to override the calculateEndDate function, specifically line 239:
endDate.setTime(startDate.getTime() + (1 * 60 * 60 * 1000)) // one hour

I searched all folders in ConcreteCMS, but it didn’t exist.

In which directory are the image files located?

Please help if you have any knowledge.

'm sorry for asking multiple times.
I’m really stumped and would appreciate some advice. I searched for the image file “duration.js” on Windows 11, but I couldn’t find it.
I also searched the “calendar” and “js” directories, but I couldn’t find it.

Where is the image hierarchy?

I can’t override it because I can’t find the original file.

I would appreciate it if you could provide information from the “concrete” directory.

Could you please help me somehow?
Thank you in advance.

The files will have been combined into the calendar ‘feature’

Thank you for your reply.

I don’t understand what you mean when you say “integrated into the ‘function’” so I’d like you to explain in more detail.

I apologize for the trouble.

When Concrete is built for distribution, groups of assets from the bedrock repository are merged together into ‘features’. So all the files in that group will be one big file in Concrete cms.

Thank you.
So you can’t find the file even after searching for it.
So do you know where the file in question is located?
Is it “fullcalendar.js”, “duration.php”, or a different file?
If you know, please let me know.
Thank you in advance.

Best approach is to probably just add an event handler rather than modifying the core files.

Something like this should probably work:

const startSelect = document.querySelector('[data-select="start-time"]');
const endSelect = document.querySelector('[data-select="end-time"]');

startSelect.addEventListener('change', function () {
  const startValue = this.value; // e.g. "8:30am"

  // Parse the start time
  const match = startValue.match(/^(\d+):(\d+)(am|pm)$/i);
  if (!match) return;

  let hours = parseInt(match[1]);
  const minutes = parseInt(match[2]);
  const period = match[3].toLowerCase();

  // Convert to 24h
  if (period === 'am' && hours === 12) hours = 0;
  if (period === 'pm' && hours !== 12) hours += 12;

  // Add 3 hours
  const totalMinutes = hours * 60 + minutes + 180;
  const endHours24 = (totalMinutes / 60 | 0) % 24;
  const endMinutes = totalMinutes % 60;

  // Convert back to 12h format
  let endHours12 = endHours24 % 12 || 12;
  const endPeriod = endHours24 < 12 ? 'am' : 'pm';
  const endValue = `${endHours12}:${String(endMinutes).padStart(2, '0')}${endPeriod}`;

  // Set the underlying select and trigger Tom Select to sync
  endSelect.value = endValue;
  endSelect.tomselect?.setValue(endValue);
});
1 Like

@Myq Into which file would the OP place this code?

1 Like

Thank you.
I’m also wondering which file and where I should write this?
I’m sorry to bother you during your busy schedule, but I would appreciate your advice.

Please explain in a way that even beginners can understand.
I apologize for the trouble.

@Myq
I hope we can resolve this soon.
Please do your best to help.

I searched online and found the following answer:

Concrete CMS’s Event Handler is a feature that allows you to add custom processing (PHP) to specific actions such as user registration or page deletion, without directly editing (hacking) core files. It primarily uses Symfony’s EventDispatcher to monitor and execute events in application/bootstrap/app.php, etc.

I added the code described above to application/bootstrap/app.php, but I’m getting an unexpected error.

The code offered by @Myq is JavaScript not PHP so you need to take it back out of app.php

I spent a little time chatting with ChatGPT about this and here’s what ‘we’ came up with. Go to the Dashboard → Sitemap and choose “Show system pages” in the options (top right). Then expand Calendars and Events and click on View Calendar and choose “Attributes”. Then add “Extra header content” and put the JavaScript below into that box and save. Don’t add any comments to this JavaScript because concrete likes to try to clean up stuff and comments seem to truncate the script. I’m not exactly sure what behavior you are looking for but this script defaults to 3 hour and if you change the start time, it populates the end time 3 hours later. You are then free to change the end time as you see fit but if you go back and change the start time again, it will insert a 3 hour end time again.

<script>
document.addEventListener('DOMContentLoaded', function () {

  function addHours(value, hoursToAdd) {
    var match = value.match(/^(\d+):(\d+)(am|pm)$/i);
    if (!match) return null;

    var hours = parseInt(match[1]);
    var minutes = parseInt(match[2]);
    var period = match[3].toLowerCase();

    if (period === 'am' && hours === 12) hours = 0;
    if (period === 'pm' && hours !== 12) hours += 12;

    var totalMinutes = hours * 60 + minutes + (hoursToAdd * 60);
    var endHours24 = Math.floor(totalMinutes / 60) % 24;
    var endMinutes = totalMinutes % 60;

    var endHours12 = endHours24 % 12 || 12;
    var endPeriod = endHours24 < 12 ? 'am' : 'pm';

    return endHours12 + ':' + String(endMinutes).padStart(2, '0') + endPeriod;
  }

  function attachTimeLogic() {

    var selects = document.querySelectorAll('select');

    var startTS = null;
    var endTS = null;

    selects.forEach(function(select) {
      if (!select.tomselect) return;

      var name = (select.name || '').toLowerCase();

      if (name.indexOf('start') !== -1) startTS = select.tomselect;
      if (name.indexOf('end') !== -1) endTS = select.tomselect;
    });

    if (!startTS || !endTS) return;
    if (startTS._smartAttached) return;
    startTS._smartAttached = true;

    var initialized = false;

    function setEnd(startValue) {
      var newEnd = addHours(startValue, 3);
      if (newEnd) endTS.setValue(newEnd);
    }

    setTimeout(function () {
      var startValue = startTS.getValue();
      if (startValue) {
        setEnd(startValue);
        initialized = true;
      }
    }, 100);

    startTS.on('change', function (value) {
      if (!initialized) return;
      setEnd(value);
    });

    if (endTS.control) {
      endTS.control.addEventListener('mousedown', function () {
        var value = endTS.getValue();
        if (!value) return;

        var option = endTS.getOption(value);
        if (option) {
          endTS.setActiveOption(option);
        }
      });
    }

  }

  var observer = new MutationObserver(function () {
    attachTimeLogic();
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true
  });

});
</script>

2 Likes

@mhawke

Thank you for looking into this.

I tried the method you suggested, but even after changing the start time for adding events, the interval remains at one hour.

I also tried clearing the cache, but the result was the same.

Is my setup incorrect?

I’ve attached a description and image of my method for your review.

I went to “Admin Panel” → “Sitemap” → “Include System Pages in Sitemap” → “Calendar and Events” → “Calendar Viewing” → “Attributes” → “Additional Header Element” and added all the code, then saved the changes.

Humm… That appears to be the correct way to do it. Here’s my “Dates” tab on my test event

First, I’d try opening this up in a browser you’ve never used before on this page in case your browser is being stubborn holding stuff in it’s cache.

Next, I’d re-visit the Extra Header Content on that page and scroll down to the very bottom and make sure it ends with a closing < /script > . Concrete likes to ‘sanitize’ this stuff and it’s possible the JS is getting cut off. That happened to me several times. Here’s the exact code off my successful test Event in case I posted something wrong above.

<script>
document.addEventListener('DOMContentLoaded', function () {

  function addHours(value, hoursToAdd) {
    var match = value.match(/^(\d+):(\d+)(am|pm)$/i);
    if (!match) return null;

    var hours = parseInt(match[1]);
    var minutes = parseInt(match[2]);
    var period = match[3].toLowerCase();

    if (period === 'am' && hours === 12) hours = 0;
    if (period === 'pm' && hours !== 12) hours += 12;

    var totalMinutes = hours * 60 + minutes + (hoursToAdd * 60);
    var endHours24 = Math.floor(totalMinutes / 60) % 24;
    var endMinutes = totalMinutes % 60;

    var endHours12 = endHours24 % 12 || 12;
    var endPeriod = endHours24 < 12 ? 'am' : 'pm';

    return endHours12 + ':' + String(endMinutes).padStart(2, '0') + endPeriod;
  }

  function attachTimeLogic() {

    var selects = document.querySelectorAll('select');

    var startTS = null;
    var endTS = null;

    selects.forEach(function(select) {
      if (!select.tomselect) return;

      var name = (select.name || '').toLowerCase();

      if (name.indexOf('start') !== -1) startTS = select.tomselect;
      if (name.indexOf('end') !== -1) endTS = select.tomselect;
    });

    if (!startTS || !endTS) return;
    if (startTS._smartAttached) return;
    startTS._smartAttached = true;

    var initialized = false;

    function setEnd(startValue) {
      var newEnd = addHours(startValue, 3);
      if (newEnd) endTS.setValue(newEnd);
    }

    setTimeout(function () {
      var startValue = startTS.getValue();
      if (startValue) {
        setEnd(startValue);
        initialized = true;
      }
    }, 100);

    startTS.on('change', function (value) {
      if (!initialized) return;
      setEnd(value);
    });

    if (endTS.control) {
      endTS.control.addEventListener('mousedown', function () {
        var value = endTS.getValue();
        if (!value) return;

        var option = endTS.getOption(value);
        if (option) {
          endTS.setActiveOption(option);
        }
      });
    }

  }

  var observer = new MutationObserver(function () {
    attachTimeLogic();
  });

  observer.observe(document.documentElement, {
    childList: true,
    subtree: true
  });

});
</script>

@mhawke

Thank you so much for trying multiple times.

I’ve tried resetting it again, but the situation is the same.

I’ve also cleared my browser cache.

I’ve cleared the admin panel cache and changed browsers, but the situation remains the same.

Could it be that something I customized previously is affecting it?

The following is a record of the customization I made previously for a different issue:

Just to be sure, I tried it with ConcreteCMS in its entirety without any customizations, but the situation was the same.

There was still no change; the interval remained at one hour.

If anyone knows the answer, I would be very grateful for your guidance

Well, this is odd. The screenshot I posted is from a version 9.4.7 site. Are there any JavaScript errors showing up in the Developer Console?

Thank you for your advice.

I’m close to the correct solution, but it’s not working.

I installed a new blank site with ConcreteCMS 9.4.8 for testing, but there’s absolutely no change; the interval remains at 1 hour. (Please see the image.)

I’ve also attached a screenshot from the developer tools.

Thank you for your review.

“I am close to the correct solution”… I love your optimism even when nothing has changed!! LOL

Can you right-click on the calendar page and “View page source” to confirm that the JavaScript segment is actually being added to the page? It should appear right near the top around line 40.

The developer errors/warnings are normal CKEditor stuff unrelated to this problem.