src/timeline.esdoc.js
/*!
* Typedef for jQuery Timeline's ESDoc
* @version: 2.0.0
*/
/** @type {string} [NAME="Timeline"] */
/** @type {string} VERSION */
/** @type {string} DATA_KEY */
/** @type {string} EVENT_KEY */
/** @type {string} PREFIX */
/** @type {string} LOADING_MESSAGE */
/** @type {number} MIN_POINTER_SIZE */
/** @type {string} DATA_API_KEY */
/** @type {Object} JQUERY_NO_CONFLICT */
/**
* In principle, this option conforms to the specification of options in "Date.prototype.toLocaleString()".
* However, there includes some extensions of this plugin original.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
*
* @typedef {Object} LocaleOptions
* @property {boolean} [hour12=false] - Whether to use 12-hour time (as opposed to 24-hour time). Possible values are true and false.
* @property {string} [localeMatcher] -
* @property {string} [timeZone] -
* @property {string} [hourCycle] -
* @property {string} [formatMatcher] -
* @property {string} [weekday] - The representation of the weekday. Possible values are "narrow", "short", "long".
* @property {string} [era] - The representation of the era. Possible values are "narrow", "short", "long".
* @property {string} [year] - The representation of the year. Possible values are "numeric", "2-digit".
* @property {string} [month] - The representation of the month. Possible values are "numeric", "2-digit", "narrow", "short", "long".
* @property {string} [day] - The representation of the day. Possible values are "numeric", "2-digit".
* @property {string} [hour] - The representation of the hour. Possible values are "numeric", "2-digit".
* @property {string} [minute] - The representation of the minute. Possible values are "numeric", "2-digit".
* @property {string} [second] - The representation of the second. Possible values are "numeric", "2-digit".
* @property {string} [timeZoneName] - The representation of the time zone name. Possible values are "short", "long".
*/
/**
*
* @typedef {Object} Headline
* @property {boolean} [display=true] - Whether to display headline
* @property {string} [title] -
* @property {boolean} [range=true] - Hide if false
* @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
* @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
* @since 2.0.0
*/
/**
*
* @typedef {Object} Footer
* @property {boolean} [display=true] - Whether to display headline
* @property {string} [content] -
* @property {boolean} [range=false] - Visible if true
* @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
* @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
* @since 2.0.0
*/
/**
*
* @typedef {Object} Sidebar
* @property {boolean} [sticky=false] - Whether does sticky the sidebar by using "display: sticky" of CSS.
* @property {boolean} [overlay=false] -
* @property {array.<String>} [list] - Define the contents of the row of the sidebar. Appropriate escaping is necessary when using HTML.
* @since 2.0.0
*/
/**
* Can define the ruler position to top or bottom and both
*
* @typedef {Object} RulerOptions
* @property {array.<String>} [lines] - Multiple tick marks can be set, and array elements are set in order from the top. Set same scale of Default.scale if omitted this. c.g. [ 'year', 'month', 'day', 'weekday' ]
* @property {number} [height=30] - The height of a row of rulers
* @property {number} [fontSize=14] -
* @property {string} [color="#777777"] -
* @property {string} [background="#FFFFFF"] -
* @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
* @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
* @since 2.0.0
*/
/**
* You can set the upper and lower ruler individually
*
* @typedef {Object} Ruler
* @property {RulerOptions} [top] - The upper ruler configuration. The upper ruler is hidden if omitted.
* @property {RulerOptions} [bottom] - The lower ruler configuration. The lower ruler is hidden if omitted.
* @since 2.0.0
*/
/**
*
*
* @typedef {Object} EventMeta
* @property {boolean} [display=false] -
* @property {string} [scale="day"] -
* @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
* @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
* @property {string} [content] - This is value for if you want to show custom content on the meta
* @since 2.0.0
*/
/**
* Default options for generating the timeline by the jQuery.Timeline plugin.
* Those defaults are overridden to undefined settings of the timeline configuration.
*
* @typedef {Object} Default
* @property {string} [type="bar"] - View type of timeline event is either "bar" or "point"
* @property {string} [scale="day"] - Timetable's minimum level scale is either "year", "month", "week", "day", "hour", "minute"
* @property {string} [startDatetime="currently"] - Beginning date time of timetable on the timeline. format is ( "^d{4}(/|-)d{2}(/|-)d{2}\sd{2}:d{2}:d{2}$" ) or "currently"
* @property {string} [endDatetime="auto"] - Ending date time of timetable on the timeline. format is ( "^d{4}(/|-)d{2}(/|-)d{2}\sd{2}:d{2}:d{2}$" ) or "auto"
* @property {Headline} [headline] - Settings for the content customize in the headline
* @property {Footer} [footer] - Settings for the content customize in the footer
* @property {number|string} [range=3] - Override the scale range of the timeline to be rendered when endDatetime is undefined or "auto"
* @property {Sidebar} [sidebar] - Settings for the content of the sidebar
* @property {number|string} [rows="auto"] - Rows of timeline event area
* @property {number} [rowHeight=48] - Height of one row
* @property {number|string} [width="auto"] - Fixed width (pixel) of timeline view
* @property {number|string} [height="auto"] - Fixed height (pixel) of timeline view; Defaults to ( rows * rowHeight )
* @property {number} [minGridSize=30] - Override value of minimum size (pixel) of timeline grid
* @property {number} [marginHeight=2] - Margin (pixel) top and bottom of events on the timeline
* @property {Ruler} [ruler] - Settings of the ruler
* @property {number|string} [rangeAlign="latest"] - Possible values are "left", "center", "right", "current", "latest" and specific event id
* @property {string} [loader="default"] - Custom loader definition, possible values are "default", false and selector of loader element
* @property {boolean} [hideScrollbar=false] - Whether or not to display the scroll bar displayed when the width of the timeline overflows (even if it is set to non-display, it will not function depending on the browser)
* @property {EventMeta} [eventMeta] - Display meta of range on event node when the timeline type is "bar"
* @property {string} [storage="session"] - Specification of Web storage to cache event data, defaults to sessionStorage
* @property {boolean} [reloadCacheKeep=true] - Whether to load cached events during reloading, the cache is discarded if false
* @property {boolean} [zoom=false] - Whether to use the ability to zoom the scale of the timeline by double clicking on any scale on the ruler
* @property {boolean} [wrapScale=true] - Whether wrapping new scale in the timeline container when zoom
* @property {string} [engine="canvas"] - Choose dependent module to core as rendering engine. It'll be "canvas" or "d3.js"; Maybe add in future version
* @property {boolean} [debug=false] - Enable to debug mode if true then output logs for debugging to console; defaults to false
* @since 2.0.0
*/
/**
* The limited grid number per scale of timeline
*
* @typedef {Object} LimitScaleGrids
* @property {number} [millennium=100] - In other words it's 100000 years
* @property {number} [century=500] - In other words it's 50000 years
* @property {number} [decade=500] - In other words it's 5000 years
* @property {number} [lustrum=500] - In other words it's 2500 years
* @property {number} [year=500] - In other words it's 500 years
* @property {number} [month=540] - In other words it's 45 years
* @property {number} [week=530] - In other words it's 10 years
* @property {number} [day=366] - In other words it's about 1 years
* @property {number} [hour=720] - In other words it's 30 days
* @property {number} [quarterHour=720] - In other words it's 7.5 days
* @property {number} [halfHour=720] - In other words it's 15 days
* @property {number} [minute=720] - In other words it's 12 hours
* @property {number} [second=900] - In other words it's 15 minutes
* @since 2.0.0
*/
/**
*
*
* @typedef {Object} RelationOption
* @property {number} [before] - Set target eventID to connect the relation line to the event (leftward on the timeline) in chronological before from oneself event.
* @property {number} [after] - Set target eventID to connect the relation line to the event (rightward on the timeline) in chronological after from oneself event.
* @property {number} [linesize] -
* @property {string} [linecolor] -
* @property {number|string|boolean} [curve] - Whether the connection line is curved if the connection events are not on the same horizontal. If you specify a boolean value or a shorthand (0 or 1 only), it will be automatically curved. As with the previous version, it is also possible to specify the type of curve using defined preset values.
*/
/**
* The preset as default of event parameters on the timeline
*
* @typedef {Object} EventParams
* @property {string} uid - An unique id of event data, this can not define because this value is automatically generate as data for cache only
* @property {?number} [eventId] - It is an ID that identifies an event for you to manipulate event data via each method. If omitted, consecutive numbers are automatically assigned.
* @property {number} x - Can not define because this value is automatically generate as data for cache only
* @property {number} y - Can not define because this value is automatically generate as data for cache only
* @property {number} width - Can not define because this value is automatically generate as data for cache only
* @property {number} height - Can not define because this value is automatically generate as data for cache only
* @property {string} start - Can not define because this value is automatically generate as data for cache only
* @property {string} end - Can not define because this value is automatically generate as data for cache only
* @property {number} row - Can not define because this value is automatically generate as data for cache only
* @property {string} [bgColor="#E7E7E7"] -
* @property {string} [color="#343A40"] -
* @property {string} [bdColor="#6C757D"] -
* @property {string} [label] -
* @property {string} [content] -
* @property {string} [image] -
* @property {number} [margin] -
* @property {string} [rangeMeta] -
* @property {number|string} [size="normal"] - Define the diameter size of pointer when type of the timeline is "point". Possible values are "large", "normal", "small" and value of pixel.
* @property {Object} [extend] - The specified key/value pair is replaced with the data attribute of the event element.
* @property {boolean} [remote=false] -
* @property {RelationOption} [relation] - Setting for connecting events by relation lines when the timeline type is "point".
* @property {function} [callback] - Callback processing that binds to openEvent method when this event is clicked.
* @since 2.0.0
*/
/*
* Binding Custom Events
*
* @typedef {Object} Event
* @property {string} INITIALIZED
* @property {string} HIDE
* @property {string} SHOW
* @property {string} CLICK_EVENT
* @property {string} FOCUSIN_EVENT
* @property {string} FOCUSOUT_EVENT
* @property {string} MOUSEENTER_POINTER
* @property {string} MOUSELEAVE_POINTER
* @property {string} ZOOMIN_SCALE
* @since 2.0.0
*/
/*
* Class name of the timeline elements created by the plugin
*
* @typedef {Object} ClassName
* @property {string} TIMELINE_CONTAINER
* @property {string} TIMELINE_MAIN
* @property {string} TIMELINE_HEADLINE
* @property {string} TIMELINE_HEADLINE_WRAPPER
* @property {string} HEADLINE_TITLE
* @property {string} RANGE_META
* @property {string} RANGE_SPAN
* @property {string} TIMELINE_EVENT_CONTAINER
* @property {string} TIMELINE_BACKGROUND_GRID
* @property {string} TIMELINE_RELATION_LINES
* @property {string} TIMELINE_EVENTS
* @property {string} TIMELINE_EVENT_NODE
* @property {string} TIMELINE_EVENT_LABEL
* @property {string} TIMELINE_EVENT_THUMBNAIL
* @property {string} TIMELINE_RULER_LINES
* @property {string} TIMELINE_RULER_ITEM
* @property {string} TIMELINE_SIDEBAR
* @property {string} TIMELINE_SIDEBAR_MARGIN
* @property {string} TIMELINE_SIDEBAR_ITEM
* @property {string} TIMELINE_FOOTER
* @property {string} TIMELINE_FOOTER_CONTENT
* @property {string} VIEWER_EVENT_TITLE
* @property {string} VIEWER_EVENT_CONTENT
* @property {string} VIEWER_EVENT_META
* @property {string} VIEWER_EVENT_IMAGE_WRAPPER
* @property {string} VIEWER_EVENT_IMAGE
* @property {string} VIEWER_EVENT_TYPE_POINTER
* @property {string} HIDE_SCROLLBAR
* @property {string} HIDE
* @property {string} RULER_ITEM_ALIGN_LEFT
* @property {string} STICKY_LEFT
* @property {string} OVERLAY
* @property {string} ALIGN_SELF_RIGHT
* @property {string} LOADER_ITEM
* @since 2.0.0
*/
/*
* Selectors assigned on the timeline element
*
* @typedef {Object} Selector
* @property {string} EVENT_NODE
* @property {string} EVENT_VIEW
* @property {string} RULER_TOP
* @property {string} RULER_BOTTOM
* @property {string} TIMELINE_CONTAINER
* @property {string} TIMELINE_RULER_TOP
* @property {string} TIMELINE_RULER_BOTTOM
* @property {string} TIMELINE_RULER_ITEM
* @property {string} TIMELINE_RELATION_LINES
* @property {string} TIMELINE_EVENTS
* @property {string} TIMELINE_SIDEBAR
* @property {string} TIMELINE_SIDEBAR_ITEM
* @property {string} TIMELINE_EVENT_NODE
* @property {string} VIEWER_EVENT_TYPE_POINTER
* @property {string} LOADER
* @property {string} DEFAULT_EVENTS
* @since 2.0.0
*/
/**
* Pluin Core Class
* @access public
* @since 2.0.0
*/
class Timeline {
constructor( element, config ) {
/** @type {Object} */
this._config = this._getConfig( config )
/** @type {Object} */
this._element = element
/** @type {?string} */
this._selector = null
/** @type {boolean} */
this._isInitialized = false
/** @type {boolean} */
this._isCached = false
/** @type {boolean} */
this._isCompleted = false
/** @type {boolean} */
this._isShown = false
/** @type {Object} */
this._instanceProps = {}
}
// Getters
/** @type {string} */
static get VERSION() {
return VERSION
}
/** @type {Default} */
static get Default() {
return Default
}
// Private
/**
* Define the default options of this plugin
* @private
* @param {Object} config - Initial options
* @return {Object} Config overrided initial options to default config
*/
_getConfig( config ) {
config = {
...Default,
...config
}
return config
}
/**
* Filter the scale key name for LimitScaleGrids
* @private
* @param {string} key
* @return {string} Filtered scale key name
*/
_filterScaleKeyName( key ) {
let filteredKey = null
switch( true ) {
case /^quarter-?(|hour)$/i.test( key ):
filteredKey = 'quarterHour'
break
case /^half-?(|hour)$/i.test( key ):
filteredKey = 'halfHour'
break
default:
filteredKey = key
}
return filteredKey
}
/**
* Initialize the plugin
* @private
*/
_init() {
this._debug( '_init' )
let _elem = this._element,
_selector = `${_elem.tagName}${( _elem.id ? `#${_elem.id}` : '' )}${( _elem.className ? `.${_elem.className.replace(/\s/g, '.')}` : '' )}`
this._selector = _selector.toLowerCase()
if ( this._isInitialized || this._isCompleted ) {
return
}
this.showLoader()
this._calcVars()
if ( ! this._verifyMaxRenderableRange() ) {
throw new RangeError( `Timeline display period exceeds maximum renderable range.` )
}
this.sleep( _sleep ).then(() => {
if ( ! this._isInitialized ) {
this._renderView()
const afterInitEvent = $.Event( Event.INITIALIZED, { _elem } )
$(_elem).trigger( afterInitEvent )
$(_elem).off( Event.INITIALIZED )
}
if ( ! this._isCached ) {
this._loadEvent()
}
if ( this._isCached ) {
this._placeEvent()
}
// Assign events for the timeline
$(document).on(
Event.CLICK_EVENT,
`${this._selector} ${Selector.EVENT_NODE}`,
( event ) => this.openEvent( event )
)
$(_elem).on(
Event.FOCUSIN_EVENT,
Selector.TIMELINE_EVENT_NODE,
( event ) => this._activeEvent( event )
)
$(_elem).on(
Event.FOCUSOUT_EVENT,
Selector.TIMELINE_EVENT_NODE,
( event ) => this._activeEvent( event )
)
if ( /^point(|er)$/i.test( this._config.type ) ) {
$(_elem).on(
Event.MOUSEENTER_POINTER,
Selector.VIEWER_EVENT_TYPE_POINTER,
( event ) => this._hoverPointer( event )
)
$(_elem).on(
Event.MOUSELEAVE_POINTER,
Selector.VIEWER_EVENT_TYPE_POINTER,
( event ) => this._hoverPointer( event )
)
}
if ( this._config.zoom ) {
$(_elem).on(
Event.ZOOMIN_SCALE,
Selector.TIMELINE_RULER_ITEM,
( event ) => this.zoomScale( event )
)
}
this._isCompleted = true
this.alignment()
})
}
/**
* Calculate each properties of the timeline instance
* @private
*/
_calcVars() {
let _opts = this._config,
_props = {}
_props.begin = this.supplement( null, this._getPluggableDatetime( _opts.startDatetime, 'first' ) )
_props.end = this.supplement( null, this._getPluggableDatetime( _opts.endDatetime, 'last' ) )
_props.scaleSize = this.supplement( null, _opts.minGridSize, this.validateNumeric )
_props.rows = this._getPluggableRows()
_props.rowSize = this.supplement( null, _opts.rowHeight, this.validateNumeric )
_props.width = this.supplement( null, _opts.width, this.validateNumeric )
_props.height = this.supplement( null, _opts.height, this.validateNumeric )
this._instanceProps = _props // pre-cache
if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
// For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
let _temp = this._verifyScale( _opts.scale ),
_values = Object.values( _temp ),
_averageDays = this.numRound( _values.reduce( ( a, v ) => a + v, 0 ) / _values.length, 4 ), // Average days within the range
_baseDaysOfScale = /^years?$/i.test( _opts.scale ) ? 365 : 30,
_totalWidth = 0
//console.log( '!', _opts.scale, _temp, _vals )
_values.forEach( ( days ) => {
_totalWidth += this.numRound( ( days * _props.scaleSize ) / _baseDaysOfScale, 2 )
})
_props.scale = _averageDays * ( 24 * 60 * 60 * 1000 )
_props.grids = _values.length
_props.variableScale = _temp
_props.fullwidth = _totalWidth
} else {
// In case of fixed length scale (:> 固定長スケールの場合
_props.scale = this._verifyScale( _opts.scale )
_props.grids = Math.ceil( ( _props.end - _props.begin ) / _props.scale )
_props.variableScale = null
_props.fullwidth = _props.grids * _props.scaleSize
}
_props.fullheight = _props.rows * _props.rowSize
// Define visible size according to full size of timeline (:> タイムラインのフルサイズに準じた可視サイズを定義
_props.visibleWidth = _props.width > 0 ? `${( _props.width <= _props.fullwidth ? _props.width : _props.fullwidth )}px` : '100%'
_props.visibleHeight = _props.height > 0 ? `${( _props.height <= _props.fullheight ? _props.height : _props.fullheight )}px` : 'auto'
for ( let _prop in _props ) {
if ( _prop === 'width' || _prop === 'height' || _prop === 'variableScale' ) {
continue
}
if ( this.is_empty( _props[_prop] ) ) {
throw new TypeError( `Property "${_prop}" cannot set because undefined or invalid variable.` )
}
}
if ( _props.fullwidth < 2 || _props.fullheight < 2 ) {
throw new TypeError( `The range of the timeline to be rendered is incorrect.` )
}
this._instanceProps = _props
}
/**
* Retrieve the pluggable datetime as milliseconds from specified keyword
* @private
* @param {string} key - Any one of "current", "auto", or datetime string
* @param {string} [round_type] -
* @return {number} This value unit is milliseconds
*/
_getPluggableDatetime( key, round_type = '' ) {
let _opts = this._config,
_date = null,
getFirstDate = ( dateObj, scale ) => {
let _tmpDate
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ):
case /^century$/i.test( scale ):
case /^dec(ade|ennium)$/i.test( scale ):
case /^lustrum$/i.test( scale ):
case /^years?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), 0, 1 )
break
case /^months?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), 1 )
break
case /^(week|day)s?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() )
break
case /^(|half|quarter)-?hours?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() )
break
case /^minutes?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() )
break
case /^seconds?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() )
break
}
return _tmpDate
},
getLastDate = ( dateObj, scale ) => {
let _tmpDate
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ):
case /^century$/i.test( scale ):
case /^dec(ade|ennium)$/i.test( scale ):
case /^lustrum$/i.test( scale ):
case /^years?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear() + 1, 0, 1 )
break
case /^months?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
break
case /^(week|day)s?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() + 1 )
break
case /^(|half|quarter)-?hours?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() + 1 )
break
case /^minutes?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() + 1 )
break
case /^seconds?$/i.test( scale ):
_tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() + 1 )
break
}
return new Date( _tmpDate.getTime() - 1 )
},
is_remapping = /^\d{1,2}(|(-|\/).+)$/.test( key.toString() )
//console.log( '!_getPluggableDatetime:', key, round_type, is_remapping )
switch ( true ) {
case /^current(|ly)$/i.test( key ):
_date = new Date()
//console.log( '!_getPluggableDatetime::currently:', _opts.scale, this.getHigherScale( _opts.scale ), key, _date.getTime() )
break
case /^auto$/i.test( key ): {
let _ms = null,
_higherScale = this.getHigherScale( _opts.scale )
if ( /^current(|ly)$/i.test( _opts.startDatetime ) ) {
_date = new Date()
//if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
_date = getFirstDate( _date, _opts.scale )
//}
} else {
_date = this.getCorrectDatetime( _opts.startDatetime )
}
if ( _opts.range || _opts.range > 0 ) {
if ( /^years?$/i.test( _higherScale ) ) {
_ms = 365.25 * 24 * 60 * 60 * 1000
} else
if ( /^months?$/i.test( _higherScale ) ) {
_ms = 30.44 * 24 * 60 * 60 * 1000
} else {
_ms = this._verifyScale( _higherScale )
}
_date.setTime( _date.getTime() + ( _ms * _opts.range ) )
} else {
if ( /^years?$/i.test( _opts.scale ) ) {
_ms = 365.25 * 24 * 60 * 60 * 1000
} else
if ( /^months?$/i.test( _opts.scale ) ) {
_ms = 30.44 * 24 * 60 * 60 * 1000
} else {
_ms = this._verifyScale( _opts.scale )
}
_date.setTime( _date.getTime() + ( _ms * LimitScaleGrids[this._filterScaleKeyName( _opts.scale )] ) )
}
// console.log( '!_getPluggableDatetime::auto:', _opts.scale, this.getHigherScale( _opts.scale ), key, _date.getTime() )
break
}
default:
_date = this.getCorrectDatetime( key )
break
}
if ( ! is_remapping ) {
is_remapping = _date.getFullYear() < 100
}
if ( ! this.is_empty( round_type ) ) {
if ( 'first' === round_type ) {
//console.log( '!_getPluggableDatetime::first:before:', key, _date, is_remapping )
_date = getFirstDate( _date, _opts.scale )
//console.log( '!_getPluggableDatetime::first:after:', key, _date, is_remapping )
} else
if ( 'last' === round_type ) {
//console.log( '!_getPluggableDatetime::last:before:', key, _date, is_remapping )
_date = getLastDate( _date, _opts.scale )
//console.log( '!_getPluggableDatetime::last:after:', key, _date, is_remapping )
}
}
if ( is_remapping ) {
_date.setFullYear( String( _date.getFullYear() ).substr(-2) )
}
//console.log( '!_getPluggableDatetime::return:', _date )
return _date.getTime()
}
/**
* Retrieve the pluggable parameter as an object
* @private
* @param {string} str_like_params - Strings that can be parsed as javascript objects
* @return {Object}
*/
_getPluggableParams( str_like_params ) {
let params = {}
if ( typeof str_like_params === 'string' && str_like_params ) {
try {
params = JSON.parse( JSON.stringify( ( new Function( `return ${str_like_params}` ) )() ) )
if ( params.hasOwnProperty( 'extend' ) ) {
params.extend = JSON.parse( JSON.stringify( ( new Function( `return ${params.extend}` ) )() ) )
}
} catch( e ) {
console.warn( 'Can not parse to object therefor invalid param.' )
}
}
return params
}
/**
* Retrieve the pluggable rows of the timeline
* @private
* @return {number}
*/
_getPluggableRows() {
let _opts = this._config,
fixed_rows = this.supplement( 'auto', _opts.rows, this.validateNumeric )
if ( fixed_rows === 'auto' ) {
fixed_rows = _opts.sidebar.list.length
}
return fixed_rows > 0 ? fixed_rows : 1
}
/**
* Verify the allowed scale, then retrieve that scale's millisecond if allowed
* @private
* @param {string} scale -
* @return {number|boolean} Return false if specified an invalid scale
*/
_verifyScale( scale ) {
let _opts = this._config,
_props = this._instanceProps,
_ms = -1
if ( typeof scale === 'undefined' || typeof scale !== 'string' ) {
return false
}
switch ( true ) {
case /^millisec(|ond)s?$/i.test( scale ):
// Millisecond (:> ミリ秒
_ms = 1
break
case /^seconds?$/i.test( scale ):
// Second (:> 秒
_ms = 1000
break
case /^minutes?$/i.test( scale ):
// Minute (:> 分
_ms = 60 * 1000
break
case /^quarter-?(|hour)$/i.test( scale ):
// Quarter of an hour (:> 15分
_ms = 15 * 60 * 1000
break
case /^half-?(|hour)$/i.test( scale ):
// Half an hour (:> 30分
_ms = 30 * 60 * 1000
break
case /^hours?$/i.test( scale ):
// Hour (:> 時(時間)
_ms = 60 * 60 * 1000
break
case /^days?$/i.test( scale ):
// Day (:> 日
_ms = 24 * 60 * 60 * 1000
break
case /^weeks?$/i.test( scale ):
// Week (:> 週
_ms = 7 * 24 * 60 * 60 * 1000
break
case /^months?$/i.test( scale ):
// Month (is the variable length scale) (:> 月(可変長スケール)
//console.log( '!_verifyScale::month:', this._instanceProps, _opts.scale )
if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
return this._diffDate( _props.begin, _props.end, scale )
} else {
_ms = 30.44 * 24 * 60 * 60 * 1000
break
}
case /^years?$/i.test( scale ):
// Year (is the variable length scale) (:> 年(可変長スケール)
if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
return this._diffDate( _props.begin, _props.end, scale )
} else {
_ms = 365.25 * 24 * 60 * 60 * 1000
break
}
case /^lustrum$/i.test( scale ):
// Lustrum (is the variable length scale, but currently does not support) (:> 五年紀 (可変長スケールだが現在サポートしてない)
// 5y = 1826 or 1827; 1826 * 24 * 60 * 60 = 15766400, 1827 * 24 * 60 * 60 = 157852800 | avg.= 157788000
//_ms = ( ( 3.1536 * Math.pow( 10, 8 ) ) / 2 ) * 1000 // <--- Useless by info of wikipedia
_ms = 157788000 * 1000
break
case /^dec(ade|ennium)$/i.test( scale ):
// Decade (is the variable length scale, but currently does not support) (:> 十年紀 (可変長スケールだが現在サポートしてない)
// 10y = 3652 or 3653; 3652 * 24 * 60 * 60 = 315532800, 3653 * 24 * 60 * 60 = 157852800 | avg. = 315576000
// _ms = ( 3.1536 * Math.pow( 10, 8 ) ) * 1000 // <--- Useless by info of wikipedia
_ms = 315576000 * 1000
break
case /^century$/i.test( scale ):
// Century (:> 世紀(百年紀)
// 100y = 36525; 36525 * 24 * 60 * 60 = 3155760000
_ms = 3155760000 * 1000
break
case /^millenniums?|millennia$/i.test( scale ):
// Millennium (:> 千年紀
// 100y = 365250
//_ms = ( 3.1536 * Math.pow( 10, 10 ) ) * 1000
_ms = 3155760000 * 10 * 1000
break
default:
console.warn( 'Specified an invalid scale.' )
_ms = -1
}
return _ms > 0 ? _ms : false
}
/**
* Verify the display period of the timeline does not exceed the maximum renderable range
* @private
* @return {boolean}
*/
_verifyMaxRenderableRange() {
// console.log( this._instanceProps.grids, '/', LimitScaleGrids[this._filterScaleKeyName( this._config.scale )] )
return this._instanceProps.grids <= LimitScaleGrids[this._filterScaleKeyName( this._config.scale )]
}
/**
* Render the view of timeline container
* @private
*/
_renderView() {
this._debug( '_renderView' )
let _elem = this._element,
_opts = this._config,
_props = this._instanceProps,
_tl_container = $('<div></div>', { class: ClassName.TIMELINE_CONTAINER, style: `width: ${_props.visibleWidth}; height: ${_props.visibleHeight};` }),
_tl_main = $('<div></div>', { class: ClassName.TIMELINE_MAIN })
//console.log( _elem, _opts, _props )
if ( $(_elem).length == 0 ) {
throw new TypeError( 'Does not exist the element to render a timeline container.' )
}
if ( _opts.debug ) {
console.info( `Timeline:{ fullWidth: ${_props.fullwidth}px,`, `fullHeight: ${_props.fullheight}px,`, `viewWidth: ${_props.visibleWidth}`, `viewHeight: ${_props.visibleHeight} }` )
}
$(_elem).css( 'position', 'relative' ) // initialize; not .empty()
if ( _opts.hideScrollbar ) {
_tl_container.addClass( ClassName.HIDE_SCROLLBAR )
}
// Create the timeline headline (:> タイムラインの見出しを生成
$(_elem).prepend( this._createHeadline() )
// Create the timeline event container (:> タイムラインのイベントコンテナを生成
_tl_main.append( this._createEventContainer() )
// Create the timeline ruler (:> タイムラインの目盛を生成
if ( ! this.is_empty( _opts.ruler.top ) ) {
_tl_main.prepend( this._createRuler( 'top' ) )
}
if ( ! this.is_empty( _opts.ruler.bottom ) ) {
_tl_main.append( this._createRuler( 'bottom' ) )
}
// Create the timeline side index (:> タイムラインのサイドインデックスを生成
let margin = {
top : parseInt( _tl_main.find( Selector.RULER_TOP ).height(), 10 ) - 1,
bottom : parseInt( _tl_main.find( Selector.RULER_BOTTOM ).height(), 10 ) - 1
}
if ( _opts.sidebar.list.length > 0 ) {
_tl_container.prepend( this._createSideIndex( margin ) )
}
// Append the timeline container in the timeline element (:> タイムライン要素にタイムラインコンテナを追加
_tl_container.append( _tl_main )
$(_elem).append( _tl_container )
// Create the timeline footer (:> タイムラインのフッタを生成
$(_elem).append( this._createFooter() )
this._isShown = true
}
/**
* Create the headline of the timeline
* @private
* @return {Object} Generated DOM element
*/
_createHeadline() {
let _opts = this._config,
_props = this._instanceProps,
_display = this.supplement( Default.headline.display, _opts.headline.display, this.validateBoolean ),
_title = this.supplement( null, _opts.headline.title ),
_range = this.supplement( Default.headline.range, _opts.headline.range, this.validateBoolean ),
_locale = this.supplement( Default.headline.locale, _opts.headline.locale ),
_format = this.supplement( Default.headline.format, _opts.headline.format ),
_begin = this.supplement( null, _props.begin ),
_end = this.supplement( null, _props.end ),
_tl_headline = $('<div></div>', { class: ClassName.TIMELINE_HEADLINE }),
_wrapper = $('<div></div>', { class: ClassName.TIMELINE_HEADLINE_WRAPPER })
// console.log( '!_createHeadline:', _opts )
if ( _title ) {
_wrapper.append( `<h3 class="${ClassName.HEADLINE_TITLE}">${_opts.headline.title}</h3>` )
}
if ( _range ) {
if ( _begin && _end ) {
let _meta = `${new Date( _begin ).toLocaleString( _locale, _format )}<span class="${ClassName.RANGE_SPAN}"></span>${new Date( _end ).toLocaleString( _locale, _format )}`
//let _meta = this.getCorrectDatetime( _begin ).toLocaleString( _locale, _format ) +'<span class="jqtl-range-span"></span>'+ this.getCorrectDatetime( _end ).toLocaleString( _locale, _format )
_wrapper.append( `<div class="${ClassName.RANGE_META}">${_meta}</div>` )
}
}
if ( ! _display ) {
_tl_headline.addClass( ClassName.HIDE )
}
return _tl_headline.append( _wrapper )
}
/**
* Create the event container of the timeline
* @private
* @return {Object} Generated DOM element
*/
_createEventContainer() {
let _opts = this._config,
_props = this._instanceProps,
_actualHeight = _props.fullheight + Math.ceil( _props.rows / 2 ),
_container = $('<div></div>', { class: ClassName.TIMELINE_EVENT_CONTAINER, style: `height:${_actualHeight}px;` }),
_events_bg = $(`<canvas width="${( _props.fullwidth - 1 )}" height="${_actualHeight}" class="${ClassName.TIMELINE_BACKGROUND_GRID}"></canvas>`),
_events_lines = $(`<canvas width="${( _props.fullwidth - 1 )}" height="${_actualHeight}" class="${ClassName.TIMELINE_RELATION_LINES}"></canvas>`),
_events_body = $('<div></div>', { class: ClassName.TIMELINE_EVENTS }),
_cy = 0,
ctx_grid = _events_bg[0].getContext('2d'),
drawRowRect = ( pos_y, color ) => {
color = this.supplement( '#FFFFFF', color )
// console.log( 0, pos_y, _fullwidth, _size_row, color )
ctx_grid.fillStyle = color
ctx_grid.fillRect( 0, pos_y + 0.5, _props.fullwidth, _props.rowSize + 1.5 )
ctx_grid.stroke()
},
drawHorizontalLine = ( pos_y, is_dotted ) => {
is_dotted = this.supplement( false, is_dotted )
// console.log( pos_y, is_dotted )
ctx_grid.strokeStyle = 'rgba( 51, 51, 51, 0.1 )'
ctx_grid.lineWidth = 1
ctx_grid.filter = 'url(#crisp)'
ctx_grid.beginPath()
if ( is_dotted ) {
ctx_grid.setLineDash([ 1, 2 ])
} else {
ctx_grid.setLineDash([])
}
ctx_grid.moveTo( 0, pos_y + 0.5 )
ctx_grid.lineTo( _props.fullwidth, pos_y + 0.5 )
ctx_grid.closePath()
ctx_grid.stroke()
},
drawVerticalLine = ( pos_x, is_dotted ) => {
is_dotted = this.supplement( false, is_dotted )
// console.log( pos_x, is_dotted )
ctx_grid.strokeStyle = 'rgba( 51, 51, 51, 0.025 )'
ctx_grid.lineWidth = 1
ctx_grid.filter = 'url(#crisp)'
ctx_grid.beginPath()
if ( is_dotted ) {
ctx_grid.setLineDash([ 1, 2 ])
} else {
ctx_grid.setLineDash([])
}
ctx_grid.moveTo( pos_x - 0.5, 0 )
ctx_grid.lineTo( pos_x - 0.5, _props.fullheight )
ctx_grid.closePath()
ctx_grid.stroke()
}
_cy = 0
for ( let i = 0; i < _props.rows; i++ ) {
_cy += i % 2 == 0 ? 1 : 0
let _pos_y = ( i * _props.rowSize ) + _cy
drawRowRect( _pos_y, i % 2 == 0 ? '#FEFEFE' : '#F8F8F8' )
}
_cy = 0
for ( let i = 1; i < _props.rows; i++ ) {
_cy += i % 2 == 0 ? 1 : 0
let _pos_y = ( i * _props.rowSize ) + _cy
drawHorizontalLine( _pos_y, true )
}
if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
// For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
let _bc = /^years?$/i.test( _opts.scale ) ? 365 : 30,
_sy = 0
for ( let _key of Object.keys( _props.variableScale ) ) {
_sy += this.numRound( ( _props.variableScale[_key] * _props.scaleSize ) / _bc, 2 )
drawVerticalLine( _sy, false )
}
} else {
// In case of fixed length scale (:> 固定長スケールの場合
for ( let i = 1; i < _props.grids; i++ ) {
drawVerticalLine( ( i * _props.scaleSize ), false )
}
}
return _container.append( _events_bg ).append( _events_lines ).append( _events_body )
}
/**
* Create the ruler of the timeline
* @private
* @param {string} position - Either "top" or "bottom" as the position of the ruler
* @return {Object} Generated DOM element
*/
_createRuler( position ) {
let _opts = this._config,
_props = this._instanceProps,
ruler_line = this.supplement( [ _opts.scale ], _opts.ruler[position].lines, ( def, val ) => Array.isArray( val ) && val.length > 0 ? val : def ),
line_height = this.supplement( Default.ruler.top.height, _opts.ruler[position].height ),
font_size = this.supplement( Default.ruler.top.fontSize, _opts.ruler[position].fontSize ),
text_color = this.supplement( Default.ruler.top.color, _opts.ruler[position].color ),
background = this.supplement( Default.ruler.top.background, _opts.ruler[position].background ),
locale = this.supplement( Default.ruler.top.locale, _opts.ruler[position].locale ),
format = this.supplement( Default.ruler.top.format, _opts.ruler[position].format ),
ruler_opts = { lines: ruler_line, height: line_height, fontSize: font_size, color: text_color, background, locale, format },
_fullwidth = _props.fullwidth - 1,
_fullheight = ruler_line.length * line_height,
_ruler = $('<div></div>', { class: `${PREFIX}ruler-${position}`, style: `height:${_fullheight}px;` }),
_ruler_bg = $(`<canvas class="${PREFIX}ruler-bg-${position}" width="${_fullwidth}" height="${_fullheight}"></canvas>`),
_ruler_body = $('<div></div>', { class: `${PREFIX}ruler-content-${position}` }),
_finalLines = 0,
ctx_ruler = _ruler_bg[0].getContext('2d')
//console.log( grids, size_per_grid, scale, begin, min_scale, ruler, position, ruler_line, line_height, ctx_ruler.canvas.width, ctx_ruler.canvas.height )
// Draw background of ruler
ctx_ruler.fillStyle = background
ctx_ruler.fillRect( 0, 0, ctx_ruler.canvas.width, ctx_ruler.canvas.height )
// Draw stroke of ruler
ctx_ruler.strokeStyle = 'rgba( 51, 51, 51, 0.1 )'
ctx_ruler.lineWidth = 1
ctx_ruler.filter = 'url(#crisp)'
ruler_line.some( ( line_scale, idx ) => {
if ( /^(quarter|half)-?(|hour)$/i.test( line_scale ) ) {
return true // break
}
ctx_ruler.beginPath()
// Draw rows
//let _line_x = position === 'top' ? 0 : ctx_ruler.canvas.width,
let _line_y = position === 'top' ? line_height * ( idx + 1 ) - 0.5 : line_height * idx + 0.5
ctx_ruler.moveTo( 0, _line_y )
ctx_ruler.lineTo( ctx_ruler.canvas.width, _line_y )
// Draw cols
let _line_grids = null,
_grid_x = 0,
_correction = -1.5
if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
// For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
_line_grids = this._filterVariableScale( line_scale )
//console.log( '!_createRuler:', line_scale, _line_grids )
for ( let _key of Object.keys( _line_grids ) ) {
_grid_x += this.numRound( _line_grids[_key], 2 )
ctx_ruler.moveTo( _grid_x + _correction, position === 'top' ? _line_y - line_height : _line_y )
ctx_ruler.lineTo( _grid_x + _correction, position === 'top' ? _line_y : _line_y + line_height )
}
} else {
// In case of fixed length scale (:> 固定長スケールの場合
_line_grids = this._getGridsPerScale( line_scale )
for ( let _val of _line_grids ) {
if ( this.is_empty( _val ) || _val >= _props.grids ) {
break
}
let _grid_width = _val * _props.scaleSize
_grid_x += _grid_width
if ( Math.ceil( _grid_x ) - _correction >= ctx_ruler.canvas.width ) {
break
}
ctx_ruler.moveTo( _grid_x + _correction, position === 'top' ? _line_y - line_height : _line_y )
ctx_ruler.lineTo( _grid_x + _correction, position === 'top' ? _line_y : _line_y + line_height )
}
}
ctx_ruler.closePath()
ctx_ruler.stroke()
_ruler_body.append( this._createRulerContent( _line_grids, line_scale, ruler_opts ) )
_finalLines++
})
if ( ruler_line.length != _finalLines ) {
_ruler.css( 'height', `${_finalLines * line_height}px` )
}
return _ruler.append( _ruler_bg ).append( _ruler_body )
}
/**
* Filter to aggregate the grid width of the variable length scale
* @private
* @param {string} target_scale -
* @return {Object}
*/
_filterVariableScale( target_scale ) {
let _opts = this._config,
_props = this._instanceProps,
_bc = /^years?$/i.test( _opts.scale ) ? 365 : 30,
scales = _props.variableScale,
retObj = {}
for ( let _dt of Object.keys( scales ) ) {
let _days = scales[_dt],
grid_size = this.numRound( ( _days * _props.scaleSize ) / _bc, 2 ),
_newKey = null,
_arr, _temp
//console.log( '!_filterVariableScale:', _dt, this.getCorrectDatetime( _dt ).getFullYear(), _days )
switch ( true ) {
case /^millenniums?|millennia$/i.test( target_scale ):
_newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 1000 )
if ( retObj.hasOwnProperty( _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
break
case /^century$/i.test( target_scale ):
_newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 100 )
if ( retObj.hasOwnProperty( _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
break
case /^dec(ade|ennium)$/i.test( target_scale ):
_newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 10 )
if ( retObj.hasOwnProperty( _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
break
case /^lustrum$/i.test( target_scale ):
_newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 5 )
if ( retObj.hasOwnProperty( _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
break
case /^years?$/i.test( target_scale ):
_newKey = `${this.getCorrectDatetime( _dt ).getFullYear()}`
if ( retObj.hasOwnProperty( _newKey ) ) {
retObj[_newKey] += grid_size
} else {
retObj[_newKey] = grid_size
}
break
case /^months?$/i.test( target_scale ):
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}`] = grid_size
break
case /^weeks?$/i.test( target_scale ):
_arr = _dt.split(',')
_temp = this.getWeek( _arr[0] )
//console.log( '!_filterVariableScale::week:', _dt, _arr[0], _temp )
retObj[`${this.getCorrectDatetime( _arr[0] ).getFullYear()},${_temp}`] = grid_size
break
case /^weekdays?$/i.test( target_scale ):
_arr = _dt.split(',')
_temp = this.getCorrectDatetime( _arr[0] ).getDay()
retObj[`${this.getCorrectDatetime( _arr[0] ).getFullYear()}/${this.getCorrectDatetime( _arr[0] ).getMonth() + 1}/1,${_temp}`] = grid_size
break
case /^days?$/i.test( target_scale ):
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1`] = grid_size
break
case /^hours?$/i.test( target_scale ):
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0`] = grid_size
break
case /^minutes?$/i.test( target_scale ):
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00`] = grid_size
break
case /^seconds?$/i.test( target_scale ):
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00:00`] = grid_size
break
default:
retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}`] = grid_size
break
}
}
return retObj
}
/**
* Get the grid number per scale (for fixed length scale)
* @private
* @param {string} target_scale -
* @return {Object}
*/
_getGridsPerScale( target_scale ) {
//let _opts = this._config,
let _props = this._instanceProps,
_scopes = [],
_scale_grids = {},
_sep = '/'
for ( let i = 0; i < _props.grids; i++ ) {
let _tmp = new Date( _props.begin + ( i * _props.scale ) ),
//let _tmp = this.getCorrectDatetime( _props.begin + ( i * _props.scale ) ),
_y = _tmp.getFullYear(),
_mil = Math.ceil( _y / 1000 ),
_cen = Math.ceil( _y / 100 ),
_dec = Math.ceil( _y / 10 ),
_lus = Math.ceil( _y / 5 ),
_m = _tmp.getMonth() + 1,
_wd = _tmp.getDay(), // 0 = Sun, ... 6 = Sat
_d = _tmp.getDate(),
_w = this.getWeek( `${_y}/${_m}/${_d}` ),
_h = _tmp.getHours(),
_min = _tmp.getMinutes(),
_s = _tmp.getSeconds()
// console.log( '!!:', _tmp, `y: ${_y}`, `w: ${_w}`, /* `mil: ${_mil}`, `cen: ${_cen}`, `dec: ${_dec}`, `lus: ${_lus}` */ )
_scopes.push({
millennium : _mil,
century : _cen,
decade : _dec,
lustrum : _lus,
year : _y,
month : `${_y}${_sep}${_m}${_sep}1`,
week : `${_y},${_w}`,
weekday : `${_y}${_sep}${_m}${_sep}${_d},${_wd}`,
day : `${_y}${_sep}${_m}${_sep}${_d}`,
hour : `${_y}${_sep}${_m}${_sep}${_d} ${_h}`,
minute : `${_y}${_sep}${_m}${_sep}${_d} ${_h}:${_min}`,
second : `${_y}${_sep}${_m}${_sep}${_d} ${_h}:${_min}:${_s}`,
datetime : _tmp.toString()
})
}
_scopes.forEach( ( _scope ) => {
//console.log( _scope[target_scale], idx );
if ( ! _scale_grids[_scope[target_scale]] ) {
_scale_grids[_scope[target_scale]] = 1
} else {
_scale_grids[_scope[target_scale]]++
}
})
//console.log( '!_getGridsPerScale:', target_scale, _scale_grids )
return this.toIterableObject( _scale_grids )
}
/**
* Create the content of ruler of the timeline
* @private
* @param {Object} _line_grids -
* @param {string} line_scale -
* @param {RulerOptions} ruler -
* @return {Object} Generated DOM element
*/
_createRulerContent( _line_grids, line_scale, ruler ) {
let _opts = this._config,
_props = this._instanceProps,
line_height = this.supplement( Default.ruler.top.height, ruler.height ),
font_size = this.supplement( Default.ruler.top.fontSize, ruler.fontSize ),
text_color = this.supplement( Default.ruler.top.color, ruler.color ),
locale = this.supplement( Default.ruler.top.locale, ruler.locale, this.validateString ),
format = this.supplement( Default.ruler.top.format, ruler.format, this.validateObject ),
_ruler_lines = $('<div></div>', { class: ClassName.TIMELINE_RULER_LINES, style: `width:100%;height:${line_height}px;` })
for ( let _key of Object.keys( _line_grids ) ) {
let _item_width = /^(year|month)s?$/i.test( _opts.scale ) ? _line_grids[_key] : _line_grids[_key] * _props.scaleSize,
_line = $('<div></div>', { class: ClassName.TIMELINE_RULER_ITEM, style: `width:${_item_width}px;height:${line_height}px;line-height:${line_height}px;font-size:${font_size}px;color:${text_color};` }),
_ruler_string = this.getLocaleString( _key, line_scale, locale, format ),
_data_ruler_item = ''
//console.log( '!_createRulerContent:', _key, _line_grids[_key], line_scale, locale, format, _item_width, _ruler_string )
_data_ruler_item = `${line_scale}-${( _data_ruler_item === '' ? String( _key ) : _data_ruler_item )}`
_line.attr( 'data-ruler-item', _data_ruler_item ).html( _ruler_string )
if ( _item_width > this.strWidth( _ruler_string ) ) {
// Adjust position of ruler item string
//console.log( _item_width, _ruler_string, _ruler_string.length, this.strWidth( _ruler_string ), $(this._element).width() )
if ( _item_width > $(this._element).width() ) {
_line.addClass( ClassName.RULER_ITEM_ALIGN_LEFT )
}
}
_ruler_lines.append( _line ).attr( 'data-ruler-scope', line_scale )
}
return _ruler_lines
}
/**
* Create the side indexes of the timeline
* @private
* @param {Object} margin -
* @param {number} margin.top -
* @param {number} margin.bottom -
* @return {Object} Generated DOM element
*/
_createSideIndex( margin ) {
let _opts = this._config,
_props = this._instanceProps,
_sticky = this.supplement( Default.sidebar.sticky, _opts.sidebar.sticky ),
_overlay = this.supplement( Default.sidebar.overlay, _opts.sidebar.overlay ),
_sbList = this.supplement( Default.sidebar.list, _opts.sidebar.list ),
_wrapper = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR }),
_margin = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR_MARGIN }),
_list = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR_ITEM }),
_c = 0.5
if ( _sticky ) {
_wrapper.addClass( ClassName.STICKY_LEFT )
}
if ( _overlay ) {
_list.addClass( ClassName.OVERLAY )
}
//_wrapper.css( 'margin-top', margin.top + 'px' ).css( 'margin-bottom', margin.bottom + 'px' )
if ( margin.top > 0 ) {
_wrapper.prepend( _margin.clone().css( 'height', `${( margin.top + 1 )}px` ) )
}
for ( let i = 0; i < _props.rows; i++ ) {
let _item = _list.clone().html( _sbList[i] )
_wrapper.append( _item )
}
_wrapper.find( Selector.TIMELINE_SIDEBAR_ITEM ).css( 'height', `${( _props.rowSize + _c )}px` ).css( 'line-height', `${( _props.rowSize + _c )}px` )
if ( margin.bottom > 0 ) {
_wrapper.append( _margin.clone().css( 'height', `${( margin.bottom + 1 )}px` ) )
}
return _wrapper
}
/**
* Create the footer of the timeline
* @private
* @return {Object} Generated DOM element
*/
_createFooter() {
let _opts = this._config,
_props = this._instanceProps,
_display = this.supplement( Default.footer.display, _opts.footer.display ),
_content = this.supplement( null, _opts.footer.content ),
_range = this.supplement( Default.footer.range, _opts.footer.range ),
_scale = this.supplement( Default.footer.scale, _opts.footer.scale ),
_locale = this.supplement( Default.footer.locale, _opts.footer.locale ),
_format = this.supplement( Default.footer.format, _opts.footer.format ),
_begin = this.supplement( null, _props.begin ),
_end = this.supplement( null, _props.end ),
_tl_footer = $('<div></div>', { class: ClassName.TIMELINE_FOOTER })
if ( _range ) {
if ( _begin && _end ) {
let _meta = `${new Date( _begin ).toLocaleString( _locale, _format )}<span class="${ClassName.RANGE_SPAN}"></span>${new Date( _end ).toLocaleString( _locale, _format )}`
//let _meta = this.getCorrectDatetime( _begin ).toLocaleString( _locale, _format ) +'<span class="jqtl-range-span"></span>'+ this.getCorrectDatetime( _end ).toLocaleString( _locale, _format )
_tl_footer.append( `<div class="${ClassName.RANGE_META} ${ClassName.ALIGN_SELF_RIGHT}">${_meta}</div>` )
}
}
if ( _content ) {
_tl_footer.append( `<div class="${ClassName.TIMELINE_FOOTER_CONTENT}">${_content}</div>` )
}
if ( ! _display ) {
_tl_footer.addClass( ClassName.HIDE )
}
return _tl_footer
}
/**
* Acquire the difference between two dates with the specified scale value
* @private
* @param {number} date1 - Number that can be parsed as datetime
* @param {number} date2 - Number that can be parsed as datetime
* @param {string} [scale="millisecond"] -
* @param {boolean} [absval=false] -
* @return {number|boolean}
*/
_diffDate( date1, date2, scale = 'millisecond', absval = false ) {
//let _opts = this._config,
let _dt1 = this.supplement( null, date1 ),
_dt2 = this.supplement( null, date2 ),
diffMS = 0,
retval = false,
lastDayOfMonth = ( dateObj ) => {
let _tmp = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
_tmp.setTime( _tmp.getTime() - 1 )
return _tmp.getDate()
},
isLeapYear = ( dateObj ) => {
let _tmp = new Date( dateObj.getFullYear(), 0, 1 ),
sum = 0
for ( let i = 0; i < 12; i++ ) {
_tmp.setMonth(i)
sum += lastDayOfMonth( _tmp )
}
return sum == 365 ? false : true
}
if ( ! _dt1 || ! _dt2 ) {
console.warn( 'Cannot parse date because invalid format or undefined.' )
return false
}
diffMS = _dt2 - _dt1
if ( absval ) {
diffMS = Math.abs( diffMS )
}
let _bd = new Date( _dt1 ),
_ed = new Date( _dt2 ),
_dy = _ed.getFullYear() - _bd.getFullYear(),
_m = {}
switch ( true ) {
case /^years?$/i.test( scale ):
if ( _dy > 0 ) {
for ( let i = 0; i <= _dy; i++ ) {
let _cd = new Date( _bd.getFullYear() + i, 0, 1 )
_m[`${_bd.getFullYear() + i}`] = isLeapYear( _cd ) ? 366 : 365
}
} else {
_m[`${_bd.getFullYear()}`] = isLeapYear( _bd ) ? 366 : 365
}
retval = _m
break
case /^months?$/i.test( scale ):
if ( _dy > 0 ) {
for ( let i = _bd.getMonth(); i < 12; i++ ) {
let _cd = new Date( _bd.getFullYear(), i, 1 )
_m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
}
if ( _dy > 1 ) {
for ( let y = 1; y < _dy; y++ ) {
for ( let i = 0; i < 12; i++ ) {
let _cd = new Date( _bd.getFullYear() + y, i, 1 )
_m[`${_bd.getFullYear() + y}/${i + 1}`] = lastDayOfMonth( _cd )
}
}
}
for ( let i = 0; i <= _ed.getMonth(); i++ ) {
let _cd = new Date( _ed.getFullYear(), i, 1 )
_m[`${_ed.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
}
} else {
for ( let i = _bd.getMonth(); i <= _ed.getMonth(); i++ ) {
let _cd = new Date( _bd.getFullYear(), i, 1 )
_m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
}
}
retval = _m
break
case /^weeks?$/i.test( scale ):
retval = Math.ceil( diffMS / ( 7 * 24 * 60 * 60 * 1000 ) )
break
case /^(|week)days?$/i.test( scale ):
retval = Math.ceil( diffMS / ( 24 * 60 * 60 * 1000 ) )
break
case /^hours?$/i.test( scale ):
retval = Math.ceil( diffMS / ( 60 * 60 * 1000 ) )
break
case /^minutes?$/i.test( scale ):
retval = Math.ceil( diffMS / ( 60 * 1000 ) )
break
case /^seconds?$/i.test( scale ):
retval = Math.ceil( diffMS / 1000 )
break
default:
retval = diffMS
break
}
//console.log( '!_diffDate:', retval )
return retval
}
/**
* Load all enabled events markupped on target element to the timeline object
* @private
*/
_loadEvent() {
this._debug( '_loadEvent' )
let _that = this,
_elem = this._element,
_event_list = $(_elem).find( Selector.DEFAULT_EVENTS ),
_cnt = 0,
events = [],
lastEventId = 0
_event_list.children().each(function() {
let _attr = $(this).attr( 'data-timeline-node' )
if ( typeof _attr !== 'undefined' && _attr !== false ) {
_cnt++
}
})
if ( _event_list.length == 0 || _cnt == 0 ) {
this._debug( 'Enable event does not exist.' )
}
// Register Event Data
_event_list.children().each(function() {
let _evt_params = _that._getPluggableParams( $(this).attr( 'data-timeline-node' ) ),
_one_event = {}
if ( ! _that.is_empty( _evt_params ) ) {
_one_event = _that._registerEventData( this, _evt_params )
events.push( _one_event )
lastEventId = Math.max( lastEventId, parseInt( _one_event.eventId, 10 ) )
}
});
// Set event id with auto increment (:> イベントIDを自動採番
let cacheIds = [] // for checking duplication of id (:> IDの重複チェック用
events.forEach( ( _evt, _i, _this ) => {
let _chkId = parseInt( _this[_i].eventId, 10 )
if ( _chkId == 0 || cacheIds.includes( _chkId ) ) {
lastEventId++
_this[_i].eventId = lastEventId
} else {
_this[_i].eventId = _chkId
}
cacheIds.push( _this[_i].eventId )
});
this._isCached = this._saveToCache( events )
}
/**
* Register one event data as object
* @private
* @param {Object} event_element -
* @param {Object} params -
* @return {Object} Registered new event data
*/
_registerEventData( event_element, params ) {
let _opts = this._config,
_props = this._instanceProps,
new_event = {
...EventParams,
...{
uid : this.generateUniqueID(),
label : $(event_element).html()
}
},
_relation = {},
_x, _w, _c //, _pointSize
//console.log( '!_registerEventData:', _opts, params )
if ( params.hasOwnProperty( 'start' ) && ! this.is_empty( params.start ) ) {
_x = this._getCoordinateX( params.start )
new_event.x = this.numRound( _x, 2 )
if ( params.hasOwnProperty( 'end' ) && ! this.is_empty( params.end ) ) {
_x = this._getCoordinateX( params.end )
_w = _x - new_event.x
new_event.width = this.numRound( _w, 2 )
if ( _opts.eventMeta.display ) {
if ( this.is_empty( _opts.eventMeta.content ) && ! params.hasOwnProperty( 'rangeMeta' ) ) {
//console.log( '!_registerEventData:', _opts.eventMeta.locale, _opts.eventMeta.format, _opts.scale, params )
new_event.rangeMeta += this.getLocaleString( params.start, _opts.eventMeta.scale, _opts.eventMeta.locale, _opts.eventMeta.format )
new_event.rangeMeta += ` - ${this.getLocaleString( params.end, _opts.eventMeta.scale, _opts.eventMeta.locale, _opts.eventMeta.format )}`
} else {
new_event.rangeMeta = _opts.eventMeta.content
}
}
} else {
new_event.width = 0
}
//console.log( 'getX:', _x, 'getW:', _w, event_element )
if ( params.hasOwnProperty( 'row' ) ) {
_c = Math.floor( params.row / 2 )
new_event.y = ( params.row - 1 ) * _opts.rowHeight + new_event.margin + _c
}
Object.keys( new_event ).forEach( ( _prop ) => {
switch( true ) {
case /^eventId$/i.test( _prop ):
if ( params.hasOwnProperty( 'id' ) && this.is_empty( new_event.eventId ) ) {
new_event.eventId = parseInt( params.id, 10 )
} else {
new_event.eventId = parseInt( params[_prop], 10 ) || 0
}
break
case /^(label|content)$/i.test( _prop ):
if ( params.hasOwnProperty( _prop ) && ! this.is_empty( params[_prop] ) ) {
new_event[_prop] = params[_prop]
}
// Override the children element to label or content setting
if ( $(event_element).children(`.event-${_prop}`).length > 0 ) {
new_event[_prop] = $(event_element).children(`.event-${_prop}`).html()
}
//console.log( '!_registerEventData:', _prop, params[_prop], new_event[_prop] )
break
case /^relation$/i.test( _prop ):
// For drawing the relation line
if ( /^point(|er)$/i.test( _opts.type ) ) {
//let _pointSize = this._getPointerSize( new_event.size, new_event.margin )
_relation.x = this.numRound( new_event.x, 2 )
_relation.y = this.numRound( ( _props.rowSize * ( ( params.row || 1 ) - 1 ) ) + ( _props.rowSize / 2 ), 2 )
//console.log( '!_registerEventData:', params, new_event.x, new_event.y, _pointSize, _relation )
new_event[_prop] = {
...params[_prop],
..._relation
}
}
break
default:
if ( params.hasOwnProperty( _prop ) && ! this.is_empty( params[_prop] ) ) {
new_event[_prop] = params[_prop]
}
break
}
});
}
//console.log( '!_registerEventData:', new_event )
return new_event
}
/**
* Get the coordinate X on the timeline of any date
* @private
* @param {string} date -
* @return {number} The pixel value as the coordinate X on timeline
*/
_getCoordinateX( date ) {
//let _opts = this._config,
let _props = this._instanceProps,
_date = this.supplement( null, this._getPluggableDatetime( date ) ),
coordinate_x = 0
if ( _date ) {
if ( _date - _props.begin >= 0 && _props.end - _date >= 0 ) {
// When the given date is within the range of timeline begin and end (:> 指定された日付がタイムラインの開始と終了の範囲内にある場合
coordinate_x = ( Math.abs( _date - _props.begin ) / _props.scale ) * _props.scaleSize
} else {
// When the given date is out of timeline range (:> 指定された日付がタイムラインの範囲外にある場合
coordinate_x = ( ( _date - _props.begin ) / _props.scale ) * _props.scaleSize
}
} else {
console.warn( 'Cannot parse date because invalid format or undefined.' )
}
return coordinate_x
}
/**
* Cache the event data to the web storage
* @private
* @param {Object} data -
*/
_saveToCache( data ) {
let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null )
if ( is_available ) {
if ( strageEngine === 'localStorage' ) {
localStorage.setItem( this._selector, JSON.stringify( data ) )
} else {
sessionStorage.setItem( this._selector, JSON.stringify( data ) )
}
return true
} else {
throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
}
}
/**
* Load the cached event data from the web storage
* @private
* @return {Object}
*/
_loadToCache() {
let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null ),
data = null
if ( is_available ) {
if ( strageEngine === 'localStorage' ) {
data = JSON.parse( localStorage.getItem( this._selector ) )
} else {
data = JSON.parse( sessionStorage.getItem( this._selector ) )
}
} else {
throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
}
return data
}
/**
* Remove the cache data on the web storage
* @private
*/
_removeCache() {
let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null )
if ( is_available ) {
if ( strageEngine === 'localStorage' ) {
localStorage.removeItem( this._selector )
} else {
sessionStorage.removeItem( this._selector )
}
} else {
throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
}
}
/**
* Controller method to place event data on timeline
* @private:
*/
_placeEvent() {
this._debug( '_placeEvent' )
if ( ! this._isCached ) {
return
}
let _elem = this._element,
_opts = this._config,
_evt_container = $(_elem).find( Selector.TIMELINE_EVENTS ),
_relation_lines = $(_elem).find( Selector.TIMELINE_RELATION_LINES ),
events = this._loadToCache(),
_sleep = _opts.debug ? DEBUG_SLEEP : 1
if ( events.length > 0 ) {
_evt_container.empty()
events.forEach( ( _evt ) => {
let _evt_elem = this._createEventNode( _evt )
if ( _evt_elem ) {
_evt_container.append( _evt_elem )
}
})
}
if ( /^point(|er)$/i.test( _opts.type ) ) {
this._drawRelationLine( events )
}
// console.log( '!_placeEvent:', _opts )
this.sleep( _sleep ).then(() => {
this.hideLoader()
_evt_container.fadeIn( 'fast', () => {
_relation_lines.fadeIn( 'fast' )
})
})
}
/**
* Create an event element on the timeline
* @private
* @param {Object} params -
* @return {Object} Generated DOM element
*/
_createEventNode( params ) {
let _opts = this._config,
_props = this._instanceProps,
_evt_elem = $('<div></div>', {
class : ClassName.TIMELINE_EVENT_NODE,
id : `evt-${params.eventId}`,
css : {
left : `${params.x}px`,
top : `${params.y}px`,
width : `${params.width}px`,
height : `${params.height}px`,
color : this.hexToRgbA( params.color ),
backgroundColor : this.hexToRgbA( params.bgColor ),
},
html : `<div class="${ClassName.TIMELINE_EVENT_LABEL}">${params.label}</div>`
})
//console.log( '!_createEventNode:', params )
// Whether this event is within the display range of the timeline (:> タイムライン表示範囲内のイベントかどうか
// For events excluded, set the width to -1 (:> 除外イベントは幅を -1 に設定する
if ( params.x >= 0 ) {
// The event start datetime is over the start datetime of the timeline (:> イベント始点がタイムラインの始点以上
if ( params.x <= _props.fullwidth ) {
// The event start datetime is less than or equal to the timeline end datetime (:> イベントの始点がタイムラインの終点以下
if ( params.x + params.width <= _props.fullwidth ) {
// The event end datetime is less than before the timeline end datetime (regular event) (:> イベント終点がタイムラインの終点以下(通常イベント)
// OK
} else {
// The event end datetime is after the timeline end datetime (event exceeded end datetime) (:> イベント終点がタイムラインの終点より後(終点超過イベント)
params.width = _props.fullwidth - params.x
}
} else {
// The event start datetime is after the timeline end datetime (exclude event) (:> イベント始点がタイムラインの終点より後(除外イベント)
params.width = -1
}
} else {
// The event start datetime is before the timeline start datetime (:> イベント始点がタイムラインの始点より前
if ( /^point(|er)$/i.test( _opts.type ) ) {
// In the case of "point" type, that is an exclude event (:> ポインター型の場合は除外イベント
params.width = -1
} else {
// The case of "bar" type
if ( params.x + params.width <= 0 ) {
// The event end datetime is less than before the timeline start datetime (exclude event) (:> イベント終点がタイムラインの始点より前(除外イベント)
params.width = -1
} else {
// The event end datetime is after the timeline start datetime (:> イベント終点がタイムラインの始点より後
if ( params.x + params.width <= _props.fullwidth ) {
// The event end datetime is less than or equal the timeline end datetime (event exceeded start datetime) (:> イベント終点がタイムラインの終点以下(始点超過イベント)
params.width = Math.abs( params.x + params.width )
params.x = 0
} else {
// The event end datetime is after the timeline end datetime (event exceeded both start and end datetime) (:> イベント終点がタイムラインの終点より後(始点・終点ともに超過イベント)
params.width = _props.fullwidth
params.x = 0
}
}
}
}
//console.log( 'x:', params.x, 'w:', params.width, 'x-end:', Math.abs( params.x ) + params.width, 'fw:', _props.fullwidth, 'ps:', params.size )
if ( /^point(|er)$/i.test( _opts.type ) ) {
if ( params.width < 0 ) {
return null
}
let _pointSize = this._getPointerSize( params.size, params.margin ),
_shiftX = this.numRound( params.x - ( _pointSize / 2 ), 2 ),
_shiftY = this.numRound( params.y + ( ( params.height - _pointSize ) / 2 ), 2 )
//console.log( '!_createEventNode:', params, _pointSize, _shiftX, _shiftY )
_evt_elem.addClass( ClassName.VIEWER_EVENT_TYPE_POINTER ).css( 'border-color', params.bdColor )
.css( 'left', `${_shiftX}px` ).css( 'top', `${_shiftY}px` ).css( 'width', `${_pointSize}px` ).css( 'height', `${_pointSize}px` )
.attr( 'data-base-size', _pointSize ).attr( 'data-base-left', _shiftX ).attr( 'data-base-top', _shiftY )
} else {
if ( params.width < 1 ) {
return null
}
_evt_elem.css( 'left', `${params.x}px` ).css( 'width', `${params.width}px` )
}
_evt_elem.attr( 'data-uid', params.uid )
if ( ! this.is_empty( params.image ) ) {
if ( /^point(|er)$/i.test( _opts.type ) ) {
_evt_elem.css( 'background-image', `url(${params.image})` )
} else {
let _imgSize = params.height - ( params.margin * 2 )
_evt_elem.prepend( `<img src="${params.image}" class="${ClassName.TIMELINE_EVENT_THUMBNAIL}" width="${_imgSize}" height="${_imgSize}" />` )
}
}
if ( /^bar$/i.test( _opts.type ) && _opts.eventMeta.display ) {
//console.log( '!_createEventNode:', params )
params.extend.meta = params.rangeMeta
}
if ( ! this.is_empty( params.extend ) ) {
for ( let _prop of Object.keys( params.extend ) ) {
_evt_elem.attr( `data-${_prop}`, params.extend[_prop] )
if ( _prop === 'toggle' && [ 'popover', 'tooltip' ].includes( params.extend[_prop] ) ) {
// for bootstrap's popover or tooltip
_evt_elem.attr( 'title', params.label )
if ( ! params.extend.hasOwnProperty( 'content' ) ) {
_evt_elem.attr( 'data-content', params.content )
}
}
}
}
if ( ! this.is_empty( params.callback ) ) {
_evt_elem.attr( 'data-callback', params.callback )
}
return _evt_elem
}
/**
* Retrieve the diameter size (pixel) of pointer
* @private
* @param {number|string} key -
* @param {number} margin
* @return {number}
*/
_getPointerSize( key, margin ) {
//let _opts = this._config,
let _props = this._instanceProps,
_max = Math.min( _props.scaleSize, _props.rowSize ) - ( margin * 2 ),
_size = null
switch ( true ) {
case /^large$/i.test( key ):
_size = Math.max( this.numRound( _max * 0.8, 1 ), MIN_POINTER_SIZE )
break
case /^normal$/i.test( key ):
_size = Math.max( this.numRound( _max / 2, 1 ), MIN_POINTER_SIZE )
break
case /^small$/i.test( key ):
_size = Math.max( this.numRound( _max / 4, 1 ), MIN_POINTER_SIZE )
break
default:
_size = Math.max( parseInt( key, 10 ), MIN_POINTER_SIZE )
}
//console.log( '!_getPointerSize:', _props, key, _max, _size )
return _size
}
/**
* Draw the relational lines
* @private
* @param {Object} events -
*/
_drawRelationLine( events ) {
let _opts = this._config,
_props = this._instanceProps,
_canvas = $(this._element).find( Selector.TIMELINE_RELATION_LINES ),
ctx_relations = _canvas[0].getContext('2d'),
drawLine = ( _sx, _sy, _ex, _ey, evt, _ba ) => {
let _curveType = {},
_radius = this.numRound( Math.min( _props.scaleSize, _props.rowSize ) / 2, 2 ),
_subRadius = this.numRound( this._getPointerSize( evt.size, _opts.marginHeight ) / 2, 2 )
// Defaults
ctx_relations.strokeStyle = EventParams.bdColor
ctx_relations.lineWidth = 2.5
ctx_relations.filter = 'url(#crisp)'
for ( let _key of Object.keys( evt.relation ) ) {
switch ( true ) {
case /^(|line)color$/i.test( _key ):
ctx_relations.strokeStyle = evt.relation[_key]
break
case /^(|line)size$/i.test( _key ):
ctx_relations.lineWidth = parseInt( evt.relation[_key], 10 ) || 2.5
break
case /^curve$/i.test( _key ):
if ( /^(r|l)(t|b),?(r|l)?(t|b)?$/i.test( evt.relation[_key] ) ) {
let _tmp = evt.relation[_key].split(',')
if ( _tmp.length == 2 ) {
_curveType.before = _tmp[0]
_curveType.after = _tmp[1]
} else {
_curveType[_ba] = _tmp[0]
}
} else
if ( ( typeof evt.relation[_key] === 'boolean' && evt.relation[_key] ) || ( typeof evt.relation[_key] === 'number' && Boolean( evt.relation[_key] ) ) ) {
// Automatically set the necessary linearity type (:> 自動線形判定
//console.log( _sx, _sy, _ex, _ey, _radius, _ba, _subRadius )
if ( _ba === 'before' ) {
// before: targetEvent[ _ex, _ey ] <---- selfEvent[ _sx, _sy ]
if ( _sy > _ey ) {
// 連結点が自分より上にある
if ( _sx > _ex ) {
// 連結点が自分より左にある "(_ex,_ey)└(_sx,_sy)" as "lb"
_curveType[_ba] = 'lb'
} else
if ( _sx < _ex ) {
// 連結点が自分より右にある "⊂ ̄" as "lb+lt"
_curveType[_ba] = 'lb+lt'
} else {
// 連結点が自分の直上 "│" to top
_curveType[_ba] = null
}
} else
if ( _sy < _ey ) {
// 連結点が自分より下にある
if ( _sx > _ex ) {
// 連結点が自分より左にある "(_ex,_ey)┌(_sx,_sy)" as "lt"
_curveType[_ba] = 'lt'
} else
if ( _sx < _ex ) {
// 連結点が自分より右にある "⊂_" as "rt+rb"
_curveType[_ba] = 'lt+lb'
} else {
// 連結点が自分の直下 "│" to bottom
_curveType[_ba] = null
}
} else {
// 連結点が自分と同じ水平上にある(左右どちらか) _sy == _ey; "─" to left or right
_curveType[_ba] = null
}
} else
if ( _ba === 'after' ) {
// after: selfEvent[ _sx, _sy ] ----> targetEvent[ _ex, _ey ]
if ( _sy < _ey ) {
// Relational endpoint is located "under" self (:> 連結点が自分の下にある
if ( _sx < _ex ) {
// Then relational endpoint is located "right" self (:> 連結点が自分の右にある "(_sx,_sy)┐(_ex,_ey)" as "rt"
_curveType[_ba] = 'rt'
} else
if ( _sx > _ex ) {
// Then relational endpoint is located "left" self (:> 連結点が自分より左にある "_⊃" as "rt+rb"
_curveType[_ba] = 'rt+rb'
} else {
// Relational endpoint is located "just under" self (:> 連結点が自分の直下 "│" to bottom
_curveType[_ba] = null
}
} else
if ( _sy > _ey ) {
// Relational endpoint is located "above" self (:> 連結点が自分より上にある
if ( _sx < _ex ) {
// Then relational endpoint is located "right" self (:> 連結点が自分の右にある "┘" as "rb"
_curveType[_ba] = 'rb'
} else
if ( _sx > _ex ) {
// Then relational endpoint is located "left" self (:> 連結点が自分より左にある " ̄⊃" as "rb+rt"
_curveType[_ba] = 'rb+rt'
} else {
// Relational endpoint is located "just under" self (:> 連結点が自分の直上 "│" to top
_curveType[_ba] = null
}
} else {
// 連結点が自分と同じ水平上にある(左右どちらか) _sy == _ey; "─" to left or right
_curveType[_ba] = null
}
}
}
break
}
}
if ( Math.abs( _ey - _sy ) > _props.rowSize ) {
_ey += Math.floor( Math.abs( _ey - _sy ) / _props.rowSize )
}
ctx_relations.beginPath()
if ( ! this.is_empty( _curveType ) ) {
// console.log( '!_drawLine:', _curveType, _sx, _sy, _ex, _ey, _radius )
switch ( true ) {
case /^lt$/i.test( _curveType[_ba] ): // "(_ex,_ey)┌(_sx,_sy)"
ctx_relations.moveTo( _sx, _sy )
if ( Math.abs( _sx - _ex ) > _radius ) {
ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
}
if ( Math.abs( _ey - _sy ) > _radius ) {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy + _radius ) // "┌"
ctx_relations.lineTo( _ex, _ey ) // "│"
} else {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┌"
}
break
case /^lb$/i.test( _curveType[_ba] ): // "(_ex,_ey)└(_sx,_sy)"
ctx_relations.moveTo( _sx, _sy )
if ( Math.abs( _sx - _ex ) > _radius ) {
ctx_relations.lineTo( _ex + _radius, _sy ) // "─"
}
if ( Math.abs( _sy - _ey ) > _radius ) {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy - _radius ) // "└"
ctx_relations.lineTo( _ex, _ey ) // "│"
} else {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "└"
}
break
case /^rt$/i.test( _curveType[_ba] ): // "(_sx,_sy)┐(_ex,_ey)"
ctx_relations.moveTo( _sx, _sy )
if ( Math.abs( _ex - _sx ) > _radius ) {
ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
}
if ( Math.abs( _ey - _sy ) > _radius ) {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy + _radius ) // "┐"
ctx_relations.lineTo( _ex, _ey )
} else {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┐"
}
break
case /^rb$/i.test( _curveType[_ba] ): // "(_sx,_sy)┘(_ex,_ey)"
ctx_relations.moveTo( _sx, _sy )
if ( Math.abs( _ex - _sx ) > _radius ) {
ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
}
if ( Math.abs( _sy - _ey ) > _radius ) {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy - _radius ) // "┘"
ctx_relations.lineTo( _ex, _ey )
} else {
ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┘"
}
break
case /^lt\+lb$/i.test( _curveType[_ba] ): // "⊂_"
case /^lb\+lt$/i.test( _curveType[_ba] ): // "⊂ ̄"
ctx_relations.moveTo( _sx, _sy )
//ctx_relations.lineTo( _sx - _subRadius, _sy ) // "─"
ctx_relations.lineTo( _sx - _radius, _sy ) // "─"
//ctx_relations.bezierCurveTo( _sx - _subRadius - _radius, _sy, _sx - _subRadius - _radius, _ey, _sx - _subRadius, _ey ) // "⊂"
ctx_relations.bezierCurveTo( _sx - _radius * 2, _sy, _sx - _radius * 2, _ey, _sx - _radius, _ey ) // "⊂"
ctx_relations.lineTo( _ex, _ey ) // "─"
break
case /^rt\+rb$/i.test( _curveType[_ba] ): // "_⊃"
case /^rb\+rt$/i.test( _curveType[_ba] ): // " ̄⊃"
ctx_relations.moveTo( _sx, _sy )
//ctx_relations.lineTo( _sx + _subRadius, _sy ) // "─"
ctx_relations.lineTo( _sx + _radius, _sy ) // "─"
//ctx_relations.bezierCurveTo( _sx + _subRadius + _radius, _sy, _sx + _subRadius + _radius, _ey, _sx + _subRadius, _ey ) // "⊃"
ctx_relations.bezierCurveTo( _sx + _radius * 2, _sy, _sx + _radius * 2, _ey, _sx + _radius, _ey ) // "⊃"
ctx_relations.lineTo( _ex, _ey ) // "─"
break
}
} else {
ctx_relations.moveTo( _sx, _sy )
ctx_relations.lineTo( _ex, _ey )
}
//ctx_relations.closePath()
ctx_relations.stroke()
}
ctx_relations.clearRect( 0, 0, _canvas[0].width, _canvas[0].height )
//console.log( '!_drawRelationLine:', _props, events, _canvas )
events.forEach( ( evt ) => {
let _rel = evt.relation,
_sx, _sy, _ex, _ey,
_targetId, _targetEvent
if ( _rel.hasOwnProperty( 'before' ) ) {
// before: targetEvent[ _ex, _ey ] <---- selfEvent[ _sx, _sy ]
// (:> before: 自分を起点( _sx, _sy )として左方向の連結点( _ex, _ey )へ向かう描画方式
_sx = _rel.x
_sy = _rel.y
_targetId = parseInt( _rel.before, 10 )
if ( _targetId < 0 ) {
_ex = 0
_ey = _sy
} else {
_targetEvent = events.find( ( _evt ) => parseInt( _evt.eventId, 10 ) == _targetId )
if ( ! this.is_empty( _targetEvent ) && _targetEvent.relation ) {
_ex = _targetEvent.relation.x < 0 ? 0 : _targetEvent.relation.x
_ey = _targetEvent.relation.y
}
}
if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
drawLine( _sx, _sy, _ex, _ey, evt, 'before' )
}
}
if ( _rel.hasOwnProperty( 'after' ) ) {
// after: selfEvent[ _sx, _sy ] ----> targetEvent[ _ex, _ey ]
// (:> after: 自分を起点( _sx, _sy )として右方向の連結点( _ex, _ey )へ向かう描画方式
_sx = _rel.x
_sy = _rel.y
_targetId = parseInt( _rel.after, 10 )
if ( _targetId < 0 ) {
_ex = _props.fullwidth
_ey = _sy
} else {
_targetEvent = events.find( ( _evt ) => parseInt( _evt.eventId, 10 ) == _targetId )
if ( ! this.is_empty( _targetEvent ) && _targetEvent.relation ) {
_ex = _targetEvent.relation.x > _props.fullwidth ? _props.fullwidth : _targetEvent.relation.x
_ey = _targetEvent.relation.y
}
}
if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
drawLine( _sx, _sy, _ex, _ey, evt, 'after' )
}
}
})
}
/**
* Retrieve the mapping data that placed current events
* @private
* @return {number[]}
*/
_mapPlacedEvents() {
let _that = this,
_tl_events = $(this._element).find( Selector.TIMELINE_EVENTS ).children(),
_cache = this._loadToCache(),
_events = []
if ( ! this._isCached || this.is_empty( _cache ) ) {
return _events
}
_tl_events.each(function() {
let _uid = $(this).data( 'uid' ),
_data = null
if ( _cache ) {
_data = _cache.find( ( _evt ) => _evt.uid === _uid ) || null
} else {
_data = $(this).data()
}
if ( ! _that.is_empty( _data ) ) {
_events.push( _data )
}
})
//console.log( '!_mapPlacedEvents:', _events )
return _events
}
/**
* Event when focus or blur
* @private
* @param {Object} event -
*/
_activeEvent( event ) {
// console.log( '!_activeEvent:', event )
let _elem = event.target
if ( 'focusin' === event.type ) {
$( Selector.TIMELINE_EVENT_NODE ).removeClass( 'active' )
$(_elem).addClass( 'active' )
} else
if ( 'focusout' === event.type ) {
$(_elem).removeClass( 'active' )
}
}
/**
* Event when hover on the pointer type event
* @private
* @param {Object} event -
*/
_hoverPointer( event ) {
let _props = this._instanceProps,
_elem = event.target,
_base = {
left : $(_elem).data( 'baseLeft' ),
top : $(_elem).data( 'baseTop' ),
width : $(_elem).data( 'baseSize' )
},
_x = _base.left,
_y = _base.top,
_w = _base.width,
_z = 5
//console.log( '!_hoverPointer:', _props )
if ( 'mouseenter' === event.type ) {
_w = Math.max( this.numRound( _w * 1.2, 'ceil' ), Math.min( _props.rowSize, _props.scaleSize ) )
_x = this.numRound( _x - ( ( _w - _base.width ) / 2 ), 2 )
_y = this.numRound( _y - ( ( _w - _base.width ) / 2 ), 2 )
_z = 9
$(_elem).trigger( Event.FOCUSIN_EVENT )
} else {
$(_elem).trigger( Event.FOCUSOUT_EVENT )
}
$(_elem).css( 'left', `${_x}px` ).css( 'top', `${_y}px` ).css( 'width', `${_w}px` ).css( 'height', `${_w}px` ).css( 'z-index', _z )
}
/**
* Echo the log of plugin for debugging
* @private
* @param {string} message -
* @param {string} [throwType="Notice"] -
*/
_debug( message, throwType = 'Notice' ) {
if ( ! this._config.debug ) {
return
}
message = this.supplement( null, message )
if ( message ) {
let _msg = typeof $(this._element).data( DATA_KEY )[message] !== 'undefined' ? `Called method "${message}".` : message,
_sty = /^Called method "/.test(_msg) ? 'font-weight:600;color:blue;' : '',
_rst = ''
if ( window.console && window.console.log ) {
if ( throwType === 'Notice' ) {
window.console.log( '%c%s%c', _sty, _msg, _rst )
} else {
throw new Error( `${_msg}` )
}
}
}
}
// Public
/**
* This method is able to call only once after completed an initializing of the plugin
* @public
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
initialized( ...args ) {
let _message = this._isInitialized ? 'Skipped because method "initialized" already has been called once' : 'initialized'
this._debug( _message )
let _elem = this._element,
_opts = this._config,
_args = args[0],
callback = _args.length > 0 && typeof _args[0] === 'function' ? _args[0] : null,
userdata = _args.length > 1 ? _args.slice(1) : null
// console.log( '!initialized:', callback, userdata )
if ( callback && ! this._isInitialized ) {
this._debug( 'Fired your callback function after initializing this plugin.' )
callback( _elem, _opts, userdata )
}
this._isInitialized = true
}
/**
* Destroy the object to which the plugin is applied
* @public
*/
destroy() {
this._debug( 'destroy' )
$.removeData( this._element, DATA_KEY )
$(window, document, this._element).off( EVENT_KEY )
$(this._element).remove()
this._removeCache()
for ( let _prop of Object.keys( this ) ) {
this[_prop] = null
delete this[_prop]
}
}
/**
* @deprecated This method has been deprecated since version 2.0.0
*/
render() {
throw new ReferenceError( 'This method named "render" has been deprecated since version 2.0.0' )
}
/**
* Show hidden timeline
* @public
*/
show() {
this._debug( 'show' )
let _elem = this._element
if ( ! this._isShown ) {
$(_elem).removeClass( ClassName.HIDE )
this._isShown = true
}
}
/**
* Hide shown timeline
* @public
*/
hide() {
this._debug( 'hide' )
let _elem = this._element
if ( this._isShown ) {
$(_elem).addClass( ClassName.HIDE )
this._isShown = false
}
}
/**
* Move shift or expand the range of timeline container as to past direction (to left)
* @public
* @param {?Object} options - Options for moving as dateback on the timeline container
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
dateback( ...args ) {
this._debug( 'dateback' )
let _args = args[0],
_opts = this._config,
moveOpts = this.supplement( null, _args[0], this.validateObject ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
newOpts = {},
begin_date, end_date, _tmpDate
if ( this.is_empty( moveOpts ) ) {
moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
} else {
if ( ! moveOpts.hasOwnProperty('shift') || moveOpts.shift !== false ) {
moveOpts.shift = true
}
if ( ! moveOpts.hasOwnProperty('scale') || ! this._verifyScale( moveOpts.scale ) ) {
moveOpts.scale = _opts.scale
}
if ( ! moveOpts.hasOwnProperty('range') || parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] ) {
moveOpts.range = _opts.range
}
}
_tmpDate = new Date( _opts.startDatetime )
begin_date = new Date( _tmpDate.getTime() - ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
newOpts.startDatetime = begin_date.toString()
if ( moveOpts.shift ) {
_tmpDate = new Date( _opts.endDatetime )
end_date = new Date( _tmpDate.getTime() - ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
newOpts.endDatetime = end_date.toString()
}
this.reload( [newOpts] )
if ( callback ) {
this._debug( 'Fired your callback function after datebacking.' )
callback( this._element, _opts, userdata )
}
}
/**
* Move shift or expand the range of timeline container as to futrue direction (to right)
* @public
* @param {?Object} options - Options for moving as dateforth on the timeline container
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
dateforth( ...args ) {
this._debug( 'dateforth' )
let _args = args[0],
_opts = this._config,
moveOpts = this.supplement( null, _args[0], this.validateObject ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
newOpts = {},
begin_date, end_date, _tmpDate
if ( this.is_empty( moveOpts ) ) {
moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
} else {
if ( ! moveOpts.hasOwnProperty('shift') || moveOpts.shift !== false ) {
moveOpts.shift = true
}
if ( ! moveOpts.hasOwnProperty('scale') || ! this._verifyScale( moveOpts.scale ) ) {
moveOpts.scale = _opts.scale
}
if ( ! moveOpts.hasOwnProperty('range') || parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] ) {
moveOpts.range = _opts.range
}
}
_tmpDate = new Date( _opts.endDatetime )
end_date = new Date( _tmpDate.getTime() + ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
newOpts.endDatetime = end_date.toString()
if ( moveOpts.shift ) {
_tmpDate = new Date( _opts.startDatetime )
begin_date = new Date( _tmpDate.getTime() + ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
newOpts.startDatetime = begin_date.toString()
}
this.reload( [newOpts] )
if ( callback ) {
this._debug( 'Fired your callback function after dateforthing.' )
callback( this._element, this._config, userdata )
}
}
/**
* Move the display position of the timeline container to the specified position
* @public
* @param {?string} position - The preset string of position on timeline you want to align. Allowed values are "left", "begin", "center", "right", "end", "latest", "current", "currently" or number of event id
* @param {?(number|string)} duration - The duration of alignment animation. Allowed values are "fast", "normal", "slow" or number of milliseconds
*/
alignment( ...args ) {
this._debug( 'alignment' )
let _opts = this._config,
_props = this._instanceProps,
_elem = this._element,
_tl_container = $(_elem).find( Selector.TIMELINE_CONTAINER ),
_movX = 0,
_args = ! this.is_empty( args ) ? args[0] : [],
position = _args.length > 0 && typeof _args[0] === 'string' ? _args[0] : _opts.rangeAlign,
duration = _args.length > 1 && /^(\d{1,}|fast|normal|slow)$/i.test( _args[1] ) ? _args[1] : 0
//console.log( args, _args, position, duration )
if ( _props.fullwidth <= _elem.scrollWidth ) {
return
}
switch ( true ) {
case /^(left|begin)$/i.test( position ):
_movX = 0
break
case /^center$/i.test( position ):
_movX = ( _tl_container[0].scrollWidth - _elem.scrollWidth ) / 2 + 1
break
case /^(right|end)$/i.test( position ):
_movX = _tl_container[0].scrollWidth - _elem.scrollWidth + 1
break
case /^latest$/i.test( position ): {
let events = this._mapPlacedEvents().sort( this.compareValues( 'x' ) ),
lastEvent = events[events.length - 1]
_movX = ! this.is_empty( lastEvent ) ? lastEvent.x : 0
// console.log( events, lastEvent, _movX, _elem.scrollWidth / 2 )
// Centering
if ( _elem.scrollWidth / 2 < _movX ) {
_movX -= Math.ceil( _elem.scrollWidth / 2 )
} else {
_movX = 0
}
// Focus target event
if ( ! this.is_empty( lastEvent ) ) {
$(`${Selector.TIMELINE_EVENT_NODE}[data-uid="${lastEvent.uid}"]`).trigger( Event.FOCUSIN_EVENT )
}
break
}
case /^\d{1,}$/.test( position ): {
let events = this._mapPlacedEvents(),
targetEvent = {}
if ( events.length > 0 ) {
targetEvent = events.find( ( evt ) => evt.eventId == parseInt( position, 10 ) )
}
_movX = ! this.is_empty( targetEvent ) ? targetEvent.x : 0
// Centering
if ( Math.ceil( _elem.scrollWidth / 2 ) < _movX ) {
_movX -= Math.ceil( _elem.scrollWidth / 2 )
} else {
_movX = 0
}
// Focus target event
if ( ! this.is_empty( targetEvent ) ) {
$(`${Selector.TIMELINE_EVENT_NODE}[data-uid="${targetEvent.uid}"]`).trigger( Event.FOCUSIN_EVENT )
}
break
}
case /^current(|ly)|now$/i.test( position ):
default: {
let _now = new Date().toString(),
_nowX = this.numRound( this._getCoordinateX( _now ), 2 )
if ( _nowX >= 0 ) {
if ( _tl_container[0].scrollWidth - _elem.scrollWidth + 1 < _nowX ) {
_movX = _tl_container[0].scrollWidth - _elem.scrollWidth + 1
} else {
_movX = _nowX
}
} else {
_movX = 0
}
break
}
}
//console.log( `!alignment::${position}:`, _props.fullwidth, _props.visibleWidth, _tl_container[0].scrollWidth, _tl_container[0].scrollLeft, _movX )
if ( duration === '0' ) {
_tl_container.scrollLeft( _movX )
} else {
_tl_container.animate({ scrollLeft: _movX }, duration )
}
}
/**
* @deprecated This method has been deprecated since version 2.0.0
*/
getOptions() {
throw new ReferenceError( 'This method named "getOptions" has been deprecated since version 2.0.0' )
}
/**
* Add new events to the rendered timeline object
* @public
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
addEvent( ...args ) {
this._debug( 'addEvent' )
let _args = args[0],
events = this.supplement( null, _args[0], this.validateArray ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
_cacheEvents = this._loadToCache(),
lastEventId = 0,
add_done = false
if ( this.is_empty( events ) || ! this._isCompleted ) {
return
}
if ( ! this.is_empty( _cacheEvents ) ) {
_cacheEvents.sort( this.compareValues( 'eventId' ) )
lastEventId = parseInt( _cacheEvents[_cacheEvents.length - 1].eventId, 10 )
}
//console.log( '!addEvent::before:', _cacheEvents, lastEventId, callback, userdata )
events.forEach( ( evt ) => {
let _one_event = this._registerEventData( '<div></div>', evt )
if ( ! this.is_empty( _one_event ) ) {
_one_event.eventId = Math.max( lastEventId + 1, parseInt( _one_event.eventId, 10 ) )
_cacheEvents.push( _one_event )
lastEventId = parseInt( _one_event.eventId, 10 )
add_done = true
}
})
//console.log( '!addEvent::after:', _cacheEvents, lastEventId, callback, userdata )
if ( ! add_done ) {
return
}
this._saveToCache( _cacheEvents )
this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after replacing events.' )
callback( this._element, this._config, userdata )
}
}
/**
* Remove events from the currently timeline object
* @public
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
removeEvent( ...args ) {
this._debug( 'removeEvent' )
let _args = args[0],
targets = this.supplement( null, _args[0], this.validateArray ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
_cacheEvents = this._loadToCache(),
condition = {},
remainEvents = [],
remove_done = false
if ( this.is_empty( targets ) || ! this._isCompleted || this.is_empty( _cacheEvents ) ) {
return
}
targets.forEach( ( cond ) => {
switch ( true ) {
case /^\d{1,}$/.test( cond ):
// By matching event ID
condition.type = 'eventId'
condition.value = parseInt( cond, 10 )
break
case /^(|\d{1,}(-|\/)\d{1,2}(-|\/)\d{1,2}(|\s\d{1,2}:\d{1,2}(|:\d{1,2})))(|,\d{1,}(-|\/)\d{1,2}(-|\/)\d{1,2}(|\s\d{1,2}:\d{1,2}(|:\d{1,2})))$/.test( cond ): {
// By matching range of datetime
let _tmp = cond.split(',')
condition.type = 'daterange'
condition.value = {}
condition.value['from'] = this.is_empty( _tmp[0] ) ? null : new Date( _tmp[0] )
condition.value['to'] = this.is_empty( _tmp[1] ) ? null : new Date( _tmp[1] )
break
}
default:
// By matching regex string
condition.type = 'regex'
condition.value = new RegExp( cond )
break
}
_cacheEvents.forEach( ( evt, _idx ) => {
let is_remove = false
switch ( condition.type ) {
case 'eventId': {
if ( parseInt( evt.eventId, 10 ) == condition.value ) {
is_remove = true
}
break
}
case 'daterange': {
let _fromX = condition.value.from ? Math.ceil( this._getCoordinateX( condition.value.from.toString() ) ) : 0,
_toX = condition.value.to ? Math.floor( this._getCoordinateX( condition.value.to.toString() ) ) : _fromX
if ( _fromX <= evt.x && evt.x <= _toX ) {
is_remove = true
}
break
}
case 'regex': {
if ( condition.value.test( JSON.stringify( evt ) ) ) {
is_remove = true
}
break
}
}
if ( ! is_remove ) {
remainEvents.push( evt )
}
})
})
remove_done = remainEvents.length !== _cacheEvents.length
if ( ! remove_done ) {
return
}
this._saveToCache( remainEvents )
this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after placing additional events.' )
callback( this._element, this._config, userdata )
}
}
/**
* Update events on the currently timeline object
* @public
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
updateEvent( ...args ) {
this._debug( 'updateEvent' )
let _args = args[0],
events = this.supplement( null, _args[0], this.validateArray ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
_cacheEvents = this._loadToCache(),
update_done = false
if ( this.is_empty( events ) || ! this._isCompleted || this.is_empty( _cacheEvents ) ) {
return
}
events.forEach( ( evt ) => {
let _upc_event = this._registerEventData( '<div></div>', evt ), // Update Candidate
_old_index = null,
_old_event = _cacheEvents.find( ( _evt, _idx ) => {
_old_index = _idx
return _evt.eventId == _upc_event.eventId
}),
_new_event = {}
if ( ! this.is_empty( _old_event ) && ! this.is_empty( _upc_event ) ) {
if ( _upc_event.hasOwnProperty( 'uid' ) ) {
delete _upc_event.uid
}
_new_event = Object.assign( _new_event, _old_event, _upc_event )
//console.log( _new_event, _old_event, _upc_event, _old_index )
_cacheEvents[_old_index] = _new_event
update_done = true
}
})
if ( ! update_done ) {
return
}
this._saveToCache( _cacheEvents )
this._placeEvent()
if ( callback ) {
this._debug( 'Fired your callback function after updating events.' )
callback( this._element, this._config, userdata )
}
}
/**
* Reload the timeline with overridable any options
* @public
* @param {?Function()} callback - Custom callback fired after calling this method
* @param {?Object} userdata - Data as object of referable in that callback
*/
reload( ...args ) {
this._debug( 'reload' )
let _args = args[0],
_upc_options = this.supplement( null, _args[0], this.validateObject ),
callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
userdata = _args.length > 2 ? _args.slice(2) : null,
_elem = this._element,
$default_evt = $(_elem).find( Selector.DEFAULT_EVENTS ),
_old_options = this._config,
_new_options = {}
if ( ! this.is_empty( _upc_options ) ) {
// _new_options = Object.assign( _new_options, _old_options, _upc_options )
_new_options = this.mergeDeep( _old_options, _upc_options )
this._config = _new_options
}
this._isInitialized = false
this._isCached = false
this._isCompleted = false
this._instanceProps = {}
$(_elem).empty().append( $default_evt )
this._calcVars()
if ( ! this._verifyMaxRenderableRange() ) {
throw new RangeError( `Timeline display period exceeds maximum renderable range.` )
}
if ( ! this._isInitialized ) {
this._renderView()
this._isInitialized = true
}
if ( this._config.reloadCacheKeep ) {
let _cacheEvents = this._loadToCache(),
_renewEvents = []
if ( ! this.is_empty( _cacheEvents ) ) {
_cacheEvents.forEach( ( evt ) => {
delete evt.uid
delete evt.x
delete evt.Y
delete evt.width
delete evt.height
delete evt.relation.x
delete evt.relation.y
_renewEvents.push( this._registerEventData( '<div></div>', evt ) )
})
}
this._isCached = this._saveToCache( _renewEvents )
} else {
this._loadEvent()
}
this._placeEvent()
this._isCompleted = true
if ( callback ) {
this._debug( 'Fired your callback function after reloading timeline.' )
callback( this._element, this._config, userdata )
}
}
/**
* The method that fires when an event on the timeline is clicked
* Note: You can hook the custom processing with the callback specified in the event parameter
* @public
* @param {Object} event -
*/
openEvent( event ) {
this._debug( 'openEvent' )
let _that = this,
_self = event.target,
$viewer = $(document).find( Selector.EVENT_VIEW ),
//eventId = parseInt( $(_self).attr( 'id' ).replace( 'evt-', '' ), 10 ),
uid = $(_self).data( 'uid' ),
//meta = this.supplement( null, $(_self).data( 'meta' ) ),
callback = this.supplement( null, $(_self).data( 'callback' ) )
//console.log( '!openEvent:', _self, $viewer, eventId, uid, meta, callback )
if ( $viewer.length > 0 ) {
$viewer.each(function() {
let _cacheEvents = _that._loadToCache(),
_eventData = _cacheEvents.find( ( event ) => event.uid === uid ),
_label = $('<div></div>', { class: ClassName.VIEWER_EVENT_TITLE }),
_content = $('<div></div>', { class: ClassName.VIEWER_EVENT_CONTENT }),
_meta = $('<div></div>', { class: ClassName.VIEWER_EVENT_META }),
_image = $('<div></div>', { class: ClassName.VIEWER_EVENT_IMAGE_WRAPPER })
//console.log( '!openEvent:', $(this), $(_self).html(), _eventData.label )
$(this).empty() // Initialize Viewer
if ( ! _that.is_empty( _eventData.image ) ) {
_image.append( `<img src="${_eventData.image}" class="${ClassName.VIEWER_EVENT_IMAGE}" />` )
$(this).append( _image )
}
if ( ! _that.is_empty( _eventData.label ) ) {
_label.html( _eventData.label )
$(this).append( _label )
}
if ( ! _that.is_empty( _eventData.content ) ) {
_content.html( _eventData.content )
$(this).append( _content )
}
if ( ! _that.is_empty( _eventData.rangeMeta ) ) {
_meta.html( _eventData.rangeMeta )
$(this).append( _meta )
}
})
}
if ( callback ) {
this._debug( `The callback "${callback}" was called by the "openEvent" method.` )
try {
Function.call( null, `return ${callback}` )()
} catch ( e ) {
throw new TypeError( e )
}
}
}
/**
* Be zoomed in scale of the timeline that fires when any scales on the ruler is double clicked
* @public
* @param {Object} event -
*/
zoomScale( event ) {
this._debug( 'zoomScale' )
let _elem = event.target,
ruler_item = $(_elem).data( 'ruler-item' ),
scaleMap = {
millennium : { years: 1000, lower: 'century', minGrids: 10 },
century : { years: 100, lower: 'decade', minGrids: 10 },
decade : { years: 10, lower: 'lustrum', minGrids: 2 },
lustrum : { years: 5, lower: 'year', minGrids: 5 },
year : { years: 1, lower: 'month', minGrids: 12 },
month : { lower: 'day', minGrids: 28 },
week : { lower: 'day', minGrids: 7 },
day : { lower: 'hour', minGrids: 24 },
weekday : { lower: 'hour', minGrids: 24 },
hour : { lower: 'minute', minGrids: 60 },
minute : { lower: 'second', minGrids: 60 },
second : { lower: null, minGrids: 60 }
},
getZoomScale = ( ruler_item ) => {
let [ scale, date_seed ] = ruler_item.split('-'),
min_grids = scaleMap[scale].minGrids,
begin_date, end_date, base_year, base_month, week_num, base_day, is_remapping, _tmpDate
switch ( true ) {
case /^millennium$/i.test( scale ):
case /^century$/i.test( scale ):
case /^decade$/i.test( scale ):
case /^lustrum$/i.test( scale ):
begin_date = `${( ( date_seed - 1 ) * scaleMap[scale].years ) + 1}/1/1`
_tmpDate = new Date( begin_date, 0, 1 ).setFullYear( date_seed * scaleMap[scale].years + 1 )
_tmpDate = new Date( _tmpDate - 1 )
end_date = `${_tmpDate.getFullYear()}/${_tmpDate.getMonth()+1}/${_tmpDate.getDate()} 23:59:59`
break
case /^year$/i.test( scale ):
begin_date = `${date_seed}/1/1`
_tmpDate = new Date( date_seed, 0, 1 ).setFullYear( parseInt( date_seed, 10 ) + 1 )
_tmpDate = new Date( _tmpDate - 1 )
end_date = `${_tmpDate.getFullYear()}/${_tmpDate.getMonth()+1}/${_tmpDate.getDate()} 23:59:59`
break
case /^month$/i.test( scale ):
[ base_year, base_month ] = date_seed.split('/')
is_remapping = parseInt( base_year, 10 ) < 100
begin_date = new Date( base_year, parseInt( base_month, 10 ) - 1, 1 )
if ( begin_date.getMonth() == 11 ) {
_tmpDate = new Date( begin_date.getFullYear() + 1, 0, 1 ).setFullYear( parseInt( base_year, 10 ) + 1 )
} else {
_tmpDate = new Date( begin_date.getFullYear(), begin_date.getMonth() + 1, 1 ).setFullYear( parseInt( base_year, 10 ) )
}
begin_date = begin_date.toString()
end_date = new Date( _tmpDate - 1 ).toString()
break
case /^week$/i.test( scale ):
[ base_year, week_num ] = date_seed.split(',')
is_remapping = parseInt( base_year, 10 ) < 100
_tmpDate = new Date( base_year, 0, 1 )
if ( is_remapping ) {
_tmpDate.setFullYear( base_year )
}
begin_date = new Date( _tmpDate.getTime() + ( week_num * 7 * 24 * 60 * 60 * 1000 ) ).toString()
end_date = new Date( new Date( begin_date ).getTime() + ( 7 * 24 * 60 * 60 * 1000 ) - 1 ).toString()
break
case /^day$/i.test( scale ):
case /^weekday$/i.test( scale ):
if ( 'weekday' === scale ) {
let _tmp = date_seed.split(',')
date_seed = _tmp[0]
}
[ base_year, base_month, base_day ] = date_seed.split('/')
is_remapping = parseInt( base_year, 10 ) < 100
_tmpDate = new Date( base_year, parseInt( base_month, 10 ) - 1, base_day )
begin_date = _tmpDate.toString()
end_date = new Date( _tmpDate.getTime() + ( 24 * 60 * 60 * 1000 ) - 1 ).toString()
//console.log( date_seed, base_year, week_num, begin_date, _tmpDate, new Date( _tmpDate ), new Date( _tmpDate - 1 ) )
break
case /^hour$/i.test( scale ):
case /^minute$/i.test( scale ):
begin_date = `${date_seed}:00`
end_date = `${date_seed}:59`
break
default:
begin_date = null
end_date = null
break
}
scale = scaleMap.hasOwnProperty( scale ) ? scaleMap[scale].lower : scale
return [ scale, begin_date, end_date, min_grids ]
},
[ to_scale, begin_date, end_date, min_grids ] = getZoomScale( ruler_item ),
zoom_options = {
startDatetime : begin_date,
endDatetime : end_date,
scale : to_scale,
}
if ( this.is_empty( zoom_options.scale ) ) {
return
}
if ( this._config.wrapScale ) {
let _wrap = Math.ceil( ( $(this._element).find(Selector.TIMELINE_CONTAINER).width() - $(this._element).find(Selector.TIMELINE_SIDEBAR).width() ) / min_grids ),
_originMinGridSize
if ( ! this._config.hasOwnProperty( 'originMinGridSize' ) ) {
// Keep an original minGridSize as cache
this._config.originMinGridSize = this._config.minGridSize
}
_originMinGridSize = this._config.originMinGridSize
zoom_options.minGridSize = Math.max( _wrap, _originMinGridSize )
}
// console.log( ruler_item, zoom_options, this._config.wrapScale, this._config.minGridSize )
this.reload( [zoom_options] )
}
/**
* Show the loader
* @public
*/
showLoader() {
this._debug( 'showLoader' )
let _elem = this._element,
_opts = this._config,
_container = $(_elem).find( Selector.TIMELINE_CONTAINER ),
width = _container.length > 0 ? _container.width() : $(_elem).width(),
height = ( _container.length > 0 ? _container.height() : $(_elem).height() ) || 120,
_loader = $('<div></div>', { id: 'jqtl-loader', style: `width:${width}px;height:${height}px;` })
//console.log( '!showLoader:', width, height, _container.length )
if ( _opts.loader === false ) {
return
}
if ( $(_opts.loader).length == 0 ) {
let _loading_text = LOADING_MESSAGE.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter( Boolean )
_loading_text.forEach( ( str, idx ) => {
let _fountain_text = $('<div></div>', { id: `jqtl-loading_${( idx + 1 )}`, class: ClassName.LOADER_ITEM }).text( str )
_loader.append( _fountain_text )
})
} else {
let _custom_loader = $(_opts.loader).clone().prop( 'hidden', false ).css( 'display', 'block' )
_loader.append( _custom_loader )
}
if ( $(_elem).find( Selector.LOADER ).length == 0 ) {
if ( _container.length > 0 ) {
_container.append( _loader )
} else {
$(_elem).css( 'position', 'relative' ).css( 'min-height', `${height}px` ).append( _loader )
}
}
}
/**
* Hide the loader
* @public
*/
hideLoader() {
this._debug( 'hideLoader' )
$(this._element).find( Selector.LOADER ).remove()
}
/* ----------------------------------------------------------------------------------------------------------------
* Utility Api
* ----------------------------------------------------------------------------------------------------------------
*/
/**
* Determine empty that like PHP
* @param {!(number|string|Object|number[]|boolean)} value - Variable you want to check
* @return {boolean}
*/
is_empty( value ) {
if ( value == null ) {
// typeof null -> object : for hack a bug of ECMAScript
// Refer: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof
return true
}
switch ( typeof value ) {
case 'object':
if ( Array.isArray( value ) ) {
// When object is array:
return ( value.length === 0 )
} else {
// When object is not array:
if ( Object.keys( value ).length > 0 || Object.getOwnPropertySymbols( value ).length > 0 ) {
return false
} else
if ( value.valueOf().length !== undefined ) {
return ( value.valueOf().length === 0 )
} else
if ( typeof value.valueOf() !== 'object' ) {
return this.is_empty( value.valueOf() )
} else {
return true
}
}
case 'string':
return ( value === '' )
case 'number':
return ( value == 0 )
case 'boolean':
return ! value
case 'undefined':
case 'null':
return true
case 'symbol': // Since ECMAScript6
case 'function':
default:
return false
}
}
/**
* Determine whether variable is an Object
* @param {!(number|string|Object|boolean)} item - Variable you want to check
* @return {boolean}
*/
is_Object( item ) {
return (item && typeof item === 'object' && ! Array.isArray( item ))
}
/**
* Merge two objects deeply as polyfill for instead "$.extend(true,target,source)"
* @param {!Object} target -
* @param {!Object} source -
* @return {Object}
*/
mergeDeep( target, source ) {
let output = Object.assign( {}, target )
if ( this.is_Object( target ) && this.is_Object( source ) ) {
for ( const key of Object.keys( source ) ) {
if ( this.is_Object( source[key] ) ) {
if ( ! ( key in target ) ) {
Object.assign( output, { [key]: source[key] } )
} else {
output[key] = this.mergeDeep( target[key], source[key] )
}
} else {
Object.assign( output, { [key]: source[key] } )
}
}
}
return output
}
/**
* Determine whether the object is iterable
* @param {!Object} obj -
* @return {boolean}
*/
is_iterable( obj ) {
return obj && typeof obj[Symbol.iterator] === 'function'
}
/**
* Add an @@iterator method to non-iterable object
* @param {!Object} obj -
* @return {Object}
*/
toIterableObject( obj ) {
if ( this.is_iterable( obj ) ) {
return obj
}
obj[Symbol.iterator] = () => {
let index = 0
return {
next() {
if ( obj.length <= index ) {
return { done: true }
} else {
return { value: obj[index++] }
}
}
}
}
return obj
}
/**
* Await until next process at specific millisec
* @param {number} [msec=1] - Millisecond
*/
sleep( msec = 1 ) {
return new Promise( ( resolve ) => {
setTimeout( resolve, msec )
})
}
/**
* Supplemental method for validating arguments in local scope
* @param {!(number|string|Object|boolean)} default_value -
* @param {?(number|string|Object|boolean)} opt_arg -
* @param {?(number|string|Object|boolean)} opt_callback - function or string of function to call
* @return {number|string|Object|boolean}
*/
supplement( default_value, opt_arg, opt_callback ) {
if ( opt_arg === undefined ) {
return default_value
}
if ( opt_callback === undefined ) {
return opt_arg
}
return opt_callback( default_value, opt_arg )
}
/**
* Generate the pluggable unique id
* @param {number} [digit=1000] -
* @return {string}
*/
generateUniqueID( digit = 1000 ) {
return new Date().getTime().toString(16) + Math.floor( digit * Math.random() ).toString(16)
}
/**
* Round a number with specific digit
* @param {!number} number -
* @param {?number} digit - Defaults to 0
* @param {string} [round_type="round"] -
* @return {number}
*/
numRound( number, digit, round_type = 'round' ) {
digit = this.supplement( 0, digit, this.validateNumeric )
let _pow = Math.pow( 10, digit )
switch ( true ) {
case /^ceil$/i.test( round_type ):
return Math.ceil( number * _pow ) / _pow
case /^floor$/i.test( round_type ):
return Math.floor( number * _pow ) / _pow
case /^round$/i.test( round_type ):
default:
return Math.round( number * _pow ) / _pow
}
}
/**
* Convert hex of color code to rgba
* @param {!string} hex -
* @param {number} [alpha=1] -
* @return {string}
*/
hexToRgbA( hex, alpha = 1 ) {
let _c
if ( /^#([A-Fa-f0-9]{3}){1,2}$/.test( hex ) ) {
_c = hex.substring(1).split('')
if ( _c.length == 3 ) {
_c= [ _c[0], _c[0], _c[1], _c[1], _c[2], _c[2] ]
}
_c = `0x${_c.join('')}`
return `rgba(${[ (_c >> 16) & 255, (_c >> 8) & 255, _c & 255 ].join(',')},${alpha})`
}
// throw new Error( 'Bad Hex' )
return hex
}
/**
* Get the correct datetime with remapping to that if the year is 0 - 99
* @param {!string} datetime_str -
* @return {?Object} - Date Object, or null if failed
*/
getCorrectDatetime( datetime_str ) {
let normalizeDate = ( dateString ) => {
// For Safari, IE
let _d = dateString.replace(/-/g, '/')
return /^\d{1,4}\/\d{1,2}$/.test( _d ) ? `${_d}/1` : _d
},
getDateObject = ( datetime_str ) => {
let _chk_str = normalizeDate( datetime_str ),
_ymd, _his, _parts, _date
switch ( true ) {
case /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))(| \d{1,2}(|:\d{1,2}(|:\d{1,2})))$/i.test( _chk_str ): {
[ _ymd, _his ] = _chk_str.split(' ')
_parts = _ymd.split('/')
if ( _parts[1] ) {
_parts[1] = parseInt( _parts[1], 10 ) - 1 // to month index
}
if ( _his ) {
_parts.push( ..._his.split(':') )
}
_date = new Date( new Date( ..._parts ).setFullYear( parseInt( _parts[0], 10 ) ) )
break
}
case /^\d+$/.test( _chk_str ):
_date = new Date( 1, 0, 1 ).setFullYear( parseInt( _chk_str, 10 ) )
break
default:
_date = new Date( _chk_str.toString() )
break
}
return _date
},
_checkDate = getDateObject( datetime_str )
if ( isNaN( _checkDate ) || this.is_empty( _checkDate ) ) {
console.warn( `"${datetime_str}" Cannot parse date because invalid format.` )
return null
}
/*
let _tempDate = new Date( normalizeDate( datetime_str ) ),
_chk_date = datetime_str.split( /-|\// )
if ( parseInt( _chk_date[0], 10 ) < 100 ) {
// Remapping if year is 0-99
_tempDate.setFullYear( parseInt( _chk_date[0], 10 ) )
}
return _tempDate
*/
if ( typeof _checkDate !== 'object' ) {
_checkDate = new Date( _checkDate )
}
//console.log( '!getCorrectDatetime::input:', datetime_str, '::output:', _checkDate, typeof _checkDate )
return _checkDate
}
/**
* Method to get week number as extension of Date object
* @param {!string} date_str -
* @return {number}
*/
getWeek( date_str ) {
let targetDate, _str, _onejan,
_millisecInDay = 24 * 60 * 60 * 1000
if ( /^\d{1,4}(|\/\d{1,2}(|\/\d{1,2}))$/.test( date_str ) ) {
_str = date_str.split('/')
if ( ! this.is_empty( _str[1] ) ) {
_str[1] = parseInt( _str[1], 10 ) - 1 // To month index
}
//console.log( '!getWeek:', _str )
targetDate = new Date( ..._str )
} else {
targetDate = new Date( date_str )
}
_onejan = new Date( targetDate.getFullYear(), 0, 1 )
return Math.ceil( ( ( ( targetDate - _onejan ) / _millisecInDay ) + _onejan.getDay() + 1 ) / 7 )
}
/**
* Retrieve one higher scale
* @param {!string} scale -
* @return {string} - String of higher scale
*/
getHigherScale( scale ) {
let higher_scale = scale
switch ( true ) {
case /^millisec(|ond)s?$/i.test( scale ):
higher_scale = 'second'
break
case /^seconds?$/i.test( scale ):
higher_scale = 'minute'
break
case /^minutes?$/i.test( scale ):
higher_scale = 'hour'
break
case /^quarter-?(|hour)$/i.test( scale ):
case /^half-?(|hour)$/i.test( scale ):
case /^hours?$/i.test( scale ):
higher_scale = 'day'
break
case /^days?$/i.test( scale ):
case /^weeks?$/i.test( scale ):
higher_scale = 'month'
break
case /^months?$/i.test( scale ):
higher_scale = 'year'
break
case /^years?$/i.test( scale ):
higher_scale = 'lustrum'
break
case /^lustrum$/i.test( scale ):
higher_scale = 'decade'
break
case /^dec(ade|ennium)$/i.test( scale ):
higher_scale = 'century'
break
case /^century$/i.test( scale ):
higher_scale = 'millennium'
break
case /^millenniums?|millennia$/i.test( scale ):
default:
break
}
return higher_scale
}
/**
* Retrieve the date string of specified locale
* @param {!string} date_seed -
* @param {string} [scale=""] -
* @param {string} [locales="en-US"] -
* @param {Object} [options={}] -
* @return {string} Locale string
*/
getLocaleString( date_seed, scale = '', locales = 'en-US', options = {} ) {
function toLocaleStringSupportsLocales() {
try {
new Date().toLocaleString( 'i' )
} catch ( e ) {
return e.name === "RangeError";
}
return false;
}
let is_toLocalString = toLocaleStringSupportsLocales(),
locale_string = '',
_options = {},
getOrdinal = ( n ) => {
let s = [ 'th', 'st', 'nd', 'rd' ], v = n % 100
return n + ( s[(v - 20)%10] || s[v] || s[0] )
},
getZerofill = ( num, digit = 4 ) => {
let strDuplicate = ( n, str ) => Array( n + 1 ).join( str ),
zero = strDuplicate( digit - num.length, '0' )
return String( num ).length == digit ? String( num ) : ( zero + num ).substr( num * -1 )
},
parseDatetime = ( date_str ) => {
let [ _ymd, _his ] = date_str.split(' '),
_parts = []
if ( /^\d{1,4}\/\d{1,2}\/\d{1,2}$/.test( _ymd ) ) {
_str = _ymd.split('/')
_parts.push( ..._str )
}
if ( /^\d{1,2}(|:\d{1,2}(|:\d{1,2}))$/.test( _his ) ) {
_str = _his.split(':')
_parts.push( ..._str )
}
if ( _parts.length > 0 ) {
return new Date( ..._parts )
} else {
return new Date( date_str )
}
},
_prop, _temp, _str, _num
for ( _prop in options ) {
if ( _prop === 'timeZone' || _prop === 'hour12' ) {
_options[_prop] = options[_prop]
}
}
//console.log( '!getLocaleString:', date_seed, scale, locales, options[scale], is_toLocalString )
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ):
case /^century$/i.test( scale ):
case /^dec(ade|ennium)$/i.test( scale ):
case /^lustrum$/i.test( scale ):
if ( options.hasOwnProperty( scale ) && options[scale] === 'ordinal' ) {
locale_string = getOrdinal( date_seed )
} else {
locale_string = date_seed
}
break
case /^years?$/i.test( scale ):
if ( is_toLocalString && options.hasOwnProperty( scale ) ) {
if ( [ 'numeric', '2-digit' ].includes( options[scale] ) ) {
_options.year = options[scale]
locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
} else
if ( 'zerofill' === options[scale] ) {
locale_string = getZerofill( date_seed )
}
}
locale_string = this.is_empty( locale_string ) ? this.getCorrectDatetime( date_seed ).getFullYear() : locale_string
break
case /^months?$/i.test( scale ):
if ( is_toLocalString && options.hasOwnProperty( scale ) ) {
if ( [ 'numeric', '2-digit', 'narrow', 'short', 'long' ].includes( options[scale] ) ) {
_options.month = options[scale]
locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
}
}
if ( this.is_empty( locale_string ) || isNaN( locale_string ) ) {
if ( /^\d{1,2}\/\d{1,2}(|\/\d{1,2})$/.test( date_seed ) ) {
_str = date_seed.split('/')
_temp = new Date( _str[0], parseInt( _str[1] - 1 ), 1 )
locale_string = _temp.toLocaleString( locales, _options )
}
}
break
case /^weeks?$/i.test( scale ):
[ _str, _num ] = date_seed.split(',')
//console.log( date_seed, _str, _num, new Date( _str ), parseDatetime( _str ), this.getCorrectDatetime( _str ) )
if ( options.hasOwnProperty( scale ) && options[scale] === 'ordinal' ) {
locale_string = getOrdinal( parseInt( _num, 10 ) )
} else {
locale_string = _num
}
break
case /^weekdays?$/i.test( scale ):
[ _str, _num ] = date_seed.split(',')
if ( /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))$/.test( _str ) ) {
_str = _str.split('/')
_temp = new Date( ..._str )
} else {
_temp = new Date( _str )
}
if ( is_toLocalString ) {
_options.weekday = options.hasOwnProperty('weekday') ? options.weekday : 'narrow'
locale_string = _temp.toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( _temp[0] ).toLocaleString( locales, _options )
} else {
let _weekday = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
locale_string = _weekday[parseInt( _num, 10 )]
}
break
case /^days?$/i.test( scale ):
if ( /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))$/.test( date_seed ) ) {
_str = date_seed.split('/')
_temp = new Date( ..._str )
} else {
_temp = new Date( date_seed )
}
if ( is_toLocalString ) {
_options.day = options.hasOwnProperty('day') ? options.day : 'numeric'
locales = options.hasOwnProperty('day') ? locales : 'en-US'
locale_string = _temp.toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
} else {
locale_string = _temp.getDate()
//locale_string = this.getCorrectDatetime( date_seed ).getDate()
}
break
case /^hours?$/i.test( scale ):
case /^(half|quarter)-?hours?$/i.test( scale ):
_temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
if ( is_toLocalString ) {
_options.hour = options.hasOwnProperty('hour') ? options.hour : 'numeric'
if ( options.hasOwnProperty('minute') ) {
_options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
} else {
locale_string = _temp.getHours()
//locale_string = this.getCorrectDatetime( date_seed ).getHours()
}
break
case /^minutes?$/i.test( scale ):
_temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
if ( is_toLocalString ) {
_options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
if ( options.hasOwnProperty('hour') ) {
_options.hour = options.hasOwnProperty('hour') ? options.hour : 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
} else {
locale_string = _temp.getMinutes()
//locale_string = this.getCorrectDatetime( date_seed ).getMinutes()
}
break
case /^seconds?$/i.test( scale ):
_temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
if ( is_toLocalString ) {
_options.second = options.hasOwnProperty('second') ? options.second : 'numeric'
if ( options.hasOwnProperty('hour') ) {
_options.hour = options.hasOwnProperty('hour') ? options.hour : 'numeric'
}
if ( options.hasOwnProperty('minute') ) {
_options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
}
locale_string = _temp.toLocaleString( locales, _options )
//locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
} else {
locale_string = _temp.getSeconds()
//locale_string = this.getCorrectDatetime( date_seed ).getSeconds()
}
break
case /^millisec(|ond)s?$/i.test( scale ):
default:
_temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
locale_string = _temp.toString()
//locale_string = this.getCorrectDatetime( date_seed )
break
}
//console.log( '!getLocaleString:', date_seed, scale, locales, options[scale], locale_string )
return locale_string
}
/**
* Get the rendering width of the given string
* @param {!string} str -
* @return {number}
*/
strWidth( str ) {
let _str_ruler = $( '<span id="jqtl-str-ruler"></span>' ),
_width = 0
if ( $('#jqtl-str-ruler').length == 0 ) {
$('body').append( _str_ruler )
}
_width = $('#jqtl-str-ruler').text( str ).get(0).offsetWidth
$('#jqtl-str-ruler').empty()
return _width
}
/**
* Sort an array by value of specific property (Note: destructive method)
* @example
* Object.sort( this.compareValues( property, order ) )
*
* @param {!string} property - To compare a property of object
* @param {string} [order="asc"] - Order to sort
* @return {number} Comparison index
*/
compareValues( property, order = 'asc' ) {
return ( a, b ) => {
if ( ! a.hasOwnProperty( property ) || ! b.hasOwnProperty( property ) ) {
return 0
}
const varA = typeof a[property] === 'string' ? a[property].toUpperCase() : a[property]
const varB = typeof b[property] === 'string' ? b[property].toUpperCase() : b[property]
let comparison = 0
if ( varA > varB ) {
comparison = 1
} else
if ( varA < varB ) {
comparison = -1
}
return order === 'desc' ? comparison * -1 : comparison
}
}
/**
* Validator for string
* @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
* @param {!(number|string|Object|boolean)} val - Value to validate
* @return {number|string|Object|boolean}
*/
validateString( def, val ) {
return typeof val === 'string' && val !== '' ? val : def
}
/**
* Validator for numeric
* @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
* @param {!(number|string|Object|boolean)} val - Value to validate
* @return {number|string|Object|boolean}
*/
validateNumeric( def, val ) {
return typeof val === 'number' ? Number( val ) : def
}
/**
* Validator for boolean
* @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
* @param {!(number|string|Object|boolean)} val - Value to validate
* @return {number|string|Object|boolean}
*/
validateBoolean( def, val ) {
return typeof val === 'boolean' || ( typeof val === 'object' && val !== null && typeof val.valueOf() === 'boolean' ) ? val : def
}
/**
* Validator for object
* @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
* @param {!(number|string|Object|boolean)} val - Value to validate
* @return {number|string|Object|boolean}
*/
validateObject( def, val ) {
return typeof val === 'object' ? val : def
}
/**
* Validator for array
* @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
* @param {!(number|string|Object|boolean)} val - Value to validate
* @return {number|string|Object|boolean}
*/
validateArray( def, val ) {
return Object.prototype.toString.call( val ) === '[object Array]' ? val : def
}
// Static
/**
* Interface for jQuery
* @interface
* @param {?(string|Object)} config - The object of plugin options or string of public method
* @param {?(...string|...Function())} args - Arguments for public method
*/
static _jQueryInterface( config, ...args ) {
return this.each(function () {
let data = $(this).data( DATA_KEY )
const _config = {
...Default,
...$(this).data(),
...typeof config === 'object' && config ? config : {}
}
if ( ! data ) {
// Apply the plugin and store the instance in data
data = new Timeline( this, _config )
$(this).data( DATA_KEY, data )
}
if ( typeof config === 'string' && config.charAt(0) != '_' ) {
if ( typeof data[config] === 'undefined' ) {
// Call no method
throw new ReferenceError( `No method named "${config}"` )
}
// Call public method
data[config]( args )
} else {
if ( ! data._isInitialized ) {
data._init()
}
}
})
}
} // class end
/* ----------------------------------------------------------------------------------------------------------------
* For jQuery
* ----------------------------------------------------------------------------------------------------------------
*/
$.fn[NAME] = Timeline._jQueryInterface
$.fn[NAME].Constructor = Timeline
$.fn[NAME].noConflict = () => {
$.fn[NAME] = JQUERY_NO_CONFLICT
return Timeline._jQueryInterface
}
/* ----------------------------------------------------------------------------------------------------------------
* For ESDoc
* ----------------------------------------------------------------------------------------------------------------
*/
export {
/*
NAME,
VERSION,
DATA_KEY,
EVENT_KEY,
PREFIX,
LOADING_MESSAGE,
MIN_POINTER_SIZE,
JQUERY_NO_CONFLICT,
*/
Default,
LimitScaleGrids,
EventParams,
/*
Event,
ClassName,
Selector,
*/
Timeline
}