src/timeline.esdoc.js
/*!
* Typedef for jQuery Timeline's ESDoc
* @version: 2.1.0
*/
/** @type {string} [NAME="Timeline"] */
/** @type {string} VERSION */
/** @type {string} DATA_KEY */
/** @type {string} EVENT_KEY */
/** @type {string} PREFIX */
/** @type {number} MIN_POINTER_SIZE */
/** @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". Then an extended value by this plugin is "zerofill".
* @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". Then an extended value by this plugin is "ordinal".
* @property {string} [hour] - The representation of the hour. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
* @property {string} [minute] - The representation of the minute. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
* @property {string} [second] - The representation of the second. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
* @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
*/
/**
* Various effect settings to the timeline object displayed in the DOM
*
* @typedef {Object} Effects
* @property {boolean} [presentTime=false] - Whether to show marking a present time on the timeline container.
* @property {boolean} [hoverEvent=true] - Whether to show the effect when individual events on the timeline container are mouse over.
* @property {boolean} [stripedGridRow=true] -
* @property {string} [horizontalGridStyle="solid"] - The style of horizontal grid line on the Timeline container. possible values are "solid", "dotted", "none".
* @property {string} [verticalGridStyle="solid"] - The style of vertical grid line on the Timeline container. possible values are "solid", "dotted", "none".
* @since 2.0.0
*/
/**
* Color scheme to overwrite defaults UI color of the event node
*
* @typedef {Object} EventColors
* @property {string} [text="#343A40"] - Defaults to text color of the event node
* @property {string} [border="#6C757D"] - Defaults to border color of the event node
* @property {string} [background="#E7E7E7"] - Defaults to background color of the event node
* @since 2.0.0
*/
/**
* Color scheme to overwrite defaults UI color of the timeline instance
*
* @typedef {Object} ThemeColors
* @property {string} [name="default"] -
* @property {string} [text="#343A40"] - Defaults to basic text color
* @property {string} [subtext="#707070"] -
* @property {string} [offtext="#BBBBBB"] -
* @property {string} [modesttext="#969696"] -
* @property {string} [line="#6C757D"] - Defaults to basic border color
* @property {string} [offline="#DDDDDD"] -
* @property {string} [activeline="#DC3545"] -
* @property {string} [background="#FFFFFF"] - Defaults to background color
* @property {string} [invertbg="#121212"] -
* @property {string} [striped1="#F7F7F7"] -
* @property {string} [striped2="#F0F0F0"] -
* @property {string} [active="#F73333"] -
* @property {string} [marker="#2C7CFF"] -
* @property {string} [gridbase="#333333"] -
* @since 2.1.0
*/
/**
* An option to overwrite defaults UI color of all event nodes
*
* @typedef {Object} ColorScheme
* @property {ThemeColors} [theme] - Color scheme to overwrite defaults UI color of the timeline instance
* @property {EventColors} [event] - Color scheme to overwrite defaults UI color of the event node
* @property {function} [hookEventColors] - You can declare a function to set colors with referring the data each event node
* @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" or "mixed"
* @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 {array.<Object>} [eventData] - You can declare the events with object format as default events you want to place
* @property {Effects} [effects] - You can declare effective styles as view of the timeline object
* @property {ColorScheme} [colorScheme] - Can overwrite defaults UI color of the event nodes
* @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} [category] -
* @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} TOUCHSTART_TIMELINE
* @property {string} TOUCHMOVE_TIMELINE
* @property {string} TOUCHEND_TIMELINE
* @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} PRESENT_TIME_MARKER
* @property {string} LOADER_CONTAINER
* @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_MAIN
* @property {string} TIMELINE_RULER_TOP
* @property {string} TIMELINE_EVENT_CONTAINER
* @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 = {}
/** @type {?Object} */
this._observer = null
}
// 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 )
}
/**
* Logger of errors when the method execution
* @private
* @param {!string} message -
* @param {string} [type='error'] -
*/
_error( message, type = 'error' ) {
if ( message && window.console ) {
type = window.console[type] ? type : 'error'
console[type]( message )
}
}
/**
* 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 {?(number|string|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 {?(number|string|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 {?(number|string|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 {?(number|string|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 {?(number|string|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 {?(number|string|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 {?(number|string|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 )
}
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
}
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 a first day of the week from week number (Note: added support for daylight savings time but needs improvement as performance has dropped)
* @param {!number} week_number -
* @param {!number} year - defaults to current year
* @return {object|boolean}
*/
getFirstDayOfWeek( week_number, year ) {
if ( this.is_empty( week_number ) ) {
return false
}
year = this.is_empty( year ) ? new Date().getFullYear() : parseInt( year, 10 )
let firstDayIndex = this._config.firstDayOfWeek,
firstDayOfYear = this.getCorrectDatetime( `${year}/1/1` ),
_weekday = firstDayOfYear.getDay(),
_keyDayOfWeek = firstDayOfYear,
_offset = _weekday > firstDayIndex ? _weekday - firstDayIndex : 0,
_weekNumber = _offset <= 0 ? 0 : 1,
hitDate
if ( _weekNumber == week_number && _weekday == firstDayIndex ) {
hitDate = firstDayOfYear
} else {
for ( let i = _offset; i < _offset + 7; i++ ) {
if ( i > _offset ) {
_keyDayOfWeek = this.modifyDate( firstDayOfYear, i, 'day' )
}
if ( _keyDayOfWeek.getDay() == firstDayIndex ) {
_weekNumber++
break
}
}
if ( _weekNumber == week_number ) {
hitDate = _keyDayOfWeek
} else {
hitDate = this.modifyDate( _keyDayOfWeek, (week_number - _weekNumber) * 7, 'day' )
}
}
return hitDate
}
/**
* Get the datetime shifted from the specified datetime by any fluctuation value
* @param {!object} datetime - to be date object filtered by getCorrectDatetime method
* @param {!number} fluctuation - an interval value to shift from given base datetime
* @param {!string} scale - the scale of an interval value
* @return {object|boolean}
*/
modifyDate( datetime, fluctuation, scale ) {
if ( this.is_empty( datetime ) || this.is_empty( fluctuation ) || this.is_empty( scale ) || ! this.verifyScale( scale ) ) {
return false
}
let baseDate = this.getCorrectDatetime( datetime ),
flct = this.validateNumeric( 0, fluctuation ),
dateElms = [
baseDate.getFullYear(), // 0: year
baseDate.getMonth(), // 1: month (index)
baseDate.getDate(), // 2: day
baseDate.getHours(), // 3: hour
baseDate.getMinutes(), // 4: minute
baseDate.getSeconds(), // 5: second
baseDate.getMilliseconds() // 6: millisec
],
tmpDate = new Date( new Date( ...dateElms ).setFullYear( dateElms[0] ) ),
isAdjust = false,
newDate
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ):
newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 1000) ) )
break
case /^century$/i.test( scale ):
newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 100) ) )
break
case /^dec(ade|ennium)$/i.test( scale ):
newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 10) ) )
break
case /^lustrum$/i.test( scale ):
newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 5) ) )
break
case /^years?$/i.test( scale ):
newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + flct ) )
break
case /^months?$/i.test( scale ):
newDate = new Date( tmpDate.setMonth( tmpDate.getMonth() + flct ) )
break
case /^weeks?$/i.test( scale ):
newDate = new Date( tmpDate.setDate( tmpDate.getDate() + (flct * 7) ) )
newDate.setHours( dateElms[3] )
newDate.setMinutes( dateElms[4] )
newDate.setSeconds( dateElms[5] )
newDate.setMilliseconds( dateElms[6] )
break
case /^(|week)days?$/i.test( scale ):
newDate = new Date( tmpDate.setDate( tmpDate.getDate() + flct ) )
newDate.setHours( dateElms[3] )
newDate.setMinutes( dateElms[4] )
newDate.setSeconds( dateElms[5] )
newDate.setMilliseconds( dateElms[6] )
break
case /^hours?$/i.test( scale ):
newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 60 * 60 * 1000 ) ) )
newDate.setMinutes( dateElms[4] )
newDate.setSeconds( dateElms[5] )
newDate.setMilliseconds( dateElms[6] )
break
case /^minutes?$/i.test( scale ):
newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 60 * 1000 ) ) )
newDate.setSeconds( dateElms[5] )
newDate.setMilliseconds( dateElms[6] )
break
case /^seconds?$/i.test( scale ):
newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 1000 ) ) )
newDate.setMilliseconds( dateElms[6] )
break
default:
newDate = new Date( tmpDate.setTime( tmpDate.getTime() + flct ) )
break
}
if ( isAdjust ) {
// Why different time of 1 min 15 sec on 12/01/1847, 0:00:00? (GMT+0001)
let divide = this.getCorrectDatetime( '1847/12/1 0:01:15' )
if ( baseDate.getTime() < divide.getTime() && newDate.getTime() >= divide.getTime() ) {
newDate = new Date( newDate.setTime( newDate.getTime() - (60 * 1000) ) )
} else
if ( baseDate.getTime() > divide.getTime() && newDate.getTime() <= divide.getTime() ) {
newDate = new Date( newDate.setTime( newDate.getTime() - (75 * 1000) ) )
}
}
return newDate
}
/**
* Acquire the difference between two dates with the specified scale value
* @param {!(number|object)} date1 - integer as milliseconds or object instanceof Date)
* @param {!(number|object)} date2 - integer as milliseconds or object instanceof Date)
* @param {string} [scale='millisecond'] - defaults to 'millisecond'
* @param {boolean} [absval=false] - defaults to false
* @return {object|boolean}
*/
diffDate( date1, date2, scale = 'millisecond', absval = false ) {
let _dt1 = date1 === undefined ? null : date1,
_dt2 = date2 === undefined ? 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 to get difference because undefined.' )
this._error( 'Cannot parse date to get difference because undefined.', 'warn' )
return false
}
diffMS = _dt2 - _dt1
if ( isNaN( diffMS ) ) {
//console.warn( 'Cannot parse date to get difference because invalid format.' )
this._error( 'Cannot parse date to get difference because invalid format.', 'warn' )
return false
}
if ( absval ) {
diffMS = Math.abs( diffMS )
}
let _bd = _dt1 instanceof Date ? _dt1 : new Date( _dt1 ),
_ed = _dt2 instanceof Date ? _dt2 : new Date( _dt2 ),
_dy = _ed.getFullYear() - _bd.getFullYear(),
_m = {}
switch ( true ) {
case /^millenniums?|millennia$/i.test( scale ): {
// return { "millennium-number": years,... }
let _by = _bd.getFullYear(),
_ey = _ed.getFullYear(),
_bm = Math.ceil( (_by == 0 ? 1 : _by) / 1000 ), // millennium of first ordinal
_em = Math.ceil( (_ey == 0 ? 1 : _ey) / 1000 ),
_cm = _bm
_m[_bm] = _em - _bm > 0 ? (_bm * 1000) - _by : _ey - _by
_cm++
while ( _cm <= _em ) {
_m[_cm] = _em - _cm > 0 ? 1000 : _ey - ((_cm - 1) * 1000)
_cm++
}
retval = _m
// return number of milliseconds
// retval = diffMS
break
}
case /^century$/i.test( scale ): {
// return { "century-number": years,... }
let _by = _bd.getFullYear(),
_ey = _ed.getFullYear(),
_bc = Math.ceil( (_by == 0 ? 1 : _by) / 100 ), // century of first ordinal
_ec = Math.ceil( (_ey == 0 ? 1 : _ey) / 100 ),
_cc = _bc
_m[_bc] = _ec - _bc > 0 ? (_bc * 100) - _by : _ey - _by
_cc++
while ( _cc <= _ec ) {
_m[_cc] = _ec - _cc > 0 ? 100 : _ey - ((_cc - 1) * 100)
_cc++
}
retval = _m
// return number of milliseconds
// retval = diffMS
break
}
case /^dec(ade|ennium)$/i.test( scale ): {
// return { "decade-number": days,... }
let _by = _bd.getFullYear(),
_ey = _ed.getFullYear(),
_cy = _by == 0 ? 1 : _by,
_cd, _days
while ( _cy <= _ey ) {
_days = isLeapYear( new Date( _cy, 0, 1 ) ) ? 366 : 365
_cd = Math.ceil( _cy / 10 ) // decade of first ordinal
if ( Object.hasOwnProperty.call( _m, _cd ) ) {
_m[_cd] += _days
} else {
_m[_cd] = _days
}
_cy++
}
retval = _m
// return number of milliseconds
// retval = diffMS
break
}
case /^lustrum$/i.test( scale ): {
// return { "lustrum-number": days,... }
let _by = _bd.getFullYear(),
_ey = _ed.getFullYear(),
_cy = _by == 0 ? 1 : _by,
_cl, _days
while ( _cy <= _ey ) {
_days = isLeapYear( new Date( _cy, 0, 1 ) ) ? 366 : 365
_cl = Math.ceil( _cy / 5 ) // lustrum of first ordinal
if ( Object.hasOwnProperty.call( _m, _cl ) ) {
_m[_cl] += _days
} else {
_m[_cl] = _days
}
_cy++
}
retval = _m
// return number of milliseconds
// retval = diffMS
break
}
case /^years?$/i.test( scale ):
// return { "year": days,... }
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 ):
// return { "year/month": days,... }
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 ): {
// return { "year,week": hours,... }
let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate() ),
_cw = this.getWeek( _cd ),
_nd = new Date( _cd ),
_pd = new Date( _cd ),
_newWeek = `${_cd.getFullYear()},${_cw}`
_nd.setDate( _nd.getDate() + 1 )
_pd.setDate( _pd.getDate() - 1 )
_m[_newWeek] = ( _cd - _pd ) / ( 60 * 60 * 1000 ) // hours of first day
while ( _nd.getTime() <= _ed.getTime() ) {
_nd.setDate( _nd.getDate() + 1 )
_cd.setDate( _cd.getDate() + 1 )
_cw = this.getWeek( _cd )
let _newWeekKey = `${_cd.getFullYear()},${_cw}`
if ( Object.hasOwnProperty.call( _m, _newWeekKey ) ) {
_m[_newWeekKey] += ( _nd - _cd ) / ( 60 * 60 * 1000 )
} else {
_m[_newWeekKey] = ( _nd - _cd ) / ( 60 * 60 * 1000 )
}
}
retval = _m
break
}
case /^(|week)days?$/i.test( scale ): {
// return { "year/month/day": hours,... }
let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate() ),
_nd = new Date( _cd ),
_pd = new Date( _cd )
_nd.setDate( _nd.getDate() + 1 )
_pd.setDate( _pd.getDate() - 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()}`] = ( _cd - _pd ) / ( 60 * 60 * 1000 )
while ( _nd.getTime() <= _ed.getTime() ) {
_nd.setDate( _nd.getDate() + 1 )
_cd.setDate( _cd.getDate() + 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()}`] = ( _nd - _cd ) / ( 60 * 60 * 1000 )
}
retval = _m
break
}
case /^hours?$/i.test( scale ): {
// return { "year/month/day hour": minutes,... }
let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours() ),
_nd = new Date( _cd ),
_pd = new Date( _cd )
_nd.setHours( _nd.getHours() + 1 )
_pd.setHours( _pd.getHours() - 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}`] = ( _cd - _pd ) / ( 60 * 1000 )
while ( _nd.getTime() <= _ed.getTime() ) {
_nd.setHours( _nd.getHours() + 1 )
_cd.setHours( _cd.getHours() + 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}`] = ( _nd - _cd ) / ( 60 * 1000 )
}
retval = _m
break
}
case /^minutes?$/i.test( scale ): {
// return { "year/month/day hour:minute": seconds,... }
let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours(), _bd.getMinutes() ),
_nd = new Date( _cd ),
_pd = new Date( _cd )
_nd.setMinutes( _nd.getMinutes() + 1 )
_pd.setMinutes( _pd.getMinutes() - 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}`] = ( _cd - _pd ) / 1000
while ( _nd.getTime() <= _ed.getTime() ) {
_nd.setMinutes( _nd.getMinutes() + 1 )
_cd.setMinutes( _cd.getMinutes() + 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}`] = ( _nd - _cd ) / 1000
}
retval = _m
break
}
case /^seconds?$/i.test( scale ): {
// return { "year/month/day hour:minute:second": milliseconds,... }
let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours(), _bd.getMinutes(), _bd.getSeconds() ),
_nd = new Date( _cd ),
_pd = new Date( _cd )
_nd.setSeconds( _nd.getSeconds() + 1 )
_pd.setSeconds( _pd.getSeconds() - 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}:${_cd.getSeconds()}`] = _cd - _pd
while ( _nd.getTime() <= _ed.getTime() ) {
_nd.setSeconds( _nd.getSeconds() + 1 )
_cd.setSeconds( _cd.getSeconds() + 1 )
_m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}:${_cd.getSeconds()}`] = _nd - _cd
}
retval = _m
break
}
default:
// return number of milliseconds
retval = diffMS
break
}
return retval
}
/**
* Verify whether is allowed scale in the plugin. Then retrieves that values of intervals on the scale if the scale is available and given arguments of date range. And return the base millisecond of scale if it is not the variable length scale (isVLS to false)
* @param {!string} scale -
* @param {number} [begin=null] - begin of range as unit millisecs that got by `Date.getTime()`
* @param {number} [end=null] - end of range as unit millisecs that got by `Date.getTime()`
* @param {boolean} [isVLS=false] - whether is variable length scale, defaults to false
* @return {object|boolean} boolean if no arguments are given after the first argument
*/
verifyScale( scale, begin = null, end = null, isVLS = false ) {
let _ms = -1,
isBool = this.is_empty( begin ) || this.is_empty( end ),
retval = isVLS ? this.diffDate( begin, end, scale ) : false
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 /^(|week)days?$/i.test( scale ):
// Day (is the variable length scale by DST) (:> 日 (サマータイムによる可変長スケール)
_ms = 24 * 60 * 60 * 1000
break
case /^weeks?$/i.test( scale ):
// Week (is the variable length scale by DST) (:> 週 (サマータイムによる可変長スケール)
_ms = 7 * 24 * 60 * 60 * 1000
break
case /^months?$/i.test( scale ):
// Month (is the variable length scale) (:> 月(可変長スケール)
_ms = 30.44 * 24 * 60 * 60 * 1000
break
case /^years?$/i.test( scale ):
// Year (is the variable length scale) (:> 年(可変長スケール)
_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}" scale.` )
this._error( `Specified an invalid "${scale}" scale.`, 'warn' )
_ms = -1
}
if ( isBool ) {
return _ms > 0
} else {
return isVLS ? retval : _ms
}
}
/**
* Retrieve one higher scale
* @param {!string} scale -
* @return {string} String of higher scale
*/
getHigherScale( scale ) {
return this.findScale( scale, 'higher' )
}
/**
* Retrieve one lower scale
* @param {!string} scale -
* @return {string} String of lower scale
*/
getLowerScale( scale ) {
return this.findScale( scale, 'lower' )
}
/**
* Find scale matched the specified condition
* @param {!string} base_scale -
* @param {!string} condition -
* @return {string|object} matched scale(s)
*/
findScale( base_scale, condition ) {
let scalePatternMap = [
[ 'millisecond', '^millisec(|ond)s?$' ],
[ 'second', '^seconds?$' ],
[ 'minute', '^minutes?$' ],
[ 'hour', '^(|half|quarter)-?(|hour)s?$' ],
[ 'day', '^(|week)days?$' ],
[ 'week', '^weeks?$' ],
[ 'month', '^months?$' ],
[ 'year', '^years?$' ],
[ 'lustrum', '^lustrum$' ],
[ 'decade', '^dec(ade|ennium)$' ],
[ 'century', '^century$' ],
[ 'millennium', '^millenniums?|millennia$' ],
],
_idx = scalePatternMap.findIndex( ( elm ) => new RegExp( `${elm[1]}`, 'i' ).test( base_scale ) ),
_narrows
switch ( true ) {
case /^higher$/i.test( condition ):
_idx = scalePatternMap[(_idx + 1)] ? _idx + 1 : _idx
return scalePatternMap[_idx][0]
case /^higher\s?all$/i.test( condition ):
_narrows = scalePatternMap.slice( _idx + 1 )
_narrows = _narrows.reduce( ( acc, cur ) => acc.concat( cur[0] ), [] )
if ( _narrows.includes( 'day' ) ) {
_narrows.push( 'weekday' )
}
return _narrows
case /^lower$/i.test( condition ):
_idx = scalePatternMap[(_idx - 1)] ? _idx - 1 : _idx
return scalePatternMap[_idx][0]
case /^lower\s?all$/i.test( condition ):
_narrows = scalePatternMap.slice( 0, _idx )
_narrows = _narrows.reduce( ( acc, cur ) => acc.concat( cur[0] ), [] )
if ( _narrows.includes( 'day' ) ) {
_narrows.push( 'weekday' )
}
return _narrows
default:
return scalePatternMap[_idx][0]
}
}
/**
* 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]
}
}
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(',')
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
}
return locale_string
}
/**
* Convert the date-time to custom formatting strings, as like ruby
* @param {!(number|object)} baseDate - should be a Date object
* @param {string} [format=''] -
* @param {string} [locales='en-US'] -
* @return {string}
*/
datetimeFormat( baseDate, format = '', locales = 'en-US' ) {
// let _baseDt = Object.prototype.toString.call( baseDate ) === '[object Date]' ? baseDate : this.getCorrectDatetime( baseDate ),
let _baseDt = baseDate instanceof Date ? baseDate : this.getCorrectDatetime( baseDate ),
_fmt = format.toString().split(''),
_ptn = 'YyZmBbdwWAaIHMSj'.split(''),
_cnvStr = '',
lastDayOfMonth = ( dateObj ) => {
let _tmp = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
_tmp.setTime( _tmp.getTime() - 1 )
return _tmp.getDate()
}
if ( this.is_empty( _fmt ) ) {
return _baseDt.toString()
}
_fmt.forEach( ( _str, _i, _orig ) => {
let _match = false,
_repStr = ''
if ( _ptn.includes( _str ) && ! this.is_empty( _orig[_i - 1] ) && _orig[_i - 1] === '%' ) {
_match = this.is_empty( _orig[_i - 2] ) || _orig[_i - 2] !== '\\'
}
if ( _match ) {
switch ( _str ) {
case 'Y':
case 'y':
case 'Z': {
// year
let _year = _baseDt.getFullYear()
if ( _str === 'Z' ) {
_repStr = _year < 10 ? `000${_year}` : _year < 100 ? `00${_year}` : _year < 1000 ? `0${_year}` : _year
} else {
_repStr = _str === 'Y' ? _year : _year.toString().slice(-2)
}
break
}
case 'm':
case 'B':
case 'b': {
// month
if ( _str === 'm' ) {
let _month = _baseDt.getMonth() + 1
_repStr = _month < 10 ? `0${_month}` : _month
} else {
let _opts = { month: _str === 'B' ? 'long' : 'short' }
_repStr = _baseDt.toLocaleDateString( locales, _opts )
}
break
}
case 'd': {
// day
let _day = _baseDt.getDate()
_repStr = _day < 10 ? `0${_day}` : _day
break
}
case 'w':
case 'A':
case 'a': {
// weekday
if ( _str === 'w' ) {
let _wday = _baseDt.getDay()
_repStr = _wday
} else {
let _opts = { weekday: _str === 'A' ? 'long' : 'short' }
_repStr = _baseDt.toLocaleDateString( locales, _opts )
}
break
}
case 'W': {
// week
_repStr = this.getWeek( _baseDt )
break
}
case 'I':
case 'H': {
// hour
let _opts = { hour12: _str === 'I', hour: 'numeric' }
_repStr = _baseDt.toLocaleTimeString( locales, _opts )
break
}
case 'M': {
// minute
_repStr = _baseDt.toLocaleTimeString( locales, { minute: 'numeric' } )
break
}
case 'S': {
// second
_repStr = _baseDt.toLocaleTimeString( locales, { second: 'numeric' } )
break
}
case 'j': {
// day of year
let _fdy = new Date( _baseDt.getFullYear(), 0, 1 ),
_month = _baseDt.getMonth(),
_days = 0, _m
for ( _m = 0; _m < _month; _m++ ) {
_fdy.setMonth( _m )
_days += lastDayOfMonth( _fdy )
}
_repStr = _days + _baseDt.getDate()
_repStr = _repStr < 10 ? `00${_repStr}` : _repStr < 100 ? `0${_repStr}` : _repStr
break
}
}
_cnvStr = _cnvStr.substring(0, _cnvStr.length - 1) + _repStr.toString()
} else {
_cnvStr += _str
}
}, _cnvStr )
_cnvStr = _cnvStr.toString().replace( /\\/g, '' )
return _cnvStr
}
/**
* 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
}
}
/**
* Getter argument as user data
* @since v2.1.0
* @param {!object} userdata -
* @return {object}
*/
getUserArg( userdata ) {
//console.log( '!_getUserArg:', userdata, typeof userdata, typeof userdata[0], this.is_Object( userdata[0] ) )
switch( typeof userdata[0] ) {
case 'string':
case 'number':
userdata = [ userdata[0] ]
break
case 'object':
if ( this.is_Object( userdata[0] ) ) {
// Object
if ( this.is_empty( userdata[0] ) ) {
userdata = {}
} else {
userdata = this.mergeDeep( {}, userdata[0] )
}
} else {
// Array
if ( this.is_empty( userdata[0] ) ) {
userdata = []
} else {
userdata = userdata[0]
}
}
break
default:
userdata = userdata[0]
break
}
return userdata
}
/**
* Apply custom theme styles
* @since v2.1.0
* @return {void}
*/
applyThemeStyle() {
let theme = this._config.colorScheme.theme,
selector = this._selector,
styleId = `${PREFIX}-theme-${selector.replace(/[.#_]/g, '-')}`,
styleTag = $('<style></style>', { id: styleId }),
_is = {},
_os = {},
cssText = ''
if ( $(`style#${styleId}`).length > 0 ) {
$(`style#${styleId}`).remove()
}
if ( 'default' === theme.name ) {
return
}
_is[Selector.TIMELINE_CONTAINER] = `border:solid 1px ${theme.offline}; background:${theme.background}`
_is[Selector.HEADLINE_TITLE] = `color:${theme.text}`
_is[Selector.RANGE_META] = `color:${theme.subtext}`
_is[Selector.TIMELINE_RULER_TOP] = `outline:solid 1px ${theme.offline}`
_is[Selector.TIMELINE_RULER_BOTTOM] = `outline:solid 1px ${theme.offline}`
_is[`${Selector.TIMELINE_RULER_LINES}:nth-child(even)`] = `background-color:${this.hexToRgbA(theme.striped1, 0.25)}`
_is[Selector.TIMELINE_RULER_ITEM] = `color:${theme.subtext}`
_is[`${Selector.TIMELINE_RULER_ITEM}:nth-child(even)`] = `background-color:${this.hexToRgbA(theme.striped2, 0.25)}`
_is[Selector.TIMELINE_EVENT_CONTAINER] = `outline:solid 1px ${theme.offline}`
_is[`${Selector.TIMELINE_EVENT_NODE}:not(.jqtl-event-type-pointer).active`] = `color:${theme.background};background-color:${theme.active}`
_is[`${Selector.TIMELINE_EVENT_NODE}:hover`] = `color:${theme.background};background-color:${theme.active}`
_is[`${Selector.TIMELINE_EVENT_NODE}:hover::after`] = `background-color:${this.hexToRgbA(theme.invertbg, 0.1)}`
_is[`${Selector.TIMELINE_EVENT_NODE}::before`] = `color:${theme.modesttext}`
_is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}`] = `border:solid 3px ${theme.line}`
_is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}.active`] = `border-color:${theme.activeline}`
_is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}:hover`] = `border-color:${theme.activeline}`
_is[Selector.TIMELINE_SIDEBAR] = `outline:solid 1px ${theme.offline}`
_is[`${Selector.TIMELINE_SIDEBAR}> [class^="jqtl-side-index-"]`] = `border-bottom:dotted 1px ${theme.offline};background-color:${theme.background};color:${theme.text}`
_is[`${Selector.TIMELINE_SIDEBAR} ${Selector.TIMELINE_SIDEBAR_ITEM}:nth-child(odd)`] = `background-color:${theme.striped1}`
_is[`${Selector.TIMELINE_SIDEBAR} ${Selector.TIMELINE_SIDEBAR_ITEM}:first-child`] = `border-top:solid 1px ${theme.offline}`
_is[Selector.TIMELINE_SIDEBAR_MARGIN] = `outline:solid 1px ${theme.offline}`
_is[`${Selector.TIMELINE_SIDEBAR_MARGIN}:first-child`] = `border-bottom:solid 1px ${theme.offline}`
_is[`${Selector.TIMELINE_SIDEBAR_MARGIN}:last-child`] = `border-top:solid 1px ${theme.offline}`
_is[Selector.OVERLAY] = `background-color:${this.hexToRgbA(theme.background, 0.65)} !important`
_is[`${Selector.OVERLAY}:nth-child(odd)`] = `background-color:${this.hexToRgbA(theme.striped1, 0.45)} !important`
_os[`${Selector.VIEWER_EVENT_TITLE},${Selector.VIEWER_EVENT_CONTENT}`] = `color:${theme.text}`
_os[`${Selector.VIEWER_EVENT_TITLE}> .event-content`] = `color:${theme.offtext}`
_os[Selector.VIEWER_EVENT_META] = `color:${theme.offtext}`
_is[Selector.PRESENT_TIME_MARKER] = `border-left:dotted 1px ${theme.marker}`
_is[`${Selector.PRESENT_TIME_MARKER}::before,${Selector.PRESENT_TIME_MARKER}::after`] = `background-color:${theme.marker}`
_is[`${Selector.LOADER_ITEM} span`] = `background:${this.hexToRgbA(theme.text, 0.15)}`
_os['@keyframes loader'] = `0%{background:${this.hexToRgbA(theme.text, 0.15)}}25%{background:${this.hexToRgbA(theme.text, 0.15)}}50%{background:${this.hexToRgbA(theme.text, 0.15)}}100%{background:${this.hexToRgbA(theme.text, 0.15)}}`
for ( let _prop of Object.keys( _is ) ) {
cssText += `${selector} ${_prop}{${_is[_prop]}}`
}
for ( let _prop of Object.keys( _os ) ) {
cssText += `${_prop}{${_os[_prop]}}`
}
$('head').append( styleTag.text( cssText ) )
}
/**
* 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
}