Our pure JavaScript Scheduler component


Post by mats »

Could you please share the full JS code for your POC so we can inspect & debug?


Post by jnarowski »

<script setup lang="ts">
// @ts-nocheck
import '@bryntum/calendar/calendar.stockholm.css'
import { ref, toRefs, computed, onMounted } from 'vue'
import { BryntumCalendar } from '@bryntum/calendar-vue-3'
import { Calendar, DateHelper } from '@bryntum/calendar'

import { PresetManager, StringHelper } from '@bryntum/schedulerpro'

interface iSpectoraCalendarProps {
  config: any
  events: any[]
  resources: any[]
  settings: any
  me: any
}

const convertTime12to24 = (time12h) => {
  const [time, modifier] = time12h.split(' ')

  let [hours, minutes] = time.split(':')

  if (hours === '12') {
    hours = '00'
  }

  if (modifier === 'PM') {
    hours = parseInt(hours, 10) + 12
  }

  return Number(hours)
}

const useCalendarConfig = () => {
  const profileSettings = props.settings.profile.attributes.settings || {}
  const dayStart = convertTime12to24(profileSettings.calendar_start_time)
  const dayEnd = convertTime12to24(profileSettings.calendar_end_time)

  // this.queryParams.type ||
  //       this.profileSettings.calendar_default_view ||
  //       'resourceTimelineDay'
  //     // if (this.isMobile) initialView = 'listWeek'
  //     const slotMinTime = this.profileSettings.calendar_start_time
  //     const slotMaxTime = this.profileSettings.calendar_end_time
  //     const offsetWeeks = this.profileSettings.calendar_offset_weeks
  //     const offsetDays = this.profileSettings.calendar_offset_days

  const baseTimelineConfig = {
    type: 'scheduler',
    displayName: 'Timeline',
    showAvatars: true,
    // eventStyle: 'calendar',
    workingTime: {
      fromHour: dayStart,
      toHour: dayEnd,
    },
    features: {
      nonWorkingTime: true,
    },
    columns: [
      {
        type: 'resourceInfo',
        field: 'name',
        text: 'Inspectors',
        width: 175,
        //
        // Custom rendering
        // https://bryntum.com/products/schedulerpro/examples/conflicts/
        //
        // htmlEncode: false,
        // // Renderer that returns a DOM config object, a more performant way than returning a html string, allows
        // // reusing elements as cells are re-rendered
        // renderer: ({ record }) => ({
        //   children: [
        //     // <i> tag with the icon
        //     record.icon
        //       ? {
        //           tag: 'i',
        //           className: `b-fa b-fa-fw ${record.icon}`,
        //           style: 'margin-right: .5em',
        //         }
        //       : null,
        //     // text node with the name
        //     record.name,
        //   ],
        // }),
      },
    ],
  }

  return {
    sidebar: props.config.sidebar,
    mode: 'week',
    features: {
      eventTooltip: {
        // Tooltip configs can be used here
        align: 'l-r', // Align left to right,
        // Custom content
        renderer: (data) => {
          console.log(data, 'abc...')
          return 'I am a tooltip!'
        },
      },
    },
    modes: {
      day: {
        ...baseTimelineConfig,
        stepUnit: 'day',
        displayName: 'Day',
        descriptionRenderer() {
          return DateHelper.format(this.startDate, 'dddd, MMMM Do, YYYY')
        },
        // https://www.bryntum.com/products/scheduler/docs/api/Scheduler/preset/ViewPreset
        viewPreset: {
          base: 'hourAndDay',
          headers: [
            {
              unit: 'hour',
              dateFormat: 'hA',
            },
          ],
        },
      },
      week: {
        ...baseTimelineConfig,
        stepUnit: 'week',
        displayName: 'Week',
        viewPreset: {
          base: 'hourAndDay',
          tickWidth: 45,
          headers: [
            {
              unit: 'day',
              dateFormat: 'ddd MM/DD',
            },
            {
              unit: 'hour',
              dateFormat: 'hA',
            },
          ],
        },
      },
      month: {
        showResourceAvatars: 'last',
        autoRowHeight: true,
      },
      year: false,
    },
  }
}

// const calendar = computed(() => calendarRef.value.instance.value)
const props = withDefaults(defineProps<iSpectoraCalendarProps>(), {
  config: () => ({}),
  settings: () => ({}),
  me: () => ({}),
  resources: () => [],
  events: () => [],
})

const calendarRef = ref(null)
const calendarConfig = ref(useCalendarConfig())

function onAutoRowHeightChanged({ checked }) {
  const calendar = calendarRef.value.instance.value

  calendar.modes.month.autoRowHeight = checked
}

// Called as the resourceFilterFilter's onChange handler
function onResourceFilterFilterChange({ value }) {
  const calendar = calendarRef.value.instance.value
  // A filter with an id replaces any previous filter with that id.
  // Leave any other filters which may be in use in place.
  calendar.widgetMap.resourceFilter.store.filter({
    id: 'resourceFilterFilter',
    filterBy: (r) => r.name.toLowerCase().startsWith(value.toLowerCase()), // a function which returns true to include the record
  })
}

const handleEventClick = (options) => {
  console.log('event clicked:', options)
}

const handleBeforeEventEdit = (options) => {
  console.log('handleBeforeEventEdit', options)
  // By returning false from a listener for an event documented as preventable the action that would otherwise be executed after the event is prevented. These events names are usually prefixed with before.
  // return false
}

const handleBeforeDragMoveEnd = (options) => {
  console.log('handleBeforeDragMoveEnd', options)

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Trigger reschedule confirmation modal and pass it the promise resolver
      // If confirmed, you can resolve. If rejected you reject
      // https://jmp.sh/15dKSBDi
      if (confirm('Are you sure you want to move this event?')) {
        console.log('confirmed...')
        resolve(true)
      } else {
        console.log('rejected...')
        reject()
      }
    }, 3000)
  })
}

const handleDragMoveEnd = (options) => {
  console.log('handleDragMoveEnd', options)
  return true
}
const handleBeforeDragResizeEnd = (options) => {
  console.log('handleBeforeDragResizeEnd', options)
}
const handleBeforeDragCreateEnd = (options) => {
  console.log('handleBeforeDragCreateEnd', options)
}

// beforeDragMoveEnd : async({ eventRecord }) => {
//               const result = await MessageDialog.confirm({
//                   title   : 'Please confirm',
//                   message : 'Is this the start time you wanted?'
//               });

//               // Return true to accept the drop or false to reject it
//               return result === MessageDialog.yesButton;
//           },
//           beforeDragResizeEnd : async({ eventRecord }) => {
//               const result = await MessageDialog.confirm({
//                   title   : 'Please confirm',
//                   message : 'Is this the duration you wanted?'
//               });

//               // Return true to accept the drop or false to reject it
//               return result === MessageDialog.yesButton;
//           },
//           beforeDragCreateEnd : async({ eventRecord }) => {
//               const result = await MessageDialog.confirm({
//                   title   : 'Please confirm',
//                   message : 'Want to create this event?'
//               });

//               // Return true to accept the drop or false to reject it
//               return result === MessageDialog.yesButton;
//           }

onMounted(() => {
  const calendar = calendarRef.value.instance.value

  // calendar.widgetMap.autoRowHeight.on({ change: onAutoRowHeightChanged })
  // calendar.widgetMap.resourceFilterFilter.on({
  //   change: onResourceFilterFilterChange,
  // })
})
</script>

<template>
  <bryntum-calendar
    ref="calendarRef"
    :events="events"
    :resources="resources"
    v-bind="calendarConfig"
    @eventclick="handleEventClick"
    @beforeDragMoveEnd="handleBeforeDragMoveEnd"
    @dragMoveEnd="handleDragMoveEnd"
  />
</template>


Post by Animal »

This is in the context of the Scheduler being owned by a Calendar, and the Calendar's tooltip take over.

renderer is correct. It is missing from the docs, and I'll get that fixed ASAP

We deprecate usage of "templates". Creating a complex HTML string, integrating conditional classes and styles is extremely ugly. You should see some of the legacy "templates" in the system that we are working to eliminate!

Usage of strings containing "<tag>" is really only useful for transfer of a representation of DOM structure over the network.

In reality, the DOM is an object tree, so it is more efficient to represent it as such. Objects can be merged, so a default configuration may be mutated easily. (Consider a default HTML string and your code has to add a class or style to it - you'd have to parse it, snip it up and do horrible string manipulation!). Finally, it's easier to convert it to real DOM.

Newer classes such as https://bryntum.com/products/calendar/docs/api/Calendar/widget/EventTip which is what is in use here use renderer, and that may produce an object based representation known as a `DomConfig: https://bryntum.com/products/calendar/docs/api/Core/helper/DomHelper#typedef-DomConfig

Example:

{
    tag : 'ul",  // May be omitted if you want a DIV

    // Conditionally added classes are much easier by just setting properties.
    // If the value of the property is truthy, the class is set
    className : {
        'b-selected' : this.isSelected(),

        // this.addedCls may be set, this allows you to add it if it exists
        [this.addedCls] : this.addedCls
    },
    style : {
        left : 0
    },
    children : [{
        tag : 'li',
        text : 'Text node'
    }]
}
````

Post by Animal »

There is also titleRenderer which is passed the eventRecord as its sole argument and returns either a simple string (The default is to return eventrecord,name), or a DomConfig


Post by Animal »

I am improving the docs for the EventTip widget. The new version will have this at the top:

Screenshot 2023-03-23 at 07.43.20.png
Screenshot 2023-03-23 at 07.43.20.png (169.69 KiB) Viewed 166 times

And the renderers will be documented like this:

Screenshot 2023-03-23 at 07.43.48.png
Screenshot 2023-03-23 at 07.43.48.png (137.43 KiB) Viewed 166 times

Post by jnarowski »

Thanks for that explanation.

Am I correct in understanding that we cannot replace the entire tooltip in a single function? This might not be an issue, but it sounds like we're constrained to designing the title and body separately using titleRenderer and renderer?


Post by Animal »

Not sure what you mean by "replace"

You can configure the tooltip to do anything and contain anything.


Post Reply