Skip to content

Instantly share code, notes, and snippets.

@ka215
Created March 31, 2021 10:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ka215/72cfc07da6d813842b9113e898260892 to your computer and use it in GitHub Desktop.
Save ka215/72cfc07da6d813842b9113e898260892 to your computer and use it in GitHub Desktop.
Source Codes of jQuery.Timeline CRUD System Sample
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jQuery.Timeline Sample - CRUD System</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ka215/jquery.timeline@main/dist/jquery.timeline.min.css">
<link rel="stylesheet" href="./dist/jqtl-crud.css?1">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
</head>
<body class="d-flex-col">
<nav role="banner">
<div class="d-flex-row">
<h1 id="page-title"><span class="jqtl-icon"></span> CRUD System for jQuery.Timeline</h1>
<label>
<select id="links">
<option value="">Other Samples</option>
<option value="./sample.html" >sample.html</option>
<option value="./sample1.html">sample1.html</option>
<option value="./sample2.html">sample2.html</option>
<option value="./sample3.html">sample3.html</option>
<option value="./sample4.html">sample4.html</option>
<option value="./sample5.html">sample5.html</option>
</select>
</label>
</div>
<div id="controller" class="d-flex-row" role="form">
<div class="d-flex-row">
<div class="d-flex-col">
<label for="change-mode" class="text-caption">Change Mode:</label>
<select id="change-mode">
<option value="read" >Default</option>
<option value="create">Create Event</option>
<option value="update">Update Event</option>
<option value="delete">Delete Event</option>
</select>
</div>
<div id="mode-description">This is the normal readonly mode.</div>
</div>
<div class="d-flex-col">
<label for="import-events" class="text-caption">Import Events:</label>
<!-- form name="import-form" class="btn-upload" -->
<div class="btn-upload">
Import File
<input type="file" id="import-events" accept="application/json,.json">
</div>
</div>
<div class="d-flex-col">
<label for="import-events" class="text-caption">Export Events:</label>
<button type="button" id="export-events" disabled>Export File</button>
</div>
<div class="d-flex-col">
<label for="number-event" class="text-caption">Auto Put Events:</label>
<div>
<input type="number" id="number-event" min="1" max="20" value="10" class="text-small">
<button type="button" id="put-events">Put Events</button>
</div>
</div>
<div class="d-flex-col">
<label for="change-type" class="text-caption">View Type:</label>
<select id="change-type">
<option value="bar" >Bar</option>
<option value="point">Point</option>
<option value="mixed">Mixed</option>
</select>
</div>
<div class="d-flex-col">
<button type="button" id="reload" class="no-label">Reload</button>
</div>
</div>
</nav>
<main role="main">
<div class="occupy-advance"></div>
<div id="crud-timeline"></div>
</main>
<footer role="contentinfo">
<p>&copy; 2020 Monaural Sound <a href="https://ka2.org/">ka2.org</a>, Powered by MAGIC METHODS</p>
</footer>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/gh/ka215/jquery.timeline@main/dist/jquery.timeline.min.js"></script>
<script src="./dist/jqtl-crud.js?1"></script>
</body>
</html>
@ka215
Copy link
Author

ka215 commented Mar 31, 2021

jqtl-crud.css:

html {
  width: 100%;
  height: auto;
}
body {
  position: relative;
  margin: 0;
  padding: 0;
  width: calc(100% - 2em);
  overflow-x: hidden;
  overflow-y: scroll;
  font-family: Arial, Helvetica, sans-serif;
  box-sizing: border-box;
}
.d-flex-row {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: space-between;
  align-content: center;
  align-items: center;
}
.d-flex-col {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  justify-content: space-between;
  align-content: center;
  align-items: start;
}
h1, h2, h3, h4, h5, h6 {
  margin: 0.25em 0;
}
label {
  font-size: 15px;
  color: #333333;
}
input:not([type="file"]):not(.upload-value), select, textarea, button {
  padding: 0.3em 0.8em;
  font-size: 15px;
  border: solid 1px #999;
  border-radius: 4px;
  background-color: #fcfcfc;
  box-sizing: border-box;
  transition: all 0.1s linear;
}
input[readonly], select[readonly], textarea[readonly] {
  color: #777;
  border: solid 1px #b0b0b0;
  background-color: #f0f0f0;
}
input:not([type="file"]):not(.upload-value):focus, textarea:focus, select:focus {
  box-shadow: 0 0 5px 0 rgba(0,115,168, 0.3);
  border: solid 1px #0073a8;
  background-color: #ffffff;
  outline: 0;
}
input[readonly]:focus, select[readonly]:focus, textarea[readonly]:focus {
  box-shadow: unset;
  border: solid 1px #b0b0b0;
  background-color: #f0f0f0;
}
button {
  padding: 0.3em 0.8em;
  font-size: 15px;
  border: solid 1px #0073a8;
  border-radius: 4px;
  background-color: #008db7;
  color: #ffffff;
  box-sizing: border-box;
  transition: all 0.2s linear;
}
button[disabled] {
  border: solid 1px #d0d0d0;
  background-color: #e8e8e8;
  color: #717171;
}
button:not([disabled]):hover, button:not([disabled]):focus {
  box-shadow: 0 0 3px 0 rgba(0,175,204, 0.3);
  border: solid 1px #008db7;
  background-color: #00afcc;
  outline: 0;
}
.btn-upload {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border: solid 1px #0073a8;
  border-radius: 4px;
  background: #008db7;
  color: #ffffff;
  text-align: center;
  padding: 0.3em 0.8em;
  cursor: pointer;
  box-sizing: border-box;
  transition: all 0.2s linear;
}
.btn-upload:hover, .btn-upload:focus {
  box-shadow: 0 0 3px 0 rgba(0,175,204, 0.3);
  border: solid 1px #008db7;
  background-color: #00afcc;
  outline: 0;
}
.btn-upload input[type="file"] {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;    
  cursor: pointer;
  opacity: 0;
}
.upload-value {
  position: absolute;
  top: -9999px;
  left: -9999px;
  display: none;
  visibility: hidden;
}
nav {
  display: block;
  margin: 0;
  padding: 0 1em 1em;
  width: 100%;
  border-bottom: solid 1px #999;
}
main {
  position: relative;
  display: block;
  margin: 0 auto;
  padding: 1em;
  width: 100%;
  height: calc(100vh - 206px);
}
footer {
  display: block;
  margin: 0;
  padding: 0.5em 1em;
  width: 100%;
  border-top: solid 1px #999;
  text-align: center;
}
footer > * {
  font-size: 86%;
  font-family: 'Lucida Console', Monaco, monospace;
  letter-spacing: -0.5px;
  color: #717477;
}
footer a, footer a:hover, footer a:active, footer a:visited { text-decoration: none; }
.mr-h { margin-right: 0.5em; }
.text-caption {
  margin-bottom: 2px;
  font-size: smaller;
  color: #717477;
}
.text-small  { width: 4em; }
.text-medium { width: 8em; }
.text-normal { width: 12em; }
.text-large  { width: 16em; }
.text-xlarge { width: 20em; }
.text-full   { width: 100%; }
.no-label { margin-top: 16px; }
#page-title {
  padding-bottom: 0.5em;
  text-align: center;
  color: #505050;
}
#controller { font-size: 15px; }
#mode-description {
  margin: auto 0;
  padding: 0 0.5em;
  width: 240px;
  height: 48px;
  color: #666;
  font-size: 14px;
}
.occupy-advance {
  position: absolute;
  top: 0;
  left: 0;
  height: 672px;
  width: 100%;
  z-index: -1;
}
.occupy-advance::before {
  position: absolute;
  content: 'Occupy the display area in advance.';
  font-size: large;
  font-weight: 500;
  color: #CCC;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.jqtl-side-index-item > * {
  padding-left: 1em !important;
  padding-right: 1em !important;
}
/* Dialog Styles */
#backdrop-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: calc(100% + 2em);
  height: calc(100% + 2em);
  margin: -1em;
  padding: 0;
  background-color: rgba(51,51,51,0.4);
  z-index: 9999;
}
#dialog {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  top: 50%;
  left: 50%;
  width: 60%;
  height: 60%;
  background-color: #FFF;
  border-radius: 6px;
  z-index: 10000;
  transform: translate(-50%,-50%);
  box-shadow: 0 0 10px 10px rgba(51,51,51,0.1);
  opacity: 0;
}
#dialog-header {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  margin: 0;
  padding: 4px 6px;
  border-radius: 6px 6px 0 0;
  border-bottom: dotted 1px #E8E8E8;
  background-color: #F4F4F4;
  z-index: 10001;
}
.dialog-title {
  width: calc(100% - 50px);
  line-height: 42px;
  margin: 0;
  padding-left: 1em;
  padding-right: 1em;
  vertical-align: center;
  font-weight: 500;
  color: #515151;
}
#dialog-dismiss {
  text-align: center;
  vertical-align: middle;
  width: 90px;
}
.dismiss-icon {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='53.7' height='53.7' viewBox='0 0 53.7 53.7'><path opacity='.6' fill='%23666E6E' d='M35.6 34.4L28 26.8l7.6-7.6c.2-.2.2-.5 0-.7l-.5-.5c-.2-.2-.5-.2-.7 0l-7.6 7.6-7.5-7.6c-.2-.2-.5-.2-.7 0l-.6.6c-.2.2-.2.5 0 .7l7.6 7.6-7.6 7.5c-.2.2-.2.5 0 .7l.5.5c.2.2.5.2.7 0l7.6-7.6 7.6 7.6c.2.2.5.2.7 0l.5-.5c.2-.2.2-.5 0-.7z'/></svg>");
  background-repeat: no-repeat;
  background-size: contain;
  position: absolute;
  cursor: pointer;
  width: 42px;
  height: 42px;
}
#dialog-body {
  margin: 0;
  padding: 1em;
  height: 100%;
  background-color: #FFF;
  z-index: 10001;
}
#notice-message {
  margin: 0;
  margin-bottom: 1em;
  color: #515151;
}
#notice-message.warn { color: #e8383d; }
.field-item {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: start;
  align-content: center;
  align-items: center;
  margin-bottom: 0.5em;
}
.field-item > label {
  width: calc(100% / 4);
  font-weight: 500;
}
.required {
  position: relative;
  display: inline-block;
  width: 1em;
  height: 1em;
}
.required::after {
  position: absolute;
  content: '*';
  color: #e8383d;
  top: 0;
  left: 2px;
}
.field-item > label + * {
  max-width: calc((100% / 4) * 3);
}
#dialog-footer {
  display: flex;
  flex-direction: row-reverse;
  justify-content: start;
  align-content: start;
  margin: 0;
  padding: 10px 6px;
  border-radius: 0 0 6px 6px;
  border-top: dotted 1px #E8E8E8;
  background-color: #FFF;
  z-index: 10001;
}
#dialog-footer > button {
  margin: auto 0.5em;
}
#btn-dialog-close {
  border: solid 1px #0073a8;
  background-color: #ffffff;
  color: #0073a8;
}
#btn-dialog-close:hover, #btn-dialog-close:focus {
  box-shadow: 0 0 3px 0 rgba(0,175,204, 0.3);
  border: solid 1px #008db7;
  background-color: #00afcc;
  color: #ffffff;
}

@ka215
Copy link
Author

ka215 commented Mar 31, 2021

jqtl-crud.js:

const init = () => {
    const JQTL_VER   = $.fn.Timeline ? $.fn.Timeline.Constructor.VERSION : '2.1.x',
          JQUERY_VER = $.fn.jquery || null

    if ( ! JQUERY_VER ) {
        console.warn( 'jQuery is not loaded or readied.' )
        return false
    } else if ( /^(\d{1}\.\d{1,2})\.\d{1,2}$/.test( JQUERY_VER ) ) {
        // Check jQuery version
        let _version = JQUERY_VER.match(/^(\d{1})\.(\d{1,2})\.(\d{1,2})$/)

        if ( parseInt(_version[1]) == 1 && parseInt(_version[2]) < 9 ) {
            console.warn( `This jQuery version ${_version[0]} is outdated.` )
            return false
        }
    }

    const initTimeline = () => {
        tlElem.Timeline( tlOpts )
        .Timeline('initialized', function() {
            console.log( arguments[2].message )
            window.addEventListener( 'resize', throttle, {passive: true} )
            window.addEventListener( 'scroll', throttle, {passive: true} )
            tlOpts = arguments[1]
            $('#change-type').get(0).options.forEach(function(elm,idx){
                if ( tlType === elm.value ) {
                    $('#change-type').get(0).options.selectedIndex = idx
                }
            })
        }, { message: 'Initialized Timeline!' } )
        .Timeline('openEvent', function(targetEvent, viewerContents) {
            //console.log( 'openEvent:', `Active Mode: ${mode}`, targetEvent )
            let nodes = {
                    label: $('<h3></h3>', { class: 'dialog-title' }),
                    content: $('<form></form>', { id: `${mode}-event-form` }),
                    button: $('<button></button>', { type: 'button', id: `btn-${mode}-event` }),
                },
                items = []

            switch ( mode ) {
                case 'update':
                    // Generate form for new event updating
                    items.push( '<p id="notice-message">Please enter at least the items marked with <span class="required"></span>.</p>' )
                    items.push( makeField( 'Event Id', 'text', 'eventId', targetEvent.eventId, 'text-medium', { readonly: true } ) )
                    items.push( makeField( 'Start Datetime', 'text', 'start', getDateString( targetEvent.start ), null, { required: true } ) )
                    items.push( makeField( 'End Datetime', 'text', 'end', targetEvent.end ? getDateString( targetEvent.end ) : '', null ) )
                    items.push( makeField( 'Row', 'number', 'row', targetEvent.row, 'text-small', { required: true } ) )
                    items.push( makeField( 'Event Label', 'text', 'label', targetEvent.label, 'text-full', { required: true } ) )
                    items.push( makeField( 'Event Content', 'textarea', 'content', targetEvent.content, 'text-full', { rows: 5, required: true } ) )
                    items.push( makeField( 'Text Color', 'text', 'color', targetEvent.color, 'text-medium', { placeholder: '#343A40' } ) )
                    items.push( makeField( 'Background Color', 'text', 'bgColor', targetEvent.bgColor, 'text-medium', { placeholder: '#E7E7E7' } ) )
                    items.push( makeField( 'Border Color', 'text', 'bdColor', targetEvent.bdColor, 'text-medium', { placeholder: '#6C757D' } ) )
                    $(nodes.content).append( ...items )
                    $(nodes.label).text('Update Event Parameters')
                    $(nodes.button).text('Update')
                    openDialog( nodes )
                    break
                case 'delete':
                    // Generate form for new event deletion
                    items.push( '<p id="notice-message">Are you sure you want to delete the following events?</p>' )
                    items.push( makeField( 'Event Id', 'text', 'eventId', targetEvent.eventId, 'text-medium', { readonly: true } ) )
                    items.push( makeField( 'Start Datetime', 'text', 'start', getDateString( targetEvent.start ), null, { readonly: true } ) )
                    items.push( makeField( 'End Datetime', 'text', 'end', targetEvent.end ? getDateString( targetEvent.end ) : '', null, { readonly: true } ) )
                    items.push( makeField( 'Row', 'number', 'row', targetEvent.row, 'text-small', { readonly: true } ) )
                    items.push( makeField( 'Event Label', 'text', 'label', targetEvent.label, 'text-full', { readonly: true } ) )
                    items.push( makeField( 'Event Content', 'textarea', 'content', targetEvent.content, 'text-full', { rows: 5, readonly: true } ) )
                    items.push( makeField( 'Text Color', 'text', 'color', targetEvent.color, 'text-medium', { readonly: true } ) )
                    items.push( makeField( 'Background Color', 'text', 'bgColor', targetEvent.bgColor, 'text-medium', { readonly: true } ) )
                    items.push( makeField( 'Border Color', 'text', 'bdColor', targetEvent.bdColor, 'text-medium', { readonly: true } ) )
                    $(nodes.content).append( ...items )
                    $(nodes.label).text('Delete Event')
                    $(nodes.button).text('Delete')
                    openDialog( nodes )
                    break
                default:
                    // Define an event handler for getting event nodes on the timeline
                    console.log( 'The event node with eventID: '+ targetEvent.eventId +' was clicked.', targetEvent, viewerContents )
                    openDialog( viewerContents )
                    break
            }
        })
    }

    const handleDblclick = function(evt) {
        if ( 'create' === mode && evt.target.closest('.jqtl-events') && evt.target.classList.contains('jqtl-events') ) {
            let targetPos   = { x: evt.offsetX, y: evt.offsetY },
                currentStDt = new Date(tlOpts.startDatetime).getTime(),
                positionRow = Math.ceil(targetPos.y / tlOpts.rowHeight),
                perPxDt, positionDt, newStartDt

            switch(true) {
                case /^years?$/i.test(tlOpts.scale):
                    perPxDt = (365*24*60*60*1000) / tlOpts.minGridSize
                    break
                case /^months?$/i.test(tlOpts.scale):
                    perPxDt = ((365/12)*24*60*60*1000) / tlOpts.minGridSize
                    break
                case /^weeks?$/i.test(tlOpts.scale):
                    perPxDt = (7*24*60*60*1000) / tlOpts.minGridSize
                    break
                case /^(week|)days?$/i.test(tlOpts.scale):
                    perPxDt = (24*60*60*1000) / tlOpts.minGridSize
                    break
                case /^hours?$/i.test(tlOpts.scale):
                    perPxDt = (60*60*1000) / tlOpts.minGridSize
                    break
                case /^minutes?$/i.test(tlOpts.scale):
                    perPxDt = (60*1000) / tlOpts.minGridSize
                    break
                case /^seconds?$/i.test(tlOpts.scale):
                    perPxDt = 1000 / tlOpts.minGridSize
                    break
            }
            positionDt = new Date(currentStDt + (perPxDt * targetPos.x))
            newStartDt = `${positionDt.getFullYear()}/${positionDt.getMonth()+1}/${positionDt.getDate()} ${positionDt.getHours()}:${positionDt.getMinutes()}:${positionDt.getSeconds()}`

            // Generate form for new event creation
            let nodes = {
                    label: $('<h3></h3>', { class: 'dialog-title' }),
                    content: $('<form></form>', { id: 'add-event-form' }),
                    button: $('<button></button>', { type: 'button', id: 'btn-add-event' }),
                },
                items = []

            items.push( '<p id="notice-message">Please enter at least the items marked with <span class="required"></span>.</p>' )
            items.push( makeField( 'Event Id', 'text', 'eventId', '', 'text-medium' ) )
            items.push( makeField( 'Start Datetime', 'text', 'start', newStartDt, null, { required: true } ) )
            items.push( makeField( 'End Datetime', 'text', 'end', '' ) )
            items.push( makeField( 'Row', 'number', 'row', positionRow, 'text-small', { required: true } ) )
            items.push( makeField( 'Event Label', 'text', 'label', '', 'text-full', { required: true } ) )
            items.push( makeField( 'Event Content', 'textarea', 'content', '', 'text-full', { rows: 5, required: true } ) )
            items.push( makeField( 'Text Color', 'text', 'color', '', 'text-medium', { placeholder: '#343A40' } ) )
            items.push( makeField( 'Background Color', 'text', 'bgColor', '', 'text-medium', { placeholder: '#E7E7E7' } ) )
            items.push( makeField( 'Border Color', 'text', 'bdColor', '', 'text-medium', { placeholder: '#6C757D' } ) )
            $(nodes.content).append( ...items )
            $(nodes.label).text('Add New Event')
            $(nodes.button).text('Add')
            openDialog( nodes )
        }
    }

    const watchElement = function( selector ) {
        let targetElm = $(selector).get(0),
            observer  = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    if ( mutation.type === 'childList' && mutation.target.className === 'jqtl-events' ) {
                    console.log(mutation)
                        $('#export-events').prop('disabled', mutation.addedNodes.length == 0)
                    }
                })
            })

        // Start watching
        observer.observe( targetElm, { childList: true, subtree: true } )
    }

    $('#change-mode').on('change', function(e){
      let modeDesc = '',
          eventContainer = tlElem.find('.jqtl-events').get(0)

      mode = e.target.value
      eventContainer.style.cursor = 'inherit'
      eventContainer.removeEventListener('dblclick', handleDblclick, false)

      switch( mode ) {
        case 'create':
          modeDesc = 'In this mode, you can add an event by double-clicking on the timeline container.'
          eventContainer.style.cursor = 'crosshair'
          eventContainer.addEventListener('dblclick', handleDblclick, false)
          break
        case 'update':
          modeDesc = 'In this mode, you can update the parameters of the clicked event.'
          break
        case 'delete':
          modeDesc = 'In this mode, you can delete the clicked event.'
          break
        default:
          modeDesc = 'This is the normal readonly mode.'
          break
      }
      $('#mode-description').text( modeDesc )
    })

    $('#put-events').on('click', function(){
      let _number = parseInt( $('#number-event')[0].value, 10 ),
          newEvt = createEvents( _number ),
          evtStr = _number > 1 ? 'events' : 'event'

      tlElem.Timeline( 'addEvent', newEvt, function(elm, opts, usrdata, addedEvents ) {
        // console.log( elm, opts, usrdata, addedEvents );
        notification( usrdata.title, usrdata.message )
      }, { title: 'Put Event(s)', message: `Put new ${_number} ${evtStr}.` } )
    });

    $(document).on('click', '#btn-add-event', function(e) {
        let fd        = new FormData( $('#add-event-form').get(0) ),
            evtParams = {},
            errors    = [],
            newId, newRow, newDt

        fd.forEach(function(val, key) {
            if ( val ) {
                switch( true ) {
                    case /^eventID$/i.test(key):
                        newId = parseInt(val, 10)
                        if ( eventExists( newId ) ) {
                            errors.push( 'This event id already exists.' )
                        } else {
                            evtParams[key] = newId
                        }
                        break
                    case /^row$/i.test(key):
                        newRow = parseInt(val, 10)
                        if ( newRow < 0 || maxList < newRow ) {
                            errors.push( 'There is set invalid row number.' )
                        } else {
                            evtParams[key] = newRow
                        }
                        break
                    case /^(start|end)$/i.test(key):
                        newDt = new Date(val).getTime()
                        evtParams[key] = newDt
                        break
                    default:
                        evtParams[key] = val
                        break
                }
            }
        })
        if ( ! evtParams.row || ! evtParams.start || ! evtParams.label || ! evtParams.content ) {
            errors.push( 'The required item is empty.' )
        }
        if ( errors.length == 0 ) {
            tlElem.Timeline( 'addEvent', [evtParams], function(elm, opts, usrdata, addedEvents) {
                console.log( 'Added new event', elm, opts, usrdata, addedEvents );
                closeDialog()
            }, {} )
        } else {
            $('#notice-message').addClass('warn').html( errors.join( '<br>' ) )
        }
    })

    $(document).on('click', '#btn-update-event', function(e) {
        let fd        = new FormData( $('#update-event-form').get(0) ),
            evtParams = {},
            errors    = [],
            modId, modRow, modDt

        fd.forEach(function(val, key) {
            if ( val ) {
                switch( true ) {
                    case /^eventID$/i.test(key):
                        modId = parseInt(val, 10)
                        if ( eventExists( modId ) ) {
                            evtParams[key] = modId
                        } else {
                            errors.push( 'That event ID is invalid.' )
                        }
                        break
                    case /^row$/i.test(key):
                        modRow = parseInt(val, 10)
                        if ( modRow < 0 || maxList < modRow ) {
                            errors.push( 'There is set invalid row number.' )
                        } else {
                            evtParams[key] = modRow
                        }
                        break
                    case /^(start|end)$/i.test(key):
                        modDt = new Date(val).getTime()
                        evtParams[key] = modDt
                        break
                    default:
                        evtParams[key] = val
                        break
                }
            }
        })
        if ( ! evtParams.eventId || ! evtParams.row || ! evtParams.start || ! evtParams.label || ! evtParams.content ) {
            errors.push( 'The required item is empty.' )
        }
        if ( errors.length == 0 ) {
            tlElem.Timeline( 'updateEvent', [evtParams], function(elm, opts, usrdata, updatedEvents) {
                console.log( 'Updated an event', elm, opts, usrdata, updatedEvents )
                closeDialog()
            }, {} )
        } else {
            $('#notice-message').addClass('warn').html( errors.join( '<br>' ) )
        }
    })

    $(document).on('click', '#btn-delete-event', function(e) {
        let fd      = new FormData( $('#delete-event-form').get(0) ),
            evtIds  = [],
            errors  = [],
            remeins = $('.jqtl-events').children().length,
            delId

        fd.forEach(function(val, key) {
            if ( val ) {
                switch( true ) {
                    case /^eventID$/i.test(key):
                        delId = parseInt(val, 10)
                        if ( eventExists( delId ) ) {
                            evtIds.push( delId )
                        } else {
                            errors.push( 'That event ID is invalid.' )
                        }
                        break
                }
            }
        })
        if ( evtIds.length > 0 && errors.length == 0 ) {
            tlElem.Timeline( 'removeEvent', evtIds, function(elm, opts, usrdata, removedEvents ) {
                console.log( 'Deleted an event', elm, opts, usrdata, removedEvents )
                if ( remeins == 1 && $('.jqtl-events').children().length == remeins ) {
                    // For Bug?
                    $('.jqtl-events').empty()
                }
                closeDialog()
            }, {} )
        } else {
            $('#notice-message').addClass('warn').html( errors.join( '<br>' ) )
        }
    })

    $('#import-events').on('change', function(e){
        let selector = `${tlElem.get(0).nodeName.toLowerCase()}#${tlElem.get(0).id}`,
            file     = e.target.files[0],
            reader   = new FileReader(),
            data     = null

        reader.readAsText( file )
        reader.addEventListener( 'load', function() {
            data = JSON.parse( reader.result )
            if ( data) {
                sessionStorage.setItem( selector, JSON.stringify( data ) )
                tlElem.Timeline( 'reload', { reloadCacheKeep: true }, function(){
                    notification( arguments[2].label, arguments[2].message )
                }, { label: 'Imported Events', message: `Completely imported ${data.length} ${data.length > 1 ? 'events' : 'event'}.` } )
            }
        }, false )
    })

    $('#export-events').on('click', function(){
        if ( !( tlElem.get(0) instanceof Element ) ) {
            return false
        }
        let selector = `${tlElem.get(0).nodeName.toLowerCase()}#${tlElem.get(0).id}`,
            data     = JSON.parse( sessionStorage.getItem( selector ) ),
            blob     = new Blob([JSON.stringify(data, null, "\t")], {type: 'application\/json'}),
            url      = URL.createObjectURL(blob),
            link     = document.createElement('a')

        if ( data.length > 0 ) {
            link.href = url
            link.download = 'events.json'
            link.click()
            URL.revokeObjectURL(url)
        } else {
            notification( 'Export Error', 'There is no event data to export.' )
        }
    })

    $('#change-type').on('change', function(e){
        tlType = e.target.value
        tlElem.Timeline( 'reload', { type: tlType, reloadCacheKeep: true }, function( elm ){
            console.log( arguments[2].message )
            $(window).scrollTop(0)
        }, { message: 'Completely Change Type of Timeline' })
    })

    $('#reload').on('click', function(){
        // It is necessary to perform a reload with the cache destroyed once in order to 
        // completely initialize the timeline, and then enable the new cache.
        let selector = `${tlElem.get(0).nodeName.toLowerCase()}#${tlElem.get(0).id}`

        sessionStorage.removeItem( selector )
        tlElem.Timeline( 'reload', tlOpts, function() {
            notification( arguments[2].label, arguments[2].message )
            $(window).scrollTop(0)
            $('#export-events').prop('disabled', true)
        }, { label: 'Timeline Reload', message: 'Completely reloaded this Timeline.' } )
    })


    $('#links').on('change', function(e){
        window.location.href = e.target.value || './sample-crud.html'
    })

    /**
     * Initial fires
     */
    let tlElem     = $('#crud-timeline'),// Variable as container to cache timeline instance
        now        = new Date(),
        listItems  = [],
        incr       = 1,
        pageWidth  = $(window).width(),
        tlType     = 'mixed',
        maxList    = 10,// display max rows of sidebar
        begin      = now.setDate(now.getDate() - ((pageWidth - (28 + 76)) / (48 * 2))),
        end        = undefined

    now.setMonth(now.getMonth() + 2)
    end = now.setDate(now.getDate() - 1);
    for ( incr = 1; incr <= maxList; incr++ ) {
        listItems.push( '<span>Row ' + incr + '</span>' );
    }

    // Defined as global variables
    window.mode    = 'default'
    window.ticking = false
    window.tlOpts  = {
        type: tlType,
        startDatetime: getModDateString( begin, { h: 0, min: 0, sec: 0 } ),
        endDatetime: getModDateString( end, { h: 23, min: 59, sec: 59 } ),
        scale: 'day',
        minGridSize: 48,
        headline: {
            display: true,
            title:   '<span></span>',
            range:   true,
            local:   'en-US',
            format:  {
                timeZone: 'UTC',
                hour12: false,
            }
        },
        sidebar: {
            list: listItems,
            sticky: true,
        },
        rows: listItems.length,
        ruler: {
            top: {
                lines: [ 'year', 'month', 'day' ],
                format: {
                    timeZone: 'UTC',
                    year: 'numeric',
                    month: 'long',
                    day: 'numeric',
                }
            },
            bottom: {
                lines: [ 'day' ],
                format: {
                    timeZone: 'UTC',
                    day: 'numeric',
                }
            }
        },
        hideScrollbar: true,
        zoom: true,
        debug: true,
    }

    // Start watching an instance of the Timeline
    watchElement( '#crud-timeline' )
    initTimeline()
    console.log( tlOpts )

}

/**
 * Various helper functions
 */
function throttle() {
    if ( ! ticking ) {
        requestAnimationFrame(() => {
            ticking = false
        })
        ticking = true
    }
}

function getDateArray( date ) {
    // Helper to get each elements of Date object as an array
    let _dt = date instanceof Date ? date : new Date( date )

        return [ _dt.getFullYear(), _dt.getMonth(), _dt.getDate(), _dt.getHours(), _dt.getMinutes(), _dt.getSeconds(), _dt.getMilliseconds() ]
}

function getDateString( date ) {
    // Helper to get Date object as a string of "Y-m-d H:i:s" format
    let _dt = getDateArray( date )

    //return _dt[0] +'-'+ (_dt[1] + 1) +'-'+ _dt[2] +' '+ _dt[3] +':'+ _dt[4] +':'+ _dt[5]
    return `${_dt[0]}-${_dt[1] + 1}-${_dt[2]} ${_dt[3]}:${_dt[4]}:${_dt[5]}`
}

function getModDateString( date, mods ) {
    // Helper to get the date string after updating the date with the specified amount time.
    let baseDt = date instanceof Date ? new Date( date.getTime() ) : new Date( date ),
        getRandomInt = function( min, max ) {
            min = Math.ceil( min )
            max = Math.floor( max )
            return Math.floor( Math.random() * (max - min) ) + min
        },
        found

    if ( mods && typeof mods === 'object' ) {
        for ( let _key in mods ) {
            found = null
            switch( true ) {
                case /^y(|ears?)$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setFullYear( baseDt.getFullYear() - Number( found[2] ) )
                        } else {
                            baseDt.setFullYear( baseDt.getFullYear() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setFullYear( getRandomInt( 1, new Date().getFullYear() ) )
                    } else {
                        baseDt.setFullYear( Number( mods[_key] ) )
                    }
                    break
                case /^mon(|ths?)$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setMonth( baseDt.getMonth() - Number( found[2] ) )
                        } else {
                            baseDt.setMonth( baseDt.getMonth() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setMonth( getRandomInt( 0, 11 ) )
                    } else {
                        baseDt.setMonth( Number( mods[_key] ) == 12 ? 11 : Number( mods[_key] ) )
                    }
                    break
                case /^d(|ays?)$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setDate( baseDt.getDate() - Number( found[2] ) )
                        } else {
                            baseDt.setDate( baseDt.getDate() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setDate( getRandomInt( 1, 31 ) )
                    } else {
                        baseDt.setDate( Number( mods[_key] ) )
                    }
                    break
                case /^h(|ours?)$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setHours( baseDt.getHours() - Number( found[2] ) )
                        } else {
                            baseDt.setHours( baseDt.getHours() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setHours( getRandomInt( 0, 23 ) )
                    } else {
                        baseDt.setHours( Number( mods[_key] ) )
                    }
                    break
                case /^min(|utes?)$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setMinutes( baseDt.getMinutes() - Number( found[2] ) )
                        } else {
                            baseDt.setMinutes( baseDt.getMinutes() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setMinutes( getRandomInt( 0, 59 ) )
                    } else {
                        baseDt.setMinutes( Number( mods[_key] ) )
                    }
                    break
                case /^s(|(ec|onds?))$/i.test( _key ):
                    if ( typeof mods[_key] === 'string' && /^(-|\+)\d+$/i.test( mods[_key] ) ) {
                        found = mods[_key].match( /^(-|\+)(\d+)$/ )
                        if ( found[1] === '-' ) {
                            baseDt.setSeconds( baseDt.getSeconds() - Number( found[2] ) )
                        } else {
                            baseDt.setSeconds( baseDt.getSeconds() + Number( found[2] ) )
                        }
                    } else if ( 'rand' === mods[_key] ) {
                        baseDt.setSeconds( getRandomInt( 0, 59 ) )
                    } else {
                        baseDt.setSeconds( Number( mods[_key] ) )
                    }
                    break
                default:
                    break
            }
        }
    }
    return getDateString( baseDt );
}

function createEvents( num ) {
    // Helper to randomly generate a specified number of events
    let nowDt    = new Date( tlOpts.startDatetime ),
        _evts    = [],
        _max     = num || 1,
        _startId = lastEventId() + 1,
        _startDt, _endDt, _row, i

    for ( i = 0; i < _max; i++ ) {
        _startDt = getDateString( nowDt.setDate(nowDt.getDate() + (Math.floor(Math.random() * 10) + 1)) )
        _endDt   = getDateString( nowDt.getTime() + ((Math.floor(Math.random() * 7) + 1) * 24 * 60 * 60 * 1000) )
        _row     = Math.floor(Math.random() * tlOpts.rows) + 1
        _evts.push( {start: _startDt, end: _endDt, row: _row, label: 'Created new event (' + (_startId + i) + ')', content: 'This is an event added by the addEvent method.' } )
    }
    return _evts
}

function openDialog( nodes, dialogWidth='60%', dialogHeight='60%' ) {
    let $backdrop = $('<div></div>', { id: 'backdrop-overlay' }),
        $dialog   = $('<div></div>', { id: 'dialog' }),
        $headline = $('<div></div>', { id: 'dialog-header' }),
        $body     = $('<div></div>', { id: 'dialog-body' }),
        $footer   = $('<div></div>', { id: 'dialog-footer' }),
        $dismiss  = $('<div></div>', { id: 'dialog-dismiss' }),
        $close    = $('<button></button>', { id: 'btn-dialog-close' })

    if ( $(document).find('#backdrop-overlay').length == 0 ) {
        $backdrop.on( 'click', function() {
            closeDialog()
        });
        $('body').append( $backdrop )
    } else {
        $(document).find('#backdrop-overlay').css({ display: 'block', visibility: 'visible' })
    }
    $dialog.css({ width: dialogWidth, height: dialogHeight })
    $dismiss.append( '<span class="dismiss-icon"></span>' )
    $(document).on('click', '.dismiss-icon', function() {
        closeDialog()
    })
    $headline.append( nodes.label, $dismiss )
    $body.append( nodes.content )
    $close.text('Close')
    $footer.append( nodes.button, $close )
    $close.on('click', function() {
        closeDialog()
    })
    $dialog.append( $headline, $body, $footer )
    $('body').append( $dialog )
    $dialog.animate({ opacity: 1 }, 300)
}

function notification( title, message ) {
    let nodes = {
            label: `<h3 class="dialog-title">${title}</h3>`,
            content: `<div style="text-align:center"><p id="notice-message">${message}</p></div>`,
        }

    openDialog( nodes, '450', '170' )
}

function closeDialog() {
    $('#dialog').animate({ opacity: 0 }, 150, 'linear', function() {
        $(this).remove()
        $(document).find('#backdrop-overlay').css({ display: 'none', visibility: 'hidden' })
    })
}

function eventExists( eventId ) {
    let $evtElms = $(document).find('.jqtl-event-node'),
        evtIds   = []

    if ( $evtElms.length > 0 ) {
        $evtElms.each(function(idx, elm) {
            evtIds.push( parseInt( elm.id.replace('evt-', ''), 10 ) )
        })
        return $.inArray( eventId, evtIds ) !== -1
    } else {
        return false
    }
}

function lastEventId() {
    let $evtElms = $(document).find('.jqtl-event-node'),
        evtIds   = []

    if ( $evtElms.length > 0 ) {
        $evtElms.each(function(idx, elm) {
            evtIds.push( parseInt( elm.id.replace('evt-', ''), 10 ) )
        })
        return Math.max( ...evtIds )
    } else {
        return 0
    }
}

function makeField( label, type, name, defval = '', classes = undefined, atts = undefined ) {
    let $item  = $('<div></div>', { class: 'field-item' }),
        $label = $('<label></label>', { for: name }),
        $field

    switch(type) {
        case 'textarea':
            $field = $('<textarea></textarea>', { id: name, name: name })
            $field.html( defval )
            break
        default:
            $field = $('<input />', { type: type, id: name, name: name, value: defval })
            break
    }
    if ( classes ) {
        $field.attr( 'class', classes )
    }
    if ( atts ) {
        for ( let _k in atts ) {
            $field.attr( _k, atts[_k] )
            if ( 'required' === _k && atts[_k] === true ) {
                $label.append( '<span class="required"></span>' )
            }
        }
    }
    if ( 'hidden' === type ) {
        return $field
    } else {
        $label.prepend( `<span>${label}</span>` )
        $item.append( $label, $field )
        return $item
    }
}

/*
 * Dispatcher
 */
if ( document.readyState === 'complete' || ( document.readyState !== 'loading' && ! document.documentElement.doScroll ) ) {
    init()
} else
if ( document.addEventListener ) {
    document.addEventListener( 'DOMContentLoaded', init, false )
} else {
    window.onload = init
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment